feat: support apply template pricing in admin charge settings

This commit is contained in:
Zhang Minghan 2024-02-28 01:42:54 +08:00
parent 40cc84a863
commit 7432ae6714
19 changed files with 428 additions and 84 deletions

View File

@ -33,13 +33,15 @@ _🚀 **Next Generation AI One-Stop Solution**_
![对话分享](/screenshot/sharing.png)
6. **原生支持文件解析**, 不依赖模型, 支持 pdf, docx, pptx, xlsx, 音频 *(需配置azure speech)*, 图片 *(需 vision 模型)* 等格式上传, 可直接从消息框 Ctrl+V 复制文件, 同时支持操作弹出窗口的点击上传和拖拽上传, 支持多文件管理 _(详情参考项目 [chatnio-blob-service](https://github.com/Deeptrain-Community/chatnio-blob-service))_
![文件上传](/screenshot/file.png)
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))_
![联网搜索](/screenshot/online.png)
8. **大文本全屏编辑支持**, 支持 *纯文本编辑*, *编辑预览模式*, *纯预览模式* 三种模式切换
![编辑器](/screenshot/editor.png)
9. **模型市场功能**, 支持模型搜索, 支持顺序拖拽, 包含模型名称, 模型描述, 模型 Tags, 模型头像, 自动绑定模型的价格设置, 自动绑定订阅配额 (包含在订阅的模型将标有 *plus* 标签)
![模型市场](/screenshot/market.png)
10. (自定义预设开发中) **支持预设功能**, 支持自定义预设和云端同步功能, 支持预设克隆, 预设头像设置, 预设简介设置
10. **支持预设功能**, 支持 ***自定义预设*** 和 **_云端同步_** 功能, 支持预设克隆, 预设头像设置, 预设简介设置
![预设设置](/screenshot/mask.png)
![预设编辑](/screenshot/mask-editor.png)
11. **支持站点公告** (公告弹窗显示, 需确认), 支持公告通知 (右侧通知显示, 无需确认)
12. **支持主题切换**, 明亮 / 暗黑显示主题切换, 自动保存主题偏好, 自动获取默认系统主题
13. **支持偏好设置**, i18n 多语言支持, 自定义最大携带会话数, 最大回复 tokens 数, 模型参数自定义, 重置设置等

View File

@ -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

View File

@ -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),
);

View File

@ -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;

View 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),
}),
);

View File

@ -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 }

View File

@ -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")}

View File

@ -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} />

View File

@ -95,7 +95,7 @@ function SyncDialog({ dispatch, open, setOpen }: SyncDialogProps) {
priority: 0,
weight: 1,
retry: 3,
secret: "",
secret,
endpoint,
mapper: "",
state: true,

View File

@ -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}

View File

@ -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) => {

View File

@ -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",

View File

@ -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",

View File

@ -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": "プリセット設定",

View File

@ -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": "Настройки маски",

View File

@ -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) =>

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

BIN
screenshot/mask.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB