This commit is contained in:
2026-04-25 16:36:34 +08:00
commit db90e7579b
1876 changed files with 189777 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -0,0 +1,12 @@
{
"label": "Genshin Toolbox",
"label.zh-CN": "原神工具箱",
"name": "com.msgbyte.genshin",
"url": "/plugins/com.msgbyte.genshin/index.js",
"icon": "/plugins/com.msgbyte.genshin/assets/icon.jpg",
"version": "0.0.0",
"author": "msgbyte",
"description": "Add Genshin-related entertainment capabilities to Tailchat",
"description.zh-CN": "为Tailchat增加原神相关的娱乐能力",
"requireRestart": true
}

View File

@@ -0,0 +1,13 @@
{
"name": "@plugins/com.msgbyte.genshin",
"main": "src/index.tsx",
"version": "0.0.0",
"private": true,
"dependencies": {
"genshin-gacha-kit": "^1.1.0",
"html-react-parser": "^1.4.5"
},
"devDependencies": {
"react": "18.2.0"
}
}

View File

@@ -0,0 +1,36 @@
import { OfficialGachaPoolItem } from 'genshin-gacha-kit';
import React from 'react';
import styled from 'styled-components';
const ItemRoot = styled.div`
position: relative;
.text {
position: absolute;
bottom: 0;
color: #444;
font-size: 24px;
width: 100%;
text-align: center;
font-weight: bold;
line-height: 44px;
text-wrap: nowrap;
}
`;
export const GachaPoolItem: React.FC<{
items: OfficialGachaPoolItem[];
}> = React.memo((props) => {
return (
<div>
{props.items.map((i) => (
<ItemRoot key={i.item_id}>
<img src={i.item_img} />
<div className="text">{i.item_name}</div>
</ItemRoot>
))}
</div>
);
});
GachaPoolItem.displayName = 'GachaPoolItem';

View File

@@ -0,0 +1,38 @@
import { AppWishResult } from 'genshin-gacha-kit';
import React from 'react';
import { WishResultText } from './WishResultText';
interface GachaResultProps {
gachaResult: AppWishResult;
withCount: boolean;
}
export const GachaResult: React.FC<GachaResultProps> = React.memo((props) => {
const { gachaResult, withCount } = props;
return (
<div>
<div style={{ color: '#c17a4e' }}>
<WishResultText
label="5星"
items={gachaResult.ssr}
withCount={withCount}
/>
</div>
<div style={{ color: '#865cad' }}>
<WishResultText
label="4星"
items={gachaResult.sr}
withCount={withCount}
/>
</div>
<div>
<WishResultText
label="3星"
items={gachaResult.r}
withCount={withCount}
/>
</div>
</div>
);
});
GachaResult.displayName = 'GachaResult';

View File

@@ -0,0 +1,22 @@
import { ModalWrapper } from '@capital/common';
import { AppGachaItem } from 'genshin-gacha-kit';
import React from 'react';
import { GachaResult } from './GachaResult';
export const WishResultModal: React.FC<{ items: AppGachaItem[] }> = React.memo(
({ items }) => {
return (
<ModalWrapper title="抽卡结果">
<GachaResult
gachaResult={{
ssr: items.filter((i) => i.rarity === 5),
sr: items.filter((i) => i.rarity === 4),
r: items.filter((i) => i.rarity === 3),
}}
withCount={false}
/>
</ModalWrapper>
);
}
);
WishResultModal.displayName = 'WishResultModal';

View File

@@ -0,0 +1,25 @@
import { AppGachaItem } from 'genshin-gacha-kit';
import { getAppGachaItemText } from '../utils';
import React from 'react';
function pickName(item: AppGachaItem) {
return item.name;
}
export const WishResultText: React.FC<{
label: string;
items: AppGachaItem[];
withCount: boolean;
}> = React.memo(({ label, items, withCount }) => {
if (items.length === 0) {
return null;
}
return (
<span>
{label}:{' '}
{items.map(withCount ? getAppGachaItemText : pickName).join(', ')}
</span>
);
});
WishResultText.displayName = 'WishResultText';

View File

@@ -0,0 +1,55 @@
import { useAsync } from '@capital/common';
import { Divider, Button, Space } from '@capital/component';
import React from 'react';
import { util } from 'genshin-gacha-kit';
import { GenshinRichtext } from '../../components/GenshinRichtext';
import { GachaPoolItem } from './GachaPoolItem';
import { useWish } from './useWish';
import { GachaResult } from './GachaResult';
export const GachaPool: React.FC<{
gachaId: string;
}> = React.memo((props) => {
const { value: poolData } = useAsync(() => {
return util.getGachaData(props.gachaId);
}, [props.gachaId]);
const { handleGacha, gachaResult, gachaCount } = useWish(poolData);
if (!poolData) {
return <div>Loading...</div>;
}
return (
<div>
<div>{poolData.banner}</div>
<div>{poolData.date_range}</div>
<div className="gacha-pool">
<GachaPoolItem items={poolData.r5_up_items ?? []} />
<GachaPoolItem items={poolData.r4_up_items ?? []} />
</div>
<Space>
<Button type="primary" onClick={() => handleGacha(1)}>
</Button>
<Button type="primary" onClick={() => handleGacha(10)}>
</Button>
</Space>
{gachaCount > 0 && (
<div>
<div>: {gachaCount} </div>
<GachaResult gachaResult={gachaResult} withCount={true} />
</div>
)}
<Divider />
<GenshinRichtext raw={poolData.content} />
</div>
);
});
GachaPool.displayName = 'GachaPool';

View File

@@ -0,0 +1,53 @@
import { openModal, showToasts } from '@capital/common';
import {
AppWishResult,
GenshinGachaKit,
OfficialGachaPool,
util,
} from 'genshin-gacha-kit';
import React from 'react';
import { useCallback, useMemo, useState } from 'react';
import { openFullScreenVideo } from '../../utils/openFullScreenVideo';
import { wishVideoUrl } from '../consts';
import { parseResultType } from '../utils';
import { WishResultModal } from './WishResultModal';
/**
* 祈愿
* @param poolData 卡池信息
*/
export function useWish(poolData: OfficialGachaPool) {
const [gachaResult, setGachaResult] = useState<AppWishResult>({
ssr: [],
sr: [],
r: [],
});
const [gachaCount, setGachaCount] = useState<number>(0);
const gachaKit = useMemo(() => {
return poolData
? new GenshinGachaKit(util.poolStructureConverter(poolData))
: null;
}, [poolData]);
const handleGacha = useCallback(
(num) => {
if (!gachaKit) {
return;
}
const res = gachaKit.multiWish(num);
openFullScreenVideo(wishVideoUrl[parseResultType(res)]).then(() => {
// showToasts('抽卡结果: ' + res.map((item) => item.name).join(','));
openModal(<WishResultModal items={res} />);
setGachaCount(gachaKit.getCounter('total') as number);
setGachaResult(JSON.parse(JSON.stringify(gachaKit.getResult())));
});
},
[gachaKit]
);
return { handleGacha, gachaResult, gachaCount };
}

View File

@@ -0,0 +1,13 @@
/**
* 视频来源于 https://github.com/uzair-ashraf/genshin-impact-wish-simulator
*/
export const wishVideoUrl = {
'5star': 'https://tailchat.moonrailgun.com/genshin/5starwish.webm',
'4star': 'https://tailchat.moonrailgun.com/genshin/4starwish.webm',
'5star-single':
'https://tailchat.moonrailgun.com/genshin/5starwish-single.webm',
'4star-single':
'https://tailchat.moonrailgun.com/genshin/4starwish-single.webm',
'3star-single':
'https://tailchat.moonrailgun.com/genshin/3starwish-single.webm',
} as const;

View File

@@ -0,0 +1,26 @@
.plugin-genshin-panel {
width: 100%;
padding: 10px;
display: flex;
flex-direction: column;
.gacha-title {
font-weight: bold;
font-size: 22px;
margin-bottom: 10px;
}
.gacha-pool {
display: flex;
flex-direction: column;
> div {
display: flex;
margin-bottom: 10px;
> div {
margin-right: 10px;
}
}
}
}

View File

@@ -0,0 +1,47 @@
import React from 'react';
import { Translate } from '../translate';
import { OfficialGachaIndex, OfficialGachaType, util } from 'genshin-gacha-kit';
import { useAsync } from '@capital/common';
import { PillTabs, LoadingSpinner } from '@capital/component';
import { GachaPool } from './GachaPool';
import _groupBy from 'lodash/groupBy';
import './index.less';
const GenshinPanel: React.FC = React.memo(() => {
const { value: gachaList, loading } = useAsync(async () => {
const gacha = await util.getGachaIndex();
const dict = _groupBy(gacha, 'gacha_type') as unknown as Record<
keyof OfficialGachaType,
OfficialGachaIndex[]
>;
// 顺序: 角色 -> 武器 -> 常驻 -> 新手
return [
...(dict['301'] ?? []),
...(dict['302'] ?? []),
...(dict['200'] ?? []),
...(dict['100'] ?? []),
];
}, []);
return (
<div className="plugin-genshin-panel">
<div className="gacha-title">
{Translate.genshin} - {Translate.gacha}
</div>
{loading && <LoadingSpinner />}
<PillTabs
items={(gachaList ?? []).map((item) => ({
key: String(item.gacha_id),
label: `${item.gacha_name}(${item.begin_time} - ${item.end_time})`,
children: <GachaPool gachaId={item.gacha_id} />,
}))}
/>
</div>
);
});
GenshinPanel.displayName = 'GenshinPanel';
export default GenshinPanel;

View File

@@ -0,0 +1,34 @@
import type { AppGachaItem } from 'genshin-gacha-kit';
import { wishVideoUrl } from './consts';
export function getAppGachaItemText(item: AppGachaItem) {
if (item.count >= 2) {
return `${item.name}(${item.count})`;
} else {
return item.name;
}
}
export function parseResultType(
items: AppGachaItem[]
): keyof typeof wishVideoUrl {
if (items.length === 1) {
// single
const rarity = items[0].rarity;
if (rarity === 3) {
return '3star-single';
} else if (rarity === 4) {
return '4star-single';
} else if (rarity === 5) {
return '5star-single';
}
} else {
if (items.some((i) => i.rarity === 5)) {
return '5star';
} else if (items.some((i) => i.rarity === 4)) {
return '4star';
}
}
return '3star-single';
}

View File

@@ -0,0 +1,99 @@
.img-color(@color, @top: 500px) {
position: absolute;
top: @top;
left: 0;
filter: drop-shadow(0 -@top 0 @color);
}
#tailchat-app {
--plugin-genshinloadingbar-background-color: #f5f5f5;
--plugin-genshinloadingbar-prospect-color: #666666;
@loading-img-height: 62.5px;
@loading-img-width: 500px;
@mobile: 719px;
@animation: plugin-genshin-loading-bar 3.5s cubic-bezier(0.28, 0.11, 0.32, 1) infinite
forwards;
.dark {
--plugin-genshinloadingbar-background-color: #2c2b30;
--plugin-genshinloadingbar-prospect-color: #ece5d8;
}
.plugin-genshin-loading-bar {
position: absolute;
top: 50%;
left: 50%;
width: @loading-img-width;
height: @loading-img-height;
transform: translate(-50%, -50%) scale(0.8);
transition: all 0.5s;
user-select: none;
overflow: hidden;
img {
.img-color(var(--plugin-genshinloadingbar-background-color));
}
&::after {
content: '';
.img-color(var(--plugin-genshinloadingbar-prospect-color));
width: @loading-img-width;
height: @loading-img-height;
background: url('https://yuanshen.site/imgs/loading-bar.png') no-repeat
left 100%;
background-size: @loading-img-width @loading-img-height;
background-position-x: 0;
animation: @animation;
}
@media screen and (max-width: @mobile) {
// Hide when vertical screen
& {
display: none;
}
// Horizontal when screen display
@media screen and (orientation: landscape) {
& {
display: block !important;
transform: translate(-50%, -50%) scale(0.7) !important;
}
}
}
@supports not (filter: drop-shadow(0 0 0 #fff)) {
// If the browser does not support Filter
&:before {
content: 'Your browser does not support the animation';
}
}
}
@keyframes plugin-genshin-loading-bar {
0% {
width: 0;
background-size: @loading-img-width @loading-img-height;
}
16.6% {
}
33.2% {
}
49.8% {
}
66.4% {
}
83% {
}
100% {
width: @loading-img-width;
}
}
}

View File

@@ -0,0 +1,15 @@
import React from 'react';
import './GenshinLoading.less';
export const GenshinLoading: React.FC = React.memo(() => {
return (
<div
className="plugin-genshin-loading-bar"
role="presentation"
aria-hidden="true"
>
<img src="https://yuanshen.site/imgs/loading-bar.png" alt="Loading..." />
</div>
);
});
GenshinLoading.displayName = 'GenshinLoading';

View File

@@ -0,0 +1,20 @@
import React, { useMemo } from 'react';
import parser from 'html-react-parser';
/**
* 原神富文本渲染
*/
export const GenshinRichtext: React.FC<{
raw: string;
}> = React.memo(({ raw }) => {
const el = useMemo(() => {
const processedHtml = raw.replace(
/\<color=(.*?)\>(.*?)\<\/color\>/g,
'<span style="color: $1;">$2</span>'
);
return parser(processedHtml);
}, [raw]);
return <>{el}</>;
});
GenshinRichtext.displayName = 'GenshinRichtext';

View File

@@ -0,0 +1,31 @@
import { GenshinRichtext } from '../GenshinRichtext';
import { render } from '@testing-library/react';
import React from 'react';
describe('GenshinRichtext', () => {
const testRawRichtext = `「<color=#cc9046FF>陵薮</color>市朝」活动祈愿已开启。活动期间内,<color=#c93f23>限定</color>5星角色<color=#debd6c>「尘世闲游·钟离(岩)」</color>以及4星角色<color=#00BFFF>「少年春衫薄·行秋(水)」</color>、<color=#945dc4>「无冕的龙王·北斗(雷)」</color>、<color=#EC4923>「智明无邪·烟绯(火)」</color>的祈愿获取概率将<color=#c93f23>大幅提升</color></P>
<color=#c93f23>※以上角色中,限定角色不会进入「奔行世间」常驻祈愿。 </color></P>
<br />
※一般情况下所有角色或武器均适用基础概率如触发概率UP、保底等以具体规则为准。 </P>
<br />
〓祈愿规则〓</P>
【5星物品】</P>
在本期「<color=#cc9046FF>陵薮</color>市朝」活动祈愿中5星角色祈愿的基础概率为<color=#c93f23>0.600%</color>,综合概率(含保底)为<color=#c93f23>1.600%</color>,最多<color=#c93f23>90</color>次祈愿必定能通过保底获取5星角色。</P>
当祈愿获取到5星角色时有<color=#c93f23>50.000%</color>的概率为本期5星UP角色<color=#debd6c>「尘世闲游·钟离(岩)」</color>。如果本次祈愿获取的5星角色非本期5星UP角色下次祈愿获取的5星角色<color=#c93f23>必定</color>为本期5星UP角色。</P>
【4星物品】</P>
在本期「<color=#cc9046FF>陵薮</color>市朝」活动祈愿中4星物品祈愿的基础概率为<color=#c93f23>5.100%</color>4星角色祈愿的基础概率为<color=#c93f23>2.550%</color>4星武器祈愿的基础概率为<color=#c93f23>2.550%</color>4星物品祈愿的综合概率含保底为<color=#c93f23>13.000%</color>。最多<color=#c93f23>10</color>次祈愿必定能通过保底获取4星或以上物品通过保底获取4星物品的概率为<color=#c93f23>99.400%</color>获取5星物品的概率为<color=#c93f23>0.600%</color>。</P>
当祈愿获取到4星物品时有<color=#c93f23>50.000%</color>的概率为本期4星UP角色<color=#00BFFF>「少年春衫薄·行秋(水)」</color>、<color=#945dc4>「无冕的龙王·北斗(雷)」</color>、<color=#EC4923>「智明无邪·烟绯(火)」</color>中的一个。如果本次祈愿获取的4星物品非本期4星UP角色下次祈愿获取的4星物品<color=#c93f23>必定</color>为本期4星UP角色。当祈愿获取到4星UP物品时每个本期4星UP角色的获取概率均等。</P>
<br />
获得4星武器时会同时获得2个<color=#bd6932>无主的星辉</color>作为副产物获得3星武器时会同时获得15个<color=#a256e1>无主的星尘</color>作为副产物。</P>
<br />
〓若获得重复角色〓</P>
无论通过何种方式包含但不限于祈愿、商城兑换、系统赠送等第2~7次获得相同5星角色时每次将转化为1个<color=#a256e1>对应角色的命星</color>和10个<color=#bd6932>无主的星辉</color>第8次及之后获得将仅转化为25个<color=#bd6932>无主的星辉</color>。</P>
无论通过何种方式包含但不限于祈愿、商城兑换、系统赠送等第2~7次获得相同4星角色时每次将转化为1个<color=#a256e1>对应角色的命星</color>和2个<color=#bd6932>无主的星辉</color>第8次及之后获得将仅转化为5个<color=#bd6932>无主的星辉</color>。</P>
<br />
※本祈愿属于「角色活动祈愿」,「角色活动祈愿」和「角色活动祈愿-2」的祈愿次数保底完全共享会一直共同累计在「角色活动祈愿」和「角色活动祈愿-2」中与其他祈愿的祈愿次数保底相互独立计算互不影响。</P>`;
test('test', () => {
const wrapper = render(<GenshinRichtext raw={testRawRichtext} />);
expect(wrapper.container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,280 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GenshinRichtext test 1`] = `
<div>
<span
style="color: rgba(204, 144, 70, 1);"
>
陵薮
</span>
市朝」活动祈愿已开启。活动期间内,
<span
style="color: rgb(201, 63, 35);"
>
限定
</span>
5星角色
<span
style="color: rgb(222, 189, 108);"
>
「尘世闲游·钟离(岩)」
</span>
以及4星角色
<span
style="color: rgb(0, 191, 255);"
>
「少年春衫薄·行秋(水)」
</span>
<span
style="color: rgb(148, 93, 196);"
>
「无冕的龙王·北斗(雷)」
</span>
<span
style="color: rgb(236, 73, 35);"
>
「智明无邪·烟绯(火)」
</span>
的祈愿获取概率将
<span
style="color: rgb(201, 63, 35);"
>
大幅提升
</span>
<p />
<span
style="color: rgb(201, 63, 35);"
>
※以上角色中,限定角色不会进入「奔行世间」常驻祈愿。
</span>
<p />
<br />
※一般情况下所有角色或武器均适用基础概率如触发概率UP、保底等以具体规则为准。
<p />
<br />
〓祈愿规则〓
<p />
【5星物品】
<p />
在本期「
<span
style="color: rgba(204, 144, 70, 1);"
>
陵薮
</span>
市朝」活动祈愿中5星角色祈愿的基础概率为
<span
style="color: rgb(201, 63, 35);"
>
0.600%
</span>
,综合概率(含保底)为
<span
style="color: rgb(201, 63, 35);"
>
1.600%
</span>
,最多
<span
style="color: rgb(201, 63, 35);"
>
90
</span>
次祈愿必定能通过保底获取5星角色。
<p />
当祈愿获取到5星角色时
<span
style="color: rgb(201, 63, 35);"
>
50.000%
</span>
的概率为本期5星UP角色
<span
style="color: rgb(222, 189, 108);"
>
「尘世闲游·钟离(岩)」
</span>
。如果本次祈愿获取的5星角色非本期5星UP角色下次祈愿获取的5星角色
<span
style="color: rgb(201, 63, 35);"
>
必定
</span>
为本期5星UP角色。
<p />
【4星物品】
<p />
在本期「
<span
style="color: rgba(204, 144, 70, 1);"
>
陵薮
</span>
市朝」活动祈愿中4星物品祈愿的基础概率为
<span
style="color: rgb(201, 63, 35);"
>
5.100%
</span>
4星角色祈愿的基础概率为
<span
style="color: rgb(201, 63, 35);"
>
2.550%
</span>
4星武器祈愿的基础概率为
<span
style="color: rgb(201, 63, 35);"
>
2.550%
</span>
4星物品祈愿的综合概率含保底
<span
style="color: rgb(201, 63, 35);"
>
13.000%
</span>
。最多
<span
style="color: rgb(201, 63, 35);"
>
10
</span>
次祈愿必定能通过保底获取4星或以上物品通过保底获取4星物品的概率为
<span
style="color: rgb(201, 63, 35);"
>
99.400%
</span>
获取5星物品的概率为
<span
style="color: rgb(201, 63, 35);"
>
0.600%
</span>
<p />
当祈愿获取到4星物品时
<span
style="color: rgb(201, 63, 35);"
>
50.000%
</span>
的概率为本期4星UP角色
<span
style="color: rgb(0, 191, 255);"
>
「少年春衫薄·行秋(水)」
</span>
<span
style="color: rgb(148, 93, 196);"
>
「无冕的龙王·北斗(雷)」
</span>
<span
style="color: rgb(236, 73, 35);"
>
「智明无邪·烟绯(火)」
</span>
中的一个。如果本次祈愿获取的4星物品非本期4星UP角色下次祈愿获取的4星物品
<span
style="color: rgb(201, 63, 35);"
>
必定
</span>
为本期4星UP角色。当祈愿获取到4星UP物品时每个本期4星UP角色的获取概率均等。
<p />
<br />
获得4星武器时会同时获得2个
<span
style="color: rgb(189, 105, 50);"
>
无主的星辉
</span>
作为副产物获得3星武器时会同时获得15个
<span
style="color: rgb(162, 86, 225);"
>
无主的星尘
</span>
作为副产物。
<p />
<br />
〓若获得重复角色〓
<p />
无论通过何种方式包含但不限于祈愿、商城兑换、系统赠送等第2~7次获得相同5星角色时每次将转化为1个
<span
style="color: rgb(162, 86, 225);"
>
对应角色的命星
</span>
和10个
<span
style="color: rgb(189, 105, 50);"
>
无主的星辉
</span>
第8次及之后获得将仅转化为25个
<span
style="color: rgb(189, 105, 50);"
>
无主的星辉
</span>
<p />
无论通过何种方式包含但不限于祈愿、商城兑换、系统赠送等第2~7次获得相同4星角色时每次将转化为1个
<span
style="color: rgb(162, 86, 225);"
>
对应角色的命星
</span>
和2个
<span
style="color: rgb(189, 105, 50);"
>
无主的星辉
</span>
第8次及之后获得将仅转化为5个
<span
style="color: rgb(189, 105, 50);"
>
无主的星辉
</span>
<p />
<br />
※本祈愿属于「角色活动祈愿」,「角色活动祈愿」和「角色活动祈愿-2」的祈愿次数保底完全共享会一直共同累计在「角色活动祈愿」和「角色活动祈愿-2」中与其他祈愿的祈愿次数保底相互独立计算互不影响。
<p />
</div>
`;

View File

@@ -0,0 +1,10 @@
import { regCustomPanel, Loadable } from '@capital/common';
import { Translate } from './translate';
regCustomPanel({
position: 'personal',
icon: 'akar-icons:game-controller',
name: 'com.msgbyte.genshin/genshinPanel',
label: Translate.genshin,
render: Loadable(() => import('./GenshinPanel')),
});

View File

@@ -0,0 +1,6 @@
import { localTrans } from '@capital/common';
export const Translate = {
genshin: localTrans({ 'zh-CN': '原神', 'en-US': 'Genshin' }),
gacha: localTrans({ 'zh-CN': '抽卡', 'en-US': 'Gacha' }),
};

View File

@@ -0,0 +1,34 @@
/**
* 打开一个全屏的video
*/
export async function openFullScreenVideo(src: string) {
return new Promise<void>((resolve, reject) => {
const containerEl = document.createElement('div');
containerEl.style.position = 'fixed';
containerEl.style.height = '100vh';
containerEl.style.width = '100vw';
containerEl.style.left = '0px';
containerEl.style.top = '0px';
containerEl.style.zIndex = '99999';
containerEl.style.backgroundColor = '#000000';
containerEl.style.display = 'flex';
containerEl.style.alignItems = 'center';
containerEl.style.justifyContent = 'center';
const videoEl = document.createElement('video');
videoEl.src = src;
videoEl.autoplay = true;
videoEl.addEventListener('ended', () => {
containerEl.removeChild(videoEl);
document.body.removeChild(containerEl);
resolve();
});
videoEl.addEventListener('error', () => {
reject();
});
containerEl.appendChild(videoEl);
document.body.appendChild(containerEl);
});
}

View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"esModuleInterop": true,
"jsx": "react",
"paths": {
"@capital/*": ["../../src/plugin/*"],
}
}
}