diff --git a/constant/chat.go b/constant/chat.go new file mode 100644 index 00000000..336a5cdc --- /dev/null +++ b/constant/chat.go @@ -0,0 +1,34 @@ +package constant + +import ( + "encoding/json" + "one-api/common" +) + +var Chats = []map[string]string{ + { + "ChatGPT Next Web 官方示例": "https://app.nextchat.dev/#/?settings={\"key\":\"{key}\",\"url\":\"{address}\"}", + }, + { + "Lobe Chat 官方示例": "https://chat-preview.lobehub.com/?settings={\"keyVaults\":{\"openai\":{\"apiKey\":\"{key}\",\"baseURL\":\"{address}/v1\"}}}", + }, + { + "AMA 问天": "ama://set-api-key?server={address}&key={key}", + }, + { + "OpenCat": "opencat://team/join?domain={address}&token={key}", + }, +} + +func UpdateChatsByJsonString(jsonString string) error { + return json.Unmarshal([]byte(jsonString), &Chats) +} + +func Chats2JsonString() string { + jsonBytes, err := json.Marshal(Chats) + if err != nil { + common.SysError("error marshalling chats: " + err.Error()) + return "[]" + } + return string(jsonBytes) +} diff --git a/controller/misc.go b/controller/misc.go index 5e12854b..d331bdc8 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -63,6 +63,7 @@ func GetStatus(c *gin.Context) { "default_collapse_sidebar": common.DefaultCollapseSidebar, "enable_online_topup": constant.PayAddress != "" && constant.EpayId != "" && constant.EpayKey != "", "mj_notify_enabled": constant.MjNotifyEnabled, + "chats": constant.Chats, }, }) return diff --git a/model/option.go b/model/option.go index 04f952d4..f5c09d73 100644 --- a/model/option.go +++ b/model/option.go @@ -69,6 +69,7 @@ func InitOptionMap() { common.OptionMap["Price"] = strconv.FormatFloat(constant.Price, 'f', -1, 64) common.OptionMap["MinTopUp"] = strconv.Itoa(constant.MinTopUp) common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString() + common.OptionMap["Chats"] = constant.Chats2JsonString() common.OptionMap["GitHubClientId"] = "" common.OptionMap["GitHubClientSecret"] = "" common.OptionMap["TelegramBotToken"] = "" @@ -248,6 +249,8 @@ func updateOptionMap(key string, value string) (err error) { constant.WorkerValidKey = value case "PayAddress": constant.PayAddress = value + case "Chats": + err = constant.UpdateChatsByJsonString(value) case "CustomCallbackAddress": constant.CustomCallbackAddress = value case "EpayId": diff --git a/web/src/components/OperationSetting.js b/web/src/components/OperationSetting.js index 1d875c61..13320f2f 100644 --- a/web/src/components/OperationSetting.js +++ b/web/src/components/OperationSetting.js @@ -10,6 +10,7 @@ import SettingsCreditLimit from '../pages/Setting/Operation/SettingsCreditLimit. import SettingsMagnification from '../pages/Setting/Operation/SettingsMagnification.js'; import { API, showError, showSuccess } from '../helpers'; +import SettingsChats from '../pages/Setting/Operation/SettingsChats.js'; const OperationSetting = () => { let [inputs, setInputs] = useState({ @@ -50,6 +51,7 @@ const OperationSetting = () => { DataExportInterval: 5, DefaultCollapseSidebar: false, // 默认折叠侧边栏 RetryTimes: 0, + Chats: "[]", }); let [loading, setLoading] = useState(false); @@ -131,6 +133,10 @@ const OperationSetting = () => { + {/* 聊天设置 */} + + + {/* 倍率设置 */} diff --git a/web/src/components/TokensTable.js b/web/src/components/TokensTable.js index 74b249ac..dee3ab38 100644 --- a/web/src/components/TokensTable.js +++ b/web/src/components/TokensTable.js @@ -24,17 +24,6 @@ import { import { IconTreeTriangleDown } from '@douyinfe/semi-icons'; import EditToken from '../pages/Token/EditToken'; -const COPY_OPTIONS = [ - { key: 'next', text: 'ChatGPT Next Web', value: 'next' }, - { key: 'ama', text: 'ChatGPT Web & Midjourney', value: 'ama' }, - { key: 'opencat', text: 'OpenCat', value: 'opencat' }, -]; - -const OPEN_LINK_OPTIONS = [ - { key: 'ama', text: 'ChatGPT Web & Midjourney', value: 'ama' }, - { key: 'opencat', text: 'OpenCat', value: 'opencat' }, -]; - function renderTimestamp(timestamp) { return <>{timestamp2string(timestamp)}; } @@ -87,27 +76,6 @@ function renderStatus(status, model_limits_enabled = false) { } const TokensTable = () => { - const link_menu = [ - { - node: 'item', - key: 'next', - name: 'ChatGPT Next Web', - onClick: () => { - onOpenLink('next'); - }, - }, - { node: 'item', key: 'ama', name: 'AMA 问天', value: 'ama' }, - { - node: 'item', - key: 'next-mj', - name: 'ChatGPT Web & Midjourney', - value: 'next-mj', - onClick: () => { - onOpenLink('next-mj'); - }, - }, - { node: 'item', key: 'opencat', name: 'OpenCat', value: 'opencat' }, - ]; const columns = [ { @@ -174,149 +142,171 @@ const TokensTable = () => { { title: '', dataIndex: 'operate', - render: (text, record, index) => ( -
- - - - - - - { - onOpenLink('next', record.key); - }, - }, - { - node: 'item', - key: 'next-mj', - disabled: !localStorage.getItem('chat_link2'), - name: 'ChatGPT Web & Midjourney', - onClick: () => { - onOpenLink('next-mj', record.key); - }, - }, - // { - // node: 'item', - // key: 'lobe', - // name: 'Lobe Chat', - // onClick: () => { - // onOpenLink('lobe', record.key); - // }, - // }, - { - node: 'item', - key: 'ama', - name: 'AMA 问天(BotGem)', - onClick: () => { - onOpenLink('ama', record.key); - }, - }, - { - node: 'item', - key: 'opencat', - name: 'OpenCat', - onClick: () => { - onOpenLink('opencat', record.key); - }, - }, - ]} - > - - - - { - manageToken(record.id, 'delete', record).then(() => { - removeRecord(record.key); - }); - }} - > - - - {record.status === 1 ? ( - - ) : ( + + - )} - -
- ), + + + + + + + { + manageToken(record.id, 'delete', record).then(() => { + removeRecord(record.key); + }); + }} + > + + + {record.status === 1 ? ( + + ) : ( + + )} + + + ); + }, }, ]; @@ -330,8 +320,7 @@ const TokensTable = () => { const [searchKeyword, setSearchKeyword] = useState(''); const [searchToken, setSearchToken] = useState(''); const [searching, setSearching] = useState(false); - const [showTopUpModal, setShowTopUpModal] = useState(false); - const [targetTokenIdx, setTargetTokenIdx] = useState(0); + const [chats, setChats] = useState([]); const [editingToken, setEditingToken] = useState({ id: undefined, }); @@ -376,16 +365,6 @@ const TokensTable = () => { setLoading(false); }; - const onPaginationChange = (e, { activePage }) => { - (async () => { - if (activePage === Math.ceil(tokens.length / pageSize) + 1) { - // In this case we have to load more data and then append them. - await loadTokens(activePage - 1); - } - setActivePage(activePage); - })(); - }; - const refresh = async () => { await loadTokens(activePage - 1); }; @@ -402,7 +381,8 @@ const TokensTable = () => { } }; - const onOpenLink = async (type, key) => { + const onOpenLink = async (type, url, record) => { + // console.log(type, url, key); let status = localStorage.getItem('status'); let serverAddress = ''; if (status) { @@ -413,36 +393,39 @@ const TokensTable = () => { serverAddress = window.location.origin; } let encodedServerAddress = encodeURIComponent(serverAddress); - const chatLink = localStorage.getItem('chat_link'); - const mjLink = localStorage.getItem('chat_link2'); - let defaultUrl; - - if (chatLink) { - defaultUrl = - chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; - } - let url; - switch (type) { - case 'ama': - url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`; - break; - case 'opencat': - url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`; - break; - case 'lobe': - url = `https://chat-preview.lobehub.com/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${encodedServerAddress}/v1"}}}`; - break; - case 'next-mj': - url = - mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; - break; - default: - if (!chatLink) { - showError('管理员未设置聊天链接'); - return; - } - url = defaultUrl; - } + url = url.replace('{address}', encodedServerAddress); + url = url.replace('{key}', 'sk-' + record.key); + // console.log(url); + // const chatLink = localStorage.getItem('chat_link'); + // const mjLink = localStorage.getItem('chat_link2'); + // let defaultUrl; + // + // if (chatLink) { + // defaultUrl = + // chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; + // } + // let url; + // switch (type) { + // case 'ama': + // url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`; + // break; + // case 'opencat': + // url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`; + // break; + // case 'lobe': + // url = `https://chat-preview.lobehub.com/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${encodedServerAddress}/v1"}}}`; + // break; + // case 'next-mj': + // url = + // mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; + // break; + // default: + // if (!chatLink) { + // showError('管理员未设置聊天链接'); + // return; + // } + // url = defaultUrl; + // } window.open(url, '_blank'); }; diff --git a/web/src/helpers/data.js b/web/src/helpers/data.js index 93380b41..4cd7a641 100644 --- a/web/src/helpers/data.js +++ b/web/src/helpers/data.js @@ -8,6 +8,7 @@ export function setStatusData(data) { localStorage.setItem('enable_drawing', data.enable_drawing); localStorage.setItem('enable_task', data.enable_task); localStorage.setItem('enable_data_export', data.enable_data_export); + localStorage.setItem('chats', JSON.stringify(data.chats)); localStorage.setItem( 'data_export_default_time', data.data_export_default_time, diff --git a/web/src/pages/Setting/Operation/SettingsChats.js b/web/src/pages/Setting/Operation/SettingsChats.js new file mode 100644 index 00000000..c6c9a770 --- /dev/null +++ b/web/src/pages/Setting/Operation/SettingsChats.js @@ -0,0 +1,148 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { Banner, Button, Col, Form, Popconfirm, Row, Space, Spin } from '@douyinfe/semi-ui'; +import { + compareObjects, + API, + showError, + showSuccess, + showWarning, + verifyJSON, + verifyJSONPromise +} from '../../../helpers'; + +export default function SettingsChats(props) { + const [loading, setLoading] = useState(false); + const [inputs, setInputs] = useState({ + Chats: "[]", + }); + const refForm = useRef(); + const [inputsRow, setInputsRow] = useState(inputs); + + async function onSubmit() { + try { + console.log('Starting validation...'); + await refForm.current.validate().then(() => { + console.log('Validation passed'); + const updateArray = compareObjects(inputs, inputsRow); + if (!updateArray.length) return showWarning('你似乎并没有修改什么'); + const requestQueue = updateArray.map((item) => { + let value = ''; + if (typeof inputs[item.key] === 'boolean') { + value = String(inputs[item.key]); + } else { + value = inputs[item.key]; + } + return API.put('/api/option/', { + key: item.key, + value + }); + }); + setLoading(true); + Promise.all(requestQueue) + .then((res) => { + if (requestQueue.length === 1) { + if (res.includes(undefined)) return; + } else if (requestQueue.length > 1) { + if (res.includes(undefined)) + return showError('部分保存失败,请重试'); + } + showSuccess('保存成功'); + props.refresh(); + }) + .catch(() => { + showError('保存失败,请重试'); + }) + .finally(() => { + setLoading(false); + }); + }).catch((error) => { + console.error('Validation failed:', error); + showError('请检查输入'); + }); + } catch (error) { + showError('请检查输入'); + console.error(error); + } + } + + async function resetModelRatio() { + try { + let res = await API.post(`/api/option/rest_model_ratio`); + // return {success, message} + if (res.data.success) { + showSuccess(res.data.message); + props.refresh(); + } else { + showError(res.data.message); + } + } catch (error) { + showError(error); + } + } + + useEffect(() => { + const currentInputs = {}; + for (let key in props.options) { + if (Object.keys(inputs).includes(key)) { + if (key === 'Chats') { + const obj = JSON.parse(props.options[key]); + currentInputs[key] = JSON.stringify(obj, null, 2); + } else { + currentInputs[key] = props.options[key]; + } + } + } + setInputs(currentInputs); + setInputsRow(structuredClone(currentInputs)); + refForm.current.setValues(currentInputs); + }, [props.options]); + + return ( + +
(refForm.current = formAPI)} + style={{ marginBottom: 15 }} + > + + + + { + return verifyJSON(value); + }, + message: '不是合法的 JSON 字符串' + } + ]} + onChange={(value) => + setInputs({ + ...inputs, + Chats: value + }) + } + /> + +
+ + + +
+ ); +} diff --git a/web/src/pages/Setting/Operation/SettingsGeneral.js b/web/src/pages/Setting/Operation/SettingsGeneral.js index ef52371e..cdb6f337 100644 --- a/web/src/pages/Setting/Operation/SettingsGeneral.js +++ b/web/src/pages/Setting/Operation/SettingsGeneral.js @@ -1,5 +1,5 @@ import React, { useEffect, useState, useRef } from 'react'; -import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui'; +import { Banner, Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui'; import { compareObjects, API, @@ -74,6 +74,10 @@ export default function GeneralSettings(props) { return ( <> +
(refForm.current = formAPI)} diff --git a/web/src/pages/Token/EditToken.js b/web/src/pages/Token/EditToken.js index f27ac48c..5cedb745 100644 --- a/web/src/pages/Token/EditToken.js +++ b/web/src/pages/Token/EditToken.js @@ -49,7 +49,7 @@ const EditToken = (props) => { group } = inputs; // const [visible, setVisible] = useState(false); - const [models, setModels] = useState({}); + const [models, setModels] = useState([]); const [groups, setGroups] = useState([]); const navigate = useNavigate(); const handleInputChange = (name, value) => {