mirror of
https://github.com/coaidev/coai.git
synced 2025-05-28 17:30:15 +09:00
feat: support apply template pricing in admin charge settings
This commit is contained in:
parent
40cc84a863
commit
7432ae6714
@ -33,13 +33,15 @@ _🚀 **Next Generation AI One-Stop Solution**_
|
||||

|
||||
6. **原生支持文件解析**, 不依赖模型, 支持 pdf, docx, pptx, xlsx, 音频 *(需配置azure speech)*, 图片 *(需 vision 模型)* 等格式上传, 可直接从消息框 Ctrl+V 复制文件, 同时支持操作弹出窗口的点击上传和拖拽上传, 支持多文件管理 _(详情参考项目 [chatnio-blob-service](https://github.com/Deeptrain-Community/chatnio-blob-service))_
|
||||

|
||||
7. 支持 DuckDuckGo / New Bing 联网搜索功能 (不依赖模型 Function Calling) _(详情参考并自行一键搭建项目 [duckduckgo-api](https://github.com/binjie09/duckduckgo-api), 感谢作者 [@binjie09](https://github.com/binjie09))_
|
||||
7. 支持 DuckDuckGo 联网搜索功能 (不依赖模型 Function Calling) _(详情参考项目 [duckduckgo-api](https://github.com/binjie09/duckduckgo-api), 需自行搭建并在系统设置中联网设置中设置, 感谢作者 [@binjie09](https://github.com/binjie09))_
|
||||

|
||||
8. **大文本全屏编辑支持**, 支持 *纯文本编辑*, *编辑预览模式*, *纯预览模式* 三种模式切换
|
||||

|
||||
9. **模型市场功能**, 支持模型搜索, 支持顺序拖拽, 包含模型名称, 模型描述, 模型 Tags, 模型头像, 自动绑定模型的价格设置, 自动绑定订阅配额 (包含在订阅的模型将标有 *plus* 标签)
|
||||

|
||||
10. (自定义预设开发中) **支持预设功能**, 支持自定义预设和云端同步功能, 支持预设克隆, 预设头像设置, 预设简介设置
|
||||
10. **支持预设功能**, 支持 ***自定义预设*** 和 **_云端同步_** 功能, 支持预设克隆, 预设头像设置, 预设简介设置
|
||||

|
||||

|
||||
11. **支持站点公告** (公告弹窗显示, 需确认), 支持公告通知 (右侧通知显示, 无需确认)
|
||||
12. **支持主题切换**, 明亮 / 暗黑显示主题切换, 自动保存主题偏好, 自动获取默认系统主题
|
||||
13. **支持偏好设置**, i18n 多语言支持, 自定义最大携带会话数, 最大回复 tokens 数, 模型参数自定义, 重置设置等
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"chat/utils"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/spf13/viper"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -49,18 +48,10 @@ func CreateGenerationWorker(c *gin.Context, user *auth.User, model string, promp
|
||||
titles := ParseTitle(title)
|
||||
result := make(chan Response, len(titles))
|
||||
|
||||
if viper.GetBool("accept_concurrent") {
|
||||
for _, name := range titles {
|
||||
go func(title string) {
|
||||
result <- GenerateArticle(c, user, model, hash, title, prompt, enableWeb)
|
||||
}(name)
|
||||
}
|
||||
} else {
|
||||
go func() {
|
||||
for _, title := range titles {
|
||||
result <- GenerateArticle(c, user, model, hash, title, prompt, enableWeb)
|
||||
}
|
||||
}()
|
||||
for _, name := range titles {
|
||||
go func(title string) {
|
||||
result <- GenerateArticle(c, user, model, hash, title, prompt, enableWeb)
|
||||
}(name)
|
||||
}
|
||||
|
||||
return len(titles), result
|
||||
|
@ -209,6 +209,10 @@ export const ChannelInfos: Record<string, ChannelInfo> = {
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultChannelModels: string[] = getUniqueList(
|
||||
Object.values(ChannelInfos).flatMap((info) => info.models),
|
||||
);
|
||||
|
||||
export const channelModels: string[] = getUniqueList(
|
||||
Object.values(ChannelInfos).flatMap((info) => info.models),
|
||||
);
|
||||
|
@ -4,6 +4,7 @@ export const nonBilling = "non-billing";
|
||||
|
||||
export const defaultChargeType = tokenBilling;
|
||||
export const chargeTypes = [nonBilling, timesBilling, tokenBilling];
|
||||
export type ChargeType = (typeof chargeTypes)[number];
|
||||
|
||||
export type ChargeBaseProps = {
|
||||
type: string;
|
||||
|
231
app/src/admin/datasets/charge.ts
Normal file
231
app/src/admin/datasets/charge.ts
Normal file
@ -0,0 +1,231 @@
|
||||
import {
|
||||
ChargeProps,
|
||||
ChargeType,
|
||||
timesBilling,
|
||||
tokenBilling,
|
||||
} from "@/admin/charge.ts";
|
||||
|
||||
export enum Currency {
|
||||
CNY = "CNY",
|
||||
USD = "USD",
|
||||
}
|
||||
|
||||
export type PricingItem = {
|
||||
models: string[];
|
||||
input?: number;
|
||||
output: number;
|
||||
currency?: Currency;
|
||||
billing_type?: ChargeType;
|
||||
};
|
||||
|
||||
export type PricingDataset = PricingItem[];
|
||||
|
||||
export const pricing: PricingDataset = [
|
||||
{
|
||||
models: [
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-3.5-turbo-0301",
|
||||
"gpt-3.5-turbo-0613",
|
||||
"gpt-3.5-turbo-instruct",
|
||||
],
|
||||
input: 0.0015,
|
||||
output: 0.002,
|
||||
},
|
||||
{
|
||||
models: ["gpt-3.5-turbo-1106"],
|
||||
input: 0.001,
|
||||
output: 0.002,
|
||||
},
|
||||
{
|
||||
models: ["gpt-3.5-turbo-0125"],
|
||||
input: 0.0005,
|
||||
output: 0.0015,
|
||||
},
|
||||
{
|
||||
models: [
|
||||
"gpt-3.5-turbo-16k",
|
||||
"gpt-3.5-turbo-16k-0301",
|
||||
"gpt-3.5-turbo-16k-0613",
|
||||
],
|
||||
input: 0.003,
|
||||
output: 0.004,
|
||||
},
|
||||
{
|
||||
models: ["gpt-4", "gpt-4-0314", "gpt-4-0613"],
|
||||
input: 0.03,
|
||||
output: 0.06,
|
||||
},
|
||||
{
|
||||
models: [
|
||||
"gpt-4-1106-preview",
|
||||
"gpt-4-0125-preview",
|
||||
"gpt-4-turbo-preview",
|
||||
"gpt-4-1106-vision-preview",
|
||||
"gpt-4-vision-preview",
|
||||
],
|
||||
input: 0.01,
|
||||
output: 0.03,
|
||||
},
|
||||
{
|
||||
models: ["gpt-4-32k", "gpt-4-32k-0314", "gpt-4-32k-0613"],
|
||||
input: 0.06,
|
||||
output: 0.12,
|
||||
},
|
||||
{
|
||||
models: ["dalle", "dall-e-2"], // dall-e-2 512x512 size
|
||||
output: 0.018,
|
||||
billing_type: timesBilling,
|
||||
},
|
||||
{
|
||||
models: ["dall-e-3"], // dall-e-3 HD 1024x1024 size
|
||||
output: 0.08,
|
||||
billing_type: timesBilling,
|
||||
},
|
||||
{
|
||||
models: [
|
||||
"claude-1",
|
||||
"claude-1-100k",
|
||||
"claude-1.2",
|
||||
"claude-1.3",
|
||||
"claude-instant",
|
||||
"claude-slack",
|
||||
],
|
||||
input: 0.0008,
|
||||
output: 0.0024,
|
||||
// input: $0.8/1m tokens, output: $2.4/1m tokens
|
||||
},
|
||||
{
|
||||
models: ["claude-2", "claude-2-100k", "claude-2.1"],
|
||||
input: 0.008,
|
||||
output: 0.024,
|
||||
},
|
||||
{
|
||||
models: ["midjourney"],
|
||||
output: 0.1,
|
||||
currency: Currency.CNY,
|
||||
billing_type: timesBilling,
|
||||
},
|
||||
{
|
||||
models: ["midjourney-fast"],
|
||||
output: 0.2,
|
||||
currency: Currency.CNY,
|
||||
billing_type: timesBilling,
|
||||
},
|
||||
{
|
||||
models: ["midjourney-turbo"],
|
||||
output: 0.5,
|
||||
currency: Currency.CNY,
|
||||
billing_type: timesBilling,
|
||||
},
|
||||
{
|
||||
models: ["spark-desk-v1.5"],
|
||||
input: 0.15,
|
||||
output: 0.15,
|
||||
currency: Currency.CNY,
|
||||
},
|
||||
{
|
||||
models: ["spark-desk-v2", "spark-desk-v3"],
|
||||
input: 0.3,
|
||||
output: 0.3,
|
||||
currency: Currency.CNY,
|
||||
},
|
||||
{
|
||||
models: ["zhipu-chatglm-lite", "zhipu-chatglm-std", "zhipu-chatglm-turbo"],
|
||||
input: 0.05,
|
||||
output: 0.05,
|
||||
currency: Currency.CNY,
|
||||
},
|
||||
{
|
||||
models: ["zhipu-chatglm-pro"],
|
||||
input: 0.1,
|
||||
output: 0.1,
|
||||
currency: Currency.CNY,
|
||||
},
|
||||
{
|
||||
models: ["qwen-plus", "qwen-plus-net"],
|
||||
input: 0.2,
|
||||
output: 0.2,
|
||||
currency: Currency.CNY,
|
||||
},
|
||||
{
|
||||
models: ["qwen-turbo", "qwen-turbo-net"],
|
||||
input: 0.08,
|
||||
output: 0.08,
|
||||
currency: Currency.CNY,
|
||||
},
|
||||
{
|
||||
models: ["chat-bison-001"], // free marked as $0.001
|
||||
output: 0.001,
|
||||
},
|
||||
{
|
||||
models: ["gemini-pro", "gemini-pro-vision"],
|
||||
input: 0.000125,
|
||||
output: 0.000375,
|
||||
},
|
||||
{
|
||||
models: ["hunyuan"],
|
||||
input: 1,
|
||||
output: 1,
|
||||
currency: Currency.CNY,
|
||||
},
|
||||
{
|
||||
models: ["360-gpt-v9"],
|
||||
input: 0.12,
|
||||
output: 0.12,
|
||||
currency: Currency.CNY,
|
||||
},
|
||||
{
|
||||
models: ["baichuan-53b"],
|
||||
input: 0.2,
|
||||
output: 0.2,
|
||||
currency: Currency.CNY,
|
||||
},
|
||||
{
|
||||
models: ["skylark-lite-public"],
|
||||
input: 0.04,
|
||||
output: 0.04,
|
||||
currency: Currency.CNY,
|
||||
},
|
||||
{
|
||||
models: ["skylark-plus-public"],
|
||||
input: 0.08,
|
||||
output: 0.08,
|
||||
currency: Currency.CNY,
|
||||
},
|
||||
{
|
||||
models: ["skylark-pro-public", "skylark-chat"],
|
||||
input: 0.11,
|
||||
output: 0.11,
|
||||
currency: Currency.CNY,
|
||||
},
|
||||
];
|
||||
|
||||
const countPricing = (
|
||||
_price?: number,
|
||||
_currency?: Currency,
|
||||
usd?: number,
|
||||
): number => {
|
||||
const price = _price ?? 0;
|
||||
const currency = _currency ?? Currency.USD;
|
||||
|
||||
switch (currency) {
|
||||
case Currency.CNY:
|
||||
return price * 10; // 1 cny = 10 quota
|
||||
case Currency.USD:
|
||||
return price * 10 * (usd ?? 1);
|
||||
default:
|
||||
return countPricing(price, Currency.USD, usd);
|
||||
}
|
||||
};
|
||||
|
||||
export const getPricing = (currency: number): ChargeProps[] =>
|
||||
pricing.map(
|
||||
(item, index): ChargeProps => ({
|
||||
id: index,
|
||||
models: item.models,
|
||||
type: item.billing_type ?? tokenBilling,
|
||||
anonymous: false,
|
||||
input: countPricing(item.input, item.currency, currency),
|
||||
output: countPricing(item.output, item.currency, currency),
|
||||
}),
|
||||
);
|
@ -12,12 +12,14 @@ type v1Models = {
|
||||
data: v1ModelItem[];
|
||||
};
|
||||
|
||||
type v1ModelItem = string | {
|
||||
id: string;
|
||||
object: string;
|
||||
created: number;
|
||||
owned_by: string;
|
||||
};
|
||||
type v1ModelItem =
|
||||
| string
|
||||
| {
|
||||
id: string;
|
||||
object: string;
|
||||
created: number;
|
||||
owned_by: string;
|
||||
};
|
||||
|
||||
type v1Resp<T> = {
|
||||
data: T;
|
||||
@ -52,7 +54,9 @@ export async function getApiModels(
|
||||
|
||||
// if data.data is an array of strings, we can just return it
|
||||
|
||||
const models = data.data ? data.data.map((model) => typeof model === "string" ? model : model.id) : [];
|
||||
const models = data.data
|
||||
? data.data.map((model) => (typeof model === "string" ? model : model.id))
|
||||
: [];
|
||||
|
||||
return models.length > 0
|
||||
? { status: true, data: models }
|
||||
|
@ -12,6 +12,8 @@ import { Input } from "@/components/ui/input.tsx";
|
||||
import { useState } from "react";
|
||||
import { NumberInput } from "@/components/ui/number-input.tsx";
|
||||
import { Switch } from "@/components/ui/switch.tsx";
|
||||
import { Alert, AlertDescription } from "./ui/alert";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
|
||||
export enum popupTypes {
|
||||
Text = "text",
|
||||
@ -40,6 +42,7 @@ export type PopupDialogProps = {
|
||||
confirmLabel?: string;
|
||||
|
||||
componentProps?: any;
|
||||
alert?: string;
|
||||
};
|
||||
|
||||
type PopupFieldProps = PopupDialogProps & {
|
||||
@ -114,6 +117,7 @@ function PopupDialog(props: PopupDialogProps) {
|
||||
confirmLabel,
|
||||
destructive,
|
||||
disabled,
|
||||
alert,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
@ -134,6 +138,12 @@ function PopupDialog(props: PopupDialogProps) {
|
||||
<PopupField {...props} value={value} setValue={setValue} />
|
||||
</div>
|
||||
)}
|
||||
{alert && (
|
||||
<Alert className={`pb-3 select-none text-secondary`}>
|
||||
<AlertCircle className="text-secondary mt-[1px] h-4 w-4" />
|
||||
<AlertDescription className={`mt-[1px]`}>{alert}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant={`outline`} onClick={() => setOpen(false)}>
|
||||
{cancelLabel || t("cancel")}
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
DownloadCloud,
|
||||
Eraser,
|
||||
EyeOff,
|
||||
KanbanSquareDashed,
|
||||
Minus,
|
||||
PencilLine,
|
||||
Plus,
|
||||
@ -39,7 +40,6 @@ import {
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command.tsx";
|
||||
import { channelModels } from "@/admin/channel.ts";
|
||||
import { toastState } from "@/api/common.ts";
|
||||
import { Switch } from "@/components/ui/switch.tsx";
|
||||
import { NumberInput } from "@/components/ui/number-input.tsx";
|
||||
@ -61,12 +61,11 @@ import {
|
||||
} from "@/admin/api/charge.ts";
|
||||
import { useEffectAsync } from "@/utils/hook.ts";
|
||||
import { cn } from "@/components/ui/lib/utils.ts";
|
||||
import { allModels } from "@/conf";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx";
|
||||
import Tips from "@/components/Tips.tsx";
|
||||
import { getQuerySelector, scrollUp } from "@/utils/dom.ts";
|
||||
import PopupDialog, { popupTypes } from "@/components/PopupDialog.tsx";
|
||||
import { getApiCharge, getV1Path } from "@/api/v1.ts";
|
||||
import { getApiCharge, getApiModels, getV1Path } from "@/api/v1.ts";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@ -75,6 +74,9 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog.tsx";
|
||||
import { getUniqueList, parseNumber } from "@/utils/base.ts";
|
||||
import { defaultChannelModels } from "@/admin/channel.ts";
|
||||
import { getPricing } from "@/admin/datasets/charge.ts";
|
||||
|
||||
const initialState: ChargeProps = {
|
||||
id: -1,
|
||||
@ -95,6 +97,14 @@ function reducer(state: ChargeProps, action: any): ChargeProps {
|
||||
const model = action.payload.trim();
|
||||
if (model.length === 0 || state.models.includes(model)) return state;
|
||||
return { ...state, models: [...state.models, model] };
|
||||
case "toggle-model":
|
||||
if (action.payload.trim().length === 0) return state;
|
||||
return state.models.includes(action.payload)
|
||||
? {
|
||||
...state,
|
||||
models: state.models.filter((model) => model !== action.payload),
|
||||
}
|
||||
: { ...state, models: [...state.models, action.payload] };
|
||||
case "remove-model":
|
||||
return {
|
||||
...state,
|
||||
@ -143,37 +153,66 @@ function preflight(state: ChargeProps): ChargeProps {
|
||||
|
||||
type SyncDialogProps = {
|
||||
current: string[];
|
||||
builtin: boolean;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
onRefresh: () => void;
|
||||
};
|
||||
|
||||
function SyncDialog({ current, open, setOpen, onRefresh }: SyncDialogProps) {
|
||||
function SyncDialog({
|
||||
builtin,
|
||||
current,
|
||||
open,
|
||||
setOpen,
|
||||
onRefresh,
|
||||
}: SyncDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [siteCharge, setSiteCharge] = useState<ChargeProps[]>([]);
|
||||
const [siteOpen, setSiteOpen] = useState(false);
|
||||
|
||||
const [overwrite, setOverwrite] = useState(false);
|
||||
|
||||
const siteModels = useMemo(() => {
|
||||
return siteCharge.flatMap((charge) => charge.models);
|
||||
}, [siteCharge]);
|
||||
|
||||
const influencedModels = useMemo(() => {
|
||||
return overwrite
|
||||
? siteModels
|
||||
: siteModels.filter((model) => !current.includes(model));
|
||||
}, [overwrite, siteModels, current]);
|
||||
const siteModels = useMemo(
|
||||
() => siteCharge.flatMap((charge) => charge.models),
|
||||
[siteCharge],
|
||||
);
|
||||
const influencedModels = useMemo(
|
||||
() =>
|
||||
overwrite
|
||||
? siteModels
|
||||
: siteModels.filter((model) => !current.includes(model)),
|
||||
[overwrite, siteModels, current],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PopupDialog
|
||||
type={popupTypes.Number}
|
||||
title={t("admin.charge.sync-builtin")}
|
||||
name={t("admin.charge.usd-currency")}
|
||||
open={open && builtin}
|
||||
setOpen={setOpen}
|
||||
defaultValue={"7.1"}
|
||||
onSubmit={async (_currency: string): Promise<boolean> => {
|
||||
const currency = parseNumber(_currency);
|
||||
const pricing = getPricing(currency);
|
||||
|
||||
setSiteCharge(pricing);
|
||||
setSiteOpen(true)
|
||||
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
<PopupDialog
|
||||
type={popupTypes.Text}
|
||||
title={t("admin.charge.sync")}
|
||||
name={t("admin.charge.sync-site")}
|
||||
placeholder={t("admin.charge.sync-placeholder")}
|
||||
open={open}
|
||||
open={open && !builtin}
|
||||
setOpen={setOpen}
|
||||
defaultValue={"https://api.chatnio.net"}
|
||||
alert={t("admin.chatnio-format-only")}
|
||||
onSubmit={async (endpoint): Promise<boolean> => {
|
||||
const path = getV1Path("/v1/charge", { endpoint });
|
||||
const charge = await getApiCharge({ endpoint });
|
||||
@ -189,13 +228,11 @@ function SyncDialog({ current, open, setOpen, onRefresh }: SyncDialogProps) {
|
||||
}
|
||||
|
||||
setSiteCharge(charge);
|
||||
setSiteOpen(true);
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
<Dialog
|
||||
open={siteCharge.length > 0}
|
||||
onOpenChange={(state: boolean) => !state && setSiteCharge([])}
|
||||
>
|
||||
<Dialog open={siteOpen} onOpenChange={setSiteOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("admin.charge.sync-option")}</DialogTitle>
|
||||
@ -213,7 +250,13 @@ function SyncDialog({ current, open, setOpen, onRefresh }: SyncDialogProps) {
|
||||
<Switch checked={overwrite} onCheckedChange={setOverwrite} />
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant={`outline`} onClick={() => setSiteCharge([])}>
|
||||
<Button
|
||||
variant={`outline`}
|
||||
onClick={() => {
|
||||
setSiteOpen(false);
|
||||
setSiteCharge([]);
|
||||
}}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
@ -228,7 +271,9 @@ function SyncDialog({ current, open, setOpen, onRefresh }: SyncDialogProps) {
|
||||
|
||||
if (resp.status) {
|
||||
setOpen(false);
|
||||
setSiteOpen(false);
|
||||
setSiteCharge([]);
|
||||
|
||||
onRefresh();
|
||||
}
|
||||
}}
|
||||
@ -255,16 +300,27 @@ function ChargeAction({
|
||||
}: ChargeActionProps) {
|
||||
const { t } = useTranslation();
|
||||
const [popup, setPopup] = useState(false);
|
||||
const [builtin, setBuiltin] = useState(false);
|
||||
|
||||
const open = (builtin: boolean) => {
|
||||
setBuiltin(builtin);
|
||||
setPopup(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex flex-row w-full h-max`}>
|
||||
<SyncDialog
|
||||
builtin={builtin}
|
||||
onRefresh={onRefresh}
|
||||
current={currentModels}
|
||||
open={popup}
|
||||
setOpen={setPopup}
|
||||
/>
|
||||
<Button variant={`outline`} onClick={() => setPopup(true)}>
|
||||
<Button variant={`default`} className={`mr-2`} onClick={() => open(true)}>
|
||||
<KanbanSquareDashed className={`w-4 h-4 mr-2`} />
|
||||
{t("admin.charge.sync-builtin")}
|
||||
</Button>
|
||||
<Button variant={`outline`} onClick={() => open(false)}>
|
||||
<Activity className={`w-4 h-4 mr-2`} />
|
||||
{t("admin.charge.sync")}
|
||||
</Button>
|
||||
@ -278,9 +334,10 @@ function ChargeAction({
|
||||
|
||||
type ChargeAlertProps = {
|
||||
models: string[];
|
||||
onClick: (model: string) => void;
|
||||
};
|
||||
|
||||
function ChargeAlert({ models }: ChargeAlertProps) {
|
||||
function ChargeAlert({ models, onClick }: ChargeAlertProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@ -293,7 +350,11 @@ function ChargeAlert({ models }: ChargeAlertProps) {
|
||||
</AlertTitle>
|
||||
<AlertDescription className={`model-list`}>
|
||||
{models.map((model, index) => (
|
||||
<div key={index} className={`model`}>
|
||||
<div
|
||||
key={index}
|
||||
className={`model cursor-pointer select-none`}
|
||||
onClick={() => onClick(model)}
|
||||
>
|
||||
{model}
|
||||
</div>
|
||||
))}
|
||||
@ -308,6 +369,7 @@ type ChargeEditorProps = {
|
||||
dispatch: (action: any) => void;
|
||||
onRefresh: () => void;
|
||||
usedModels: string[];
|
||||
supportModels: string[];
|
||||
};
|
||||
|
||||
function ChargeEditor({
|
||||
@ -315,11 +377,18 @@ function ChargeEditor({
|
||||
dispatch,
|
||||
onRefresh,
|
||||
usedModels,
|
||||
supportModels,
|
||||
}: ChargeEditorProps) {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [model, setModel] = useState("");
|
||||
|
||||
const channelModels = useMemo(
|
||||
() => getUniqueList([...supportModels, ...defaultChannelModels]),
|
||||
[supportModels],
|
||||
);
|
||||
|
||||
const unusedModels = useMemo(() => {
|
||||
return channelModels.filter(
|
||||
(model) =>
|
||||
@ -649,6 +718,8 @@ function ChargeWidget() {
|
||||
const [form, dispatch] = useReducer(reducer, initialState);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [supportModels, setSupportModels] = useState<string[]>([]);
|
||||
|
||||
const currentModels = useMemo(() => {
|
||||
return data.flatMap((charge) => charge.models);
|
||||
}, [data]);
|
||||
@ -659,14 +730,17 @@ function ChargeWidget() {
|
||||
|
||||
const unusedModels = useMemo(() => {
|
||||
if (loading) return [];
|
||||
return allModels.filter(
|
||||
return supportModels.filter(
|
||||
(model) => !usedModels.includes(model) && model.trim() !== "",
|
||||
);
|
||||
}, [loading, allModels, usedModels]);
|
||||
}, [loading, supportModels, usedModels]);
|
||||
|
||||
async function refresh() {
|
||||
setLoading(true);
|
||||
const resp = await listCharge();
|
||||
const models = await getApiModels();
|
||||
setSupportModels(models.data);
|
||||
|
||||
setLoading(false);
|
||||
toastState(toast, t, resp);
|
||||
setData(resp.data);
|
||||
@ -681,11 +755,15 @@ function ChargeWidget() {
|
||||
onRefresh={refresh}
|
||||
currentModels={currentModels}
|
||||
/>
|
||||
<ChargeAlert models={unusedModels} />
|
||||
<ChargeAlert
|
||||
models={unusedModels}
|
||||
onClick={(model) => dispatch({ type: "toggle-model", payload: model })}
|
||||
/>
|
||||
<ChargeEditor
|
||||
onRefresh={refresh}
|
||||
form={form}
|
||||
dispatch={dispatch}
|
||||
supportModels={supportModels}
|
||||
usedModels={usedModels}
|
||||
/>
|
||||
<ChargeTable data={data} dispatch={dispatch} onRefresh={refresh} />
|
||||
|
@ -95,7 +95,7 @@ function SyncDialog({ dispatch, open, setOpen }: SyncDialogProps) {
|
||||
priority: 0,
|
||||
weight: 1,
|
||||
retry: 3,
|
||||
secret: "",
|
||||
secret,
|
||||
endpoint,
|
||||
mapper: "",
|
||||
state: true,
|
||||
|
@ -11,7 +11,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 focus:border-input",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
|
@ -68,7 +68,11 @@ const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
|
||||
return (
|
||||
<Input
|
||||
ref={ref}
|
||||
className={cn("number-input", className, !isValid && "border-red-600")}
|
||||
className={cn(
|
||||
"number-input transition",
|
||||
className,
|
||||
!isValid && "border-red-600 focus:border-red-700",
|
||||
)}
|
||||
id={props.id}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
|
@ -483,6 +483,7 @@
|
||||
"error": "请求失败",
|
||||
"default-password": "密码修改提示",
|
||||
"default-password-prompt": "您的管理员密码为默认密码,为了您的账号安全,请尽快修改密码。(前往后台管理 - 系统设置 - 修改 Root 密码)",
|
||||
"chatnio-format-only": "此格式为 Chat Nio 独有格式",
|
||||
"identity": {
|
||||
"normal": "普通用户",
|
||||
"api_paid": "其他付费用户",
|
||||
@ -627,7 +628,7 @@
|
||||
"add-rule": "添加规则",
|
||||
"update-rule": "更新规则",
|
||||
"unused-model": "部分模型计费规则未设置",
|
||||
"unused-model-tip": "计费规则未设置的模型将不会计费并支持匿名调用,请谨慎设置。",
|
||||
"unused-model-tip": "计费规则未设置的模型为避免损失,普通用户将无法请求",
|
||||
"sync": "同步上游",
|
||||
"sync-option": "同步选项",
|
||||
"sync-site": "上游地址",
|
||||
@ -637,7 +638,9 @@
|
||||
"sync-failed-prompt": "地址无法请求或者计费规则为空\n(端点:{{endpoint}})",
|
||||
"sync-prompt": "已从上游获取 {{length}} 个模型的规则,将影响当前 {{influence}} 个模型的规则,是否继续?",
|
||||
"sync-overwrite": "覆盖已有规则",
|
||||
"sync-confirm": "确认同步"
|
||||
"sync-confirm": "确认同步",
|
||||
"sync-builtin": "应用内置价格",
|
||||
"usd-currency": "美元兑人民币汇率"
|
||||
},
|
||||
"system": {
|
||||
"general": "常规设置",
|
||||
@ -676,7 +679,9 @@
|
||||
"customWhitelistPlaceholder": "请输入自定义域名后缀列表(输入后将出现在选项列表中可供选择),使用英文逗号分隔,如:example.com,example.net",
|
||||
"searchEndpoint": "搜索接入点",
|
||||
"searchQuery": "最大搜索结果数",
|
||||
"searchTip": "DuckDuckGo 搜索接入点,如不填写自动使用 WebPilot 和 New Bing 逆向进行搜索功能(速度较慢)。\nDuckDuckGo API 项目搭建:[duckduckgo-api](https://github.com/binjie09/duckduckgo-api)。",
|
||||
"searchQueryTip": "最大搜索结果数,默认为 5",
|
||||
"searchTip": "DuckDuckGo 联网搜索接入点。\nDuckDuckGo API 项目搭建:[duckduckgo-api](https://github.com/binjie09/duckduckgo-api)。",
|
||||
"searchPlaceholder": "DuckDuckGo 接入点 (格式仅需填写 https://example.com)",
|
||||
"closeRegistration": "暂停注册",
|
||||
"closeRegistrationTip": "暂停注册,关闭后新用户将无法注册",
|
||||
"relayPlan": "订阅配额支持中转 API",
|
||||
|
@ -469,7 +469,7 @@
|
||||
"add-rule": "Add Rule",
|
||||
"update-rule": "Update Rule",
|
||||
"unused-model": "Some model billing rules are not set",
|
||||
"unused-model-tip": "Models that do not have billing rules set will not be billed and will support anonymous calls, please set them carefully.",
|
||||
"unused-model-tip": "Models not set up by billing rules To avoid losses, regular users will not be able to request",
|
||||
"sync": "Sync upstream",
|
||||
"sync-option": "Synchronization Options",
|
||||
"sync-site": "Upstream address",
|
||||
@ -479,7 +479,9 @@
|
||||
"sync-failed-prompt": "Address could not be requested or billing rule is empty\n(Endpoint: {{endpoint}})",
|
||||
"sync-prompt": "The rules for {{length}} models have been fetched from upstream and will affect the rules for the current {{influence}} models. Do you want to continue?",
|
||||
"sync-overwrite": "Overwrite existing rules",
|
||||
"sync-confirm": "Confirm Sync"
|
||||
"sync-confirm": "Confirm Sync",
|
||||
"sync-builtin": "Built-in price in the app",
|
||||
"usd-currency": "USD to RMB exchange rate"
|
||||
},
|
||||
"system": {
|
||||
"general": "General Settings",
|
||||
@ -548,7 +550,9 @@
|
||||
"footerPlaceholder": "Please enter footer information (Markdown/HTML format supported)",
|
||||
"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)"
|
||||
"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)",
|
||||
"searchQueryTip": "Maximum number of search results, default is 5",
|
||||
"searchPlaceholder": "DuckDuckGo Access Point (Format only https://example.com)"
|
||||
},
|
||||
"user": "User Management",
|
||||
"invitation-code": "Invitation Code",
|
||||
@ -655,7 +659,8 @@
|
||||
"ban-action-desc": "Are you sure you want to ban this user?",
|
||||
"unban-action": " unBlock User",
|
||||
"unban-action-desc": "Are you sure you want to unblock this user?",
|
||||
"billing": "Income"
|
||||
"billing": "Income",
|
||||
"chatnio-format-only": "This format is unique to Chat Nio"
|
||||
},
|
||||
"mask": {
|
||||
"title": "Mask Settings",
|
||||
|
@ -469,7 +469,7 @@
|
||||
"add-rule": "規則の追加",
|
||||
"update-rule": "ルールを更新",
|
||||
"unused-model": "一部のモデルの請求ルールが設定されていません",
|
||||
"unused-model-tip": "請求ルールが設定されていないモデルは請求されず、匿名通話をサポートします。慎重に設定してください。",
|
||||
"unused-model-tip": "請求ルールで設定されていないモデル損失を回避するため、通常のユーザーはリクエストできません",
|
||||
"sync": "アップストリームを同期",
|
||||
"sync-option": "同期のオプション",
|
||||
"sync-site": "アップストリームアドレス",
|
||||
@ -479,7 +479,9 @@
|
||||
"sync-failed-prompt": "住所をリクエストできなかったか、請求ルールが空です\n(エンドポイント:{{ endpoint }})",
|
||||
"sync-prompt": "{{length}}モデルのルールはアップストリームから取得されており、現在の{{influence}}モデルのルールに影響します。続行しますか?",
|
||||
"sync-overwrite": "既存のルールを上書きする",
|
||||
"sync-confirm": "同期を確認"
|
||||
"sync-confirm": "同期を確認",
|
||||
"sync-builtin": "アプリに組み込まれている料金",
|
||||
"usd-currency": "米ドル対人民元為替レート"
|
||||
},
|
||||
"system": {
|
||||
"general": "全般設定",
|
||||
@ -548,7 +550,9 @@
|
||||
"footerPlaceholder": "フッター情報を入力してください( Markdown/HTML形式に対応)",
|
||||
"authFooter": "ログイン後にフッターを非表示にする",
|
||||
"relayPlan": "サブスクリプションクォータサポートステージングAPI",
|
||||
"relayPlanTip": "サブスクリプションクォータはトランジットAPIをサポートしています。トランジットAPI請求を開いた後、ユーザーサブスクリプションクォータの使用が優先されます\n(ヒント:サブスクリプションは時間のクォータであり、トークンの請求モデルはコストに影響する可能性があります)"
|
||||
"relayPlanTip": "サブスクリプションクォータはトランジットAPIをサポートしています。トランジットAPI請求を開いた後、ユーザーサブスクリプションクォータの使用が優先されます\n(ヒント:サブスクリプションは時間のクォータであり、トークンの請求モデルはコストに影響する可能性があります)",
|
||||
"searchQueryTip": "検索結果の最大数、デフォルトは5です",
|
||||
"searchPlaceholder": "DuckDuckGoアクセスポイント(フォーマットのみhttps://example.com )"
|
||||
},
|
||||
"user": "ユーザー管理",
|
||||
"invitation-code": "招待コード",
|
||||
@ -655,7 +659,8 @@
|
||||
"ban-action-desc": "このユーザーを禁止してもよろしいですか?",
|
||||
"unban-action": "ユーザーのブロックを解除する",
|
||||
"unban-action-desc": "このユーザーのブロックを解除してもよろしいですか?",
|
||||
"billing": "収入"
|
||||
"billing": "収入",
|
||||
"chatnio-format-only": "このフォーマットはChat Nioに固有です"
|
||||
},
|
||||
"mask": {
|
||||
"title": "プリセット設定",
|
||||
|
@ -469,7 +469,7 @@
|
||||
"add-rule": "Добавить правило",
|
||||
"update-rule": "Обновить правило",
|
||||
"unused-model": "Некоторые правила выставления счетов модели не установлены",
|
||||
"unused-model-tip": "Модели, которые не имеют установленных правил выставления счетов, не будут выставляться счета и будут поддерживать анонимные звонки, пожалуйста, установите их тщательно.",
|
||||
"unused-model-tip": "Модели не настроены по правилам выставления счетов Чтобы избежать потерь, обычные пользователи не смогут запрашивать",
|
||||
"sync": "Синхронизация выше ПО потоку",
|
||||
"sync-option": "Параметры синхронизации",
|
||||
"sync-site": "Адрес выше по потоку",
|
||||
@ -479,7 +479,9 @@
|
||||
"sync-failed-prompt": "Адрес не может быть запрошен или правило выставления счетов пусто\n(Конечная точка: {{endpoint}})",
|
||||
"sync-prompt": "Правила для {{length}} моделей были взяты из верхнего потока и повлияют на правила для текущих {{influence}} моделей. Продолжить?",
|
||||
"sync-overwrite": "Перезаписать существующие правила",
|
||||
"sync-confirm": "Подтвердить синхронизацию"
|
||||
"sync-confirm": "Подтвердить синхронизацию",
|
||||
"sync-builtin": "Встроенная цена в приложении",
|
||||
"usd-currency": "Обменный курс доллара США к юаню"
|
||||
},
|
||||
"system": {
|
||||
"general": "Общие настройки",
|
||||
@ -548,7 +550,9 @@
|
||||
"footerPlaceholder": "Пожалуйста, введите информацию нижнего колонтитула (поддерживается формат Markdown/HTML)",
|
||||
"authFooter": "Скрыть нижний колонтитул после входа в систему",
|
||||
"relayPlan": "API промежуточной поддержки квот подписки",
|
||||
"relayPlanTip": "Квота подписки поддерживает транзитный API, после открытия транзитного API биллинг будет отдавать приоритет использованию пользовательской квоты подписки\n(Совет: Подписка - это квота раз, модель биллинга для токенов может повлиять на стоимость)"
|
||||
"relayPlanTip": "Квота подписки поддерживает транзитный API, после открытия транзитного API биллинг будет отдавать приоритет использованию пользовательской квоты подписки\n(Совет: Подписка - это квота раз, модель биллинга для токенов может повлиять на стоимость)",
|
||||
"searchQueryTip": "Максимальное количество результатов поиска, по умолчанию 5",
|
||||
"searchPlaceholder": "Точка доступа DuckDuckGo (только в формате https://example.com)"
|
||||
},
|
||||
"user": "Управление пользователями",
|
||||
"invitation-code": "Код приглашения",
|
||||
@ -655,7 +659,8 @@
|
||||
"ban-action-desc": "Вы уверены, что хотите заблокировать этого пользователя?",
|
||||
"unban-action": "Разблокировать пользователя",
|
||||
"unban-action-desc": "Вы уверены, что хотите разблокировать этого пользователя?",
|
||||
"billing": "Доходы"
|
||||
"billing": "Доходы",
|
||||
"chatnio-format-only": "Этот формат уникален для Chat Nio"
|
||||
},
|
||||
"mask": {
|
||||
"title": "Настройки маски",
|
||||
|
@ -755,11 +755,17 @@ function Search({ data, dispatch, onChange }: CompProps<SearchState>) {
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder={`DuckDuckGo API Endpoint`}
|
||||
placeholder={t("admin.system.searchPlaceholder")}
|
||||
/>
|
||||
</ParagraphItem>
|
||||
<ParagraphItem>
|
||||
<Label>{t("admin.system.searchQuery")}</Label>
|
||||
<Label>
|
||||
{t("admin.system.searchQuery")}
|
||||
<Tips
|
||||
className={`inline-block`}
|
||||
content={t("admin.system.searchQueryTip")}
|
||||
/>
|
||||
</Label>
|
||||
<NumberInput
|
||||
value={data.query}
|
||||
onValueChange={(value) =>
|
||||
|
@ -162,15 +162,16 @@ func (m *ChargeManager) SyncRule(charge *Charge, overwrite bool) {
|
||||
}
|
||||
|
||||
func (m *ChargeManager) SyncRuleWithOverwrite(charge *Charge) {
|
||||
cached := make([]string, 0)
|
||||
if len(charge.Models) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, model := range charge.GetModels() {
|
||||
if raw := m.GetRuleByModel(model); raw != nil {
|
||||
if len(raw.Models) == 1 {
|
||||
// rule is already exist and only contains this model, just update it
|
||||
instance := raw.New(model)
|
||||
instance.Id = raw.Id
|
||||
m.UpdateRawRule(instance)
|
||||
// rule is already exist and only contains this model, just delete it
|
||||
|
||||
m.DeleteRawRule(raw.Id)
|
||||
} else {
|
||||
// rule is already exist and contains other models, delete this model from it and add a new rule
|
||||
// delete model from raw rule
|
||||
@ -178,21 +179,13 @@ func (m *ChargeManager) SyncRuleWithOverwrite(charge *Charge) {
|
||||
return m != model
|
||||
})
|
||||
m.UpdateRawRule(raw)
|
||||
|
||||
// add new rule
|
||||
cached = append(cached, model)
|
||||
}
|
||||
} else {
|
||||
// rule is not exist, add a new rule
|
||||
cached = append(cached, model)
|
||||
}
|
||||
}
|
||||
|
||||
if len(cached) > 0 {
|
||||
instance := charge.New("")
|
||||
instance.Models = cached
|
||||
m.AddRawRule(instance)
|
||||
}
|
||||
instance := charge.New("")
|
||||
instance.Models = charge.Models
|
||||
m.AddRawRule(instance)
|
||||
}
|
||||
|
||||
func (m *ChargeManager) SyncRuleWithoutOverwrite(charge *Charge) {
|
||||
|
BIN
screenshot/mask-editor.png
Normal file
BIN
screenshot/mask-editor.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 73 KiB |
BIN
screenshot/mask.png
Normal file
BIN
screenshot/mask.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 109 KiB |
Loading…
Reference in New Issue
Block a user