diff --git a/README.md b/README.md index 3a63e3e..086cbe3 100644 --- a/README.md +++ b/README.md @@ -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 数, 模型参数自定义, 重置设置等 diff --git a/addition/article/generate.go b/addition/article/generate.go index a5ea56c..b35f854 100644 --- a/addition/article/generate.go +++ b/addition/article/generate.go @@ -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 diff --git a/app/src/admin/channel.ts b/app/src/admin/channel.ts index 37515c2..16dc1b9 100644 --- a/app/src/admin/channel.ts +++ b/app/src/admin/channel.ts @@ -209,6 +209,10 @@ export const ChannelInfos: Record = { }, }; +export const defaultChannelModels: string[] = getUniqueList( + Object.values(ChannelInfos).flatMap((info) => info.models), +); + export const channelModels: string[] = getUniqueList( Object.values(ChannelInfos).flatMap((info) => info.models), ); diff --git a/app/src/admin/charge.ts b/app/src/admin/charge.ts index 1697525..c6ee470 100644 --- a/app/src/admin/charge.ts +++ b/app/src/admin/charge.ts @@ -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; diff --git a/app/src/admin/datasets/charge.ts b/app/src/admin/datasets/charge.ts new file mode 100644 index 0000000..c4dff60 --- /dev/null +++ b/app/src/admin/datasets/charge.ts @@ -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), + }), + ); diff --git a/app/src/api/v1.ts b/app/src/api/v1.ts index 2122eac..2de7f39 100644 --- a/app/src/api/v1.ts +++ b/app/src/api/v1.ts @@ -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 = { 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 } diff --git a/app/src/components/PopupDialog.tsx b/app/src/components/PopupDialog.tsx index 0b6e471..d1eb885 100644 --- a/app/src/components/PopupDialog.tsx +++ b/app/src/components/PopupDialog.tsx @@ -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) { )} + {alert && ( + + + {alert} + + )} + @@ -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) { {models.map((model, index) => ( -
+
onClick(model)} + > {model}
))} @@ -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([]); + 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} /> - + dispatch({ type: "toggle-model", payload: model })} + /> diff --git a/app/src/components/admin/assemblies/ChannelTable.tsx b/app/src/components/admin/assemblies/ChannelTable.tsx index 772264e..26ed5a8 100644 --- a/app/src/components/admin/assemblies/ChannelTable.tsx +++ b/app/src/components/admin/assemblies/ChannelTable.tsx @@ -95,7 +95,7 @@ function SyncDialog({ dispatch, open, setOpen }: SyncDialogProps) { priority: 0, weight: 1, retry: 3, - secret: "", + secret, endpoint, mapper: "", state: true, diff --git a/app/src/components/ui/input.tsx b/app/src/components/ui/input.tsx index edfa129..2aa154d 100644 --- a/app/src/components/ui/input.tsx +++ b/app/src/components/ui/input.tsx @@ -11,7 +11,7 @@ const Input = React.forwardRef( ( return ( { diff --git a/app/src/resources/i18n/cn.json b/app/src/resources/i18n/cn.json index 2ef267a..f11c86c 100644 --- a/app/src/resources/i18n/cn.json +++ b/app/src/resources/i18n/cn.json @@ -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", diff --git a/app/src/resources/i18n/en.json b/app/src/resources/i18n/en.json index 91cce1a..e8eace1 100644 --- a/app/src/resources/i18n/en.json +++ b/app/src/resources/i18n/en.json @@ -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", diff --git a/app/src/resources/i18n/ja.json b/app/src/resources/i18n/ja.json index 1556f0e..5c6e516 100644 --- a/app/src/resources/i18n/ja.json +++ b/app/src/resources/i18n/ja.json @@ -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": "プリセット設定", diff --git a/app/src/resources/i18n/ru.json b/app/src/resources/i18n/ru.json index cbda957..777610e 100644 --- a/app/src/resources/i18n/ru.json +++ b/app/src/resources/i18n/ru.json @@ -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": "Настройки маски", diff --git a/app/src/routes/admin/System.tsx b/app/src/routes/admin/System.tsx index e9951db..97845e5 100644 --- a/app/src/routes/admin/System.tsx +++ b/app/src/routes/admin/System.tsx @@ -755,11 +755,17 @@ function Search({ data, dispatch, onChange }: CompProps) { value: e.target.value, }) } - placeholder={`DuckDuckGo API Endpoint`} + placeholder={t("admin.system.searchPlaceholder")} /> - + diff --git a/channel/charge.go b/channel/charge.go index 710c168..d45ab09 100644 --- a/channel/charge.go +++ b/channel/charge.go @@ -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) { diff --git a/screenshot/mask-editor.png b/screenshot/mask-editor.png new file mode 100644 index 0000000..2fddc33 Binary files /dev/null and b/screenshot/mask-editor.png differ diff --git a/screenshot/mask.png b/screenshot/mask.png new file mode 100644 index 0000000..0fde4a8 Binary files /dev/null and b/screenshot/mask.png differ