Compare commits

...

142 Commits

Author SHA1 Message Date
Calcium-Ion
c1a5b0e1bb Merge pull request #623 from Calcium-Ion/channel-setting
feat: implement channel settings configuration
2024-12-15 15:55:13 +08:00
CalciumIon
f2809917f8 feat: implement channel settings configuration
fix #620
2024-12-15 15:52:41 +08:00
CalciumIon
a4c43bb83b feat: Enhance Operation Settings with Group and Model Ratio Management
- Added new components for GroupRatioSettings and ModelRatioSettings to manage group and model ratios.
- Integrated tabs in OperationSetting to switch between model and visual ratio settings.
- Updated translations for new settings and improved existing ones in the English locale file.
- Refactored ModelSettingsVisualEditor to support dynamic pricing and ratio configurations.

This update improves the user interface for managing operational settings, enhancing usability and localization support.
2024-12-14 22:13:31 +08:00
Calcium-Ion
6ca68651ff Merge pull request #618 from HynoR/feat/modeledit
feat: 可视化模型定价编辑器
2024-12-14 21:32:05 +08:00
Calcium-Ion
e2f6444b83 Merge pull request #617 from kingxjs/main
Available models could not be populated when adding a new channel
2024-12-14 21:31:05 +08:00
Calcium-Ion
8e2b6d0aad Merge pull request #622 from Calcium-Ion/i18n-fix
feat: Enhance i18n support
2024-12-14 14:12:27 +08:00
CalciumIon
41a7cee98e feat: Refactor App and ChannelsTable components for improved i18n support
- Removed redundant user and status loading logic from the App component, centralizing it in the PageLayout component for better maintainability.
- Enhanced the ChannelsTable component by integrating translation functions for various UI elements, ensuring consistent localization of titles and modal messages.
- Updated the English locale file with new translation keys for sub-channel modifications, improving the overall localization coverage.
- Streamlined the code structure in multiple components to enhance readability and performance.
2024-12-14 14:09:30 +08:00
CalciumIon
68b87736b6 feat: Enhance i18n support in Home component and update translations
- Integrated translation functions in the Home component to support dynamic localization for various UI elements, improving accessibility for users in different languages.
- Added new translation keys for "Telegram authentication", "Linux DO authentication", and "License" in the English locale file, expanding the localization coverage.
- Updated existing text elements to utilize translation functions, ensuring consistency in language display across the application.
2024-12-14 12:58:10 +08:00
CalciumIon
b86aeb9150 feat: Implement status loading in App component and refactor SiderBar
- Added a new function to load status data from the API in the App component, enhancing the application's ability to display real-time status updates.
- Integrated error handling for API calls to improve user feedback in case of connection issues.
- Removed the redundant status loading logic from the SiderBar component, streamlining the code and ensuring a single source of truth for status management.
- Updated the useEffect hook in SiderBar to maintain sidebar collapse state based on local storage, improving user experience.
2024-12-14 12:57:56 +08:00
CalciumIon
cfdf6e48f1 Update README 2024-12-13 23:48:18 +08:00
CalciumIon
68b7e0e96a refactor: Remove unused translation function calls in LogsTable component
- Eliminated unnecessary calls to the translation function in the LogsTable component, streamlining the code and improving performance.
- This change enhances readability and reduces potential overhead from unused localization logic.
2024-12-13 22:34:10 +08:00
CalciumIon
84130cdf50 Update README.en.md 2024-12-13 21:21:28 +08:00
CalciumIon
d54cab2ab9 Add README.en.md 2024-12-13 20:21:34 +08:00
CalciumIon
7aa8b88c89 Update README.md 2024-12-13 20:15:50 +08:00
CalciumIon
a485db5065 fix: Refine sider visibility logic in HeaderBar component
- Updated the click handler in HeaderBar to correctly toggle the visibility of the sider based on the current item selection.
- Ensured that the sider is hidden when navigating to the home item and displayed conditionally for other items, improving the user interface responsiveness.
2024-12-13 19:28:09 +08:00
Calcium-Ion
5f06feb9a1 feat: support i18n
feat: support i18n
2024-12-13 19:24:15 +08:00
CalciumIon
221d7b5c99 feat: Integrate i18n support and enhance UI text localization
- Added internationalization (i18n) support across various components, enabling dynamic language switching and improved user experience.
- Updated multiple components to utilize translation functions for labels, buttons, and messages, ensuring consistent language display.
- Enhanced the user interface by refining text elements in the ChannelsTable, LogsTable, and various settings pages, improving clarity and accessibility.
- Adjusted CSS styles for better responsiveness and layout consistency across different screen sizes.
2024-12-13 19:03:14 +08:00
HynoR
3587f2c6e9 feat: 增加价格和倍率的互斥验证,优化模型名称输入提示 2024-12-13 14:45:49 +08:00
HynoR
498590d9fd feat: 优化模型设置可视化编辑器,增强输入验证和提示信息 2024-12-13 14:42:02 +08:00
HynoR
8504e07245 feat: 添加保存功能并优化模型数据提交逻辑 2024-12-13 14:29:43 +08:00
TAKO
2eddb93432 Merge branch 'Calcium-Ion:main' into feat/modeledit 2024-12-13 14:11:31 +08:00
HynoR
369ecf365a feat: 添加模型设置可视化编辑器组件 2024-12-13 14:10:38 +08:00
迷糊虫
36bf4b3926 Update EditChannel.js
Fixes an issue with the OpenAI models interface where it fails to get a list of models.
2024-12-13 13:36:03 +08:00
CalciumIon
2a6f3ad27f fix: Adjust inner padding style in PageLayout component
- Updated the overflowY style to always be 'auto', ensuring consistent scrolling behavior.
- Maintained conditional inner padding based on the styleState, enhancing layout responsiveness.
2024-12-12 23:34:14 +08:00
CalciumIon
cd21aa1c56 feat: init i18n 2024-12-12 23:32:55 +08:00
CalciumIon
16599a900b fix: Refine sider visibility and inner padding logic in StyleProvider component
- Consolidated the logic for managing sider visibility and inner padding based on the current pathname, improving responsiveness to navigation changes.
- Ensured that the sider is hidden on specific paths and adjusted inner padding accordingly, enhancing the user interface experience on both mobile and desktop views.
2024-12-12 20:52:22 +08:00
CalciumIon
d241e4fe29 fix: Update label truncation logic in Playground and adjust sider visibility in HeaderBar
- Modified the group label truncation in the Playground component to shorten labels exceeding 16 characters for better mobile display.
- Corrected the conditional rendering logic in the HeaderBar to toggle the sider visibility based on its current state, enhancing user interface responsiveness.
2024-12-12 20:39:49 +08:00
CalciumIon
e17f36e7b7 fix: Correct inner padding and sider visibility logic in HeaderBar, PageLayout, and SiderBar components
- Updated the click handler in HeaderBar to toggle inner padding and sider visibility correctly based on the selected item.
- Adjusted the conditional rendering of SiderBar in PageLayout to ensure it displays when the sider is shown.
- Refined the inner padding logic in SiderBar to maintain consistent behavior when selecting items.
- Introduced a new function in Style context to manage sider visibility based on the current pathname, enhancing responsiveness to navigation changes.
2024-12-12 20:31:40 +08:00
CalciumIon
263547ebb7 refactor: Simplify average calculations in Detail component
- Streamlined the calculation of average RPM and average TPM by removing unnecessary function calls and directly applying the `toFixed(3)` method within the JSX.
- Improved code readability and maintainability by reducing the number of lines and enhancing clarity in the calculation logic.
2024-12-12 19:21:08 +08:00
CalciumIon
5d338337a0 feat: 兼容OpenAI格式下设置gemini模型联网搜索 #615 2024-12-12 17:58:25 +08:00
CalciumIon
b1fb595610 feat: add model gemini-2.0-flash-exp 2024-12-12 17:21:37 +08:00
CalciumIon
44512d3c28 feat: Enhance group label display in Playground component
- Updated the group selection input to truncate long labels on mobile devices, ensuring better readability and a cleaner interface.
- Implemented a conditional label adjustment that shortens group names exceeding 18 characters, appending '...' for clarity.
2024-12-12 16:35:13 +08:00
Calcium-Ion
430d5fcd6a Merge pull request #616 from Calcium-Ion/panel
feat: 完善数据看板功能
2024-12-12 16:19:27 +08:00
CalciumIon
6625563f80 feat: Enhance quota data handling and CSS styling
- Updated the `increaseQuotaData` function to include `tokenUsed` parameter for better quota tracking.
- Modified the `GetAllQuotaDates` function to sum `token_used` alongside `count` and `quota` for comprehensive data retrieval.
- Improved CSS styles for better layout responsiveness, including padding adjustments for navigation elements and description cards.
2024-12-12 16:18:14 +08:00
CalciumIon
b2d36b946d feat: Update SiderBar and Detail components for improved navigation and data visualization
- Removed the '模型价格' (Pricing) link from the SiderBar for a cleaner interface.
- Added a new '数据看板' (Data Dashboard) link to the SiderBar, enhancing navigation options.
- Refactored the Detail component to include user context and style context for better state management.
- Introduced new state variables to track token consumption and updated data handling for charts.
- Enhanced the layout with additional cards and tabs for displaying user quota and usage statistics.
- Improved data processing logic for pie and line charts, ensuring accurate representation of user data.
2024-12-12 16:11:17 +08:00
CalciumIon
ab4c9fdb8f feat: Enhance color mapping and chart rendering in Detail component
- Added base and extended color palettes for improved model color mapping.
- Introduced a new `modelToColor` function to dynamically assign colors based on model names.
- Updated the Detail component to utilize the new color mapping for pie and line charts.
- Refactored chart data handling to support dynamic color assignment and improved data visualization.
- Cleaned up unused state variables and optimized data loading logic for better performance.
2024-12-12 14:56:16 +08:00
CalciumIon
79de02b05f chore: Update dependencies and refactor JSON handling #614
- Removed the `bytedance/sonic` dependency and replaced its usage with the standard `encoding/json` package for JSON marshalling in `relay-text.go`.
- Updated `go.mod` to reflect the removal of `sonic` and adjusted the version of `sonic/loader`.
- Cleaned up `go.sum` to ensure consistency with the updated dependencies.
2024-12-12 14:14:24 +08:00
Calcium-Ion
0455f30d16 Merge pull request #613 from Calcium-Ion/mobile
feat: Add pricing link to HeaderBar component
2024-12-11 23:14:45 +08:00
Calcium-Ion
21d4dcadab Merge pull request #612 from Calcium-Ion/mobile
feat: Refactor style management for inner padding in layout components
2024-12-11 23:14:10 +08:00
CalciumIon
f0d9c89659 feat: Add pricing link to HeaderBar component
- Introduced a new '定价' (Pricing) item in the HeaderBar navigation for better accessibility to pricing information.
- Updated routing to include the new '/pricing' path.
- Adjusted user display in the HeaderBar for mobile responsiveness, hiding the username on smaller screens for a cleaner interface.
2024-12-11 23:13:46 +08:00
CalciumIon
28fa77cc92 feat: Refactor style management for inner padding in layout components
- Updated HeaderBar, PageLayout, and SiderBar components to manage inner padding state based on selected items.
- Replaced `isChatPage` state with `shouldInnerPadding` in Style context for better clarity and functionality.
- Enhanced user experience by dynamically adjusting content padding based on navigation selections.
2024-12-11 23:08:52 +08:00
Calcium-Ion
db84b26e2d Merge pull request #611 from Calcium-Ion/mobile
feat: 前端美化
2024-12-11 21:41:09 +08:00
CalciumIon
024cdb08df feat: Update model lists and enhance model retrieval in Adaptor
- Refactored ModelList in the gemini constant to include new models and remove outdated ones.
- Modified the GetModelList function in the Adaptor to consolidate model lists from multiple sources, ensuring a comprehensive and updated list is returned.
- Commented out deprecated models in the vertex constants for clarity and future reference.
2024-12-11 21:39:41 +08:00
CalciumIon
89136dfa9e feat: Add filtering and search functionality to model selection in EditChannel and EditTagModal
- Implemented filter and search position options in the model selection dropdowns for both EditChannel and EditTagModal components.
- Enhanced user experience by allowing users to easily find and select models from a potentially large list.
2024-12-11 21:33:30 +08:00
CalciumIon
5f0322b672 feat: Add custom model input functionality in EditTagModal
- Introduced a new input field for adding custom model names in the EditTagModal component.
- Implemented logic to handle the addition of custom models, including validation to prevent duplicates.
- Enhanced user experience by providing feedback when attempting to add existing models.
- Updated state management to reflect changes in the model options dynamically.
2024-12-11 21:31:29 +08:00
CalciumIon
379b08f691 feat: Update user group handling in Playground component
- Enhanced the Playground component to prioritize the user's group by moving it to the front of the local group options if it exists.
- Improved user experience by ensuring the default group selection reflects the user's current group, if available.
2024-12-11 21:25:50 +08:00
CalciumIon
afb7b661ee feat: Implement chat page state management in layout and sidebar
- Added `isChatPage` state to the Style context to manage chat page layout.
- Updated `PageLayout` component to adjust padding based on the chat page state.
- Enhanced `SiderBar` component to dispatch chat page state changes when chat-related items are selected.
2024-12-11 21:17:46 +08:00
CalciumIon
60710d6c68 feat: Add renderModelPriceSimple function and update LogsTable component
- Introduced a new helper function `renderModelPriceSimple` to simplify the rendering of model price information.
- Updated the `LogsTable` component to utilize `renderModelPriceSimple`, enhancing the display of model pricing and grouping information.
- Removed the previous implementation of `renderModelPrice` from the `LogsTable` for cleaner code.
2024-12-11 21:06:26 +08:00
CalciumIon
77b8d918de refactor: Simplify PersonalSetting component layout
- Moved footer content from the Card component to a separate Descriptions component for better structure.
- Maintained the display of user quota, historical consumption, and request count while improving readability.
2024-12-11 20:36:44 +08:00
Calcium-Ion
69f57728b2 Merge pull request #610 from Calcium-Ion/mobile
feat: Update dependencies and restructure Playground component
2024-12-11 18:28:11 +08:00
CalciumIon
7cab9d7c8a feat: Update dependencies and restructure Playground component
- Upgraded @douyinfe/semi-ui from version 2.63.1 to 2.69.1 in package.json.
- Updated pnpm-lock.yaml to reflect new dependency versions and lockfile format.
- Moved Playground component to a new directory structure under pages.
- Enhanced Playground component with new features and improved user experience.
2024-12-11 18:27:30 +08:00
Calcium-Ion
e5dc21d56b Merge pull request #609 from Calcium-Ion/mobile
feat: 界面美化
2024-12-11 17:33:32 +08:00
CalciumIon
713de36ecd feat: Enhance EditRedemption component with default name handling 2024-12-11 17:28:59 +08:00
CalciumIon
64e085dc4c feat: 首页优化 2024-12-11 17:19:03 +08:00
CalciumIon
3622c664b6 feat: 侧边栏移动端优化 2024-12-11 16:11:27 +08:00
CalciumIon
18a8216a43 feat: 优化playground搜索模型功能 2024-12-10 23:48:55 +08:00
CalciumIon
5d1087a6a9 fix: 编辑标签文字错误 2024-12-09 23:45:12 +08:00
CalciumIon
cf8b30edfa fix: edit channel weight and priority 2024-12-09 21:26:17 +08:00
CalciumIon
56ccb30a94 fix: 渠道标签开启下使用ID排序出错 2024-12-09 20:38:03 +08:00
CalciumIon
2c79811cb1 feat: update playground roleConfig 2024-12-09 15:03:04 +08:00
Calcium-Ion
1e1a22e7b3 Merge pull request #605 from jochne/patch-1
Update relay-xunfei.go
2024-12-08 18:50:56 +08:00
jochne
70b5a7fd88 Update relay-xunfei.go
按照讯飞的最新文档,Spark Lite请求地址,对应的domain参数为lite
参考来源:https://www.xfyun.cn/doc/spark/Web.html#_1-接口说明
2024-12-08 01:04:43 +08:00
CalciumIon
dd293f80ae fix: telegram register 2024-12-07 18:08:51 +08:00
Calcium-Ion
904a1858e4 Merge pull request #600 from wzxjohn/upstream
feat: support Azure Comm Service SMTP
2024-12-07 15:24:17 +08:00
wzxjohn
568d4e3f71 feat: support Azure Comm Service SMTP 2024-12-07 00:37:11 +08:00
Calcium-Ion
3eca58093f Merge pull request #597 from daggeryu/patch-3
fix 关键词搜索加标签聚合时,大于1个空标签渠道无法展开的问题
2024-12-06 22:10:08 +08:00
CalciumIon
aa82adc5a9 feat: 兼容渠道搜索下标签聚合功能 2024-12-06 22:03:50 +08:00
CalciumIon
195ab1fdd5 feat: add gemini tool_calls finish reason 2024-12-06 14:31:27 +08:00
daggeryu
b9007ced90 fix 关键词搜索加标签聚合时,大于1个空标签渠道无法展开的问题 2024-12-05 23:21:20 +08:00
Calcium-Ion
8c42ea19b9 Merge pull request #596 from daggeryu/patch-2
fix: 关键词搜索时无法展开测试模型
2024-12-05 22:53:05 +08:00
Calcium-Ion
9ce8940d11 删除无用代码 2024-12-05 22:52:49 +08:00
daggeryu
3aa591785d fix: 关键词搜索时无法展开测试模型 2024-12-05 22:41:55 +08:00
Calcium-Ion
a7b5d684cc Update README.md 2024-12-05 18:07:36 +08:00
CalciumIon
be556a23cc feat: add deepseek channel type 2024-12-05 17:50:08 +08:00
CalciumIon
98373f486e feat: update go-epay 2024-12-05 17:45:54 +08:00
CalciumIon
45bf496183 Update go.mod 2024-12-05 15:25:42 +08:00
Calcium-Ion
c645bf7eb0 Merge pull request #594 from Calcium-Ion/gzip
fix: 标签分组功能开启时无法展开测试模型
2024-12-05 14:42:16 +08:00
CalciumIon
b5d273b680 fix: 标签分组功能开启时无法展开测试模型 2024-12-05 14:41:40 +08:00
Calcium-Ion
6b2f675308 Merge pull request #593 from Calcium-Ion/gzip
feat: support br
2024-12-04 23:56:14 +08:00
CalciumIon
4c809277aa feat: support br 2024-12-04 23:53:02 +08:00
Calcium-Ion
c75bc956b3 Merge pull request #592 from Calcium-Ion/gzip
feat: support gzip
2024-12-04 23:25:19 +08:00
CalciumIon
3089af6b08 feat: support gzip 2024-12-04 23:24:46 +08:00
CalciumIon
d9b622c8ed fix: email panic 2024-12-04 22:08:47 +08:00
Calcium-Ion
4937a6d1ed Merge pull request #591 from Calcium-Ion/no-cache
feat: add Cache-Control header to API requests
2024-12-04 20:53:22 +08:00
CalciumIon
de9a0d65ae feat: add Cache-Control header to API requests 2024-12-04 20:51:55 +08:00
Calcium-Ion
ee99041910 Merge pull request #590 from iszcz/new512
realtime令牌额度检测和http
2024-12-04 19:49:57 +08:00
iszcz
c8a29251ac 1 2024-12-04 16:20:42 +08:00
CalciumIon
07b1c9a4db Update docker-compose.yml 2024-12-03 16:48:38 +08:00
Calcium-Ion
5d8de46e4c Merge pull request #589 from mrhaoji/main
fix: 360智脑接口地址更新
2024-12-03 13:41:14 +08:00
Benny
28885feea2 fix: 360智能接口地址更新 2024-12-02 15:59:08 +00:00
Calcium-Ion
f693c13ce6 Merge pull request #588 from iszcz/new512
渠道tag编辑名称
2024-12-01 23:14:35 +08:00
iszcz
89cd0db28c Update EditTagModal.js 2024-12-01 22:36:51 +08:00
Calcium-Ion
ae57dd7b8b Update README.md 2024-12-01 21:58:36 +08:00
CalciumIon
87d763e641 Merge remote-tracking branch 'origin/main' 2024-12-01 13:59:13 +08:00
Calcium-Ion
08f3562e53 Merge pull request #587 from Calcium-Ion/channel-tag
feat: add tag aggregation mode to channels API and UI
2024-12-01 09:25:43 +08:00
CalciumIon
88b0e6a768 feat: add tag aggregation mode to channels API and UI 2024-12-01 09:24:43 +08:00
CalciumIon
a9f739a7e2 refactor: improve validation logic and error handling in relay-text.go
- Simplified validation checks for MaxTokens and Messages fields.
- Enhanced error messages for better clarity.
- Updated goroutine to avoid passing context unnecessarily.
2024-12-01 08:24:41 +08:00
CalciumIon
6d4edc1f5b fix: realtime 2024-11-30 23:32:42 +08:00
CalciumIon
2d1b2676f7 Update docker-compose.yml 2024-11-30 21:36:34 +08:00
CalciumIon
1035a8e0df Update README.md 2024-11-30 20:47:26 +08:00
Calcium-Ion
ea433b2ed6 Merge pull request #586 from Calcium-Ion/channel-tag
fix: tag channel copy
2024-11-30 19:52:58 +08:00
CalciumIon
bb0c504709 fix: tag channel copy 2024-11-30 19:52:36 +08:00
Calcium-Ion
48abfd055c Merge pull request #574 from Calcium-Ion/channel-tag
feat: 初步集成渠道标签分组功能
2024-11-30 17:45:16 +08:00
CalciumIon
6693072c49 feat: 完善标签编辑(优先级,权重) 2024-11-30 17:43:03 +08:00
CalciumIon
3053d94170 feat: 完善标签编辑 2024-11-30 16:57:58 +08:00
CalciumIon
1774be8536 fix: xAI missing finish_reason #572 2024-11-30 16:57:57 +08:00
Calcium-Ion
821f3a7522 Merge pull request #582 from prnake/patch-1
feat: add claude-3-5-haiku-20241022
2024-11-30 15:07:31 +08:00
CalciumIon
9c4d30602c feat: 完善标签编辑 2024-11-29 23:58:31 +08:00
CalciumIon
7b3394d863 chore: update default STREAMING_TIMEOUT 2024-11-28 23:59:10 +08:00
papersnake
999ba11363 feat: add claude-3-5-haiku-20241022 2024-11-27 13:33:37 +08:00
CalciumIon
7ebc1cfb60 fix: stt模型计费 2024-11-25 19:31:59 +08:00
Calcium-Ion
b71e33b095 Update README.md 2024-11-24 16:51:46 +08:00
CalciumIon
15842163be feat: support audio response_format #580 2024-11-24 16:44:27 +08:00
CalciumIon
e57788375e Update README.md 2024-11-23 16:58:57 +08:00
CalciumIon
78cac7085c Update README.md 2024-11-23 16:38:59 +08:00
Calcium-Ion
76f7474640 Merge pull request #579 from HynoR/main
Chore: support gpt-4o-2024-11-20
2024-11-23 16:34:41 +08:00
CalciumIon
0dd1953cd6 Update BT.md 2024-11-23 16:33:24 +08:00
Calcium-Ion
019361a762 Update BT.md 2024-11-23 16:28:06 +08:00
CalciumIon
9b9d73e725 Update README.md 2024-11-23 16:27:39 +08:00
CalciumIon
9e08709756 chore: Update docker-compose.yml 2024-11-23 16:26:30 +08:00
CalciumIon
05b5d6f255 Update README.md 2024-11-23 16:26:15 +08:00
HynoR
79b6c0a73e Chore: support gpt-4o-2024-11-20 2024-11-21 09:14:23 +08:00
Calcium-Ion
462c2cc1a1 Merge pull request #493 from xixingya/feature/bug-fix
ratio must gte 0
2024-11-19 18:34:24 +08:00
CalciumIon
e8286e479b chore: go.mod 2024-11-19 15:14:40 +08:00
CalciumIon
ed2ec69545 feat: 暂时禁用透传功能 2024-11-19 15:06:54 +08:00
CalciumIon
a167dd9a23 feat: 暂时禁用透传功能 2024-11-19 15:04:19 +08:00
CalciumIon
6e6e390f6f feat: 一键编辑标签下渠道重定向 2024-11-19 01:43:05 +08:00
CalciumIon
807385d3d1 fix: search channel #442 2024-11-19 01:39:27 +08:00
CalciumIon
0ce600ed49 feat: 渠道标签分组 2024-11-19 01:13:18 +08:00
CalciumIon
334a2424e9 fix: oauth aff 2024-11-18 18:53:55 +08:00
CalciumIon
7db703374c fix: oauth aff 2024-11-18 18:52:14 +08:00
Calcium-Ion
6a42ccf00e Merge pull request #569 from utopeadia/main
Modify the default gemini API to v1beta
2024-11-17 16:27:38 +08:00
Calcium-Ion
7aa7114bb9 Merge branch 'main' into main 2024-11-17 16:27:29 +08:00
Calcium-Ion
c3e6b2408e Merge pull request #570 from leezhuuuuu/main
增加对于gemini-exp-1114模型的支持,映射到v1beta
2024-11-17 16:26:36 +08:00
leezhuuuuu
4601932902 feat: add support for gemini-exp-1114 model / 添加 gemini-exp-1114 模型支持
# feat: add support for gemini-exp-1114 model / 添加 gemini-exp-1114 模型支持

## Changes / 更改内容
- Add gemini-exp-1114 to ModelList in constant.go
- Add gemini-exp-1114 to GeminiModelMap with v1beta API version
- 在 constant.go 的 ModelList 中添加 gemini-exp-1114 模型
- 在 GeminiModelMap 中添加 gemini-exp-1114 的 v1beta API 版本映射

## Testing / 测试情况
- [x] Tested gemini-exp-1114 model API calls / 已测试 gemini-exp-1114 模型的 API 调用
- [x] Verified existing models still work / 验证现有模型仍然正常工作
- [x] Confirmed v1beta API version works correctly / 确认 v1beta API 版本正常工作

## Related Issues / 相关问题
- Fix 404 error when calling gemini-exp-1114 model / 修复调用 gemini-exp-1114 模型时的 404 错误

## Implementation Details / 实现细节
- Use configuration-based approach instead of code modification / 使用基于配置的方式而不是修改代码
- Maintain clean separation of concerns / 保持关注点分离
- Keep backward compatibility / 保持向后兼容性

## Notes / 注意事项
- This PR follows the principle of minimal invasion / 本 PR 遵循最小侵入原则
- Configuration changes only / 仅包含配置更改
2024-11-16 21:52:37 +08:00
leezhuuuuu
5d96f7b2cc 增加对于gemini-exp-1114模型的支持,映射到v1beta
feat(gemini): add support for gemini-exp-1114 model

- Add gemini-exp-1114 to ModelList in constant.go
- Update GetRequestURL in adaptor.go to use v1beta API version for gemini-exp-1114
- Keep backward compatibility for other models

This change enables the use of the experimental gemini-exp-1114 model by correctly routing its requests to the v1beta API endpoint while maintaining existing functionality for other models.
2024-11-16 21:29:35 +08:00
HowieWood
8eb32e9b3f Modify the default gemini API to v1beta 2024-11-16 12:21:50 +00:00
CalciumIon
320e6ec5a4 fix: aws claude 2024-11-14 15:12:34 +08:00
liuzhifei
e0f19e5ed7 ratio must gte 0 2024-09-20 18:33:17 +08:00
liuzhifei
3d33079de0 ratio must gte 0 2024-09-20 18:27:16 +08:00
liuzhifei
1d064a2e88 ratio must gte 0 2024-09-20 18:09:40 +08:00
liuzhifei
4eae3b2177 ratio must gte 0 2024-09-20 17:51:42 +08:00
114 changed files with 11077 additions and 5197 deletions

View File

@@ -53,7 +53,7 @@
# Gemini 安全设置
# GEMINI_SAFETY_SETTING=BLOCK_NONE
# Gemini版本设置
# GEMINI_MODEL_MAP=gemini-1.5-pro-latest:v1beta,gemini-1.5-pro-001:v1beta
# GEMINI_MODEL_MAP=gemini-1.0-pro:v1
# Cohere 安全设置
# COHERE_SAFETY_SETTING=NONE
# 是否统计图片token

3
BT.md Normal file
View File

@@ -0,0 +1,3 @@
密钥为环境变量SESSION_SECRET
![8285bba413e770fe9620f1bf9b40d44e](https://github.com/user-attachments/assets/7a6fc03e-c457-45e4-b8f9-184508fc26b0)

150
README.en.md Normal file
View File

@@ -0,0 +1,150 @@
<div align="center">
![new-api](/web/public/logo.png)
# New API
🍥 Next Generation LLM Gateway and AI Asset Management System
<a href="https://trendshift.io/repositories/8227" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8227" alt="Calcium-Ion%2Fnew-api | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<p align="center">
<a href="https://raw.githubusercontent.com/Calcium-Ion/new-api/main/LICENSE">
<img src="https://img.shields.io/github/license/Calcium-Ion/new-api?color=brightgreen" alt="license">
</a>
<a href="https://github.com/Calcium-Ion/new-api/releases/latest">
<img src="https://img.shields.io/github/v/release/Calcium-Ion/new-api?color=brightgreen&include_prereleases" alt="release">
</a>
<a href="https://github.com/users/Calcium-Ion/packages/container/package/new-api">
<img src="https://img.shields.io/badge/docker-ghcr.io-blue" alt="docker">
</a>
<a href="https://hub.docker.com/r/CalciumIon/new-api">
<img src="https://img.shields.io/badge/docker-dockerHub-blue" alt="docker">
</a>
<a href="https://goreportcard.com/report/github.com/Calcium-Ion/new-api">
<img src="https://goreportcard.com/badge/github.com/Calcium-Ion/new-api" alt="GoReportCard">
</a>
</p>
</div>
## 📝 Project Description
> [!NOTE]
> This is an open-source project developed based on [One API](https://github.com/songquanpeng/one-api)
> [!IMPORTANT]
> - Users must comply with OpenAI's [Terms of Use](https://openai.com/policies/terms-of-use) and relevant laws and regulations. Not to be used for illegal purposes.
> - This project is for personal learning only. Stability is not guaranteed, and no technical support is provided.
## ✨ Key Features
1. 🎨 New UI interface (some interfaces pending update)
2. 🌍 Multi-language support (work in progress)
3. 🎨 Added [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy) interface support, [Integration Guide](Midjourney.md)
4. 💰 Online recharge support, configurable in system settings:
- [x] EasyPay
5. 🔍 Query usage quota by key:
- Works with [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)
6. 📑 Configurable items per page in pagination
7. 🔄 Compatible with original One API database (one-api.db)
8. 💵 Support per-request model pricing, configurable in System Settings - Operation Settings
9. ⚖️ Support channel **weighted random** selection
10. 📈 Data dashboard (console)
11. 🔒 Configurable model access per token
12. 🤖 Telegram authorization login support:
1. System Settings - Configure Login Registration - Allow Telegram Login
2. Send /setdomain command to [@Botfather](https://t.me/botfather)
3. Select your bot, then enter http(s)://your-website/login
4. Telegram Bot name is the bot username without @
13. 🎵 Added [Suno API](https://github.com/Suno-API/Suno-API) interface support, [Integration Guide](Suno.md)
14. 🔄 Support for Rerank models, compatible with Cohere and Jina, can integrate with Dify, [Integration Guide](Rerank.md)
15.**[OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime/integration)** - Support for OpenAI's Realtime API, including Azure channels
## Model Support
This version additionally supports:
1. Third-party model **gps** (gpt-4-gizmo-*)
2. [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy) interface, [Integration Guide](Midjourney.md)
3. Custom channels with full API URL support
4. [Suno API](https://github.com/Suno-API/Suno-API) interface, [Integration Guide](Suno.md)
5. Rerank models, supporting [Cohere](https://cohere.ai/) and [Jina](https://jina.ai/), [Integration Guide](Rerank.md)
6. Dify
You can add custom models gpt-4-gizmo-* in channels. These are third-party models and cannot be called with official OpenAI keys.
## Additional Configurations Beyond One API
- `GENERATE_DEFAULT_TOKEN`: Generate initial token for new users, default `false`
- `STREAMING_TIMEOUT`: Set streaming response timeout, default 60 seconds
- `DIFY_DEBUG`: Output workflow and node info to client for Dify channel, default `true`
- `FORCE_STREAM_OPTION`: Override client stream_options parameter, default `true`
- `GET_MEDIA_TOKEN`: Calculate image tokens, default `true`
- `GET_MEDIA_TOKEN_NOT_STREAM`: Calculate image tokens in non-stream mode, default `true`
- `UPDATE_TASK`: Update async tasks (Midjourney, Suno), default `true`
- `GEMINI_MODEL_MAP`: Specify Gemini model versions (v1/v1beta), format: "model:version", comma-separated
- `COHERE_SAFETY_SETTING`: Cohere model [safety settings](https://docs.cohere.com/docs/safety-modes#overview), options: `NONE`, `CONTEXTUAL`, `STRICT`, default `NONE`
## Deployment
> [!TIP]
> Latest Docker image: `calciumion/new-api:latest`
> Default account: root, password: 123456
> Update command:
> ```
> docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR
> ```
### Requirements
- Local database (default): SQLite (Docker deployment must mount `/data` directory)
- Remote database: MySQL >= 5.7.8, PgSQL >= 9.6
### Docker Deployment
### Using Docker Compose (Recommended)
```shell
# Clone project
git clone https://github.com/Calcium-Ion/new-api.git
cd new-api
# Edit docker-compose.yml as needed
# Start
docker-compose up -d
```
### Direct Docker Image Usage
```shell
# SQLite deployment:
docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
# MySQL deployment (add -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi"), modify database connection parameters as needed
# Example:
docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
```
## Channel Retry
Channel retry is implemented, configurable in `Settings->Operation Settings->General Settings`. **Cache recommended**.
First retry uses same priority, second retry uses next priority, and so on.
### Cache Configuration
1. `REDIS_CONN_STRING`: Use Redis as cache
+ Example: `REDIS_CONN_STRING=redis://default:redispw@localhost:49153`
2. `MEMORY_CACHE_ENABLED`: Enable memory cache, default `false`
+ Example: `MEMORY_CACHE_ENABLED=true`
### Why Some Errors Don't Retry
Error codes 400, 504, 524 won't retry
### To Enable Retry for 400
In `Channel->Edit`, set `Status Code Override` to:
```json
{
"400": "500"
}
```
## Integration Guides
- [Midjourney Integration](Midjourney.md)
- [Suno Integration](Suno.md)
## Related Projects
- [One API](https://github.com/songquanpeng/one-api): Original project
- [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy): Midjourney interface support
- [chatnio](https://github.com/Deeptrain-Community/chatnio): Next-gen AI B/C solution
- [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool): Query usage quota by key
## 🌟 Star History
[![Star History Chart](https://api.star-history.com/svg?repos=Calcium-Ion/new-api&type=Date)](https://star-history.com/#Calcium-Ion/new-api&Date)

167
README.md
View File

@@ -1,21 +1,92 @@
<p align="right">
<strong>中文</strong> | <a href="./README.en.md">English</a>
</p>
<div align="center">
![new-api](/web/public/logo.png)
# New API
🍥新一代大模型网关与AI资产管理系统
<a href="https://trendshift.io/repositories/8227" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8227" alt="Calcium-Ion%2Fnew-api | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<p align="center">
<a href="https://raw.githubusercontent.com/Calcium-Ion/new-api/main/LICENSE">
<img src="https://img.shields.io/github/license/Calcium-Ion/new-api?color=brightgreen" alt="license">
</a>
<a href="https://github.com/Calcium-Ion/new-api/releases/latest">
<img src="https://img.shields.io/github/v/release/Calcium-Ion/new-api?color=brightgreen&include_prereleases" alt="release">
</a>
<a href="https://github.com/users/Calcium-Ion/packages/container/package/new-api">
<img src="https://img.shields.io/badge/docker-ghcr.io-blue" alt="docker">
</a>
<a href="https://hub.docker.com/r/CalciumIon/new-api">
<img src="https://img.shields.io/badge/docker-dockerHub-blue" alt="docker">
</a>
<a href="https://goreportcard.com/report/github.com/Calcium-Ion/new-api">
<img src="https://goreportcard.com/badge/github.com/Calcium-Ion/new-api" alt="GoReportCard">
</a>
</p>
</div>
> [!NOTE]
## 📝 项目说明
> [!NOTE]
> 本项目为开源项目,在[One API](https://github.com/songquanpeng/one-api)的基础上进行二次开发
> [!IMPORTANT]
> 使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
> 本项目仅供个人学习使用,不保证稳定性,且不提供任何技术支持。
> 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
> [!IMPORTANT]
> - 使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
> - 本项目仅供个人学习使用,不保证稳定性,且不提供任何技术支持。
> - 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
## ✨ 主要特性
1. 🎨 全新的UI界面部分界面还待更新
2. 🌍 多语言支持(待完善)
3. 🎨 添加[Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口支持,[对接文档](Midjourney.md)
4. 💰 支持在线充值功能,可在系统设置中设置:
- [x] 易支付
5. 🔍 支持用key查询使用额度
- 配合项目[neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)可实现用key查询使用
6. 📑 分页支持选择每页显示数量
7. 🔄 兼容原版One API的数据库可直接使用原版数据库one-api.db
8. 💵 支持模型按次数收费,可在 系统设置-运营设置 中设置
9. ⚖️ 支持渠道**加权随机**
10. 📈 数据看板(控制台)
11. 🔒 可设置令牌能调用的模型
12. 🤖 支持Telegram授权登录
1. 系统设置-配置登录注册-允许通过Telegram登录
2. 对[@Botfather](https://t.me/botfather)输入指令/setdomain
3. 选择你的bot然后输入http(s)://你的网站地址/login
4. Telegram Bot 名称是bot username 去掉@后的字符串
13. 🎵 添加 [Suno API](https://github.com/Suno-API/Suno-API)接口支持,[对接文档](Suno.md)
14. 🔄 支持Rerank模型目前兼容Cohere和Jina可接入Dify[对接文档](Rerank.md)
15.**[OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime/integration)** - 支持OpenAI的Realtime API支持Azure渠道
## 模型支持
此版本额外支持以下模型:
1. 第三方模型 **gps** gpt-4-gizmo-*
2. [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口,[对接文档](Midjourney.md)
3. 自定义渠道,支持填入完整调用地址
4. [Suno API](https://github.com/Suno-API/Suno-API) 接口,[对接文档](Suno.md)
5. Rerank模型目前支持[Cohere](https://cohere.ai/)和[Jina](https://jina.ai/)[对接文档](Rerank.md)
6. Dify
您可以在渠道中添加自定义模型gpt-4-gizmo-*此模型并非OpenAI官方模型而是第三方模型使用官方key无法调用。
## 比原版One API多出的配置
- `GENERATE_DEFAULT_TOKEN`:是否为新注册用户生成初始令牌,默认为 `false`
- `STREAMING_TIMEOUT`:设置流式一次回复的超时时间,默认为 60 秒。
- `DIFY_DEBUG`:设置 Dify 渠道是否输出工作流和节点信息到客户端,默认为 `true`
- `FORCE_STREAM_OPTION`是否覆盖客户端stream_options参数请求上游返回流模式usage默认为 `true`建议开启不影响客户端传入stream_options参数返回结果。
- `GET_MEDIA_TOKEN`是否统计图片token默认为 `true`关闭后将不再在本地计算图片token可能会导致和上游计费不同此项覆盖 `GET_MEDIA_TOKEN_NOT_STREAM` 选项作用。
- `GET_MEDIA_TOKEN_NOT_STREAM`:是否在非流(`stream=false`情况下统计图片token默认为 `true`
- `UPDATE_TASK`是否更新异步任务Midjourney、Suno默认为 `true`,关闭后将不会更新任务进度。
- `GEMINI_MODEL_MAP`Gemini模型指定版本(v1/v1beta),使用“模型:版本”指定,","分隔,例如:-e GEMINI_MODEL_MAP="gemini-1.5-pro-latest:v1beta,gemini-1.5-pro-001:v1beta",为空则使用默认配置(v1beta)
- `COHERE_SAFETY_SETTING`Cohere模型[安全设置](https://docs.cohere.com/docs/safety-modes#overview),可选值为 `NONE`, `CONTEXTUAL``STRICT`,默认为 `NONE`
## 部署
> [!TIP]
> 最新版Docker镜像`calciumion/new-api:latest`
> 默认账号root 密码123456
@@ -24,63 +95,28 @@
> docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR
> ```
## 主要变更
此分叉版本的主要变更如下:
1. 全新的UI界面部分界面还待更新
2. 添加[Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口的支持,[对接文档](Midjourney.md)
3. 支持在线充值功能,可在系统设置中设置,当前支持的支付接口:
+ [x] 易支付
4. 支持用key查询使用额度:
+ 配合项目[neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)可实现用key查询使用
5. 渠道显示已使用额度,支持指定组织访问
6. 分页支持选择每页显示数量
7. 兼容原版One API的数据库可直接使用原版数据库one-api.db
8. 支持模型按次数收费,可在 系统设置-运营设置 中设置
9. 支持渠道**加权随机**
10. 数据看板
11. 可设置令牌能调用的模型
12. 支持Telegram授权登录。
1. 系统设置-配置登录注册-允许通过Telegram登录
2. 对[@Botfather](https://t.me/botfather)输入指令/setdomain
3. 选择你的bot然后输入http(s)://你的网站地址/login
4. Telegram Bot 名称是bot username 去掉@后的字符串
13. 添加 [Suno API](https://github.com/Suno-API/Suno-API)接口的支持,[对接文档](Suno.md)
14. 支持Rerank模型目前仅兼容Cohere和Jina可接入Dify[对接文档](Rerank.md)
15. **[OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime/integration)** - 支持OpenAI的Realtime API支持Azure渠道。
## 模型支持
此版本额外支持以下模型:
1. 第三方模型 **gps** gpt-4-gizmo-*
2. 智谱glm-4vglm-4v识图
3. Anthropic Claude 3
4. [Ollama](https://github.com/ollama/ollama?tab=readme-ov-file),添加渠道时,密钥可以随便填写,默认的请求地址是[http://localhost:11434](http://localhost:11434),如果需要修改请在渠道中修改
5. [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口,[对接文档](Midjourney.md)
6. [零一万物](https://platform.lingyiwanwu.com/)
7. 自定义渠道,支持填入完整调用地址
8. [Suno API](https://github.com/Suno-API/Suno-API) 接口,[对接文档](Suno.md)
9. Rerank模型目前支持[Cohere](https://cohere.ai/)和[Jina](https://jina.ai/)[对接文档](Rerank.md)
10. Dify
11. Vertex AI目前兼容ClaudeGeminiLlama3.1
您可以在渠道中添加自定义模型gpt-4-gizmo-*此模型并非OpenAI官方模型而是第三方模型使用官方key无法调用。
## 比原版One API多出的配置
- `GENERATE_DEFAULT_TOKEN`:是否为新注册用户生成初始令牌,默认为 `false`
- `STREAMING_TIMEOUT`:设置流式一次回复的超时时间,默认为 30 秒。
- `DIFY_DEBUG`:设置 Dify 渠道是否输出工作流和节点信息到客户端,默认为 `true`
- `FORCE_STREAM_OPTION`是否覆盖客户端stream_options参数请求上游返回流模式usage默认为 `true`建议开启不影响客户端传入stream_options参数返回结果。
- `GET_MEDIA_TOKEN`是统计图片token默认为 `true`关闭后将不再在本地计算图片token可能会导致和上游计费不同此项覆盖 `GET_MEDIA_TOKEN_NOT_STREAM` 选项作用。
- `GET_MEDIA_TOKEN_NOT_STREAM`:是否在非流(`stream=false`情况下统计图片token默认为 `true`
- `UPDATE_TASK`是否更新异步任务Midjourney、Suno默认为 `true`,关闭后将不会更新任务进度。
- `GEMINI_MODEL_MAP`Gemini模型指定版本(v1/v1beta),使用“模型:版本”指定,","分隔,例如:-e GEMINI_MODEL_MAP="gemini-1.5-pro-latest:v1beta,gemini-1.5-pro-001:v1beta",为空则使用默认配置
- `COHERE_SAFETY_SETTING`Cohere模型[安全设置](https://docs.cohere.com/docs/safety-modes#overview),可选值为 `NONE`, `CONTEXTUAL``STRICT`,默认为 `NONE`
## 部署
### 部署要求
- 本地数据库默认SQLiteDocker 部署默认使用 SQLite必须挂载 `/data` 目录到宿主机)
- 远程数据库MySQL 版本 >= 5.7.8PgSQL 版本 >= 9.6
### 使用宝塔面板Docker功能部署
安装宝塔面板 (**9.2.0版本**及以上),前往 [宝塔面板](https://www.bt.cn/new/download.html) 官网,选择正式版的脚本下载安装
安装后登录宝塔面板,在菜单栏中点击 Docker ,首次进入会提示安装 Docker 服务,点击立即安装,按提示完成安装
安装完成后在应用商店中找到 **New-API** ,点击安装,配置基本选项 即可完成安装
[图文教程](BT.md)
### 基于 Docker 进行部署
### 使用 Docker Compose 部署(推荐)
```shell
# 下载项目
git clone https://github.com/Calcium-Ion/new-api.git
cd new-api
# 按需编辑 docker-compose.yml
# 启动
docker-compose up -d
```
### 直接使用 Docker 镜像
```shell
# 使用 SQLite 的部署命令:
docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
@@ -88,16 +124,6 @@ docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -
# 例如:
docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
```
### 使用宝塔面板Docker功能部署
```shell
# 使用 SQLite 的部署命令:
docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /www/wwwroot/new-api:/data calciumion/new-api:latest
# 使用 MySQL 的部署命令,在上面的基础上添加 `-e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi"`,请自行修改数据库连接参数。
# 例如:
# 注意数据库要开启远程访问并且只允许服务器IP访问
docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(宝塔的服务器地址:宝塔数据库端口)/宝塔数据库名称" -e TZ=Asia/Shanghai -v /www/wwwroot/new-api:/data calciumion/new-api:latest
# 注意数据库要开启远程访问并且只允许服务器IP访问
```
## 渠道重试
渠道重试功能已经实现,可以在`设置->运营设置->通用设置`设置重试次数,**建议开启缓存**功能。
@@ -128,6 +154,7 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
![796df8d287b7b7bd7853b2497e7df511](https://github.com/user-attachments/assets/255b5e97-2d3a-4434-b4fa-e922ad88ff5a)
![image](https://github.com/Calcium-Ion/new-api/assets/61247483/ad0e7aae-0203-471c-9716-2d83768927d4)
![image](https://github.com/user-attachments/assets/29f81de5-33fc-4fc5-a5ff-f9b54b653c7c)
![image](https://github.com/Calcium-Ion/new-api/assets/61247483/3ca0b282-00ff-4c96-bf9d-e29ef615c605)
夜间模式
@@ -135,7 +162,7 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
![image](https://github.com/Calcium-Ion/new-api/assets/61247483/af9a07ee-5101-4b3d-8bd9-ae21a4fd7e9e)
## 交流群
<img src="https://github.com/Calcium-Ion/new-api/assets/61247483/de536a8a-0161-47a7-a0a2-66ef6de81266" width="200">
<img src="https://github.com/user-attachments/assets/9ca0bc82-e057-4230-a28d-9f198fa022e3" width="200">
## 相关项目
- [One API](https://github.com/songquanpeng/one-api):原版项目
@@ -143,6 +170,6 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
- [chatnio](https://github.com/Deeptrain-Community/chatnio):下一代 AI 一站式 B/C 端解决方案
- [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)用key查询使用额度
## Star History
## 🌟 Star History
[![Star History Chart](https://api.star-history.com/svg?repos=Calcium-Ion/new-api&type=Date)](https://star-history.com/#Calcium-Ion/new-api&Date)

View File

@@ -229,6 +229,7 @@ const (
ChannelTypeSiliconFlow = 40
ChannelTypeVertexAi = 41
ChannelTypeMistral = 42
ChannelTypeDeepSeek = 43
ChannelTypeDummy // this one is only for count, do not add any channel after this
@@ -254,7 +255,7 @@ var ChannelBaseURLs = []string{
"https://open.bigmodel.cn", // 16
"https://dashscope.aliyuncs.com", // 17
"", // 18
"https://ai.360.cn", // 19
"https://api.360.cn", // 19
"https://openrouter.ai/api", // 20
"https://api.aiproxy.io", // 21
"https://fastgpt.run/api/openapi", // 22
@@ -278,4 +279,5 @@ var ChannelBaseURLs = []string{
"https://api.siliconflow.cn", //40
"", //41
"https://api.mistral.ai", //42
"https://api.deepseek.com", //43
}

View File

@@ -9,15 +9,23 @@ import (
"time"
)
func generateMessageID() string {
domain := strings.Split(SMTPAccount, "@")[1]
return fmt.Sprintf("<%d.%s@%s>", time.Now().UnixNano(), GetRandomString(12), domain)
func generateMessageID() (string, error) {
split := strings.Split(SMTPFrom, "@")
if len(split) < 2 {
return "", fmt.Errorf("invalid SMTP account")
}
domain := strings.Split(SMTPFrom, "@")[1]
return fmt.Sprintf("<%d.%s@%s>", time.Now().UnixNano(), GetRandomString(12), domain), nil
}
func SendEmail(subject string, receiver string, content string) error {
if SMTPFrom == "" { // for compatibility
SMTPFrom = SMTPAccount
}
id, err2 := generateMessageID()
if err2 != nil {
return err2
}
if SMTPServer == "" && SMTPAccount == "" {
return fmt.Errorf("SMTP 服务器未配置")
}
@@ -28,7 +36,7 @@ func SendEmail(subject string, receiver string, content string) error {
"Date: %s\r\n"+
"Message-ID: %s\r\n"+ // 添加 Message-ID 头
"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n",
receiver, SystemName, SMTPFrom, encodedSubject, time.Now().Format(time.RFC1123Z), generateMessageID(), content))
receiver, SystemName, SMTPFrom, encodedSubject, time.Now().Format(time.RFC1123Z), id, content))
auth := smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer)
addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort)
to := strings.Split(receiver, ";")
@@ -71,11 +79,11 @@ func SendEmail(subject string, receiver string, content string) error {
if err != nil {
return err
}
} else if isOutlookServer(SMTPAccount) {
} else if isOutlookServer(SMTPAccount) || SMTPServer == "smtp.azurecomm.net" {
auth = LoginAuth(SMTPAccount, SMTPToken)
err = smtp.SendMail(addr, auth, SMTPAccount, to, mail)
err = smtp.SendMail(addr, auth, SMTPFrom, to, mail)
} else {
err = smtp.SendMail(addr, auth, SMTPAccount, to, mail)
err = smtp.SendMail(addr, auth, SMTPFrom, to, mail)
}
return err
}

View File

@@ -2,6 +2,7 @@ package common
import (
"encoding/json"
"errors"
)
var GroupRatio = map[string]float64{
@@ -31,3 +32,17 @@ func GetGroupRatio(name string) float64 {
}
return ratio
}
func CheckGroupRatio(jsonStr string) error {
checkGroupRatio := make(map[string]float64)
err := json.Unmarshal([]byte(jsonStr), &checkGroupRatio)
if err != nil {
return err
}
for name, ratio := range checkGroupRatio {
if ratio < 0 {
return errors.New("group ratio must be not less than 0: " + name)
}
}
return nil
}

View File

@@ -33,17 +33,18 @@ var defaultModelRatio = map[string]float64{
"gpt-4-32k": 30,
//"gpt-4-32k-0314": 30, //deprecated
"gpt-4-32k-0613": 30,
"gpt-4-1106-preview": 5, // $0.01 / 1K tokens
"gpt-4-0125-preview": 5, // $0.01 / 1K tokens
"gpt-4-turbo-preview": 5, // $0.01 / 1K tokens
"gpt-4-vision-preview": 5, // $0.01 / 1K tokens
"gpt-4-1106-vision-preview": 5, // $0.01 / 1K tokens
"chatgpt-4o-latest": 2.5, // $0.01 / 1K tokens
"gpt-4o": 1.25, // $0.01 / 1K tokens
"gpt-4o-audio-preview": 1.25, // $0.0015 / 1K tokens
"gpt-4o-audio-preview-2024-10-01": 1.25, // $0.0015 / 1K tokens
"gpt-4o-2024-08-06": 1.25, // $0.01 / 1K tokens
"gpt-4o-2024-05-13": 2.5,
"gpt-4-1106-preview": 5, // $10 / 1M tokens
"gpt-4-0125-preview": 5, // $10 / 1M tokens
"gpt-4-turbo-preview": 5, // $10 / 1M tokens
"gpt-4-vision-preview": 5, // $10 / 1M tokens
"gpt-4-1106-vision-preview": 5, // $10 / 1M tokens
"chatgpt-4o-latest": 2.5, // $5 / 1M tokens
"gpt-4o": 1.25, // $2.5 / 1M tokens
"gpt-4o-audio-preview": 1.25, // $2.5 / 1M tokens
"gpt-4o-audio-preview-2024-10-01": 1.25, // $2.5 / 1M tokens
"gpt-4o-2024-05-13": 2.5, // $5 / 1M tokens
"gpt-4o-2024-08-06": 1.25, // $2.5 / 1M tokens
"gpt-4o-2024-11-20": 1.25, // $2.5 / 1M tokens
"gpt-4o-realtime-preview": 2.5,
"o1-preview": 7.5,
"o1-preview-2024-09-12": 7.5,
@@ -149,6 +150,7 @@ var defaultModelRatio = map[string]float64{
"360gpt-turbo": 0.0858, // ¥0.0012 / 1k tokens
"360gpt-turbo-responsibility-8k": 0.8572, // ¥0.012 / 1k tokens
"360gpt-pro": 0.8572, // ¥0.012 / 1k tokens
"360gpt2-pro": 0.8572, // ¥0.012 / 1k tokens
"embedding-bert-512-v1": 0.0715, // ¥0.001 / 1k tokens
"embedding_s1_v1": 0.0715, // ¥0.001 / 1k tokens
"semantic_similarity_s1_v1": 0.0715, // ¥0.001 / 1k tokens

View File

@@ -0,0 +1,5 @@
package constant
var (
ForceFormat = "force_format" // ForceFormat 强制格式化为OpenAI格式
)

View File

@@ -7,7 +7,7 @@ import (
"strings"
)
var StreamingTimeout = common.GetEnvOrDefault("STREAMING_TIMEOUT", 30)
var StreamingTimeout = common.GetEnvOrDefault("STREAMING_TIMEOUT", 60)
var DifyDebug = common.GetEnvOrDefaultBool("DIFY_DEBUG", true)
// ForceStreamOption 覆盖请求参数强制返回usage信息
@@ -20,16 +20,7 @@ var GetMediaTokenNotStream = common.GetEnvOrDefaultBool("GET_MEDIA_TOKEN_NOT_STR
var UpdateTask = common.GetEnvOrDefaultBool("UPDATE_TASK", true)
var GeminiModelMap = map[string]string{
"gemini-1.5-pro-latest": "v1beta",
"gemini-1.5-pro-001": "v1beta",
"gemini-1.5-pro": "v1beta",
"gemini-1.5-pro-exp-0801": "v1beta",
"gemini-1.5-pro-exp-0827": "v1beta",
"gemini-1.5-flash-latest": "v1beta",
"gemini-1.5-flash-exp-0827": "v1beta",
"gemini-1.5-flash-001": "v1beta",
"gemini-1.5-flash": "v1beta",
"gemini-ultra": "v1beta",
"gemini-1.0-pro": "v1",
}
func InitEnv() {

View File

@@ -0,0 +1,6 @@
package constant
var (
FinishReasonStop = "stop"
FinishReasonToolCalls = "tool_calls"
)

View File

@@ -3,12 +3,13 @@ package controller
import (
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"one-api/common"
"one-api/model"
"strconv"
"strings"
"github.com/gin-gonic/gin"
)
type OpenAIModel struct {
@@ -48,19 +49,41 @@ func GetAllChannels(c *gin.Context) {
if pageSize < 0 {
pageSize = common.ItemsPerPage
}
channelData := make([]*model.Channel, 0)
idSort, _ := strconv.ParseBool(c.Query("id_sort"))
channels, err := model.GetAllChannels(p*pageSize, pageSize, false, idSort)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode"))
if enableTagMode {
tags, err := model.GetPaginatedTags(p*pageSize, pageSize)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
for _, tag := range tags {
if tag != nil && *tag != "" {
tagChannel, err := model.GetChannelsByTag(*tag, idSort)
if err == nil {
channelData = append(channelData, tagChannel...)
}
}
}
} else {
channels, err := model.GetAllChannels(p*pageSize, pageSize, false, idSort)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
channelData = channels
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": channels,
"data": channelData,
})
return
}
@@ -144,19 +167,41 @@ func SearchChannels(c *gin.Context) {
keyword := c.Query("keyword")
group := c.Query("group")
modelKeyword := c.Query("model")
//idSort, _ := strconv.ParseBool(c.Query("id_sort"))
channels, err := model.SearchChannels(keyword, group, modelKeyword)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
idSort, _ := strconv.ParseBool(c.Query("id_sort"))
enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode"))
channelData := make([]*model.Channel, 0)
if enableTagMode {
tags, err := model.SearchTags(keyword, group, modelKeyword, idSort)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
for _, tag := range tags {
if tag != nil && *tag != "" {
tagChannel, err := model.GetChannelsByTag(*tag, idSort)
if err == nil {
channelData = append(channelData, tagChannel...)
}
}
}
} else {
channels, err := model.SearchChannels(keyword, group, modelKeyword, idSort)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
channelData = channels
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": channels,
"data": channelData,
})
return
}
@@ -279,6 +324,98 @@ func DeleteDisabledChannel(c *gin.Context) {
return
}
type ChannelTag struct {
Tag string `json:"tag"`
NewTag *string `json:"new_tag"`
Priority *int64 `json:"priority"`
Weight *uint `json:"weight"`
ModelMapping *string `json:"model_mapping"`
Models *string `json:"models"`
Groups *string `json:"groups"`
}
func DisableTagChannels(c *gin.Context) {
channelTag := ChannelTag{}
err := c.ShouldBindJSON(&channelTag)
if err != nil || channelTag.Tag == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "参数错误",
})
return
}
err = model.DisableChannelByTag(channelTag.Tag)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
})
return
}
func EnableTagChannels(c *gin.Context) {
channelTag := ChannelTag{}
err := c.ShouldBindJSON(&channelTag)
if err != nil || channelTag.Tag == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "参数错误",
})
return
}
err = model.EnableChannelByTag(channelTag.Tag)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
})
return
}
func EditTagChannels(c *gin.Context) {
channelTag := ChannelTag{}
err := c.ShouldBindJSON(&channelTag)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "参数错误",
})
return
}
if channelTag.Tag == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "tag不能为空",
})
return
}
err = model.EditChannelByTag(channelTag.Tag, channelTag.NewTag, channelTag.ModelMapping, channelTag.Models, channelTag.Groups, channelTag.Priority, channelTag.Weight)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
})
return
}
type ChannelBatch struct {
Ids []int `json:"ids"`
}

View File

@@ -142,8 +142,13 @@ func GitHubOAuth(c *gin.Context) {
user.Email = githubUser.Email
user.Role = common.RoleCommonUser
user.Status = common.UserStatusEnabled
affCode := session.Get("aff")
inviterId := 0
if affCode != nil {
inviterId, _ = model.GetUserIdByAffCode(affCode.(string))
}
if err := user.Insert(0); err != nil {
if err := user.Insert(inviterId); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
@@ -227,6 +232,10 @@ func GitHubBind(c *gin.Context) {
func GenerateOAuthCode(c *gin.Context) {
session := sessions.Default(c)
state := common.GetRandomString(12)
affCode := c.Query("aff")
if affCode != "" {
session.Set("aff", affCode)
}
session.Set("oauth_state", state)
err := session.Save()
if err != nil {

View File

@@ -237,7 +237,13 @@ func LinuxdoOAuth(c *gin.Context) {
user.Role = common.RoleCommonUser
user.Status = common.UserStatusEnabled
if err := user.Insert(0); err != nil {
affCode := session.Get("aff")
inviterId := 0
if affCode != nil {
inviterId, _ = model.GetUserIdByAffCode(affCode.(string))
}
if err := user.Insert(inviterId); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),

View File

@@ -82,6 +82,15 @@ func UpdateOption(c *gin.Context) {
})
return
}
case "GroupRatio":
err = common.CheckGroupRatio(option.Value)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
}
err = model.UpdateOption(option.Key, option.Value)
if err != nil {

View File

@@ -85,14 +85,13 @@ func RequestEpay(c *gin.Context) {
c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"})
return
}
var payType epay.PurchaseType
payType := "wxpay"
if req.PaymentMethod == "zfb" {
payType = epay.Alipay
payType = "alipay"
}
if req.PaymentMethod == "wx" {
req.PaymentMethod = "wxpay"
payType = epay.WechatPay
payType = "wxpay"
}
callBackAddress := service.GetCallbackAddress()
returnUrl, _ := url.Parse(constant.ServerAddress + "/log")

View File

@@ -3,7 +3,6 @@ version: '3.4'
services:
new-api:
image: calciumion/new-api:latest
# build: .
container_name: new-api
restart: always
command: --log-dir /app/logs
@@ -13,16 +12,17 @@ services:
- ./data:/data
- ./logs:/app/logs
environment:
- SQL_DSN=root:123456@tcp(host.docker.internal:3306)/new-api # 修改此行,或注释掉以使用 SQLite 作为数据库
- SQL_DSN=root:123456@tcp(mysql:3306)/new-api # Point to the mysql service
- REDIS_CONN_STRING=redis://redis
- SESSION_SECRET=random_string # 修改为随机字符串
- TZ=Asia/Shanghai
# - NODE_TYPE=slave # 多机部署时从节点取消注释该行
# - SYNC_FREQUENCY=60 # 需要定期从数据库加载数据时取消注释该行
# - FRONTEND_BASE_URL=https://openai.justsong.cn # 多机部署时从节点取消注释该行
# - SESSION_SECRET=random_string # 多机部署时设置,必须修改这个随机字符串!!!!!!!
# - NODE_TYPE=slave # Uncomment for slave node in multi-node deployment
# - SYNC_FREQUENCY=60 # Uncomment if regular database syncing is needed
# - FRONTEND_BASE_URL=https://openai.justsong.cn # Uncomment for multi-node deployment with front-end URL
depends_on:
- redis
- mysql
healthcheck:
test: [ "CMD-SHELL", "wget -q -O - http://localhost:3000/api/status | grep -o '\"success\":\\s*true' | awk -F: '{print $2}'" ]
interval: 30s
@@ -33,3 +33,18 @@ services:
image: redis:latest
container_name: redis
restart: always
mysql:
image: mysql:8.2
container_name: mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: 123456 # Ensure this matches the password in SQL_DSN
MYSQL_DATABASE: new-api
volumes:
- mysql_data:/var/lib/mysql
# ports:
# - "3306:3306" # If you want to access MySQL from outside Docker, uncomment
volumes:
mysql_data:

44
go.mod
View File

@@ -1,34 +1,34 @@
module one-api
// +heroku goVersion go1.18
go 1.21
toolchain go1.22.4
go 1.23.4
require (
github.com/Calcium-Ion/go-epay v0.0.2
github.com/Calcium-Ion/go-epay v0.0.4
github.com/andybalholm/brotli v1.1.1
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0
github.com/aws/aws-sdk-go-v2 v1.26.1
github.com/aws/aws-sdk-go-v2/credentials v1.17.11
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.7.4
github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b
github.com/gin-contrib/cors v1.4.0
github.com/gin-contrib/cors v1.7.2
github.com/gin-contrib/gzip v0.0.6
github.com/gin-contrib/sessions v0.0.5
github.com/gin-contrib/static v0.0.1
github.com/gin-gonic/gin v1.9.1
github.com/go-playground/validator/v10 v10.19.0
github.com/go-playground/validator/v10 v10.20.0
github.com/go-redis/redis/v8 v8.11.5
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.0
github.com/jinzhu/copier v0.4.0
github.com/joho/godotenv v1.5.1
github.com/pkg/errors v0.9.1
github.com/pkoukk/tiktoken-go v0.1.7
github.com/samber/lo v1.39.0
github.com/shirou/gopsutil v3.21.11+incompatible
golang.org/x/crypto v0.26.0
golang.org/x/image v0.15.0
golang.org/x/crypto v0.27.0
golang.org/x/image v0.23.0
gorm.io/driver/mysql v1.4.3
gorm.io/driver/postgres v1.5.2
gorm.io/driver/sqlite v1.4.3
@@ -41,9 +41,11 @@ require (
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect
github.com/aws/smithy-go v1.20.2 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
@@ -58,33 +60,31 @@ require (
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.1 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.1 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.1 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/arch v0.12.0 // indirect
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

84
go.sum
View File

@@ -1,5 +1,7 @@
github.com/Calcium-Ion/go-epay v0.0.2 h1:3knFBuaBFpHzsGeGQU/QxUqZSHh5s0+jGo0P62pJzWc=
github.com/Calcium-Ion/go-epay v0.0.2/go.mod h1:cxo/ZOg8ClvE3VAnCmEzbuyAZINSq7kFEN9oHj5WQ2U=
github.com/Calcium-Ion/go-epay v0.0.4 h1:C96M7WfRLadcIVscWzwLiYs8etI1wrDmtFMuK2zP22A=
github.com/Calcium-Ion/go-epay v0.0.4/go.mod h1:cxo/ZOg8ClvE3VAnCmEzbuyAZINSq7kFEN9oHj5WQ2U=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0 h1:onfun1RA+KcxaMk1lfrRnwCd1UUuOjJM/lri5eM1qMs=
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0/go.mod h1:4yg+jNTYlDEzBjhGS96v+zjyA3lfXlFd5CiTLIkPBLI=
github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6 h1:HblK3eJHq54yET63qPCTJnks3loDse5xRmmqHgHzwoI=
@@ -20,14 +22,16 @@ github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b h1:LTGVFpNmNHhj0vhOlfgWueFJ32eK9blaIlHR2ciXOT0=
github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -40,8 +44,8 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs=
github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
@@ -69,8 +73,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
@@ -98,12 +102,12 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI=
github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@@ -117,8 +121,9 @@ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
@@ -153,8 +158,8 @@ github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg=
github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -172,6 +177,7 @@ github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMT
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -179,7 +185,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
@@ -192,26 +198,28 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8=
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -219,18 +227,17 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
@@ -262,4 +269,5 @@ gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU=
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -174,11 +174,11 @@
"\"验证码\"": "\"Verification code\"",
"全部用户": "All users",
"当前用户": "Current user",
"'全部'": "'All'",
"'充值'": "'Recharge'",
"'消费'": "'Consumption'",
"'管理'": "'Management'",
"'系统'": "'System'",
"全部'": "All'",
"充值'": "Recharge'",
"消费'": "Consumption'",
"管理'": "Management'",
"系统'": "System'",
" 充值 ": " Recharge ",
" 消费 ": " Consumption ",
" 管理 ": " Management ",
@@ -377,6 +377,7 @@
"添加新的用户": "Add New User",
"自定义": "Custom",
"等价金额": "Equivalent Amount",
"等价金额:{{quota}}": "Equivalent amount: {{quota}}",
"未登录或登录已过期,请重新登录": "Not logged in or login has expired, please log in again",
"请求次数过多,请稍后再试": "Too many requests, please try again later",
"服务器内部错误,请联系管理员": "Server internal error, please contact the administrator",
@@ -525,5 +526,13 @@
"模型版本": "Model version",
"请输入星火大模型版本注意是接口地址中的版本号例如v2.1": "Please enter the version of the Starfire model, note that it is the version number in the interface address, for example: v2.1",
"点击查看": "click to view",
"请确保已在 Azure 上创建了 gpt-35-turbo 模型,并且 apiVersion 已正确填写!": "Please make sure that the gpt-35-turbo model has been created on Azure, and the apiVersion has been filled in correctly!"
"请确保已在 Azure 上创建了 gpt-35-turbo 模型,并且 apiVersion 已正确填写!": "Please make sure that the gpt-35-turbo model has been created on Azure, and the apiVersion has been filled in correctly!",
"第 {{start}} - {{end}} 条,共 {{total}} 条": "Items {{start}} - {{end}} of {{total}}",
"模型测试": "Model test",
"请选择最长响应时间": "Please select the longest response time",
"成功时自动启用通道": "Enable channel when successful",
"分钟": "minutes",
"设置过短会影响数据库性能": "Setting too short will affect database performance",
"仅修改展示粒度,统计精确到小时": "Only modify display granularity, statistics accurate to the hour",
"当运行通道全部测试时,超过此时间将自动禁用通道": "When running all channel tests, the channel will be automatically disabled when this time is exceeded"
}

View File

@@ -33,7 +33,7 @@ var indexPage []byte
func main() {
err := godotenv.Load(".env")
if err != nil {
common.SysLog("Can't load .env file")
common.SysError("failed to load .env file: " + err.Error())
}
common.SetupLogger()

View File

@@ -212,6 +212,7 @@ func TokenAuth() func(c *gin.Context) {
}
c.Set("id", token.UserId)
c.Set("token_id", token.Id)
c.Set("token_key", token.Key)
c.Set("token_name", token.Name)
c.Set("token_unlimited_quota", token.UnlimitedQuota)
if !token.UnlimitedQuota {

View File

@@ -213,6 +213,7 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
c.Set("channel_id", channel.Id)
c.Set("channel_name", channel.Name)
c.Set("channel_type", channel.Type)
c.Set("channel_setting", channel.GetSetting())
if nil != channel.OpenAIOrganization && "" != *channel.OpenAIOrganization {
c.Set("channel_organization", *channel.OpenAIOrganization)
}

38
middleware/gzip.go Normal file
View File

@@ -0,0 +1,38 @@
package middleware
import (
"compress/gzip"
"github.com/andybalholm/brotli"
"github.com/gin-gonic/gin"
"io"
"net/http"
)
func DecompressRequestMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.Body == nil || c.Request.Method == http.MethodGet {
c.Next()
return
}
switch c.GetHeader("Content-Encoding") {
case "gzip":
gzipReader, err := gzip.NewReader(c.Request.Body)
if err != nil {
c.AbortWithStatus(http.StatusBadRequest)
return
}
defer gzipReader.Close()
// Replace the request body with the decompressed data
c.Request.Body = io.NopCloser(gzipReader)
c.Request.Header.Del("Content-Encoding")
case "br":
reader := brotli.NewReader(c.Request.Body)
c.Request.Body = io.NopCloser(reader)
c.Request.Header.Del("Content-Encoding")
}
// Continue processing the request
c.Next()
}
}

View File

@@ -10,12 +10,13 @@ import (
)
type Ability struct {
Group string `json:"group" gorm:"type:varchar(64);primaryKey;autoIncrement:false"`
Model string `json:"model" gorm:"type:varchar(64);primaryKey;autoIncrement:false"`
ChannelId int `json:"channel_id" gorm:"primaryKey;autoIncrement:false;index"`
Enabled bool `json:"enabled"`
Priority *int64 `json:"priority" gorm:"bigint;default:0;index"`
Weight uint `json:"weight" gorm:"default:0;index"`
Group string `json:"group" gorm:"type:varchar(64);primaryKey;autoIncrement:false"`
Model string `json:"model" gorm:"type:varchar(64);primaryKey;autoIncrement:false"`
ChannelId int `json:"channel_id" gorm:"primaryKey;autoIncrement:false;index"`
Enabled bool `json:"enabled"`
Priority *int64 `json:"priority" gorm:"bigint;default:0;index"`
Weight uint `json:"weight" gorm:"default:0;index"`
Tag *string `json:"tag" gorm:"index"`
}
func GetGroupModels(group string) []string {
@@ -149,6 +150,7 @@ func (channel *Channel) AddAbilities() error {
Enabled: channel.Status == common.ChannelStatusEnabled,
Priority: channel.Priority,
Weight: uint(channel.GetWeight()),
Tag: channel.Tag,
}
abilities = append(abilities, ability)
}
@@ -190,6 +192,24 @@ func UpdateAbilityStatus(channelId int, status bool) error {
return DB.Model(&Ability{}).Where("channel_id = ?", channelId).Select("enabled").Update("enabled", status).Error
}
func UpdateAbilityStatusByTag(tag string, status bool) error {
return DB.Model(&Ability{}).Where("tag = ?", tag).Select("enabled").Update("enabled", status).Error
}
func UpdateAbilityByTag(tag string, newTag *string, priority *int64, weight *uint) error {
ability := Ability{}
if newTag != nil {
ability.Tag = newTag
}
if priority != nil {
ability.Priority = priority
}
if weight != nil {
ability.Weight = *weight
}
return DB.Model(&Ability{}).Where("tag = ?", tag).Updates(ability).Error
}
func FixAbility() (int, error) {
var channelIds []int
count := 0

View File

@@ -2,9 +2,10 @@ package model
import (
"encoding/json"
"gorm.io/gorm"
"one-api/common"
"strings"
"gorm.io/gorm"
)
type Channel struct {
@@ -32,6 +33,8 @@ type Channel struct {
Priority *int64 `json:"priority" gorm:"bigint;default:0"`
AutoBan *int `json:"auto_ban" gorm:"default:1"`
OtherInfo string `json:"other_info"`
Tag *string `json:"tag" gorm:"index"`
Setting string `json:"setting" gorm:"type:text"`
}
func (channel *Channel) GetModels() []string {
@@ -61,6 +64,17 @@ func (channel *Channel) SetOtherInfo(otherInfo map[string]interface{}) {
channel.OtherInfo = string(otherInfoBytes)
}
func (channel *Channel) GetTag() string {
if channel.Tag == nil {
return ""
}
return *channel.Tag
}
func (channel *Channel) SetTag(tag string) {
channel.Tag = &tag
}
func (channel *Channel) GetAutoBan() bool {
if channel.AutoBan == nil {
return false
@@ -87,7 +101,17 @@ func GetAllChannels(startIdx int, num int, selectAll bool, idSort bool) ([]*Chan
return channels, err
}
func SearchChannels(keyword string, group string, model string) ([]*Channel, error) {
func GetChannelsByTag(tag string, idSort bool) ([]*Channel, error) {
var channels []*Channel
order := "priority desc"
if idSort {
order = "id desc"
}
err := DB.Where("tag = ?", tag).Order(order).Find(&channels).Error
return channels, err
}
func SearchChannels(keyword string, group string, model string, idSort bool) ([]*Channel, error) {
var channels []*Channel
keyCol := "`key`"
groupCol := "`group`"
@@ -100,6 +124,11 @@ func SearchChannels(keyword string, group string, model string) ([]*Channel, err
modelsCol = `"models"`
}
order := "priority desc"
if idSort {
order = "id desc"
}
// 构造基础查询
baseQuery := DB.Model(&Channel{}).Omit(keyCol)
@@ -122,7 +151,7 @@ func SearchChannels(keyword string, group string, model string) ([]*Channel, err
}
// 执行查询
err := baseQuery.Where(whereClause, args...).Order("priority desc").Find(&channels).Error
err := baseQuery.Where(whereClause, args...).Order(order).Find(&channels).Error
if err != nil {
return nil, err
}
@@ -288,6 +317,74 @@ func UpdateChannelStatusById(id int, status int, reason string) {
}
func EnableChannelByTag(tag string) error {
err := DB.Model(&Channel{}).Where("tag = ?", tag).Update("status", common.ChannelStatusEnabled).Error
if err != nil {
return err
}
err = UpdateAbilityStatusByTag(tag, true)
return err
}
func DisableChannelByTag(tag string) error {
err := DB.Model(&Channel{}).Where("tag = ?", tag).Update("status", common.ChannelStatusManuallyDisabled).Error
if err != nil {
return err
}
err = UpdateAbilityStatusByTag(tag, false)
return err
}
func EditChannelByTag(tag string, newTag *string, modelMapping *string, models *string, group *string, priority *int64, weight *uint) error {
updateData := Channel{}
shouldReCreateAbilities := false
updatedTag := tag
// 如果 newTag 不为空且不等于 tag则更新 tag
if newTag != nil && *newTag != tag {
updateData.Tag = newTag
updatedTag = *newTag
}
if modelMapping != nil && *modelMapping != "" {
updateData.ModelMapping = modelMapping
}
if models != nil && *models != "" {
shouldReCreateAbilities = true
updateData.Models = *models
}
if group != nil && *group != "" {
shouldReCreateAbilities = true
updateData.Group = *group
}
if priority != nil {
updateData.Priority = priority
}
if weight != nil {
updateData.Weight = weight
}
err := DB.Model(&Channel{}).Where("tag = ?", tag).Updates(updateData).Error
if err != nil {
return err
}
if shouldReCreateAbilities {
channels, err := GetChannelsByTag(updatedTag, false)
if err == nil {
for _, channel := range channels {
err = channel.UpdateAbilities()
if err != nil {
common.SysError("failed to update abilities: " + err.Error())
}
}
}
} else {
err := UpdateAbilityByTag(tag, newTag, priority, weight)
if err != nil {
return err
}
}
return nil
}
func UpdateChannelUsedQuota(id int, quota int) {
if common.BatchUpdateEnabled {
addNewRecord(BatchUpdateTypeChannelUsedQuota, id, quota)
@@ -312,3 +409,84 @@ func DeleteDisabledChannel() (int64, error) {
result := DB.Where("status = ? or status = ?", common.ChannelStatusAutoDisabled, common.ChannelStatusManuallyDisabled).Delete(&Channel{})
return result.RowsAffected, result.Error
}
func GetPaginatedTags(offset int, limit int) ([]*string, error) {
var tags []*string
err := DB.Model(&Channel{}).Select("DISTINCT tag").Where("tag != ''").Offset(offset).Limit(limit).Find(&tags).Error
return tags, err
}
func SearchTags(keyword string, group string, model string, idSort bool) ([]*string, error) {
var tags []*string
keyCol := "`key`"
groupCol := "`group`"
modelsCol := "`models`"
// 如果是 PostgreSQL使用双引号
if common.UsingPostgreSQL {
keyCol = `"key"`
groupCol = `"group"`
modelsCol = `"models"`
}
order := "priority desc"
if idSort {
order = "id desc"
}
// 构造基础查询
baseQuery := DB.Model(&Channel{}).Omit(keyCol)
// 构造WHERE子句
var whereClause string
var args []interface{}
if group != "" && group != "null" {
var groupCondition string
if common.UsingMySQL {
groupCondition = `CONCAT(',', ` + groupCol + `, ',') LIKE ?`
} else {
// sqlite, PostgreSQL
groupCondition = `(',' || ` + groupCol + ` || ',') LIKE ?`
}
whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ?) AND " + modelsCol + ` LIKE ? AND ` + groupCondition
args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+model+"%", "%,"+group+",%")
} else {
whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ?) AND " + modelsCol + " LIKE ?"
args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+model+"%")
}
subQuery := baseQuery.Where(whereClause, args...).
Select("tag").
Where("tag != ''").
Order(order)
err := DB.Table("(?) as sub", subQuery).
Select("DISTINCT tag").
Find(&tags).Error
if err != nil {
return nil, err
}
return tags, nil
}
func (channel *Channel) GetSetting() map[string]interface{} {
setting := make(map[string]interface{})
if channel.Setting != "" {
err := json.Unmarshal([]byte(channel.Setting), &setting)
if err != nil {
common.SysError("failed to unmarshal setting: " + err.Error())
}
}
return setting
}
func (channel *Channel) SetSetting(setting map[string]interface{}) {
settingBytes, err := json.Marshal(setting)
if err != nil {
common.SysError("failed to marshal setting: " + err.Error())
return
}
channel.Setting = string(settingBytes)
}

View File

@@ -85,7 +85,7 @@ func SaveQuotaDataCache() {
//quotaDataDB.Count += quotaData.Count
//quotaDataDB.Quota += quotaData.Quota
//DB.Table("quota_data").Save(quotaDataDB)
increaseQuotaData(quotaData.UserID, quotaData.Username, quotaData.ModelName, quotaData.Count, quotaData.Quota, quotaData.CreatedAt)
increaseQuotaData(quotaData.UserID, quotaData.Username, quotaData.ModelName, quotaData.Count, quotaData.Quota, quotaData.CreatedAt, quotaData.TokenUsed)
} else {
DB.Table("quota_data").Create(quotaData)
}
@@ -94,11 +94,12 @@ func SaveQuotaDataCache() {
common.SysLog(fmt.Sprintf("保存数据看板数据成功,共保存%d条数据", size))
}
func increaseQuotaData(userId int, username string, modelName string, count int, quota int, createdAt int64) {
func increaseQuotaData(userId int, username string, modelName string, count int, quota int, createdAt int64, tokenUsed int) {
err := DB.Table("quota_data").Where("user_id = ? and username = ? and model_name = ? and created_at = ?",
userId, username, modelName, createdAt).Updates(map[string]interface{}{
"count": gorm.Expr("count + ?", count),
"quota": gorm.Expr("quota + ?", quota),
"count": gorm.Expr("count + ?", count),
"quota": gorm.Expr("quota + ?", quota),
"token_used": gorm.Expr("token_used + ?", tokenUsed),
}).Error
if err != nil {
common.SysLog(fmt.Sprintf("increaseQuotaData error: %s", err))
@@ -127,6 +128,6 @@ func GetAllQuotaDates(startTime int64, endTime int64, username string) (quotaDat
// 从quota_data表中查询数据
// only select model_name, sum(count) as count, sum(quota) as quota, model_name, created_at from quota_data group by model_name, created_at;
//err = DB.Table("quota_data").Where("created_at >= ? and created_at <= ?", startTime, endTime).Find(&quotaDatas).Error
err = DB.Table("quota_data").Select("model_name, sum(count) as count, sum(quota) as quota, created_at").Where("created_at >= ? and created_at <= ?", startTime, endTime).Group("model_name, created_at").Find(&quotaDatas).Error
err = DB.Table("quota_data").Select("model_name, sum(count) as count, sum(quota) as quota, sum(token_used) as token_used, created_at").Where("created_at >= ? and created_at <= ?", startTime, endTime).Group("model_name, created_at").Find(&quotaDatas).Error
return quotaDatas, err
}

View File

@@ -4,6 +4,7 @@ var ModelList = []string{
"360gpt-turbo",
"360gpt-turbo-responsibility-8k",
"360gpt-pro",
"360gpt2-pro",
"360GPT_S2_V9",
"embedding-bert-512-v1",
"embedding_s1_v1",

View File

@@ -9,6 +9,7 @@ var awsModelIDMap = map[string]string{
"claude-3-haiku-20240307": "anthropic.claude-3-haiku-20240307-v1:0",
"claude-3-5-sonnet-20240620": "anthropic.claude-3-5-sonnet-20240620-v1:0",
"claude-3-5-sonnet-20241022": "anthropic.claude-3-5-sonnet-20241022-v2:0",
"claude-3-5-haiku-20241022": "anthropic.claude-3-5-haiku-20241022-v1:0",
}
var ChannelName = "aws"

View File

@@ -7,9 +7,9 @@ import (
type AwsClaudeRequest struct {
// AnthropicVersion should be "bedrock-2023-05-31"
AnthropicVersion string `json:"anthropic_version"`
System string `json:"system"`
System string `json:"system,omitempty"`
Messages []claude.ClaudeMessage `json:"messages"`
MaxTokens int `json:"max_tokens,omitempty"`
MaxTokens uint `json:"max_tokens,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
TopP float64 `json:"top_p,omitempty"`
TopK int `json:"top_k,omitempty"`
@@ -17,3 +17,18 @@ type AwsClaudeRequest struct {
Tools []claude.Tool `json:"tools,omitempty"`
ToolChoice any `json:"tool_choice,omitempty"`
}
func copyRequest(req *claude.ClaudeRequest) *AwsClaudeRequest {
return &AwsClaudeRequest{
AnthropicVersion: "bedrock-2023-05-31",
System: req.System,
Messages: req.Messages,
MaxTokens: req.MaxTokens,
Temperature: req.Temperature,
TopP: req.TopP,
TopK: req.TopK,
StopSequences: req.StopSequences,
Tools: req.Tools,
ToolChoice: req.ToolChoice,
}
}

View File

@@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
"io"
"net/http"
@@ -78,13 +77,7 @@ func awsHandler(c *gin.Context, info *relaycommon.RelayInfo, requestMode int) (*
return wrapErr(errors.New("request not found")), nil
}
claudeReq := claudeReq_.(*claude.ClaudeRequest)
awsClaudeReq := &AwsClaudeRequest{
AnthropicVersion: "bedrock-2023-05-31",
}
if err = copier.Copy(awsClaudeReq, claudeReq); err != nil {
return wrapErr(errors.Wrap(err, "copy request")), nil
}
awsClaudeReq := copyRequest(claudeReq)
awsReq.Body, err = json.Marshal(awsClaudeReq)
if err != nil {
return wrapErr(errors.Wrap(err, "marshal request")), nil
@@ -136,12 +129,7 @@ func awsStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
}
claudeReq := claudeReq_.(*claude.ClaudeRequest)
awsClaudeReq := &AwsClaudeRequest{
AnthropicVersion: "bedrock-2023-05-31",
}
if err = copier.Copy(awsClaudeReq, claudeReq); err != nil {
return wrapErr(errors.Wrap(err, "copy request")), nil
}
awsClaudeReq := copyRequest(claudeReq)
awsReq.Body, err = json.Marshal(awsClaudeReq)
if err != nil {
return wrapErr(errors.Wrap(err, "marshal request")), nil

View File

@@ -9,8 +9,8 @@ import (
"io"
"net/http"
"one-api/common"
"one-api/constant"
"one-api/dto"
relaycommon "one-api/relay/common"
"one-api/service"
"strings"
"sync"
@@ -75,7 +75,7 @@ func streamResponseBaidu2OpenAI(baiduResponse *BaiduChatStreamResponse) *dto.Cha
var choice dto.ChatCompletionsStreamResponseChoice
choice.Delta.SetContentString(baiduResponse.Result)
if baiduResponse.IsEnd {
choice.FinishReason = &relaycommon.StopFinishReason
choice.FinishReason = &constant.FinishReasonStop
}
response := dto.ChatCompletionsStreamResponse{
Id: baiduResponse.Id,

View File

@@ -0,0 +1,71 @@
package deepseek
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"io"
"net/http"
"one-api/dto"
"one-api/relay/channel"
"one-api/relay/channel/openai"
relaycommon "one-api/relay/common"
)
type Adaptor struct {
}
func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
}
func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
}
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
return fmt.Sprintf("%s/chat/completions", info.BaseUrl), nil
}
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
channel.SetupApiRequestHeader(info, c, req)
req.Set("Authorization", "Bearer "+info.ApiKey)
return nil
}
func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) {
if request == nil {
return nil, errors.New("request is nil")
}
return request, nil
}
func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) {
return nil, nil
}
func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {
return channel.DoApiRequest(a, c, info, requestBody)
}
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
if info.IsStream {
err, usage = openai.OaiStreamHandler(c, resp, info)
} else {
err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName)
}
return
}
func (a *Adaptor) GetModelList() []string {
return ModelList
}
func (a *Adaptor) GetChannelName() string {
return ChannelName
}

View File

@@ -0,0 +1,7 @@
package deepseek
var ModelList = []string{
"deepseek-chat", "deepseek-coder",
}
var ChannelName = "deepseek"

View File

@@ -30,13 +30,13 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
}
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
// 从映射中获取模型名称对应的版本,如果找不到就使用 info.ApiVersion 或默认的版本 "v1"
// 从映射中获取模型名称对应的版本,如果找不到就使用 info.ApiVersion 或默认的版本 "v1beta"
version, beta := constant.GeminiModelMap[info.UpstreamModelName]
if !beta {
if info.ApiVersion != "" {
version = info.ApiVersion
} else {
version = "v1"
version = "v1beta"
}
}

View File

@@ -5,8 +5,10 @@ const (
)
var ModelList = []string{
"gemini-1.0-pro-latest", "gemini-1.0-pro-001", "gemini-1.5-pro-latest", "gemini-1.5-flash-latest", "gemini-ultra",
"gemini-1.0-pro-vision-latest", "gemini-1.0-pro-vision-001", "gemini-1.5-pro-exp-0827", "gemini-1.5-flash-exp-0827",
"gemini-1.5-pro-latest", "gemini-1.5-flash-latest", "gemini-ultra",
"gemini-1.5-pro-exp-0827", "gemini-1.5-flash-exp-0827",
"gemini-exp-1114", "gemini-exp-1206",
"gemini-2.0-flash-exp",
}
var ChannelName = "google gemini"

View File

@@ -34,6 +34,7 @@ type GeminiChatSafetySettings struct {
}
type GeminiChatTools struct {
GoogleSearch any `json:"googleSearch,omitempty"`
FunctionDeclarations any `json:"functionDeclarations,omitempty"`
}

View File

@@ -8,6 +8,7 @@ import (
"io"
"net/http"
"one-api/common"
"one-api/constant"
"one-api/dto"
relaycommon "one-api/relay/common"
"one-api/service"
@@ -44,13 +45,25 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) *GeminiChatReques
}
if textRequest.Tools != nil {
functions := make([]dto.FunctionCall, 0, len(textRequest.Tools))
googleSearch := false
for _, tool := range textRequest.Tools {
if tool.Function.Name == "googleSearch" {
googleSearch = true
continue
}
functions = append(functions, tool.Function)
}
geminiRequest.Tools = []GeminiChatTools{
{
FunctionDeclarations: functions,
},
if len(functions) > 0 {
geminiRequest.Tools = []GeminiChatTools{
{
FunctionDeclarations: functions,
},
}
}
if googleSearch {
geminiRequest.Tools = append(geminiRequest.Tools, GeminiChatTools{
GoogleSearch: make(map[string]string),
})
}
} else if textRequest.Functions != nil {
geminiRequest.Tools = []GeminiChatTools{
@@ -133,7 +146,6 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) *GeminiChatReques
shouldAddDummyModelMessage = false
}
}
return &geminiRequest
}
@@ -186,10 +198,11 @@ func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResp
Role: "assistant",
Content: content,
},
FinishReason: relaycommon.StopFinishReason,
FinishReason: constant.FinishReasonStop,
}
if len(candidate.Content.Parts) > 0 {
if candidate.Content.Parts[0].FunctionCall != nil {
choice.FinishReason = constant.FinishReasonToolCalls
choice.Message.ToolCalls = getToolCalls(&candidate)
} else {
choice.Message.SetStringContent(candidate.Content.Parts[0].Text)
@@ -262,7 +275,7 @@ func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycom
}
}
response := service.GenerateStopResponse(id, createAt, info.UpstreamModelName, relaycommon.StopFinishReason)
response := service.GenerateStopResponse(id, createAt, info.UpstreamModelName, constant.FinishReasonStop)
service.ObjectData(c, response)
usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens

View File

@@ -32,11 +32,15 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
if info.RelayMode == constant.RelayModeRealtime {
// trim https
baseUrl := strings.TrimPrefix(info.BaseUrl, "https://")
baseUrl = strings.TrimPrefix(baseUrl, "http://")
baseUrl = "wss://" + baseUrl
info.BaseUrl = baseUrl
if strings.HasPrefix(info.BaseUrl, "https://") {
baseUrl := strings.TrimPrefix(info.BaseUrl, "https://")
baseUrl = "wss://" + baseUrl
info.BaseUrl = baseUrl
} else if strings.HasPrefix(info.BaseUrl, "http://") {
baseUrl := strings.TrimPrefix(info.BaseUrl, "http://")
baseUrl = "ws://" + baseUrl
info.BaseUrl = baseUrl
}
}
switch info.ChannelType {
case common.ChannelTypeAzure:
@@ -132,6 +136,19 @@ func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInf
writer.WriteField("model", request.Model)
// 获取所有表单字段
formData := c.Request.PostForm
// 遍历表单字段并打印输出
for key, values := range formData {
if key == "model" {
continue
}
for _, value := range values {
writer.WriteField(key, value)
}
}
// 添加文件字段
file, header, err := c.Request.FormFile("file")
if err != nil {

View File

@@ -9,7 +9,7 @@ var ModelList = []string{
"gpt-4-turbo-preview", "gpt-4-turbo", "gpt-4-turbo-2024-04-09",
"gpt-4-vision-preview",
"chatgpt-4o-latest",
"gpt-4o", "gpt-4o-2024-05-13", "gpt-4o-2024-08-06",
"gpt-4o", "gpt-4o-2024-05-13", "gpt-4o-2024-08-06", "gpt-4o-2024-11-20",
"gpt-4o-mini", "gpt-4o-mini-2024-07-18",
"o1-preview", "o1-preview-2024-09-12",
"o1-mini", "o1-mini-2024-09-12",

View File

@@ -5,9 +5,6 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/bytedance/gopkg/util/gopool"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"io"
"net/http"
"one-api/common"
@@ -19,9 +16,33 @@ import (
"strings"
"sync"
"time"
"github.com/bytedance/gopkg/util/gopool"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
func sendStreamData(c *gin.Context, data string, forceFormat bool) error {
if data == "" {
return nil
}
if forceFormat {
var lastStreamResponse dto.ChatCompletionsStreamResponse
if err := json.Unmarshal(common.StringToByteSlice(data), &lastStreamResponse); err != nil {
return err
}
return service.ObjectData(c, lastStreamResponse)
}
return service.StringData(c, data)
}
func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
if resp == nil || resp.Body == nil {
common.LogError(c, "invalid response or response body")
return service.OpenAIErrorWrapper(fmt.Errorf("invalid response"), "invalid_response", http.StatusInternalServerError), nil
}
containStreamUsage := false
var responseId string
var createAt int64 = 0
@@ -31,6 +52,13 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
var responseTextBuilder strings.Builder
var usage = &dto.Usage{}
var streamItems []string // store stream items
var forceFormat bool
if info.ChannelType == common.ChannelTypeCustom {
if forceFmt, ok := info.ChannelSetting["force_format"].(bool); ok {
forceFormat = forceFmt
}
}
toolCount := 0
scanner := bufio.NewScanner(resp.Body)
@@ -62,7 +90,7 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
data = data[6:]
if !strings.HasPrefix(data, "[DONE]") {
if lastStreamData != "" {
err := service.StringData(c, lastStreamData)
err := sendStreamData(c, lastStreamData, forceFormat)
if err != nil {
common.LogError(c, "streaming error: "+err.Error())
}
@@ -98,9 +126,14 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
shouldSendLastResp = false
}
}
for _, choice := range lastStreamResponse.Choices {
if choice.FinishReason != nil {
shouldSendLastResp = true
}
}
}
if shouldSendLastResp {
service.StringData(c, lastStreamData)
sendStreamData(c, lastStreamData, forceFormat)
}
// 计算token
@@ -279,7 +312,6 @@ func OpenaiTTSHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
}
func OpenaiSTTHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, responseFormat string) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
var audioResp dto.AudioResponse
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
@@ -288,11 +320,6 @@ func OpenaiSTTHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
if err != nil {
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
}
err = json.Unmarshal(responseBody, &audioResp)
if err != nil {
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
}
// Reset response body
resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
// We shouldn't set the header before we parse the response body, because the parse part may fail.
@@ -376,6 +403,10 @@ func getTextFromJSON(body []byte) (string, error) {
}
func OpenaiRealtimeHandler(c *gin.Context, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.RealtimeUsage) {
if info == nil || info.ClientWs == nil || info.TargetWs == nil {
return service.OpenAIErrorWrapper(fmt.Errorf("invalid websocket connection"), "invalid_connection", http.StatusBadRequest), nil
}
info.IsStream = true
clientConn := info.ClientWs
targetConn := info.TargetWs
@@ -391,6 +422,11 @@ func OpenaiRealtimeHandler(c *gin.Context, info *relaycommon.RelayInfo) (*dto.Op
sumUsage := &dto.RealtimeUsage{}
gopool.Go(func() {
defer func() {
if r := recover(); r != nil {
errChan <- fmt.Errorf("panic in client reader: %v", r)
}
}()
for {
select {
case <-c.Done():
@@ -446,6 +482,11 @@ func OpenaiRealtimeHandler(c *gin.Context, info *relaycommon.RelayInfo) (*dto.Op
})
gopool.Go(func() {
defer func() {
if r := recover(); r != nil {
errChan <- fmt.Errorf("panic in target reader: %v", r)
}
}()
for {
select {
case <-c.Done():
@@ -569,6 +610,10 @@ func OpenaiRealtimeHandler(c *gin.Context, info *relaycommon.RelayInfo) (*dto.Op
}
func preConsumeUsage(ctx *gin.Context, info *relaycommon.RelayInfo, usage *dto.RealtimeUsage, totalUsage *dto.RealtimeUsage) error {
if usage == nil || totalUsage == nil {
return fmt.Errorf("invalid usage pointer")
}
totalUsage.TotalTokens += usage.TotalTokens
totalUsage.InputTokens += usage.InputTokens
totalUsage.OutputTokens += usage.OutputTokens

View File

@@ -7,8 +7,8 @@ import (
"io"
"net/http"
"one-api/common"
"one-api/constant"
"one-api/dto"
relaycommon "one-api/relay/common"
"one-api/service"
)
@@ -63,7 +63,7 @@ func streamResponsePaLM2OpenAI(palmResponse *PaLMChatResponse) *dto.ChatCompleti
if len(palmResponse.Candidates) > 0 {
choice.Delta.SetContentString(palmResponse.Candidates[0].Content)
}
choice.FinishReason = &relaycommon.StopFinishReason
choice.FinishReason = &constant.FinishReasonStop
var response dto.ChatCompletionsStreamResponse
response.Object = "chat.completion.chunk"
response.Model = "palm2"

View File

@@ -12,8 +12,8 @@ import (
"io"
"net/http"
"one-api/common"
"one-api/constant"
"one-api/dto"
relaycommon "one-api/relay/common"
"one-api/service"
"strconv"
"strings"
@@ -81,7 +81,7 @@ func streamResponseTencent2OpenAI(TencentResponse *TencentChatResponse) *dto.Cha
var choice dto.ChatCompletionsStreamResponseChoice
choice.Delta.SetContentString(TencentResponse.Choices[0].Delta.Content)
if TencentResponse.Choices[0].FinishReason == "stop" {
choice.FinishReason = &relaycommon.StopFinishReason
choice.FinishReason = &constant.FinishReasonStop
}
response.Choices = append(response.Choices, choice)
}

View File

@@ -176,7 +176,20 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
}
func (a *Adaptor) GetModelList() []string {
return ModelList
var modelList []string
for i, s := range ModelList {
modelList = append(modelList, s)
ModelList[i] = s
}
for i, s := range claude.ModelList {
modelList = append(modelList, s)
claude.ModelList[i] = s
}
for i, s := range gemini.ModelList {
modelList = append(modelList, s)
gemini.ModelList[i] = s
}
return modelList
}
func (a *Adaptor) GetChannelName() string {

View File

@@ -1,13 +1,13 @@
package vertex
var ModelList = []string{
"claude-3-sonnet-20240229",
"claude-3-opus-20240229",
"claude-3-haiku-20240307",
"claude-3-5-sonnet-20240620",
//"claude-3-sonnet-20240229",
//"claude-3-opus-20240229",
//"claude-3-haiku-20240307",
//"claude-3-5-sonnet-20240620",
//"gemini-1.5-pro-latest", "gemini-1.5-flash-latest",
"gemini-1.5-pro-001", "gemini-1.5-flash-001", "gemini-pro", "gemini-pro-vision",
//"gemini-1.5-pro-001", "gemini-1.5-flash-001", "gemini-pro", "gemini-pro-vision",
"meta/llama3-405b-instruct-maas",
}

View File

@@ -12,8 +12,8 @@ import (
"net/http"
"net/url"
"one-api/common"
"one-api/constant"
"one-api/dto"
relaycommon "one-api/relay/common"
"one-api/service"
"strings"
"time"
@@ -67,7 +67,7 @@ func responseXunfei2OpenAI(response *XunfeiChatResponse) *dto.OpenAITextResponse
Role: "assistant",
Content: content,
},
FinishReason: relaycommon.StopFinishReason,
FinishReason: constant.FinishReasonStop,
}
fullTextResponse := dto.OpenAITextResponse{
Object: "chat.completion",
@@ -89,7 +89,7 @@ func streamResponseXunfei2OpenAI(xunfeiResponse *XunfeiChatResponse) *dto.ChatCo
var choice dto.ChatCompletionsStreamResponseChoice
choice.Delta.SetContentString(xunfeiResponse.Payload.Choices.Text[0].Content)
if xunfeiResponse.Payload.Choices.Status == 2 {
choice.FinishReason = &relaycommon.StopFinishReason
choice.FinishReason = &constant.FinishReasonStop
}
response := dto.ChatCompletionsStreamResponse{
Object: "chat.completion.chunk",
@@ -245,7 +245,7 @@ func xunfeiMakeRequest(textRequest dto.GeneralOpenAIRequest, domain, authUrl, ap
func apiVersion2domain(apiVersion string) string {
switch apiVersion {
case "v1.1":
return "general"
return "lite"
case "v2.1":
return "generalv2"
case "v3.1":

View File

@@ -8,8 +8,8 @@ import (
"io"
"net/http"
"one-api/common"
"one-api/constant"
"one-api/dto"
relaycommon "one-api/relay/common"
"one-api/service"
"strings"
"sync"
@@ -139,7 +139,7 @@ func streamResponseZhipu2OpenAI(zhipuResponse string) *dto.ChatCompletionsStream
func streamMetaResponseZhipu2OpenAI(zhipuResponse *ZhipuStreamMetaResponse) (*dto.ChatCompletionsStreamResponse, *dto.Usage) {
var choice dto.ChatCompletionsStreamResponseChoice
choice.Delta.SetContentString("")
choice.FinishReason = &relaycommon.StopFinishReason
choice.FinishReason = &constant.FinishReasonStop
response := dto.ChatCompletionsStreamResponse{
Id: zhipuResponse.RequestId,
Object: "chat.completion.chunk",

View File

@@ -1,19 +1,21 @@
package common
import (
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"one-api/common"
"one-api/dto"
"one-api/relay/constant"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
type RelayInfo struct {
ChannelType int
ChannelId int
TokenId int
TokenKey string
UserId int
Group string
TokenUnlimited bool
@@ -42,6 +44,7 @@ type RelayInfo struct {
RealtimeTools []dto.RealTimeTool
IsFirstRequest bool
AudioUsage bool
ChannelSetting map[string]interface{}
}
func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo {
@@ -56,8 +59,10 @@ func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo {
func GenRelayInfo(c *gin.Context) *RelayInfo {
channelType := c.GetInt("channel_type")
channelId := c.GetInt("channel_id")
channelSetting := c.GetStringMap("channel_setting")
tokenId := c.GetInt("token_id")
tokenKey := c.GetString("token_key")
userId := c.GetInt("id")
group := c.GetString("group")
tokenUnlimited := c.GetBool("token_unlimited_quota")
@@ -73,6 +78,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
ChannelType: channelType,
ChannelId: channelId,
TokenId: tokenId,
TokenKey: tokenKey,
UserId: userId,
Group: group,
TokenUnlimited: tokenUnlimited,
@@ -84,6 +90,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
ApiVersion: c.GetString("api_version"),
ApiKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "),
Organization: c.GetString("channel_organization"),
ChannelSetting: channelSetting,
}
if strings.HasPrefix(c.Request.URL.Path, "/pg") {
info.IsPlayground = true

View File

@@ -10,8 +10,6 @@ import (
"strings"
)
var StopFinishReason = "stop"
func GetFullRequestURL(baseURL string, requestURL string, channelType int) string {
fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)

View File

@@ -26,6 +26,7 @@ const (
APITypeSiliconFlow
APITypeVertexAi
APITypeMistral
APITypeDeepSeek
APITypeDummy // this one is only for count, do not add any channel after this
)
@@ -75,6 +76,8 @@ func ChannelType2APIType(channelType int) (int, bool) {
apiType = APITypeVertexAi
case common.ChannelTypeMistral:
apiType = APITypeMistral
case common.ChannelTypeDeepSeek:
apiType = APITypeDeepSeek
}
if apiType == -1 {
return APITypeOpenAI, false

View File

@@ -33,12 +33,19 @@ func getAndValidAudioRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.
}
}
default:
if audioRequest.Model == "" {
audioRequest.Model = c.PostForm("model")
err = c.Request.ParseForm()
if err != nil {
return nil, err
}
formData := c.Request.PostForm
if audioRequest.Model == "" {
audioRequest.Model = formData.Get("model")
}
if audioRequest.Model == "" {
return nil, errors.New("model is required")
}
audioRequest.ResponseFormat = formData.Get("response_format")
if audioRequest.ResponseFormat == "" {
audioRequest.ResponseFormat = "json"
}

View File

@@ -2,7 +2,6 @@ package relay
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
@@ -35,7 +34,7 @@ func getAndValidateTextRequest(c *gin.Context, relayInfo *relaycommon.RelayInfo)
textRequest.Model = c.Param("model")
}
if textRequest.MaxTokens < 0 || textRequest.MaxTokens > math.MaxInt32/2 {
if textRequest.MaxTokens > math.MaxInt32/2 {
return nil, errors.New("max_tokens is invalid")
}
if textRequest.Model == "" {
@@ -47,12 +46,12 @@ func getAndValidateTextRequest(c *gin.Context, relayInfo *relaycommon.RelayInfo)
return nil, errors.New("field prompt is required")
}
case relayconstant.RelayModeChatCompletions:
if textRequest.Messages == nil || len(textRequest.Messages) == 0 {
if len(textRequest.Messages) == 0 {
return nil, errors.New("field messages is required")
}
case relayconstant.RelayModeEmbeddings:
case relayconstant.RelayModeModerations:
if textRequest.Input == "" || textRequest.Input == nil {
if textRequest.Input == nil || textRequest.Input == "" {
return nil, errors.New("field input is required")
}
case relayconstant.RelayModeEdits:
@@ -76,7 +75,7 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
}
// map model name
isModelMapped := false
//isModelMapped := false
modelMapping := c.GetString("model_mapping")
//isModelMapped := false
if modelMapping != "" && modelMapping != "{}" {
@@ -86,7 +85,7 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
return service.OpenAIErrorWrapperLocal(err, "unmarshal_model_mapping_failed", http.StatusInternalServerError)
}
if modelMap[textRequest.Model] != "" {
isModelMapped = true
//isModelMapped = true
textRequest.Model = modelMap[textRequest.Model]
// set upstream model name
//isModelMapped = true
@@ -165,23 +164,25 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
adaptor.Init(relayInfo)
var requestBody io.Reader
if relayInfo.ChannelType == common.ChannelTypeOpenAI && !isModelMapped {
body, err := common.GetRequestBody(c)
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "get_request_body_failed", http.StatusInternalServerError)
}
requestBody = bytes.NewBuffer(body)
} else {
convertedRequest, err := adaptor.ConvertRequest(c, relayInfo, textRequest)
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "convert_request_failed", http.StatusInternalServerError)
}
jsonData, err := json.Marshal(convertedRequest)
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError)
}
requestBody = bytes.NewBuffer(jsonData)
//if relayInfo.ChannelType == common.ChannelTypeOpenAI && !isModelMapped {
// body, err := common.GetRequestBody(c)
// if err != nil {
// return service.OpenAIErrorWrapperLocal(err, "get_request_body_failed", http.StatusInternalServerError)
// }
// requestBody = bytes.NewBuffer(body)
//} else {
//
//}
convertedRequest, err := adaptor.ConvertRequest(c, relayInfo, textRequest)
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "convert_request_failed", http.StatusInternalServerError)
}
jsonData, err := json.Marshal(convertedRequest)
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError)
}
requestBody = bytes.NewBuffer(jsonData)
statusCodeMappingStr := c.GetString("status_code_mapping")
var httpResp *http.Response
@@ -261,7 +262,7 @@ func preConsumeQuota(c *gin.Context, preConsumedQuota int, relayInfo *relaycommo
return 0, 0, service.OpenAIErrorWrapperLocal(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden)
}
if userQuota-preConsumedQuota < 0 {
return 0, 0, service.OpenAIErrorWrapperLocal(errors.New(fmt.Sprintf("chat pre-consumed quota failed, user quota: %d, need quota: %d", userQuota, preConsumedQuota)), "insufficient_user_quota", http.StatusBadRequest)
return 0, 0, service.OpenAIErrorWrapperLocal(fmt.Errorf("chat pre-consumed quota failed, user quota: %d, need quota: %d", userQuota, preConsumedQuota), "insufficient_user_quota", http.StatusBadRequest)
}
err = model.CacheDecreaseUserQuota(relayInfo.UserId, preConsumedQuota)
if err != nil {
@@ -295,13 +296,14 @@ func preConsumeQuota(c *gin.Context, preConsumedQuota int, relayInfo *relaycommo
func returnPreConsumedQuota(c *gin.Context, relayInfo *relaycommon.RelayInfo, userQuota int, preConsumedQuota int) {
if preConsumedQuota != 0 {
go func(ctx context.Context) {
// return pre-consumed quota
err := model.PostConsumeTokenQuota(relayInfo, userQuota, -preConsumedQuota, 0, false)
go func() {
relayInfoCopy := *relayInfo
err := model.PostConsumeTokenQuota(&relayInfoCopy, userQuota, -preConsumedQuota, 0, false)
if err != nil {
common.SysError("error return pre-consumed quota: " + err.Error())
}
}(c)
}()
}
}

View File

@@ -9,6 +9,7 @@ import (
"one-api/relay/channel/claude"
"one-api/relay/channel/cloudflare"
"one-api/relay/channel/cohere"
"one-api/relay/channel/deepseek"
"one-api/relay/channel/dify"
"one-api/relay/channel/gemini"
"one-api/relay/channel/jina"
@@ -71,6 +72,8 @@ func GetAdaptor(apiType int) channel.Adaptor {
return &vertex.Adaptor{}
case constant.APITypeMistral:
return &mistral.Adaptor{}
case constant.APITypeDeepSeek:
return &deepseek.Adaptor{}
}
return nil
}

View File

@@ -91,6 +91,9 @@ func SetApiRouter(router *gin.Engine) {
channelRoute.POST("/", controller.AddChannel)
channelRoute.PUT("/", controller.UpdateChannel)
channelRoute.DELETE("/disabled", controller.DeleteDisabledChannel)
channelRoute.POST("/tag/disabled", controller.DisableTagChannels)
channelRoute.POST("/tag/enabled", controller.EnableTagChannels)
channelRoute.PUT("/tag", controller.EditTagChannels)
channelRoute.DELETE("/:id", controller.DeleteChannel)
channelRoute.POST("/batch", controller.DeleteChannelBatch)
channelRoute.POST("/fix", controller.FixChannelsAbilities)

View File

@@ -9,6 +9,7 @@ import (
func SetRelayRouter(router *gin.Engine) {
router.Use(middleware.CORS())
router.Use(middleware.DecompressRequestMiddleware())
// https://platform.openai.com/docs/api-reference/introduction
modelsRouter := router.Group("/v1/models")
modelsRouter.Use(middleware.TokenAuth())

View File

@@ -22,7 +22,7 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag
return err
}
token, err := model.CacheGetTokenByKey(strings.TrimLeft(relayInfo.ApiKey, "sk-"))
token, err := model.CacheGetTokenByKey(strings.TrimLeft(relayInfo.TokenKey, "sk-"))
if err != nil {
return err
}
@@ -53,7 +53,7 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag
return errors.New(fmt.Sprintf("用户额度不足,剩余额度为 %d", userQuota))
}
if token.RemainQuota < quota {
if !token.UnlimitedQuota && token.RemainQuota < quota {
return errors.New(fmt.Sprintf("令牌额度不足,剩余额度为 %d", token.RemainQuota))
}

View File

@@ -5,7 +5,7 @@
"type": "module",
"dependencies": {
"@douyinfe/semi-icons": "^2.63.1",
"@douyinfe/semi-ui": "^2.63.1",
"@douyinfe/semi-ui": "^2.69.1",
"@visactor/react-vchart": "~1.8.8",
"@visactor/vchart": "~1.8.8",
"@visactor/vchart-semi-theme": "~1.8.8",
@@ -23,7 +23,10 @@
"react-turnstile": "^1.0.5",
"semantic-ui-offline": "^2.5.0",
"semantic-ui-react": "^2.1.3",
"sse": "github:mpetazzoni/sse.js"
"sse": "github:mpetazzoni/sse.js",
"i18next": "^23.16.8",
"react-i18next": "^13.0.0",
"i18next-browser-languagedetector": "^7.2.0"
},
"scripts": {
"dev": "vite",

5224
web/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,40 +24,18 @@ import { Layout } from '@douyinfe/semi-ui';
import Midjourney from './pages/Midjourney';
import Pricing from './pages/Pricing/index.js';
import Task from "./pages/Task/index.js";
import Playground from './components/Playground.js';
import Playground from './pages/Playground/Playground.js';
import OAuth2Callback from "./components/OAuth2Callback.js";
import { useTranslation } from 'react-i18next';
import { StatusContext } from './context/Status';
import { setStatusData } from './helpers/data.js';
import { API, showError } from './helpers';
const Home = lazy(() => import('./pages/Home'));
const Detail = lazy(() => import('./pages/Detail'));
const About = lazy(() => import('./pages/About'));
function App() {
const [userState, userDispatch] = useContext(UserContext);
// const [statusState, statusDispatch] = useContext(StatusContext);
const loadUser = () => {
let user = localStorage.getItem('user');
if (user) {
let data = JSON.parse(user);
userDispatch({ type: 'login', payload: data });
}
};
useEffect(() => {
loadUser();
let systemName = getSystemName();
if (systemName) {
document.title = systemName;
}
let logo = getLogo();
if (logo) {
let linkElement = document.querySelector("link[rel~='icon']");
if (linkElement) {
linkElement.href = logo;
}
}
}, []);
return (
<>
<Routes>

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,10 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { getFooterHTML, getSystemName } from '../helpers';
import { Layout, Tooltip } from '@douyinfe/semi-ui';
const FooterBar = () => {
const { t } = useTranslation();
const systemName = getSystemName();
const [footer, setFooter] = useState(getFooterHTML());
let remainCheckTimes = 5;
@@ -24,7 +25,7 @@ const FooterBar = () => {
>
New API {import.meta.env.VITE_REACT_APP_VERSION}{' '}
</a>
{' '}
{t('由')}{' '}
<a
href='https://github.com/Calcium-Ion'
target='_blank'
@@ -32,7 +33,7 @@ const FooterBar = () => {
>
Calcium-Ion
</a>{' '}
开发基于{' '}
{t('开发,基于')}{' '}
<a
href='https://github.com/songquanpeng/one-api'
target='_blank'

View File

@@ -2,6 +2,7 @@ import React, { useContext, useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { UserContext } from '../context/User';
import { useSetTheme, useTheme } from '../context/Theme';
import { useTranslation } from 'react-i18next';
import { API, getLogo, getSystemName, isMobile, showSuccess } from '../helpers';
import '../index.css';
@@ -9,70 +10,61 @@ import '../index.css';
import fireworks from 'react-fireworks';
import {
IconClose,
IconHelpCircle,
IconHome,
IconHomeStroked,
IconKey,
IconHomeStroked, IconIndentLeft,
IconKey, IconMenu,
IconNoteMoneyStroked,
IconPriceTag,
IconUser
IconUser,
IconLanguage
} from '@douyinfe/semi-icons';
import { Avatar, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
import { Avatar, Button, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
import { stringToColor } from '../helpers/render';
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
// HeaderBar Buttons
let headerButtons = [
{
text: '关于',
itemKey: 'about',
to: '/about',
icon: <IconHelpCircle />,
},
];
let buttons = [
{
text: '首页',
itemKey: 'home',
to: '/',
// icon: <IconHomeStroked />,
},
// {
// text: 'Playground',
// itemKey: 'playground',
// to: '/playground',
// // icon: <IconNoteMoneyStroked />,
// },
];
if (localStorage.getItem('chat_link')) {
headerButtons.splice(1, 0, {
name: '聊天',
to: '/chat',
icon: 'comments',
});
}
import { StyleContext } from '../context/Style/index.js';
const HeaderBar = () => {
const { t, i18n } = useTranslation();
const [userState, userDispatch] = useContext(UserContext);
const [styleState, styleDispatch] = useContext(StyleContext);
let navigate = useNavigate();
const [currentLang, setCurrentLang] = useState(i18n.language);
const [showSidebar, setShowSidebar] = useState(false);
const systemName = getSystemName();
const logo = getLogo();
const currentDate = new Date();
// enable fireworks on new year(1.1 and 2.9-2.24)
const isNewYear =
(currentDate.getMonth() === 0 && currentDate.getDate() === 1) ||
(currentDate.getMonth() === 1 &&
currentDate.getDate() >= 9 &&
currentDate.getDate() <= 24);
(currentDate.getMonth() === 0 && currentDate.getDate() === 1);
let buttons = [
{
text: t('首页'),
itemKey: 'home',
to: '/',
},
{
text: t('控制台'),
itemKey: 'detail',
to: '/',
},
{
text: t('定价'),
itemKey: 'pricing',
to: '/pricing',
},
{
text: t('关于'),
itemKey: 'about',
to: '/about',
},
];
async function logout() {
setShowSidebar(false);
await API.get('/api/user/logout');
showSuccess('注销成功!');
showSuccess(t('注销成功!'));
userDispatch({ type: 'logout' });
localStorage.removeItem('user');
navigate('/login');
@@ -102,42 +94,82 @@ const HeaderBar = () => {
}
}, []);
useEffect(() => {
const handleLanguageChanged = (lng) => {
setCurrentLang(lng);
};
i18n.on('languageChanged', handleLanguageChanged);
return () => {
i18n.off('languageChanged', handleLanguageChanged);
};
}, [i18n]);
const handleLanguageChange = (lang) => {
i18n.changeLanguage(lang);
};
return (
<>
<Layout>
<div style={{ width: '100%' }}>
<Nav
className={'topnav'}
mode={'horizontal'}
// bodyStyle={{ height: 100 }}
renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
const routerMap = {
about: '/about',
login: '/login',
register: '/register',
pricing: '/pricing',
detail: '/detail',
home: '/',
};
return (
<Link
style={{ textDecoration: 'none' }}
to={routerMap[props.itemKey]}
>
{itemElement}
</Link>
<div onClick={(e) => {
if (props.itemKey === 'home') {
styleDispatch({ type: 'SET_INNER_PADDING', payload: false });
styleDispatch({ type: 'SET_SIDER', payload: false });
} else {
styleDispatch({ type: 'SET_INNER_PADDING', payload: true });
if (!styleState.isMobile) {
styleDispatch({ type: 'SET_SIDER', payload: true });
}
}
}}>
<Link
className="header-bar-text"
style={{ textDecoration: 'none' }}
to={routerMap[props.itemKey]}
>
{itemElement}
</Link>
</div>
);
}}
selectedKeys={[]}
// items={headerButtons}
onSelect={(key) => {}}
header={isMobile()?{
header={styleState.isMobile?{
logo: (
<img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
<>
{
!styleState.showSider ?
<Button icon={<IconMenu />} theme="light" aria-label={t('展开侧边栏')} onClick={
() => styleDispatch({ type: 'SET_SIDER', payload: true })
} />:
<Button icon={<IconIndentLeft />} theme="light" aria-label={t('闭侧边栏')} onClick={
() => styleDispatch({ type: 'SET_SIDER', payload: false })
} />
}
</>
),
}:{
logo: (
<img src={logo} alt='logo' />
),
text: systemName,
}}
items={buttons}
footer={
@@ -157,27 +189,49 @@ const HeaderBar = () => {
<Nav.Item itemKey={'new-year'} text={'🏮'} />
</Dropdown>
)}
<Nav.Item itemKey={'about'} icon={<IconHelpCircle />} />
{/* <Nav.Item itemKey={'about'} icon={<IconHelpCircle />} /> */}
<>
{!isMobile() && (
<Switch
checkedText='🌞'
size={'large'}
checked={theme === 'dark'}
uncheckedText='🌙'
onChange={(checked) => {
setTheme(checked);
}}
/>
)}
<Switch
checkedText='🌞'
size={styleState.isMobile?'default':'large'}
checked={theme === 'dark'}
uncheckedText='🌙'
onChange={(checked) => {
setTheme(checked);
}}
/>
</>
<Dropdown
position='bottomRight'
render={
<Dropdown.Menu>
<Dropdown.Item
onClick={() => handleLanguageChange('zh')}
type={currentLang === 'zh' ? 'primary' : 'tertiary'}
>
中文
</Dropdown.Item>
<Dropdown.Item
onClick={() => handleLanguageChange('en')}
type={currentLang === 'en' ? 'primary' : 'tertiary'}
>
English
</Dropdown.Item>
</Dropdown.Menu>
}
>
<Nav.Item
itemKey={'language'}
icon={<IconLanguage />}
/>
</Dropdown>
{userState.user ? (
<>
<Dropdown
position='bottomRight'
render={
<Dropdown.Menu>
<Dropdown.Item onClick={logout}>退出</Dropdown.Item>
<Dropdown.Item onClick={logout}>{t('退出')}</Dropdown.Item>
</Dropdown.Menu>
}
>
@@ -188,21 +242,25 @@ const HeaderBar = () => {
>
{userState.user.username[0]}
</Avatar>
<span>{userState.user.username}</span>
{styleState.isMobile?null:<Text>{userState.user.username}</Text>}
</Dropdown>
</>
) : (
<>
<Nav.Item
itemKey={'login'}
text={'登录'}
// icon={<IconKey />}
/>
<Nav.Item
itemKey={'register'}
text={'注册'}
text={!styleState.isMobile?t('登录'):null}
icon={<IconUser />}
/>
{
!styleState.isMobile && (
<Nav.Item
itemKey={'register'}
text={t('注册')}
icon={<IconKey />}
/>
)
}
</>
)}
</>

View File

@@ -28,6 +28,7 @@ import { IconGithubLogo, IconAlarm } from '@douyinfe/semi-icons';
import WeChatIcon from './WeChatIcon';
import { setUserData } from '../helpers/data.js';
import LinuxDoIcon from './LinuxDoIcon.js';
import { useTranslation } from 'react-i18next';
const LoginForm = () => {
const [inputs, setInputs] = useState({
@@ -44,11 +45,19 @@ const LoginForm = () => {
const [turnstileToken, setTurnstileToken] = useState('');
let navigate = useNavigate();
const [status, setStatus] = useState({});
const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false);
const { t } = useTranslation();
const logo = getLogo();
let affCode = new URLSearchParams(window.location.search).get('aff');
if (affCode) {
localStorage.setItem('aff', affCode);
}
useEffect(() => {
if (searchParams.get('expired')) {
showError('未登录或登录已过期,请重新登录');
showError(t('未登录或登录已过期,请重新登录'));
}
let status = localStorage.getItem('status');
if (status) {
@@ -61,7 +70,6 @@ const LoginForm = () => {
}
}, []);
const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false);
const onWeChatLoginClicked = () => {
setShowWeChatLoginModal(true);
@@ -176,20 +184,20 @@ const LoginForm = () => {
<div style={{ width: 500 }}>
<Card>
<Title heading={2} style={{ textAlign: 'center' }}>
用户登录
{t('用户登录')}
</Title>
<Form>
<Form.Input
field={'username'}
label={'用户名'}
placeholder='用户名'
label={t('用户名/邮箱')}
placeholder={t('用户名/邮箱')}
name='username'
onChange={(value) => handleChange('username', value)}
/>
<Form.Input
field={'password'}
label={'密码'}
placeholder='密码'
label={t('密码')}
placeholder={t('密码')}
name='password'
type='password'
onChange={(value) => handleChange('password', value)}
@@ -203,7 +211,7 @@ const LoginForm = () => {
htmlType={'submit'}
onClick={handleSubmit}
>
登录
{t('登录')}
</Button>
</Form>
<div
@@ -214,10 +222,10 @@ const LoginForm = () => {
}}
>
<Text>
没有账号请先 <Link to='/register'>注册账号</Link>
{t('没有账户?')} <Link to='/register'>{t('点击注册')}</Link>
</Text>
<Text>
忘记密码 <Link to='/reset'>点击重置</Link>
{t('忘记密码?')} <Link to='/reset'>{t('点击重置')}</Link>
</Text>
</div>
{status.github_oauth ||
@@ -226,7 +234,7 @@ const LoginForm = () => {
status.linuxdo_oauth ? (
<>
<Divider margin='12px' align='center'>
第三方登录
{t('第三方登录')}
</Divider>
<div
style={{
@@ -290,12 +298,12 @@ const LoginForm = () => {
<></>
)}
<Modal
title='微信扫码登录'
title={t('微信扫码登录')}
visible={showWeChatLoginModal}
maskClosable={true}
onOk={onSubmitWeChatVerificationCode}
onCancel={() => setShowWeChatLoginModal(false)}
okText={'登录'}
okText={t('登录')}
size={'small'}
centered={true}
>
@@ -310,14 +318,14 @@ const LoginForm = () => {
</div>
<div style={{ textAlign: 'center' }}>
<p>
微信扫码关注公众号输入验证码获取验证码三分钟内有效
{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}
</p>
</div>
<Form size='large'>
<Form.Input
field={'wechat_verification_code'}
placeholder='验证码'
label={'验证码'}
placeholder={t('验证码')}
label={t('验证码')}
value={inputs.wechat_verification_code}
onChange={(value) =>
handleChange('wechat_verification_code', value)

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
API,
copy,
@@ -25,7 +26,7 @@ import {
import { ITEMS_PER_PAGE } from '../constants';
import {
renderAudioModelPrice,
renderModelPrice,
renderModelPrice, renderModelPriceSimple,
renderNumber,
renderQuota,
stringToColor
@@ -40,8 +41,8 @@ function renderTimestamp(timestamp) {
}
const MODE_OPTIONS = [
{ key: 'all', text: '全部用户', value: 'all' },
{ key: 'self', text: '当前用户', value: 'self' },
{ key: 'all', text: 'all', value: 'all' },
{ key: 'self', text: 'current user', value: 'self' },
];
const colors = [
@@ -62,123 +63,92 @@ const colors = [
'yellow',
];
function renderType(type) {
switch (type) {
case 1:
const LogsTable = () => {
const { t } = useTranslation();
function renderType(type) {
switch (type) {
case 1:
return <Tag color='cyan' size='large'>{t('充值')}</Tag>;
case 2:
return <Tag color='lime' size='large'>{t('消费')}</Tag>;
case 3:
return <Tag color='orange' size='large'>{t('管理')}</Tag>;
case 4:
return <Tag color='purple' size='large'>{t('系统')}</Tag>;
default:
return <Tag color='black' size='large'>{t('未知')}</Tag>;
}
}
function renderIsStream(bool) {
if (bool) {
return <Tag color='blue' size='large'>{t('流')}</Tag>;
} else {
return <Tag color='purple' size='large'>{t('非流')}</Tag>;
}
}
function renderUseTime(type) {
const time = parseInt(type);
if (time < 101) {
return (
<Tag color='cyan' size='large'>
<Tag color='green' size='large'>
{' '}
充值{' '}
{time} s{' '}
</Tag>
);
case 2:
return (
<Tag color='lime' size='large'>
{' '}
消费{' '}
</Tag>
);
case 3:
} else if (time < 300) {
return (
<Tag color='orange' size='large'>
{' '}
管理{' '}
{time} s{' '}
</Tag>
);
case 4:
} else {
return (
<Tag color='purple' size='large'>
<Tag color='red' size='large'>
{' '}
系统{' '}
{time} s{' '}
</Tag>
);
default:
}
}
function renderFirstUseTime(type) {
let time = parseFloat(type) / 1000.0;
time = time.toFixed(1);
if (time < 3) {
return (
<Tag color='black' size='large'>
<Tag color='green' size='large'>
{' '}
未知{' '}
{time} s{' '}
</Tag>
);
}
}
} else if (time < 10) {
return (
<Tag color='orange' size='large'>
{' '}
{time} s{' '}
</Tag>
);
} else {
return (
<Tag color='red' size='large'>
{' '}
{time} s{' '}
</Tag>
);
}
}
function renderIsStream(bool) {
if (bool) {
return (
<Tag color='blue' size='large'>
</Tag>
);
} else {
return (
<Tag color='purple' size='large'>
非流
</Tag>
);
}
}
function renderUseTime(type) {
const time = parseInt(type);
if (time < 101) {
return (
<Tag color='green' size='large'>
{' '}
{time} s{' '}
</Tag>
);
} else if (time < 300) {
return (
<Tag color='orange' size='large'>
{' '}
{time} s{' '}
</Tag>
);
} else {
return (
<Tag color='red' size='large'>
{' '}
{time} s{' '}
</Tag>
);
}
}
function renderFirstUseTime(type) {
let time = parseFloat(type) / 1000.0;
time = time.toFixed(1);
if (time < 3) {
return (
<Tag color='green' size='large'>
{' '}
{time} s{' '}
</Tag>
);
} else if (time < 10) {
return (
<Tag color='orange' size='large'>
{' '}
{time} s{' '}
</Tag>
);
} else {
return (
<Tag color='red' size='large'>
{' '}
{time} s{' '}
</Tag>
);
}
}
const LogsTable = () => {
const columns = [
{
title: '时间',
title: t('时间'),
dataIndex: 'timestamp2string',
},
{
title: '渠道',
title: t('渠道'),
dataIndex: 'channel',
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
@@ -204,7 +174,7 @@ const LogsTable = () => {
},
},
{
title: '用户',
title: t('用户'),
dataIndex: 'username',
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
@@ -226,7 +196,7 @@ const LogsTable = () => {
},
},
{
title: '令牌',
title: t('令牌'),
dataIndex: 'token_name',
render: (text, record, index) => {
return record.type === 0 || record.type === 2 ? (
@@ -239,7 +209,7 @@ const LogsTable = () => {
}}
>
{' '}
{text}{' '}
{t(text)}{' '}
</Tag>
</div>
) : (
@@ -248,14 +218,14 @@ const LogsTable = () => {
},
},
{
title: '类型',
title: t('类型'),
dataIndex: 'type',
render: (text, record, index) => {
return <>{renderType(text)}</>;
},
},
{
title: '模型',
title: t('模型'),
dataIndex: 'model_name',
render: (text, record, index) => {
return record.type === 0 || record.type === 2 ? (
@@ -277,7 +247,7 @@ const LogsTable = () => {
},
},
{
title: '用时/首字',
title: t('用时/首字'),
dataIndex: 'use_time',
render: (text, record, index) => {
if (record.is_stream) {
@@ -304,7 +274,7 @@ const LogsTable = () => {
},
},
{
title: '提示',
title: t('提示'),
dataIndex: 'prompt_tokens',
render: (text, record, index) => {
return record.type === 0 || record.type === 2 ? (
@@ -315,7 +285,7 @@ const LogsTable = () => {
},
},
{
title: '补全',
title: t('补全'),
dataIndex: 'completion_tokens',
render: (text, record, index) => {
return parseInt(text) > 0 &&
@@ -327,7 +297,7 @@ const LogsTable = () => {
},
},
{
title: '花费',
title: t('花费'),
dataIndex: 'quota',
render: (text, record, index) => {
return record.type === 0 || record.type === 2 ? (
@@ -338,11 +308,11 @@ const LogsTable = () => {
},
},
{
title: '重试',
title: t('重试'),
dataIndex: 'retry',
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
let content = '渠道' + record.channel;
let content = t('渠道') + `${record.channel}`;
if (record.other !== '') {
let other = JSON.parse(record.other);
if (other === null) {
@@ -357,7 +327,7 @@ const LogsTable = () => {
// channel id array
let useChannel = other.admin_info.use_channel;
let useChannelStr = useChannel.join('->');
content = `渠道${useChannelStr}`;
content = t('渠道') + `${useChannelStr}`;
}
}
}
@@ -365,7 +335,7 @@ const LogsTable = () => {
},
},
{
title: '详情',
title: t('详情'),
dataIndex: 'content',
render: (text, record, index) => {
let other = getLogOther(record.other);
@@ -386,14 +356,11 @@ const LogsTable = () => {
);
}
// let content = renderModelPrice(
// record.prompt_tokens,
// record.completion_tokens,
// other.model_ratio,
// other.model_price,
// other.completion_ratio,
// other.group_ratio,
// );
let content = renderModelPriceSimple(
other.model_ratio,
other.model_price,
other.group_ratio,
);
return (
<Paragraph
ellipsis={{
@@ -401,7 +368,7 @@ const LogsTable = () => {
}}
style={{ maxWidth: 240 }}
>
调用消费
{content}
</Paragraph>
);
},
@@ -496,13 +463,13 @@ const LogsTable = () => {
const { success, message, data } = res.data;
if (success) {
Modal.info({
title: '用户信息',
title: t('用户信息'),
content: (
<div style={{ padding: 12 }}>
<p>用户名: {data.username}</p>
<p>余额: {renderQuota(data.quota)}</p>
<p>已用额度{renderQuota(data.used_quota)}</p>
<p>请求次数{renderNumber(data.request_count)}</p>
<p>{t('用户名')}: {data.username}</p>
<p>{t('余额')}: {renderQuota(data.quota)}</p>
<p>{t('已用额度')}{renderQuota(data.used_quota)}</p>
<p>{t('请求次数')}{renderNumber(data.request_count)}</p>
</div>
),
centered: true,
@@ -540,26 +507,26 @@ const LogsTable = () => {
}
if (other?.ws || other?.audio) {
expandDataLocal.push({
key: '语音输入',
key: t('语音输入'),
value: other.audio_input,
});
expandDataLocal.push({
key: '语音输出',
key: t('语音输出'),
value: other.audio_output,
});
expandDataLocal.push({
key: '文字输入',
key: t('文字输入'),
value: other.text_input,
});
expandDataLocal.push({
key: '文字输出',
key: t('文字输出'),
value: other.text_output,
});
}
expandDataLocal.push({
key: '日志详情',
key: t('日志详情'),
value: logs[i].content,
})
});
if (logs[i].type === 2) {
let content = '';
if (other?.ws || other?.audio) {
@@ -586,7 +553,7 @@ const LogsTable = () => {
);
}
expandDataLocal.push({
key: '计费过程',
key: t('计费过程'),
value: content,
});
}
@@ -679,7 +646,7 @@ const LogsTable = () => {
<Spin spinning={loadingStat}>
<Space>
<Tag color='green' size='large' style={{ padding: 15 }}>
总消耗额度: {renderQuota(stat.quota)}
{t('总消耗额度')}: {renderQuota(stat.quota)}
</Tag>
<Tag color='blue' size='large' style={{ padding: 15 }}>
RPM: {stat.rpm}
@@ -694,25 +661,25 @@ const LogsTable = () => {
<>
<Form.Input
field='token_name'
label='令牌名称'
label={t('令牌名称')}
style={{ width: 176 }}
value={token_name}
placeholder={'可选值'}
placeholder={t('可选值')}
name='token_name'
onChange={(value) => handleInputChange(value, 'token_name')}
/>
<Form.Input
field='model_name'
label='模型名称'
label={t('模型名称')}
style={{ width: 176 }}
value={model_name}
placeholder='可选值'
placeholder={t('可选值')}
name='model_name'
onChange={(value) => handleInputChange(value, 'model_name')}
/>
<Form.DatePicker
field='start_timestamp'
label='起始时间'
label={t('起始时间')}
style={{ width: 272 }}
initValue={start_timestamp}
value={start_timestamp}
@@ -723,7 +690,7 @@ const LogsTable = () => {
<Form.DatePicker
field='end_timestamp'
fluid
label='结束时间'
label={t('结束时间')}
style={{ width: 272 }}
initValue={end_timestamp}
value={end_timestamp}
@@ -735,26 +702,26 @@ const LogsTable = () => {
<>
<Form.Input
field='channel'
label='渠道 ID'
label={t('渠道 ID')}
style={{ width: 176 }}
value={channel}
placeholder='可选值'
placeholder={t('可选值')}
name='channel'
onChange={(value) => handleInputChange(value, 'channel')}
/>
<Form.Input
field='username'
label='用户名称'
label={t('用户名称')}
style={{ width: 176 }}
value={username}
placeholder={'可选值'}
placeholder={t('可选值')}
name='username'
onChange={(value) => handleInputChange(value, 'username')}
/>
</>
)}
<Button
label='查询'
label={t('查询')}
type='primary'
htmlType='submit'
className='btn-margin-right'
@@ -762,7 +729,7 @@ const LogsTable = () => {
loading={loading}
style={{ marginTop: 24 }}
>
查询
{t('查询')}
</Button>
<Form.Section></Form.Section>
</>
@@ -776,11 +743,11 @@ const LogsTable = () => {
loadLogs(0, pageSize, parseInt(value));
}}
>
<Select.Option value='0'>全部</Select.Option>
<Select.Option value='1'>充值</Select.Option>
<Select.Option value='2'>消费</Select.Option>
<Select.Option value='3'>管理</Select.Option>
<Select.Option value='4'>系统</Select.Option>
<Select.Option value='0'>{t('全部')}</Select.Option>
<Select.Option value='1'>{t('充值')}</Select.Option>
<Select.Option value='2'>{t('消费')}</Select.Option>
<Select.Option value='3'>{t('管理')}</Select.Option>
<Select.Option value='4'>{t('系统')}</Select.Option>
</Select>
</div>
<Table

View File

@@ -21,6 +21,7 @@ import {
Typography,
} from '@douyinfe/semi-ui';
import { ITEMS_PER_PAGE } from '../constants';
import { useTranslation } from 'react-i18next';
const colors = [
'amber',
@@ -40,247 +41,241 @@ const colors = [
'yellow',
];
function renderType(type) {
switch (type) {
case 'IMAGINE':
return (
<Tag color='blue' size='large'>
绘图
</Tag>
);
case 'UPSCALE':
return (
<Tag color='orange' size='large'>
放大
</Tag>
);
case 'VARIATION':
return (
<Tag color='purple' size='large'>
变换
</Tag>
);
case 'HIGH_VARIATION':
return (
<Tag color='purple' size='large'>
强变换
</Tag>
);
case 'LOW_VARIATION':
return (
<Tag color='purple' size='large'>
弱变换
</Tag>
);
case 'PAN':
return (
<Tag color='cyan' size='large'>
平移
</Tag>
);
case 'DESCRIBE':
return (
<Tag color='yellow' size='large'>
图生文
</Tag>
);
case 'BLEND':
return (
<Tag color='lime' size='large'>
图混合
</Tag>
);
case 'UPLOAD':
return (
<Tag color='blue' size='large'>
上传文件
</Tag>
);
case 'SHORTEN':
return (
<Tag color='pink' size='large'>
缩词
</Tag>
);
case 'REROLL':
return (
<Tag color='indigo' size='large'>
重绘
</Tag>
);
case 'INPAINT':
return (
<Tag color='violet' size='large'>
局部重绘-提交
</Tag>
);
case 'ZOOM':
return (
<Tag color='teal' size='large'>
变焦
</Tag>
);
case 'CUSTOM_ZOOM':
return (
<Tag color='teal' size='large'>
自定义变焦-提交
</Tag>
);
case 'MODAL':
return (
<Tag color='green' size='large'>
窗口处理
</Tag>
);
case 'SWAP_FACE':
return (
<Tag color='light-green' size='large'>
换脸
</Tag>
);
default:
return (
<Tag color='white' size='large'>
未知
</Tag>
);
}
}
function renderCode(code) {
switch (code) {
case 1:
return (
<Tag color='green' size='large'>
已提交
</Tag>
);
case 21:
return (
<Tag color='lime' size='large'>
等待中
</Tag>
);
case 22:
return (
<Tag color='orange' size='large'>
重复提交
</Tag>
);
case 0:
return (
<Tag color='yellow' size='large'>
未提交
</Tag>
);
default:
return (
<Tag color='white' size='large'>
未知
</Tag>
);
}
}
function renderStatus(type) {
// Ensure all cases are string literals by adding quotes.
switch (type) {
case 'SUCCESS':
return (
<Tag color='green' size='large'>
成功
</Tag>
);
case 'NOT_START':
return (
<Tag color='grey' size='large'>
未启动
</Tag>
);
case 'SUBMITTED':
return (
<Tag color='yellow' size='large'>
队列中
</Tag>
);
case 'IN_PROGRESS':
return (
<Tag color='blue' size='large'>
执行中
</Tag>
);
case 'FAILURE':
return (
<Tag color='red' size='large'>
失败
</Tag>
);
case 'MODAL':
return (
<Tag color='yellow' size='large'>
窗口等待
</Tag>
);
default:
return (
<Tag color='white' size='large'>
未知
</Tag>
);
}
}
const renderTimestamp = (timestampInSeconds) => {
const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒
const year = date.getFullYear(); // 获取年份
const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份从0开始需要+1并保证两位数
const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数
const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数
const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数
const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出
};
// 修改renderDuration函数以包含颜色逻辑
function renderDuration(submit_time, finishTime) {
// 确保startTime和finishTime都是有效的时间戳
if (!submit_time || !finishTime) return 'N/A';
// 将时间戳转换为Date对象
const start = new Date(submit_time);
const finish = new Date(finishTime);
// 计算时间差(毫秒)
const durationMs = finish - start;
// 将时间差转换为秒,并保留一位小数
const durationSec = (durationMs / 1000).toFixed(1);
// 设置颜色大于60秒则为红色小于等于60秒则为绿色
const color = durationSec > 60 ? 'red' : 'green';
// 返回带有样式的颜色标签
return (
<Tag color={color} size="large">
{durationSec}
</Tag>
);
}
const LogsTable = () => {
const { t } = useTranslation();
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalContent, setModalContent] = useState('');
function renderType(type) {
switch (type) {
case 'IMAGINE':
return (
<Tag color='blue' size='large'>
{t('绘图')}
</Tag>
);
case 'UPSCALE':
return (
<Tag color='orange' size='large'>
{t('放大')}
</Tag>
);
case 'VARIATION':
return (
<Tag color='purple' size='large'>
{t('变换')}
</Tag>
);
case 'HIGH_VARIATION':
return (
<Tag color='purple' size='large'>
{t('强变换')}
</Tag>
);
case 'LOW_VARIATION':
return (
<Tag color='purple' size='large'>
{t('弱变换')}
</Tag>
);
case 'PAN':
return (
<Tag color='cyan' size='large'>
{t('平移')}
</Tag>
);
case 'DESCRIBE':
return (
<Tag color='yellow' size='large'>
{t('图生文')}
</Tag>
);
case 'BLEND':
return (
<Tag color='lime' size='large'>
{t('图混合')}
</Tag>
);
case 'UPLOAD':
return (
<Tag color='blue' size='large'>
上传文件
</Tag>
);
case 'SHORTEN':
return (
<Tag color='pink' size='large'>
{t('缩词')}
</Tag>
);
case 'REROLL':
return (
<Tag color='indigo' size='large'>
{t('重绘')}
</Tag>
);
case 'INPAINT':
return (
<Tag color='violet' size='large'>
{t('局部重绘-提交')}
</Tag>
);
case 'ZOOM':
return (
<Tag color='teal' size='large'>
{t('变焦')}
</Tag>
);
case 'CUSTOM_ZOOM':
return (
<Tag color='teal' size='large'>
{t('自定义变焦-提交')}
</Tag>
);
case 'MODAL':
return (
<Tag color='green' size='large'>
{t('窗口处理')}
</Tag>
);
case 'SWAP_FACE':
return (
<Tag color='light-green' size='large'>
{t('换脸')}
</Tag>
);
default:
return (
<Tag color='white' size='large'>
{t('未知')}
</Tag>
);
}
}
function renderCode(code) {
switch (code) {
case 1:
return (
<Tag color='green' size='large'>
{t('已提交')}
</Tag>
);
case 21:
return (
<Tag color='lime' size='large'>
{t('等待中')}
</Tag>
);
case 22:
return (
<Tag color='orange' size='large'>
{t('重复提交')}
</Tag>
);
case 0:
return (
<Tag color='yellow' size='large'>
{t('未提交')}
</Tag>
);
default:
return (
<Tag color='white' size='large'>
{t('未知')}
</Tag>
);
}
}
function renderStatus(type) {
switch (type) {
case 'SUCCESS':
return (
<Tag color='green' size='large'>
{t('成功')}
</Tag>
);
case 'NOT_START':
return (
<Tag color='grey' size='large'>
{t('未启动')}
</Tag>
);
case 'SUBMITTED':
return (
<Tag color='yellow' size='large'>
{t('队列中')}
</Tag>
);
case 'IN_PROGRESS':
return (
<Tag color='blue' size='large'>
{t('执行中')}
</Tag>
);
case 'FAILURE':
return (
<Tag color='red' size='large'>
{t('失败')}
</Tag>
);
case 'MODAL':
return (
<Tag color='yellow' size='large'>
{t('窗口等待')}
</Tag>
);
default:
return (
<Tag color='white' size='large'>
{t('未知')}
</Tag>
);
}
}
const renderTimestamp = (timestampInSeconds) => {
const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒
const year = date.getFullYear(); // 获取年份
const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份从0开始需要+1并保证两位数
const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数
const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数
const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数
const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出
};
// 修改renderDuration函数以包含颜色逻辑
function renderDuration(submit_time, finishTime) {
if (!submit_time || !finishTime) return 'N/A';
const start = new Date(submit_time);
const finish = new Date(finishTime);
const durationMs = finish - start;
const durationSec = (durationMs / 1000).toFixed(1);
const color = durationSec > 60 ? 'red' : 'green';
return (
<Tag color={color} size="large">
{durationSec} {t('秒')}
</Tag>
);
}
const columns = [
{
title: '提交时间',
title: t('提交时间'),
dataIndex: 'submit_time',
render: (text, record, index) => {
return <div>{renderTimestamp(text / 1000)}</div>;
},
},
{
title: '花费时间',
title: t('花费时间'),
dataIndex: 'finish_time', // 以finish_time作为dataIndex
key: 'finish_time',
render: (finish, record) => {
@@ -289,7 +284,7 @@ const LogsTable = () => {
},
},
{
title: '渠道',
title: t('渠道'),
dataIndex: 'channel_id',
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
@@ -310,21 +305,21 @@ const LogsTable = () => {
},
},
{
title: '类型',
title: t('类型'),
dataIndex: 'action',
render: (text, record, index) => {
return <div>{renderType(text)}</div>;
},
},
{
title: '任务ID',
title: t('任务ID'),
dataIndex: 'mj_id',
render: (text, record, index) => {
return <div>{text}</div>;
},
},
{
title: '提交结果',
title: t('提交结果'),
dataIndex: 'code',
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
@@ -332,7 +327,7 @@ const LogsTable = () => {
},
},
{
title: '任务状态',
title: t('任务状态'),
dataIndex: 'status',
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
@@ -340,7 +335,7 @@ const LogsTable = () => {
},
},
{
title: '进度',
title: t('进度'),
dataIndex: 'progress',
render: (text, record, index) => {
return (
@@ -363,11 +358,11 @@ const LogsTable = () => {
},
},
{
title: '结果图片',
title: t('结果图片'),
dataIndex: 'image_url',
render: (text, record, index) => {
if (!text) {
return '无';
return t('无');
}
return (
<Button
@@ -376,7 +371,7 @@ const LogsTable = () => {
setIsModalOpenurl(true); // 打开模态框
}}
>
查看图片
{t('查看图片')}
</Button>
);
},
@@ -387,7 +382,7 @@ const LogsTable = () => {
render: (text, record, index) => {
// 如果text未定义返回替代文本例如空字符串''或其他
if (!text) {
return '无';
return t('无');
}
return (
@@ -410,7 +405,7 @@ const LogsTable = () => {
render: (text, record, index) => {
// 如果text未定义返回替代文本例如空字符串''或其他
if (!text) {
return '无';
return t('无');
}
return (
@@ -428,12 +423,12 @@ const LogsTable = () => {
},
},
{
title: '失败原因',
title: t('失败原因'),
dataIndex: 'fail_reason',
render: (text, record, index) => {
// 如果text未定义返回替代文本例如空字符串''或其他
if (!text) {
return '无';
return t('无');
}
return (
@@ -565,7 +560,7 @@ const LogsTable = () => {
{isAdminUser && showBanner ? (
<Banner
type='info'
description='当前未开启Midjourney回调部分项目可能无法获得绘图结果可在运营设置中开启。'
description={t('当前未开启Midjourney回调部分项目可能无法获得绘图结果可在运营设置中开启。')}
/>
) : (
<></>
@@ -574,25 +569,25 @@ const LogsTable = () => {
<>
<Form.Input
field='channel_id'
label='渠道 ID'
label={t('渠道 ID')}
style={{ width: 176 }}
value={channel_id}
placeholder={'可选值'}
placeholder={t('可选值')}
name='channel_id'
onChange={(value) => handleInputChange(value, 'channel_id')}
/>
<Form.Input
field='mj_id'
label='任务 ID'
label={t('任务 ID')}
style={{ width: 176 }}
value={mj_id}
placeholder='可选值'
placeholder={t('可选值')}
name='mj_id'
onChange={(value) => handleInputChange(value, 'mj_id')}
/>
<Form.DatePicker
field='start_timestamp'
label='起始时间'
label={t('起始时间')}
style={{ width: 272 }}
initValue={start_timestamp}
value={start_timestamp}
@@ -603,7 +598,7 @@ const LogsTable = () => {
<Form.DatePicker
field='end_timestamp'
fluid
label='结束时间'
label={t('结束时间')}
style={{ width: 272 }}
initValue={end_timestamp}
value={end_timestamp}
@@ -614,13 +609,13 @@ const LogsTable = () => {
<Form.Section>
<Button
label='查询'
label={t('查询')}
type='primary'
htmlType='submit'
className='btn-margin-right'
onClick={refresh}
>
查询
{t('查询')}
</Button>
</Form.Section>
</>
@@ -635,6 +630,12 @@ const LogsTable = () => {
total: logCount,
pageSizeOpts: [10, 20, 50, 100],
onPageChange: handlePageChange,
formatPageText: (page) =>
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: logCount
}),
}}
loading={loading}
/>

View File

@@ -1,5 +1,6 @@
import React, { useContext, useEffect, useRef, useMemo, useState } from 'react';
import { API, copy, showError, showInfo, showSuccess } from '../helpers';
import { useTranslation } from 'react-i18next';
import {
Banner,
@@ -23,65 +24,8 @@ import {
import { UserContext } from '../context/User/index.js';
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
function renderQuotaType(type) {
// Ensure all cases are string literals by adding quotes.
switch (type) {
case 1:
return (
<Tag color='teal' size='large'>
按次计费
</Tag>
);
case 0:
return (
<Tag color='violet' size='large'>
按量计费
</Tag>
);
default:
return '未知';
}
}
function renderAvailable(available) {
return available ? (
<Popover
content={
<div style={{ padding: 8 }}>您的分组可以使用该模型</div>
}
position='top'
key={available}
style={{
backgroundColor: 'rgba(var(--semi-blue-4),1)',
borderColor: 'rgba(var(--semi-blue-4),1)',
color: 'var(--semi-color-white)',
borderWidth: 1,
borderStyle: 'solid',
}}
>
<IconVerify style={{ color: 'green' }} size="large" />
</Popover>
) : (
<Popover
content={
<div style={{ padding: 8 }}>您的分组无权使用该模型</div>
}
position='top'
key={available}
style={{
backgroundColor: 'rgba(var(--semi-blue-4),1)',
borderColor: 'rgba(var(--semi-blue-4),1)',
color: 'var(--semi-color-white)',
borderWidth: 1,
borderStyle: 'solid',
}}
>
<IconUploadError style={{ color: '#FFA54F' }} size="large" />
</Popover>
);
}
const ModelPricing = () => {
const { t } = useTranslation();
const [filteredValue, setFilteredValue] = useState([]);
const compositionRef = useRef({ isComposition: false });
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
@@ -115,10 +59,68 @@ const ModelPricing = () => {
const newFilteredValue = value ? [value] : [];
setFilteredValue(newFilteredValue);
};
function renderQuotaType(type) {
// Ensure all cases are string literals by adding quotes.
switch (type) {
case 1:
return (
<Tag color='teal' size='large'>
{t('按次计费')}
</Tag>
);
case 0:
return (
<Tag color='violet' size='large'>
{t('按量计费')}
</Tag>
);
default:
return t('未知');
}
}
function renderAvailable(available) {
return available ? (
<Popover
content={
<div style={{ padding: 8 }}>{t('您的分组可以使用该模型')}</div>
}
position='top'
key={available}
style={{
backgroundColor: 'rgba(var(--semi-blue-4),1)',
borderColor: 'rgba(var(--semi-blue-4),1)',
color: 'var(--semi-color-white)',
borderWidth: 1,
borderStyle: 'solid',
}}
>
<IconVerify style={{ color: 'green' }} size="large" />
</Popover>
) : (
<Popover
content={
<div style={{ padding: 8 }}>{t('您的分组无权使用该模型')}</div>
}
position='top'
key={available}
style={{
backgroundColor: 'rgba(var(--semi-blue-4),1)',
borderColor: 'rgba(var(--semi-blue-4),1)',
color: 'var(--semi-color-white)',
borderWidth: 1,
borderStyle: 'solid',
}}
>
<IconUploadError style={{ color: '#FFA54F' }} size="large" />
</Popover>
);
}
const columns = [
{
title: '可用性',
title: t('可用性'),
dataIndex: 'available',
render: (text, record, index) => {
// if record.enable_groups contains selectedGroup, then available is true
@@ -127,20 +129,8 @@ const ModelPricing = () => {
sorter: (a, b) => a.available - b.available,
},
{
title: (
<Space>
<span>模型名称</span>
<Input
placeholder='模糊搜索'
style={{ width: 200 }}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onChange={handleChange}
showClear
/>
</Space>
),
dataIndex: 'model_name', // 以finish_time作为dataIndex
title: t('模型名称'),
dataIndex: 'model_name',
render: (text, record, index) => {
return (
<>
@@ -161,7 +151,7 @@ const ModelPricing = () => {
filteredValue,
},
{
title: '计费类型',
title: t('计费类型'),
dataIndex: 'quota_type',
render: (text, record, index) => {
return renderQuotaType(parseInt(text));
@@ -169,7 +159,7 @@ const ModelPricing = () => {
sorter: (a, b) => a.quota_type - b.quota_type,
},
{
title: '可用分组',
title: t('可用分组'),
dataIndex: 'enable_groups',
render: (text, record, index) => {
// enable_groups is a string array
@@ -193,7 +183,10 @@ const ModelPricing = () => {
size='large'
onClick={() => {
setSelectedGroup(group);
showInfo('当前查看的分组为:' + group + ',倍率为:' + groupRatio[group]);
showInfo(t('当前查看的分组为:{{group}},倍率为:{{ratio}}', {
group: group,
ratio: groupRatio[group]
}));
}}
>
{group}
@@ -208,10 +201,13 @@ const ModelPricing = () => {
{
title: () => (
<span style={{'display':'flex','alignItems':'center'}}>
倍率
{t('倍率')}
<Popover
content={
<div style={{ padding: 8 }}>倍率是为了方便换算不同价格的模型<br/>点击查看倍率说明</div>
<div style={{ padding: 8 }}>
{t('倍率是为了方便换算不同价格的模型')}<br/>
{t('点击查看倍率说明')}
</div>
}
position='top'
style={{
@@ -237,18 +233,18 @@ const ModelPricing = () => {
let completionRatio = parseFloat(record.completion_ratio.toFixed(3));
content = (
<>
<Text>模型{record.quota_type === 0 ? text : '无'}</Text>
<Text>{t('模型倍率')}{record.quota_type === 0 ? text : t('无')}</Text>
<br />
<Text>补全{record.quota_type === 0 ? completionRatio : '无'}</Text>
<Text>{t('补全倍率')}{record.quota_type === 0 ? completionRatio : t('无')}</Text>
<br />
<Text>分组{groupRatio[selectedGroup]}</Text>
<Text>{t('分组倍率')}{groupRatio[selectedGroup]}</Text>
</>
);
return <div>{content}</div>;
},
},
{
title: '模型价格',
title: t('模型价格'),
dataIndex: 'model_price',
render: (text, record, index) => {
let content = text;
@@ -261,14 +257,14 @@ const ModelPricing = () => {
groupRatio[selectedGroup];
content = (
<>
<Text>提示 ${inputRatioPrice} / 1M tokens</Text>
<Text>{t('提示')} ${inputRatioPrice} / 1M tokens</Text>
<br />
<Text>补全 ${completionRatioPrice} / 1M tokens</Text>
<Text>{t('补全')} ${completionRatioPrice} / 1M tokens</Text>
</>
);
} else {
let price = parseFloat(text) * groupRatio[selectedGroup];
content = <>模型价格${price}</>;
content = <>${t('模型价格')}${price}</>;
}
return <div>{content}</div>;
},
@@ -349,41 +345,62 @@ const ModelPricing = () => {
type="success"
fullMode={false}
closeIcon="null"
description={`您的默认分组为:${userState.user.group},分组倍率为:${groupRatio[userState.user.group]}`}
description={t('您的默认分组为:{{group}},分组倍率为:{{ratio}}', {
group: userState.user.group,
ratio: groupRatio[userState.user.group]
})}
/>
) : (
<Banner
type='warning'
fullMode={false}
closeIcon="null"
description={`您还未登陆,显示的价格为默认分组倍率: ${groupRatio['default']}`}
description={t('您还未登陆,显示的价格为默认分组倍率: {{ratio}}', {
ratio: groupRatio['default']
})}
/>
)}
<br/>
<Banner
type="info"
fullMode={false}
description={<div>按量计费费用 = 分组倍率 × 模型倍率 × 提示token数 + 补全token数 × 补全倍率/ 500000 单位美元</div>}
description={<div>{t('按量计费费用 = 分组倍率 × 模型倍率 × 提示token数 + 补全token数 × 补全倍率)/ 500000 (单位:美元)')}</div>}
closeIcon="null"
/>
<br/>
<Button
theme='light'
type='tertiary'
style={{width: 150}}
onClick={() => {
copyText(selectedRowKeys);
}}
disabled={selectedRowKeys == ""}
>
复制选中模型
</Button>
<Space style={{ marginBottom: 16 }}>
<Input
placeholder={t('模糊搜索模型名称')}
style={{ width: 200 }}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onChange={handleChange}
showClear
/>
<Button
theme='light'
type='tertiary'
style={{width: 150}}
onClick={() => {
copyText(selectedRowKeys);
}}
disabled={selectedRowKeys == ""}
>
{t('复制选中模型')}
</Button>
</Space>
<Table
style={{ marginTop: 5 }}
columns={columns}
dataSource={models}
loading={loading}
pagination={{
formatPageText: (page) =>
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: models.length
}),
pageSize: models.length,
showSizeChanger: false,
}}

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Card, Spin } from '@douyinfe/semi-ui';
import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
import SettingsGeneral from '../pages/Setting/Operation/SettingsGeneral.js';
import SettingsDrawing from '../pages/Setting/Operation/SettingsDrawing.js';
import SettingsSensitiveWords from '../pages/Setting/Operation/SettingsSensitiveWords.js';
@@ -8,11 +8,17 @@ import SettingsDataDashboard from '../pages/Setting/Operation/SettingsDataDashbo
import SettingsMonitoring from '../pages/Setting/Operation/SettingsMonitoring.js';
import SettingsCreditLimit from '../pages/Setting/Operation/SettingsCreditLimit.js';
import SettingsMagnification from '../pages/Setting/Operation/SettingsMagnification.js';
import ModelSettingsVisualEditor from '../pages/Setting/Operation/ModelSettingsVisualEditor.js';
import GroupRatioSettings from '../pages/Setting/Operation/GroupRatioSettings.js';
import ModelRatioSettings from '../pages/Setting/Operation/ModelRatioSettings.js';
import { API, showError, showSuccess } from '../helpers';
import SettingsChats from '../pages/Setting/Operation/SettingsChats.js';
import { useTranslation } from 'react-i18next';
const OperationSetting = () => {
const { t } = useTranslation();
let [inputs, setInputs] = useState({
QuotaForNewUser: 0,
QuotaForInviter: 0,
@@ -137,9 +143,20 @@ const OperationSetting = () => {
<Card style={{ marginTop: '10px' }}>
<SettingsChats options={inputs} refresh={onRefresh} />
</Card>
{/* 倍率设置 */}
{/* 分组倍率设置 */}
<Card style={{ marginTop: '10px' }}>
<SettingsMagnification options={inputs} refresh={onRefresh} />
<GroupRatioSettings options={inputs} refresh={onRefresh} />
</Card>
{/* 合并模型倍率设置和可视化倍率设置 */}
<Card style={{ marginTop: '10px' }}>
<Tabs type="line">
<Tabs.TabPane tab={t('模型倍率设置')} itemKey="model">
<ModelRatioSettings options={inputs} refresh={onRefresh} />
</Tabs.TabPane>
<Tabs.TabPane tab={t('可视化倍率设置')} itemKey="visual">
<ModelSettingsVisualEditor options={inputs} refresh={onRefresh} />
</Tabs.TabPane>
</Tabs>
</Card>
</Spin>
</>

View File

@@ -2,8 +2,10 @@ import React, { useEffect, useRef, useState } from 'react';
import { Banner, Button, Col, Form, Row } from '@douyinfe/semi-ui';
import { API, showError, showSuccess } from '../helpers';
import { marked } from 'marked';
import { useTranslation } from 'react-i18next';
const OtherSetting = () => {
const { t } = useTranslation();
let [inputs, setInputs] = useState({
Notice: '',
SystemName: '',
@@ -54,10 +56,10 @@ const OtherSetting = () => {
try {
setLoadingInput((loadingInput) => ({ ...loadingInput, Notice: true }));
await updateOption('Notice', inputs.Notice);
showSuccess('公告已更新');
showSuccess(t('公告已更新'));
} catch (error) {
console.error('公告更新失败', error);
showError('公告更新失败');
console.error(t('公告更新失败'), error);
showError(t('公告更新失败'));
} finally {
setLoadingInput((loadingInput) => ({ ...loadingInput, Notice: false }));
}
@@ -72,10 +74,10 @@ const OtherSetting = () => {
SystemName: true,
}));
await updateOption('SystemName', inputs.SystemName);
showSuccess('系统名称已更新');
showSuccess(t('系统名称已更新'));
} catch (error) {
console.error('系统名称更新失败', error);
showError('系统名称更新失败');
console.error(t('系统名称更新失败'), error);
showError(t('系统名称更新失败'));
} finally {
setLoadingInput((loadingInput) => ({
...loadingInput,
@@ -193,17 +195,17 @@ const OtherSetting = () => {
getFormApi={(formAPI) => (formAPISettingGeneral.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'通用设置'}>
<Form.Section text={t('通用设置')}>
<Form.TextArea
label={'公告'}
placeholder={'在此输入新的公告内容,支持 Markdown & HTML 代码'}
label={t('公告')}
placeholder={t('在此输入新的公告内容,支持 Markdown & HTML 代码')}
field={'Notice'}
onChange={handleInputChange}
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
autosize={{ minRows: 6, maxRows: 12 }}
/>
<Button onClick={submitNotice} loading={loadingInput['Notice']}>
设置公告
{t('设置公告')}
</Button>
</Form.Section>
</Form>
@@ -213,10 +215,10 @@ const OtherSetting = () => {
getFormApi={(formAPI) => (formAPIPersonalization.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'个性化设置'}>
<Form.Section text={t('个性化设置')}>
<Form.Input
label={'系统名称'}
placeholder={'在此输入系统名称'}
label={t('系统名称')}
placeholder={t('在此输入系统名称')}
field={'SystemName'}
onChange={handleInputChange}
/>
@@ -224,22 +226,20 @@ const OtherSetting = () => {
onClick={submitSystemName}
loading={loadingInput['SystemName']}
>
设置系统名称
{t('设置系统名称')}
</Button>
<Form.Input
label={'Logo 图片地址'}
placeholder={'在此输入 Logo 图片地址'}
label={t('Logo 图片地址')}
placeholder={t('在此输入 Logo 图片地址')}
field={'Logo'}
onChange={handleInputChange}
/>
<Button onClick={submitLogo} loading={loadingInput['Logo']}>
设置 Logo
{t('设置 Logo')}
</Button>
<Form.TextArea
label={'首页内容'}
placeholder={
'在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。'
}
label={t('首页内容')}
placeholder={t('在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页')}
field={'HomePageContent'}
onChange={handleInputChange}
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
@@ -249,39 +249,35 @@ const OtherSetting = () => {
onClick={() => submitOption('HomePageContent')}
loading={loadingInput['HomePageContent']}
>
设置首页内容
{t('设置首页内容')}
</Button>
<Form.TextArea
label={'关于'}
placeholder={
'在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面。'
}
label={t('关于')}
placeholder={t('在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面')}
field={'About'}
onChange={handleInputChange}
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
autosize={{ minRows: 6, maxRows: 12 }}
/>
<Button onClick={submitAbout} loading={loadingInput['About']}>
设置关于
{t('设置关于')}
</Button>
{/* */}
<Banner
fullMode={false}
type='info'
description='移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目'
description={t('移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目')}
closeIcon={null}
style={{ marginTop: 15 }}
/>
<Form.Input
label={'页脚'}
placeholder={
'在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码'
}
label={t('页脚')}
placeholder={t('在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码')}
field={'Footer'}
onChange={handleInputChange}
/>
<Button onClick={submitFooter} loading={loadingInput['Footer']}>
设置页脚
{t('设置页脚')}
</Button>
</Form.Section>
</Form>

View File

@@ -0,0 +1,92 @@
import HeaderBar from './HeaderBar.js';
import { Layout } from '@douyinfe/semi-ui';
import SiderBar from './SiderBar.js';
import App from '../App.js';
import FooterBar from './Footer.js';
import { ToastContainer } from 'react-toastify';
import React, { useContext, useEffect } from 'react';
import { StyleContext } from '../context/Style/index.js';
import { useTranslation } from 'react-i18next';
import { API, getLogo, getSystemName, showError } from '../helpers/index.js';
import { setStatusData } from '../helpers/data.js';
import { UserContext } from '../context/User/index.js';
import { StatusContext } from '../context/Status/index.js';
const { Sider, Content, Header, Footer } = Layout;
const PageLayout = () => {
const [userState, userDispatch] = useContext(UserContext);
const [statusState, statusDispatch] = useContext(StatusContext);
const [styleState, styleDispatch] = useContext(StyleContext);
const { i18n } = useTranslation();
const loadUser = () => {
let user = localStorage.getItem('user');
if (user) {
let data = JSON.parse(user);
userDispatch({ type: 'login', payload: data });
}
};
const loadStatus = async () => {
try {
const res = await API.get('/api/status');
const { success, data } = res.data;
if (success) {
statusDispatch({ type: 'set', payload: data });
setStatusData(data);
} else {
showError('Unable to connect to server');
}
} catch (error) {
showError('Failed to load status');
}
};
useEffect(() => {
loadUser();
loadStatus().catch(console.error);
let systemName = getSystemName();
if (systemName) {
document.title = systemName;
}
let logo = getLogo();
if (logo) {
let linkElement = document.querySelector("link[rel~='icon']");
if (linkElement) {
linkElement.href = logo;
}
}
// 从localStorage获取上次使用的语言
const savedLang = localStorage.getItem('i18nextLng');
if (savedLang) {
i18n.changeLanguage(savedLang);
}
}, [i18n]);
return (
<Layout style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<Header>
<HeaderBar />
</Header>
<Layout style={{ flex: 1, overflow: 'hidden' }}>
<Sider>
{styleState.showSider ? <SiderBar /> : null}
</Sider>
<Layout>
<Content
style={{ overflowY: 'auto', padding: styleState.shouldInnerPadding? '24px': '0' }}
>
<App />
</Content>
<Layout.Footer>
<FooterBar />
</Layout.Footer>
</Layout>
</Layout>
<ToastContainer />
</Layout>
)
}
export default PageLayout;

View File

@@ -33,10 +33,12 @@ import {
stringToColor,
} from '../helpers/render';
import TelegramLoginButton from 'react-telegram-login';
import { useTranslation } from 'react-i18next';
const PersonalSetting = () => {
const [userState, userDispatch] = useContext(UserContext);
let navigate = useNavigate();
const { t } = useTranslation();
const [inputs, setInputs] = useState({
wechat_verification_code: '',
@@ -110,7 +112,7 @@ const PersonalSetting = () => {
if (success) {
setSystemToken(data);
await copy(data);
showSuccess(`令牌已重置并已复制到剪贴板`);
showSuccess(t('令牌已重置并已复制到剪贴板'));
} else {
showError(message);
}
@@ -151,18 +153,18 @@ const PersonalSetting = () => {
const handleAffLinkClick = async (e) => {
e.target.select();
await copy(e.target.value);
showSuccess(`邀请链接已复制到剪切板`);
showSuccess(t('邀请链接已复制到剪切板'));
};
const handleSystemTokenClick = async (e) => {
e.target.select();
await copy(e.target.value);
showSuccess(`系统令牌已复制到剪切板`);
showSuccess(t('系统令牌已复制到剪切板'));
};
const deleteAccount = async () => {
if (inputs.self_account_deletion_confirmation !== userState.user.username) {
showError('请输入你的账户名以确认删除!');
showError(t('请输入你的账户名以确认删除!'));
return;
}
@@ -170,7 +172,7 @@ const PersonalSetting = () => {
const {success, message} = res.data;
if (success) {
showSuccess('账户已删除!');
showSuccess(t('账户已删除!'));
await API.get('/api/user/logout');
userDispatch({type: 'logout'});
localStorage.removeItem('user');
@@ -187,7 +189,7 @@ const PersonalSetting = () => {
);
const {success, message} = res.data;
if (success) {
showSuccess('微信账户绑定成功!');
showSuccess(t('微信账户绑定成功!'));
setShowWeChatBindModal(false);
} else {
showError(message);
@@ -196,7 +198,7 @@ const PersonalSetting = () => {
const changePassword = async () => {
if (inputs.set_new_password !== inputs.set_new_password_confirmation) {
showError('两次输入的密码不一致!');
showError(t('两次输入的密码不一致!'));
return;
}
const res = await API.put(`/api/user/self`, {
@@ -204,7 +206,7 @@ const PersonalSetting = () => {
});
const {success, message} = res.data;
if (success) {
showSuccess('密码修改成功!');
showSuccess(t('密码修改成功!'));
setShowWeChatBindModal(false);
} else {
showError(message);
@@ -214,7 +216,7 @@ const PersonalSetting = () => {
const transfer = async () => {
if (transferAmount < getQuotaPerUnit()) {
showError('划转金额最低为' + renderQuota(getQuotaPerUnit()));
showError(t('划转金额最低为') + ' ' + renderQuota(getQuotaPerUnit()));
return;
}
const res = await API.post(`/api/user/aff_transfer`, {
@@ -232,7 +234,7 @@ const PersonalSetting = () => {
const sendVerificationCode = async () => {
if (inputs.email === '') {
showError('请输入邮箱!');
showError(t('请输入邮箱!'));
return;
}
setDisableButton(true);
@@ -246,7 +248,7 @@ const PersonalSetting = () => {
);
const {success, message} = res.data;
if (success) {
showSuccess('验证码发送成功,请检查邮箱!');
showSuccess(t('验证码发送成功,请检查邮箱!'));
} else {
showError(message);
}
@@ -255,7 +257,7 @@ const PersonalSetting = () => {
const bindEmail = async () => {
if (inputs.email_verification_code === '') {
showError('请输入邮箱验证码!');
showError(t('请输入邮箱验证码!'));
return;
}
setLoading(true);
@@ -264,7 +266,7 @@ const PersonalSetting = () => {
);
const {success, message} = res.data;
if (success) {
showSuccess('邮箱账户绑定成功!');
showSuccess(t('邮箱账户绑定成功!'));
setShowEmailBindModal(false);
userState.user.email = inputs.email;
} else {
@@ -299,7 +301,7 @@ const PersonalSetting = () => {
<Layout>
<Layout.Content>
<Modal
title='请输入要划转的数量'
title={t('请输入要划转的数量')}
visible={openTransfer}
onOk={transfer}
onCancel={handleCancel}
@@ -308,7 +310,7 @@ const PersonalSetting = () => {
centered={true}
>
<div style={{marginTop: 20}}>
<Typography.Text>{`可用额度${renderQuotaWithPrompt(userState?.user?.aff_quota)}`}</Typography.Text>
<Typography.Text>{t('可用额度')}{renderQuotaWithPrompt(userState?.user?.aff_quota)}</Typography.Text>
<Input
style={{marginTop: 5}}
value={userState?.user?.aff_quota}
@@ -317,8 +319,7 @@ const PersonalSetting = () => {
</div>
<div style={{marginTop: 20}}>
<Typography.Text>
{`划转额度${renderQuotaWithPrompt(transferAmount)} 最低` +
renderQuota(getQuotaPerUnit())}
{t('划转额度')}{renderQuotaWithPrompt(transferAmount)} {t('最低') + renderQuota(getQuotaPerUnit())}
</Typography.Text>
<div>
<InputNumber
@@ -348,9 +349,9 @@ const PersonalSetting = () => {
title={<Typography.Text>{getUsername()}</Typography.Text>}
description={
isRoot() ? (
<Tag color='red'>管理员</Tag>
<Tag color='red'>{t('管理员')}</Tag>
) : (
<Tag color='blue'>普通用户</Tag>
<Tag color='blue'>{t('普通用户')}</Tag>
)
}
></Card.Meta>
@@ -363,42 +364,24 @@ const PersonalSetting = () => {
</Space>
</>
}
footer={
<Descriptions row>
<Descriptions.Item itemKey='当前余额'>
{renderQuota(userState?.user?.quota)}
</Descriptions.Item>
<Descriptions.Item itemKey='历史消耗'>
{renderQuota(userState?.user?.used_quota)}
</Descriptions.Item>
<Descriptions.Item itemKey='请求次数'>
{userState.user?.request_count}
</Descriptions.Item>
</Descriptions>
}
>
<Typography.Title heading={6}>可用模型</Typography.Title>
<div style={{marginTop: 10}}>
<Space wrap>
{models.map((model) => (
<Tag
key={model}
color='cyan'
onClick={() => {
copyText(model);
}}
>
{model}
</Tag>
))}
</Space>
</div>
<Descriptions row>
<Descriptions.Item itemKey={t('当前余额')}>
{renderQuota(userState?.user?.quota)}
</Descriptions.Item>
<Descriptions.Item itemKey={t('历史消耗')}>
{renderQuota(userState?.user?.used_quota)}
</Descriptions.Item>
<Descriptions.Item itemKey={t('请求次数')}>
{userState.user?.request_count}
</Descriptions.Item>
</Descriptions>
</Card>
<Card
style={{marginTop: 10}}
footer={
<div>
<Typography.Text>邀请链接</Typography.Text>
<Typography.Text>{t('邀请链接')}</Typography.Text>
<Input
style={{marginTop: 10}}
value={affLink}
@@ -408,35 +391,35 @@ const PersonalSetting = () => {
</div>
}
>
<Typography.Title heading={6}>邀请信息</Typography.Title>
<Typography.Title heading={6}>{t('邀请信息')}</Typography.Title>
<div style={{marginTop: 10}}>
<Descriptions row>
<Descriptions.Item itemKey='待使用收益'>
<span style={{color: 'rgba(var(--semi-red-5), 1)'}}>
{renderQuota(userState?.user?.aff_quota)}
</span>
<Descriptions.Item itemKey={t('待使用收益')}>
<span style={{color: 'rgba(var(--semi-red-5), 1)'}}>
{renderQuota(userState?.user?.aff_quota)}
</span>
<Button
type={'secondary'}
onClick={() => setOpenTransfer(true)}
size={'small'}
style={{marginLeft: 10}}
>
划转
{t('划转')}
</Button>
</Descriptions.Item>
<Descriptions.Item itemKey='总收益'>
<Descriptions.Item itemKey={t('总收益')}>
{renderQuota(userState?.user?.aff_history_quota)}
</Descriptions.Item>
<Descriptions.Item itemKey='邀请人数'>
<Descriptions.Item itemKey={t('邀请人数')}>
{userState?.user?.aff_count}
</Descriptions.Item>
</Descriptions>
</div>
</Card>
<Card style={{marginTop: 10}}>
<Typography.Title heading={6}>个人信息</Typography.Title>
<Typography.Title heading={6}>{t('个人信息')}</Typography.Title>
<div style={{marginTop: 20}}>
<Typography.Text strong>邮箱</Typography.Text>
<Typography.Text strong>{t('邮箱')}</Typography.Text>
<div
style={{display: 'flex', justifyContent: 'space-between'}}
>
@@ -445,7 +428,7 @@ const PersonalSetting = () => {
value={
userState.user && userState.user.email !== ''
? userState.user.email
: '未绑定'
: t('未绑定')
}
readonly={true}
></Input>
@@ -457,14 +440,14 @@ const PersonalSetting = () => {
}}
>
{userState.user && userState.user.email !== ''
? '修改绑定'
: '绑定邮箱'}
? t('修改绑定')
: t('绑定邮箱')}
</Button>
</div>
</div>
</div>
<div style={{marginTop: 10}}>
<Typography.Text strong>微信</Typography.Text>
<Typography.Text strong>{t('微信')}</Typography.Text>
<div
style={{display: 'flex', justifyContent: 'space-between'}}
>
@@ -472,8 +455,8 @@ const PersonalSetting = () => {
<Input
value={
userState.user && userState.user.wechat_id !== ''
? '已绑定'
: '未绑定'
? t('已绑定')
: t('未绑定')
}
readonly={true}
></Input>
@@ -485,13 +468,13 @@ const PersonalSetting = () => {
!status.wechat_login
}
>
{status.wechat_login ? '绑定' : '未启用'}
{status.wechat_login ? t('绑定') : t('未启用')}
</Button>
</div>
</div>
</div>
<div style={{marginTop: 10}}>
<Typography.Text strong>GitHub</Typography.Text>
<Typography.Text strong>{t('GitHub')}</Typography.Text>
<div
style={{display: 'flex', justifyContent: 'space-between'}}
>
@@ -500,7 +483,7 @@ const PersonalSetting = () => {
value={
userState.user && userState.user.github_id !== ''
? userState.user.github_id
: '未绑定'
: t('未绑定')
}
readonly={true}
></Input>
@@ -515,13 +498,13 @@ const PersonalSetting = () => {
!status.github_oauth
}
>
{status.github_oauth ? '绑定' : '未启用'}
{status.github_oauth ? t('绑定') : t('未启用')}
</Button>
</div>
</div>
</div>
<div style={{marginTop: 10}}>
<Typography.Text strong>Telegram</Typography.Text>
<Typography.Text strong>{t('Telegram')}</Typography.Text>
<div
style={{display: 'flex', justifyContent: 'space-between'}}
>
@@ -530,7 +513,7 @@ const PersonalSetting = () => {
value={
userState.user && userState.user.telegram_id !== ''
? userState.user.telegram_id
: '未绑定'
: t('未绑定')
}
readonly={true}
></Input>
@@ -538,7 +521,7 @@ const PersonalSetting = () => {
<div>
{status.telegram_oauth ? (
userState.user.telegram_id !== '' ? (
<Button disabled={true}>已绑定</Button>
<Button disabled={true}>{t('已绑定')}</Button>
) : (
<TelegramLoginButton
dataAuthUrl='/api/oauth/telegram/bind'
@@ -546,13 +529,13 @@ const PersonalSetting = () => {
/>
)
) : (
<Button disabled={true}>未启用</Button>
<Button disabled={true}>{t('未启用')}</Button>
)}
</div>
</div>
</div>
<div style={{marginTop: 10}}>
<Typography.Text strong>LinuxDO</Typography.Text>
<Typography.Text strong>{t('LinuxDO')}</Typography.Text>
<div
style={{display: 'flex', justifyContent: 'space-between'}}
>
@@ -561,7 +544,7 @@ const PersonalSetting = () => {
value={
userState.user && userState.user.linux_do_id !== ''
? userState.user.linux_do_id
: '未绑定'
: t('未绑定')
}
readonly={true}
></Input>
@@ -576,7 +559,7 @@ const PersonalSetting = () => {
!status.linuxdo_oauth
}
>
{status.linuxdo_oauth ? '绑定' : '未启用'}
{status.linuxdo_oauth ? t('绑定') : t('未启用')}
</Button>
</div>
</div>
@@ -584,14 +567,14 @@ const PersonalSetting = () => {
<div style={{marginTop: 10}}>
<Space>
<Button onClick={generateAccessToken}>
生成系统访问令牌
{t('生成系统访问令牌')}
</Button>
<Button
onClick={() => {
setShowChangePasswordModal(true);
}}
>
修改密码
{t('修改密码')}
</Button>
<Button
type={'danger'}
@@ -599,7 +582,7 @@ const PersonalSetting = () => {
setShowAccountDeleteModal(true);
}}
>
删除个人账户
{t('删除个人账户')}
</Button>
</Space>
@@ -617,7 +600,7 @@ const PersonalSetting = () => {
setShowWeChatBindModal(true);
}}
>
绑定微信账号
{t('绑定微信账号')}
</Button>
)}
<Modal
@@ -641,7 +624,7 @@ const PersonalSetting = () => {
}
/>
<Button color='' fluid size='large' onClick={bindWeChat}>
绑定
{t('绑定')}
</Button>
</Modal>
</div>
@@ -655,7 +638,7 @@ const PersonalSetting = () => {
centered={true}
maskClosable={false}
>
<Typography.Title heading={6}>绑定邮箱地址</Typography.Title>
<Typography.Title heading={6}>{t('绑定邮箱地址')}</Typography.Title>
<div
style={{
marginTop: 20,
@@ -747,7 +730,7 @@ const PersonalSetting = () => {
<div style={{marginTop: 20}}>
<Input
name='set_new_password'
placeholder='新密码'
placeholder={t('新密码')}
value={inputs.set_new_password}
onChange={(value) =>
handleInputChange('set_new_password', value)
@@ -756,7 +739,7 @@ const PersonalSetting = () => {
<Input
style={{marginTop: 20}}
name='set_new_password_confirmation'
placeholder='确认新密码'
placeholder={t('确认新密码')}
value={inputs.set_new_password_confirmation}
onChange={(value) =>
handleInputChange('set_new_password_confirmation', value)

View File

@@ -19,55 +19,55 @@ import {
Tag,
} from '@douyinfe/semi-ui';
import EditRedemption from '../pages/Redemption/EditRedemption';
import { useTranslation } from 'react-i18next';
function renderTimestamp(timestamp) {
return <>{timestamp2string(timestamp)}</>;
}
function renderStatus(status) {
switch (status) {
case 1:
return (
<Tag color='green' size='large'>
未使用
</Tag>
);
case 2:
return (
<Tag color='red' size='large'>
{' '}
已禁用{' '}
</Tag>
);
case 3:
return (
<Tag color='grey' size='large'>
{' '}
已使用{' '}
</Tag>
);
default:
return (
<Tag color='black' size='large'>
{' '}
未知状态{' '}
</Tag>
);
}
}
const RedemptionsTable = () => {
const { t } = useTranslation();
const renderStatus = (status) => {
switch (status) {
case 1:
return (
<Tag color='green' size='large'>
{t('未使用')}
</Tag>
);
case 2:
return (
<Tag color='red' size='large'>
{t('已禁用')}
</Tag>
);
case 3:
return (
<Tag color='grey' size='large'>
{t('已使用')}
</Tag>
);
default:
return (
<Tag color='black' size='large'>
{t('未知状态')}
</Tag>
);
}
};
const columns = [
{
title: 'ID',
title: t('ID'),
dataIndex: 'id',
},
{
title: '名称',
title: t('名称'),
dataIndex: 'name',
},
{
title: '状态',
title: t('状态'),
dataIndex: 'status',
key: 'status',
render: (text, record, index) => {
@@ -75,24 +75,24 @@ const RedemptionsTable = () => {
},
},
{
title: '额度',
title: t('额度'),
dataIndex: 'quota',
render: (text, record, index) => {
return <div>{renderQuota(parseInt(text))}</div>;
},
},
{
title: '创建时间',
title: t('创建时间'),
dataIndex: 'created_time',
render: (text, record, index) => {
return <div>{renderTimestamp(text)}</div>;
},
},
{
title: '兑换人ID',
title: t('兑换人ID'),
dataIndex: 'used_user_id',
render: (text, record, index) => {
return <div>{text === 0 ? '无' : text}</div>;
return <div>{text === 0 ? t('无') : text}</div>;
},
},
{
@@ -102,7 +102,7 @@ const RedemptionsTable = () => {
<div>
<Popover content={record.key} style={{ padding: 20 }} position='top'>
<Button theme='light' type='tertiary' style={{ marginRight: 1 }}>
查看
{t('查看')}
</Button>
</Popover>
<Button
@@ -113,11 +113,11 @@ const RedemptionsTable = () => {
await copyText(record.key);
}}
>
复制
{t('复制')}
</Button>
<Popconfirm
title='确定是否要删除此兑换码?'
content='此修改将不可逆'
title={t('确定是否要删除此兑换码?')}
content={t('此修改将不可逆')}
okType={'danger'}
position={'left'}
onConfirm={() => {
@@ -127,7 +127,7 @@ const RedemptionsTable = () => {
}}
>
<Button theme='light' type='danger' style={{ marginRight: 1 }}>
删除
{t('删除')}
</Button>
</Popconfirm>
{record.status === 1 ? (
@@ -139,7 +139,7 @@ const RedemptionsTable = () => {
manageRedemption(record.id, 'disable', record);
}}
>
禁用
{t('禁用')}
</Button>
) : (
<Button
@@ -151,7 +151,7 @@ const RedemptionsTable = () => {
}}
disabled={record.status === 3}
>
启用
{t('启用')}
</Button>
)}
<Button
@@ -164,7 +164,7 @@ const RedemptionsTable = () => {
}}
disabled={record.status !== 1}
>
编辑
{t('编辑')}
</Button>
</div>
),
@@ -239,10 +239,10 @@ const RedemptionsTable = () => {
const copyText = async (text) => {
if (await copy(text)) {
showSuccess('已复制到剪贴板!');
showSuccess(t('已复制到剪贴板!'));
} else {
// setSearchKeyword(text);
Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
}
};
@@ -286,7 +286,7 @@ const RedemptionsTable = () => {
}
const { success, message } = res.data;
if (success) {
showSuccess('操作成功完成!');
showSuccess(t('操作成功完成!'));
let redemption = res.data.data;
let newRedemptions = [...redemptions];
// let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
@@ -381,11 +381,11 @@ const RedemptionsTable = () => {
></EditRedemption>
<Form onSubmit={searchRedemptions}>
<Form.Input
label='搜索关键字'
label={t('搜索关键字')}
field='keyword'
icon='search'
iconPosition='left'
placeholder='关键字(id或者名称)'
placeholder={t('关键字(id或者名称)')}
value={searchKeyword}
loading={searching}
onChange={handleKeywordChange}
@@ -404,14 +404,14 @@ const RedemptionsTable = () => {
setShowEdit(true);
}}
>
添加兑换码
{t('添加兑换码')}
</Button>
<Button
label='复制所选兑换码'
label={t('复制所选兑换码')}
type='warning'
onClick={async () => {
if (selectedKeys.length === 0) {
showError('请至少选择一个兑换码!');
showError(t('请至少选择一个兑换码!'));
return;
}
let keys = '';
@@ -421,7 +421,7 @@ const RedemptionsTable = () => {
await copyText(keys);
}}
>
复制所选兑换码到剪贴板
{t('复制所选兑换码到剪贴板')}
</Button>
</div>
@@ -436,7 +436,11 @@ const RedemptionsTable = () => {
// showSizeChanger: true,
// pageSizeOptions: [10, 20, 50, 100],
formatPageText: (page) =>
`${page.currentStart} - ${page.currentEnd} 条,共 ${redemptions.length}`,
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: redemptions.length
}),
// onPageSizeChange: (size) => {
// setPageSize(size);
// setActivePage(1);

View File

@@ -1,12 +1,21 @@
import React, { useEffect, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { API, getLogo, showError, showInfo, showSuccess } from '../helpers';
import { API, getLogo, showError, showInfo, showSuccess, updateAPI } from '../helpers';
import Turnstile from 'react-turnstile';
import { Button, Card, Form, Layout } from '@douyinfe/semi-ui';
import { Button, Card, Divider, Form, Icon, Layout, Modal } from '@douyinfe/semi-ui';
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
import { IconGithubLogo } from '@douyinfe/semi-icons';
import { onGitHubOAuthClicked, onLinuxDOOAuthClicked } from './utils.js';
import LinuxDoIcon from './LinuxDoIcon.js';
import WeChatIcon from './WeChatIcon.js';
import TelegramLoginButton from 'react-telegram-login/src';
import { setUserData } from '../helpers/data.js';
import { UserContext } from '../context/User/index.js';
import { useTranslation } from 'react-i18next';
const RegisterForm = () => {
const { t } = useTranslation();
const [inputs, setInputs] = useState({
username: '',
password: '',
@@ -16,11 +25,16 @@ const RegisterForm = () => {
});
const { username, password, password2 } = inputs;
const [showEmailVerification, setShowEmailVerification] = useState(false);
const [userState, userDispatch] = useContext(UserContext);
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
const [turnstileToken, setTurnstileToken] = useState('');
const [loading, setLoading] = useState(false);
const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false);
const [status, setStatus] = useState({});
let navigate = useNavigate();
const logo = getLogo();
let affCode = new URLSearchParams(window.location.search).get('aff');
if (affCode) {
localStorage.setItem('aff', affCode);
@@ -30,6 +44,7 @@ const RegisterForm = () => {
let status = localStorage.getItem('status');
if (status) {
status = JSON.parse(status);
setStatus(status);
setShowEmailVerification(status.email_verification);
if (status.turnstile_check) {
setTurnstileEnabled(true);
@@ -38,7 +53,32 @@ const RegisterForm = () => {
}
});
let navigate = useNavigate();
const onWeChatLoginClicked = () => {
setShowWeChatLoginModal(true);
};
const onSubmitWeChatVerificationCode = async () => {
if (turnstileEnabled && turnstileToken === '') {
showInfo('请稍后几秒重试Turnstile 正在检查用户环境!');
return;
}
const res = await API.get(
`/api/oauth/wechat?code=${inputs.wechat_verification_code}`,
);
const { success, message, data } = res.data;
if (success) {
userDispatch({ type: 'login', payload: data });
localStorage.setItem('user', JSON.stringify(data));
setUserData(data);
updateAPI();
navigate('/');
showSuccess('登录成功!');
setShowWeChatLoginModal(false);
} else {
showError(message);
}
};
function handleChange(name, value) {
setInputs((inputs) => ({ ...inputs, [name]: value }));
@@ -97,6 +137,38 @@ const RegisterForm = () => {
setLoading(false);
};
const onTelegramLoginClicked = async (response) => {
const fields = [
'id',
'first_name',
'last_name',
'username',
'photo_url',
'auth_date',
'hash',
'lang',
];
const params = {};
fields.forEach((field) => {
if (response[field]) {
params[field] = response[field];
}
});
const res = await API.get(`/api/oauth/telegram/login`, { params });
const { success, message, data } = res.data;
if (success) {
userDispatch({ type: 'login', payload: data });
localStorage.setItem('user', JSON.stringify(data));
showSuccess('登录成功!');
setUserData(data);
updateAPI();
navigate('/');
} else {
showError(message);
}
};
return (
<div>
<Layout>
@@ -112,28 +184,28 @@ const RegisterForm = () => {
<div style={{ width: 500 }}>
<Card>
<Title heading={2} style={{ textAlign: 'center' }}>
新用户注册
{t('新用户注册')}
</Title>
<Form size="large">
<Form.Input
field={'username'}
label={'用户名'}
placeholder="用户名"
label={t('用户名')}
placeholder={t('用户名')}
name="username"
onChange={(value) => handleChange('username', value)}
/>
<Form.Input
field={'password'}
label={'密码'}
placeholder="密码,最短 8 位,最长 20 位"
label={t('密码')}
placeholder={t('输入密码,最短 8 位,最长 20 位')}
name="password"
type="password"
onChange={(value) => handleChange('password', value)}
/>
<Form.Input
field={'password2'}
label={'确认密码'}
placeholder="确认密码"
label={t('确认密码')}
placeholder={t('确认密码')}
name="password2"
type="password"
onChange={(value) => handleChange('password2', value)}
@@ -142,21 +214,21 @@ const RegisterForm = () => {
<>
<Form.Input
field={'email'}
label={'邮箱'}
placeholder="输入邮箱地址"
label={t('邮箱')}
placeholder={t('输入邮箱地址')}
onChange={(value) => handleChange('email', value)}
name="email"
type="email"
suffix={
<Button onClick={sendVerificationCode} disabled={loading}>
获取验证码
{t('获取验证码')}
</Button>
}
/>
<Form.Input
field={'verification_code'}
label={'验证码'}
placeholder="输入验证码"
label={t('验证码')}
placeholder={t('输入验证码')}
onChange={(value) => handleChange('verification_code', value)}
name="verification_code"
/>
@@ -172,7 +244,7 @@ const RegisterForm = () => {
htmlType={'submit'}
onClick={handleSubmit}
>
注册
{t('注册')}
</Button>
</Form>
<div
@@ -183,20 +255,133 @@ const RegisterForm = () => {
}}
>
<Text>
已有账户
{t('已有账户?')}
<Link to="/login">
点击登录
{t('点击登录')}
</Link>
</Text>
</div>
{status.github_oauth ||
status.wechat_login ||
status.telegram_oauth ||
status.linuxdo_oauth ? (
<>
<Divider margin='12px' align='center'>
{t('第三方登录')}
</Divider>
<div
style={{
display: 'flex',
justifyContent: 'center',
marginTop: 20,
}}
>
{status.github_oauth ? (
<Button
type='primary'
icon={<IconGithubLogo />}
onClick={() =>
onGitHubOAuthClicked(status.github_client_id)
}
/>
) : (
<></>
)}
{status.linuxdo_oauth ? (
<Button
icon={<LinuxDoIcon />}
onClick={() =>
onLinuxDOOAuthClicked(status.linuxdo_client_id)
}
/>
) : (
<></>
)}
{status.wechat_login ? (
<Button
type='primary'
style={{ color: 'rgba(var(--semi-green-5), 1)' }}
icon={<Icon svg={<WeChatIcon />} />}
onClick={onWeChatLoginClicked}
/>
) : (
<></>
)}
</div>
{status.telegram_oauth ? (
<>
<div
style={{
display: 'flex',
justifyContent: 'center',
marginTop: 5,
}}
>
<TelegramLoginButton
dataOnauth={onTelegramLoginClicked}
botName={status.telegram_bot_name}
/>
</div>
</>
) : (
<></>
)}
</>
) : (
<></>
)}
</Card>
{turnstileEnabled ? (
<Turnstile
sitekey={turnstileSiteKey}
onVerify={(token) => {
setTurnstileToken(token);
<Modal
title={t('微信扫码登录')}
visible={showWeChatLoginModal}
maskClosable={true}
onOk={onSubmitWeChatVerificationCode}
onCancel={() => setShowWeChatLoginModal(false)}
okText={t('登录')}
size={'small'}
centered={true}
>
<div
style={{
display: 'flex',
alignItem: 'center',
flexDirection: 'column',
}}
/>
>
<img src={status.wechat_qrcode} />
</div>
<div style={{ textAlign: 'center' }}>
<p>
{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}
</p>
</div>
<Form size='large'>
<Form.Input
field={'wechat_verification_code'}
placeholder={t('验证码')}
label={t('验证码')}
value={inputs.wechat_verification_code}
onChange={(value) =>
handleChange('wechat_verification_code', value)
}
/>
</Form>
</Modal>
{turnstileEnabled ? (
<div
style={{
display: 'flex',
justifyContent: 'center',
marginTop: 20,
}}
>
<Turnstile
sitekey={turnstileSiteKey}
onVerify={(token) => {
setTurnstileToken(token);
}}
/>
</div>
) : (
<></>
)}

View File

@@ -2,6 +2,7 @@ import React, { useContext, useEffect, useMemo, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { UserContext } from '../context/User';
import { StatusContext } from '../context/Status';
import { useTranslation } from 'react-i18next';
import {
API,
@@ -31,14 +32,16 @@ import { Avatar, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
import { setStatusData } from '../helpers/data.js';
import { stringToColor } from '../helpers/render.js';
import { useSetTheme, useTheme } from '../context/Theme/index.js';
import { StyleContext } from '../context/Style/index.js';
// HeaderBar Buttons
const SiderBar = () => {
const [userState, userDispatch] = useContext(UserContext);
const { t } = useTranslation();
const [styleState, styleDispatch] = useContext(StyleContext);
const [statusState, statusDispatch] = useContext(StatusContext);
const defaultIsCollapsed =
isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true';
localStorage.getItem('default_collapse_sidebar') === 'true';
const [selectedKeys, setSelectedKeys] = useState(['home']);
const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed);
@@ -73,62 +76,26 @@ const SiderBar = () => {
icon: <IconCommentStroked />,
},
{
text: '模型价格',
itemKey: 'pricing',
to: '/pricing',
icon: <IconPriceTag />,
},
{
text: '渠道',
text: t('渠道'),
itemKey: 'channel',
to: '/channel',
icon: <IconLayers />,
className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle',
},
{
text: '聊天',
text: t('聊天'),
itemKey: 'chat',
// to: '/chat',
items: chatItems,
icon: <IconComment />,
// className: localStorage.getItem('chat_link')
// ? 'semi-navigation-item-normal'
// : 'tableHiddle',
},
{
text: '令牌',
text: t('令牌'),
itemKey: 'token',
to: '/token',
icon: <IconKey />,
},
{
text: '兑换码',
itemKey: 'redemption',
to: '/redemption',
icon: <IconGift />,
className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle',
},
{
text: '钱包',
itemKey: 'topup',
to: '/topup',
icon: <IconCreditCard />,
},
{
text: '用户管理',
itemKey: 'user',
to: '/user',
icon: <IconUser />,
className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle',
},
{
text: '日志',
itemKey: 'log',
to: '/log',
icon: <IconHistogram />,
},
{
text: '数据看板',
text: t('数据看板'),
itemKey: 'detail',
to: '/detail',
icon: <IconCalendarClock />,
@@ -138,7 +105,33 @@ const SiderBar = () => {
: 'tableHiddle',
},
{
text: '绘图',
text: t('兑换码'),
itemKey: 'redemption',
to: '/redemption',
icon: <IconGift />,
className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle',
},
{
text: t('钱包'),
itemKey: 'topup',
to: '/topup',
icon: <IconCreditCard />,
},
{
text: t('用户管理'),
itemKey: 'user',
to: '/user',
icon: <IconUser />,
className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle',
},
{
text: t('日志'),
itemKey: 'log',
to: '/log',
icon: <IconHistogram />,
},
{
text: t('绘图'),
itemKey: 'midjourney',
to: '/midjourney',
icon: <IconImage />,
@@ -148,7 +141,7 @@ const SiderBar = () => {
: 'tableHiddle',
},
{
text: '异步任务',
text: t('异步任务'),
itemKey: 'task',
to: '/task',
icon: <IconChecklistStroked />,
@@ -158,53 +151,30 @@ const SiderBar = () => {
: 'tableHiddle',
},
{
text: '设置',
text: t('设置'),
itemKey: 'setting',
to: '/setting',
icon: <IconSetting />,
},
// {
// text: '关于',
// itemKey: 'about',
// to: '/about',
// icon: <IconAt/>
// }
],
[
localStorage.getItem('enable_data_export'),
localStorage.getItem('enable_drawing'),
localStorage.getItem('enable_task'),
localStorage.getItem('chat_link'), chatItems,
localStorage.getItem('chat_link'),
chatItems,
isAdmin(),
t,
],
);
const loadStatus = async () => {
const res = await API.get('/api/status');
if (res === undefined) {
return;
}
const { success, data } = res.data;
if (success) {
statusDispatch({ type: 'set', payload: data });
setStatusData(data);
} else {
showError('无法正常连接至服务器!');
}
};
useEffect(() => {
loadStatus().then(() => {
setIsCollapsed(
isMobile() ||
localStorage.getItem('default_collapse_sidebar') === 'true',
);
});
let localKey = window.location.pathname.split('/')[1];
if (localKey === '') {
localKey = 'home';
}
setSelectedKeys([localKey]);
let chatLink = localStorage.getItem('chat_link');
if (!chatLink) {
let chats = localStorage.getItem('chats');
@@ -232,6 +202,8 @@ const SiderBar = () => {
}
}
}
setIsCollapsed(localStorage.getItem('default_collapse_sidebar') === 'true');
}, []);
return (
@@ -239,7 +211,6 @@ const SiderBar = () => {
<Nav
style={{ maxWidth: 220, height: '100%' }}
defaultIsCollapsed={
isMobile() ||
localStorage.getItem('default_collapse_sidebar') === 'true'
}
isCollapsed={isCollapsed}
@@ -280,21 +251,15 @@ const SiderBar = () => {
}}
items={headerButtons}
onSelect={(key) => {
if (key.itemKey.toString().startsWith('chat')) {
styleDispatch({ type: 'SET_INNER_PADDING', payload: false });
} else {
styleDispatch({ type: 'SET_INNER_PADDING', payload: true });
}
setSelectedKeys([key.itemKey]);
}}
footer={
<>
{isMobile() && (
<Switch
checkedText='🌞'
size={'small'}
checked={theme === 'dark'}
uncheckedText='🌙'
onChange={(checked) => {
setTheme(checked);
}}
/>
)}
</>
}
>

View File

@@ -23,67 +23,66 @@ import {
import { IconTreeTriangleDown } from '@douyinfe/semi-icons';
import EditToken from '../pages/Token/EditToken';
import { useTranslation } from 'react-i18next';
function renderTimestamp(timestamp) {
return <>{timestamp2string(timestamp)}</>;
}
function renderStatus(status, model_limits_enabled = false) {
switch (status) {
case 1:
if (model_limits_enabled) {
return (
<Tag color='green' size='large'>
已启用限制模型
</Tag>
);
} else {
return (
<Tag color='green' size='large'>
已启用
</Tag>
);
}
case 2:
return (
<Tag color='red' size='large'>
{' '}
已禁用{' '}
</Tag>
);
case 3:
return (
<Tag color='yellow' size='large'>
{' '}
已过期{' '}
</Tag>
);
case 4:
return (
<Tag color='grey' size='large'>
{' '}
已耗尽{' '}
</Tag>
);
default:
return (
<Tag color='black' size='large'>
{' '}
未知状态{' '}
</Tag>
);
}
}
const TokensTable = () => {
const { t } = useTranslation();
const renderStatus = (status, model_limits_enabled = false) => {
switch (status) {
case 1:
if (model_limits_enabled) {
return (
<Tag color='green' size='large'>
{t('已启用:限制模型')}
</Tag>
);
} else {
return (
<Tag color='green' size='large'>
{t('已启用')}
</Tag>
);
}
case 2:
return (
<Tag color='red' size='large'>
{t('已禁用')}
</Tag>
);
case 3:
return (
<Tag color='yellow' size='large'>
{t('已过期')}
</Tag>
);
case 4:
return (
<Tag color='grey' size='large'>
{t('已耗尽')}
</Tag>
);
default:
return (
<Tag color='black' size='large'>
{t('未知状态')}
</Tag>
);
}
};
const columns = [
{
title: '名称',
title: t('名称'),
dataIndex: 'name',
},
{
title: '状态',
title: t('状态'),
dataIndex: 'status',
key: 'status',
render: (text, record, index) => {
@@ -96,21 +95,21 @@ const TokensTable = () => {
},
},
{
title: '已用额度',
title: t('已用额度'),
dataIndex: 'used_quota',
render: (text, record, index) => {
return <div>{renderQuota(parseInt(text))}</div>;
},
},
{
title: '剩余额度',
title: t('剩余额度'),
dataIndex: 'remain_quota',
render: (text, record, index) => {
return (
<div>
{record.unlimited_quota ? (
<Tag size={'large'} color={'white'}>
无限制
{t('无限制')}
</Tag>
) : (
<Tag size={'large'} color={'light-blue'}>
@@ -122,19 +121,19 @@ const TokensTable = () => {
},
},
{
title: '创建时间',
title: t('创建时间'),
dataIndex: 'created_time',
render: (text, record, index) => {
return <div>{renderTimestamp(text)}</div>;
},
},
{
title: '过期时间',
title: t('过期时间'),
dataIndex: 'expired_time',
render: (text, record, index) => {
return (
<div>
{record.expired_time === -1 ? '永不过期' : renderTimestamp(text)}
{record.expired_time === -1 ? t('永不过期') : renderTimestamp(text)}
</div>
);
},
@@ -199,7 +198,7 @@ const TokensTable = () => {
} catch (e) {
console.log(e);
showError('聊天链接配置错误,请联系管理员');
showError(t('聊天链接配置错误,请联系管理员'));
}
}
return (
@@ -210,7 +209,7 @@ const TokensTable = () => {
position='top'
>
<Button theme='light' type='tertiary' style={{ marginRight: 1 }}>
查看
{t('查看')}
</Button>
</Popover>
<Button
@@ -221,24 +220,24 @@ const TokensTable = () => {
await copyText('sk-' + record.key);
}}
>
复制
{t('复制')}
</Button>
<SplitButtonGroup
style={{ marginRight: 1 }}
aria-label='项目操作按钮组'
aria-label={t('项目操作按钮组')}
>
<Button
theme='light'
style={{ color: 'rgba(var(--semi-teal-7), 1)' }}
onClick={() => {
if (chatsArray.length === 0) {
showError('请联系管理员配置聊天链接');
showError(t('请联系管理员配置聊天链接'));
} else {
onOpenLink('default', chats[0][Object.keys(chats[0])[0]], record);
}
}}
>
聊天
{t('聊天')}
</Button>
<Dropdown
trigger='click'
@@ -256,8 +255,8 @@ const TokensTable = () => {
</Dropdown>
</SplitButtonGroup>
<Popconfirm
title='确定是否要删除此令牌?'
content='此修改将不可逆'
title={t('确定是否要删除此令牌?')}
content={t('此修改将不可逆')}
okType={'danger'}
position={'left'}
onConfirm={() => {
@@ -267,7 +266,7 @@ const TokensTable = () => {
}}
>
<Button theme='light' type='danger' style={{ marginRight: 1 }}>
删除
{t('删除')}
</Button>
</Popconfirm>
{record.status === 1 ? (
@@ -279,7 +278,7 @@ const TokensTable = () => {
manageToken(record.id, 'disable', record);
}}
>
禁用
{t('禁用')}
</Button>
) : (
<Button
@@ -290,7 +289,7 @@ const TokensTable = () => {
manageToken(record.id, 'enable', record);
}}
>
启用
{t('启用')}
</Button>
)}
<Button
@@ -302,7 +301,7 @@ const TokensTable = () => {
setShowEdit(true);
}}
>
编辑
{t('编辑')}
</Button>
</div>
);
@@ -371,10 +370,10 @@ const TokensTable = () => {
const copyText = async (text) => {
if (await copy(text)) {
showSuccess('已复制到剪贴板!');
showSuccess(t('已复制到剪贴板!'));
} else {
Modal.error({
title: '无法复制到剪贴板,请手动复制',
title: t('无法复制到剪贴板,请手动复制'),
content: text,
size: 'large',
});
@@ -395,37 +394,6 @@ const TokensTable = () => {
let encodedServerAddress = encodeURIComponent(serverAddress);
url = url.replaceAll('{address}', encodedServerAddress);
url = url.replaceAll('{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');
};
@@ -571,29 +539,29 @@ const TokensTable = () => {
>
<Form.Input
field='keyword'
label='搜索关键字'
placeholder='令牌名称'
label={t('搜索关键字')}
placeholder={t('令牌名称')}
value={searchKeyword}
loading={searching}
onChange={handleKeywordChange}
/>
<Form.Input
field='token'
label='Key'
placeholder='密钥'
label={t('密钥')}
placeholder={t('密钥')}
value={searchToken}
loading={searching}
onChange={handleSearchTokenChange}
/>
<Button
label='查询'
label={t('查询')}
type='primary'
htmlType='submit'
className='btn-margin-right'
onClick={searchTokens}
style={{ marginRight: 8 }}
>
查询
{t('查询')}
</Button>
</Form>
<Divider style={{margin:'15px 0'}}/>
@@ -609,14 +577,14 @@ const TokensTable = () => {
setShowEdit(true);
}}
>
添加令牌
{t('添加令牌')}
</Button>
<Button
label='复制所选令牌'
label={t('复制所选令牌')}
type='warning'
onClick={async () => {
if (selectedKeys.length === 0) {
showError('请至少选择一个令牌!');
showError(t('请至少选择一个令牌!'));
return;
}
let keys = '';
@@ -627,7 +595,7 @@ const TokensTable = () => {
await copyText(keys);
}}
>
复制所选令牌到剪贴板
{t('复制所选令牌到剪贴板')}
</Button>
</div>
@@ -642,7 +610,11 @@ const TokensTable = () => {
showSizeChanger: true,
pageSizeOptions: [10, 20, 50, 100],
formatPageText: (page) =>
`${page.currentStart} - ${page.currentEnd} 条,共 ${tokens.length}`,
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: tokens.length
}),
onPageSizeChange: (size) => {
setPageSize(size);
setActivePage(1);

View File

@@ -13,67 +13,69 @@ import { ITEMS_PER_PAGE } from '../constants';
import { renderGroup, renderNumber, renderQuota } from '../helpers/render';
import AddUser from '../pages/User/AddUser';
import EditUser from '../pages/User/EditUser';
function renderRole(role) {
switch (role) {
case 1:
return <Tag size='large'>普通用户</Tag>;
case 10:
return (
<Tag color='yellow' size='large'>
管理员
</Tag>
);
case 100:
return (
<Tag color='orange' size='large'>
超级管理员
</Tag>
);
default:
return (
<Tag color='red' size='large'>
未知身份
</Tag>
);
}
}
import { useTranslation } from 'react-i18next';
const UsersTable = () => {
const { t } = useTranslation();
function renderRole(role) {
switch (role) {
case 1:
return <Tag size='large'>{t('普通用户')}</Tag>;
case 10:
return (
<Tag color='yellow' size='large'>
{t('管理员')}
</Tag>
);
case 100:
return (
<Tag color='orange' size='large'>
{t('超级管理员')}
</Tag>
);
default:
return (
<Tag color='red' size='large'>
{t('未知身份')}
</Tag>
);
}
}
const columns = [
{
title: 'ID',
dataIndex: 'id',
},
{
title: '用户名',
title: t('用户名'),
dataIndex: 'username',
},
{
title: '分组',
title: t('分组'),
dataIndex: 'group',
render: (text, record, index) => {
return <div>{renderGroup(text)}</div>;
},
},
{
title: '统计信息',
title: t('统计信息'),
dataIndex: 'info',
render: (text, record, index) => {
return (
<div>
<Space spacing={1}>
<Tooltip content={'剩余额度'}>
<Tooltip content={t('剩余额度')}>
<Tag color='white' size='large'>
{renderQuota(record.quota)}
</Tag>
</Tooltip>
<Tooltip content={'已用额度'}>
<Tooltip content={t('已用额度')}>
<Tag color='white' size='large'>
{renderQuota(record.used_quota)}
</Tag>
</Tooltip>
<Tooltip content={'调用次数'}>
<Tooltip content={t('调用次数')}>
<Tag color='white' size='large'>
{renderNumber(record.request_count)}
</Tag>
@@ -84,26 +86,26 @@ const UsersTable = () => {
},
},
{
title: '邀请信息',
title: t('邀请信息'),
dataIndex: 'invite',
render: (text, record, index) => {
return (
<div>
<Space spacing={1}>
<Tooltip content={'邀请人数'}>
<Tooltip content={t('邀请人数')}>
<Tag color='white' size='large'>
{renderNumber(record.aff_count)}
</Tag>
</Tooltip>
<Tooltip content={'邀请总收益'}>
<Tooltip content={t('邀请总收益')}>
<Tag color='white' size='large'>
{renderQuota(record.aff_history_quota)}
</Tag>
</Tooltip>
<Tooltip content={'邀请人ID'}>
<Tooltip content={t('邀请人ID')}>
{record.inviter_id === 0 ? (
<Tag color='white' size='large'>
{t('无')}
</Tag>
) : (
<Tag color='white' size='large'>
@@ -117,20 +119,20 @@ const UsersTable = () => {
},
},
{
title: '角色',
title: t('角色'),
dataIndex: 'role',
render: (text, record, index) => {
return <div>{renderRole(text)}</div>;
},
},
{
title: '状态',
title: t('状态'),
dataIndex: 'status',
render: (text, record, index) => {
return (
<div>
{record.DeletedAt !== null ? (
<Tag color='red'>已注销</Tag>
<Tag color='red'>{t('已注销')}</Tag>
) : (
renderStatus(text)
)}
@@ -148,29 +150,25 @@ const UsersTable = () => {
) : (
<>
<Popconfirm
title='确定?'
title={t('确定?')}
okType={'warning'}
onConfirm={() => {
manageUser(record.id, 'promote', record);
}}
>
<Button theme='light' type='warning' style={{ marginRight: 1 }}>
提升
{t('提升')}
</Button>
</Popconfirm>
<Popconfirm
title='确定?'
title={t('确定?')}
okType={'warning'}
onConfirm={() => {
manageUser(record.id, 'demote', record);
}}
>
<Button
theme='light'
type='secondary'
style={{ marginRight: 1 }}
>
降级
<Button theme='light' type='secondary' style={{ marginRight: 1 }}>
{t('降级')}
</Button>
</Popconfirm>
{record.status === 1 ? (
@@ -182,7 +180,7 @@ const UsersTable = () => {
manageUser(record.id, 'disable', record);
}}
>
禁用
{t('禁用')}
</Button>
) : (
<Button
@@ -194,7 +192,7 @@ const UsersTable = () => {
}}
disabled={record.status === 3}
>
启用
{t('启用')}
</Button>
)}
<Button
@@ -206,11 +204,11 @@ const UsersTable = () => {
setShowEditUser(true);
}}
>
编辑
{t('编辑')}
</Button>
<Popconfirm
title='确定是否要注销此用户?'
content='相当于删除用户,此修改将不可逆'
title={t('确定是否要注销此用户?')}
content={t('相当于删除用户,此修改将不可逆')}
okType={'danger'}
position={'left'}
onConfirm={() => {
@@ -220,7 +218,7 @@ const UsersTable = () => {
}}
>
<Button theme='light' type='danger' style={{ marginRight: 1 }}>
注销
{t('注销')}
</Button>
</Popconfirm>
</>
@@ -327,17 +325,17 @@ const UsersTable = () => {
const renderStatus = (status) => {
switch (status) {
case 1:
return <Tag size='large'>已激活</Tag>;
return <Tag size='large'>{t('已激活')}</Tag>;
case 2:
return (
<Tag size='large' color='red'>
已封禁
{t('已封禁')}
</Tag>
);
default:
return (
<Tag size='large' color='grey'>
未知状态
{t('未知状态')}
</Tag>
);
}
@@ -452,41 +450,41 @@ const UsersTable = () => {
>
<div style={{ display: 'flex' }}>
<Space>
<Form.Input
label='搜索关键字'
icon='search'
field='keyword'
iconPosition='left'
placeholder='搜索用户的 ID用户名显示名称以及邮箱地址 ...'
value={searchKeyword}
loading={searching}
onChange={(value) => handleKeywordChange(value)}
/>
<Form.Select
field='group'
label='分组'
optionList={groupOptions}
onChange={(value) => {
setSearchGroup(value);
searchUsers(searchKeyword, value);
}}
/>
<Button
label='查询'
type='primary'
htmlType='submit'
className='btn-margin-right'
>
查询
</Button>
<Form.Input
label={t('搜索关键字')}
icon='search'
field='keyword'
iconPosition='left'
placeholder={t('搜索用户的 ID用户名显示名称以及邮箱地址 ...')}
value={searchKeyword}
loading={searching}
onChange={(value) => handleKeywordChange(value)}
/>
<Form.Select
field='group'
label={t('分组')}
optionList={groupOptions}
onChange={(value) => {
setSearchGroup(value);
searchUsers(searchKeyword, value);
}}
/>
<Button
theme='light'
type='primary'
onClick={() => {
setShowAddUser(true);
}}
label={t('查询')}
type='primary'
htmlType='submit'
className='btn-margin-right'
>
添加用户
{t('查询')}
</Button>
<Button
theme='light'
type='primary'
onClick={() => {
setShowAddUser(true);
}}
>
{t('添加用户')}
</Button>
</Space>
</div>
@@ -496,6 +494,12 @@ const UsersTable = () => {
columns={columns}
dataSource={pageData}
pagination={{
formatPageText: (page) =>
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: users.length
}),
currentPage: activePage,
pageSize: ITEMS_PER_PAGE,
total: userCount,

View File

@@ -0,0 +1,21 @@
import { Input, Typography } from '@douyinfe/semi-ui';
import React from 'react';
const TextInput = ({ label, name, value, onChange, placeholder, type = 'text' }) => {
return (
<>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>{label}</Typography.Text>
</div>
<Input
name={name}
placeholder={placeholder}
onChange={(value) => onChange(value)}
value={value}
autoComplete="new-password"
/>
</>
);
}
export default TextInput;

View File

@@ -0,0 +1,21 @@
import { Input, InputNumber, Typography } from '@douyinfe/semi-ui';
import React from 'react';
const TextNumberInput = ({ label, name, value, onChange, placeholder }) => {
return (
<>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>{label}</Typography.Text>
</div>
<InputNumber
name={name}
placeholder={placeholder}
onChange={(value) => onChange(value)}
value={value}
autoComplete="new-password"
/>
</>
);
}
export default TextNumberInput;

View File

@@ -1,7 +1,12 @@
import { API, showError } from '../helpers';
export async function getOAuthState() {
const res = await API.get('/api/oauth/state');
let path = '/api/oauth/state';
let affCode = localStorage.getItem('aff');
if (affCode && affCode.length > 0) {
path += `?aff=${affCode}`;
}
const res = await API.get(path);
const { success, message, data } = res.data;
if (success) {
return data;

View File

@@ -44,13 +44,6 @@ export const CHANNEL_OPTIONS = [
color: 'teal',
label: 'Azure OpenAI'
},
{
key: 24,
text: 'Google Gemini',
value: 24,
color: 'orange',
label: 'Google Gemini'
},
{
key: 34,
text: 'Cohere',
@@ -58,6 +51,8 @@ export const CHANNEL_OPTIONS = [
color: 'purple',
label: 'Cohere'
},
{ key: 39, text: 'Cloudflare', value: 39, color: 'grey', label: 'Cloudflare' },
{ key: 43, text: 'DeepSeek', value: 43, color: 'blue', label: 'DeepSeek' },
{
key: 15,
text: '百度文心千帆',
@@ -93,6 +88,13 @@ export const CHANNEL_OPTIONS = [
color: 'purple',
label: '智谱 GLM-4V'
},
{
key: 24,
text: 'Google Gemini',
value: 24,
color: 'orange',
label: 'Google Gemini'
},
{
key: 11,
text: 'Google PaLM2',
@@ -100,7 +102,6 @@ export const CHANNEL_OPTIONS = [
color: 'orange',
label: 'Google PaLM2'
},
{ key: 39, text: 'Cloudflare', value: 39, color: 'grey', label: 'Cloudflare' },
{ key: 25, text: 'Moonshot', value: 25, color: 'green', label: 'Moonshot' },
{ key: 19, text: '360 智脑', value: 19, color: 'blue', label: '360 智脑' },
{ key: 23, text: '腾讯混元', value: 23, color: 'teal', label: '腾讯混元' },

View File

@@ -0,0 +1,80 @@
// contexts/User/index.jsx
import React, { useState, useEffect } from 'react';
import { isMobile } from '../../helpers/index.js';
export const StyleContext = React.createContext({
dispatch: () => null,
});
export const StyleProvider = ({ children }) => {
const [state, setState] = useState({
isMobile: false,
showSider: false,
shouldInnerPadding: false,
});
const dispatch = (action) => {
if ('type' in action) {
switch (action.type) {
case 'TOGGLE_SIDER':
setState(prev => ({ ...prev, showSider: !prev.showSider }));
break;
case 'SET_SIDER':
setState(prev => ({ ...prev, showSider: action.payload }));
break;
case 'SET_MOBILE':
setState(prev => ({ ...prev, isMobile: action.payload }));
break;
case 'SET_INNER_PADDING':
setState(prev => ({ ...prev, shouldInnerPadding: action.payload }));
break;
default:
setState(prev => ({ ...prev, ...action }));
}
} else {
setState(prev => ({ ...prev, ...action }));
}
};
useEffect(() => {
const updateIsMobile = () => {
dispatch({ type: 'SET_MOBILE', payload: isMobile() });
};
updateIsMobile();
const updateShowSider = () => {
// check pathname
const pathname = window.location.pathname;
if (pathname === '' || pathname === '/' || pathname.includes('/home') || pathname.includes('/chat')) {
dispatch({ type: 'SET_SIDER', payload: false });
dispatch({ type: 'SET_INNER_PADDING', payload: false });
} else {
dispatch({ type: 'SET_SIDER', payload: true });
dispatch({ type: 'SET_INNER_PADDING', payload: true });
}
if (isMobile()) {
dispatch({ type: 'SET_SIDER', payload: false });
}
};
updateShowSider()
// Optionally, add event listeners to handle window resize
window.addEventListener('resize', updateIsMobile);
// Cleanup event listener on component unmount
return () => {
window.removeEventListener('resize', updateIsMobile);
};
}, []);
return (
<StyleContext.Provider value={[state, dispatch]}>
{children}
</StyleContext.Provider>
);
};

View File

@@ -6,7 +6,8 @@ export let API = axios.create({
? import.meta.env.VITE_REACT_APP_SERVER_URL
: '',
headers: {
'New-API-User': getUserIdFromLocalStorage()
'New-API-User': getUserIdFromLocalStorage(),
'Cache-Control': 'no-store'
}
});
@@ -16,7 +17,8 @@ export function updateAPI() {
? import.meta.env.VITE_REACT_APP_SERVER_URL
: '',
headers: {
'New-API-User': getUserIdFromLocalStorage()
'New-API-User': getUserIdFromLocalStorage(),
'Cache-Control': 'no-store'
}
});
}

View File

@@ -1,3 +1,4 @@
import i18next from 'i18next';
import { Tag } from '@douyinfe/semi-ui';
export function renderText(text, limit) {
@@ -16,7 +17,7 @@ export function renderGroup(group) {
if (group === '') {
return (
<Tag size='large' key='default' color='orange'>
用户分组
{i18next.t('用户分组')}
</Tag>
);
}
@@ -67,6 +68,8 @@ export function renderQuotaNumberWithDigit(num, digits = 2) {
}
export function renderNumberWithPoint(num) {
if (num === undefined)
return '';
num = num.toFixed(2);
if (num >= 100000) {
// Convert number to string to manipulate it
@@ -142,14 +145,16 @@ export function renderModelPrice(
completionRatio,
groupRatio,
) {
// 1 ratio = $0.002 / 1K tokens
if (modelPrice !== -1) {
return '模型价格:$' + modelPrice + ' * 分组倍率:' + groupRatio + ' = $' + modelPrice * groupRatio;
return i18next.t('模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}', {
price: modelPrice,
ratio: groupRatio,
total: modelPrice * groupRatio
});
} else {
if (completionRatio === undefined) {
completionRatio = 0;
}
// 这里的 *2 是因为 1倍率=0.002刀,请勿删除
let inputRatioPrice = modelRatio * 2.0;
let completionRatioPrice = modelRatio * 2.0 * completionRatio;
let price =
@@ -158,21 +163,52 @@ export function renderModelPrice(
return (
<>
<article>
<p>提示${inputRatioPrice} * {groupRatio} = ${inputRatioPrice * groupRatio} / 1M tokens</p>
<p>补全${completionRatioPrice} * {groupRatio} = ${completionRatioPrice * groupRatio} / 1M tokens</p>
<p>{i18next.t('提示:${{price}} * {{ratio}} = ${{total}} / 1M tokens', {
price: inputRatioPrice,
ratio: groupRatio,
total: inputRatioPrice * groupRatio
})}</p>
<p>{i18next.t('补全:${{price}} * {{ratio}} = ${{total}} / 1M tokens', {
price: completionRatioPrice,
ratio: groupRatio,
total: completionRatioPrice * groupRatio
})}</p>
<p></p>
<p>
提示 {inputTokens} tokens / 1M tokens * ${inputRatioPrice} + 补全{' '}
{completionTokens} tokens / 1M tokens * ${completionRatioPrice} * 分组 {groupRatio} =
${price.toFixed(6)}
{i18next.t('提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', {
input: inputTokens,
price: inputRatioPrice,
completion: completionTokens,
compPrice: completionRatioPrice,
ratio: groupRatio,
total: price.toFixed(6)
})}
</p>
<p>仅供参考以实际扣费为准</p>
<p>{i18next.t('仅供参考,以实际扣费为准')}</p>
</article>
</>
);
}
}
export function renderModelPriceSimple(
modelRatio,
modelPrice = -1,
groupRatio,
) {
if (modelPrice !== -1) {
return i18next.t('价格:${{price}} * 分组:{{ratio}}', {
price: modelPrice,
ratio: groupRatio
});
} else {
return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}}', {
ratio: modelRatio,
groupRatio: groupRatio
});
}
}
export function renderAudioModelPrice(
inputTokens,
completionTokens,
@@ -209,8 +245,12 @@ export function renderAudioModelPrice(
<p>音频补全${inputRatioPrice} * {groupRatio} * {audioRatio} * {audioCompletionRatio} = ${inputRatioPrice * audioRatio * audioCompletionRatio * groupRatio} / 1M tokens</p>
<p></p>
<p>
提示 {inputTokens} tokens / 1M tokens * ${inputRatioPrice} + 补全{' '}
{completionTokens} tokens / 1M tokens * ${completionRatioPrice} +
{i18next.t('提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} +', {
input: inputTokens,
price: inputRatioPrice,
completion: completionTokens,
compPrice: completionRatioPrice
})}
</p>
<p>
音频提示 {audioInputTokens} tokens / 1M tokens * ${inputRatioPrice} * {audioRatio} + 音频补全 {audioCompletionTokens} tokens / 1M tokens * ${inputRatioPrice} * {audioRatio} * {audioCompletionRatio}
@@ -230,7 +270,7 @@ export function renderQuotaWithPrompt(quota, digits) {
let displayInCurrency = localStorage.getItem('display_in_currency');
displayInCurrency = displayInCurrency === 'true';
if (displayInCurrency) {
return `(等价金额:${renderQuota(quota, digits)}`;
return '|' + i18next.t('等价金额') + ': ' + renderQuota(quota, digits) + '';
}
return '';
}
@@ -253,6 +293,44 @@ const colors = [
'yellow',
];
// 基础10色色板 (N ≤ 10)
const baseColors = [
'#1664FF', // 主色
'#1AC6FF',
'#FF8A00',
'#3CC780',
'#7442D4',
'#FFC400',
'#304D77',
'#B48DEB',
'#009488',
'#FF7DDA'
];
// 扩展20色色板 (10 < N ≤ 20)
const extendedColors = [
'#1664FF',
'#B2CFFF',
'#1AC6FF',
'#94EFFF',
'#FF8A00',
'#FFCE7A',
'#3CC780',
'#B9EDCD',
'#7442D4',
'#DDC5FA',
'#FFC400',
'#FAE878',
'#304D77',
'#8B959E',
'#B48DEB',
'#EFE3FF',
'#009488',
'#59BAA8',
'#FF7DDA',
'#FFCFEE'
];
export const modelColorMap = {
'dall-e': 'rgb(147,112,219)', // 深紫色
// 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
@@ -262,7 +340,7 @@ export const modelColorMap = {
'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿
'gpt-3.5-turbo-1106': 'rgb(32,178,170)', // 浅海洋绿
'gpt-3.5-turbo-16k': 'rgb(149,252,206)', // 淡橙色
'gpt-3.5-turbo-16k-0613': 'rgb(119,255,214)', // 淡桃
'gpt-3.5-turbo-16k-0613': 'rgb(119,255,214)', // 淡桃<EFBFBD><EFBFBD><EFBFBD>
'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', // 粉蓝色
'gpt-4': 'rgb(135,206,235)', // 天蓝色
// 'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色
@@ -285,7 +363,7 @@ export const modelColorMap = {
'text-embedding-ada-002': 'rgb(255,182,193)', // 浅粉红
'text-embedding-v1': 'rgb(255,174,185)', // 浅粉红色(略有区别)
'text-moderation-latest': 'rgb(255,130,171)', // 强粉色
'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(Babbage相同表示同一类功能
'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(<EFBFBD><EFBFBD><EFBFBD>Babbage相同表示同一类功能
'tts-1': 'rgb(255,140,0)', // 深橙色
'tts-1-1106': 'rgb(255,165,0)', // 橙色
'tts-1-hd': 'rgb(255,215,0)', // 金色
@@ -297,14 +375,33 @@ export const modelColorMap = {
'claude-2.1': 'rgb(255,209,190)', // 浅橙色(略有区别)
};
export function modelToColor(modelName) {
// 1. 如果模型在预定义的 modelColorMap 中,使用预定义颜色
if (modelColorMap[modelName]) {
return modelColorMap[modelName];
}
// 2. 生成一个稳定的数字作为索引
let hash = 0;
for (let i = 0; i < modelName.length; i++) {
hash = ((hash << 5) - hash) + modelName.charCodeAt(i);
hash = hash & hash; // Convert to 32-bit integer
}
hash = Math.abs(hash);
// 3. 根据模型名称长度选择不同的色板
const colorPalette = modelName.length > 10 ? extendedColors : baseColors;
// 4. 使用hash值选择颜色
const index = hash % colorPalette.length;
return colorPalette[index];
}
export function stringToColor(str) {
let sum = 0;
// 对字符串中的每个字符进行操作
for (let i = 0; i < str.length; i++) {
// 将字符的ASCII值加到sum中
sum += str.charCodeAt(i);
}
// 使用模运算得到个位数
let i = sum % colors.length;
return colors[i];
}

26
web/src/i18n/i18n.js Normal file
View File

@@ -0,0 +1,26 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import enTranslation from './locales/en.json';
import zhTranslation from './locales/zh.json';
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
en: {
translation: enTranslation
},
zh: {
translation: zhTranslation
}
},
fallbackLng: 'zh',
interpolation: {
escapeValue: false
}
});
export default i18n;

File diff suppressed because it is too large Load Diff

1235
web/src/i18n/locales/en.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
{
"首页": "首页",
"控制台": "控制台",
"定价": "定价",
"关于": "关于",
"登录": "登录",
"注册": "注册",
"退出": "退出",
"语言": "语言",
"展开侧边栏": "展开侧边栏",
"关闭侧边栏": "关闭侧边栏",
"注销成功!": "注销成功!"
}

View File

@@ -17,7 +17,36 @@ body {
flex-direction: column;
}
#root > section > header > section > div > div > div > div.semi-navigation-header-list-outer > div.semi-navigation-list-wrapper > ul > div > a > li > span{
font-weight: 600 !important;
}
.semi-descriptions-double-small .semi-descriptions-item {
padding-right: 30px;
}
@media only screen and (max-width: 767px) {
/*.semi-navigation-sub-wrap .semi-navigation-sub-title, .semi-navigation-item {*/
/* padding: 0 0;*/
/*}*/
.topnav .semi-navigation-list-wrapper {
max-width: calc(55vw - 20px);
overflow-x: auto;
scrollbar-width: none;
}
#root > section > header > section > div > div > div > div.semi-navigation-footer > div > a > li {
padding: 0 0;
}
#root > section > header > section > div > div > div > div.semi-navigation-header-list-outer > div.semi-navigation-list-wrapper > ul > div > a > li {
padding: 0 5px;
}
#root > section > header > section > div > div > div > div.semi-navigation-footer > div:nth-child(1) > a > li {
padding: 0 5px;
}
.semi-navigation-footer {
padding-left: 0;
padding-right: 0;
}
.semi-table-tbody,
.semi-table-row,
.semi-table-row-cell {
@@ -39,6 +68,10 @@ body {
row-gap: 3px;
column-gap: 10px;
}
.semi-navigation-horizontal .semi-navigation-header {
margin-right: 0;
}
}
.semi-table-tbody > .semi-table-row > .semi-table-row-cell {
@@ -83,11 +116,6 @@ code {
monospace;
}
.semi-navigation-vertical {
/*display: flex;*/
/*flex-direction: column;*/
}
.semi-navigation-item {
margin-bottom: 0;
}

View File

@@ -13,6 +13,9 @@ import { Layout } from '@douyinfe/semi-ui';
import SiderBar from './components/SiderBar';
import { ThemeProvider } from './context/Theme';
import FooterBar from './components/Footer';
import { StyleProvider } from './context/Style/index.js';
import PageLayout from './components/PageLayout.js';
import './i18n/i18n.js';
// initialization
@@ -24,27 +27,9 @@ root.render(
<UserProvider>
<BrowserRouter>
<ThemeProvider>
<Layout style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<Header>
<HeaderBar />
</Header>
<Layout style={{ flex: 1, overflow: 'hidden' }}>
<Sider>
<SiderBar />
</Sider>
<Layout>
<Content
style={{ overflowY: 'auto', padding: '24px' }}
>
<App />
</Content>
<Layout.Footer>
<FooterBar></FooterBar>
</Layout.Footer>
</Layout>
</Layout>
<ToastContainer />
</Layout>
<StyleProvider>
<PageLayout/>
</StyleProvider>
</ThemeProvider>
</BrowserRouter>
</UserProvider>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,366 @@
import React, { useState, useEffect } from 'react';
import { API, showError, showInfo, showSuccess, showWarning, verifyJSON } from '../../helpers';
import { SideSheet, Space, Button, Input, Typography, Spin, Modal, Select, Banner, TextArea } from '@douyinfe/semi-ui';
import TextInput from '../../components/custom/TextInput.js';
import { getChannelModels } from '../../components/utils.js';
const MODEL_MAPPING_EXAMPLE = {
'gpt-3.5-turbo': 'gpt-3.5-turbo-0125'
};
const EditTagModal = (props) => {
const { visible, tag, handleClose, refresh } = props;
const [loading, setLoading] = useState(false);
const [originModelOptions, setOriginModelOptions] = useState([]);
const [modelOptions, setModelOptions] = useState([]);
const [groupOptions, setGroupOptions] = useState([]);
const [basicModels, setBasicModels] = useState([]);
const [fullModels, setFullModels] = useState([]);
const [customModel, setCustomModel] = useState('');
const originInputs = {
tag: '',
new_tag: null,
model_mapping: null,
groups: [],
models: [],
}
const [inputs, setInputs] = useState(originInputs);
const handleInputChange = (name, value) => {
setInputs((inputs) => ({ ...inputs, [name]: value }));
if (name === 'type') {
let localModels = [];
switch (value) {
case 2:
localModels = [
'mj_imagine',
'mj_variation',
'mj_reroll',
'mj_blend',
'mj_upscale',
'mj_describe',
'mj_uploads'
];
break;
case 5:
localModels = [
'swap_face',
'mj_imagine',
'mj_variation',
'mj_reroll',
'mj_blend',
'mj_upscale',
'mj_describe',
'mj_zoom',
'mj_shorten',
'mj_modal',
'mj_inpaint',
'mj_custom_zoom',
'mj_high_variation',
'mj_low_variation',
'mj_pan',
'mj_uploads'
];
break;
case 36:
localModels = [
'suno_music',
'suno_lyrics'
];
break;
default:
localModels = getChannelModels(value);
break;
}
if (inputs.models.length === 0) {
setInputs((inputs) => ({ ...inputs, models: localModels }));
}
setBasicModels(localModels);
}
};
const fetchModels = async () => {
try {
let res = await API.get(`/api/channel/models`);
let localModelOptions = res.data.data.map((model) => ({
label: model.id,
value: model.id
}));
setOriginModelOptions(localModelOptions);
setFullModels(res.data.data.map((model) => model.id));
setBasicModels(
res.data.data
.filter((model) => {
return model.id.startsWith('gpt-') || model.id.startsWith('text-');
})
.map((model) => model.id)
);
} catch (error) {
showError(error.message);
}
};
const fetchGroups = async () => {
try {
let res = await API.get(`/api/group/`);
if (res === undefined) {
return;
}
setGroupOptions(
res.data.data.map((group) => ({
label: group,
value: group
}))
);
} catch (error) {
showError(error.message);
}
};
const handleSave = async () => {
setLoading(true);
let data = {
tag: tag,
}
if (inputs.model_mapping !== null && inputs.model_mapping !== '') {
if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
showInfo('模型映射必须是合法的 JSON 格式!');
setLoading(false);
return;
}
data.model_mapping = inputs.model_mapping
}
if (inputs.groups.length > 0) {
data.groups = inputs.groups.join(',');
}
if (inputs.models.length > 0) {
data.models = inputs.models.join(',');
}
data.new_tag = inputs.new_tag;
// check have any change
if (data.model_mapping === undefined && data.groups === undefined && data.models === undefined && data.new_tag === undefined) {
showWarning('没有任何修改!');
setLoading(false);
return;
}
await submit(data);
setLoading(false);
};
const submit = async (data) => {
try {
const res = await API.put('/api/channel/tag', data);
if (res?.data?.success) {
showSuccess('标签更新成功!');
refresh();
handleClose();
}
} catch (error) {
showError(error);
}
}
useEffect(() => {
let localModelOptions = [...originModelOptions];
inputs.models.forEach((model) => {
if (!localModelOptions.find((option) => option.label === model)) {
localModelOptions.push({
label: model,
value: model
});
}
});
setModelOptions(localModelOptions);
}, [originModelOptions, inputs.models]);
useEffect(() => {
setInputs({
...originInputs,
tag: tag,
new_tag: tag,
})
fetchModels().then();
fetchGroups().then();
}, [visible]);
const addCustomModels = () => {
if (customModel.trim() === '') return;
// 使用逗号分隔字符串,然后去除每个模型名称前后的空格
const modelArray = customModel.split(',').map((model) => model.trim());
let localModels = [...inputs.models];
let localModelOptions = [...modelOptions];
let hasError = false;
modelArray.forEach((model) => {
// 检查模型是否已存在,且模型名称非空
if (model && !localModels.includes(model)) {
localModels.push(model); // 添加到模型列表
localModelOptions.push({
// 添加到下拉选项
key: model,
text: model,
value: model
});
} else if (model) {
showError('某些模型已存在!');
hasError = true;
}
});
if (hasError) return; // 如果有错误则终止操作
// 更新状态值
setModelOptions(localModelOptions);
setCustomModel('');
handleInputChange('models', localModels);
};
return (
<SideSheet
title="编辑标签"
visible={visible}
onCancel={handleClose}
footer={
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Space>
<Button onClick={handleClose}>取消</Button>
<Button type="primary" onClick={handleSave} loading={loading}>保存</Button>
</Space>
</div>
}
>
<div style={{ marginTop: 10 }}>
<Banner
type={'warning'}
description={
<>
所有编辑均为覆盖操作留空则不更改
</>
}
></Banner>
</div>
<Spin spinning={loading}>
<TextInput
label="标签名,留空则解散标签"
name="newTag"
value={inputs.new_tag}
onChange={(value) => setInputs({ ...inputs, new_tag: value })}
placeholder="请输入新标签"
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>模型留空则不更改</Typography.Text>
</div>
<Select
placeholder={'请选择该渠道所支持的模型,留空则不更改'}
name="models"
required
multiple
selection
filter
searchPosition='dropdown'
onChange={(value) => {
handleInputChange('models', value);
}}
value={inputs.models}
autoComplete="new-password"
optionList={modelOptions}
/>
<Input
addonAfter={
<Button type="primary" onClick={addCustomModels}>
填入
</Button>
}
placeholder="输入自定义模型名称"
value={customModel}
onChange={(value) => {
setCustomModel(value.trim());
}}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>分组留空则不更改</Typography.Text>
</div>
<Select
placeholder={'请选择可以使用该渠道的分组,留空则不更改'}
name="groups"
required
multiple
selection
allowAdditions
additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
onChange={(value) => {
handleInputChange('groups', value);
}}
value={inputs.groups}
autoComplete="new-password"
optionList={groupOptions}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>模型重定向</Typography.Text>
</div>
<TextArea
placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,留空则不更改`}
name="model_mapping"
onChange={(value) => {
handleInputChange('model_mapping', value);
}}
autosize
value={inputs.model_mapping}
autoComplete="new-password"
/>
<Space>
<Typography.Text
style={{
color: 'rgba(var(--semi-blue-5), 1)',
userSelect: 'none',
cursor: 'pointer'
}}
onClick={() => {
handleInputChange(
'model_mapping',
JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)
);
}}
>
填入模板
</Typography.Text>
<Typography.Text
style={{
color: 'rgba(var(--semi-blue-5), 1)',
userSelect: 'none',
cursor: 'pointer'
}}
onClick={() => {
handleInputChange(
'model_mapping',
JSON.stringify({}, null, 2)
);
}}
>
清空重定向
</Typography.Text>
<Typography.Text
style={{
color: 'rgba(var(--semi-blue-5), 1)',
userSelect: 'none',
cursor: 'pointer'
}}
onClick={() => {
handleInputChange(
'model_mapping',
""
);
}}
>
不更改
</Typography.Text>
</Space>
</Spin>
</SideSheet>
);
};
export default EditTagModal;

View File

@@ -1,18 +1,22 @@
import React from 'react';
import ChannelsTable from '../../components/ChannelsTable';
import { Layout } from '@douyinfe/semi-ui';
import { useTranslation } from 'react-i18next';
const File = () => (
<>
<Layout>
<Layout.Header>
<h3>管理渠道</h3>
const File = () => {
const { t } = useTranslation();
return (
<>
<Layout>
<Layout.Header>
<h3>{t('管理渠道')}</h3>
</Layout.Header>
<Layout.Content>
<ChannelsTable />
</Layout.Content>
</Layout>
</>
);
</Layout.Content>
</Layout>
</>
);
};
export default File;

View File

@@ -1,8 +1,8 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useContext, useEffect, useRef, useState } from 'react';
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
import { Button, Col, Form, Layout, Row, Spin } from '@douyinfe/semi-ui';
import VChart from '@visactor/vchart';
import { Button, Card, Col, Descriptions, Form, Layout, Row, Spin, Tabs } from '@douyinfe/semi-ui';
import { VChart } from "@visactor/react-vchart";
import {
API,
isAdmin,
@@ -17,11 +17,18 @@ import {
renderQuota,
renderQuotaNumberWithDigit,
stringToColor,
modelToColor,
} from '../../helpers/render';
import { UserContext } from '../../context/User/index.js';
import { StyleContext } from '../../context/Style/index.js';
import { useTranslation } from 'react-i18next';
const Detail = (props) => {
const { t } = useTranslation();
const formRef = useRef();
let now = new Date();
const [userState, userDispatch] = useContext(UserContext);
const [styleState, styleDispatch] = useContext(StyleContext);
const [inputs, setInputs] = useState({
username: '',
token_name: '',
@@ -40,32 +47,76 @@ const Detail = (props) => {
inputs;
const isAdminUser = isAdmin();
const initialized = useRef(false);
const [modelDataChart, setModelDataChart] = useState(null);
const [modelDataPieChart, setModelDataPieChart] = useState(null);
const [loading, setLoading] = useState(false);
const [quotaData, setQuotaData] = useState([]);
const [consumeQuota, setConsumeQuota] = useState(0);
const [consumeTokens, setConsumeTokens] = useState(0);
const [times, setTimes] = useState(0);
const [dataExportDefaultTime, setDataExportDefaultTime] = useState(
localStorage.getItem('data_export_default_time') || 'hour',
);
const handleInputChange = (value, name) => {
if (name === 'data_export_default_time') {
setDataExportDefaultTime(value);
return;
}
setInputs((inputs) => ({ ...inputs, [name]: value }));
};
const spec_line = {
type: 'bar',
data: [
{
id: 'barData',
values: [],
const [pieData, setPieData] = useState([{ type: 'null', value: '0' }]);
const [lineData, setLineData] = useState([]);
const [spec_pie, setSpecPie] = useState({
type: 'pie',
data: [{
id: 'id0',
values: pieData
}],
outerRadius: 0.8,
innerRadius: 0.5,
padAngle: 0.6,
valueField: 'value',
categoryField: 'type',
pie: {
style: {
cornerRadius: 10,
},
],
state: {
hover: {
outerRadius: 0.85,
stroke: '#000',
lineWidth: 1,
},
selected: {
outerRadius: 0.85,
stroke: '#000',
lineWidth: 1,
},
},
},
title: {
visible: true,
text: t('模型调用次数占比'),
subtext: `${t('总计')}${renderNumber(times)}`,
},
legends: {
visible: true,
orient: 'left',
},
label: {
visible: true,
},
tooltip: {
mark: {
content: [
{
key: (datum) => datum['type'],
value: (datum) => renderNumber(datum['value']),
},
],
},
},
color: {
specified: modelColorMap,
},
});
const [spec_line, setSpecLine] = useState({
type: 'bar',
data: [{
id: 'barData',
values: lineData
}],
xField: 'Time',
yField: 'Usage',
seriesField: 'Model',
@@ -76,11 +127,10 @@ const Detail = (props) => {
},
title: {
visible: true,
text: '模型消耗分布',
subtext: '0',
text: t('模型消耗分布'),
subtext: `${t('总计')}${renderQuota(consumeQuota, 2)}`,
},
bar: {
// The state style of bar
state: {
hover: {
stroke: '#000',
@@ -106,9 +156,7 @@ const Detail = (props) => {
},
],
updateContent: (array) => {
// sort by value
array.sort((a, b) => b.value - a.value);
// add $
let sum = 0;
for (let i = 0; i < array.length; i++) {
sum += parseFloat(array[i].value);
@@ -117,9 +165,8 @@ const Detail = (props) => {
4,
);
}
// add to first
array.unshift({
key: '总计',
key: t('总计'),
value: renderQuotaNumberWithDigit(sum, 4),
});
return array;
@@ -129,196 +176,197 @@ const Detail = (props) => {
color: {
specified: modelColorMap,
},
});
// 添加一个新的状态来存储模型-颜色映射
const [modelColors, setModelColors] = useState({});
const handleInputChange = (value, name) => {
if (name === 'data_export_default_time') {
setDataExportDefaultTime(value);
return;
}
setInputs((inputs) => ({ ...inputs, [name]: value }));
};
const spec_pie = {
type: 'pie',
data: [
{
id: 'id0',
values: [{ type: 'null', value: '0' }],
},
],
outerRadius: 0.8,
innerRadius: 0.5,
padAngle: 0.6,
valueField: 'value',
categoryField: 'type',
pie: {
style: {
cornerRadius: 10,
},
state: {
hover: {
outerRadius: 0.85,
stroke: '#000',
lineWidth: 1,
},
selected: {
outerRadius: 0.85,
stroke: '#000',
lineWidth: 1,
},
},
},
title: {
visible: true,
text: '模型调用次数占比',
},
legends: {
visible: true,
orient: 'left',
},
label: {
visible: true,
},
tooltip: {
mark: {
content: [
{
key: (datum) => datum['type'],
value: (datum) => renderNumber(datum['value']),
},
],
},
},
color: {
specified: modelColorMap,
},
};
const loadQuotaData = async (lineChart, pieChart) => {
const loadQuotaData = async () => {
setLoading(true);
let url = '';
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
if (isAdminUser) {
url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
} else {
url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
}
const res = await API.get(url);
const { success, message, data } = res.data;
if (success) {
setQuotaData(data);
if (data.length === 0) {
data.push({
count: 0,
model_name: '无数据',
quota: 0,
created_at: now.getTime() / 1000,
try {
let url = '';
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
if (isAdminUser) {
url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
} else {
url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
}
const res = await API.get(url);
const { success, message, data } = res.data;
if (success) {
setQuotaData(data);
if (data.length === 0) {
data.push({
count: 0,
model_name: '无数据',
quota: 0,
created_at: now.getTime() / 1000,
});
}
// 根据dataExportDefaultTime重制时间粒度
let timeGranularity = 3600;
if (dataExportDefaultTime === 'day') {
timeGranularity = 86400;
} else if (dataExportDefaultTime === 'week') {
timeGranularity = 604800;
}
// sort created_at
data.sort((a, b) => a.created_at - b.created_at);
data.forEach((item) => {
item['created_at'] =
Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
});
updateChartData(data);
} else {
showError(message);
}
// 根据dataExportDefaultTime重制时间粒度
let timeGranularity = 3600;
if (dataExportDefaultTime === 'day') {
timeGranularity = 86400;
} else if (dataExportDefaultTime === 'week') {
timeGranularity = 604800;
}
// sort created_at
data.sort((a, b) => a.created_at - b.created_at);
data.forEach((item) => {
item['created_at'] =
Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
});
updateChart(lineChart, pieChart, data);
} else {
showError(message);
} finally {
setLoading(false);
}
setLoading(false);
};
const refresh = async () => {
await loadQuotaData(modelDataChart, modelDataPieChart);
await loadQuotaData();
};
const initChart = async () => {
let lineChart = modelDataChart;
if (!modelDataChart) {
lineChart = new VChart(spec_line, { dom: 'model_data' });
setModelDataChart(lineChart);
lineChart.renderAsync();
}
let pieChart = modelDataPieChart;
if (!modelDataPieChart) {
pieChart = new VChart(spec_pie, { dom: 'model_pie' });
setModelDataPieChart(pieChart);
pieChart.renderAsync();
}
console.log('init vchart');
await loadQuotaData(lineChart, pieChart);
await loadQuotaData();
};
const updateChart = (lineChart, pieChart, data) => {
if (isAdminUser) {
// 将所有用户合并
}
let pieData = [];
let lineData = [];
let consumeQuota = 0;
let times = 0;
for (let i = 0; i < data.length; i++) {
const item = data[i];
consumeQuota += item.quota;
times += item.count;
// 合并model_name
let pieItem = pieData.find((it) => it.type === item.model_name);
const updateChartData = (data) => {
let newPieData = [];
let newLineData = [];
let totalQuota = 0;
let totalTimes = 0;
let uniqueModels = new Set();
let totalTokens = 0;
// 收集所有唯一的模型名称和时间点
let uniqueTimes = new Set();
data.forEach(item => {
uniqueModels.add(item.model_name);
uniqueTimes.add(timestamp2string1(item.created_at, dataExportDefaultTime));
totalTokens += item.token_used;
});
// 处理颜色映射
const newModelColors = {};
Array.from(uniqueModels).forEach((modelName) => {
newModelColors[modelName] = modelColorMap[modelName] ||
modelColors[modelName] ||
modelToColor(modelName);
});
setModelColors(newModelColors);
// 处理饼图数据
for (let item of data) {
totalQuota += item.quota;
totalTimes += item.count;
let pieItem = newPieData.find((it) => it.type === item.model_name);
if (pieItem) {
pieItem.value += item.count;
} else {
pieData.push({
newPieData.push({
type: item.model_name,
value: item.count,
});
}
// 合并created_at和model_name 为 lineData, created_at 数据类型是小时的时间戳
// 转换日期格式
let createTime = timestamp2string1(
item.created_at,
dataExportDefaultTime,
);
let lineItem = lineData.find(
(it) => it.Time === createTime && it.Model === item.model_name,
);
if (lineItem) {
lineItem.Usage += parseFloat(getQuotaWithUnit(item.quota));
} else {
lineData.push({
Time: createTime,
Model: item.model_name,
Usage: parseFloat(getQuotaWithUnit(item.quota)),
});
}
}
setConsumeQuota(consumeQuota);
setTimes(times);
// sort by count
pieData.sort((a, b) => b.value - a.value);
spec_pie.title.subtext = `总计:${renderNumber(times)}`;
spec_pie.data[0].values = pieData;
// 处理柱状图数据
let timePoints = Array.from(uniqueTimes);
if (timePoints.length < 7) {
// 根据时间粒度生成合适的时间点
const generateTimePoints = () => {
let lastTime = Math.max(...data.map(item => item.created_at));
let points = [];
let interval = dataExportDefaultTime === 'hour' ? 3600
: dataExportDefaultTime === 'day' ? 86400
: 604800;
spec_line.title.subtext = `总计:${renderQuota(consumeQuota, 2)}`;
spec_line.data[0].values = lineData;
pieChart.updateSpec(spec_pie);
lineChart.updateSpec(spec_line);
for (let i = 0; i < 7; i++) {
points.push(timestamp2string1(lastTime - (i * interval), dataExportDefaultTime));
}
return points.reverse();
};
// pieChart.updateData('id0', pieData);
// lineChart.updateData('barData', lineData);
pieChart.reLayout();
lineChart.reLayout();
timePoints = generateTimePoints();
}
// 为每个时间点和模型生成数据
timePoints.forEach(time => {
Array.from(uniqueModels).forEach(model => {
let existingData = data.find(item =>
timestamp2string1(item.created_at, dataExportDefaultTime) === time &&
item.model_name === model
);
newLineData.push({
Time: time,
Model: model,
Usage: existingData ? parseFloat(getQuotaWithUnit(existingData.quota)) : 0
});
});
});
// 排序
newPieData.sort((a, b) => b.value - a.value);
newLineData.sort((a, b) => a.Time.localeCompare(b.Time));
// 更新图表配置和数据
setSpecPie(prev => ({
...prev,
data: [{ id: 'id0', values: newPieData }],
title: {
...prev.title,
subtext: `${t('总计')}${renderNumber(totalTimes)}`
},
color: {
specified: newModelColors
}
}));
setSpecLine(prev => ({
...prev,
data: [{ id: 'barData', values: newLineData }],
title: {
...prev.title,
subtext: `${t('总计')}${renderQuota(totalQuota, 2)}`
},
color: {
specified: newModelColors
}
}));
setPieData(newPieData);
setLineData(newLineData);
setConsumeQuota(totalQuota);
setTimes(totalTimes);
setConsumeTokens(totalTokens);
};
const getUserData = async () => {
let res = await API.get(`/api/user/self`);
const {success, message, data} = res.data;
if (success) {
userDispatch({type: 'login', payload: data});
} else {
showError(message);
}
};
useEffect(() => {
// setDataExportDefaultTime(localStorage.getItem('data_export_default_time'));
// if (dataExportDefaultTime === 'day') {
// // 设置开始时间为7天前
// let st = timestamp2string(now.getTime() / 1000 - 86400 * 7)
// inputs.start_timestamp = st;
// formRef.current.formApi.setValue('start_timestamp', st);
// }
getUserData()
if (!initialized.current) {
initVChartSemiTheme({
isWatchingThemeSwitch: true,
@@ -332,14 +380,14 @@ const Detail = (props) => {
<>
<Layout>
<Layout.Header>
<h3>数据看板</h3>
<h3>{t('数据看板')}</h3>
</Layout.Header>
<Layout.Content>
<Form ref={formRef} layout='horizontal' style={{ marginTop: 10 }}>
<>
<Form.DatePicker
field='start_timestamp'
label='起始时间'
label={t('起始时间')}
style={{ width: 272 }}
initValue={start_timestamp}
value={start_timestamp}
@@ -352,7 +400,7 @@ const Detail = (props) => {
<Form.DatePicker
field='end_timestamp'
fluid
label='结束时间'
label={t('结束时间')}
style={{ width: 272 }}
initValue={end_timestamp}
value={end_timestamp}
@@ -362,15 +410,15 @@ const Detail = (props) => {
/>
<Form.Select
field='data_export_default_time'
label='时间粒度'
label={t('时间粒度')}
style={{ width: 176 }}
initValue={dataExportDefaultTime}
placeholder={'时间粒度'}
placeholder={t('时间粒度')}
name='data_export_default_time'
optionList={[
{ label: '小时', value: 'hour' },
{ label: '天', value: 'day' },
{ label: '周', value: 'week' },
{ label: t('小时'), value: 'hour' },
{ label: t('天'), value: 'day' },
{ label: t('周'), value: 'week' },
]}
onChange={(value) =>
handleInputChange(value, 'data_export_default_time')
@@ -380,42 +428,102 @@ const Detail = (props) => {
<>
<Form.Input
field='username'
label='用户名称'
label={t('用户名称')}
style={{ width: 176 }}
value={username}
placeholder={'可选值'}
placeholder={t('可选值')}
name='username'
onChange={(value) => handleInputChange(value, 'username')}
/>
</>
)}
<Button
label={t('查询')}
type='primary'
htmlType='submit'
className='btn-margin-right'
onClick={refresh}
loading={loading}
style={{ marginTop: 24 }}
>
{t('查询')}
</Button>
<Form.Section>
<Button
label='查询'
type='primary'
htmlType='submit'
className='btn-margin-right'
onClick={refresh}
loading={loading}
>
查询
</Button>
</Form.Section>
</>
</Form>
<Spin spinning={loading}>
<div style={{ height: 500 }}>
<div
id='model_pie'
style={{ width: '100%', minWidth: 100 }}
></div>
</div>
<div style={{ height: 500 }}>
<div
id='model_data'
style={{ width: '100%', minWidth: 100 }}
></div>
</div>
<Row gutter={{ xs: 16, sm: 16, md: 16, lg: 24, xl: 24, xxl: 24 }} style={{marginTop: 20}} type="flex" justify="space-between">
<Col span={styleState.isMobile?24:8}>
<Card className='panel-desc-card'>
<Descriptions row size="small">
<Descriptions.Item itemKey={t('当前余额')}>
{renderQuota(userState?.user?.quota)}
</Descriptions.Item>
<Descriptions.Item itemKey={t('历史消耗')}>
{renderQuota(userState?.user?.used_quota)}
</Descriptions.Item>
<Descriptions.Item itemKey={t('请求次数')}>
{userState.user?.request_count}
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
<Col span={styleState.isMobile?24:8}>
<Card>
<Descriptions row size="small">
<Descriptions.Item itemKey={t('统计额度')}>
{renderQuota(consumeQuota)}
</Descriptions.Item>
<Descriptions.Item itemKey={t('统计Tokens')}>
{consumeTokens}
</Descriptions.Item>
<Descriptions.Item itemKey={t('统计次数')}>
{times}
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
<Col span={styleState.isMobile ? 24 : 8}>
<Card>
<Descriptions row size='small'>
<Descriptions.Item itemKey={t('平均RPM')}>
{(times /
((Date.parse(end_timestamp) -
Date.parse(start_timestamp)) /
60000)).toFixed(3)}
</Descriptions.Item>
<Descriptions.Item itemKey={t('平均TPM')}>
{(consumeTokens /
((Date.parse(end_timestamp) -
Date.parse(start_timestamp)) /
60000)).toFixed(3)}
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
</Row>
<Card style={{marginTop: 20}}>
<Tabs type="line" defaultActiveKey="1">
<Tabs.TabPane tab={t('消耗分布')} itemKey="1">
<div style={{ height: 500 }}>
<VChart
spec={spec_line}
option={{ mode: "desktop-browser" }}
/>
</div>
</Tabs.TabPane>
<Tabs.TabPane tab={t('调用次数分布')} itemKey="2">
<div style={{ height: 500 }}>
<VChart
spec={spec_pie}
option={{ mode: "desktop-browser" }}
/>
</div>
</Tabs.TabPane>
</Tabs>
</Card>
</Spin>
</Layout.Content>
</Layout>

View File

@@ -3,11 +3,15 @@ import { Card, Col, Row } from '@douyinfe/semi-ui';
import { API, showError, showNotice, timestamp2string } from '../../helpers';
import { StatusContext } from '../../context/Status';
import { marked } from 'marked';
import { StyleContext } from '../../context/Style/index.js';
import { useTranslation } from 'react-i18next';
const Home = () => {
const { t } = useTranslation();
const [statusState] = useContext(StatusContext);
const [homePageContentLoaded, setHomePageContentLoaded] = useState(false);
const [homePageContent, setHomePageContent] = useState('');
const [styleState, styleDispatch] = useContext(StyleContext);
const displayNotice = async () => {
const res = await API.get('/api/notice');
@@ -50,7 +54,8 @@ const Home = () => {
useEffect(() => {
displayNotice().then();
displayHomePageContent().then();
}, []);
});
return (
<>
{homePageContentLoaded && homePageContent === '' ? (
@@ -58,13 +63,13 @@ const Home = () => {
<Card
bordered={false}
headerLine={false}
title='系统状况'
title={t('系统状况')}
bodyStyle={{ padding: '10px 20px' }}
>
<Row gutter={16}>
<Col span={12}>
<Card
title='系统信息'
title={t('系统信息')}
headerExtraContent={
<span
style={{
@@ -72,19 +77,19 @@ const Home = () => {
color: 'var(--semi-color-text-1)',
}}
>
系统信息总览
{t('系统信息总览')}
</span>
}
>
<p>名称{statusState?.status?.system_name}</p>
<p>{t('名称')}{statusState?.status?.system_name}</p>
<p>
版本
{t('版本')}
{statusState?.status?.version
? statusState?.status?.version
: 'unknown'}
</p>
<p>
源码
{t('源码')}
<a
href='https://github.com/Calcium-Ion/new-api'
target='_blank'
@@ -94,7 +99,7 @@ const Home = () => {
</a>
</p>
<p>
协议
{t('协议')}
<a
href='https://www.apache.org/licenses/LICENSE-2.0'
target='_blank'
@@ -103,12 +108,12 @@ const Home = () => {
Apache-2.0 License
</a>
</p>
<p>启动时间{getStartTimeString()}</p>
<p>{t('启动时间')}{getStartTimeString()}</p>
</Card>
</Col>
<Col span={12}>
<Card
title='系统配置'
title={t('系统配置')}
headerExtraContent={
<span
style={{
@@ -116,45 +121,45 @@ const Home = () => {
color: 'var(--semi-color-text-1)',
}}
>
系统配置总览
{t('系统配置总览')}
</span>
}
>
<p>
邮箱验证
{t('邮箱验证')}
{statusState?.status?.email_verification === true
? '已启用'
: '未启用'}
? t('已启用')
: t('未启用')}
</p>
<p>
GitHub 身份验证
{t('GitHub 身份验证')}
{statusState?.status?.github_oauth === true
? '已启用'
: '未启用'}
? t('已启用')
: t('未启用')}
</p>
<p>
微信身份验证
{t('微信身份验证')}
{statusState?.status?.wechat_login === true
? '已启用'
: '未启用'}
? t('已启用')
: t('未启用')}
</p>
<p>
Turnstile 用户校验
{t('Turnstile 用户校验')}
{statusState?.status?.turnstile_check === true
? '已启用'
: '未启用'}
? t('已启用')
: t('未启用')}
</p>
<p>
Telegram 身份验证
{t('Telegram 身份验证')}
{statusState?.status?.telegram_oauth === true
? '已启用'
: '未启用'}
? t('已启用')
: t('未启用')}
</p>
<p>
Linux DO 身份验证
{t('Linux DO 身份验证')}
{statusState?.status?.linuxdo_oauth === true
? '已启用'
: '未启用'}
? t('已启用')
: t('未启用')}
</p>
</Card>
</Col>

View File

@@ -1,24 +1,27 @@
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { UserContext } from '../context/User';
import { API, getUserIdFromLocalStorage, showError } from '../helpers';
import { Card, Chat, Input, Layout, Select, Slider, TextArea, Typography } from '@douyinfe/semi-ui';
import { UserContext } from '../../context/User/index.js';
import { API, getUserIdFromLocalStorage, showError } from '../../helpers/index.js';
import { Card, Chat, Input, Layout, Select, Slider, TextArea, Typography, Button } from '@douyinfe/semi-ui';
import { SSE } from 'sse';
import { IconSetting } from '@douyinfe/semi-icons';
import { StyleContext } from '../../context/Style/index.js';
import { useTranslation } from 'react-i18next';
const defaultMessage = [
{
role: 'user',
id: '2',
createAt: 1715676751919,
content: "你好",
const roleInfo = {
user: {
name: 'User',
avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
},
{
role: 'assistant',
id: '3',
createAt: 1715676751919,
content: "你好,请问有什么可以帮助您的吗?",
assistant: {
name: 'Assistant',
avatar: 'logo.png'
},
system: {
name: 'System',
avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
}
];
}
let id = 4;
function getId() {
@@ -26,6 +29,23 @@ function getId() {
}
const Playground = () => {
const { t } = useTranslation();
const defaultMessage = [
{
role: 'user',
id: '2',
createAt: 1715676751919,
content: t('你好'),
},
{
role: 'assistant',
id: '3',
createAt: 1715676751919,
content: t('你好,请问有什么可以帮助您的吗?'),
}
];
const [inputs, setInputs] = useState({
model: 'gpt-4o-mini',
group: '',
@@ -39,6 +59,8 @@ const Playground = () => {
const [message, setMessage] = useState(defaultMessage);
const [models, setModels] = useState([]);
const [groups, setGroups] = useState([]);
const [showSettings, setShowSettings] = useState(true);
const [styleState, styleDispatch] = useContext(StyleContext);
const handleInputChange = (name, value) => {
setInputs((inputs) => ({ ...inputs, [name]: value }));
@@ -46,7 +68,7 @@ const Playground = () => {
useEffect(() => {
if (searchParams.get('expired')) {
showError('未登录或登录已过期,请重新登录!');
showError(t('未登录或登录已过期,请重新登录!'));
}
let status = localStorage.getItem('status');
if (status) {
@@ -67,7 +89,7 @@ const Playground = () => {
}));
setModels(localModelOptions);
} else {
showError(message);
showError(t(message));
}
};
@@ -75,31 +97,33 @@ const Playground = () => {
let res = await API.get(`/api/user/self/groups`);
const { success, message, data } = res.data;
if (success) {
// return data is a map, key is group name, value is group description
// label is group description, value is group name
let localGroupOptions = Object.keys(data).map((group) => ({
label: data[group],
value: group,
}));
// handleInputChange('group', localGroupOptions[0].value);
if (localGroupOptions.length > 0) {
// set default group at first
localGroupOptions.unshift({
label: '用户分组',
value: '',
});
} else {
if (localGroupOptions.length === 0) {
localGroupOptions = [{
label: '用户分组',
label: t('用户分组'),
value: '',
}];
setGroups(localGroupOptions);
} else {
const localUser = JSON.parse(localStorage.getItem('user'));
const userGroup = (userState.user && userState.user.group) || (localUser && localUser.group);
if (userGroup) {
const userGroupIndex = localGroupOptions.findIndex(g => g.value === userGroup);
if (userGroupIndex > -1) {
const userGroupOption = localGroupOptions.splice(userGroupIndex, 1)[0];
localGroupOptions.unshift(userGroupOption);
}
}
}
setGroups(localGroupOptions);
handleInputChange('group', localGroupOptions[0].value);
} else {
showError(message);
showError(t(message));
}
};
@@ -242,94 +266,147 @@ const Playground = () => {
})
}, []);
const SettingsToggle = () => {
if (!styleState.isMobile) return null;
return (
<Button
icon={<IconSetting />}
style={{
position: 'absolute',
left: showSettings ? -10 : -20,
top: '50%',
transform: 'translateY(-50%)',
zIndex: 1000,
width: 40,
height: 40,
borderRadius: '0 20px 20px 0',
padding: 0,
boxShadow: '2px 0 8px rgba(0, 0, 0, 0.15)',
}}
onClick={() => setShowSettings(!showSettings)}
theme="solid"
type="primary"
/>
);
};
function CustomInputRender(props) {
const { detailProps } = props;
const { clearContextNode, uploadNode, inputNode, sendNode, onClick } = detailProps;
return <div style={{margin: '8px 16px', display: 'flex', flexDirection:'row',
alignItems: 'flex-end', borderRadius: 16,padding: 10, border: '1px solid var(--semi-color-border)'}}
onClick={onClick}
>
{/*{uploadNode}*/}
{inputNode}
{sendNode}
</div>
}
const renderInputArea = useCallback((props) => {
return (<CustomInputRender {...props} />)
}, []);
return (
<Layout style={{height: '100%'}}>
<Layout.Sider>
<Card style={commonOuterStyle}>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>分组</Typography.Text>
</div>
<Select
placeholder={'请选择分组'}
name='group'
required
selection
onChange={(value) => {
handleInputChange('group', value);
}}
value={inputs.group}
autoComplete='new-password'
optionList={groups}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>模型</Typography.Text>
</div>
<Select
placeholder={'请选择模型'}
name='model'
required
selection
filter
onChange={(value) => {
handleInputChange('model', value);
}}
value={inputs.model}
autoComplete='new-password'
optionList={models}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>Temperature</Typography.Text>
</div>
<Slider
step={0.1}
min={0.1}
max={1}
value={inputs.temperature}
onChange={(value) => {
handleInputChange('temperature', value);
}}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>MaxTokens</Typography.Text>
</div>
<Input
placeholder='MaxTokens'
name='max_tokens'
required
autoComplete='new-password'
defaultValue={0}
value={inputs.max_tokens}
onChange={(value) => {
handleInputChange('max_tokens', value);
}}
/>
{(showSettings || !styleState.isMobile) && (
<Layout.Sider style={{ display: styleState.isMobile ? 'block' : 'initial' }}>
<Card style={commonOuterStyle}>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>{t('分组')}</Typography.Text>
</div>
<Select
placeholder={t('请选择分组')}
name='group'
required
selection
onChange={(value) => {
handleInputChange('group', value);
}}
value={inputs.group}
autoComplete='new-password'
optionList={groups.map((group) => ({
...group,
label: styleState.isMobile && group.label.length > 16
? group.label.substring(0, 16) + '...'
: group.label,
}))}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>{t('模型')}</Typography.Text>
</div>
<Select
placeholder={t('请选择模型')}
name='model'
required
selection
searchPosition='dropdown'
filter
onChange={(value) => {
handleInputChange('model', value);
}}
value={inputs.model}
autoComplete='new-password'
optionList={models}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>Temperature</Typography.Text>
</div>
<Slider
step={0.1}
min={0.1}
max={1}
value={inputs.temperature}
onChange={(value) => {
handleInputChange('temperature', value);
}}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>MaxTokens</Typography.Text>
</div>
<Input
placeholder='MaxTokens'
name='max_tokens'
required
autoComplete='new-password'
defaultValue={0}
value={inputs.max_tokens}
onChange={(value) => {
handleInputChange('max_tokens', value);
}}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>System</Typography.Text>
</div>
<TextArea
placeholder='System Prompt'
name='system'
required
autoComplete='new-password'
autosize
defaultValue={systemPrompt}
// value={systemPrompt}
onChange={(value) => {
setSystemPrompt(value);
}}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>System</Typography.Text>
</div>
<TextArea
placeholder='System Prompt'
name='system'
required
autoComplete='new-password'
autosize
defaultValue={systemPrompt}
// value={systemPrompt}
onChange={(value) => {
setSystemPrompt(value);
}}
/>
</Card>
</Layout.Sider>
</Card>
</Layout.Sider>
)}
<Layout.Content>
<div style={{height: '100%'}}>
<div style={{height: '100%', position: 'relative'}}>
<SettingsToggle />
<Chat
chatBoxRenderConfig={{
renderChatBoxAction: () => {
return <div></div>
}
}}
renderInputArea={renderInputArea}
roleConfig={roleInfo}
style={commonOuterStyle}
chats={message}
onMessageSend={onMessageSend}

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
API,
downloadTextAsFile,
@@ -7,7 +8,7 @@ import {
showError,
showSuccess,
} from '../../helpers';
import { renderQuotaWithPrompt } from '../../helpers/render';
import { getQuotaPerUnit, renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
import {
AutoComplete,
Button,
@@ -22,6 +23,7 @@ import Title from '@douyinfe/semi-ui/lib/es/typography/title';
import { Divider } from 'semantic-ui-react';
const EditRedemption = (props) => {
const { t } = useTranslation();
const isEdit = props.editingRedemption.id !== undefined;
const [loading, setLoading] = useState(isEdit);
@@ -66,11 +68,16 @@ const EditRedemption = (props) => {
}, [props.editingRedemption.id]);
const submit = async () => {
if (!isEdit && inputs.name === '') return;
let name = inputs.name;
if (!isEdit && inputs.name === '') {
// set default name
name = t('新建兑换码') + ' ' + renderQuota(quota);
}
setLoading(true);
let localInputs = inputs;
localInputs.count = parseInt(localInputs.count);
localInputs.quota = parseInt(localInputs.quota);
localInputs.name = name;
let res;
if (isEdit) {
res = await API.put(`/api/redemption/`, {
@@ -85,11 +92,11 @@ const EditRedemption = (props) => {
const { success, message, data } = res.data;
if (success) {
if (isEdit) {
showSuccess('兑换码更新成功!');
showSuccess(t('兑换码更新成功!'));
props.refresh();
props.handleClose();
} else {
showSuccess('兑换码创建成功!');
showSuccess(t('兑换码创建成功!'));
setInputs(originInputs);
props.refresh();
props.handleClose();
@@ -102,13 +109,12 @@ const EditRedemption = (props) => {
for (let i = 0; i < data.length; i++) {
text += data[i] + '\n';
}
// downloadTextAsFile(text, `${inputs.name}.txt`);
Modal.confirm({
title: '兑换码创建成功',
title: t('兑换码创建成功'),
content: (
<div>
<p>兑换码创建成功是否下载兑换码</p>
<p>兑换码将以文本文件的形式下载文件名为兑换码的名称</p>
<p>{t('兑换码创建成功是否下载兑换码?')}</p>
<p>{t('兑换码将以文本文件的形式下载文件名为兑换码的名称。')}</p>
</div>
),
onOk: () => {
@@ -125,7 +131,7 @@ const EditRedemption = (props) => {
placement={isEdit ? 'right' : 'left'}
title={
<Title level={3}>
{isEdit ? '更新兑换码信息' : '创建新的兑换码'}
{isEdit ? t('更新兑换码信息') : t('创建新的兑换码')}
</Title>
}
headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
@@ -135,7 +141,7 @@ const EditRedemption = (props) => {
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Space>
<Button theme='solid' size={'large'} onClick={submit}>
提交
{t('提交')}
</Button>
<Button
theme='solid'
@@ -143,7 +149,7 @@ const EditRedemption = (props) => {
type={'tertiary'}
onClick={handleCancel}
>
取消
{t('取消')}
</Button>
</Space>
</div>
@@ -155,9 +161,9 @@ const EditRedemption = (props) => {
<Spin spinning={loading}>
<Input
style={{ marginTop: 20 }}
label='名称'
label={t('名称')}
name='name'
placeholder={'请输入名称'}
placeholder={t('请输入名称')}
onChange={(value) => handleInputChange('name', value)}
value={name}
autoComplete='new-password'
@@ -165,12 +171,12 @@ const EditRedemption = (props) => {
/>
<Divider />
<div style={{ marginTop: 20 }}>
<Typography.Text>{`额度${renderQuotaWithPrompt(quota)}`}</Typography.Text>
<Typography.Text>{t('额度') + renderQuotaWithPrompt(quota)}</Typography.Text>
</div>
<AutoComplete
style={{ marginTop: 8 }}
name='quota'
placeholder={'请输入额度'}
placeholder={t('请输入额度')}
onChange={(value) => handleInputChange('quota', value)}
value={quota}
autoComplete='new-password'
@@ -188,12 +194,12 @@ const EditRedemption = (props) => {
{!isEdit && (
<>
<Divider />
<Typography.Text>生成数量</Typography.Text>
<Typography.Text>{t('生成数量')}</Typography.Text>
<Input
style={{ marginTop: 8 }}
label='生成数量'
label={t('生成数量')}
name='count'
placeholder={'请输入生成数量'}
placeholder={t('请输入生成数量')}
onChange={(value) => handleInputChange('count', value)}
value={count}
autoComplete='new-password'

View File

@@ -1,12 +1,15 @@
import React from 'react';
import RedemptionsTable from '../../components/RedemptionsTable';
import { Layout } from '@douyinfe/semi-ui';
import { useTranslation } from 'react-i18next';
const Redemption = () => (
<>
<Layout>
<Layout.Header>
<h3>管理兑换码</h3>
const Redemption = () => {
const { t } = useTranslation();
return (
<>
<Layout>
<Layout.Header>
<h3>{t('管理兑换码')}</h3>
</Layout.Header>
<Layout.Content>
<RedemptionsTable />
@@ -14,5 +17,6 @@ const Redemption = () => (
</Layout>
</>
);
}
export default Redemption;

View File

@@ -0,0 +1,131 @@
import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
import {
compareObjects,
API,
showError,
showSuccess,
showWarning,
verifyJSON,
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function GroupRatioSettings(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
GroupRatio: '',
UserUsableGroups: ''
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
async function onSubmit() {
try {
await refForm.current.validate().then(() => {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => {
const value = typeof inputs[item.key] === 'boolean'
? String(inputs[item.key])
: inputs[item.key];
return API.put('/api/option/', { key: item.key, value });
});
setLoading(true);
Promise.all(requestQueue)
.then((res) => {
if (res.includes(undefined)) {
return showError(requestQueue.length > 1 ? t('部分保存失败,请重试') : t('保存失败'));
}
for (let i = 0; i < res.length; i++) {
if (!res[i].data.success) {
return showError(res[i].data.message);
}
}
showSuccess(t('保存成功'));
props.refresh();
})
.catch(error => {
console.error('Unexpected error:', error);
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
});
}).catch(() => {
showError(t('请检查输入'));
});
} catch (error) {
showError(t('请检查输入'));
console.error(error);
}
}
useEffect(() => {
const currentInputs = {};
for (let key in props.options) {
if (Object.keys(inputs).includes(key)) {
currentInputs[key] = props.options[key];
}
}
setInputs(currentInputs);
setInputsRow(structuredClone(currentInputs));
refForm.current.setValues(currentInputs);
}, [props.options]);
return (
<Spin spinning={loading}>
<Form
values={inputs}
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={t('分组设置')}>
<Row gutter={16}>
<Col span={16}>
<Form.TextArea
label={t('分组倍率')}
placeholder={t('为一个 JSON 文本,键为分组名称,值为倍率')}
field={'GroupRatio'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => verifyJSON(value),
message: t('不是合法的 JSON 字符串')
}
]}
onChange={(value) => setInputs({ ...inputs, GroupRatio: value })}
/>
</Col>
</Row>
<Row gutter={16}>
<Col span={16}>
<Form.TextArea
label={t('用户可选分组')}
placeholder={t('为一个 JSON 文本,键为分组名称,值为分组描述')}
field={'UserUsableGroups'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => verifyJSON(value),
message: t('不是合法的 JSON 字符串')
}
]}
onChange={(value) => setInputs({ ...inputs, UserUsableGroups: value })}
/>
</Col>
</Row>
</Form.Section>
</Form>
<Button onClick={onSubmit}>{t('保存分组倍率设置')}</Button>
</Spin>
);
}

View File

@@ -0,0 +1,178 @@
import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Form, Popconfirm, Row, Space, Spin } from '@douyinfe/semi-ui';
import {
compareObjects,
API,
showError,
showSuccess,
showWarning,
verifyJSON,
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function ModelRatioSettings(props) {
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
ModelPrice: '',
ModelRatio: '',
CompletionRatio: '',
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
const { t } = useTranslation();
async function onSubmit() {
try {
await refForm.current.validate().then(() => {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => {
const value = typeof inputs[item.key] === 'boolean'
? String(inputs[item.key])
: inputs[item.key];
return API.put('/api/option/', { key: item.key, value });
});
setLoading(true);
Promise.all(requestQueue)
.then((res) => {
if (res.includes(undefined)) {
return showError(requestQueue.length > 1 ? t('部分保存失败,请重试') : t('保存失败'));
}
for (let i = 0; i < res.length; i++) {
if (!res[i].data.success) {
return showError(res[i].data.message);
}
}
showSuccess(t('保存成功'));
props.refresh();
})
.catch(error => {
console.error('Unexpected error:', error);
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
});
}).catch(() => {
showError(t('请检查输入'));
});
} catch (error) {
showError(t('请检查输入'));
console.error(error);
}
}
async function resetModelRatio() {
try {
let res = await API.post(`/api/option/rest_model_ratio`);
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)) {
currentInputs[key] = props.options[key];
}
}
setInputs(currentInputs);
setInputsRow(structuredClone(currentInputs));
refForm.current.setValues(currentInputs);
}, [props.options]);
return (
<Spin spinning={loading}>
<Form
values={inputs}
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section>
<Row gutter={16}>
<Col span={16}>
<Form.TextArea
label={t('模型固定价格')}
extraText={t('一次调用消耗多少刀,优先级大于模型倍率')}
placeholder={t('为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 "gpt-4-gizmo-*": 0.1一次消耗0.1刀')}
field={'ModelPrice'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => verifyJSON(value),
message: '不是合法的 JSON 字符串'
}
]}
onChange={(value) => setInputs({ ...inputs, ModelPrice: value })}
/>
</Col>
</Row>
<Row gutter={16}>
<Col span={16}>
<Form.TextArea
label={t('模型倍率')}
placeholder={t('为一个 JSON 文本,键为模型名称,值为倍率')}
field={'ModelRatio'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => verifyJSON(value),
message: '不是合法的 JSON 字符串'
}
]}
onChange={(value) => setInputs({ ...inputs, ModelRatio: value })}
/>
</Col>
</Row>
<Row gutter={16}>
<Col span={16}>
<Form.TextArea
label={t('模型补全倍率(仅对自定义模型有效)')}
extraText={t('仅对自定义模型有效')}
placeholder={t('为一个 JSON 文本,键为模型名称,值为倍率')}
field={'CompletionRatio'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => verifyJSON(value),
message: '不是合法的 JSON 字符串'
}
]}
onChange={(value) => setInputs({ ...inputs, CompletionRatio: value })}
/>
</Col>
</Row>
</Form.Section>
</Form>
<Space>
<Button onClick={onSubmit}>{t('保存模型倍率设置')}</Button>
<Popconfirm
title={t('确定重置模型倍率吗?')}
content={t('此修改将不可逆')}
okType={'danger'}
position={'top'}
onConfirm={resetModelRatio}
>
<Button type={'danger'}>{t('重置模型倍率')}</Button>
</Popconfirm>
</Space>
</Spin>
);
}

View File

@@ -0,0 +1,319 @@
// ModelSettingsVisualEditor.js
import React, { useEffect, useState } from 'react';
import { Table, Button, Input, Modal, Form, Space } from '@douyinfe/semi-ui';
import { IconDelete, IconPlus, IconSearch, IconSave } from '@douyinfe/semi-icons';
import { showError, showSuccess } from '../../../helpers';
import { API } from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function ModelSettingsVisualEditor(props) {
const { t } = useTranslation();
const [models, setModels] = useState([]);
const [visible, setVisible] = useState(false);
const [currentModel, setCurrentModel] = useState(null);
const [searchText, setSearchText] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [loading, setLoading] = useState(false);
const pageSize = 10;
useEffect(() => {
try {
const modelPrice = JSON.parse(props.options.ModelPrice || '{}');
const modelRatio = JSON.parse(props.options.ModelRatio || '{}');
const completionRatio = JSON.parse(props.options.CompletionRatio || '{}');
// 合并所有模型名称
const modelNames = new Set([
...Object.keys(modelPrice),
...Object.keys(modelRatio),
...Object.keys(completionRatio)
]);
const modelData = Array.from(modelNames).map(name => ({
name,
price: modelPrice[name] === undefined ? '' : modelPrice[name],
ratio: modelRatio[name] === undefined ? '' : modelRatio[name],
completionRatio: completionRatio[name] === undefined ? '' : completionRatio[name]
}));
setModels(modelData);
} catch (error) {
console.error('JSON解析错误:', error);
}
}, [props.options]);
// 首先声明分页相关的工具函数
const getPagedData = (data, currentPage, pageSize) => {
const start = (currentPage - 1) * pageSize;
const end = start + pageSize;
return data.slice(start, end);
};
// 在 return 语句之前,先处理过滤和分页逻辑
const filteredModels = models.filter(model =>
searchText ? model.name.toLowerCase().includes(searchText.toLowerCase()) : true
);
// 然后基于过滤后的数据计算分页数据
const pagedData = getPagedData(filteredModels, currentPage, pageSize);
const SubmitData = async () => {
setLoading(true);
const output = {
ModelPrice: {},
ModelRatio: {},
CompletionRatio: {}
};
let currentConvertModelName = '';
try {
// 数据转换
models.forEach(model => {
currentConvertModelName = model.name;
if (model.price !== '') {
// 如果价格不为空,则转换为浮点数,忽略倍率参数
output.ModelPrice[model.name] = parseFloat(model.price)
} else {
if (model.ratio !== '') output.ModelRatio[model.name] = parseFloat(model.ratio);
if (model.completionRatio != '') output.CompletionRatio[model.name] = parseFloat(model.completionRatio);
}
});
// 准备API请求数组
const finalOutput = {
ModelPrice: JSON.stringify(output.ModelPrice, null, 2),
ModelRatio: JSON.stringify(output.ModelRatio, null, 2),
CompletionRatio: JSON.stringify(output.CompletionRatio, null, 2)
};
const requestQueue = Object.entries(finalOutput).map(([key, value]) => {
return API.put('/api/option/', {
key,
value
});
});
// 批量处理请求
const results = await Promise.all(requestQueue);
// 验证结果
if (requestQueue.length === 1) {
if (results.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (results.includes(undefined)) {
return showError('部分保存失败,请重试');
}
}
// 检查每个请求的结果
for (const res of results) {
if (!res.data.success) {
return showError(res.data.message);
}
}
showSuccess('保存成功');
props.refresh();
} catch (error) {
console.error('保存失败:', error);
showError('保存失败,请重试');
} finally {
setLoading(false);
}
};
const columns = [
{
title: t('模型名称'),
dataIndex: 'name',
key: 'name',
},
{
title: t('模型固定价格'),
dataIndex: 'price',
key: 'price',
render: (text, record) => (
<Input
value={text}
placeholder={t('按量计费')}
onChange={value => updateModel(record.name, 'price', value)}
/>
)
},
{
title: t('模型倍率'),
dataIndex: 'ratio',
key: 'ratio',
render: (text, record) => (
<Input
value={text}
placeholder={record.price !== '' ? t('模型倍率') : t('默认补全倍率')}
disabled={record.price !== ''}
onChange={value => updateModel(record.name, 'ratio', value)}
/>
)
},
{
title: t('补全倍率'),
dataIndex: 'completionRatio',
key: 'completionRatio',
render: (text, record) => (
<Input
value={text}
placeholder={record.price !== '' ? t('补全倍率') : t('默认补全倍率')}
disabled={record.price !== ''}
onChange={value => updateModel(record.name, 'completionRatio', value)}
/>
)
},
{
title: t('操作'),
key: 'action',
render: (_, record) => (
<Button
icon={<IconDelete />}
type="danger"
onClick={() => deleteModel(record.name)}
/>
)
}
];
const updateModel = (name, field, value) => {
if (isNaN(value)) {
showError('请输入数字');
return;
}
setModels(prev =>
prev.map(model =>
model.name === name
? { ...model, [field]: value }
: model
)
);
};
const deleteModel = (name) => {
setModels(prev => prev.filter(model => model.name !== name));
};
const addModel = (values) => {
// 检查模型名称是否存在, 如果存在则拒绝添加
if (models.some(model => model.name === values.name)) {
showError('模型名称已存在');
return;
}
// 不允许同时添加固定价格和倍率
if (values.price !== '' && (values.ratio !== '' || values.completionRatio !== '')) {
showError('固定价格和倍率不能同时存在');
return;
}
setModels(prev => [{
name: values.name,
price: values.price || '',
ratio: values.ratio || '',
completionRatio: values.completionRatio || ''
}, ...prev]);
setVisible(false);
showSuccess('添加成功');
};
return (
<>
<Space vertical align="start" style={{ width: '100%' }}>
<Space>
<Button icon={<IconPlus />} onClick={() => setVisible(true)}>
{t('添加模型')}
</Button>
<Button type="primary" icon={<IconSave />} onClick={SubmitData}>
{t('应用更改')}
</Button>
<Input
prefix={<IconSearch />}
placeholder={t('搜索模型名称')}
value={searchText}
onChange={value => {
setSearchText(value)
setCurrentPage(1);
}}
style={{ width: 200 }}
/>
</Space>
<Table
columns={columns}
dataSource={pagedData}
pagination={{
currentPage: currentPage,
pageSize: pageSize,
total: filteredModels.length,
onPageChange: page => setCurrentPage(page),
formatPageText: (page) =>
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: filteredModels.length
}),
showTotal: true,
showSizeChanger: false
}}
/>
</Space>
<Modal
title={t('添加模型')}
visible={visible}
onCancel={() => setVisible(false)}
onOk={() => {
currentModel && addModel(currentModel);
}}
>
<Form>
<Form.Input
field="name"
label={t('模型名称')}
placeholder="strawberry"
required
onChange={value => setCurrentModel(prev => ({ ...prev, name: value }))}
/>
<Form.Switch
field="priceMode"
label={<>{t('定价模式')}{currentModel?.priceMode ? t("固定价格") : t("倍率模式")}</>}
onChange={checked => {
setCurrentModel(prev => ({
...prev,
price: '',
ratio: '',
completionRatio: '',
priceMode: checked
}));
}}
/>
{currentModel?.priceMode ? (
<Form.Input
field="price"
label={t('固定价格(每次)')}
placeholder={t('输入每次价格')}
onChange={value => setCurrentModel(prev => ({ ...prev, price: value }))}
/>
) : (
<>
<Form.Input
field="ratio"
label={t('模型倍率')}
placeholder={t('输入模型倍率')}
onChange={value => setCurrentModel(prev => ({ ...prev, ratio: value }))}
/>
<Form.Input
field="completionRatio"
label={t('补全倍率')}
placeholder={t('输入补全价格')}
onChange={value => setCurrentModel(prev => ({ ...prev, completionRatio: value }))}
/>
</>
)}
</Form>
</Modal>
</>
);
}

View File

@@ -9,8 +9,10 @@ import {
verifyJSON,
verifyJSONPromise
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function SettingsChats(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
Chats: "[]",
@@ -24,7 +26,7 @@ export default function SettingsChats(props) {
await refForm.current.validate().then(() => {
console.log('Validation passed');
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning('你似乎并没有修改什么');
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => {
let value = '';
if (typeof inputs[item.key] === 'boolean') {
@@ -44,23 +46,23 @@ export default function SettingsChats(props) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined))
return showError('部分保存失败,请重试');
return showError(t('部分保存失败,请重试'));
}
showSuccess('保存成功');
showSuccess(t('保存成功'));
props.refresh();
})
.catch(() => {
showError('保存失败,请重试');
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
});
}).catch((error) => {
console.error('Validation failed:', error);
showError('请检查输入');
showError(t('请检查输入'));
});
} catch (error) {
showError('请检查输入');
showError(t('请检查输入'));
console.error(error);
}
}
@@ -104,19 +106,19 @@ export default function SettingsChats(props) {
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'令牌聊天设置'}>
<Form.Section text={t('令牌聊天设置')}>
<Banner
type='warning'
description={'必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能'}
description={t('必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能')}
/>
<Banner
type='info'
description={'链接中的{key}将自动替换为sk-xxxx{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1'}
description={t('链接中的{key}将自动替换为sk-xxxx{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1')}
/>
<Form.TextArea
label={'聊天配置'}
label={t('聊天配置')}
extraText={''}
placeholder={'为一个 JSON 文本'}
placeholder={t('为一个 JSON 文本')}
field={'Chats'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
@@ -126,7 +128,7 @@ export default function SettingsChats(props) {
validator: (rule, value) => {
return verifyJSON(value);
},
message: '不是合法的 JSON 字符串'
message: t('不是合法的 JSON 字符串')
}
]}
onChange={(value) =>
@@ -140,7 +142,7 @@ export default function SettingsChats(props) {
</Form>
<Space>
<Button onClick={onSubmit}>
保存聊天设置
{t('保存聊天设置')}
</Button>
</Space>
</Spin>

Some files were not shown because too many files have changed in this diff Show More