feat: support customize subscription usage cover relay api

This commit is contained in:
Zhang Minghan 2024-02-14 09:09:05 +08:00
parent 86198ea5fa
commit 304e071718
13 changed files with 69 additions and 10 deletions

View File

@ -22,6 +22,7 @@ export type SiteInfo = {
auth_footer: boolean; auth_footer: boolean;
article: string[]; article: string[];
generation: string[]; generation: string[];
relay_plan: boolean;
}; };
export async function getSiteInfo(): Promise<SiteInfo> { export async function getSiteInfo(): Promise<SiteInfo> {
@ -43,6 +44,7 @@ export async function getSiteInfo(): Promise<SiteInfo> {
mail: false, mail: false,
article: [], article: [],
generation: [], generation: [],
relay_plan: false,
}; };
} }
} }

View File

@ -32,6 +32,7 @@ export type SearchState = {
export type SiteState = { export type SiteState = {
close_register: boolean; close_register: boolean;
relay_plan: boolean;
quota: number; quota: number;
buy_link: string; buy_link: string;
announcement: string; announcement: string;
@ -118,6 +119,7 @@ export const initialSystemState: SystemProps = {
file: "", file: "",
}, },
site: { site: {
relay_plan: false,
close_register: false, close_register: false,
quota: 0, quota: 0,
buy_link: "", buy_link: "",

View File

@ -22,7 +22,7 @@ import {
openDialog as openQuotaDialog, openDialog as openQuotaDialog,
dialogSelector as quotaDialogSelector, dialogSelector as quotaDialogSelector,
} from "@/store/quota.ts"; } from "@/store/quota.ts";
import { Calendar } from "lucide-react"; import { Calendar, Info } from "lucide-react";
import { useEffectAsync } from "@/utils/hook.ts"; import { useEffectAsync } from "@/utils/hook.ts";
import { selectAuthenticated } from "@/store/auth.ts"; import { selectAuthenticated } from "@/store/auth.ts";
import SubscriptionUsage from "@/components/home/subscription/SubscriptionUsage.tsx"; import SubscriptionUsage from "@/components/home/subscription/SubscriptionUsage.tsx";
@ -38,6 +38,7 @@ import {
import { cn } from "@/components/ui/lib/utils.ts"; import { cn } from "@/components/ui/lib/utils.ts";
import { Badge } from "@/components/ui/badge.tsx"; import { Badge } from "@/components/ui/badge.tsx";
import { subscriptionDataSelector } from "@/store/globals.ts"; import { subscriptionDataSelector } from "@/store/globals.ts";
import { infoRelayPlanSelector } from "@/store/info.ts";
type PlanItemProps = { type PlanItemProps = {
level: number; level: number;
@ -104,6 +105,8 @@ function SubscriptionDialog() {
const subscriptionData = useSelector(subscriptionDataSelector); const subscriptionData = useSelector(subscriptionDataSelector);
const relayPlan = useSelector(infoRelayPlanSelector);
const plan = useMemo( const plan = useMemo(
() => getPlan(subscriptionData, level), () => getPlan(subscriptionData, level),
[subscriptionData, level], [subscriptionData, level],
@ -139,6 +142,14 @@ function SubscriptionDialog() {
> >
{t("sub.quota-link")} {t("sub.quota-link")}
</p> </p>
{!relayPlan && (
<div
className={`sub-tip flex flex-row items-center w-max mx-auto select-none`}
>
<Info className={`h-4 w-4 mr-1.5`} />
{t("sub.plan-not-support-relay")}
</div>
)}
{subscription && ( {subscription && (
<div className={`sub-row`}> <div className={`sub-row`}>
<SubscriptionUsage <SubscriptionUsage

View File

@ -7,6 +7,7 @@ export type InfoForm = {
auth_footer: boolean; auth_footer: boolean;
article: string[]; article: string[];
generation: string[]; generation: string[];
relay_plan: boolean;
}; };
export const infoEvent = new EventCommitter<InfoForm>({ export const infoEvent = new EventCommitter<InfoForm>({

View File

@ -198,6 +198,7 @@
"title": "订阅", "title": "订阅",
"disable": "本站订阅功能已被关闭", "disable": "本站订阅功能已被关闭",
"quota-link": "寻求弹性计费?购买点数", "quota-link": "寻求弹性计费?购买点数",
"plan-not-support-relay": "站点订阅配额不涵盖中转 API , 中转 API 请使用弹性计费点数",
"subscription-link": "寻求固定计费?订阅计划", "subscription-link": "寻求固定计费?订阅计划",
"dialog-title": "订阅计划", "dialog-title": "订阅计划",
"free": "免费版", "free": "免费版",
@ -658,6 +659,8 @@
"searchTip": "DuckDuckGo 搜索接入点,如不填写自动使用 WebPilot 和 New Bing 逆向进行搜索功能(速度较慢)。\nDuckDuckGo API 项目搭建:[duckduckgo-api](https://github.com/binjie09/duckduckgo-api)。", "searchTip": "DuckDuckGo 搜索接入点,如不填写自动使用 WebPilot 和 New Bing 逆向进行搜索功能(速度较慢)。\nDuckDuckGo API 项目搭建:[duckduckgo-api](https://github.com/binjie09/duckduckgo-api)。",
"closeRegistration": "暂停注册", "closeRegistration": "暂停注册",
"closeRegistrationTip": "暂停注册,关闭后新用户将无法注册", "closeRegistrationTip": "暂停注册,关闭后新用户将无法注册",
"relayPlan": "订阅配额支持中转 API",
"relayPlanTip": "订阅配额支持中转 API开启后中转 API 计费会优先考虑使用用户订阅配额\n提示订阅为次数配额对 Token 计费的模型可能会影响成本)",
"quota": "用户初始点数", "quota": "用户初始点数",
"quotaTip": "用户注册后赠送的点数", "quotaTip": "用户注册后赠送的点数",
"buyLink": "购买链接", "buyLink": "购买链接",

View File

@ -193,7 +193,8 @@
"plan-usage": "{{name}} uses {{times}} times per month", "plan-usage": "{{name}} uses {{times}} times per month",
"plan-tip": "Callable Model", "plan-tip": "Callable Model",
"disable": "This site's subscription feature has been turned off", "disable": "This site's subscription feature has been turned off",
"plan-unlimited-usage": "{{name}} has unlimited uses" "plan-unlimited-usage": "{{name}} has unlimited uses",
"plan-not-support-relay": "Site subscription quota does not cover staging API, please use flexible billing credits for staging API"
}, },
"cancel": "Cancel", "cancel": "Cancel",
"confirm": "Confirm", "confirm": "Confirm",
@ -531,7 +532,9 @@
"closeRegistrationTip": "Registration is paused, new users will not be able to register after closing", "closeRegistrationTip": "Registration is paused, new users will not be able to register after closing",
"footer": "Footer Infor", "footer": "Footer Infor",
"footerPlaceholder": "Please enter footer information (Markdown/HTML format supported)", "footerPlaceholder": "Please enter footer information (Markdown/HTML format supported)",
"authFooter": "Hide footer after login" "authFooter": "Hide footer after login",
"relayPlan": "Subscription Quota Support Staging API",
"relayPlanTip": "Subscription quota supports the transit API, after opening the transit API billing will give priority to the use of user subscription quota\n(Tip: Subscription is a quota of times, the model of billing for tokens may affect the cost)"
}, },
"user": "User Management", "user": "User Management",
"invitation-code": "Invitation Code", "invitation-code": "Invitation Code",

View File

@ -193,7 +193,8 @@
"plan-usage": "{{name}}は月に{{times}}回使用", "plan-usage": "{{name}}は月に{{times}}回使用",
"plan-tip": "呼び出し可能なモデル", "plan-tip": "呼び出し可能なモデル",
"disable": "このサイトのサブスクリプション機能はオフになっています", "disable": "このサイトのサブスクリプション機能はオフになっています",
"plan-unlimited-usage": "{{name}}は無制限に使用できます" "plan-unlimited-usage": "{{name}}は無制限に使用できます",
"plan-not-support-relay": "サイトサブスクリプションクォータはステージングAPIをカバーしていません。ステージングAPIに柔軟な請求クレジットを使用してください"
}, },
"cancel": "キャンセル", "cancel": "キャンセル",
"confirm": "確認", "confirm": "確認",
@ -531,7 +532,9 @@
"closeRegistrationTip": "登録が一時停止されています。新規ユーザーは閉じると登録できなくなります", "closeRegistrationTip": "登録が一時停止されています。新規ユーザーは閉じると登録できなくなります",
"footer": "フッター情報", "footer": "フッター情報",
"footerPlaceholder": "フッター情報を入力してください( Markdown/HTML形式に対応", "footerPlaceholder": "フッター情報を入力してください( Markdown/HTML形式に対応",
"authFooter": "ログイン後にフッターを非表示にする" "authFooter": "ログイン後にフッターを非表示にする",
"relayPlan": "サブスクリプションクォータサポートステージングAPI",
"relayPlanTip": "サブスクリプションクォータはトランジットAPIをサポートしています。トランジットAPI請求を開いた後、ユーザーサブスクリプションクォータの使用が優先されます\nヒントサブスクリプションは時間のクォータであり、トークンの請求モデルはコストに影響する可能性があります"
}, },
"user": "ユーザー管理", "user": "ユーザー管理",
"invitation-code": "招待コード", "invitation-code": "招待コード",

View File

@ -193,7 +193,8 @@
"plan-usage": "{{name}} использует {{times}} раз в месяц", "plan-usage": "{{name}} использует {{times}} раз в месяц",
"plan-tip": "Вызываемая модель", "plan-tip": "Вызываемая модель",
"disable": "Функция подписки на этом сайте отключена", "disable": "Функция подписки на этом сайте отключена",
"plan-unlimited-usage": "{{name}} имеет неограниченное количество пользователей" "plan-unlimited-usage": "{{name}} имеет неограниченное количество пользователей",
"plan-not-support-relay": "Квота подписки на сайт не распространяется на промежуточный API, пожалуйста, используйте гибкие биллинговые кредиты для промежуточного API"
}, },
"cancel": "Отмена", "cancel": "Отмена",
"confirm": "Подтвердить", "confirm": "Подтвердить",
@ -531,7 +532,9 @@
"closeRegistrationTip": "Регистрация приостановлена, новые пользователи не смогут зарегистрироваться после закрытия", "closeRegistrationTip": "Регистрация приостановлена, новые пользователи не смогут зарегистрироваться после закрытия",
"footer": "Информация нижнего колонтитула", "footer": "Информация нижнего колонтитула",
"footerPlaceholder": "Пожалуйста, введите информацию нижнего колонтитула (поддерживается формат Markdown/HTML)", "footerPlaceholder": "Пожалуйста, введите информацию нижнего колонтитула (поддерживается формат Markdown/HTML)",
"authFooter": "Скрыть нижний колонтитул после входа в систему" "authFooter": "Скрыть нижний колонтитул после входа в систему",
"relayPlan": "API промежуточной поддержки квот подписки",
"relayPlanTip": "Квота подписки поддерживает транзитный API, после открытия транзитного API биллинг будет отдавать приоритет использованию пользовательской квоты подписки\n(Совет: Подписка - это квота раз, модель биллинга для токенов может повлиять на стоимость)"
}, },
"user": "Управление пользователями", "user": "Управление пользователями",
"invitation-code": "Код приглашения", "invitation-code": "Код приглашения",

View File

@ -482,6 +482,21 @@ function Site({ data, dispatch, onChange }: CompProps<SiteState>) {
}} }}
/> />
</ParagraphItem> </ParagraphItem>
<ParagraphItem>
<Label>
{t("admin.system.relayPlan")}
<Tips
className={`inline-block`}
content={t("admin.system.relayPlanTip")}
/>
</Label>
<Switch
checked={data.relay_plan}
onCheckedChange={(value) => {
dispatch({ type: "update:site.relay_plan", value });
}}
/>
</ParagraphItem>
<ParagraphItem> <ParagraphItem>
<Label className={`flex flex-row items-center`}> <Label className={`flex flex-row items-center`}>
{t("admin.system.quota")} {t("admin.system.quota")}

View File

@ -19,6 +19,7 @@ export const infoSlice = createSlice({
generation: getArrayMemory("generation"), generation: getArrayMemory("generation"),
footer: getMemory("footer"), footer: getMemory("footer"),
auth_footer: getBooleanMemory("auth_footer", false), auth_footer: getBooleanMemory("auth_footer", false),
relay_plan: getBooleanMemory("relay_plan", false),
} as InfoForm, } as InfoForm,
reducers: { reducers: {
setForm: (state, action) => { setForm: (state, action) => {
@ -29,6 +30,7 @@ export const infoSlice = createSlice({
state.generation = form.generation ?? []; state.generation = form.generation ?? [];
state.footer = form.footer ?? ""; state.footer = form.footer ?? "";
state.auth_footer = form.auth_footer ?? false; state.auth_footer = form.auth_footer ?? false;
state.relay_plan = form.relay_plan ?? false;
setBooleanMemory("mail", state.mail); setBooleanMemory("mail", state.mail);
setMemory("contact", state.contact); setMemory("contact", state.contact);
@ -36,6 +38,7 @@ export const infoSlice = createSlice({
setArrayMemory("generation", state.generation); setArrayMemory("generation", state.generation);
setMemory("footer", state.footer); setMemory("footer", state.footer);
setBooleanMemory("auth_footer", state.auth_footer); setBooleanMemory("auth_footer", state.auth_footer);
setBooleanMemory("relay_plan", state.relay_plan);
}, },
}, },
}); });
@ -56,3 +59,5 @@ export const infoFooterSelector = (state: RootState): string =>
state.info.footer; state.info.footer;
export const infoAuthFooterSelector = (state: RootState): boolean => export const infoAuthFooterSelector = (state: RootState): boolean =>
state.info.auth_footer; state.info.auth_footer;
export const infoRelayPlanSelector = (state: RootState): boolean =>
state.info.relay_plan;

View File

@ -21,6 +21,7 @@ type ApiInfo struct {
Mail bool `json:"mail"` Mail bool `json:"mail"`
Article []string `json:"article"` Article []string `json:"article"`
Generation []string `json:"generation"` Generation []string `json:"generation"`
RelayPlan bool `json:"relay_plan"`
} }
type generalState struct { type generalState struct {
@ -33,6 +34,7 @@ type generalState struct {
type siteState struct { type siteState struct {
CloseRegister bool `json:"close_register" mapstructure:"closeregister"` CloseRegister bool `json:"close_register" mapstructure:"closeregister"`
RelayPlan bool `json:"relay_plan" mapstructure:"relayplan"`
Quota float64 `json:"quota" mapstructure:"quota"` Quota float64 `json:"quota" mapstructure:"quota"`
BuyLink string `json:"buy_link" mapstructure:"buylink"` BuyLink string `json:"buy_link" mapstructure:"buylink"`
Announcement string `json:"announcement" mapstructure:"announcement"` Announcement string `json:"announcement" mapstructure:"announcement"`
@ -119,6 +121,7 @@ func (c *SystemConfig) AsInfo() ApiInfo {
Mail: c.IsMailValid(), Mail: c.IsMailValid(),
Article: c.Common.Article, Article: c.Common.Article,
Generation: c.Common.Generation, Generation: c.Common.Generation,
RelayPlan: c.Site.RelayPlan,
} }
} }
@ -256,3 +259,7 @@ func (c *SystemConfig) GetCacheAcceptedSize() int64 {
func (c *SystemConfig) IsCloseRegister() bool { func (c *SystemConfig) IsCloseRegister() bool {
return c.Site.CloseRegister return c.Site.CloseRegister
} }
func (c *SystemConfig) SupportRelayPlan() bool {
return c.Site.RelayPlan
}

View File

@ -16,6 +16,10 @@ import (
"time" "time"
) )
func supportRelayPlan() bool {
return channel.SystemInstance.SupportRelayPlan()
}
func ChatRelayAPI(c *gin.Context) { func ChatRelayAPI(c *gin.Context) {
username := utils.GetUserFromContext(c) username := utils.GetUserFromContext(c)
if username == "" { if username == "" {
@ -61,9 +65,9 @@ func ChatRelayAPI(c *gin.Context) {
} }
if form.Stream { if form.Stream {
sendStreamTranshipmentResponse(c, form, messages, id, created, user, false) sendStreamTranshipmentResponse(c, form, messages, id, created, user, supportRelayPlan())
} else { } else {
sendTranshipmentResponse(c, form, messages, id, created, user, false) sendTranshipmentResponse(c, form, messages, id, created, user, supportRelayPlan())
} }
} }

View File

@ -54,7 +54,7 @@ func ImagesRelayAPI(c *gin.Context) {
return return
} }
createRelayImageObject(c, form, prompt, created, user, false) createRelayImageObject(c, form, prompt, created, user, supportRelayPlan())
} }
func getImageProps(form RelayImageForm, messages []globals.Message, buffer *utils.Buffer) *adapter.ChatProps { func getImageProps(form RelayImageForm, messages []globals.Message, buffer *utils.Buffer) *adapter.ChatProps {