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,5 @@
const { defineConfig } = require('@coze-arch/stylelint-config');
module.exports = defineConfig({
extends: [],
});

View File

@@ -0,0 +1,63 @@
# @coze-common/assets
通用全局样式 & 图片啥的
## Overview
This package is part of the Coze Studio monorepo and provides utilities functionality. It serves as a core component in the Coze ecosystem.
## Getting Started
### Installation
Add this package to your `package.json`:
```json
{
"dependencies": {
"@coze-common/assets": "workspace:*"
}
}
```
Then run:
```bash
rush update
```
### Usage
```typescript
import { /* exported functions/components */ } from '@coze-common/assets';
// Example usage
// TODO: Add specific usage examples
```
## Features
- Core functionality for Coze Studio
- TypeScript support
- Modern ES modules
## API Reference
Please refer to the TypeScript definitions for detailed API documentation.
## Development
This package is built with:
- TypeScript
- Modern JavaScript
- ESLint for code quality
## Contributing
This package is part of the Coze Studio monorepo. Please follow the monorepo contribution guidelines.
## License
Apache-2.0

View File

@@ -0,0 +1,8 @@
{
"operationSettings": [
{
"operationName": "ts-check",
"outputFolderNames": ["./dist"]
}
]
}

View File

@@ -0,0 +1,6 @@
{
"codecov": {
"coverage": 0,
"incrementCoverage": 0
}
}

View File

@@ -0,0 +1,7 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'web',
rules: {},
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 B

View File

@@ -0,0 +1,23 @@
{
"name": "@coze-common/assets",
"version": "0.0.1",
"description": "通用全局样式 & 图片啥的",
"license": "Apache-2.0",
"author": "fanchen@bytedance.com",
"maintainers": [],
"scripts": {
"build": "exit 0",
"lint": "eslint ./ --cache"
},
"dependencies": {},
"devDependencies": {
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/stylelint-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
"react": "~18.2.0",
"react-dom": "~18.2.0"
}
}

View File

@@ -0,0 +1,13 @@
@common-box-shadow: 0px 2px 8px 0px rgba(31, 35, 41, 0.02),
0px 2px 4px 0px rgba(31, 35, 41, 0.02), 0px 2px 2px 0px rgba(31, 35, 41, 0.02);
.common-svg-icon(@size:14px, @color:#3370ff) {
> svg {
width: @size;
height: @size;
> path {
fill: @color;
}
}
}

View File

@@ -0,0 +1,98 @@
@layer base {
// 新增css色值仅业务使用
:root {
--coze-fg-image-bots: rgba(6, 7, 9, 80%);
--coze-fg-image-white: rgba(255, 255, 255);
--coze-fg-image-user: rgba(255, 255, 255, 92%);
--coze-fg-image-user-name: rgba(255, 255, 255);
--coze-fg-image-secondary: rgba(255, 255, 255, 60%);
--coze-bg-image-user: rgba(56, 57, 58, 70%);
--coze-bg-image-bots: rgba(249, 249, 249, 80%);
--coze-bg-image-question: rgba(235, 235, 235, 65%);
--coze-stroke-image-bots: rgba(6, 7, 9, 10%);
--coze-stroke-image-user: rgba(255, 255, 255, 12%);
--coze-stroke-image-hover: rgba(255, 255, 255, 28%);
}
}
@layer components {
// 新增语义化class仅业务使用
.coz-fg-images-bots {
color: var(--coze-fg-images-bots);
}
.coz-fg-images-white {
color: var(--coze-fg-image-white);
text-shadow: 0 0.5px 1px rgba(0, 0, 0, 25%);
}
.coz-bg-images-white {
background-color: var(--coze-fg-image-white);
}
.coz-fg-images-user {
color: var(--coze-fg-image-user);
}
.coz-fg-images-user-name {
color: var(--coze-fg-image-user-name);
text-shadow: 0 0.5px 1px rgba(0, 0, 0, 50%);
}
.coz-fg-images-secondary {
color: var(--coze-fg-image-secondary);
}
.coz-bg-images-secondary {
background-color: var(--coze-fg-image-secondary);
}
.coz-bg-image-user {
background-color: var(--coze-bg-image-user);
// 由于Chrome浏览器不兼容 父级使用mask 和 子级使用backdrop-filter表现为backdrop-filter不生效这里增加滤镜替代方案
// 规划后期气泡改为实色方案,删除此属性
filter: drop-shadow(0 0 0 var(--coze-bg-image-user));
backdrop-filter: drop-shadow(0 0 0 var(--coze-bg-image-user));
// stylelint-disable-next-line -- 必须兼容这种情况
-webkit-backdrop-filter: drop-shadow(0 0 0 var(--coze-bg-image-user));
}
.coz-bg-image-bots {
background-color: var(--coze-bg-image-bots);
filter: drop-shadow(0 0 0 var(--coze-bg-image-bots));
backdrop-filter: drop-shadow(0 0 0 var(--coze-bg-image-bots));
// stylelint-disable-next-line -- 必须兼容这种情况
-webkit-backdrop-filter: drop-shadow(0 0 0 var(--coze-bg-image-bots));
}
.coz-bg-image-question {
background-color: var(--coze-bg-image-question);
filter: drop-shadow(0 0 0 var(--coze-bg-image-question));
backdrop-filter: drop-shadow(0 0 0 var(--coze-bg-image-question));
// stylelint-disable-next-line -- 必须兼容这种情况
-webkit-backdrop-filter: drop-shadow(0 0 0 var(--coze-bg-image-question));
}
.coz-stroke-image-bots {
border: 1px solid var(--coze-stroke-image-bots)
}
.coz-stroke-image-user {
border: 1px solid var(--coze-stroke-image-user)
}
.coz-stroke-image-hover {
border: 1px solid var(--coze-stroke-image-hover);
}
.coz-fg-images-bots-2 {
@apply text-foreground-3;
}
.coz-fg-images-bots-3 {
color: rgba(var(--coze-fg-3)); // 可以继续复用 base token 里的色值;
}
}

View File

@@ -0,0 +1,2 @@
@import './tailwind.less';
@import './image-colors.less';

View File

@@ -0,0 +1,67 @@
.border-line(@radius: 8px, @color: #eceef0) {
&::after {
content: '';
position: absolute;
width: 200%;
height: 200%;
top: 0;
left: 0;
transform-origin: 0 0;
border-width: 1px;
border-style: solid;
transform: scale(0.5, 0.5);
box-sizing: border-box;
pointer-events: none;
color: @color;
border-color: @color;
border-radius: @radius;
}
}
.base-border-line(@color: #eceef0) {
content: '';
display: inline-block;
position: absolute;
background: @color;
}
.border-left-line(@color: #eceef0) {
&::after {
top: 0;
left: 0;
bottom: 0;
width: 1px;
transform: scaleX(0.5);
.base-border-line(@color);
}
}
.border-right-line(@color: #eceef0) {
&::after {
top: 0;
right: 0;
bottom: 0;
width: 1px;
transform: scaleX(0.5);
.base-border-line(@color);
}
}
.border-top-line(@color: #eceef0) {
&::after {
top: 0;
right: 0;
left: 0;
height: 1px;
transform: scaleY(0.5);
.base-border-line(@color);
}
}
.border-bottom-line(@color: #eceef0) {
&::after {
left: 0;
right: 0;
bottom: 0;
height: 1px;
transform: scaleY(0.5);
.base-border-line(@color);
}
}

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind utilities;
@tailwind components;

View File

@@ -0,0 +1,13 @@
@bg-gray-blue: rgba(
240,
245,
255,
0.81
); // highlight the area of the background
@text-gray-blue: #536eb1; // highlight the area of the text
@bg-white-smoke: #f5f5f5; // background of input/table/btn TODOrgba(46, 50, 56, 0.05)) #2e3238
@border-light-gray: rgba(28, 29, 35, 0.12);
@error-red: #f93920;
@text-title-black: #2e3238;
@text-highlight-blue: #3370ff;

View File

@@ -0,0 +1,4 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="64" height="64" rx="8" fill="#E8E8EA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.8282 24.3937C14.6856 20.6529 17.7136 17.9991 21.4554 17.1463C24.1276 16.5373 27.5923 16 31.5001 16C35.4078 16 38.8725 16.5373 41.5447 17.1463C45.2865 17.9991 48.3144 20.6529 49.1717 24.3936C49.6371 26.4242 50 28.9955 50 32.0772C50 35.5629 49.5357 38.3594 48.984 40.4522C48.1535 43.6022 45.645 45.8852 42.4839 46.6729C39.8563 47.3277 36.1635 47.9344 31.5001 47.9344C26.8366 47.9344 23.1438 47.3277 20.5162 46.6729C17.3551 45.8852 14.8464 43.6022 14.016 40.452C13.4643 38.3594 13 35.5628 13 32.0772C13 28.9956 13.3629 26.4242 13.8282 24.3937ZM38.0978 33.3965C38.0978 31.9368 39.281 30.7536 40.7407 30.7536C42.2003 30.7536 43.3835 31.9368 43.3835 33.3965V36.0393C43.3835 37.4989 42.2003 38.6821 40.7407 38.6821C39.281 38.6821 38.0978 37.4989 38.0978 36.0393V33.3965ZM22.2407 30.7537C20.7811 30.7537 19.5979 31.937 19.5979 33.3966V36.0394C19.5979 37.499 20.7811 38.6823 22.2407 38.6823C23.7003 38.6823 24.8836 37.499 24.8836 36.0394V33.3966C24.8836 31.937 23.7003 30.7537 22.2407 30.7537Z" fill="#CDCDCD"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22ZM9.48544 7H11.0225C11.2106 7 11.3793 7.12745 11.4465 7.32043L14.5702 16.2923C14.6838 16.6186 14.4641 16.9686 14.1456 16.9686H13.3052C13.1152 16.9686 12.9453 16.8389 12.8792 16.6435L12.3143 14.9739H8.18872L7.62275 16.6422C7.55654 16.8373 7.38669 16.9668 7.19682 16.9668H6.36382C6.04537 16.9668 5.82567 16.6169 5.93924 16.2906L9.06143 7.32043C9.12863 7.12745 9.48544 7 9.48544 7ZM8.8588 12.9804H11.6401L10.2689 8.94692H10.2262L8.8588 12.9804ZM16.2827 7C15.973 7 15.722 7.25106 15.722 7.56075V16.4393C15.722 16.7489 15.973 17 16.2827 17H17.0304C17.3401 17 17.5911 16.7489 17.5911 16.4393V7.56075C17.5911 7.25106 17.3401 7 17.0304 7H16.2827Z"
fill="#1D1C23" />
</svg>

After

Width:  |  Height:  |  Size: 932 B

View File

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22ZM9.48544 7H11.0225C11.2106 7 11.3793 7.12745 11.4465 7.32043L14.5702 16.2923C14.6838 16.6186 14.4641 16.9686 14.1456 16.9686H13.3052C13.1152 16.9686 12.9453 16.8389 12.8792 16.6435L12.3143 14.9739H8.18872L7.62275 16.6422C7.55654 16.8373 7.38669 16.9668 7.19682 16.9668H6.36382C6.04537 16.9668 5.82567 16.6169 5.93924 16.2906L9.06143 7.32043C9.12863 7.12745 9.48544 7 9.48544 7ZM8.8588 12.9804H11.6401L10.2689 8.94692H10.2262L8.8588 12.9804ZM16.2827 7C15.973 7 15.722 7.25106 15.722 7.56075V16.4393C15.722 16.7489 15.973 17 16.2827 17H17.0304C17.3401 17 17.5911 16.7489 17.5911 16.4393V7.56075C17.5911 7.25106 17.3401 7 17.0304 7H16.2827Z"
fill="#4D53E8" />
</svg>

After

Width:  |  Height:  |  Size: 932 B

View File

@@ -0,0 +1,18 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1163_68314)">
<mask id="mask0_1163_68314" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<path d="M16 0H0V16H16V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_1163_68314)">
<path d="M0.941895 4.15078C0.941895 2.37897 2.37824 0.942627 4.15005 0.942627C5.92187 0.942627 7.35821 2.37897 7.35821 4.15078V6.07568C7.35821 6.7844 6.78367 7.35894 6.07494 7.35894H4.15005C2.37824 7.35894 0.941895 5.9226 0.941895 4.15078Z" fill="#1D1C23"/>
<path d="M0.941895 11.8503C0.941895 10.0784 2.37824 8.64209 4.15005 8.64209H6.07494C6.78367 8.64209 7.35821 9.21663 7.35821 9.92535V11.8503C7.35821 13.6221 5.92187 15.0584 4.15005 15.0584C2.37824 15.0584 0.941895 13.6221 0.941895 11.8503Z" fill="#1D1C23"/>
<path d="M9.92462 8.64209C9.2159 8.64209 8.64136 9.21663 8.64136 9.92535V11.8503C8.64136 13.6221 10.0777 15.0584 11.8495 15.0584C13.6213 15.0584 15.0577 13.6221 15.0577 11.8503C15.0577 10.0784 13.6213 8.64209 11.8495 8.64209H9.92462Z" fill="#1D1C23"/>
<path d="M8.6416 4.15054C8.6416 2.37872 10.0779 0.942383 11.8498 0.942383C13.6216 0.942383 15.0579 2.37872 15.0579 4.15054C15.0579 5.92235 13.6216 7.3587 11.8498 7.3587H9.92486C9.21614 7.3587 8.6416 6.78416 8.6416 6.07543V4.15054Z" fill="#1D1C23"/>
</g>
</g>
<defs>
<clipPath id="clip0_1163_68314">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,18 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1163_68199)">
<mask id="mask0_1163_68199" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<path d="M16 0H0V16H16V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_1163_68199)">
<path d="M0.941895 4.15078C0.941895 2.37897 2.37824 0.942627 4.15005 0.942627C5.92187 0.942627 7.35821 2.37897 7.35821 4.15078V6.07568C7.35821 6.7844 6.78367 7.35894 6.07494 7.35894H4.15005C2.37824 7.35894 0.941895 5.9226 0.941895 4.15078Z" fill="#4D53E8"/>
<path d="M0.941895 11.8503C0.941895 10.0784 2.37824 8.64209 4.15005 8.64209H6.07494C6.78367 8.64209 7.35821 9.21663 7.35821 9.92535V11.8503C7.35821 13.6221 5.92187 15.0584 4.15005 15.0584C2.37824 15.0584 0.941895 13.6221 0.941895 11.8503Z" fill="#4D53E8"/>
<path d="M9.92462 8.64209C9.2159 8.64209 8.64136 9.21663 8.64136 9.92535V11.8503C8.64136 13.6221 10.0777 15.0584 11.8495 15.0584C13.6213 15.0584 15.0577 13.6221 15.0577 11.8503C15.0577 10.0784 13.6213 8.64209 11.8495 8.64209H9.92462Z" fill="#4D53E8"/>
<path d="M8.6416 4.15054C8.6416 2.37872 10.0779 0.942383 11.8498 0.942383C13.6216 0.942383 15.0579 2.37872 15.0579 4.15054C15.0579 5.92235 13.6216 7.3587 11.8498 7.3587H9.92486C9.21614 7.3587 8.6416 6.78416 8.6416 6.07543V4.15054Z" fill="#4D53E8"/>
</g>
</g>
<defs>
<clipPath id="clip0_1163_68199">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,33 @@
<svg width="68" height="78" viewBox="0 0 68 78" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_4434_146488)">
<rect x="7" y="5" width="54" height="64" rx="3" fill="#1C2333" shape-rendering="crispEdges" />
<rect x="6.75" y="4.75" width="54.5" height="64.5" rx="3.25" stroke="#99B6FF" stroke-opacity="0.12"
stroke-width="0.5" shape-rendering="crispEdges" />
<rect x="11" y="9" width="46" height="16" rx="1" fill="white" fill-opacity="0.1" />
<g clip-path="url(#clip0_4434_146488)">
<rect x="11" y="28" width="46" height="3" rx="1.5" fill="white" fill-opacity="0.08" />
<rect x="11" y="33" width="22" height="3" rx="1.5" fill="white" fill-opacity="0.08" />
<rect x="11" y="38" width="26" height="3" rx="1.5" fill="white" fill-opacity="0.08" />
<rect x="11" y="44" width="46" height="3" rx="1.5" fill="white" fill-opacity="0.08" />
<rect x="11" y="49" width="22" height="3" rx="1.5" fill="white" fill-opacity="0.08" />
<rect x="11" y="54" width="26" height="3" rx="1.5" fill="white" fill-opacity="0.08" />
</g>
</g>
<defs>
<filter id="filter0_d_4434_146488" x="0.5" y="0.5" width="67" height="77" filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha" />
<feOffset dy="2" />
<feGaussianBlur stdDeviation="3" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4434_146488" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_4434_146488" result="shape" />
</filter>
<clipPath id="clip0_4434_146488">
<rect width="46" height="29" fill="white" transform="translate(11 28)" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,33 @@
<svg width="68" height="78" viewBox="0 0 68 78" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_4434_161940)">
<rect x="7" y="5" width="54" height="64" rx="3" fill="#F9F9F9" shape-rendering="crispEdges" />
<rect x="6.75" y="4.75" width="54.5" height="64.5" rx="3.25" stroke="#060709" stroke-opacity="0.1"
stroke-width="0.5" shape-rendering="crispEdges" />
<rect x="11" y="9" width="46" height="16" rx="1" fill="#060709" fill-opacity="0.12" />
<g clip-path="url(#clip0_4434_161940)">
<rect x="11" y="28" width="46" height="3" rx="1.5" fill="#060709" fill-opacity="0.08" />
<rect x="11" y="33" width="22" height="3" rx="1.5" fill="#060709" fill-opacity="0.08" />
<rect x="11" y="38" width="26" height="3" rx="1.5" fill="#060709" fill-opacity="0.08" />
<rect x="11" y="44" width="46" height="3" rx="1.5" fill="#060709" fill-opacity="0.08" />
<rect x="11" y="49" width="22" height="3" rx="1.5" fill="#060709" fill-opacity="0.08" />
<rect x="11" y="54" width="26" height="3" rx="1.5" fill="#060709" fill-opacity="0.08" />
</g>
</g>
<defs>
<filter id="filter0_d_4434_161940" x="0.5" y="0.5" width="67" height="77" filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha" />
<feOffset dy="2" />
<feGaussianBlur stdDeviation="3" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4434_161940" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_4434_161940" result="shape" />
</filter>
<clipPath id="clip0_4434_161940">
<rect width="46" height="29" fill="white" transform="translate(11 28)" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,24 @@
{
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"compilerOptions": {
"jsx": "preserve",
"useUnknownInCatchVariables": false,
"types": [],
"rootDir": "./src",
"outDir": "./dist",
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
},
"include": ["./src"],
"references": [
{
"path": "../../../config/eslint-config/tsconfig.build.json"
},
{
"path": "../../../config/stylelint-config/tsconfig.build.json"
},
{
"path": "../../../config/ts-config/tsconfig.build.json"
}
],
"$schema": "https://json.schemastore.org/tsconfig"
}

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"composite": true
},
"references": [
{
"path": "./tsconfig.build.json"
},
{
"path": "./tsconfig.misc.json"
}
],
"exclude": ["**/*"]
}

View File

@@ -0,0 +1,18 @@
{
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"$schema": "https://json.schemastore.org/tsconfig",
"include": ["__tests__", "vitest.config.mts"],
"exclude": ["./dist"],
"references": [
{
"path": "./tsconfig.build.json"
}
],
"compilerOptions": {
"jsx": "preserve",
"rootDir": "./",
"outDir": "./dist",
"useUnknownInCatchVariables": false,
"types": []
}
}

View File

@@ -0,0 +1,69 @@
# @coze-common/auth-adapter
统一的权限控制逻辑
## Overview
This package is part of the Coze Studio monorepo and provides utilities functionality. It serves as a core component in the Coze ecosystem.
## Getting Started
### Installation
Add this package to your `package.json`:
```json
{
"dependencies": {
"@coze-common/auth-adapter": "workspace:*"
}
}
```
Then run:
```bash
rush update
```
### Usage
```typescript
import { /* exported functions/components */ } from '@coze-common/auth-adapter';
// Example usage
// TODO: Add specific usage examples
```
## Features
- Core functionality for Coze Studio
- TypeScript support
- Modern ES modules
## API Reference
### Exports
- `useInitSpaceRole`
- `useInitProjectRole`
For detailed API documentation, please refer to the TypeScript definitions.
## Development
This package is built with:
- TypeScript
- React
- Vitest for testing
- ESLint for code quality
## Contributing
This package is part of the Coze Studio monorepo. Please follow the monorepo contribution guidelines.
## License
Apache-2.0

View File

@@ -0,0 +1,93 @@
/*
* 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 { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook } from '@testing-library/react-hooks';
import { ProjectRoleType, useProjectAuthStore } from '@coze-common/auth';
import { useInitProjectRole } from '../../src/project/use-init-project-role';
// Mock the auth store
vi.mock('@coze-common/auth', () => ({
useProjectAuthStore: vi.fn(),
ProjectRoleType: {
Owner: 'owner',
},
}));
describe('useInitProjectRole', () => {
const mockIsReady = {
'project-1': true,
'project-2': true,
};
const mockSetIsReady = vi.fn();
const mockSetRoles = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
(useProjectAuthStore as any).mockImplementation((selector: any) =>
selector({
setIsReady: mockSetIsReady,
setRoles: mockSetRoles,
isReady: mockIsReady,
}),
);
});
it('should initialize project role and set ready state', () => {
const spaceId = 'space-1';
const projectId = 'project-1';
const { result } = renderHook(() => useInitProjectRole(spaceId, projectId));
console.log('result', result.current);
console.log('mockIsReady', mockIsReady);
// 验证是否调用了 setRoles 和 setIsReady
expect(mockSetRoles).toHaveBeenCalledWith(projectId, [
ProjectRoleType.Owner,
]);
expect(mockSetIsReady).toHaveBeenCalledWith(projectId, true);
// 验证返回值
expect(result.current).toBe(true);
});
it('should handle multiple project IDs correctly', () => {
const testSpaceId = 'space-1';
const projectId1 = 'project-1';
const projectId2 = 'project-2';
const { rerender } = renderHook(
({ spaceId, projectId }) => useInitProjectRole(spaceId, projectId),
{
initialProps: { spaceId: testSpaceId, projectId: projectId1 },
},
);
expect(mockSetRoles).toHaveBeenCalledWith(projectId1, [
ProjectRoleType.Owner,
]);
expect(mockSetIsReady).toHaveBeenCalledWith(projectId1, true);
// 重新渲染,使用新的 projectId
rerender({ spaceId: testSpaceId, projectId: projectId2 });
expect(mockSetRoles).toHaveBeenCalledWith(projectId2, [
ProjectRoleType.Owner,
]);
expect(mockSetIsReady).toHaveBeenCalledWith(projectId2, true);
});
});

View File

@@ -0,0 +1,77 @@
/*
* 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 { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook } from '@testing-library/react-hooks';
import { useSpaceAuthStore } from '@coze-common/auth';
import { SpaceRoleType } from '@coze-arch/idl/developer_api';
import { useInitSpaceRole } from '../../src/space/use-init-space-role';
// Mock the auth store
vi.mock('@coze-common/auth', () => ({
useSpaceAuthStore: vi.fn(),
}));
describe('useInitSpaceRole', () => {
const mockSetIsReady = vi.fn();
const mockSetRoles = vi.fn();
const mockIsReady = {
'space-1': true,
'space-2': true,
};
beforeEach(() => {
vi.clearAllMocks();
(useSpaceAuthStore as any).mockImplementation((selector: any) =>
selector({
setIsReady: mockSetIsReady,
setRoles: mockSetRoles,
isReady: mockIsReady,
}),
);
});
it('should initialize space role and set ready state', () => {
const spaceId = 'space-1';
const { result } = renderHook(() => useInitSpaceRole(spaceId));
// 验证是否调用了 setRoles 和 setIsReady
expect(mockSetRoles).toHaveBeenCalledWith(spaceId, [SpaceRoleType.Owner]);
expect(mockSetIsReady).toHaveBeenCalledWith(spaceId, true);
// 验证返回值
expect(result.current).toBe(true);
});
it('should handle multiple space IDs correctly', () => {
const spaceId1 = 'space-1';
const spaceId2 = 'space-2';
const { rerender } = renderHook(({ id }) => useInitSpaceRole(id), {
initialProps: { id: spaceId1 },
});
expect(mockSetRoles).toHaveBeenCalledWith(spaceId1, [SpaceRoleType.Owner]);
expect(mockSetIsReady).toHaveBeenCalledWith(spaceId1, true);
// 重新渲染,使用新的 spaceId
rerender({ id: spaceId2 });
expect(mockSetRoles).toHaveBeenCalledWith(spaceId2, [SpaceRoleType.Owner]);
expect(mockSetIsReady).toHaveBeenCalledWith(spaceId2, true);
});
});

View File

@@ -0,0 +1,12 @@
{
"operationSettings": [
{
"operationName": "test:cov",
"outputFolderNames": ["coverage"]
},
{
"operationName": "ts-check",
"outputFolderNames": ["./dist"]
}
]
}

View File

@@ -0,0 +1,6 @@
{
"codecov": {
"coverage": 0,
"incrementCoverage": 0
}
}

View File

@@ -0,0 +1,7 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'node',
rules: {},
});

View File

@@ -0,0 +1,37 @@
{
"name": "@coze-common/auth-adapter",
"version": "0.0.1",
"description": "统一的权限控制逻辑",
"license": "Apache-2.0",
"author": "sunzhiyuan.evan@bytedance.com",
"maintainers": [],
"main": "src/index.ts",
"scripts": {
"build": "exit 0",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"dependencies": {
"@coze-arch/idl": "workspace:*",
"@coze-common/auth": "workspace:*",
"react": "~18.2.0",
"zustand": "^4.4.7"
},
"devDependencies": {
"@coze-arch/bot-typings": "workspace:*",
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"@testing-library/react-hooks": "^8.0.1",
"@types/node": "^18",
"@types/react": "18.2.37",
"@vitest/coverage-v8": "~3.0.5",
"react-dom": "~18.2.0",
"sucrase": "^3.32.0",
"vitest": "~3.0.5"
}
}

View File

@@ -0,0 +1,19 @@
/*
* 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 { useInitSpaceRole } from './space/use-init-space-role';
export { useInitProjectRole } from './project/use-init-project-role';

View File

@@ -0,0 +1,37 @@
/*
* 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 { useEffect } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { useProjectAuthStore, ProjectRoleType } from '@coze-common/auth';
export function useInitProjectRole(spaceId: string, projectId: string) {
const { setIsReady, setRoles, isReady } = useProjectAuthStore(
useShallow(store => ({
isReady: store.isReady[projectId],
setIsReady: store.setIsReady,
setRoles: store.setRoles,
})),
);
useEffect(() => {
setRoles(projectId, [ProjectRoleType.Owner]);
setIsReady(projectId, true);
}, [projectId]);
return isReady; // 是否初始化完成。
}

View File

@@ -0,0 +1,42 @@
/*
* 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.
*/
/**
* @file 社区版暂时不提供权限控制功能,本文件中导出的方法用于未来拓展使用。
*/
import { useEffect } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { SpaceRoleType } from '@coze-arch/idl/developer_api';
import { useSpaceAuthStore } from '@coze-common/auth';
export function useInitSpaceRole(spaceId: string) {
const { setIsReady, setRoles, isReady } = useSpaceAuthStore(
useShallow(store => ({
setIsReady: store.setIsReady,
setRoles: store.setRoles,
isReady: store.isReady[spaceId],
})),
);
useEffect(() => {
setRoles(spaceId, [SpaceRoleType.Owner]);
setIsReady(spaceId, true);
}, [spaceId]);
return isReady;
}

View File

@@ -0,0 +1,19 @@
/*
* 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_DEV_MODE: boolean;
/// <reference types='@coze-arch/bot-typings' />

View File

@@ -0,0 +1,31 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@coze-arch/ts-config/tsconfig.node.json",
"compilerOptions": {
"types": [],
"rootDir": "./src",
"outDir": "./dist",
"tsBuildInfoFile": "./dist/tsconfig.build.tsbuildinfo"
},
"include": ["src"],
"references": [
{
"path": "../../arch/bot-typings/tsconfig.build.json"
},
{
"path": "../../arch/idl/tsconfig.build.json"
},
{
"path": "../auth/tsconfig.build.json"
},
{
"path": "../../../config/eslint-config/tsconfig.build.json"
},
{
"path": "../../../config/ts-config/tsconfig.build.json"
},
{
"path": "../../../config/vitest-config/tsconfig.build.json"
}
]
}

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"composite": true
},
"references": [
{
"path": "./tsconfig.build.json"
},
{
"path": "./tsconfig.misc.json"
}
],
"exclude": ["**/*"]
}

View File

@@ -0,0 +1,16 @@
{
"extends": "@coze-arch/ts-config/tsconfig.node.json",
"$schema": "https://json.schemastore.org/tsconfig",
"include": ["__tests__", "vitest.config.ts"],
"exclude": ["./dist"],
"references": [
{
"path": "./tsconfig.build.json"
}
],
"compilerOptions": {
"rootDir": "./",
"outDir": "./dist",
"types": ["vitest/globals"]
}
}

View File

@@ -0,0 +1,22 @@
/*
* 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 { defineConfig } from '@coze-arch/vitest-config';
export default defineConfig({
dirname: __dirname,
preset: 'web',
});

View File

@@ -0,0 +1,75 @@
# @coze-common/auth
统一的权限控制逻辑
## Overview
This package is part of the Coze Studio monorepo and provides utilities functionality. It includes store.
## Getting Started
### Installation
Add this package to your `package.json`:
```json
{
"dependencies": {
"@coze-common/auth": "workspace:*"
}
}
```
Then run:
```bash
rush update
```
### Usage
```typescript
import { /* exported functions/components */ } from '@coze-common/auth';
// Example usage
// TODO: Add specific usage examples
```
## Features
- Store
## API Reference
### Exports
- `useDestorySpace`
- `useSpaceAuth`
- `ESpacePermisson, SpaceRoleType`
- `useSpaceRole`
- `useSpaceAuthStore`
- `useProjectAuth`
- `useDestoryProject`
- `EProjectPermission, ProjectRoleType`
- `useProjectRole`
- `useProjectAuthStore`
For detailed API documentation, please refer to the TypeScript definitions.
## Development
This package is built with:
- TypeScript
- React
- Vitest for testing
- ESLint for code quality
## Contributing
This package is part of the Coze Studio monorepo. Please follow the monorepo contribution guidelines.
## License
Apache-2.0

View File

@@ -0,0 +1,393 @@
/*
* 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 { describe, it, expect } from 'vitest';
import { SpaceRoleType, SpaceType } from '@coze-arch/idl/developer_api';
import {
ProjectRoleType,
EProjectPermission,
} from '../../src/project/constants';
import { calcPermission } from '../../src/project/calc-permission';
describe('Project Calc Permission', () => {
describe('个人空间权限', () => {
it('应该为个人空间返回正确的权限', () => {
const params = {
projectRoles: [],
spaceRoles: [],
spaceType: SpaceType.Personal,
};
// 个人空间应该有查看权限
expect(calcPermission(EProjectPermission.View, params)).toBe(true);
// 个人空间应该有编辑信息权限
expect(calcPermission(EProjectPermission.EDIT_INFO, params)).toBe(true);
// 个人空间应该有删除权限
expect(calcPermission(EProjectPermission.DELETE, params)).toBe(true);
// 个人空间应该有发布权限
expect(calcPermission(EProjectPermission.PUBLISH, params)).toBe(true);
// 个人空间应该有创建资源权限
expect(calcPermission(EProjectPermission.CREATE_RESOURCE, params)).toBe(
true,
);
// 个人空间应该有复制资源权限
expect(calcPermission(EProjectPermission.COPY_RESOURCE, params)).toBe(
true,
);
// 个人空间应该有复制项目权限
expect(calcPermission(EProjectPermission.COPY, params)).toBe(true);
// 个人空间应该有测试运行插件权限
expect(calcPermission(EProjectPermission.TEST_RUN_PLUGIN, params)).toBe(
true,
);
// 个人空间应该有测试运行工作流权限
expect(calcPermission(EProjectPermission.TEST_RUN_WORKFLOW, params)).toBe(
true,
);
});
it('应该为个人空间返回正确的无效权限', () => {
const params = {
projectRoles: [],
spaceRoles: [],
spaceType: SpaceType.Personal,
};
// 个人空间不应该有添加协作者权限
expect(calcPermission(EProjectPermission.ADD_COLLABORATOR, params)).toBe(
false,
);
// 个人空间不应该有删除协作者权限
expect(
calcPermission(EProjectPermission.DELETE_COLLABORATOR, params),
).toBe(false);
});
});
describe('团队空间项目角色权限', () => {
it('应该为项目所有者角色返回正确的权限', () => {
const params = {
projectRoles: [ProjectRoleType.Owner],
spaceRoles: [],
spaceType: SpaceType.Team,
};
// 项目所有者应该有查看权限
expect(calcPermission(EProjectPermission.View, params)).toBe(true);
// 项目所有者应该有编辑信息权限
expect(calcPermission(EProjectPermission.EDIT_INFO, params)).toBe(true);
// 项目所有者应该有删除权限
expect(calcPermission(EProjectPermission.DELETE, params)).toBe(true);
// 项目所有者应该有发布权限
expect(calcPermission(EProjectPermission.PUBLISH, params)).toBe(true);
// 项目所有者应该有创建资源权限
expect(calcPermission(EProjectPermission.CREATE_RESOURCE, params)).toBe(
true,
);
// 项目所有者应该有复制资源权限
expect(calcPermission(EProjectPermission.COPY_RESOURCE, params)).toBe(
true,
);
// 项目所有者应该有复制项目权限
expect(calcPermission(EProjectPermission.COPY, params)).toBe(true);
// 项目所有者应该有测试运行插件权限
expect(calcPermission(EProjectPermission.TEST_RUN_PLUGIN, params)).toBe(
true,
);
// 项目所有者应该有测试运行工作流权限
expect(calcPermission(EProjectPermission.TEST_RUN_WORKFLOW, params)).toBe(
true,
);
// 项目所有者应该有添加协作者权限
expect(calcPermission(EProjectPermission.ADD_COLLABORATOR, params)).toBe(
true,
);
// 项目所有者应该有删除协作者权限
expect(
calcPermission(EProjectPermission.DELETE_COLLABORATOR, params),
).toBe(true);
});
it('应该为项目编辑者角色返回正确的权限', () => {
const params = {
projectRoles: [ProjectRoleType.Editor],
spaceRoles: [],
spaceType: SpaceType.Team,
};
// 项目编辑者应该有查看权限
expect(calcPermission(EProjectPermission.View, params)).toBe(true);
// 项目编辑者应该有编辑信息权限
expect(calcPermission(EProjectPermission.EDIT_INFO, params)).toBe(true);
// 项目编辑者应该有创建资源权限
expect(calcPermission(EProjectPermission.CREATE_RESOURCE, params)).toBe(
true,
);
// 项目编辑者应该有复制资源权限
expect(calcPermission(EProjectPermission.COPY_RESOURCE, params)).toBe(
true,
);
// 项目编辑者应该有复制项目权限
expect(calcPermission(EProjectPermission.COPY, params)).toBe(true);
// 项目编辑者应该有测试运行插件权限
expect(calcPermission(EProjectPermission.TEST_RUN_PLUGIN, params)).toBe(
true,
);
// 项目编辑者应该有测试运行工作流权限
expect(calcPermission(EProjectPermission.TEST_RUN_WORKFLOW, params)).toBe(
true,
);
// 项目编辑者应该有添加协作者权限
expect(calcPermission(EProjectPermission.ADD_COLLABORATOR, params)).toBe(
true,
);
// 项目编辑者不应该有删除权限
expect(calcPermission(EProjectPermission.DELETE, params)).toBe(false);
// 项目编辑者不应该有发布权限
expect(calcPermission(EProjectPermission.PUBLISH, params)).toBe(false);
// 项目编辑者不应该有删除协作者权限
expect(
calcPermission(EProjectPermission.DELETE_COLLABORATOR, params),
).toBe(false);
});
});
describe('团队空间角色权限', () => {
it('应该为空间成员角色返回正确的权限', () => {
const params = {
projectRoles: [],
spaceRoles: [SpaceRoleType.Member],
spaceType: SpaceType.Team,
};
// 空间成员应该有查看权限
expect(calcPermission(EProjectPermission.View, params)).toBe(true);
// 空间成员应该有复制项目权限
expect(calcPermission(EProjectPermission.COPY, params)).toBe(true);
// 空间成员应该有测试运行工作流权限
expect(calcPermission(EProjectPermission.TEST_RUN_WORKFLOW, params)).toBe(
true,
);
// 空间成员不应该有编辑信息权限
expect(calcPermission(EProjectPermission.EDIT_INFO, params)).toBe(false);
// 空间成员不应该有删除权限
expect(calcPermission(EProjectPermission.DELETE, params)).toBe(false);
// 空间成员不应该有发布权限
expect(calcPermission(EProjectPermission.PUBLISH, params)).toBe(false);
// 空间成员不应该有创建资源权限
expect(calcPermission(EProjectPermission.CREATE_RESOURCE, params)).toBe(
false,
);
// 空间成员不应该有复制资源权限
expect(calcPermission(EProjectPermission.COPY_RESOURCE, params)).toBe(
false,
);
// 空间成员不应该有测试运行插件权限
expect(calcPermission(EProjectPermission.TEST_RUN_PLUGIN, params)).toBe(
false,
);
// 空间成员不应该有添加协作者权限
expect(calcPermission(EProjectPermission.ADD_COLLABORATOR, params)).toBe(
false,
);
// 空间成员不应该有删除协作者权限
expect(
calcPermission(EProjectPermission.DELETE_COLLABORATOR, params),
).toBe(false);
});
it('应该为空间所有者角色返回正确的权限', () => {
const params = {
projectRoles: [],
spaceRoles: [SpaceRoleType.Owner],
spaceType: SpaceType.Team,
};
// 空间所有者应该有查看权限
expect(calcPermission(EProjectPermission.View, params)).toBe(true);
// 空间所有者应该有复制项目权限
expect(calcPermission(EProjectPermission.COPY, params)).toBe(true);
// 空间所有者应该有测试运行工作流权限
expect(calcPermission(EProjectPermission.TEST_RUN_WORKFLOW, params)).toBe(
true,
);
});
it('应该为空间管理员角色返回正确的权限', () => {
const params = {
projectRoles: [],
spaceRoles: [SpaceRoleType.Admin],
spaceType: SpaceType.Team,
};
// 空间管理员应该有查看权限
expect(calcPermission(EProjectPermission.View, params)).toBe(true);
// 空间管理员应该有复制项目权限
expect(calcPermission(EProjectPermission.COPY, params)).toBe(true);
// 空间管理员应该有测试运行工作流权限
expect(calcPermission(EProjectPermission.TEST_RUN_WORKFLOW, params)).toBe(
true,
);
});
it('应该为默认角色返回正确的权限', () => {
const params = {
projectRoles: [],
spaceRoles: [SpaceRoleType.Default],
spaceType: SpaceType.Team,
};
// 默认角色不应该有任何权限
expect(calcPermission(EProjectPermission.View, params)).toBe(false);
expect(calcPermission(EProjectPermission.EDIT_INFO, params)).toBe(false);
expect(calcPermission(EProjectPermission.DELETE, params)).toBe(false);
expect(calcPermission(EProjectPermission.PUBLISH, params)).toBe(false);
expect(calcPermission(EProjectPermission.CREATE_RESOURCE, params)).toBe(
false,
);
expect(calcPermission(EProjectPermission.COPY_RESOURCE, params)).toBe(
false,
);
expect(calcPermission(EProjectPermission.COPY, params)).toBe(false);
expect(calcPermission(EProjectPermission.TEST_RUN_PLUGIN, params)).toBe(
false,
);
expect(calcPermission(EProjectPermission.TEST_RUN_WORKFLOW, params)).toBe(
false,
);
expect(calcPermission(EProjectPermission.ADD_COLLABORATOR, params)).toBe(
false,
);
expect(
calcPermission(EProjectPermission.DELETE_COLLABORATOR, params),
).toBe(false);
});
});
describe('混合角色权限', () => {
it('应该在同时拥有项目角色和空间角色时返回正确的权限', () => {
const params = {
projectRoles: [ProjectRoleType.Editor],
spaceRoles: [SpaceRoleType.Member],
spaceType: SpaceType.Team,
};
// 应该有项目编辑者的所有权限
expect(calcPermission(EProjectPermission.View, params)).toBe(true);
expect(calcPermission(EProjectPermission.EDIT_INFO, params)).toBe(true);
expect(calcPermission(EProjectPermission.CREATE_RESOURCE, params)).toBe(
true,
);
expect(calcPermission(EProjectPermission.COPY_RESOURCE, params)).toBe(
true,
);
expect(calcPermission(EProjectPermission.COPY, params)).toBe(true);
expect(calcPermission(EProjectPermission.TEST_RUN_PLUGIN, params)).toBe(
true,
);
expect(calcPermission(EProjectPermission.TEST_RUN_WORKFLOW, params)).toBe(
true,
);
expect(calcPermission(EProjectPermission.ADD_COLLABORATOR, params)).toBe(
true,
);
// 不应该有项目编辑者没有的权限
expect(calcPermission(EProjectPermission.DELETE, params)).toBe(false);
expect(calcPermission(EProjectPermission.PUBLISH, params)).toBe(false);
expect(
calcPermission(EProjectPermission.DELETE_COLLABORATOR, params),
).toBe(false);
});
it('应该在没有有效角色时返回 false', () => {
const params = {
projectRoles: [],
spaceRoles: [],
spaceType: SpaceType.Team,
};
// 没有角色不应该有任何权限
expect(calcPermission(EProjectPermission.View, params)).toBe(false);
expect(calcPermission(EProjectPermission.EDIT_INFO, params)).toBe(false);
expect(calcPermission(EProjectPermission.DELETE, params)).toBe(false);
expect(calcPermission(EProjectPermission.PUBLISH, params)).toBe(false);
expect(calcPermission(EProjectPermission.CREATE_RESOURCE, params)).toBe(
false,
);
expect(calcPermission(EProjectPermission.COPY_RESOURCE, params)).toBe(
false,
);
expect(calcPermission(EProjectPermission.COPY, params)).toBe(false);
expect(calcPermission(EProjectPermission.TEST_RUN_PLUGIN, params)).toBe(
false,
);
expect(calcPermission(EProjectPermission.TEST_RUN_WORKFLOW, params)).toBe(
false,
);
expect(calcPermission(EProjectPermission.ADD_COLLABORATOR, params)).toBe(
false,
);
expect(
calcPermission(EProjectPermission.DELETE_COLLABORATOR, params),
).toBe(false);
});
});
});

View File

@@ -0,0 +1,90 @@
/*
* 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 { describe, it, expect } from 'vitest';
import {
ProjectRoleType,
EProjectPermission,
} from '../../src/project/constants';
describe('Project Constants', () => {
describe('ProjectRoleType', () => {
it('应该定义所有必要的角色类型', () => {
// 验证所有角色类型都已定义
expect(ProjectRoleType.Owner).toBeDefined();
expect(ProjectRoleType.Editor).toBeDefined();
// 验证角色类型的值
expect(ProjectRoleType.Owner).toBe('owner');
expect(ProjectRoleType.Editor).toBe('editor');
});
it('应该包含正确数量的角色类型', () => {
// 验证角色类型的数量
const roleTypeCount = Object.keys(ProjectRoleType).filter(key =>
isNaN(Number(key)),
).length;
expect(roleTypeCount).toBe(2); // Owner 和 Editor
});
});
describe('EProjectPermission', () => {
it('应该定义所有必要的权限点', () => {
// 验证所有权限点都已定义
expect(EProjectPermission.View).toBeDefined();
expect(EProjectPermission.EDIT_INFO).toBeDefined();
expect(EProjectPermission.DELETE).toBeDefined();
expect(EProjectPermission.PUBLISH).toBeDefined();
expect(EProjectPermission.CREATE_RESOURCE).toBeDefined();
expect(EProjectPermission.COPY_RESOURCE).toBeDefined();
expect(EProjectPermission.COPY).toBeDefined();
expect(EProjectPermission.TEST_RUN_PLUGIN).toBeDefined();
expect(EProjectPermission.TEST_RUN_WORKFLOW).toBeDefined();
expect(EProjectPermission.ADD_COLLABORATOR).toBeDefined();
expect(EProjectPermission.DELETE_COLLABORATOR).toBeDefined();
});
it('应该为每个权限点分配唯一的值', () => {
// 创建一个集合来存储所有权限点的值
const permissionValues = new Set();
// 获取所有权限点的值
Object.values(EProjectPermission)
.filter(value => typeof value === 'number')
.forEach(value => {
permissionValues.add(value);
});
// 验证权限点的数量与唯一值的数量相同
const numericKeys = Object.keys(EProjectPermission).filter(
key => !isNaN(Number(key)),
).length;
expect(permissionValues.size).toBe(numericKeys);
});
it('应该包含正确数量的权限点', () => {
// 验证权限点的数量
const permissionCount = Object.keys(EProjectPermission).filter(key =>
isNaN(Number(key)),
).length;
expect(permissionCount).toBe(12); // 11个权限点
});
});
});

View File

@@ -0,0 +1,97 @@
/*
* 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 { SpaceRoleType, SpaceType } from '@coze-arch/idl/developer_api';
import {
EProjectPermission,
ProjectRoleType,
} from '../../src/project/constants';
import { calcPermission } from '../../src/project/calc-permission';
describe('calcPermission', () => {
it('should return true for personal space with valid permission', () => {
const result = calcPermission(EProjectPermission.View, {
projectRoles: [],
spaceRoles: [],
spaceType: SpaceType.Personal,
});
expect(result).toBe(true);
});
it('should return false for personal space with invalid permission', () => {
const result = calcPermission(EProjectPermission.ADD_COLLABORATOR, {
projectRoles: [],
spaceRoles: [],
spaceType: SpaceType.Personal,
});
expect(result).toBe(false);
});
it('should return true for team space with project role permission', () => {
const result = calcPermission(EProjectPermission.DELETE, {
projectRoles: [ProjectRoleType.Owner],
spaceRoles: [],
spaceType: SpaceType.Team,
});
expect(result).toBe(true);
});
it('should return false for team space with invalid project role permission', () => {
const result = calcPermission(EProjectPermission.DELETE, {
projectRoles: [ProjectRoleType.Editor],
spaceRoles: [],
spaceType: SpaceType.Team,
});
expect(result).toBe(false);
});
it('should return true for team space with space role permission', () => {
const result = calcPermission(EProjectPermission.COPY, {
projectRoles: [],
spaceRoles: [SpaceRoleType.Member],
spaceType: SpaceType.Team,
});
expect(result).toBe(true);
});
it('should return false for team space with invalid space role permission', () => {
const result = calcPermission(EProjectPermission.DELETE, {
projectRoles: [],
spaceRoles: [SpaceRoleType.Member],
spaceType: SpaceType.Team,
});
expect(result).toBe(false);
});
it('should return true for team space with both project and space role permissions', () => {
const result = calcPermission(EProjectPermission.PUBLISH, {
projectRoles: [ProjectRoleType.Editor],
spaceRoles: [SpaceRoleType.Member],
spaceType: SpaceType.Team,
});
expect(result).toBe(false);
});
it('should return false for team space with no valid permissions', () => {
const result = calcPermission(EProjectPermission.DELETE, {
projectRoles: [ProjectRoleType.Editor],
spaceRoles: [SpaceRoleType.Default],
spaceType: SpaceType.Team,
});
expect(result).toBe(false);
});
});

View File

@@ -0,0 +1,190 @@
/*
* 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 { describe, it, expect, beforeEach } from 'vitest';
import { renderHook, act } from '@testing-library/react-hooks';
import { ProjectRoleType } from '../../src/project/constants';
vi.stubGlobal('IS_DEV_MODE', true);
describe('Project Auth Store', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetAllMocks();
});
describe('setRoles', () => {
it('应该正确设置项目角色', async () => {
const { useProjectAuthStore } = await vi.importActual(
'../../src/project/store',
);
const { result } = renderHook(() => useProjectAuthStore());
const projectId = 'test-project-1';
const roles = [ProjectRoleType.Owner];
await act(() => {
result.current.setRoles(projectId, roles);
});
expect(result.current.roles[projectId]).toEqual(roles);
});
it('应该能够更新已存在的项目角色', async () => {
const { useProjectAuthStore } = await vi.importActual(
'../../src/project/store',
);
const { result } = renderHook(() => useProjectAuthStore());
const projectId = 'test-project-1';
const initialRoles = [ProjectRoleType.Owner];
const updatedRoles = [ProjectRoleType.Editor];
await act(() => {
result.current.setRoles(projectId, initialRoles);
});
expect(result.current.roles[projectId]).toEqual(initialRoles);
await act(() => {
result.current.setRoles(projectId, updatedRoles);
});
expect(result.current.roles[projectId]).toEqual(updatedRoles);
});
it('应该能够同时管理多个项目的角色', async () => {
const { useProjectAuthStore } = await vi.importActual(
'../../src/project/store',
);
const { result } = renderHook(() => useProjectAuthStore());
const projectId1 = 'test-project-1';
const projectId2 = 'test-project-2';
const roles1 = [ProjectRoleType.Owner];
const roles2 = [ProjectRoleType.Editor];
await act(() => {
result.current.setRoles(projectId1, roles1);
result.current.setRoles(projectId2, roles2);
});
expect(result.current.roles[projectId1]).toEqual(roles1);
expect(result.current.roles[projectId2]).toEqual(roles2);
});
});
describe('setIsReady', () => {
it('应该正确设置项目准备状态', async () => {
const { useProjectAuthStore } = await vi.importActual(
'../../src/project/store',
);
const { result } = renderHook(() => useProjectAuthStore());
const projectId = 'test-project-1';
await act(() => {
result.current.setIsReady(projectId, true);
});
expect(result.current.isReady[projectId]).toBe(true);
});
it('应该能够更新已存在的项目准备状态', async () => {
const { useProjectAuthStore } = await vi.importActual(
'../../src/project/store',
);
const { result } = renderHook(() => useProjectAuthStore());
const projectId = 'test-project-1';
await act(() => {
result.current.setIsReady(projectId, true);
});
expect(result.current.isReady[projectId]).toBe(true);
await act(() => {
result.current.setIsReady(projectId, false);
});
expect(result.current.isReady[projectId]).toBe(false);
});
it('应该能够同时管理多个项目的准备状态', async () => {
const { useProjectAuthStore } = await vi.importActual(
'../../src/project/store',
);
const { result } = renderHook(() => useProjectAuthStore());
const projectId1 = 'test-project-1';
const projectId2 = 'test-project-2';
await act(() => {
result.current.setIsReady(projectId1, true);
result.current.setIsReady(projectId2, false);
});
expect(result.current.isReady[projectId1]).toBe(true);
expect(result.current.isReady[projectId2]).toBe(false);
});
});
describe('destory', () => {
it('应该正确清除项目数据', async () => {
const { useProjectAuthStore } = await vi.importActual(
'../../src/project/store',
);
const { result } = renderHook(() => useProjectAuthStore());
const projectId = 'test-project-1';
const roles = [ProjectRoleType.Owner];
// 设置初始数据
await act(() => {
result.current.setRoles(projectId, roles);
result.current.setIsReady(projectId, true);
});
// 验证数据已设置
expect(result.current.roles[projectId]).toEqual(roles);
expect(result.current.isReady[projectId]).toBe(true);
// 销毁数据
result.current.destory(projectId);
// 验证数据已清除
expect(result.current.roles[projectId]).toEqual([]);
expect(result.current.isReady[projectId]).toBe(false);
});
it('应该只清除指定项目的数据,不影响其他项目', async () => {
const { useProjectAuthStore } = await vi.importActual(
'../../src/project/store',
);
const { result } = renderHook(() => useProjectAuthStore());
const projectId1 = 'test-project-1';
const projectId2 = 'test-project-2';
const roles1 = [ProjectRoleType.Owner];
const roles2 = [ProjectRoleType.Editor];
// 设置初始数据
result.current.setRoles(projectId1, roles1);
result.current.setRoles(projectId2, roles2);
result.current.setIsReady(projectId1, true);
result.current.setIsReady(projectId2, true);
// 销毁项目1的数据
result.current.destory(projectId1);
// 验证项目1的数据已清除项目2的数据保持不变
expect(result.current.roles[projectId1]).toEqual([]);
expect(result.current.isReady[projectId1]).toBe(false);
expect(result.current.roles[projectId2]).toEqual(roles2);
expect(result.current.isReady[projectId2]).toBe(true);
});
});
});

View File

@@ -0,0 +1,135 @@
/*
* 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 { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook } from '@testing-library/react-hooks';
// 模拟 React 的 useEffect
const cleanupFns = new Map();
vi.mock('react', () => ({
useEffect: vi.fn((fn, deps) => {
// 执行 effect 函数并获取清理函数
const cleanup = fn();
// 存储清理函数,以便在 unmount 时调用
cleanupFns.set(fn, cleanup);
// 返回清理函数
return cleanup;
}),
}));
import { useDestoryProject } from '../../src/project/use-destory-project';
import { useProjectAuthStore } from '../../src/project/store';
// 模拟 useProjectAuthStore
vi.mock('../../src/project/store', () => {
const destorySpy = vi.fn();
return {
useProjectAuthStore: vi.fn(() => destorySpy),
};
});
// 创建一个包装函数,确保在 unmount 时调用清理函数
function renderHookWithCleanup(callback, options = {}) {
const result = renderHook(callback, options);
const originalUnmount = result.unmount;
result.unmount = () => {
// 调用所有清理函数
cleanupFns.forEach(cleanup => {
if (typeof cleanup === 'function') {
cleanup();
}
});
// 调用原始的 unmount
originalUnmount();
};
return result;
}
describe('useDestoryProject', () => {
beforeEach(() => {
vi.clearAllMocks();
cleanupFns.clear();
});
it('应该在组件卸载时调用 destory 方法', () => {
const projectId = 'test-project-id';
const destorySpy = vi.fn();
// 模拟 useProjectAuthStore 返回 destorySpy
(useProjectAuthStore as any).mockReturnValue(destorySpy);
// 渲染 hook
const { unmount } = renderHookWithCleanup(() =>
useDestoryProject(projectId),
);
// 验证初始状态下 destory 未被调用
expect(destorySpy).not.toHaveBeenCalled();
// 卸载组件
unmount();
// 验证 destory 被调用,且参数正确
expect(destorySpy).toHaveBeenCalledTimes(1);
expect(destorySpy).toHaveBeenCalledWith(projectId);
});
it('应该在组件卸载时清除正确的项目数据', () => {
const projectId1 = 'test-project-id-1';
const destorySpy = vi.fn();
// 模拟 useProjectAuthStore 返回 destorySpy
(useProjectAuthStore as any).mockReturnValue(destorySpy);
// 渲染 hook
const { unmount } = renderHookWithCleanup(() =>
useDestoryProject(projectId1),
);
// 卸载组件
unmount();
// 验证 destory 被调用,且参数为 projectId1
expect(destorySpy).toHaveBeenCalledTimes(1);
expect(destorySpy).toHaveBeenCalledWith(projectId1);
});
it('应该为不同的项目ID调用不同的清理函数', () => {
const projectId2 = 'test-project-id-2';
const destorySpy = vi.fn();
// 清除之前的所有模拟和清理函数
vi.clearAllMocks();
cleanupFns.clear();
// 模拟 useProjectAuthStore 返回 destorySpy
(useProjectAuthStore as any).mockReturnValue(destorySpy);
// 渲染 hook
const { unmount } = renderHookWithCleanup(() =>
useDestoryProject(projectId2),
);
// 卸载组件
unmount();
// 验证 destory 被调用,且参数为 projectId2
expect(destorySpy).toHaveBeenCalledTimes(1);
expect(destorySpy).toHaveBeenCalledWith(projectId2);
});
});

View File

@@ -0,0 +1,148 @@
/*
* 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 { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook } from '@testing-library/react-hooks';
import { SpaceType } from '@coze-arch/idl/developer_api';
import { useSpace } from '@coze-arch/foundation-sdk';
import { useSpaceRole } from '../../src/space/use-space-role';
import { SpaceRoleType } from '../../src/space/constants';
import { useProjectRole } from '../../src/project/use-project-role';
import { useProjectAuth } from '../../src/project/use-project-auth';
import {
EProjectPermission,
ProjectRoleType,
} from '../../src/project/constants';
import { calcPermission } from '../../src/project/calc-permission';
// 模拟依赖
vi.mock('@coze-arch/foundation-sdk', () => ({
useSpace: vi.fn(),
}));
vi.mock('../../src/space/use-space-role', () => ({
useSpaceRole: vi.fn(),
}));
vi.mock('../../src/project/use-project-role', () => ({
useProjectRole: vi.fn(),
}));
vi.mock('../../src/project/calc-permission', () => ({
calcPermission: vi.fn(),
}));
describe('useProjectAuth', () => {
const projectId = 'test-project-id';
const spaceId = 'test-space-id';
const permissionKey = EProjectPermission.View;
beforeEach(() => {
vi.clearAllMocks();
// 模拟 useSpace 返回空间信息
(useSpace as any).mockReturnValue({
space_type: SpaceType.Team,
});
// 模拟 useSpaceRole 返回空间角色
(useSpaceRole as any).mockReturnValue([SpaceRoleType.Member]);
// 模拟 useProjectRole 返回项目角色
(useProjectRole as any).mockReturnValue([ProjectRoleType.Editor]);
// 模拟 calcPermission 返回权限结果
(calcPermission as any).mockReturnValue(true);
});
it('应该调用 calcPermission 并返回正确的权限结果', () => {
// 渲染 hook
const { result } = renderHook(() =>
useProjectAuth(permissionKey, projectId, spaceId),
);
// 验证 useSpace 被调用
expect(useSpace).toHaveBeenCalledWith(spaceId);
// 验证 useSpaceRole 被调用
expect(useSpaceRole).toHaveBeenCalledWith(spaceId);
// 验证 useProjectRole 被调用
expect(useProjectRole).toHaveBeenCalledWith(projectId);
// 验证 calcPermission 被调用,且参数正确
expect(calcPermission).toHaveBeenCalledWith(permissionKey, {
projectRoles: [ProjectRoleType.Editor],
spaceRoles: [SpaceRoleType.Member],
spaceType: SpaceType.Team,
});
// 验证返回值
expect(result.current).toBe(true);
});
it('应该在 calcPermission 返回 false 时返回 false', () => {
// 模拟 calcPermission 返回 false
(calcPermission as any).mockReturnValue(false);
// 渲染 hook
const { result } = renderHook(() =>
useProjectAuth(permissionKey, projectId, spaceId),
);
// 验证返回值
expect(result.current).toBe(false);
});
it('应该在空间类型不存在时抛出错误', () => {
// 模拟 useSpace 返回没有 space_type 的对象
(useSpace as any).mockReturnValue({});
// 使用 vi.spyOn 监听 console.error 以防止测试输出错误信息
vi.spyOn(console, 'error').mockImplementation(() => {
// 空实现,防止错误输出
});
// 验证抛出错误
expect(() => {
const { result } = renderHook(() =>
useProjectAuth(permissionKey, projectId, spaceId),
);
// 强制访问 result.current 触发错误
console.log(result.current);
}).toThrow('useSpaceAuth must be used after space list has been pulled.');
});
it('应该在空间为 null 时抛出错误', () => {
// 模拟 useSpace 返回 null
(useSpace as any).mockReturnValue(null);
// 使用 vi.spyOn 监听 console.error 以防止测试输出错误信息
vi.spyOn(console, 'error').mockImplementation(() => {
// 空实现,防止错误输出
});
// 验证抛出错误
expect(() => {
const { result } = renderHook(() =>
useProjectAuth(permissionKey, projectId, spaceId),
);
// 强制访问 result.current 触发错误
console.log(result.current);
}).toThrow('useSpaceAuth must be used after space list has been pulled.');
});
});

View File

@@ -0,0 +1,106 @@
/*
* 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 { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook } from '@testing-library/react-hooks';
import { useProjectRole } from '../../src/project/use-project-role';
import { useProjectAuthStore } from '../../src/project/store';
import { ProjectRoleType } from '../../src/project/constants';
// 模拟依赖
vi.mock('../../src/project/store', () => ({
useProjectAuthStore: vi.fn(),
}));
describe('useProjectRole', () => {
const projectId = 'test-project-id';
beforeEach(() => {
vi.clearAllMocks();
});
it('应该返回正确的项目角色', () => {
const expectedRoles = [ProjectRoleType.Owner];
// 模拟 useProjectAuthStore 返回项目角色和 ready 状态
(useProjectAuthStore as any).mockReturnValue({
isReady: true,
role: expectedRoles,
});
// 渲染 hook
const { result } = renderHook(() => useProjectRole(projectId));
// 验证 useProjectAuthStore 被调用
expect(useProjectAuthStore).toHaveBeenCalled();
// 验证返回值
expect(result.current).toEqual(expectedRoles);
});
it('应该在项目未准备好时抛出错误', () => {
// 模拟 useProjectAuthStore 返回未准备好的状态
(useProjectAuthStore as any).mockReturnValue({
isReady: false,
role: [],
});
// 使用 vi.spyOn 监听 console.error 以防止测试输出错误信息
vi.spyOn(console, 'error').mockImplementation(() => {
// 空实现,防止错误输出
});
// 验证抛出错误
expect(() => {
const { result } = renderHook(() => useProjectRole(projectId));
// 强制访问 result.current 触发错误
console.log(result.current);
}).toThrow(
'useProjectAuth must be used after useInitProjectRole has been completed.',
);
});
it('应该在角色为 undefined 时返回空数组', () => {
// 模拟 useProjectAuthStore 返回 undefined 角色
(useProjectAuthStore as any).mockReturnValue({
isReady: true,
role: undefined,
});
// 渲染 hook
const { result } = renderHook(() => useProjectRole(projectId));
// 验证返回值为空数组
expect(result.current).toEqual([]);
});
it('应该处理多种角色类型', () => {
const expectedRoles = [ProjectRoleType.Owner, ProjectRoleType.Editor];
// 模拟 useProjectAuthStore 返回多种角色
(useProjectAuthStore as any).mockReturnValue({
isReady: true,
role: expectedRoles,
});
// 渲染 hook
const { result } = renderHook(() => useProjectRole(projectId));
// 验证返回值
expect(result.current).toEqual(expectedRoles);
});
});

View File

@@ -0,0 +1,238 @@
/*
* 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 { describe, it, expect } from 'vitest';
import { SpaceRoleType } from '@coze-arch/idl/developer_api';
import { ESpacePermisson } from '../../src/space/constants';
import { calcPermission } from '../../src/space/calc-permission';
describe('Space Calc Permission', () => {
describe('calcPermission', () => {
it('应该为 Owner 角色返回正确的权限', () => {
// Owner 应该有更新空间的权限
expect(
calcPermission(ESpacePermisson.UpdateSpace, [SpaceRoleType.Owner]),
).toBe(true);
// Owner 应该有删除空间的权限
expect(
calcPermission(ESpacePermisson.DeleteSpace, [SpaceRoleType.Owner]),
).toBe(true);
// Owner 应该有添加成员的权限
expect(
calcPermission(ESpacePermisson.AddBotSpaceMember, [
SpaceRoleType.Owner,
]),
).toBe(true);
// Owner 应该有移除成员的权限
expect(
calcPermission(ESpacePermisson.RemoveSpaceMember, [
SpaceRoleType.Owner,
]),
).toBe(true);
// Owner 应该有转移所有权的权限
expect(
calcPermission(ESpacePermisson.TransferSpace, [SpaceRoleType.Owner]),
).toBe(true);
// Owner 应该有更新成员的权限
expect(
calcPermission(ESpacePermisson.UpdateSpaceMember, [
SpaceRoleType.Owner,
]),
).toBe(true);
// Owner 应该有管理 API 的权限
expect(calcPermission(ESpacePermisson.API, [SpaceRoleType.Owner])).toBe(
true,
);
});
it('应该为 Admin 角色返回正确的权限', () => {
// Admin 应该有添加成员的权限
expect(
calcPermission(ESpacePermisson.AddBotSpaceMember, [
SpaceRoleType.Admin,
]),
).toBe(true);
// Admin 应该有移除成员的权限
expect(
calcPermission(ESpacePermisson.RemoveSpaceMember, [
SpaceRoleType.Admin,
]),
).toBe(true);
// Admin 应该有退出空间的权限
expect(
calcPermission(ESpacePermisson.ExitSpace, [SpaceRoleType.Admin]),
).toBe(true);
// Admin 应该有更新成员的权限
expect(
calcPermission(ESpacePermisson.UpdateSpaceMember, [
SpaceRoleType.Admin,
]),
).toBe(true);
// Admin 不应该有更新空间的权限
expect(
calcPermission(ESpacePermisson.UpdateSpace, [SpaceRoleType.Admin]),
).toBe(false);
// Admin 不应该有删除空间的权限
expect(
calcPermission(ESpacePermisson.DeleteSpace, [SpaceRoleType.Admin]),
).toBe(false);
// Admin 不应该有转移所有权的权限
expect(
calcPermission(ESpacePermisson.TransferSpace, [SpaceRoleType.Admin]),
).toBe(false);
// Admin 不应该有管理 API 的权限
expect(calcPermission(ESpacePermisson.API, [SpaceRoleType.Admin])).toBe(
false,
);
});
it('应该为 Member 角色返回正确的权限', () => {
// Member 应该有退出空间的权限
expect(
calcPermission(ESpacePermisson.ExitSpace, [SpaceRoleType.Member]),
).toBe(true);
// Member 不应该有更新空间的权限
expect(
calcPermission(ESpacePermisson.UpdateSpace, [SpaceRoleType.Member]),
).toBe(false);
// Member 不应该有删除空间的权限
expect(
calcPermission(ESpacePermisson.DeleteSpace, [SpaceRoleType.Member]),
).toBe(false);
// Member 不应该有添加成员的权限
expect(
calcPermission(ESpacePermisson.AddBotSpaceMember, [
SpaceRoleType.Member,
]),
).toBe(false);
// Member 不应该有移除成员的权限
expect(
calcPermission(ESpacePermisson.RemoveSpaceMember, [
SpaceRoleType.Member,
]),
).toBe(false);
// Member 不应该有转移所有权的权限
expect(
calcPermission(ESpacePermisson.TransferSpace, [SpaceRoleType.Member]),
).toBe(false);
// Member 不应该有更新成员的权限
expect(
calcPermission(ESpacePermisson.UpdateSpaceMember, [
SpaceRoleType.Member,
]),
).toBe(false);
// Member 不应该有管理 API 的权限
expect(calcPermission(ESpacePermisson.API, [SpaceRoleType.Member])).toBe(
false,
);
});
it('应该为 Default 角色返回正确的权限', () => {
// Default 不应该有任何权限
expect(
calcPermission(ESpacePermisson.UpdateSpace, [SpaceRoleType.Default]),
).toBe(false);
expect(
calcPermission(ESpacePermisson.DeleteSpace, [SpaceRoleType.Default]),
).toBe(false);
expect(
calcPermission(ESpacePermisson.AddBotSpaceMember, [
SpaceRoleType.Default,
]),
).toBe(false);
expect(
calcPermission(ESpacePermisson.RemoveSpaceMember, [
SpaceRoleType.Default,
]),
).toBe(false);
expect(
calcPermission(ESpacePermisson.ExitSpace, [SpaceRoleType.Default]),
).toBe(false);
expect(
calcPermission(ESpacePermisson.TransferSpace, [SpaceRoleType.Default]),
).toBe(false);
expect(
calcPermission(ESpacePermisson.UpdateSpaceMember, [
SpaceRoleType.Default,
]),
).toBe(false);
expect(calcPermission(ESpacePermisson.API, [SpaceRoleType.Default])).toBe(
false,
);
});
it('应该处理多个角色的情况', () => {
// 当用户同时拥有 Member 和 Admin 角色时,应该有两个角色的所有权限
expect(
calcPermission(ESpacePermisson.ExitSpace, [
SpaceRoleType.Member,
SpaceRoleType.Admin,
]),
).toBe(true);
expect(
calcPermission(ESpacePermisson.RemoveSpaceMember, [
SpaceRoleType.Member,
SpaceRoleType.Admin,
]),
).toBe(true);
// 即使其中一个角色没有权限,只要有一个角色有权限,就应该返回 true
expect(
calcPermission(ESpacePermisson.UpdateSpace, [
SpaceRoleType.Member,
SpaceRoleType.Owner,
]),
).toBe(true);
});
it('应该处理空角色数组', () => {
// 当没有角色时,应该返回 false
expect(calcPermission(ESpacePermisson.UpdateSpace, [])).toBe(false);
expect(calcPermission(ESpacePermisson.ExitSpace, [])).toBe(false);
});
it('应该处理未知角色', () => {
// 当角色未知时,应该返回 false
expect(
calcPermission(ESpacePermisson.UpdateSpace, [
'UnknownRole' as unknown as SpaceRoleType,
]),
).toBe(false);
});
});
});

View File

@@ -0,0 +1,68 @@
/*
* 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 { describe, it, expect } from 'vitest';
import { SpaceRoleType } from '@coze-arch/idl/developer_api';
import { ESpacePermisson } from '../../src/space/constants';
describe('Space Constants', () => {
describe('ESpacePermisson', () => {
it('应该定义所有必要的权限点', () => {
// 验证所有权限点都已定义
expect(ESpacePermisson.UpdateSpace).toBeDefined();
expect(ESpacePermisson.DeleteSpace).toBeDefined();
expect(ESpacePermisson.AddBotSpaceMember).toBeDefined();
expect(ESpacePermisson.RemoveSpaceMember).toBeDefined();
expect(ESpacePermisson.ExitSpace).toBeDefined();
expect(ESpacePermisson.TransferSpace).toBeDefined();
expect(ESpacePermisson.UpdateSpaceMember).toBeDefined();
expect(ESpacePermisson.API).toBeDefined();
});
it('应该为每个权限点分配唯一的值', () => {
// 创建一个集合来存储所有权限点的值
const permissionValues = new Set();
// 获取所有权限点的值
Object.values(ESpacePermisson)
.filter(value => typeof value === 'number')
.forEach(value => {
permissionValues.add(value);
});
// 验证权限点的数量与唯一值的数量相同
const numericKeys = Object.keys(ESpacePermisson).filter(
key => !isNaN(Number(key)),
).length;
expect(permissionValues.size).toBe(numericKeys);
});
});
describe('SpaceRoleType', () => {
it('应该正确导出 SpaceRoleType', () => {
// 验证 SpaceRoleType 已正确导出
expect(SpaceRoleType).toBeDefined();
// 验证 SpaceRoleType 包含必要的角色
expect(SpaceRoleType.Owner).toBeDefined();
expect(SpaceRoleType.Admin).toBeDefined();
expect(SpaceRoleType.Member).toBeDefined();
expect(SpaceRoleType.Default).toBeDefined();
});
});
});

View File

@@ -0,0 +1,67 @@
/*
* 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 { SpaceRoleType } from '@coze-arch/idl/developer_api';
import { ESpacePermisson } from '../../src/space/constants';
import { calcPermission } from '../../src/space/calc-permission';
describe('calcPermission', () => {
it('should return true for Owner role with UpdateSpace permission', () => {
expect(
calcPermission(ESpacePermisson.UpdateSpace, [SpaceRoleType.Owner]),
).toBe(true);
});
it('should return true for Admin role with RemoveSpaceMember permission', () => {
expect(
calcPermission(ESpacePermisson.RemoveSpaceMember, [SpaceRoleType.Admin]),
).toBe(true);
});
it('should return true for Member role with ExitSpace permission', () => {
expect(
calcPermission(ESpacePermisson.ExitSpace, [SpaceRoleType.Member]),
).toBe(true);
});
it('should return false for Member role with UpdateSpace permission', () => {
expect(
calcPermission(ESpacePermisson.UpdateSpace, [SpaceRoleType.Member]),
).toBe(false);
});
it('should return true for multiple roles with overlapping permissions', () => {
expect(
calcPermission(ESpacePermisson.ExitSpace, [
SpaceRoleType.Admin,
SpaceRoleType.Member,
]),
).toBe(true);
});
it('should return false for unknown role', () => {
expect(
calcPermission(ESpacePermisson.UpdateSpace, [
'UnknownRole' as unknown as SpaceRoleType,
]),
).toBe(false);
});
it('should return false for no roles', () => {
expect(calcPermission(ESpacePermisson.UpdateSpace, [])).toBe(false);
});
});

View File

@@ -0,0 +1,208 @@
/*
* 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 { describe, it, expect, beforeEach, vi } from 'vitest';
import { renderHook, act } from '@testing-library/react-hooks';
import { SpaceRoleType } from '@coze-arch/idl/developer_api';
// 模拟全局变量
vi.stubGlobal('IS_DEV_MODE', true);
describe('Space Auth Store', () => {
beforeEach(() => {
// 重置模块缓存,确保每个测试都使用新的 store 实例
vi.resetModules();
});
describe('setRoles', () => {
it('应该正确设置空间角色', async () => {
// 动态导入 store 模块,确保每次测试都获取新的实例
const { useSpaceAuthStore } = await vi.importActual(
'../../src/space/store',
);
const { result } = renderHook(() => useSpaceAuthStore());
const roles = [SpaceRoleType.Owner, SpaceRoleType.Admin];
await act(() => {
result.current.setRoles('space1', roles);
});
expect(result.current.roles.space1).toEqual(roles);
});
it('应该能够为多个空间设置不同的角色', async () => {
const { useSpaceAuthStore } = await vi.importActual(
'../../src/space/store',
);
const { result } = renderHook(() => useSpaceAuthStore());
const roles1 = [SpaceRoleType.Owner];
const roles2 = [SpaceRoleType.Member];
await act(() => {
result.current.setRoles('space1', roles1);
result.current.setRoles('space2', roles2);
});
expect(result.current.roles.space1).toEqual(roles1);
expect(result.current.roles.space2).toEqual(roles2);
});
it('应该能够更新已存在空间的角色', async () => {
const { useSpaceAuthStore } = await vi.importActual(
'../../src/space/store',
);
const { result } = renderHook(() => useSpaceAuthStore());
const initialRoles = [SpaceRoleType.Owner];
const updatedRoles = [SpaceRoleType.Admin];
await act(() => {
result.current.setRoles('space1', initialRoles);
});
expect(result.current.roles.space1).toEqual(initialRoles);
await act(() => {
result.current.setRoles('space1', updatedRoles);
});
expect(result.current.roles.space1).toEqual(updatedRoles);
});
});
describe('setIsReady', () => {
it('应该正确设置空间数据准备状态', async () => {
const { useSpaceAuthStore } = await vi.importActual(
'../../src/space/store',
);
const { result } = renderHook(() => useSpaceAuthStore());
await act(() => {
result.current.setIsReady('space1', true);
});
expect(result.current.isReady.space1).toBe(true);
});
it('应该能够为多个空间设置不同的准备状态', async () => {
const { useSpaceAuthStore } = await vi.importActual(
'../../src/space/store',
);
const { result } = renderHook(() => useSpaceAuthStore());
await act(() => {
result.current.setIsReady('space1', true);
result.current.setIsReady('space2', false);
});
expect(result.current.isReady.space1).toBe(true);
expect(result.current.isReady.space2).toBe(false);
});
it('应该能够更新已存在空间的准备状态', async () => {
const { useSpaceAuthStore } = await vi.importActual(
'../../src/space/store',
);
const { result } = renderHook(() => useSpaceAuthStore());
await act(() => {
result.current.setIsReady('space1', false);
});
expect(result.current.isReady.space1).toBe(false);
await act(() => {
result.current.setIsReady('space1', true);
});
expect(result.current.isReady.space1).toBe(true);
});
});
describe('destory', () => {
it('应该正确清除空间数据', async () => {
const { useSpaceAuthStore } = await vi.importActual(
'../../src/space/store',
);
const { result } = renderHook(() => useSpaceAuthStore());
const roles = [SpaceRoleType.Owner];
// 设置初始数据
await act(() => {
result.current.setRoles('space1', roles);
result.current.setIsReady('space1', true);
});
// 验证数据已设置
expect(result.current.roles.space1).toEqual(roles);
expect(result.current.isReady.space1).toBe(true);
// 销毁数据
await act(() => {
result.current.destory('space1');
});
// 验证数据已清除
expect(result.current.roles.space1).toEqual([]);
expect(result.current.isReady.space1).toBeUndefined();
});
it('应该只清除指定空间的数据', async () => {
const { useSpaceAuthStore } = await vi.importActual(
'../../src/space/store',
);
const { result } = renderHook(() => useSpaceAuthStore());
// 设置两个空间的数据
await act(() => {
result.current.setRoles('space1', [SpaceRoleType.Owner]);
result.current.setIsReady('space1', true);
result.current.setRoles('space2', [SpaceRoleType.Member]);
result.current.setIsReady('space2', true);
});
// 只销毁 space1 的数据
await act(() => {
result.current.destory('space1');
});
// 验证 space1 的数据已清除
expect(result.current.roles.space1).toEqual([]);
expect(result.current.isReady.space1).toBeUndefined();
// 验证 space2 的数据保持不变
expect(result.current.roles.space2).toEqual([SpaceRoleType.Member]);
expect(result.current.isReady.space2).toBe(true);
});
});
describe('初始状态', () => {
it('应该有正确的初始状态', async () => {
const { useSpaceAuthStore } = await vi.importActual(
'../../src/space/store',
);
const { result } = renderHook(() => useSpaceAuthStore());
// 重置 store 确保测试环境干净
await act(() => {
Object.keys(result.current.roles).forEach(spaceId => {
result.current.destory(spaceId);
});
});
// 验证初始状态
expect(result.current.roles).toEqual({});
expect(result.current.isReady).toEqual({});
});
});
});

View File

@@ -0,0 +1,108 @@
/*
* 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 { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook } from '@testing-library/react-hooks';
// 模拟 React 的 useEffect
const cleanupFns = new Map();
vi.mock('react', () => ({
useEffect: vi.fn((fn, deps) => {
// 执行 effect 函数并获取清理函数
const cleanup = fn();
// 存储清理函数,以便在 unmount 时调用
cleanupFns.set(fn, cleanup);
// 返回清理函数
return cleanup;
}),
}));
// 模拟 store
const mockDestory = vi.fn();
vi.mock('../../src/space/store', () => ({
useSpaceAuthStore: vi.fn(selector => selector({ destory: mockDestory })),
}));
// 创建一个包装函数,确保在 unmount 时调用清理函数
function renderHookWithCleanup(callback, options = {}) {
const result = renderHook(callback, options);
const originalUnmount = result.unmount;
result.unmount = () => {
// 调用所有清理函数
cleanupFns.forEach(cleanup => {
if (typeof cleanup === 'function') {
cleanup();
}
});
// 调用原始的 unmount
originalUnmount();
};
return result;
}
import { useDestorySpace } from '../../src/space/use-destory-space';
describe('useDestorySpace', () => {
beforeEach(() => {
vi.clearAllMocks();
cleanupFns.clear();
});
it('应该在组件卸载时调用 destory 方法', () => {
const spaceId = 'test-space-id';
// 渲染 hook
const { unmount } = renderHookWithCleanup(() => useDestorySpace(spaceId));
// 初始时不应调用 destory
expect(mockDestory).not.toHaveBeenCalled();
// 模拟组件卸载
unmount();
// 卸载时应调用 destory 并传入正确的 spaceId
expect(mockDestory).toHaveBeenCalledTimes(1);
expect(mockDestory).toHaveBeenCalledWith(spaceId);
});
it('应该为不同的 spaceId 调用 destory 方法', () => {
const spaceId1 = 'space-id-1';
const spaceId2 = 'space-id-2';
// 渲染第一个 hook 实例
const { unmount: unmount1 } = renderHookWithCleanup(() =>
useDestorySpace(spaceId1),
);
// 渲染第二个 hook 实例
const { unmount: unmount2 } = renderHookWithCleanup(() =>
useDestorySpace(spaceId2),
);
// 卸载第一个实例
unmount1();
expect(mockDestory).toHaveBeenCalledWith(spaceId1);
// 卸载第二个实例
unmount2();
expect(mockDestory).toHaveBeenCalledWith(spaceId2);
// 总共应调用两次
expect(mockDestory).toHaveBeenCalledTimes(4);
});
});

View File

@@ -0,0 +1,112 @@
/*
* 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 { describe, it, expect, vi } from 'vitest';
import { renderHook } from '@testing-library/react-hooks';
import { SpaceRoleType } from '@coze-arch/idl/developer_api';
import { ESpacePermisson } from '../../src/space/constants';
// 模拟 useSpaceRole
vi.mock('../../src/space/use-space-role', () => ({
useSpaceRole: vi.fn(),
}));
// 模拟 calcPermission
vi.mock('../../src/space/calc-permission', () => ({
calcPermission: vi.fn(),
}));
import { useSpaceRole } from '../../src/space/use-space-role';
import { calcPermission } from '../../src/space/calc-permission';
import { useSpaceAuth } from '../../src/space/use-space-auth';
describe('useSpaceAuth', () => {
it('应该使用 useSpaceRole 获取角色并调用 calcPermission 计算权限', () => {
const spaceId = 'test-space-id';
const permissionKey = ESpacePermisson.UpdateSpace;
const mockRoles = [SpaceRoleType.Owner];
// 模拟 useSpaceRole 返回角色
(useSpaceRole as unknown as ReturnType<typeof vi.fn>).mockReturnValue(
mockRoles,
);
// 模拟 calcPermission 返回权限结果
(calcPermission as unknown as ReturnType<typeof vi.fn>).mockReturnValue(
true,
);
// 渲染 hook
const { result } = renderHook(() => useSpaceAuth(permissionKey, spaceId));
// 验证 useSpaceRole 被调用,并传入正确的 spaceId
expect(useSpaceRole).toHaveBeenCalledWith(spaceId);
// 验证 calcPermission 被调用,并传入正确的参数
expect(calcPermission).toHaveBeenCalledWith(permissionKey, mockRoles);
// 验证返回值与 calcPermission 的返回值一致
expect(result.current).toBe(true);
});
it('应该在没有权限时返回 false', () => {
const spaceId = 'test-space-id';
const permissionKey = ESpacePermisson.UpdateSpace;
const mockRoles = [SpaceRoleType.Member];
// 模拟 useSpaceRole 返回角色
(useSpaceRole as unknown as ReturnType<typeof vi.fn>).mockReturnValue(
mockRoles,
);
// 模拟 calcPermission 返回权限结果
(calcPermission as unknown as ReturnType<typeof vi.fn>).mockReturnValue(
false,
);
// 渲染 hook
const { result } = renderHook(() => useSpaceAuth(permissionKey, spaceId));
// 验证返回值与 calcPermission 的返回值一致
expect(result.current).toBe(false);
});
it('应该在角色为空数组时返回 false', () => {
const spaceId = 'test-space-id';
const permissionKey = ESpacePermisson.UpdateSpace;
const mockRoles: SpaceRoleType[] = [];
// 模拟 useSpaceRole 返回空角色数组
(useSpaceRole as unknown as ReturnType<typeof vi.fn>).mockReturnValue(
mockRoles,
);
// 模拟 calcPermission 返回权限结果
(calcPermission as unknown as ReturnType<typeof vi.fn>).mockReturnValue(
false,
);
// 渲染 hook
const { result } = renderHook(() => useSpaceAuth(permissionKey, spaceId));
// 验证 calcPermission 被调用,并传入正确的参数
expect(calcPermission).toHaveBeenCalledWith(permissionKey, mockRoles);
// 验证返回值与 calcPermission 的返回值一致
expect(result.current).toBe(false);
});
});

View File

@@ -0,0 +1,138 @@
/*
* 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 { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook } from '@testing-library/react-hooks';
import { SpaceRoleType } from '@coze-arch/idl/developer_api';
import { useSpaceAuthStore } from '../../src/space/store';
// 模拟 zustand
vi.mock('zustand/react/shallow', () => ({
useShallow: fn => fn,
}));
// 模拟 foundation-sdk
const mockUseSpace = vi.fn();
vi.mock('@coze-arch/foundation-sdk', () => ({
useSpace: (...args) => mockUseSpace(...args),
}));
// 模拟 store
vi.mock('../../src/space/store', () => ({
useSpaceAuthStore: vi.fn(),
}));
// 导入实际模块,确保在模拟之后导入
import { useSpaceRole } from '../../src/space/use-space-role';
describe('useSpaceRole', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('应该在 space 存在且 isReady 为 true 时返回角色', () => {
const spaceId = 'test-space-id';
const mockSpace = { id: spaceId, name: 'Test Space' };
const mockRoles = [SpaceRoleType.Owner];
// 模拟 useSpace 返回 space 对象
mockUseSpace.mockReturnValue(mockSpace);
// 模拟 useSpaceAuthStore 返回 isReady 和 role
(useSpaceAuthStore as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
isReady: true,
role: mockRoles,
});
// 渲染 hook
const { result } = renderHook(() => useSpaceRole(spaceId));
// 验证 useSpace 被调用,并传入正确的 spaceId
expect(mockUseSpace).toHaveBeenCalledWith(spaceId);
// 验证 useSpaceAuthStore 被调用,并传入正确的选择器
expect(useSpaceAuthStore).toHaveBeenCalled();
// 验证返回值与预期一致
expect(result.current).toEqual(mockRoles);
});
it('应该在 space 不存在时抛出错误', () => {
const spaceId = 'test-space-id';
// 模拟 useSpace 返回 null
mockUseSpace.mockReturnValue(null);
// 使用 vi.spyOn 监听 console.error 以防止测试输出错误信息
vi.spyOn(console, 'error').mockImplementation(() => {
// 空实现,防止错误输出
});
// 验证渲染 hook 时抛出错误
expect(() => useSpaceRole(spaceId)).toThrow(
'useSpaceAuth must be used after space list has been pulled.',
);
});
it('应该在 isReady 为 false 时抛出错误', () => {
const spaceId = 'test-space-id';
const mockSpace = { id: spaceId, name: 'Test Space' };
// 模拟 useSpace 返回 space 对象
mockUseSpace.mockReturnValue(mockSpace);
// 模拟 useSpaceAuthStore 返回 isReady 为 false
(useSpaceAuthStore as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
isReady: false,
role: null,
});
// 使用 vi.spyOn 监听 console.error 以防止测试输出错误信息
vi.spyOn(console, 'error').mockImplementation(() => {
// 空实现,防止错误输出
});
// 验证渲染 hook 时抛出错误
expect(() => useSpaceRole(spaceId)).toThrow(
'useSpaceAuth must be used after useInitSpaceRole has been completed.',
);
});
it('应该在 role 不存在时抛出错误', () => {
const spaceId = 'test-space-id';
const mockSpace = { id: spaceId, name: 'Test Space' };
// 模拟 useSpace 返回 space 对象
mockUseSpace.mockReturnValue(mockSpace);
// 模拟 useSpaceAuthStore 返回 isReady 为 true但 role 为 null
(useSpaceAuthStore as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
isReady: true,
role: null,
});
// 使用 vi.spyOn 监听 console.error 以防止测试输出错误信息
vi.spyOn(console, 'error').mockImplementation(() => {
// 空实现,防止错误输出
});
// 验证渲染 hook 时抛出错误
expect(() => useSpaceRole(spaceId)).toThrow(
`Can not get space role of space: ${spaceId}`,
);
});
});

View File

@@ -0,0 +1,12 @@
{
"operationSettings": [
{
"operationName": "test:cov",
"outputFolderNames": ["coverage"]
},
{
"operationName": "ts-check",
"outputFolderNames": ["./dist"]
}
]
}

View File

@@ -0,0 +1,6 @@
{
"codecov": {
"coverage": 0,
"incrementCoverage": 0
}
}

View File

@@ -0,0 +1,7 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'node',
rules: {},
});

View File

@@ -0,0 +1,42 @@
{
"name": "@coze-common/auth",
"version": "0.0.1",
"description": "统一的权限控制逻辑",
"license": "Apache-2.0",
"author": "sunzhiyuan.evan@bytedance.com",
"maintainers": [],
"main": "src/index.ts",
"scripts": {
"build": "exit 0",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"dependencies": {
"@coze-arch/bot-api": "workspace:*",
"@coze-arch/bot-space-api": "workspace:*",
"@coze-arch/foundation-sdk": "workspace:*",
"@coze-arch/idl": "workspace:*",
"@coze-arch/logger": "workspace:*",
"@coze-arch/report-events": "workspace:*",
"ahooks": "^3.7.8",
"react": "~18.2.0",
"zustand": "^4.4.7"
},
"devDependencies": {
"@coze-arch/bot-typings": "workspace:*",
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"@testing-library/react-hooks": "^8.0.1",
"@types/node": "^18",
"@types/react": "18.2.37",
"@vitest/coverage-v8": "~3.0.5",
"react-dom": "~18.2.0",
"sucrase": "^3.32.0",
"vitest": "~3.0.5"
}
}

View File

@@ -0,0 +1,27 @@
/*
* 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 { useDestorySpace } from './space/use-destory-space';
export { useSpaceAuth } from './space/use-space-auth';
export { ESpacePermisson, SpaceRoleType } from './space/constants';
export { useSpaceRole } from './space/use-space-role';
export { useSpaceAuthStore } from './space/store';
export { useProjectAuth } from './project/use-project-auth';
export { useDestoryProject } from './project/use-destory-project';
export { EProjectPermission, ProjectRoleType } from './project/constants';
export { useProjectRole } from './project/use-project-role';
export { useProjectAuthStore } from './project/store';

View File

@@ -0,0 +1,109 @@
/*
* 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 { SpaceRoleType, SpaceType } from '@coze-arch/idl/developer_api';
import { ProjectRoleType, EProjectPermission } from './constants';
const projectRolePermissionMapOfTeamSpace = {
[ProjectRoleType.Owner]: [
EProjectPermission.View,
EProjectPermission.EDIT_INFO,
EProjectPermission.DELETE,
EProjectPermission.PUBLISH,
EProjectPermission.CREATE_RESOURCE,
EProjectPermission.COPY_RESOURCE,
EProjectPermission.COPY,
EProjectPermission.TEST_RUN_PLUGIN,
EProjectPermission.TEST_RUN_WORKFLOW,
EProjectPermission.ADD_COLLABORATOR,
EProjectPermission.DELETE_COLLABORATOR,
EProjectPermission.ROLLBACK,
],
[ProjectRoleType.Editor]: [
EProjectPermission.View,
EProjectPermission.EDIT_INFO,
EProjectPermission.CREATE_RESOURCE,
EProjectPermission.COPY_RESOURCE,
EProjectPermission.COPY,
EProjectPermission.TEST_RUN_PLUGIN,
EProjectPermission.TEST_RUN_WORKFLOW,
EProjectPermission.ADD_COLLABORATOR,
],
};
const spaceRolePermissionMapOfTeamSpace = {
[SpaceRoleType.Member]: [
EProjectPermission.View,
EProjectPermission.COPY,
EProjectPermission.TEST_RUN_WORKFLOW,
],
[SpaceRoleType.Owner]: [
EProjectPermission.View,
EProjectPermission.COPY,
EProjectPermission.TEST_RUN_WORKFLOW,
],
[SpaceRoleType.Admin]: [
EProjectPermission.View,
EProjectPermission.COPY,
EProjectPermission.TEST_RUN_WORKFLOW,
],
[SpaceRoleType.Default]: [] as EProjectPermission[],
};
const personalSpacePermission = [
EProjectPermission.View,
EProjectPermission.EDIT_INFO,
EProjectPermission.PUBLISH,
EProjectPermission.DELETE,
EProjectPermission.CREATE_RESOURCE,
EProjectPermission.COPY_RESOURCE,
EProjectPermission.COPY,
EProjectPermission.TEST_RUN_PLUGIN,
EProjectPermission.TEST_RUN_WORKFLOW,
EProjectPermission.ROLLBACK,
];
export function calcPermission(
key: EProjectPermission,
{
projectRoles,
spaceRoles,
spaceType,
}: {
projectRoles: ProjectRoleType[];
spaceRoles: SpaceRoleType[];
spaceType: SpaceType;
},
) {
if (spaceType === SpaceType.Personal) {
return personalSpacePermission.includes(key);
} else {
for (const projectRole of projectRoles) {
if (projectRolePermissionMapOfTeamSpace[projectRole]?.includes(key)) {
return true;
}
}
for (const spaceRole of spaceRoles) {
if (spaceRolePermissionMapOfTeamSpace[spaceRole]?.includes(key)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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.
*/
// TODO: 替换成Project接口导出的idl
export enum ProjectRoleType {
Owner = 'owner',
Editor = 'editor',
}
export enum EProjectPermission {
/**
* 访问/查看project
*/
View,
/**
* 编辑project基础信息
*/
EDIT_INFO,
/**
* 删除project
*/
DELETE,
/**
* 发布project
*/
PUBLISH,
/**
* 创建project内资源
*/
CREATE_RESOURCE,
/**
* 在project内复制资源
*/
COPY_RESOURCE,
/**
* 复制project/创建副本
*/
COPY,
/**
* 试运行plugin
*/
TEST_RUN_PLUGIN,
/**
* 试运行workflow
*/
TEST_RUN_WORKFLOW,
/**
* 添加project协作者
*/
ADD_COLLABORATOR,
/**
* 删除project协作者
*/
DELETE_COLLABORATOR,
/**
* 回滚 APP 版本
*/
ROLLBACK,
}

View File

@@ -0,0 +1,71 @@
/*
* 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 { type ProjectRoleType } from './constants';
interface ProjectAuthStoreState {
// 每一个Project的角色数据
roles: {
[projectId: string]: ProjectRoleType[];
};
// 每一个Project的角色数据的初始化状态是否完成初始化。
isReady: {
[projectId: string]: boolean;
};
}
interface SpaceAuthStoreAction {
// 设置projectId对应的Project的角色
setRoles: (projectId: string, role: ProjectRoleType[]) => void;
// 设置projectId对应的Project的数据是否ready
setIsReady: (projectId: string, isReady: boolean) => void;
// 回收Project数据
destory: (projectId) => void;
}
/**
* ProjectAuthStore设计成支持多Project切换维护多个Project的数据防止因为Project切换时序导致的bug。
*/
export const useProjectAuthStore = create<
ProjectAuthStoreState & SpaceAuthStoreAction
>()(
devtools(
set => ({
roles: {},
isReady: {},
setRoles: (projectId, roles) =>
set(state => ({
roles: {
...state.roles,
[projectId]: roles,
},
})),
setIsReady: (projectId, isReady) =>
set(state => ({ isReady: { ...state.isReady, [projectId]: isReady } })),
destory: projectId =>
set(state => ({
roles: { ...state.roles, [projectId]: [] },
isReady: { ...state.isReady, [projectId]: false },
})),
}),
{
enabled: IS_DEV_MODE,
name: 'botStudio.projectAuthStore',
},
),
);

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.
*/
import { useEffect } from 'react';
import { useProjectAuthStore } from './store';
export function useDestoryProject(projectId: string) {
const destorySpace = useProjectAuthStore(store => store.destory);
return useEffect(
() => () => {
// 空间组件销毁时清空对应space数据
destorySpace(projectId);
},
[],
);
}

View File

@@ -0,0 +1,50 @@
/*
* 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 { useSpace } from '@coze-arch/foundation-sdk';
import { useSpaceRole } from '../space/use-space-role';
import { useProjectRole } from './use-project-role';
import { type EProjectPermission } from './constants';
import { calcPermission } from './calc-permission';
export function useProjectAuth(
key: EProjectPermission,
projectId: string,
spaceId: string,
) {
// 获取space类型信息
const space = useSpace(spaceId);
if (!space?.space_type) {
throw new Error(
'useSpaceAuth must be used after space list has been pulled.',
);
}
// 获取space role信息
const spaceRoles = useSpaceRole(spaceId);
// 获取project role信息
const projectRoles = useProjectRole(projectId);
// 计算权限点
return calcPermission(key, {
projectRoles,
spaceRoles,
spaceType: space.space_type,
});
}

View File

@@ -0,0 +1,38 @@
/*
* 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 { useShallow } from 'zustand/react/shallow';
import { useProjectAuthStore } from './store';
import { type ProjectRoleType } from './constants';
export function useProjectRole(projectId: string): ProjectRoleType[] {
const { isReady: isProjectReady, role: projectRole = [] } =
useProjectAuthStore(
useShallow(store => ({
isReady: store.isReady[projectId],
role: store.roles[projectId],
})),
);
if (!isProjectReady) {
throw new Error(
'useProjectAuth must be used after useInitProjectRole has been completed.',
);
}
return projectRole;
}

View File

@@ -0,0 +1,51 @@
/*
* 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 { SpaceRoleType } from '@coze-arch/idl/developer_api';
import { ESpacePermisson } from './constants';
const permissionMap = {
[SpaceRoleType.Owner]: [
ESpacePermisson.UpdateSpace,
ESpacePermisson.DeleteSpace,
ESpacePermisson.AddBotSpaceMember,
ESpacePermisson.RemoveSpaceMember,
ESpacePermisson.TransferSpace,
ESpacePermisson.UpdateSpaceMember,
ESpacePermisson.API,
],
[SpaceRoleType.Admin]: [
ESpacePermisson.AddBotSpaceMember,
ESpacePermisson.RemoveSpaceMember,
ESpacePermisson.ExitSpace,
ESpacePermisson.UpdateSpaceMember,
],
[SpaceRoleType.Member]: [ESpacePermisson.ExitSpace],
// [SpaceRoleType.Default]: [],
};
export const calcPermission = (
key: ESpacePermisson,
roles: SpaceRoleType[],
) => {
for (const role of roles) {
if (permissionMap[role]?.includes(key)) {
return true;
}
}
return false;
};

View File

@@ -0,0 +1,58 @@
/*
* 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 enum ESpacePermisson {
/**
* 更新空间
*/
UpdateSpace,
/**
* 删除空间
*/
DeleteSpace,
/**
* 添加成员
*/
AddBotSpaceMember,
/**
* 移除空间成员
*/
RemoveSpaceMember,
/**
* 退出空间
*/
ExitSpace,
/**
* 转移owner权限
*/
TransferSpace,
/**
* 更新成员
*/
UpdateSpaceMember,
/**
* 管理API-KEY
*/
API,
}
/**
* 空间角色枚举
*/
export { SpaceRoleType } from '@coze-arch/idl/developer_api';

View File

@@ -0,0 +1,70 @@
/*
* 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 { type SpaceRoleType } from '@coze-arch/idl/developer_api';
interface SpaceAuthStoreState {
// 每一个空间的角色数据
roles: {
[spaceId: string]: SpaceRoleType[];
};
// 每一个空间的角色数据的初始化状态,是否完成初始化。
isReady: {
[spaceId: string]: boolean;
};
}
interface SpaceAuthStoreAction {
// 设置spaceId对应的空间的角色
setRoles: (spaceId: string, roles: SpaceRoleType[]) => void;
// 设置spaceId对应的空间的数据是否ready
setIsReady: (spaceId: string, isReady: boolean) => void;
// 回收空间数据
destory: (spaceId) => void;
}
/**
* SpaceAuthStore设计成支持多空间切换维护多个空间的数据位置因为空间切换时序导致的bug。
*/
export const useSpaceAuthStore = create<
SpaceAuthStoreState & SpaceAuthStoreAction
>()(
devtools(
set => ({
roles: {},
isReady: {},
setRoles: (spaceId, roles) =>
set(state => ({
roles: {
...state.roles,
[spaceId]: roles,
},
})),
setIsReady: (spaceId, isReady) =>
set(state => ({ isReady: { ...state.isReady, [spaceId]: isReady } })),
destory: spaceId =>
set(state => ({
roles: { ...state.roles, [spaceId]: [] },
isReady: { ...state.isReady, [spaceId]: undefined },
})),
}),
{
enabled: IS_DEV_MODE,
name: 'botStudio.spaceAuthStore',
},
),
);

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.
*/
import { useEffect } from 'react';
import { useSpaceAuthStore } from './store';
export function useDestorySpace(spaceId: string) {
const destorySpace = useSpaceAuthStore(store => store.destory);
return useEffect(
() => () => {
// 空间组件销毁时清空对应space数据
destorySpace(spaceId);
},
[],
);
}

View File

@@ -0,0 +1,26 @@
/*
* 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 { useSpaceRole } from './use-space-role';
import { type ESpacePermisson } from './constants';
import { calcPermission } from './calc-permission';
export function useSpaceAuth(key: ESpacePermisson, spaceId: string) {
// 获取space role信息
const role = useSpaceRole(spaceId);
// 计算权限点
return calcPermission(key, role);
}

View File

@@ -0,0 +1,50 @@
/*
* 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 { useShallow } from 'zustand/react/shallow';
import { useSpace } from '@coze-arch/foundation-sdk';
import { useSpaceAuthStore } from './store';
export function useSpaceRole(spaceId: string) {
// 获取space信息已有hook。
const space = useSpace(spaceId);
if (!space) {
throw new Error(
'useSpaceAuth must be used after space list has been pulled.',
);
}
const { isReady, role } = useSpaceAuthStore(
useShallow(store => ({
isReady: store.isReady[spaceId],
role: store.roles[spaceId],
})),
);
if (!isReady) {
throw new Error(
'useSpaceAuth must be used after useInitSpaceRole has been completed.',
);
}
if (!role) {
throw new Error(`Can not get space role of space: ${spaceId}`);
}
return role;
}

View File

@@ -0,0 +1,19 @@
/*
* 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_DEV_MODE: boolean;
/// <reference types='@coze-arch/bot-typings' />

View File

@@ -0,0 +1,43 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@coze-arch/ts-config/tsconfig.node.json",
"compilerOptions": {
"types": [],
"rootDir": "./src",
"outDir": "./dist",
"tsBuildInfoFile": "./dist/tsconfig.build.tsbuildinfo"
},
"include": ["src"],
"references": [
{
"path": "../../arch/bot-api/tsconfig.build.json"
},
{
"path": "../../arch/bot-space-api/tsconfig.build.json"
},
{
"path": "../../arch/bot-typings/tsconfig.build.json"
},
{
"path": "../../arch/foundation-sdk/tsconfig.build.json"
},
{
"path": "../../arch/idl/tsconfig.build.json"
},
{
"path": "../../arch/logger/tsconfig.build.json"
},
{
"path": "../../arch/report-events/tsconfig.build.json"
},
{
"path": "../../../config/eslint-config/tsconfig.build.json"
},
{
"path": "../../../config/ts-config/tsconfig.build.json"
},
{
"path": "../../../config/vitest-config/tsconfig.build.json"
}
]
}

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"composite": true
},
"references": [
{
"path": "./tsconfig.build.json"
},
{
"path": "./tsconfig.misc.json"
}
],
"exclude": ["**/*"]
}

View File

@@ -0,0 +1,16 @@
{
"extends": "@coze-arch/ts-config/tsconfig.node.json",
"$schema": "https://json.schemastore.org/tsconfig",
"include": ["__tests__", "vitest.config.ts"],
"exclude": ["./dist"],
"references": [
{
"path": "./tsconfig.build.json"
}
],
"compilerOptions": {
"rootDir": "./",
"outDir": "./dist",
"types": ["vitest/globals"]
}
}

View File

@@ -0,0 +1,28 @@
/*
* 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 { defineConfig } from '@coze-arch/vitest-config';
export default defineConfig({
dirname: __dirname,
preset: 'web',
test: {
// 全局测试超时时间(毫秒)
testTimeout: 10000, // 10秒
// Hook 超时时间(毫秒)
hookTimeout: 10000, // 10秒
},
});

View File

@@ -0,0 +1,26 @@
import path from 'path';
/** @type { import('@storybook/react-vite').StorybookConfig } */
const config = {
stories: ['../stories/**/*.mdx', '../stories/**/*.stories.tsx'],
framework: {
name: '@edenx/storybook',
options: {
bundler: 'webpack',
configPath: path.resolve(__dirname, '../edenx.config.ts'),
},
},
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-onboarding',
'@storybook/addon-interactions',
],
docs: {
autodocs: 'tag',
},
typescript: {
reactDocgen: 'react-docgen',
},
};
export default config;

View File

@@ -0,0 +1,14 @@
/** @type { import('@storybook/react').Preview } */
const preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;

View File

@@ -0,0 +1,5 @@
const { defineConfig } = require('@coze-arch/stylelint-config');
module.exports = defineConfig({
extends: [],
});

View File

@@ -0,0 +1,16 @@
# @coze-common/biz-components
> Project template for react component with storybook.
## Features
- [x] eslint & ts
- [x] esm bundle
- [x] umd bundle
- [x] storybook
## Commands
- init: `rush update`
- dev: `npm run dev`
- build: `npm run build`

View File

@@ -0,0 +1,12 @@
{
"operationSettings": [
{
"operationName": "test:cov",
"outputFolderNames": ["coverage"]
},
{
"operationName": "ts-check",
"outputFolderNames": ["./dist"]
}
]
}

View File

@@ -0,0 +1,7 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'web',
rules: {},
});

View File

@@ -0,0 +1,104 @@
{
"name": "@coze-common/biz-components",
"version": "0.0.1",
"description": "通用业务组件",
"license": "Apache-2.0",
"author": "zhuxiaowei.711@bytedance.com",
"maintainers": [],
"exports": {
".": "./src/index.tsx",
"./banner": "./src/banner/index.tsx",
"./picture-upload": "./src/picture-upload/index.ts",
"./parameters": "./src/parameters/index.ts",
"./coachmark": "./src/coachmark/index.tsx",
"./select-intelligence-modal": "./src/select-intelligence-modal/index.ts"
},
"main": "src/index.tsx",
"typesVersions": {
"*": {
"banner": [
"./src/banner/index.tsx"
],
"picture-upload": [
"./src/picture-upload/index.ts"
],
"parameters": [
"./src/parameters/index.ts"
],
"coachmark": [
"./src/coachmark/index.tsx"
],
"select-intelligence-modal": [
"./src/select-intelligence-modal/index.ts"
]
}
},
"scripts": {
"build": "exit 0",
"dev": "storybook dev -p 6006",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"dependencies": {
"@coze-arch/bot-api": "workspace:*",
"@coze-arch/bot-error": "workspace:*",
"@coze-arch/bot-icons": "workspace:*",
"@coze-arch/bot-semi": "workspace:*",
"@coze-arch/bot-utils": "workspace:*",
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
"@coze-arch/foundation-sdk": "workspace:*",
"@coze-arch/i18n": "workspace:*",
"@coze-arch/idl": "workspace:*",
"@coze-arch/report-events": "workspace:*",
"@coze-arch/semi-theme-hand01": "0.0.6-alpha.346d77",
"@coze-data/e2e": "workspace:*",
"@coze-foundation/local-storage": "workspace:*",
"@douyinfe/semi-webpack-plugin": "2.61.0",
"ahooks": "^3.7.8",
"axios": "^1.4.0",
"classnames": "^2.3.2",
"dompurify": "3.0.8",
"lodash-es": "^4.17.21",
"nanoid": "^4.0.2",
"react-joyride": "^2.8.2",
"zod": "3.22.4"
},
"devDependencies": {
"@coze-arch/bot-typings": "workspace:*",
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/stylelint-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@storybook/addon-essentials": "^7.6.7",
"@storybook/addon-interactions": "^7.6.7",
"@storybook/addon-links": "^7.6.7",
"@storybook/addon-onboarding": "^1.0.10",
"@storybook/client-api": "^7.6.17",
"@storybook/react": "^7.6.7",
"@storybook/react-vite": "^7.6.7",
"@types/dompurify": "3.0.5",
"@types/lodash-es": "^4.17.10",
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
"@vitest/coverage-v8": "~3.0.5",
"debug": "^4.3.4",
"i18next": ">= 19.0.0",
"react": "~18.2.0",
"react-dom": "~18.2.0",
"react-is": ">= 16.8.0",
"storybook": "^7.6.7",
"styled-components": ">= 2",
"stylelint": "^15.11.0",
"typescript": "~5.8.2",
"vite": "^4.3.9",
"vitest": "~3.0.5",
"webpack": "~5.91.0"
},
"peerDependencies": {
"react": ">=18.2.0",
"react-dom": ">=18.2.0"
},
"// deps": "debug@^4.3.4 为脚本自动补齐,请勿改动"
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,142 @@
/*
* 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 React, { useState, type FC } from 'react';
import { I18n } from '@coze-arch/i18n';
import {
Switch,
TextArea,
Button,
type ButtonProps,
} from '@coze-arch/coze-design';
interface IValue {
isOpen?: boolean;
replyText?: string;
}
interface AsyncFormProps {
value?: IValue;
onChange?: (value: IValue) => void;
switchStatus?: 'default' | 'hidden' | 'disabled';
disabled?: boolean;
textAreaVisible?: boolean;
saveButtonProps?: ButtonProps;
}
const REPLY_MAX_LENGTH = 1000;
const validate = (value: IValue, needReply: boolean): string => {
if (!needReply) {
return '';
}
if (!value.replyText) {
return I18n.t('asyn_task_reply_need');
}
if (value.replyText.length > REPLY_MAX_LENGTH) {
return I18n.t('asyn_task_reply_toolong');
}
return '';
};
export const AsyncSettingUI: FC<AsyncFormProps> = props => {
const {
onChange,
switchStatus = 'default',
textAreaVisible = true,
saveButtonProps = {},
disabled,
} = props;
const [value, setValue] = useState<IValue>(props.value ?? {});
const [error, setError] = useState('');
return (
<div className="flex flex-col h-full gap-[12px] text-lg">
<div className="flex flex-col gap-[4px]">
<div className="flex">
<div className="flex-1 font-semibold coz-fg-primary">
{I18n.t('asyn_task_setting_title')}
</div>
{switchStatus === 'hidden' ? null : (
<Switch
size="small"
disabled={switchStatus === 'disabled' || disabled}
checked={value?.isOpen}
onChange={(v: boolean) => {
setValue({
...value,
isOpen: v,
});
}}
/>
)}
</div>
<div className="coz-fg-secondary">
{I18n.t('asyn_task_setting_desc')}
</div>
</div>
{textAreaVisible && value?.isOpen ? (
<div className="flex flex-col flex-1 gap-[12px]">
<div className="font-semibold coz-fg-primary">
{I18n.t('asyn_task_setting_response_title')}
<span className="coz-fg-hglt-red">*</span>
</div>
<div className="flex-1">
<TextArea
disabled={disabled}
error={!!error}
className="h-[135px]"
suffix={
<div>{`${
value?.replyText?.length || 0
}/${REPLY_MAX_LENGTH}`}</div>
}
value={value?.replyText}
onChange={(v: string) => {
const newValue = {
...value,
replyText: v,
};
setValue(newValue);
setError(validate(newValue, textAreaVisible));
}}
placeholder={I18n.t('asyn_task_setting_response_content')}
/>
{error ? (
<div className="coz-fg-hglt-red text-base">{error}</div>
) : undefined}
</div>
</div>
) : undefined}
<div className="flex justify-end mt-auto">
<Button
{...saveButtonProps}
disabled={disabled}
onClick={() => {
setError(validate(value, textAreaVisible));
if (!error) {
onChange?.(value);
}
}}
>
{I18n.t('Save')}
</Button>
</div>
</div>
);
};

View File

@@ -0,0 +1,25 @@
.banner-preview {
@apply w-full;
.label {
@apply coz-fg-white-dim text-xxl font-medium;
line-height: 22px;
}
.icon {
& path {
@apply coz-fg-hglt-plus-dim;
}
}
a {
@apply coz-fg-white-dim text-xxl font-medium underline;
line-height: 22px;
&:visited,&:active,&:focus {
@apply coz-fg-white-dim;
}
}
}

View File

@@ -0,0 +1,80 @@
/*
* 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 React, { type CSSProperties, forwardRef } from 'react';
import DOMPurify from 'dompurify';
import classNames from 'classnames';
import { IconCozCrossFill } from '@coze-arch/coze-design/icons';
import {
Banner as CozeDesignBanner,
type BannerProps as CozeDesignBannerProps,
} from '@coze-arch/coze-design';
import styles from './index.module.less';
export interface BannerProps {
label?: string;
backgroundColor?: string;
showClose?: boolean;
className?: string;
style?: CSSProperties;
labelClassName?: string;
labelStyle?: CSSProperties;
bannerProps?: CozeDesignBannerProps;
}
export const Banner = forwardRef<HTMLDivElement, BannerProps>(
(
{
className,
style,
label,
backgroundColor,
showClose = true,
labelClassName,
labelStyle,
bannerProps,
},
ref,
) => {
const description = (
<span
className={classNames(labelClassName, styles.label)}
style={labelStyle}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(label || '', {
ALLOWED_ATTR: ['href', 'target'],
}),
}}
/>
);
return (
<div ref={ref} className={className} style={style}>
<CozeDesignBanner
icon={null}
className={styles['banner-preview']}
style={{ backgroundColor }}
closeIcon={
showClose ? <IconCozCrossFill className={styles.icon} /> : null
}
description={description}
{...bannerProps}
/>
</div>
);
},
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,4 @@
.container {
padding: 8px;
background: var(--Bg-COZ-bg-max, #FFF);
}

View File

@@ -0,0 +1,21 @@
/*
* 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 s from './index.module.less';
export const Container = ({ children }: { children: React.ReactNode }) => (
<div className={s.container}>{children}</div>
);

View File

@@ -0,0 +1,172 @@
/*
* 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 Joyride, {
type Props,
ACTIONS,
EVENTS,
type CallBackProps,
} from 'react-joyride';
import React, { useState, useEffect, useCallback } from 'react';
import { typeSafeJSONParse } from '@coze-arch/bot-utils';
import { localStorageService } from '@coze-foundation/local-storage';
import { Tooltip, type IExtraAction } from './tooltip';
import { StepCard } from './step-card';
export { type Placement, type Step } from 'react-joyride';
const COACHMARK_KEY = 'coachmark';
const COACHMARK_END = 10000;
export default function Coachmark({
steps,
extraAction,
showProgress = true,
caseId,
itemIndex = 0,
}: {
steps: Props['steps'];
showProgress?: Props['showProgress'];
extraAction?: IExtraAction;
caseId: string;
itemIndex?: number;
}) {
const [visible, setVisible] = useState(false);
const [stepIndex, setStepIndex] = useState(itemIndex);
const initVisible = async (cid: string) => {
const coachMarkStorage =
await localStorageService.getValueSync(COACHMARK_KEY);
// readStep 代表已读的step index
const readStep = (
typeSafeJSONParse(coachMarkStorage) as Record<string, number> | undefined
)?.[cid];
// 如果没有读过或者读过的step index 小于当前项的index则展示。
const shouldShow = readStep === undefined || itemIndex > readStep;
setVisible(shouldShow);
};
// 设置已读的step index
const setCoachmarkReadStep = useCallback(
(step: number) => {
const coachmarkStorage =
localStorageService.getValue(COACHMARK_KEY) ?? '{}';
const coachmarkValue: Record<string, number | undefined> =
(typeSafeJSONParse(coachmarkStorage) ?? {}) as unknown as Record<
string,
number | undefined
>;
// 如果没有读过或者要设置的index大于已读的step index 才设置,否则忽略。
if (
coachmarkValue[caseId] === undefined ||
step > Number(coachmarkValue[caseId])
) {
localStorageService.setValue(
COACHMARK_KEY,
JSON.stringify({
...coachmarkValue,
[caseId]: step,
}),
);
}
},
[caseId],
);
const handleJoyrideCallback = (data: CallBackProps) => {
const { action, index, type } = data;
if (
[EVENTS.STEP_AFTER, EVENTS.TARGET_NOT_FOUND].includes(
type as 'step:after' | 'error:target_not_found',
)
) {
const nextIndex = index + (action === ACTIONS.PREV ? -1 : 1);
// 设置已经读过的step index
setCoachmarkReadStep(index);
setStepIndex(nextIndex);
}
};
useEffect(() => {
initVisible(caseId);
return () => {
setCoachmarkReadStep(itemIndex);
};
}, [caseId, setCoachmarkReadStep, itemIndex]);
return visible ? (
<Joyride
steps={steps}
tooltipComponent={props => (
<Tooltip
{...props}
extraAction={extraAction}
showProgress={showProgress}
onClose={() => {
setVisible(false);
setCoachmarkReadStep(COACHMARK_END);
}}
/>
)}
continuous
disableOverlay
disableScrollParentFix
stepIndex={stepIndex}
callback={handleJoyrideCallback}
spotlightPadding={-6}
styles={{
options: {
zIndex: 100,
primaryColor: '#4E40E5',
},
buttonClose: {
display: 'none',
},
tooltip: {
width: 300,
padding: 8,
borderRadius: 12,
},
tooltipContent: {
padding: 0,
},
buttonBack: {
display: 'none', // 隐藏返回按钮
},
}}
floaterProps={{
styles: {
arrow: {
length: 7,
spread: 14,
margin: 40,
},
floater: {
filter: 'none',
},
},
}}
/>
) : null;
}
export { StepCard };

Some files were not shown because too many files have changed in this diff Show More