Compare commits

...

183 Commits

Author SHA1 Message Date
CalciumIon
52c023a1dd fix #663 2024-12-27 21:59:05 +08:00
CalciumIon
1cef91a741 fix: prevent duplicate models in user group retrieval 2024-12-27 21:25:44 +08:00
CalciumIon
77861e6440 refactor: improve user group handling and add GetUserUsableGroups function
- Introduced a new function `GetUserUsableGroupsCopy` to return a copy of user usable groups.
- Updated `GetUserUsableGroups` to utilize the new function for better encapsulation.
- Changed variable names from `UserUsableGroups` to `userUsableGroups` for consistency.
- Enhanced `GetUserUsableGroups` logic to ensure it returns a copy of the groups, preventing unintended modifications.
2024-12-27 21:19:22 +08:00
CalciumIon
5f082d72bb update dockerignore 2024-12-27 20:49:58 +08:00
CalciumIon
0fd0e5d309 fix: oauth bind 2024-12-27 18:32:11 +08:00
CalciumIon
d2297d2723 feat: update o1 default token encoder 2024-12-27 15:03:10 +08:00
CalciumIon
62ae46b552 feat: support azure stream_options 2024-12-26 22:51:06 +08:00
CalciumIon
0b1354ed51 update model ratio 2024-12-26 16:03:22 +08:00
Calcium-Ion
132c71390c Merge pull request #661 from tenacioustommy/fix-title-schema
fix delete title schema
2024-12-26 14:27:07 +08:00
Calcium-Ion
bb3deb7b93 Merge pull request #662 from xqx333/main
fix 重试过程多次获取图片
2024-12-26 14:26:50 +08:00
CalciumIon
f92d96e298 fix: update render function for quota display in Detail page 2024-12-26 14:25:44 +08:00
xqx333
c86762b656 Update relay-text.go
在上下文中存入promptTokens,避免重试过程重复计算
2024-12-26 02:00:04 +08:00
tenacious
3409d7a6b6 fix delete title schema 2024-12-26 00:24:45 +08:00
CalciumIon
bfba4866a5 fix: validate number input in renderQuotaNumberWithDigit and improve data handling in Detail page
- Added input validation to ensure that the `num` parameter in `renderQuotaNumberWithDigit` is a valid number, returning 0 for invalid inputs.
- Updated the `Detail` component to use `datum['rawQuota']` instead of `datum['Usage']` for rendering quota values, ensuring more accurate data representation.
- Enhanced data aggregation logic to handle cases where quota values may be missing or invalid, improving overall data integrity in charts and tables.
- Removed unnecessary time granularity calculations and streamlined the data processing for better performance.
2024-12-25 23:16:35 +08:00
CalciumIon
4fc1fe318e refactor: migrate group ratio and user usable groups logic to new setting package
- Replaced references to common.GroupRatio and common.UserUsableGroups with corresponding functions from the new setting package across multiple controllers and services.
- Introduced new setting functions for managing group ratios and user usable groups, enhancing code organization and maintainability.
- Updated related functions to ensure consistent behavior with the new setting package integration.
2024-12-25 19:31:12 +08:00
CalciumIon
b3576f24ef fix typo 2024-12-25 18:44:45 +08:00
CalciumIon
ed4d26fc9e fix: update MaxCompletionTokens for model prefix handling in buildTestRequest function 2024-12-25 17:55:20 +08:00
CalciumIon
ba56e2e8ca fix: correct user retrieval in GetPricing function 2024-12-25 14:29:52 +08:00
CalciumIon
7c20e6d047 fix: resolve pricing calculation issue (#659) 2024-12-25 14:26:43 +08:00
CalciumIon
72d6898eb5 feat: Implement batch tagging functionality for channels
- Added a new endpoint to batch set tags for multiple channels, allowing users to update tags efficiently.
- Introduced a new `BatchSetChannelTag` function in the controller to handle incoming requests and validate parameters.
- Updated the `BatchSetChannelTag` method in the model to manage database transactions and ensure data integrity during tag updates.
- Enhanced the ChannelsTable component in the frontend to support batch tag setting, including UI elements for user interaction.
- Updated localization files to include new translation keys related to batch operations and tag settings.
2024-12-25 14:19:00 +08:00
CalciumIon
f2c9388139 fix: update searchUsers function to include searchKeyword and searchGroup parameters 2024-12-25 13:44:55 +08:00
Calcium-Ion
aaf5cecefd Merge pull request #656 from Yan-Zero/main
fix: gemini function call
2024-12-25 13:38:34 +08:00
CalciumIon
fe2165ace6 fix: #657 2024-12-24 22:30:05 +08:00
CalciumIon
3003d12a20 fix: get upstream models 2024-12-24 20:48:21 +08:00
Yan
a8a2195ab1 Merge branch 'Calcium-Ion:main' into main 2024-12-24 20:46:16 +08:00
Yan
d40e6ec25d fix: gemini func call 2024-12-24 20:46:02 +08:00
CalciumIon
8129aa76f9 feat: Enhance pricing functionality with user group support
- Updated the GetPricing function in the backend to include user group information, allowing for dynamic adjustment of group ratios based on the user's group.
- Implemented logic to filter group ratios based on the user's usable groups, improving the accuracy of pricing data returned.
- Modified the ModelPricing component to utilize the new usable group data, ensuring only relevant groups are displayed in the UI.
- Enhanced state management in the frontend to accommodate the new usable group information, improving user experience and data consistency.
2024-12-24 19:23:29 +08:00
CalciumIon
fb8595da18 feat: Update localization and enhance token editing functionality
- Added new translation keys for English localization in `en.json`, including "Token group, default is the your's group" and "IP whitelist (do not overly trust this function)".
- Refactored `EditToken.js` to utilize the `useTranslation` hook for improved internationalization, ensuring all user-facing strings are translatable.
- Updated error and success messages to use translation functions, enhancing user experience for non-English speakers.
- Improved UI elements to support localization, including labels, placeholders, and button texts, ensuring consistency across the token editing interface.
2024-12-24 18:40:18 +08:00
CalciumIon
93cda60d44 feat: Add FetchModels endpoint and refactor FetchUpstreamModels
- Introduced a new `FetchModels` endpoint to retrieve model IDs from a specified base URL and API key, enhancing flexibility for different channel types.
- Refactored `FetchUpstreamModels` to simplify base URL handling and improve error messages during response parsing.
- Updated API routes to include the new endpoint and adjusted the frontend to utilize the new fetch mechanism for model lists.
- Removed outdated checks for channel type in the frontend, streamlining the model fetching process.
2024-12-24 18:02:08 +08:00
CalciumIon
2ec5eafbce feat: Enhance LogsTable component with mobile support and date handling improvements
- Added mobile-specific date pickers for start and end timestamps in the LogsTable component, improving user experience on mobile devices.
- Updated the input handling for date values to ensure valid date formats are maintained.
- Introduced a new translation key for "时间范围" (Time range) in the English locale file to support localization efforts.
2024-12-24 15:44:11 +08:00
CalciumIon
be0c240e97 Merge remote-tracking branch 'origin/main' 2024-12-24 14:48:43 +08:00
CalciumIon
7180e6f114 feat: Enhance logging functionality with group support
- Added a new 'group' parameter to various logging functions, including RecordConsumeLog, GetAllLogs, and GetUserLogs, to allow for more granular log tracking.
- Updated the logs table component to display group information, improving the visibility of log data.
- Refactored related functions to accommodate the new group parameter, ensuring consistent handling across the application.
- Improved the initialization of the group column for PostgreSQL compatibility.
2024-12-24 14:48:11 +08:00
Calcium-Ion
61495a460a Merge pull request #652 from Yan-Zero/main
fix: mutil func call in gemini
2024-12-23 20:50:31 +08:00
CalciumIon
cf3287a10a Merge remote-tracking branch 'origin/main' 2024-12-23 20:48:31 +08:00
CalciumIon
f3f1817aea feat: Add request start time context key and update middleware
- Introduced a new constant `ContextKeyRequestStartTime` to store the request start time in the context, enhancing request tracking.
- Updated the `Distribute` middleware to set the request start time in the context using the new constant.
- Modified the `GenRelayInfo` function to retrieve the request start time from the context, ensuring accurate timing information is used in relay operations.
2024-12-23 20:48:10 +08:00
Yan
a4795737fe fix: mutil func call in gemini 2024-12-23 01:26:14 +08:00
Calcium-Ion
eec8f523ce Merge pull request #651 from tenacioustommy/fix-gemini-json
fix-gemini-json-schema
2024-12-23 00:04:08 +08:00
CalciumIon
58fac129d6 feat: Enhance GeminiChatHandler to include RelayInfo
- Updated the GeminiChatHandler function to accept an additional parameter, RelayInfo, allowing for better context handling during chat operations.
- Modified the DoResponse method in the Adaptor to pass RelayInfo to GeminiChatHandler, ensuring consistent usage of upstream model information.
- Enhanced the GeminiChatStreamHandler to utilize the upstream model name from RelayInfo, improving response accuracy and data representation in Gemini requests.
2024-12-23 00:02:15 +08:00
CalciumIon
241c9389ef refactor: Remove unused context and logging in CovertGemini2OpenAI function
- Eliminated the unused `context` import and the logging of `geminiRequest` in the `CovertGemini2OpenAI` function, improving code cleanliness and reducing unnecessary overhead.
- This change enhances the maintainability of the code by removing redundant elements that do not contribute to functionality.
2024-12-22 23:54:11 +08:00
CalciumIon
1d0ef89ce9 feat: Add FunctionResponse type and enhance GeminiPart structure
- Introduced a new `FunctionResponse` type to encapsulate function call responses, improving the clarity of data handling.
- Updated the `GeminiPart` struct to include the new `FunctionResponse` field, allowing for better representation of function call results in Gemini requests.
- Modified the `CovertGemini2OpenAI` function to handle tool calls more effectively by setting the message role and appending function responses to the Gemini parts, enhancing the integration with OpenAI and Gemini systems.
2024-12-22 23:53:25 +08:00
tenacious
cce2990db6 fix-gemini-json 2024-12-22 23:48:09 +08:00
CalciumIon
a7e1d17c3e feat: Introduce settings package and refactor constants
- Added a new `setting` package to replace the `constant` package for configuration management, improving code organization and clarity.
- Moved various configuration variables such as `ServerAddress`, `PayAddress`, and `SensitiveWords` to the new `setting` package.
- Updated references throughout the codebase to use the new `setting` package, ensuring consistent access to configuration values.
- Introduced new files for managing chat settings and midjourney settings, enhancing modularity and maintainability of the code.
2024-12-22 17:24:29 +08:00
CalciumIon
c4e256e69b refactor: Update Message methods to use pointer receivers
- Refactored ParseToolCalls, SetToolCalls, IsStringContent, and ParseContent methods in the Message struct to use pointer receivers, improving efficiency and consistency in handling mutable state.
- Enhanced code readability and maintainability by ensuring all relevant methods operate on the pointer receiver, aligning with Go best practices.
2024-12-22 16:30:18 +08:00
CalciumIon
87a5e40daf refactor: Update SetToolCalls method to use pointer receiver
- Changed the SetToolCalls method to use a pointer receiver for the Message struct, allowing for modifications to the original instance.
- This change improves the method's efficiency and aligns with Go best practices for mutating struct methods.
2024-12-22 16:22:55 +08:00
CalciumIon
0c326556aa refactor: Update OpenAI request and message handling
- Changed the type of ToolCalls in the Message struct from `any` to `json.RawMessage` for better type safety and clarity.
- Introduced ParseToolCalls and SetToolCalls methods to handle ToolCalls more effectively, improving code readability and maintainability.
- Updated the ParseContent method to work with the new MediaContent type instead of MediaMessage, enhancing the structure of content parsing.
- Refactored Gemini relay functions to utilize the new ToolCalls handling methods, streamlining the integration with OpenAI and Gemini systems.
2024-12-22 16:20:30 +08:00
Calcium-Ion
794f6a6e34 Merge pull request #648 from palboss/main
解决  #534 用户管理-管理用户-查询报错 (SQLSTATE 42601)-postgresql
2024-12-22 14:37:34 +08:00
CalciumIon
656e809202 refactor: Simplify Gemini function parameter handling
- Removed redundant checks for non-empty properties in function parameters.
- Set function parameters to nil when no properties are needed, streamlining the logic for handling Gemini requests.
- Improved code clarity and maintainability by eliminating unnecessary complexity.
2024-12-22 14:35:21 +08:00
CalciumIon
53ab2aaee4 feat: Enhance Gemini function parameter handling
- Added logic to ensure that function parameters have non-empty properties.
- Implemented checks to add a default empty property if no parameters are needed.
- Updated the required field to match existing properties, improving the robustness of the Gemini function integration.
2024-12-22 14:29:14 +08:00
borland
a02bc3342f Update user.go 2024-12-22 00:03:00 +08:00
borland
f54d0cb3b0 Update user.go 2024-12-22 00:02:28 +08:00
CalciumIon
a5c48c2772 feat: Enhance LogsTable to render group information
- Added `renderGroup` function to improve the display of log data by rendering the 'group' information in the LogsTable component.
- Updated the rendering logic to utilize the new function, enhancing the UI's clarity and usability for grouped logs.
2024-12-21 20:28:26 +08:00
CalciumIon
cffaf0d636 feat: Add log information generation and enhance LogsTable component
- Introduced `log_info_generate.go` to implement functions for generating various log information, including text, WebSocket, and audio details.
- Enhanced `LogsTable` component to display the 'group' information from the log data, improving the visibility of grouped logs in the UI.
2024-12-21 20:24:22 +08:00
CalciumIon
865b98a454 Merge branch 'feat/o1'
# Conflicts:
#	dto/openai_request.go
2024-12-21 16:45:45 +08:00
Calcium-Ion
5bdbf3a673 Merge pull request #645 from MartialBE/gemini_res_format
支持gemini结构化输出
2024-12-21 16:40:51 +08:00
MartialBE
43a7b59b68 feat: support for Gemini structured output. 2024-12-21 16:01:17 +08:00
HynoR
eac3463401 Merge remote-tracking branch 'origin/feat/o1' into feat/o1 2024-12-20 23:14:20 +08:00
HynoR
1fa478af20 feat: 适配o1模型 2024-12-20 23:14:10 +08:00
TAKO
3b58b4989d Merge branch 'Calcium-Ion:main' into feat/o1 2024-12-20 22:12:26 +08:00
HynoR
0b1ba2eeb9 feat: 适配o1模型 2024-12-20 22:09:02 +08:00
HynoR
35277f2b4a feat: 适配o1模型 2024-12-20 22:07:53 +08:00
CalciumIon
03256dbdad refactor: Enhance error handling in Gemini request conversion
- Updated `CovertGemini2OpenAI` function to return an error alongside the GeminiChatRequest, improving error reporting for image processing.
- Modified `ConvertRequest` methods in both `adaptor.go` files to handle potential errors from the Gemini conversion, ensuring robust request handling.
- Improved clarity and maintainability of the code by explicitly managing error cases during request conversion.
2024-12-20 21:50:58 +08:00
CalciumIon
f9a7f6085e feat: Add GEMINI_VISION_MAX_IMAGE_NUM configuration
- Introduced `GEMINI_VISION_MAX_IMAGE_NUM` to README files for better user guidance.
- Updated `env.go` to retrieve the maximum image number from environment variables, defaulting to 16.
- Modified image handling logic in `relay-gemini.go` to respect the new configuration, allowing disabling of the limit by setting it to -1.
- Removed hardcoded constant for maximum image number in `constant.go` to streamline configuration management.
2024-12-20 21:36:23 +08:00
Calcium-Ion
c289694fe8 Merge pull request #636 from HynoR/fix/smfix
fix: 修复添加模型切换模式时,初始化空值导致的判断问题
2024-12-20 20:33:01 +08:00
Calcium-Ion
c37319cb4c Merge pull request #641 from HynoR/main
chore: 更新遗漏的gemini模型
2024-12-20 20:32:47 +08:00
Calcium-Ion
57f036747b Merge pull request #642 from MartialBE/fix_gemini_thinking
fix: 修复gemini thinking在stream下会内容丢失
2024-12-20 20:32:23 +08:00
MartialBE
158edab974 fix: Fix the issue where Gemini loses content when converting OpenAI format in the stream. 2024-12-20 20:24:49 +08:00
HynoR
f3a7119745 chore: 更新gemini模型 2024-12-20 19:17:56 +08:00
HynoR
3fa1d93b9c chore: 更新gemini模型 2024-12-20 19:14:53 +08:00
TAKO
5e3ba45748 Merge branch 'Calcium-Ion:main' into fix/smfix 2024-12-20 19:11:32 +08:00
CalciumIon
cab30014ad feat: Add new experimental Gemini versions to ModelList
- Included additional versions: "gemini-2.0-flash-thinking-exp" and "gemini-2.0-flash-thinking-exp-1219".
- Added comments to categorize versions as old, experimental, and flash experimental for better clarity.
2024-12-20 13:26:51 +08:00
CalciumIon
9a54b345c5 feat: support gemini-2.0-flash-thinking #639 #637 2024-12-20 13:20:07 +08:00
HynoR
e3a0162206 fix: 修复添加模型切换模式时,初始化空值导致的判断问题 2024-12-19 19:08:04 +08:00
CalciumIon
2d865eb735 refactor: Improve channel status update logic and clean up code
- Simplified conditional checks in UpdateChannelStatusById function in channel.go to enhance readability.
- Commented out unused image number check in relay-gemini.go for clarity.
- Updated JSON field in en.json for currency consistency, changing "元" from "RMB/CNY" to "CNY" and added a space in "实付金额:" for formatting.
2024-12-17 15:33:16 +08:00
Calcium-Ion
70e7860838 Merge pull request #631 from xqx333/main
fix
2024-12-17 14:39:39 +08:00
xqx333
1f4fc2d590 Update channel.go 2024-12-17 14:30:31 +08:00
Calcium-Ion
c76021e9a1 Merge pull request #630 from xqx333/main
修复自动禁用会对数据库进行多次更新的问题
2024-12-17 12:22:27 +08:00
xqx333
173c9bc669 Update channel.go 2024-12-17 12:11:24 +08:00
xqx333
739c0cc334 Update cache.go 2024-12-17 12:10:05 +08:00
CalciumIon
4163713714 refactor: Update SystemInstructions type in GeminiChatRequest and adjust handling in CovertGemini2OpenAI
- Changed SystemInstructions from *GeminiPart to *GeminiChatContent in GeminiChatRequest for improved structure.
- Updated CovertGemini2OpenAI function to accommodate the new SystemInstructions type, ensuring proper handling of message content.
2024-12-16 22:41:23 +08:00
CalciumIon
76acfdc9e2 fix: Correct JSON field name for SystemInstructions in GeminiChatRequest
- Updated the JSON field name from "system_instructions" to "system_instruction" to ensure consistency and accuracy in the data structure.
2024-12-16 22:12:56 +08:00
CalciumIon
fe6f3d7978 feat: Enhance Home component to support language messaging
- Added language messaging functionality to the iframe in the Home component.
- This update ensures that the iframe receives the current language setting, improving localization support.
2024-12-16 21:10:46 +08:00
Calcium-Ion
1f70904fc5 Merge pull request #628 from Calcium-Ion/pr482-merge
merge 428
2024-12-16 21:05:58 +08:00
CalciumIon
fd86de19b5 feat: Enhance HeaderBar to support language change messaging
- Added functionality to post a message to the iframe when the language is changed.
- This update improves localization support by ensuring that the iframe content updates according to the selected language.
2024-12-16 21:05:02 +08:00
CalciumIon
c8f437c13a Merge remote-tracking branch 'guoruqiang/main' into pr482-merge
# Conflicts:
#	README.md
#	web/src/components/HeaderBar.js
#	web/src/components/SiderBar.js
2024-12-16 20:56:53 +08:00
CalciumIon
e6c6bbdef9 feat: support gemini SystemInstructions #408 2024-12-16 20:19:29 +08:00
CalciumIon
aab3887d2c Update README.md 2024-12-16 18:12:17 +08:00
Calcium-Ion
25ab1d400f Merge pull request #625 from QAbot-zh/fix/bindwechat
fix bindWeChat tips
2024-12-15 20:59:47 +08:00
Q.A.zh
6c995d4bd9 fix bindWeChat tips 2024-12-15 12:53:16 +00:00
Calcium-Ion
fb9f50ce79 Merge pull request #624 from Calcium-Ion/channel-setting
feat: Add collapsible section for available models in PersonalSettings
2024-12-15 16:32:09 +08:00
CalciumIon
e8444edce7 feat: Add collapsible section for available models in PersonalSettings 2024-12-15 16:31:00 +08:00
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
G.RQ
b0d655e2b9 Merge branch 'Calcium-Ion:main' into main 2024-09-25 14:40:51 +08:00
GuoRuqiang
e4ccaddf4a 撤回修改 2024-09-25 06:39:32 +00:00
GuoRuqiang
b139588aa1 更新LoginForm 提示 2024-09-25 06:38:17 +00:00
G.RQ
ee17e307f0 Update README.md
增加主要变更说明15. 支持使用路由/chat2link 进入聊天界面
2024-09-22 23:09:45 +08:00
GuoRuqiang
574d7a0914 使用postMessage向iframe传参theme-mode,实现切换子页面主题的功能
子页面的js示例
```
<script>
    // 接收父页面的主题模式
    window.addEventListener('message', function(event) {
        if (event.data.themeMode) {
            var theme = event.data.themeMode;
            // 测试是否正确接受到theme-mode的值
            // console.log('Received theme mode from parent:', theme);
            applyTheme(theme);
        }
    });

    // 定义一个函数来应用主题
    function applyTheme(theme) {
        var body = document.body;
        if (theme === 'dark') {
            body.classList.add("dark-mode");
            document.getElementById("darkModeToggle").checked = true;
        } else {
            body.classList.remove("dark-mode");
            document.getElementById("darkModeToggle").checked = false;
        }
    }
</script>
```
2024-09-22 14:09:03 +00:00
GuoRuqiang
edce1f7046 手动合并upstream 2024-09-22 14:04:15 +00:00
GuoRuqiang
bab718e9bc 聊天按钮适配移动端 2024-09-20 04:45:33 +00:00
G.RQ
509a1a0bac Merge branch 'Calcium-Ion:main' into main 2024-09-19 15:19:46 +08:00
GuoRuqiang
0b2ae561bf Merge branch 'Calcium-Ion:main' into main 2024-09-18 20:53:24 +08:00
GuoRuqiang
016e071d50 update HeaderBar 2024-09-18 10:29:25 +00:00
141 changed files with 10726 additions and 5728 deletions

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
.github
.git
*.md
.vscode
.gitignore
Makefile

2
.gitignore vendored
View File

@@ -8,3 +8,5 @@ build
logs
web/dist
.env
one-api
.DS_Store

151
README.en.md Normal file
View File

@@ -0,0 +1,151 @@
<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`
- `GEMINI_VISION_MAX_IMAGE_NUM`: Gemini model maximum image number, default `16`, set to `-1` to disable
## 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)

112
README.md
View File

@@ -1,68 +1,79 @@
<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)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
> [!TIP]
> 最新版Docker镜像`calciumion/new-api:latest`
> 默认账号root 密码123456
> 更新指令:
> ```
> 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. 🎨 全新的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渠道
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渠道
16. 支持使用路由/chat2link 进入聊天界面
## 模型支持
此版本额外支持以下模型:
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
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无法调用。
@@ -76,7 +87,16 @@
- `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`
- `GEMINI_VISION_MAX_IMAGE_NUM`Gemini模型最大图片数量默认为 `16`,设置为 `-1` 则不限制。
## 部署
> [!TIP]
> 最新版Docker镜像`calciumion/new-api:latest`
> 默认账号root 密码123456
> 更新指令:
> ```
> docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR
> ```
### 部署要求
- 本地数据库默认SQLiteDocker 部署默认使用 SQLite必须挂载 `/data` 目录到宿主机)
- 远程数据库MySQL 版本 >= 5.7.8PgSQL 版本 >= 9.6
@@ -152,6 +172,10 @@ 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
其他基于New API的项目
- [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon)New API高性能优化版并支持Claude格式
- [VoAPI](https://github.com/VoAPI/VoAPI)基于New API的闭源项目
## 🌟 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://api.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

@@ -46,6 +46,8 @@ var defaultModelRatio = map[string]float64{
"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": 7.5,
"o1-2024-12-17": 7.5,
"o1-preview": 7.5,
"o1-preview-2024-09-12": 7.5,
"o1-mini": 1.5,
@@ -354,7 +356,7 @@ func GetCompletionRatio(name string) float64 {
}
return 2
}
if strings.HasPrefix(name, "o1-") {
if strings.HasPrefix(name, "o1") {
return 4
}
if name == "chatgpt-4o-latest" {

View File

@@ -1,46 +0,0 @@
package common
import (
"encoding/json"
)
var UserUsableGroups = map[string]string{
"default": "默认分组",
"vip": "vip分组",
}
func UserUsableGroups2JSONString() string {
jsonBytes, err := json.Marshal(UserUsableGroups)
if err != nil {
SysError("error marshalling user groups: " + err.Error())
}
return string(jsonBytes)
}
func UpdateUserUsableGroupsByJSONString(jsonStr string) error {
UserUsableGroups = make(map[string]string)
return json.Unmarshal([]byte(jsonStr), &UserUsableGroups)
}
func GetUserUsableGroups(userGroup string) map[string]string {
if userGroup == "" {
// 如果userGroup为空返回UserUsableGroups
return UserUsableGroups
}
// 如果userGroup不在UserUsableGroups中返回UserUsableGroups + userGroup
if _, ok := UserUsableGroups[userGroup]; !ok {
appendUserUsableGroups := make(map[string]string)
for k, v := range UserUsableGroups {
appendUserUsableGroups[k] = v
}
appendUserUsableGroups[userGroup] = "用户分组"
return appendUserUsableGroups
}
// 如果userGroup在UserUsableGroups中返回UserUsableGroups
return UserUsableGroups
}
func GroupInUserUsableGroups(groupName string) bool {
_, ok := UserUsableGroups[groupName]
return ok
}

View File

@@ -4,7 +4,6 @@ import (
crand "crypto/rand"
"encoding/base64"
"fmt"
"github.com/google/uuid"
"html/template"
"log"
"math/big"
@@ -15,6 +14,8 @@ import (
"strconv"
"strings"
"time"
"github.com/google/uuid"
)
func OpenBrowser(url string) {

View File

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

5
constant/context_key.go Normal file
View File

@@ -0,0 +1,5 @@
package constant
const (
ContextKeyRequestStartTime = "request_start_time"
)

View File

@@ -23,6 +23,8 @@ var GeminiModelMap = map[string]string{
"gemini-1.0-pro": "v1",
}
var GeminiVisionMaxImageNum = common.GetEnvOrDefault("GEMINI_VISION_MAX_IMAGE_NUM", 16)
func InitEnv() {
modelVersionMapStr := strings.TrimSpace(os.Getenv("GEMINI_MODEL_MAP"))
if modelVersionMapStr == "" {

View File

@@ -0,0 +1,9 @@
package constant
var (
FinishReasonStop = "stop"
FinishReasonToolCalls = "tool_calls"
FinishReasonLength = "length"
FinishReasonFunctionCall = "function_call"
FinishReasonContentFilter = "content_filter"
)

View File

@@ -1,11 +1,5 @@
package constant
var MjNotifyEnabled = false
var MjAccountFilterEnabled = false
var MjModeClearEnabled = false
var MjForwardUrlEnabled = true
var MjActionCheckSuccessEnabled = true
const (
MjErrorUnknown = 5
MjRequestError = 4

View File

@@ -141,7 +141,8 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
milliseconds := tok.Sub(tik).Milliseconds()
consumedTime := float64(milliseconds) / 1000.0
other := service.GenerateTextOtherInfo(c, meta, modelRatio, 1, completionRatio, modelPrice)
model.RecordConsumeLog(c, 1, channel.Id, usage.PromptTokens, usage.CompletionTokens, testModel, "模型测试", quota, "模型测试", 0, quota, int(consumedTime), false, other)
model.RecordConsumeLog(c, 1, channel.Id, usage.PromptTokens, usage.CompletionTokens, testModel, "模型测试",
quota, "模型测试", 0, quota, int(consumedTime), false, "default", other)
common.SysLog(fmt.Sprintf("testing channel #%d, response: \n%s", channel.Id, string(respBody)))
return nil, nil
}
@@ -151,8 +152,10 @@ func buildTestRequest(model string) *dto.GeneralOpenAIRequest {
Model: "", // this will be set later
Stream: false,
}
if strings.HasPrefix(model, "o1-") {
testRequest.MaxCompletionTokens = 1
if strings.HasPrefix(model, "o1") {
testRequest.MaxCompletionTokens = 10
} else if strings.HasPrefix(model, "gemini-2.0-flash-thinking") {
testRequest.MaxTokens = 2
} else {
testRequest.MaxTokens = 1
}

View File

@@ -63,7 +63,7 @@ func GetAllChannels(c *gin.Context) {
}
for _, tag := range tags {
if tag != nil && *tag != "" {
tagChannel, err := model.GetChannelsByTag(*tag)
tagChannel, err := model.GetChannelsByTag(*tag, idSort)
if err == nil {
channelData = append(channelData, tagChannel...)
}
@@ -97,6 +97,7 @@ func FetchUpstreamModels(c *gin.Context) {
})
return
}
channel, err := model.GetChannelById(id, true)
if err != nil {
c.JSON(http.StatusOK, gin.H{
@@ -105,34 +106,35 @@ func FetchUpstreamModels(c *gin.Context) {
})
return
}
if channel.Type != common.ChannelTypeOpenAI {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "仅支持 OpenAI 类型渠道",
})
return
//if channel.Type != common.ChannelTypeOpenAI {
// c.JSON(http.StatusOK, gin.H{
// "success": false,
// "message": "仅支持 OpenAI 类型渠道",
// })
// return
//}
baseURL := common.ChannelBaseURLs[channel.Type]
if channel.GetBaseURL() != "" {
baseURL = channel.GetBaseURL()
}
url := fmt.Sprintf("%s/v1/models", *channel.BaseURL)
url := fmt.Sprintf("%s/v1/models", baseURL)
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
result := OpenAIModelsResponse{}
err = json.Unmarshal(body, &result)
if err != nil {
var result OpenAIModelsResponse
if err = json.Unmarshal(body, &result); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
}
if !result.Success {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "上游返回错误",
"message": fmt.Sprintf("解析响应失败: %s", err.Error()),
})
return
}
var ids []string
@@ -168,18 +170,40 @@ func SearchChannels(c *gin.Context) {
group := c.Query("group")
modelKeyword := c.Query("model")
idSort, _ := strconv.ParseBool(c.Query("id_sort"))
channels, err := model.SearchChannels(keyword, group, modelKeyword, idSort)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
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
}
@@ -395,7 +419,8 @@ func EditTagChannels(c *gin.Context) {
}
type ChannelBatch struct {
Ids []int `json:"ids"`
Ids []int `json:"ids"`
Tag *string `json:"tag"`
}
func DeleteChannelBatch(c *gin.Context) {
@@ -470,3 +495,105 @@ func UpdateChannel(c *gin.Context) {
})
return
}
func FetchModels(c *gin.Context) {
var req struct {
BaseURL string `json:"base_url"`
Key string `json:"key"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid request",
})
return
}
baseURL := req.BaseURL
if baseURL == "" {
baseURL = "https://api.openai.com"
}
client := &http.Client{}
url := fmt.Sprintf("%s/v1/models", baseURL)
request, err := http.NewRequest("GET", url, nil)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": err.Error(),
})
return
}
request.Header.Set("Authorization", "Bearer "+req.Key)
response, err := client.Do(request)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": err.Error(),
})
return
}
//check status code
if response.StatusCode != http.StatusOK {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to fetch models",
})
return
}
defer response.Body.Close()
var result struct {
Data []struct {
ID string `json:"id"`
} `json:"data"`
}
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": err.Error(),
})
return
}
var models []string
for _, model := range result.Data {
models = append(models, model.ID)
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": models,
})
}
func BatchSetChannelTag(c *gin.Context) {
channelBatch := ChannelBatch{}
err := c.ShouldBindJSON(&channelBatch)
if err != nil || len(channelBatch.Ids) == 0 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "参数错误",
})
return
}
err = model.BatchSetChannelTag(channelBatch.Ids, channelBatch.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": "",
"data": len(channelBatch.Ids),
})
return
}

View File

@@ -3,13 +3,13 @@ package controller
import (
"github.com/gin-gonic/gin"
"net/http"
"one-api/common"
"one-api/model"
"one-api/setting"
)
func GetGroups(c *gin.Context) {
groupNames := make([]string, 0)
for groupName, _ := range common.GroupRatio {
for groupName, _ := range setting.GetGroupRatioCopy() {
groupNames = append(groupNames, groupName)
}
c.JSON(http.StatusOK, gin.H{
@@ -24,9 +24,9 @@ func GetUserGroups(c *gin.Context) {
userGroup := ""
userId := c.GetInt("id")
userGroup, _ = model.CacheGetUserGroup(userId)
for groupName, _ := range common.GroupRatio {
for groupName, _ := range setting.GetGroupRatioCopy() {
// UserUsableGroups contains the groups that the user can use
userUsableGroups := common.GetUserUsableGroups(userGroup)
userUsableGroups := setting.GetUserUsableGroups(userGroup)
if _, ok := userUsableGroups[groupName]; ok {
usableGroups[groupName] = userUsableGroups[groupName]
}

View File

@@ -25,7 +25,8 @@ func GetAllLogs(c *gin.Context) {
tokenName := c.Query("token_name")
modelName := c.Query("model_name")
channel, _ := strconv.Atoi(c.Query("channel"))
logs, total, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, tokenName, (p-1)*pageSize, pageSize, channel)
group := c.Query("group")
logs, total, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, tokenName, (p-1)*pageSize, pageSize, channel, group)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
@@ -63,7 +64,8 @@ func GetUserLogs(c *gin.Context) {
endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
tokenName := c.Query("token_name")
modelName := c.Query("model_name")
logs, total, err := model.GetUserLogs(userId, logType, startTimestamp, endTimestamp, modelName, tokenName, (p-1)*pageSize, pageSize)
group := c.Query("group")
logs, total, err := model.GetUserLogs(userId, logType, startTimestamp, endTimestamp, modelName, tokenName, (p-1)*pageSize, pageSize, group)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
@@ -146,7 +148,8 @@ func GetLogsStat(c *gin.Context) {
username := c.Query("username")
modelName := c.Query("model_name")
channel, _ := strconv.Atoi(c.Query("channel"))
stat := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName, channel)
group := c.Query("group")
stat := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName, channel, group)
//tokenNum := model.SumUsedToken(logType, startTimestamp, endTimestamp, modelName, username, "")
c.JSON(http.StatusOK, gin.H{
"success": true,
@@ -168,7 +171,8 @@ func GetLogsSelfStat(c *gin.Context) {
tokenName := c.Query("token_name")
modelName := c.Query("model_name")
channel, _ := strconv.Atoi(c.Query("channel"))
quotaNum := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName, channel)
group := c.Query("group")
quotaNum := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName, channel, group)
//tokenNum := model.SumUsedToken(logType, startTimestamp, endTimestamp, modelName, username, tokenName)
c.JSON(200, gin.H{
"success": true,

View File

@@ -10,10 +10,10 @@ import (
"log"
"net/http"
"one-api/common"
"one-api/constant"
"one-api/dto"
"one-api/model"
"one-api/service"
"one-api/setting"
"strconv"
"time"
)
@@ -231,9 +231,9 @@ func GetAllMidjourney(c *gin.Context) {
if logs == nil {
logs = make([]*model.Midjourney, 0)
}
if constant.MjForwardUrlEnabled {
if setting.MjForwardUrlEnabled {
for i, midjourney := range logs {
midjourney.ImageUrl = constant.ServerAddress + "/mj/image/" + midjourney.MjId
midjourney.ImageUrl = setting.ServerAddress + "/mj/image/" + midjourney.MjId
logs[i] = midjourney
}
}
@@ -263,9 +263,9 @@ func GetUserMidjourney(c *gin.Context) {
if logs == nil {
logs = make([]*model.Midjourney, 0)
}
if constant.MjForwardUrlEnabled {
if setting.MjForwardUrlEnabled {
for i, midjourney := range logs {
midjourney.ImageUrl = constant.ServerAddress + "/mj/image/" + midjourney.MjId
midjourney.ImageUrl = setting.ServerAddress + "/mj/image/" + midjourney.MjId
logs[i] = midjourney
}
}

View File

@@ -5,8 +5,8 @@ import (
"fmt"
"net/http"
"one-api/common"
"one-api/constant"
"one-api/model"
"one-api/setting"
"strings"
"github.com/gin-gonic/gin"
@@ -47,9 +47,9 @@ func GetStatus(c *gin.Context) {
"footer_html": common.Footer,
"wechat_qrcode": common.WeChatAccountQRCodeImageURL,
"wechat_login": common.WeChatAuthEnabled,
"server_address": constant.ServerAddress,
"price": constant.Price,
"min_topup": constant.MinTopUp,
"server_address": setting.ServerAddress,
"price": setting.Price,
"min_topup": setting.MinTopUp,
"turnstile_check": common.TurnstileCheckEnabled,
"turnstile_site_key": common.TurnstileSiteKey,
"top_up_link": common.TopUpLink,
@@ -63,9 +63,9 @@ func GetStatus(c *gin.Context) {
"enable_data_export": common.DataExportEnabled,
"data_export_default_time": common.DataExportDefaultTime,
"default_collapse_sidebar": common.DefaultCollapseSidebar,
"enable_online_topup": constant.PayAddress != "" && constant.EpayId != "" && constant.EpayKey != "",
"mj_notify_enabled": constant.MjNotifyEnabled,
"chats": constant.Chats,
"enable_online_topup": setting.PayAddress != "" && setting.EpayId != "" && setting.EpayKey != "",
"mj_notify_enabled": setting.MjNotifyEnabled,
"chats": setting.Chats,
},
})
return
@@ -207,7 +207,7 @@ func SendPasswordResetEmail(c *gin.Context) {
}
code := common.GenerateVerificationCode(0)
common.RegisterVerificationCodeWithKey(email, code, common.PasswordResetPurpose)
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", constant.ServerAddress, email, code)
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", setting.ServerAddress, email, code)
subject := fmt.Sprintf("%s密码重置", common.SystemName)
content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+
"<p>点击 <a href='%s'>此处</a> 进行密码重置。</p>"+

View File

@@ -5,6 +5,7 @@ import (
"net/http"
"one-api/common"
"one-api/model"
"one-api/setting"
"strings"
"github.com/gin-gonic/gin"
@@ -83,7 +84,7 @@ func UpdateOption(c *gin.Context) {
return
}
case "GroupRatio":
err = common.CheckGroupRatio(option.Value)
err = setting.CheckGroupRatio(option.Value)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,

View File

@@ -4,14 +4,38 @@ import (
"github.com/gin-gonic/gin"
"one-api/common"
"one-api/model"
"one-api/setting"
)
func GetPricing(c *gin.Context) {
pricing := model.GetPricing()
userId, exists := c.Get("id")
usableGroup := map[string]string{}
groupRatio := map[string]float64{}
for s, f := range setting.GetGroupRatioCopy() {
groupRatio[s] = f
}
var group string
if exists {
user, err := model.GetUserById(userId.(int), false)
if err == nil {
group = user.Group
}
}
usableGroup = setting.GetUserUsableGroups(group)
// check groupRatio contains usableGroup
for group := range setting.GetGroupRatioCopy() {
if _, ok := usableGroup[group]; !ok {
delete(groupRatio, group)
}
}
c.JSON(200, gin.H{
"success": true,
"data": pricing,
"group_ratio": common.GroupRatio,
"success": true,
"data": pricing,
"group_ratio": groupRatio,
"usable_group": usableGroup,
})
}

View File

@@ -17,6 +17,7 @@ import (
"one-api/relay/constant"
relayconstant "one-api/relay/constant"
"one-api/service"
"one-api/setting"
"strings"
)
@@ -83,7 +84,7 @@ func Playground(c *gin.Context) {
if group == "" {
group = userGroup
} else {
if !common.GroupInUserUsableGroups(group) && group != userGroup {
if !setting.GroupInUserUsableGroups(group) && group != userGroup {
openaiErr = service.OpenAIErrorWrapperLocal(errors.New("无权访问该分组"), "group_not_allowed", http.StatusForbidden)
return
}

View File

@@ -8,9 +8,9 @@ import (
"log"
"net/url"
"one-api/common"
"one-api/constant"
"one-api/model"
"one-api/service"
"one-api/setting"
"strconv"
"sync"
"time"
@@ -28,13 +28,13 @@ type AmountRequest struct {
}
func GetEpayClient() *epay.Client {
if constant.PayAddress == "" || constant.EpayId == "" || constant.EpayKey == "" {
if setting.PayAddress == "" || setting.EpayId == "" || setting.EpayKey == "" {
return nil
}
withUrl, err := epay.NewClient(&epay.Config{
PartnerID: constant.EpayId,
Key: constant.EpayKey,
}, constant.PayAddress)
PartnerID: setting.EpayId,
Key: setting.EpayKey,
}, setting.PayAddress)
if err != nil {
return nil
}
@@ -50,12 +50,12 @@ func getPayMoney(amount float64, group string) float64 {
if topupGroupRatio == 0 {
topupGroupRatio = 1
}
payMoney := amount * constant.Price * topupGroupRatio
payMoney := amount * setting.Price * topupGroupRatio
return payMoney
}
func getMinTopup() int {
minTopup := constant.MinTopUp
minTopup := setting.MinTopUp
if !common.DisplayInCurrencyEnabled {
minTopup = minTopup * int(common.QuotaPerUnit)
}
@@ -85,17 +85,16 @@ 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")
returnUrl, _ := url.Parse(setting.ServerAddress + "/log")
notifyUrl, _ := url.Parse(callBackAddress + "/api/user/epay/notify")
tradeNo := fmt.Sprintf("%s%d", common.GetRandomString(6), time.Now().Unix())
tradeNo = fmt.Sprintf("USR%dNO%s", id, tradeNo)

View File

@@ -6,6 +6,7 @@ import (
"net/http"
"one-api/common"
"one-api/model"
"one-api/setting"
"strconv"
"strings"
"sync"
@@ -454,7 +455,15 @@ func GetUserModels(c *gin.Context) {
})
return
}
models := model.GetGroupModels(user.Group)
groups := setting.GetUserUsableGroups(user.Group)
var models []string
for group := range groups {
for _, g := range model.GetGroupModels(group) {
if !common.StringsContains(models, g) {
models = append(models, g)
}
}
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",

View File

@@ -3,39 +3,48 @@ package dto
import "encoding/json"
type ResponseFormat struct {
Type string `json:"type,omitempty"`
Type string `json:"type,omitempty"`
JsonSchema *FormatJsonSchema `json:"json_schema,omitempty"`
}
type FormatJsonSchema struct {
Description string `json:"description,omitempty"`
Name string `json:"name"`
Schema any `json:"schema,omitempty"`
Strict any `json:"strict,omitempty"`
}
type GeneralOpenAIRequest struct {
Model string `json:"model,omitempty"`
Messages []Message `json:"messages,omitempty"`
Prompt any `json:"prompt,omitempty"`
Stream bool `json:"stream,omitempty"`
StreamOptions *StreamOptions `json:"stream_options,omitempty"`
MaxTokens uint `json:"max_tokens,omitempty"`
MaxCompletionTokens uint `json:"max_completion_tokens,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
TopP float64 `json:"top_p,omitempty"`
TopK int `json:"top_k,omitempty"`
Stop any `json:"stop,omitempty"`
N int `json:"n,omitempty"`
Input any `json:"input,omitempty"`
Instruction string `json:"instruction,omitempty"`
Size string `json:"size,omitempty"`
Functions any `json:"functions,omitempty"`
FrequencyPenalty float64 `json:"frequency_penalty,omitempty"`
PresencePenalty float64 `json:"presence_penalty,omitempty"`
ResponseFormat any `json:"response_format,omitempty"`
EncodingFormat any `json:"encoding_format,omitempty"`
Seed float64 `json:"seed,omitempty"`
Tools []ToolCall `json:"tools,omitempty"`
ToolChoice any `json:"tool_choice,omitempty"`
User string `json:"user,omitempty"`
LogProbs bool `json:"logprobs,omitempty"`
TopLogProbs int `json:"top_logprobs,omitempty"`
Dimensions int `json:"dimensions,omitempty"`
Modalities any `json:"modalities,omitempty"`
Audio any `json:"audio,omitempty"`
Model string `json:"model,omitempty"`
Messages []Message `json:"messages,omitempty"`
Prompt any `json:"prompt,omitempty"`
Stream bool `json:"stream,omitempty"`
StreamOptions *StreamOptions `json:"stream_options,omitempty"`
MaxTokens uint `json:"max_tokens,omitempty"`
MaxCompletionTokens uint `json:"max_completion_tokens,omitempty"`
ReasoningEffort string `json:"reasoning_effort,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
TopP float64 `json:"top_p,omitempty"`
TopK int `json:"top_k,omitempty"`
Stop any `json:"stop,omitempty"`
N int `json:"n,omitempty"`
Input any `json:"input,omitempty"`
Instruction string `json:"instruction,omitempty"`
Size string `json:"size,omitempty"`
Functions any `json:"functions,omitempty"`
FrequencyPenalty float64 `json:"frequency_penalty,omitempty"`
PresencePenalty float64 `json:"presence_penalty,omitempty"`
ResponseFormat *ResponseFormat `json:"response_format,omitempty"`
EncodingFormat any `json:"encoding_format,omitempty"`
Seed float64 `json:"seed,omitempty"`
Tools []ToolCall `json:"tools,omitempty"`
ToolChoice any `json:"tool_choice,omitempty"`
User string `json:"user,omitempty"`
LogProbs bool `json:"logprobs,omitempty"`
TopLogProbs int `json:"top_logprobs,omitempty"`
Dimensions int `json:"dimensions,omitempty"`
Modalities any `json:"modalities,omitempty"`
Audio any `json:"audio,omitempty"`
}
type OpenAITools struct {
@@ -80,11 +89,11 @@ type Message struct {
Role string `json:"role"`
Content json.RawMessage `json:"content"`
Name *string `json:"name,omitempty"`
ToolCalls any `json:"tool_calls,omitempty"`
ToolCalls json.RawMessage `json:"tool_calls,omitempty"`
ToolCallId string `json:"tool_call_id,omitempty"`
}
type MediaMessage struct {
type MediaContent struct {
Type string `json:"type"`
Text string `json:"text"`
ImageUrl any `json:"image_url,omitempty"`
@@ -107,7 +116,23 @@ const (
ContentTypeInputAudio = "input_audio"
)
func (m Message) StringContent() string {
func (m *Message) ParseToolCalls() []ToolCall {
if m.ToolCalls == nil {
return nil
}
var toolCalls []ToolCall
if err := json.Unmarshal(m.ToolCalls, &toolCalls); err == nil {
return toolCalls
}
return toolCalls
}
func (m *Message) SetToolCalls(toolCalls any) {
toolCallsJson, _ := json.Marshal(toolCalls)
m.ToolCalls = toolCallsJson
}
func (m *Message) StringContent() string {
var stringContent string
if err := json.Unmarshal(m.Content, &stringContent); err == nil {
return stringContent
@@ -120,7 +145,7 @@ func (m *Message) SetStringContent(content string) {
m.Content = jsonContent
}
func (m Message) IsStringContent() bool {
func (m *Message) IsStringContent() bool {
var stringContent string
if err := json.Unmarshal(m.Content, &stringContent); err == nil {
return true
@@ -128,11 +153,11 @@ func (m Message) IsStringContent() bool {
return false
}
func (m Message) ParseContent() []MediaMessage {
var contentList []MediaMessage
func (m *Message) ParseContent() []MediaContent {
var contentList []MediaContent
var stringContent string
if err := json.Unmarshal(m.Content, &stringContent); err == nil {
contentList = append(contentList, MediaMessage{
contentList = append(contentList, MediaContent{
Type: ContentTypeText,
Text: stringContent,
})
@@ -148,7 +173,7 @@ func (m Message) ParseContent() []MediaMessage {
switch contentMap["type"] {
case ContentTypeText:
if subStr, ok := contentMap["text"].(string); ok {
contentList = append(contentList, MediaMessage{
contentList = append(contentList, MediaContent{
Type: ContentTypeText,
Text: subStr,
})
@@ -161,7 +186,7 @@ func (m Message) ParseContent() []MediaMessage {
} else {
subObj["detail"] = "high"
}
contentList = append(contentList, MediaMessage{
contentList = append(contentList, MediaContent{
Type: ContentTypeImageURL,
ImageUrl: MessageImageUrl{
Url: subObj["url"].(string),
@@ -169,7 +194,7 @@ func (m Message) ParseContent() []MediaMessage {
},
})
} else if url, ok := contentMap["image_url"].(string); ok {
contentList = append(contentList, MediaMessage{
contentList = append(contentList, MediaContent{
Type: ContentTypeImageURL,
ImageUrl: MessageImageUrl{
Url: url,
@@ -179,7 +204,7 @@ func (m Message) ParseContent() []MediaMessage {
}
case ContentTypeInputAudio:
if subObj, ok := contentMap["input_audio"].(map[string]any); ok {
contentList = append(contentList, MediaMessage{
contentList = append(contentList, MediaContent{
Type: ContentTypeInputAudio,
InputAudio: MessageInputAudio{
Data: subObj["data"].(string),

34
go.mod
View File

@@ -1,24 +1,22 @@
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/bytedance/sonic v1.12.4
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
@@ -29,8 +27,8 @@ require (
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
@@ -43,7 +41,8 @@ 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/loader v0.2.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/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
@@ -61,9 +60,9 @@ 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/json-iterator/go v1.1.12 // indirect
@@ -74,19 +73,18 @@ require (
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.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/sync v0.10.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.17.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
)

64
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,11 +22,10 @@ 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.12.4 h1:9Csb3c9ZJhfUWeMtpCDCq6BUoH5ogfDFLUgQ/jG+R0k=
github.com/bytedance/sonic v1.12.4/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
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/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
github.com/bytedance/sonic/loader v0.2.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/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
@@ -43,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=
@@ -72,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=
@@ -101,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=
@@ -157,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=
@@ -176,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=
@@ -183,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=
@@ -196,25 +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.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=
@@ -231,8 +236,8 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
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=
@@ -265,3 +270,4 @@ 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

@@ -1,529 +0,0 @@
{
"%.6f 额度": "%.6f quota",
"%d 点额度": "%d point quota",
"尚未实现": "Not yet implemented",
"余额不足": "Insufficient balance",
"危险操作": "Hazardous operations",
"输入你的账户名": "Enter your account name",
"确认删除": "Confirm Delete",
"确认绑定": "Confirm Binding",
"您正在删除自己的帐户,将清空所有数据且不可恢复": "You are deleting your account, all data will be cleared and unrecoverable.",
"\"通道「%s」#%d已被禁用\"": "\"Channel %s (#%d) has been disabled\"",
"通道「%s」#%d已被禁用原因%s": "Channel %s (#%d) has been disabled, reason: %s",
"测试已在运行中": "Test is already running",
"响应时间 %.2fs 超过阈值 %.2fs": "Response time %.2fs exceeds threshold %.2fs",
"通道测试完成": "Channel test completed",
"通道测试完成,如果没有收到禁用通知,说明所有通道都正常": "Channel test completed, if you have not received the disable notification, it means that all channels are normal",
"无法连接至 GitHub 服务器,请稍后重试!": "Unable to connect to GitHub server, please try again later!",
"返回值非法,用户字段为空,请稍后重试!": "The return value is illegal, the user field is empty, please try again later!",
"管理员未开启通过 GitHub 登录以及注册": "The administrator did not turn on login and registration via GitHub",
"管理员关闭了新用户注册": "The administrator has turned off new user registration",
"用户已被封禁": "User has been banned",
"该 GitHub 账户已被绑定": "The GitHub account has been bound",
"邮箱地址已被占用": "Email address is occupied",
"%s邮箱验证邮件": "%s Email verification email",
"<p>您好,你正在进行%s邮箱验证。</p>": "<p>Hello, you are verifying %s email.</p>",
"<p>您的验证码为: <strong>%s</strong></p>": "<p>Your verification code is: <strong>%s</strong></p>",
"<p>验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>": "<p>The verification code is valid within %d minutes. If it is not your operation, please ignore it.</p>",
"无效的参数": "Invalid parameter",
"该邮箱地址未注册": "The email address is not registered",
"%s密码重置": "%s Password reset",
"<p>您好,你正在进行%s密码重置。</p>": "<p>Hello, you are resetting %s password.</p>",
"<p>点击<a href='%s'>此处</a>进行密码重置。</p>": "<p>Click <a href='%s'>here</a> to reset your password.</p>",
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>": "<p>The reset link is valid within %d minutes. If it is not your operation, please ignore it.</p>",
"重置链接非法或已过期": "Reset link is illegal or expired",
"无法启用 GitHub OAuth请先填入 GitHub Client ID 以及 GitHub Client Secret": "Unable to enable GitHub OAuth, please fill in GitHub Client ID and GitHub Client Secret first!",
"无法启用微信登录,请先填入微信登录相关配置信息!": "Unable to enable WeChat login, please fill in the relevant configuration information for WeChat login first!",
"无法启用 Turnstile 校验,请先填入 Turnstile 校验相关配置信息!": "Unable to enable Turnstile verification, please fill in the relevant configuration information for Turnstile verification first!",
"兑换码名称长度必须在1-20之间": "The length of the redemption code name must be between 1-20",
"兑换码个数必须大于0": "The number of redemption codes must be greater than 0",
"一次兑换码批量生成的个数不能大于 100": "The number of redemption codes generated in a batch cannot be greater than 100",
"通过令牌「%s」使用模型 %s 消耗 %s模型倍率 %.2f,分组倍率 %.2f": "Using model %s with token %s consumes %s (model rate %.2f, group rate %.2f)",
"当前分组上游负载已饱和,请稍后再试": "The current group load is saturated, please try again later",
"令牌名称过长": "Token name is too long",
"令牌已过期,无法启用,请先修改令牌过期时间,或者设置为永不过期": "The token has expired and cannot be enabled. Please modify the expiration time of the token, or set it to never expire.",
"令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度": "The available quota of the token has been used up and cannot be enabled. Please modify the remaining quota of the token, or set it to unlimited quota",
"管理员关闭了密码登录": "The administrator has turned off password login",
"无法保存会话信息,请重试": "Unable to save session information, please try again",
"管理员关闭了通过密码进行注册,请使用第三方账户验证的形式进行注册": "The administrator has turned off registration via password. Please use the form of third-party account verification to register",
"输入不合法 ": "Input is illegal ",
"管理员开启了邮箱验证,请输入邮箱地址和验证码": "The administrator has turned on email verification, please enter the email address and verification code",
"验证码错误或已过期": "Verification code error or expired",
"无权获取同级或更高等级用户的信息": "No permission to get information of users at the same level or higher",
"请重试,系统生成的 UUID 竟然重复了!": "Please try again, the system-generated UUID is actually duplicated!",
"输入不合法": "Input is illegal",
"无权更新同权限等级或更高权限等级的用户信息": "No permission to update user information with the same permission level or higher permission level",
"管理员将用户额度从 %s修改为 %s": "The administrator changed the user quota from %s to %s",
"无权删除同权限等级或更高权限等级的用户": "No permission to delete users with the same permission level or higher permission level",
"无法创建权限大于等于自己的用户": "Unable to create users with permissions greater than or equal to your own",
"用户不存在": "User does not exist",
"无法禁用超级管理员用户": "Unable to disable super administrator user",
"无法删除超级管理员用户": "Unable to delete super administrator user",
"普通管理员用户无法提升其他用户为管理员": "Ordinary administrator users cannot promote other users to administrators",
"该用户已经是管理员": "The user is already an administrator",
"无法降级超级管理员用户": "Unable to downgrade super administrator user",
"该用户已经是普通用户": "The user is already an ordinary user",
"管理员未开启通过微信登录以及注册": "The administrator has not enabled login and registration via WeChat",
"该微信账号已被绑定": "The WeChat account has been bound",
"无权进行此操作,未登录且未提供 access token": "No permission to perform this operation, not logged in and no access token provided",
"无权进行此操作access token 无效": "No permission to perform this operation, access token is invalid",
"无权进行此操作,权限不足": "No permission to perform this operation, insufficient permissions",
"普通用户不支持指定渠道": "Ordinary users do not support specifying channels",
"无效的渠道 ID": "Invalid channel ID",
"该渠道已被禁用": "The channel has been disabled",
"无效的请求": "Invalid request",
"无可用渠道": "No available channels",
"Turnstile token 为空": "Turnstile token is empty",
"Turnstile 校验失败,请刷新重试!": "Turnstile verification failed, please refresh and try again!",
"id 为空!": "id is empty!",
"未提供兑换码": "No redemption code provided",
"无效的 user id": "Invalid user id",
"无效的兑换码": "Invalid redemption code",
"该兑换码已被使用": "The redemption code has been used",
"通过兑换码充值 %s": "Recharge %s through redemption code",
"未提供令牌": "No token provided",
"该令牌状态不可用": "The token status is not available",
"该令牌已过期": "The token has expired",
"该令牌额度已用尽": "The token quota has been used up",
"无效的令牌": "Invalid token",
"id 或 userId 为空!": "id or userId is empty!",
"quota 不能为负数!": "quota cannot be negative!",
"令牌额度不足": "Insufficient token quota",
"用户额度不足": "Insufficient user quota",
"您的额度即将用尽": "Your quota is about to run out",
"您的额度已用尽": "Your quota has been used up",
"%s当前剩余额度为 %d为了不影响您的使用请及时充值。<br/>充值链接:<a href='%s'>%s</a>": "%s, the current remaining quota is %d, in order not to affect your use, please recharge in time. <br/> Recharge link: <a href='%s'>%s</a>",
"affCode 为空!": "affCode is empty!",
"新用户注册赠送 %s": "New user registration gives %s",
"使用邀请码赠送 %s": "Use invitation code to give %s",
"邀请用户赠送 %s": "Invite users to give %s",
"用户名或密码为空": "Username or password is empty",
"用户名或密码错误,或用户已被封禁": "Username or password is wrong, or user has been banned",
"email 为空!": "email is empty!",
"GitHub id 为空!": "GitHub id is empty!",
"WeChat id 为空!": "WeChat id is empty!",
"username 为空!": "username is empty!",
"邮箱地址或密码为空!": "Email address or password is empty!",
"OpenAI 接口聚合管理,支持多种渠道包括 Azure可用于二次分发管理 key仅单可执行文件已打包好 Docker 镜像,一键部署,开箱即用": "OpenAI interface aggregation management, supports multiple channels including Azure, can be used for secondary distribution management key, only single executable file, Docker image has been packaged, one-click deployment, out of the box",
"未知类型": "Unknown type",
"不支持": "Not supported",
"操作成功完成!": "Operation completed successfully!",
"已启用": "Enabled",
"已禁用": "Disabled",
"未知状态": "Unknown status",
" 秒": "s",
" 分钟 ": " m ",
" 小时 ": " h ",
" 天 ": " d ",
" 个月 ": " M ",
" 年 ": " y ",
"未测试": "Not tested",
"通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。": "Channel ${name} test succeeded, time consumed ${time.toFixed(2)} s.",
"已成功开始测试所有已启用通道,请刷新页面查看结果。": "All enabled channels have been successfully tested, please refresh the page to view the results.",
"通道 ${name} 余额更新成功!": "Channel ${name} balance updated successfully!",
"已更新完毕所有已启用通道余额!": "The balance of all enabled channels has been updated!",
"搜索渠道的 ID名称和密钥 ...": "Search for channel ID, name and key ...",
"名称": "Name",
"分组": "Group",
"类型": "Type",
"状态": "Status",
"响应时间": "Response time",
"余额": "Balance",
"操作": "Operation",
"未更新": "Not updated",
"测试": "Test",
"更新余额": "Update balance",
"删除": "Delete",
"删除渠道 {channel.name}": "Delete channel {channel.name}",
"禁用": "Disable",
"启用": "Enable",
"编辑": "Edit",
"添加新的渠道": "Add a new channel",
"测试所有已启用通道": "Test all enabled channels",
"更新所有已启用通道余额": "Update the balance of all enabled channels",
"刷新": "Refresh",
"处理中...": "Processing...",
"绑定成功!": "Binding succeeded!",
"登录成功!": "Login succeeded!",
"操作失败,重定向至登录界面中...": "Operation failed, redirecting to the login page...",
"出现错误,第 ${count} 次重试中...": "An error occurred, retrying for the ${count} time...",
"首页": "Home",
"渠道": "Channel",
"令牌": "Token",
"兑换": "Redeem",
"充值": "Recharge",
"用户": "User",
"日志": "Log",
"设置": "Settings",
"关于": "About",
"聊天": "Chat",
"注销成功!": "Logout succeeded!",
"注销": "Logout",
"登录": "Login",
"注册": "Register",
"加载{name}中...": "Loading {name}...",
"未登录或登录已过期,请重新登录!": "Not logged in or login has expired, please log in again!",
"用户登录": "User login",
"\"用户名\"": "\"Username\"",
"\"密码\"": "\"Password\"",
"忘记密码?": "Forget password?",
"点击重置": "Click to reset",
" 没有账户?": "; No account?",
"点击注册": "Click to register",
"微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)": "Scan the QR code of WeChat to follow the official account, enter \"verification code\" to get the verification code (valid within three minutes)",
"\"验证码\"": "\"Verification code\"",
"全部用户": "All users",
"当前用户": "Current user",
"'全部'": "'All'",
"'充值'": "'Recharge'",
"'消费'": "'Consumption'",
"'管理'": "'Management'",
"'系统'": "'System'",
" 充值 ": " Recharge ",
" 消费 ": " Consumption ",
" 管理 ": " Management ",
" 系统 ": " System ",
" 未知 ": " Unknown ",
"时间": "Time",
"详情": "Details",
"选择模式": "Select mode",
"选择明细分类": "Select details category",
"模型倍率不是合法的 JSON 字符串": "Model rate is not a valid JSON string",
"分组倍率不是合法的 JSON 字符串": "Group rate is not a valid JSON string",
"通用设置": "General Settings",
"充值链接": "Recharge Link",
"例如发卡网站的购买链接": "For example, the purchase link of the card issuing website",
"聊天页面链接": "Chat Page Link",
"例如 ChatGPT Next Web 的部署地址": "For example, the deployment address of ChatGPT Next Web",
"单位美元额度": "Unit Dollar Quota",
"一单位货币能兑换的额度": "Quota that can be exchanged for one unit of currency",
"启用额度消费日志记录": "Enable quota consumption log recording",
"以货币形式显示额度": "Display quota in the form of currency",
"相关 API 显示令牌额度而非用户额度": "Related API displays token quota instead of user quota",
"保存通用设置": "Save General Settings",
"监控设置": "Monitoring Settings",
"最长响应时间": "Longest Response Time",
"单位秒": "Unit in seconds",
"当运行通道全部测试时": "When all operating channels are tested",
"超过此时间将自动禁用通道": "Channels will be automatically disabled if this time is exceeded",
"额度提醒阈值": "Quota reminder threshold",
"低于此额度时将发送邮件提醒用户": "Email will be sent to remind users when the quota is below this",
"失败时自动禁用通道": "Automatically disable the channel when it fails",
"保存监控设置": "Save Monitoring Settings",
"额度设置": "Quota Settings",
"新用户初始额度": "Initial quota for new users",
"例如": "For example",
"请求预扣费额度": "Request for pre-deducted quota",
"请求结束后多退少补": "Refund more or less after the request ends",
"邀请新用户奖励额度": "Invite new users to reward quota",
"新用户使用邀请码奖励额度": "New user rewards quota using invitation code",
"保存额度设置": "Save Quota Settings",
"倍率设置": "Rate Settings",
"模型倍率": "Model rate",
"为一个 JSON 文本": "Is a JSON text",
"键为模型名称": "Key is model name",
"值为倍率": "Value is the rate",
"分组倍率": "Group rate",
"键为分组名称": "Key is group name",
"保存倍率设置": "Save Rate Settings",
"已是最新版本": "Is the latest version",
"检查更新": "Check for updates",
"公告": "Announcement",
"在此输入新的公告内容,支持 Markdown & HTML 代码": "Enter the new announcement content here, supports Markdown & HTML code",
"保存公告": "Save Announcement",
"个性化设置": "Personalization Settings",
"系统名称": "System Name",
"在此输入系统名称": "Enter the system name here",
"设置系统名称": "Set system name",
"图片地址": "Image URL",
"在此输入 Logo 图片地址": "Enter the Logo image URL here",
"首页内容": "Home Page Content",
"在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页": "Enter the homepage content here, supports Markdown & HTML code. Once set, the status information of the homepage will not be displayed. If a link is entered, it will be used as the src attribute of the iframe, allowing you to set any webpage as the homepage.",
"保存首页内容": "Save Home Page Content",
"在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面": "Enter new about content here, supports Markdown & HTML code. If a link is entered, it will be used as the src attribute of the iframe, allowing you to set any webpage as the about page.",
"保存关于": "Save About",
"移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目": "Removal of One API copyright mark must first be authorized. Project maintenance requires a lot of effort. If this project is meaningful to you, please actively support it.",
"页脚": "Footer",
"在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码": "Enter the new footer here, leave blank to use the default footer, supports HTML code.",
"设置页脚": "Set Footer",
"新版本": "New Version",
"关闭": "Close",
"密码已重置并已复制到剪贴板": "Password has been reset and copied to clipboard",
"密码重置确认": "Password Reset Confirmation",
"邮箱地址": "Email Address",
"提交": "Submit",
"请稍后几秒重试": "Please retry in a few seconds",
"正在检查用户环境": "Checking user environment",
"重置邮件发送成功": "Reset mail sent successfully",
"请检查邮箱": "Please check your email",
"密码重置": "Password Reset",
"令牌已重置并已复制到剪贴板": "Token has been reset and copied to clipboard",
"邀请链接已复制到剪切板": "Invitation link has been copied to clipboard",
"微信账户绑定成功": "WeChat account binding succeeded",
"验证码发送成功": "Verification code sent successfully",
"邮箱账户绑定成功": "Email account binding succeeded",
"注意": "Note",
"此处生成的令牌用于系统管理": "The token generated here is used for system management",
"而非用于请求 OpenAI 相关的服务": "Not for requesting OpenAI related services",
"请知悉": "Please be aware",
"更新个人信息": "Update Personal Information",
"生成系统访问令牌": "Generate System Access Token",
"复制邀请链接": "Copy Invitation Link",
"账号绑定": "Account Binding",
"绑定微信账号": "Bind WeChat Account",
"微信扫码关注公众号": "Scan the QR code with WeChat to follow the official account",
"输入": "Enter",
"验证码": "Verification Code",
"获取验证码": "Get Verification Code",
"三分钟内有效": "Valid for three minutes",
"绑定": "Bind",
"绑定 GitHub 账号": "Bind GitHub Account",
"绑定邮箱地址": "Bind Email Address",
"输入邮箱地址": "Enter Email Address",
"未使用": "Unused",
"已使用": "Used",
"操作成功完成": "Operation successfully completed",
"搜索兑换码的 ID 和名称": "Search for ID and name",
"额度": "Quota",
"创建时间": "Creation Time",
"兑换时间": "Redemption Time",
"尚未兑换": "Not yet redeemed",
"已复制到剪贴板": "Copied to clipboard",
"无法复制到剪贴板": "Unable to copy to clipboard",
"请手动复制": "Please copy manually",
"已将兑换码填入搜索框": "The voucher code has been filled into the search box",
"复制": "Copy",
"添加新的兑换码": "Add a new voucher",
"密码长度不得小于 8 位": "Password length must not be less than 8 characters",
"两次输入的密码不一致": "The two passwords entered do not match",
"注册成功": "Registration succeeded",
"请稍后几秒重试Turnstile 正在检查用户环境": "Please retry in a few seconds, Turnstile is checking user environment",
"验证码发送成功,请检查你的邮箱": "Verification code sent successfully, please check your email",
"新用户注册": "New User Registration",
"输入用户名,最长 12 位": "Enter username, up to 12 characters",
"输入密码,最短 8 位,最长 20 位": "Enter password, at least 8 characters and up to 20 characters",
"输入验证码": "Enter Verification Code",
"已有账户": "Already have an account",
"点击登录": "Click to log in",
"服务器地址": "Server Address",
"更新服务器地址": "Update Server Address",
"配置登录注册": "Configure Login/Registration",
"允许通过密码进行登录": "Allow login via password",
"允许通过密码进行注册": "Allow registration via password",
"通过密码注册时需要进行邮箱验证": "Email verification is required when registering via password",
"允许通过 GitHub 账户登录 & 注册": "Allow login & registration via GitHub account",
"允许通过微信登录 & 注册": "Allow login & registration via WeChat",
"允许新用户注册(此项为否时,新用户将无法以任何方式进行注册": "Allow new user registration (if this option is off, new users will not be able to register in any way",
"启用 Turnstile 用户校验": "Enable Turnstile user verification",
"配置 SMTP": "Configure SMTP",
"用以支持系统的邮件发送": "To support the system email sending",
"SMTP 服务器地址": "SMTP Server Address",
"例如smtp.qq.com": "For example: smtp.qq.com",
"SMTP 端口": "SMTP Port",
"默认: 587": "Default: 587",
"SMTP 账户": "SMTP Account",
"通常是邮箱地址": "Usually an email address",
"发送者邮箱": "Sender email",
"通常和邮箱地址保持一致": "Usually consistent with the email address",
"SMTP 访问凭证": "SMTP Access Credential",
"敏感信息不会发送到前端显示": "Sensitive information will not be displayed in the frontend",
"保存 SMTP 设置": "Save SMTP Settings",
"配置 GitHub OAuth App": "Configure GitHub OAuth App",
"配置 Linuxdo OAuth App": "Configure Linuxdo OAuth App",
"用以支持通过 GitHub 进行登录注册": "To support login & registration via GitHub",
"点击此处": "Click here",
"管理你的 GitHub OAuth App": "Manage your GitHub OAuth App",
"输入你注册的 GitHub OAuth APP 的 ID": "Enter your registered GitHub OAuth APP ID",
"保存 GitHub OAuth 设置": "Save GitHub OAuth Settings",
"配置 WeChat Server": "Configure WeChat Server",
"用以支持通过微信进行登录注册": "To support login & registration via WeChat",
"了解 WeChat Server": "Learn about WeChat Server",
"WeChat Server 访问凭证": "WeChat Server Access Credential",
"微信公众号二维码图片链接": "WeChat Public Account QR Code Image Link",
"输入一个图片链接": "Enter an image link",
"保存 WeChat Server 设置": "Save WeChat Server Settings",
"配置 Turnstile": "Configure Turnstile",
"用以支持用户校验": "To support user verification",
"管理你的 Turnstile Sites推荐选择 Invisible Widget Type": "Manage your Turnstile Sites, recommend selecting Invisible Widget Type",
"输入你注册的 Turnstile Site Key": "Enter your registered Turnstile Site Key",
"保存 Turnstile 设置": "Save Turnstile Settings",
"已过期": "Expired",
"已耗尽": "Exhausted",
"搜索令牌的名称 ...": "Search for the name of the token...",
"已用额度": "Quota used",
"剩余额度": "Remaining quota",
"过期时间": "Expiration time",
"无": "None",
"无限制": "Unlimited",
"永不过期": "Never expires",
"无法复制到剪贴板,请手动复制,已将令牌填入搜索框": "Unable to copy to clipboard, please copy manually, the token has been entered into the search box",
"删除令牌": "Delete Token",
"添加新的令牌": "Add New Token",
"普通用户": "Regular User",
"管理员": "Admin",
"超级管理员": "Super Admin",
"未知身份": "Unknown Identity",
"已激活": "Activated",
"已封禁": "Banned",
"搜索用户的 ID用户名显示名称以及邮箱地址 ...": "Search user ID, username, display name, and email address...",
"用户名": "Username",
"统计信息": "Statistics",
"用户角色": "User Role",
"未绑定邮箱地址": "Email not bound",
"请求次数": "Number of Requests",
"提升": "Promote",
"降级": "Demote",
"删除用户": "Delete User",
"添加新的用户": "Add New User",
"自定义": "Custom",
"等价金额": "Equivalent Amount",
"未登录或登录已过期,请重新登录": "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",
"本站仅作演示之用,无服务端": "This site is for demonstration purposes only, no server-side",
"超级管理员未设置充值链接!": "Super administrator has not set the recharge link!",
"错误:": "Error: ",
"新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面": "New version available: ${data.version}, please refresh the page using shortcut Shift + F5",
"无法正常连接至服务器": "Unable to connect to the server normally",
"管理渠道": "Manage Channels",
"系统状况": "System Status",
"系统信息": "System Information",
"系统信息总览": "System Information Overview",
"版本": "Version",
"源码": "Source Code",
"启动时间": "Startup Time",
"系统配置": "System Configuration",
"系统配置总览": "System Configuration Overview",
"邮箱验证": "Email Verification",
"未启用": "Not Enabled",
"GitHub 身份验证": "GitHub Authentication",
"微信身份验证": "WeChat Authentication",
"Turnstile 用户校验": "Turnstile User Verification",
"创建新的渠道": "Create New Channel",
"镜像": "Mirror",
"请输入镜像站地址格式为https://domain.com可不填不填则使用渠道默认值": "Please enter the mirror site address, the format is: https://domain.com, it can be left blank, if left blank, the default value of the channel will be used",
"模型": "Model",
"请选择该通道所支持的模型": "Please select the model supported by the channel",
"填入基础模型": "Fill in the basic model",
"填入所有模型": "Fill in all models",
"清除所有模型": "Clear all models",
"密钥": "Key",
"请输入密钥": "Please enter the key",
"批量创建": "Batch Create",
"更新渠道信息": "Update Channel Information",
"我的令牌": "My Tokens",
"管理兑换码": "Manage Redeem Codes",
"兑换码": "Redeem Code",
"管理用户": "Manage Users",
"额度明细": "Quota Details",
"个人设置": "Personal Settings",
"运营设置": "Operation Settings",
"系统设置": "System Settings",
"其他设置": "Other Settings",
"项目仓库地址": "Project Repository Address",
"可在设置页面设置关于内容,支持 HTML & Markdown": "You can set the content about in the settings page, support HTML & Markdown",
"由{' '}": "built by{' '}",
"构建,源代码遵循{' '}": ", the source code licensed under{' '}",
"MIT 协议": "MIT License",
"充值额度": "Recharge Quota",
"获取兑换码": "Get Redeem Code",
"一个月后过期": "Expires after one month",
"一天后过期": "Expires after one day",
"一小时后过期": "Expires after one hour",
"一分钟后过期": "Expires after one minute",
"创建新的令牌": "Create New Token",
"注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。": "Note that the quota of the token is only used to limit the maximum quota usage of the token itself, and the actual usage is limited by the remaining quota of the account.",
"设为无限额度": "Set to unlimited quota",
"更新令牌信息": "Update Token Information",
"请输入充值码!": "Please enter the recharge code!",
"请输入名称": "Please enter a name",
"请输入密钥,一行一个": "Please enter the key, one per line",
"请输入额度": "Please enter the quota",
"令牌创建成功": "Token created successfully",
"令牌更新成功": "Token updated successfully",
"充值成功!": "Recharge successful!",
"更新用户信息": "Update User Information",
"请输入新的用户名": "Please enter a new username",
"密码": "Password",
"请输入新的密码": "Please enter a new password",
"显示名称": "Display Name",
"请输入新的显示名称": "Please enter a new display name",
"已绑定的 GitHub 账户": "GitHub Account Bound",
"此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "This item is read-only. Users need to bind through the relevant binding button on the personal settings page, and cannot be modified directly",
"已绑定的微信账户": "WeChat Account Bound",
"已绑定的邮箱账户": "Email Account Bound",
"用户信息更新成功!": "User information updated successfully!",
"模型倍率 %.2f,分组倍率 %.2f": "model rate %.2f, group rate %.2f",
"使用明细(总消耗额度:{renderQuota(stat.quota)}": "Usage Details (Total Consumption Quota: {renderQuota(stat.quota)})",
"用户名称": "User Name",
"令牌名称": "Token Name",
"留空则查询全部用户": "Leave blank to query all users",
"留空则查询全部令牌": "Leave blank to query all tokens",
"模型名称": "Model Name",
"留空则查询全部模型": "Leave blank to query all models",
"起始时间": "Start Time",
"结束时间": "End Time",
"查询": "Query",
"提示": "Prompt",
"补全": "Completion",
"消耗额度": "Used Quota",
"可选值": "Optional Values",
"渠道不存在:%d": "Channel does not exist: %d",
"数据库一致性已被破坏,请联系管理员": "Database consistency has been broken, please contact the administrator",
"使用近似的方式估算 token 数以减少计算量": "Estimate the number of tokens in an approximate way to reduce computational load",
"请填写ChannelName和ChannelKey": "Please fill in the ChannelName and ChannelKey!",
"请至少选择一个Model": "Please select at least one Model!",
"加载首页内容失败": "Failed to load the homepage content",
"加载关于内容失败": "Failed to load the About content",
"兑换码更新成功!": "Redemption code updated successfully!",
"兑换码创建成功!": "Redemption code created successfully!",
"用户账户创建成功!": "User account created successfully!",
"生成数量": "Generate quantity",
"请输入生成数量": "Please enter the quantity to generate",
"创建新用户账户": "Create new user account",
"渠道更新成功!": "Channel updated successfully!",
"渠道创建成功!": "Channel created successfully!",
"请选择分组": "Please select a group",
"更新兑换码信息": "Update redemption code information",
"创建新的兑换码": "Create a new redemption code",
"请在系统设置页面编辑分组倍率以添加新的分组:": "Please edit the group ratio in the system settings page to add a new group:",
"未找到所请求的页面": "The requested page was not found",
"过期时间格式错误!": "Expiration time format error!",
"请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss-1 表示无限制": "Please enter the expiration time, the format is yyyy-MM-dd HH:mm:ss, -1 means no limit",
"此项可选,为一个 JSON 文本,键为用户请求的模型名称,值为要替换的模型名称,例如:": "This is optional, it's a JSON text, the key is the model name requested by the user, and the value is the model name to be replaced, for example:",
"此项可选,输入镜像站地址,格式为:": "This is optional, enter the mirror site address, the format is:",
"模型映射": "Model mapping",
"请输入默认 API 版本例如2023-03-15-preview该配置可以被实际的请求查询参数所覆盖": "Please enter the default API version, for example: 2023-03-15-preview, this configuration can be overridden by the actual request query parameters",
"默认": "Default",
"图片演示": "Image demo",
"参数替换为你的部署名称(模型名称中的点会被剔除)": "Replace the parameter with your deployment name (dots in the model name will be removed)",
"模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!",
"取消无限额度": "Cancel unlimited quota",
"取消": "Cancel",
"请输入新的剩余额度": "Please enter the new remaining quota",
"请输入单个兑换码中包含的额度": "Please enter the quota included in a single redemption code",
"请输入用户名": "Please enter username",
"请输入显示名称": "Please enter display name",
"请输入密码": "Please enter password",
"模型部署名称必须和模型名称保持一致": "The model deployment name must be consistent with the model name",
",因为 One API 会把请求体中的 model": ", because One API will take the model in the request body",
"请输入 AZURE_OPENAI_ENDPOINT": "Please enter AZURE_OPENAI_ENDPOINT",
"请输入自定义渠道的 Base URL": "Please enter the Base URL of the custom channel",
"Homepage URL 填": "Fill in the Homepage URL",
"Authorization callback URL 填": "Fill in the Authorization callback URL",
"请为通道命名": "Please name the channel",
"此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:": "This is optional, used to modify the model name in the request body, it's a JSON string, the key is the model name in the request, and the value is the model name to be replaced, for example:",
"模型重定向": "Model redirection",
"请输入渠道对应的鉴权密钥": "Please enter the authentication key corresponding to the channel",
"注意,": "Note that, ",
",图片演示。": "related image demo.",
"令牌创建成功,请在列表页面点击复制获取令牌!": "Token created successfully, please click copy on the list page to get the token!",
"代理": "Proxy",
"此项可选,用于通过代理站来进行 API 调用请输入代理站地址格式为https://domain.com": "This is optional, used to make API calls through the proxy site, please enter the proxy site address, the format is: https://domain.com",
"取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?": "Canceling password login will cause all users (including administrators) who have not bound other login methods to be unable to log in via password, confirm cancel?",
"按照如下格式输入:": "Enter in the following format:",
"模型版本": "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!"
}

View File

@@ -1,61 +0,0 @@
import argparse
import json
import os
def list_file_paths(path):
file_paths = []
for root, dirs, files in os.walk(path):
if "node_modules" in dirs:
dirs.remove("node_modules")
if "build" in dirs:
dirs.remove("build")
if "i18n" in dirs:
dirs.remove("i18n")
for file in files:
file_path = os.path.join(root, file)
if file_path.endswith("png") or file_path.endswith("ico") or file_path.endswith("db") or file_path.endswith("exe"):
continue
file_paths.append(file_path)
for dir in dirs:
dir_path = os.path.join(root, dir)
file_paths += list_file_paths(dir_path)
return file_paths
def replace_keys_in_repository(repo_path, json_file_path):
with open(json_file_path, 'r', encoding="utf-8") as json_file:
key_value_pairs = json.load(json_file)
pairs = []
for key, value in key_value_pairs.items():
pairs.append((key, value))
pairs.sort(key=lambda x: len(x[0]), reverse=True)
files = list_file_paths(repo_path)
print('Total files: {}'.format(len(files)))
for file_path in files:
replace_keys_in_file(file_path, pairs)
def replace_keys_in_file(file_path, pairs):
try:
with open(file_path, 'r', encoding="utf-8") as file:
content = file.read()
for key, value in pairs:
content = content.replace(key, value)
with open(file_path, 'w', encoding="utf-8") as file:
file.write(content)
except UnicodeDecodeError:
print('UnicodeDecodeError: {}'.format(file_path))
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Replace keys in repository.')
parser.add_argument('--repository_path', help='Path to repository')
parser.add_argument('--json_file_path', help='Path to JSON file')
args = parser.parse_args()
replace_keys_in_repository(args.repository_path, args.json_file_path)

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

@@ -10,8 +10,10 @@ import (
"one-api/model"
relayconstant "one-api/relay/constant"
"one-api/service"
"one-api/setting"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
)
@@ -42,12 +44,12 @@ func Distribute() func(c *gin.Context) {
tokenGroup := c.GetString("token_group")
if tokenGroup != "" {
// check common.UserUsableGroups[userGroup]
if _, ok := common.GetUserUsableGroups(userGroup)[tokenGroup]; !ok {
if _, ok := setting.GetUserUsableGroups(userGroup)[tokenGroup]; !ok {
abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("令牌分组 %s 已被禁用", tokenGroup))
return
}
// check group in common.GroupRatio
if _, ok := common.GroupRatio[tokenGroup]; !ok {
if !setting.ContainsGroupRatio(tokenGroup) {
abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("分组 %s 已被弃用", tokenGroup))
return
}
@@ -112,6 +114,7 @@ func Distribute() func(c *gin.Context) {
}
}
}
c.Set(constant.ContextKeyRequestStartTime, time.Now())
SetupContextForSelectedChannel(c, channel, modelRequest.Model)
c.Next()
}
@@ -213,6 +216,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

@@ -3,10 +3,11 @@ package model
import (
"errors"
"fmt"
"github.com/samber/lo"
"gorm.io/gorm"
"one-api/common"
"strings"
"github.com/samber/lo"
"gorm.io/gorm"
)
type Ability struct {
@@ -173,18 +174,67 @@ func (channel *Channel) DeleteAbilities() error {
// UpdateAbilities updates abilities of this channel.
// Make sure the channel is completed before calling this function.
func (channel *Channel) UpdateAbilities() error {
// A quick and dirty way to update abilities
func (channel *Channel) UpdateAbilities(tx *gorm.DB) error {
isNewTx := false
// 如果没有传入事务,创建新的事务
if tx == nil {
tx = DB.Begin()
if tx.Error != nil {
return tx.Error
}
isNewTx = true
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
}
// First delete all abilities of this channel
err := channel.DeleteAbilities()
err := tx.Where("channel_id = ?", channel.Id).Delete(&Ability{}).Error
if err != nil {
if isNewTx {
tx.Rollback()
}
return err
}
// Then add new abilities
err = channel.AddAbilities()
if err != nil {
return err
models_ := strings.Split(channel.Models, ",")
groups_ := strings.Split(channel.Group, ",")
abilities := make([]Ability, 0, len(models_))
for _, model := range models_ {
for _, group := range groups_ {
ability := Ability{
Group: group,
Model: model,
ChannelId: channel.Id,
Enabled: channel.Status == common.ChannelStatusEnabled,
Priority: channel.Priority,
Weight: uint(channel.GetWeight()),
Tag: channel.Tag,
}
abilities = append(abilities, ability)
}
}
if len(abilities) > 0 {
for _, chunk := range lo.Chunk(abilities, 50) {
err = tx.Create(&chunk).Error
if err != nil {
if isNewTx {
tx.Rollback()
}
return err
}
}
}
// 如果是新创建的事务,需要提交
if isNewTx {
return tx.Commit().Error
}
return nil
}
@@ -246,7 +296,7 @@ func FixAbility() (int, error) {
return 0, err
}
for _, channel := range channels {
err := channel.UpdateAbilities()
err := channel.UpdateAbilities(nil)
if err != nil {
common.SysError(fmt.Sprintf("Update abilities of channel %d failed: %s", channel.Id, err.Error()))
} else {

View File

@@ -342,3 +342,14 @@ func CacheGetChannel(id int) (*Channel, error) {
}
return c, nil
}
func CacheUpdateChannelStatus(id int, status int) {
if (!common.MemoryCacheEnabled) {
return
}
channelSyncLock.Lock()
defer channelSyncLock.Unlock()
if channel, ok := channelsIDM[id]; ok {
channel.Status = status
}
}

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"one-api/common"
"strings"
"sync"
"gorm.io/gorm"
)
@@ -34,6 +35,7 @@ type Channel struct {
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 {
@@ -100,9 +102,13 @@ func GetAllChannels(startIdx int, num int, selectAll bool, idSort bool) ([]*Chan
return channels, err
}
func GetChannelsByTag(tag string) ([]*Channel, error) {
func GetChannelsByTag(tag string, idSort bool) ([]*Channel, error) {
var channels []*Channel
err := DB.Where("tag = ?", tag).Find(&channels).Error
order := "priority desc"
if idSort {
order = "id desc"
}
err := DB.Where("tag = ?", tag).Order(order).Find(&channels).Error
return channels, err
}
@@ -251,7 +257,7 @@ func (channel *Channel) Update() error {
return err
}
DB.Model(channel).First(channel, "id = ?", channel.Id)
err = channel.UpdateAbilities()
err = channel.UpdateAbilities(nil)
return err
}
@@ -285,7 +291,25 @@ func (channel *Channel) Delete() error {
return err
}
var channelStatusLock sync.Mutex
func UpdateChannelStatusById(id int, status int, reason string) {
if common.MemoryCacheEnabled {
channelStatusLock.Lock()
channelCache, _ := CacheGetChannel(id)
// 如果缓存渠道存在,且状态已是目标状态,直接返回
if channelCache != nil && channelCache.Status == status {
channelStatusLock.Unlock()
return
}
// 如果缓存渠道不存在(说明已经被禁用),且要设置的状态不为启用,直接返回
if channelCache == nil && status != common.ChannelStatusEnabled {
channelStatusLock.Unlock()
return
}
CacheUpdateChannelStatus(id, status)
channelStatusLock.Unlock()
}
err := UpdateAbilityStatus(id, status == common.ChannelStatusEnabled)
if err != nil {
common.SysError("failed to update ability status: " + err.Error())
@@ -362,10 +386,10 @@ func EditChannelByTag(tag string, newTag *string, modelMapping *string, models *
return err
}
if shouldReCreateAbilities {
channels, err := GetChannelsByTag(updatedTag)
channels, err := GetChannelsByTag(updatedTag, false)
if err == nil {
for _, channel := range channels {
err = channel.UpdateAbilities()
err = channel.UpdateAbilities(nil)
if err != nil {
common.SysError("failed to update abilities: " + err.Error())
}
@@ -410,3 +434,117 @@ func GetPaginatedTags(offset int, limit int) ([]*string, error) {
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)
}
func GetChannelsByIds(ids []int) ([]*Channel, error) {
var channels []*Channel
err := DB.Where("id in (?)", ids).Find(&channels).Error
return channels, err
}
func BatchSetChannelTag(ids []int, tag *string) error {
// 开启事务
tx := DB.Begin()
if tx.Error != nil {
return tx.Error
}
// 更新标签
err := tx.Model(&Channel{}).Where("id in (?)", ids).Update("tag", tag).Error
if err != nil {
tx.Rollback()
return err
}
// update ability status
channels, err := GetChannelsByIds(ids)
if err != nil {
tx.Rollback()
return err
}
for _, channel := range channels {
err = channel.UpdateAbilities(tx)
if err != nil {
tx.Rollback()
return err
}
}
// 提交事务
return tx.Commit().Error
}

View File

@@ -12,6 +12,16 @@ import (
"gorm.io/gorm"
)
var groupCol string
func init() {
if common.UsingPostgreSQL {
groupCol = `"group"`
} else {
groupCol = "`group`"
}
}
type Log struct {
Id int `json:"id" gorm:"index:idx_created_at_id,priority:1"`
UserId int `json:"user_id" gorm:"index"`
@@ -28,6 +38,7 @@ type Log struct {
IsStream bool `json:"is_stream" gorm:"default:false"`
ChannelId int `json:"channel" gorm:"index"`
TokenId int `json:"token_id" gorm:"default:0;index"`
Group string `json:"group" gorm:"index"`
Other string `json:"other"`
}
@@ -70,7 +81,9 @@ func RecordLog(userId int, logType int, content string) {
}
}
func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptTokens int, completionTokens int, modelName string, tokenName string, quota int, content string, tokenId int, userQuota int, useTimeSeconds int, isStream bool, other map[string]interface{}) {
func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptTokens int, completionTokens int,
modelName string, tokenName string, quota int, content string, tokenId int, userQuota int, useTimeSeconds int,
isStream bool, group string, other map[string]interface{}) {
common.LogInfo(ctx, fmt.Sprintf("record consume log: userId=%d, 用户调用前余额=%d, channelId=%d, promptTokens=%d, completionTokens=%d, modelName=%s, tokenName=%s, quota=%d, content=%s", userId, userQuota, channelId, promptTokens, completionTokens, modelName, tokenName, quota, content))
if !common.LogConsumeEnabled {
return
@@ -92,6 +105,7 @@ func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptToke
TokenId: tokenId,
UseTime: useTimeSeconds,
IsStream: isStream,
Group: group,
Other: otherStr,
}
err := LOG_DB.Create(log).Error
@@ -105,7 +119,7 @@ func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptToke
}
}
func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, startIdx int, num int, channel int) (logs []*Log, total int64, err error) {
func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, startIdx int, num int, channel int, group string) (logs []*Log, total int64, err error) {
var tx *gorm.DB
if logType == LogTypeUnknown {
tx = LOG_DB
@@ -130,6 +144,9 @@ func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName
if channel != 0 {
tx = tx.Where("channel_id = ?", channel)
}
if group != "" {
tx = tx.Where(groupCol+" = ?", group)
}
err = tx.Model(&Log{}).Count(&total).Error
if err != nil {
return nil, 0, err
@@ -141,7 +158,7 @@ func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName
return logs, total, err
}
func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int64, modelName string, tokenName string, startIdx int, num int) (logs []*Log, total int64, err error) {
func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int64, modelName string, tokenName string, startIdx int, num int, group string) (logs []*Log, total int64, err error) {
var tx *gorm.DB
if logType == LogTypeUnknown {
tx = LOG_DB.Where("user_id = ?", userId)
@@ -160,6 +177,9 @@ func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int
if endTimestamp != 0 {
tx = tx.Where("created_at <= ?", endTimestamp)
}
if group != "" {
tx = tx.Where(groupCol+" = ?", group)
}
err = tx.Model(&Log{}).Count(&total).Error
if err != nil {
return nil, 0, err
@@ -193,7 +213,7 @@ type Stat struct {
Tpm int `json:"tpm"`
}
func SumUsedQuota(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, channel int) (stat Stat) {
func SumUsedQuota(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, channel int, group string) (stat Stat) {
tx := LOG_DB.Table("logs").Select("sum(quota) quota")
// 为rpm和tpm创建单独的查询
@@ -221,6 +241,10 @@ func SumUsedQuota(logType int, startTimestamp int64, endTimestamp int64, modelNa
tx = tx.Where("channel_id = ?", channel)
rpmTpmQuery = rpmTpmQuery.Where("channel_id = ?", channel)
}
if group != "" {
tx = tx.Where(groupCol+" = ?", group)
rpmTpmQuery = rpmTpmQuery.Where(groupCol+" = ?", group)
}
tx = tx.Where("type = ?", LogTypeConsume)
rpmTpmQuery = rpmTpmQuery.Where("type = ?", LogTypeConsume)

View File

@@ -2,7 +2,7 @@ package model
import (
"one-api/common"
"one-api/constant"
"one-api/setting"
"strconv"
"strings"
"time"
@@ -61,16 +61,16 @@ func InitOptionMap() {
common.OptionMap["SystemName"] = common.SystemName
common.OptionMap["Logo"] = common.Logo
common.OptionMap["ServerAddress"] = ""
common.OptionMap["WorkerUrl"] = constant.WorkerUrl
common.OptionMap["WorkerValidKey"] = constant.WorkerValidKey
common.OptionMap["WorkerUrl"] = setting.WorkerUrl
common.OptionMap["WorkerValidKey"] = setting.WorkerValidKey
common.OptionMap["PayAddress"] = ""
common.OptionMap["CustomCallbackAddress"] = ""
common.OptionMap["EpayId"] = ""
common.OptionMap["EpayKey"] = ""
common.OptionMap["Price"] = strconv.FormatFloat(constant.Price, 'f', -1, 64)
common.OptionMap["MinTopUp"] = strconv.Itoa(constant.MinTopUp)
common.OptionMap["Price"] = strconv.FormatFloat(setting.Price, 'f', -1, 64)
common.OptionMap["MinTopUp"] = strconv.Itoa(setting.MinTopUp)
common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
common.OptionMap["Chats"] = constant.Chats2JsonString()
common.OptionMap["Chats"] = setting.Chats2JsonString()
common.OptionMap["GitHubClientId"] = ""
common.OptionMap["GitHubClientSecret"] = ""
common.OptionMap["TelegramBotToken"] = ""
@@ -87,8 +87,8 @@ func InitOptionMap() {
common.OptionMap["PreConsumedQuota"] = strconv.Itoa(common.PreConsumedQuota)
common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString()
common.OptionMap["ModelPrice"] = common.ModelPrice2JSONString()
common.OptionMap["GroupRatio"] = common.GroupRatio2JSONString()
common.OptionMap["UserUsableGroups"] = common.UserUsableGroups2JSONString()
common.OptionMap["GroupRatio"] = setting.GroupRatio2JSONString()
common.OptionMap["UserUsableGroups"] = setting.UserUsableGroups2JSONString()
common.OptionMap["CompletionRatio"] = common.CompletionRatio2JSONString()
common.OptionMap["TopUpLink"] = common.TopUpLink
common.OptionMap["ChatLink"] = common.ChatLink
@@ -98,17 +98,17 @@ func InitOptionMap() {
common.OptionMap["DataExportInterval"] = strconv.Itoa(common.DataExportInterval)
common.OptionMap["DataExportDefaultTime"] = common.DataExportDefaultTime
common.OptionMap["DefaultCollapseSidebar"] = strconv.FormatBool(common.DefaultCollapseSidebar)
common.OptionMap["MjNotifyEnabled"] = strconv.FormatBool(constant.MjNotifyEnabled)
common.OptionMap["MjAccountFilterEnabled"] = strconv.FormatBool(constant.MjAccountFilterEnabled)
common.OptionMap["MjModeClearEnabled"] = strconv.FormatBool(constant.MjModeClearEnabled)
common.OptionMap["MjForwardUrlEnabled"] = strconv.FormatBool(constant.MjForwardUrlEnabled)
common.OptionMap["MjActionCheckSuccessEnabled"] = strconv.FormatBool(constant.MjActionCheckSuccessEnabled)
common.OptionMap["CheckSensitiveEnabled"] = strconv.FormatBool(constant.CheckSensitiveEnabled)
common.OptionMap["CheckSensitiveOnPromptEnabled"] = strconv.FormatBool(constant.CheckSensitiveOnPromptEnabled)
common.OptionMap["MjNotifyEnabled"] = strconv.FormatBool(setting.MjNotifyEnabled)
common.OptionMap["MjAccountFilterEnabled"] = strconv.FormatBool(setting.MjAccountFilterEnabled)
common.OptionMap["MjModeClearEnabled"] = strconv.FormatBool(setting.MjModeClearEnabled)
common.OptionMap["MjForwardUrlEnabled"] = strconv.FormatBool(setting.MjForwardUrlEnabled)
common.OptionMap["MjActionCheckSuccessEnabled"] = strconv.FormatBool(setting.MjActionCheckSuccessEnabled)
common.OptionMap["CheckSensitiveEnabled"] = strconv.FormatBool(setting.CheckSensitiveEnabled)
common.OptionMap["CheckSensitiveOnPromptEnabled"] = strconv.FormatBool(setting.CheckSensitiveOnPromptEnabled)
//common.OptionMap["CheckSensitiveOnCompletionEnabled"] = strconv.FormatBool(constant.CheckSensitiveOnCompletionEnabled)
common.OptionMap["StopOnSensitiveEnabled"] = strconv.FormatBool(constant.StopOnSensitiveEnabled)
common.OptionMap["SensitiveWords"] = constant.SensitiveWordsToString()
common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(constant.StreamCacheQueueLength)
common.OptionMap["StopOnSensitiveEnabled"] = strconv.FormatBool(setting.StopOnSensitiveEnabled)
common.OptionMap["SensitiveWords"] = setting.SensitiveWordsToString()
common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(setting.StreamCacheQueueLength)
common.OptionMapRWMutex.Unlock()
loadOptionsFromDatabase()
@@ -209,23 +209,23 @@ func updateOptionMap(key string, value string) (err error) {
case "DefaultCollapseSidebar":
common.DefaultCollapseSidebar = boolValue
case "MjNotifyEnabled":
constant.MjNotifyEnabled = boolValue
setting.MjNotifyEnabled = boolValue
case "MjAccountFilterEnabled":
constant.MjAccountFilterEnabled = boolValue
setting.MjAccountFilterEnabled = boolValue
case "MjModeClearEnabled":
constant.MjModeClearEnabled = boolValue
setting.MjModeClearEnabled = boolValue
case "MjForwardUrlEnabled":
constant.MjForwardUrlEnabled = boolValue
setting.MjForwardUrlEnabled = boolValue
case "MjActionCheckSuccessEnabled":
constant.MjActionCheckSuccessEnabled = boolValue
setting.MjActionCheckSuccessEnabled = boolValue
case "CheckSensitiveEnabled":
constant.CheckSensitiveEnabled = boolValue
setting.CheckSensitiveEnabled = boolValue
case "CheckSensitiveOnPromptEnabled":
constant.CheckSensitiveOnPromptEnabled = boolValue
setting.CheckSensitiveOnPromptEnabled = boolValue
//case "CheckSensitiveOnCompletionEnabled":
// constant.CheckSensitiveOnCompletionEnabled = boolValue
case "StopOnSensitiveEnabled":
constant.StopOnSensitiveEnabled = boolValue
setting.StopOnSensitiveEnabled = boolValue
case "SMTPSSLEnabled":
common.SMTPSSLEnabled = boolValue
}
@@ -245,25 +245,25 @@ func updateOptionMap(key string, value string) (err error) {
case "SMTPToken":
common.SMTPToken = value
case "ServerAddress":
constant.ServerAddress = value
setting.ServerAddress = value
case "WorkerUrl":
constant.WorkerUrl = value
setting.WorkerUrl = value
case "WorkerValidKey":
constant.WorkerValidKey = value
setting.WorkerValidKey = value
case "PayAddress":
constant.PayAddress = value
setting.PayAddress = value
case "Chats":
err = constant.UpdateChatsByJsonString(value)
err = setting.UpdateChatsByJsonString(value)
case "CustomCallbackAddress":
constant.CustomCallbackAddress = value
setting.CustomCallbackAddress = value
case "EpayId":
constant.EpayId = value
setting.EpayId = value
case "EpayKey":
constant.EpayKey = value
setting.EpayKey = value
case "Price":
constant.Price, _ = strconv.ParseFloat(value, 64)
setting.Price, _ = strconv.ParseFloat(value, 64)
case "MinTopUp":
constant.MinTopUp, _ = strconv.Atoi(value)
setting.MinTopUp, _ = strconv.Atoi(value)
case "TopupGroupRatio":
err = common.UpdateTopupGroupRatioByJSONString(value)
case "GitHubClientId":
@@ -313,9 +313,9 @@ func updateOptionMap(key string, value string) (err error) {
case "ModelRatio":
err = common.UpdateModelRatioByJSONString(value)
case "GroupRatio":
err = common.UpdateGroupRatioByJSONString(value)
err = setting.UpdateGroupRatioByJSONString(value)
case "UserUsableGroups":
err = common.UpdateUserUsableGroupsByJSONString(value)
err = setting.UpdateUserUsableGroupsByJSONString(value)
case "CompletionRatio":
err = common.UpdateCompletionRatioByJSONString(value)
case "ModelPrice":
@@ -331,9 +331,9 @@ func updateOptionMap(key string, value string) (err error) {
case "QuotaPerUnit":
common.QuotaPerUnit, _ = strconv.ParseFloat(value, 64)
case "SensitiveWords":
constant.SensitiveWordsFromString(value)
setting.SensitiveWordsFromString(value)
case "StreamCacheQueueLength":
constant.StreamCacheQueueLength, _ = strconv.Atoi(value)
setting.StreamCacheQueueLength, _ = strconv.Atoi(value)
}
return err
}

View File

@@ -5,8 +5,8 @@ import (
"fmt"
"gorm.io/gorm"
"one-api/common"
"one-api/constant"
relaycommon "one-api/relay/common"
"one-api/setting"
"strconv"
"strings"
)
@@ -325,7 +325,7 @@ func PostConsumeTokenQuota(relayInfo *relaycommon.RelayInfo, userQuota int, quot
prompt = "您的额度已用尽"
}
if email != "" {
topUpLink := fmt.Sprintf("%s/topup", constant.ServerAddress)
topUpLink := fmt.Sprintf("%s/topup", setting.ServerAddress)
err = common.SendEmail(prompt, email,
fmt.Sprintf("%s当前剩余额度为 %d为了不影响您的使用请及时充值。<br/>充值链接:<a href='%s'>%s</a>", prompt, userQuota, topUpLink, topUpLink))
if err != nil {

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

@@ -89,26 +89,31 @@ func SearchUsers(keyword string, group string) ([]*User, error) {
var users []*User
var err error
groupCol := "`group`"
if common.UsingPostgreSQL {
groupCol = `"group"`
}
// 尝试将关键字转换为整数ID
keywordInt, err := strconv.Atoi(keyword)
if err == nil {
// 如果转换成功按照ID和可选的组别搜索用户
query := DB.Unscoped().Omit("password").Where("`id` = ?", keywordInt)
query := DB.Unscoped().Omit("password").Where("id = ?", keywordInt)
if group != "" {
query = query.Where("`group` = ?", group) // 使用反引号包围group
query = query.Where(groupCol+" = ?", group) // 使用反引号包围group
}
err = query.Find(&users).Error
if err != nil || len(users) > 0 {
return users, err
}
}
err = nil
query := DB.Unscoped().Omit("password")
likeCondition := "`username` LIKE ? OR `email` LIKE ? OR `display_name` LIKE ?"
likeCondition := "username LIKE ? OR email LIKE ? OR display_name LIKE ?"
if group != "" {
query = query.Where("("+likeCondition+") AND `group` = ?", "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", group)
query = query.Where("("+likeCondition+") AND "+groupCol+" = ?", "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", group)
} else {
query = query.Where(likeCondition, "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
}

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

@@ -240,14 +240,7 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*ClaudeR
claudeMediaMessages = append(claudeMediaMessages, claudeMediaMessage)
}
if message.ToolCalls != nil {
for _, tc := range message.ToolCalls.([]interface{}) {
toolCallJSON, _ := json.Marshal(tc)
var toolCall dto.ToolCall
err := json.Unmarshal(toolCallJSON, &toolCall)
if err != nil {
common.SysError("tool call is not a dto.ToolCall: " + fmt.Sprintf("%v", tc))
continue
}
for _, toolCall := range message.ParseToolCalls() {
inputObj := make(map[string]any)
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &inputObj); err != nil {
common.SysError("tool call function arguments is not a map[string]any: " + fmt.Sprintf("%v", toolCall.Function.Arguments))
@@ -393,7 +386,7 @@ func ResponseClaude2OpenAI(reqMode int, claudeResponse *ClaudeResponse) *dto.Ope
}
choice.SetStringContent(responseText)
if len(tools) > 0 {
choice.Message.ToolCalls = tools
choice.Message.SetToolCalls(tools)
}
fullTextResponse.Model = claudeResponse.Model
choices = append(choices, choice)

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

@@ -57,7 +57,11 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, re
if request == nil {
return nil, errors.New("request is nil")
}
return CovertGemini2OpenAI(*request), nil
ai, err := CovertGemini2OpenAI(*request)
if err != nil {
return nil, err
}
return ai, nil
}
func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) {
@@ -72,7 +76,7 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
if info.IsStream {
err, usage = GeminiChatStreamHandler(c, resp, info)
} else {
err, usage = GeminiChatHandler(c, resp)
err, usage = GeminiChatHandler(c, resp, info)
}
return
}

View File

@@ -1,13 +1,19 @@
package gemini
const (
GeminiVisionMaxImageNum = 16
)
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-exp-1114",
// stable version
"gemini-1.5-pro", "gemini-1.5-flash", "gemini-1.5-flash-8b",
// latest version
"gemini-1.5-pro-latest", "gemini-1.5-flash-latest",
// legacy version
"gemini-1.5-pro-exp-0827", "gemini-1.5-flash-exp-0827",
// exp
"gemini-exp-1114", "gemini-exp-1121", "gemini-exp-1206",
// flash exp
"gemini-2.0-flash-exp",
// thinking exp
"gemini-2.0-flash-thinking-exp",
"gemini-2.0-flash-thinking-exp-1219",
}
var ChannelName = "google gemini"

View File

@@ -1,10 +1,11 @@
package gemini
type GeminiChatRequest struct {
Contents []GeminiChatContent `json:"contents"`
SafetySettings []GeminiChatSafetySettings `json:"safety_settings,omitempty"`
GenerationConfig GeminiChatGenerationConfig `json:"generation_config,omitempty"`
Tools []GeminiChatTools `json:"tools,omitempty"`
Contents []GeminiChatContent `json:"contents"`
SafetySettings []GeminiChatSafetySettings `json:"safety_settings,omitempty"`
GenerationConfig GeminiChatGenerationConfig `json:"generation_config,omitempty"`
Tools []GeminiChatTool `json:"tools,omitempty"`
SystemInstructions *GeminiChatContent `json:"system_instruction,omitempty"`
}
type GeminiInlineData struct {
@@ -17,10 +18,39 @@ type FunctionCall struct {
Arguments any `json:"args"`
}
type GeminiFunctionResponseContent struct {
Name string `json:"name"`
Content any `json:"content"`
}
type FunctionResponse struct {
Name string `json:"name"`
Response GeminiFunctionResponseContent `json:"response"`
}
type GeminiPartExecutableCode struct {
Language string `json:"language,omitempty"`
Code string `json:"code,omitempty"`
}
type GeminiPartCodeExecutionResult struct {
Outcome string `json:"outcome,omitempty"`
Output string `json:"output,omitempty"`
}
type GeminiFileData struct {
MimeType string `json:"mimeType,omitempty"`
FileUri string `json:"fileUri,omitempty"`
}
type GeminiPart struct {
Text string `json:"text,omitempty"`
InlineData *GeminiInlineData `json:"inlineData,omitempty"`
FunctionCall *FunctionCall `json:"functionCall,omitempty"`
Text string `json:"text,omitempty"`
InlineData *GeminiInlineData `json:"inlineData,omitempty"`
FunctionCall *FunctionCall `json:"functionCall,omitempty"`
FunctionResponse *FunctionResponse `json:"functionResponse,omitempty"`
FileData *GeminiFileData `json:"fileData,omitempty"`
ExecutableCode *GeminiPartExecutableCode `json:"executableCode,omitempty"`
CodeExecutionResult *GeminiPartCodeExecutionResult `json:"codeExecutionResult,omitempty"`
}
type GeminiChatContent struct {
@@ -33,22 +63,28 @@ type GeminiChatSafetySettings struct {
Threshold string `json:"threshold"`
}
type GeminiChatTools struct {
FunctionDeclarations any `json:"functionDeclarations,omitempty"`
type GeminiChatTool struct {
GoogleSearch any `json:"googleSearch,omitempty"`
GoogleSearchRetrieval any `json:"googleSearchRetrieval,omitempty"`
CodeExecution any `json:"codeExecution,omitempty"`
FunctionDeclarations any `json:"functionDeclarations,omitempty"`
}
type GeminiChatGenerationConfig struct {
Temperature float64 `json:"temperature,omitempty"`
TopP float64 `json:"topP,omitempty"`
TopK float64 `json:"topK,omitempty"`
MaxOutputTokens uint `json:"maxOutputTokens,omitempty"`
CandidateCount int `json:"candidateCount,omitempty"`
StopSequences []string `json:"stopSequences,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
TopP float64 `json:"topP,omitempty"`
TopK float64 `json:"topK,omitempty"`
MaxOutputTokens uint `json:"maxOutputTokens,omitempty"`
CandidateCount int `json:"candidateCount,omitempty"`
StopSequences []string `json:"stopSequences,omitempty"`
ResponseMimeType string `json:"responseMimeType,omitempty"`
ResponseSchema any `json:"responseSchema,omitempty"`
Seed int64 `json:"seed,omitempty"`
}
type GeminiChatCandidate struct {
Content GeminiChatContent `json:"content"`
FinishReason string `json:"finishReason"`
FinishReason *string `json:"finishReason"`
Index int64 `json:"index"`
SafetyRatings []GeminiChatSafetyRating `json:"safetyRatings"`
}

View File

@@ -4,18 +4,21 @@ import (
"bufio"
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"io"
"net/http"
"one-api/common"
"one-api/constant"
"one-api/dto"
relaycommon "one-api/relay/common"
"one-api/service"
"strings"
"github.com/gin-gonic/gin"
)
// Setting safety to the lowest possible values since Gemini is already powerless enough
func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) *GeminiChatRequest {
func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) (*GeminiChatRequest, error) {
geminiRequest := GeminiChatRequest{
Contents: make([]GeminiChatContent, 0, len(textRequest.Messages)),
SafetySettings: []GeminiChatSafetySettings{
@@ -35,53 +38,156 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) *GeminiChatReques
Category: "HARM_CATEGORY_DANGEROUS_CONTENT",
Threshold: common.GeminiSafetySetting,
},
{
Category: "HARM_CATEGORY_CIVIC_INTEGRITY",
Threshold: common.GeminiSafetySetting,
},
},
GenerationConfig: GeminiChatGenerationConfig{
Temperature: textRequest.Temperature,
TopP: textRequest.TopP,
MaxOutputTokens: textRequest.MaxTokens,
Seed: int64(textRequest.Seed),
},
}
// openaiContent.FuncToToolCalls()
if textRequest.Tools != nil {
functions := make([]dto.FunctionCall, 0, len(textRequest.Tools))
googleSearch := false
codeExecution := false
for _, tool := range textRequest.Tools {
if tool.Function.Name == "googleSearch" {
googleSearch = true
continue
}
if tool.Function.Name == "codeExecution" {
codeExecution = true
continue
}
if tool.Function.Parameters != nil {
params, ok := tool.Function.Parameters.(map[string]interface{})
if ok {
if props, hasProps := params["properties"].(map[string]interface{}); hasProps {
if len(props) == 0 {
tool.Function.Parameters = nil
}
}
}
}
functions = append(functions, tool.Function)
}
geminiRequest.Tools = []GeminiChatTools{
{
FunctionDeclarations: functions,
},
if codeExecution {
geminiRequest.Tools = append(geminiRequest.Tools, GeminiChatTool{
CodeExecution: make(map[string]string),
})
}
if googleSearch {
geminiRequest.Tools = append(geminiRequest.Tools, GeminiChatTool{
GoogleSearch: make(map[string]string),
})
}
if len(functions) > 0 {
geminiRequest.Tools = append(geminiRequest.Tools, GeminiChatTool{
FunctionDeclarations: functions,
})
}
// common.SysLog("tools: " + fmt.Sprintf("%+v", geminiRequest.Tools))
// json_data, _ := json.Marshal(geminiRequest.Tools)
// common.SysLog("tools_json: " + string(json_data))
} else if textRequest.Functions != nil {
geminiRequest.Tools = []GeminiChatTools{
geminiRequest.Tools = []GeminiChatTool{
{
FunctionDeclarations: textRequest.Functions,
},
}
}
shouldAddDummyModelMessage := false
if textRequest.ResponseFormat != nil && (textRequest.ResponseFormat.Type == "json_schema" || textRequest.ResponseFormat.Type == "json_object") {
geminiRequest.GenerationConfig.ResponseMimeType = "application/json"
if textRequest.ResponseFormat.JsonSchema != nil && textRequest.ResponseFormat.JsonSchema.Schema != nil {
cleanedSchema := removeAdditionalPropertiesWithDepth(textRequest.ResponseFormat.JsonSchema.Schema, 0)
geminiRequest.GenerationConfig.ResponseSchema = cleanedSchema
}
}
tool_call_ids := make(map[string]string)
var system_content []string
//shouldAddDummyModelMessage := false
for _, message := range textRequest.Messages {
if message.Role == "system" {
system_content = append(system_content, message.StringContent())
continue
} else if message.Role == "tool" || message.Role == "function" {
if len(geminiRequest.Contents) == 0 || geminiRequest.Contents[len(geminiRequest.Contents)-1].Role == "model" {
geminiRequest.Contents = append(geminiRequest.Contents, GeminiChatContent{
Role: "user",
})
}
var parts = &geminiRequest.Contents[len(geminiRequest.Contents)-1].Parts
name := ""
if message.Name != nil {
name = *message.Name
} else if val, exists := tool_call_ids[message.ToolCallId]; exists {
name = val
}
content := common.StrToMap(message.StringContent())
functionResp := &FunctionResponse{
Name: name,
Response: GeminiFunctionResponseContent{
Name: name,
Content: content,
},
}
if content == nil {
functionResp.Response.Content = message.StringContent()
}
*parts = append(*parts, GeminiPart{
FunctionResponse: functionResp,
})
continue
}
var parts []GeminiPart
content := GeminiChatContent{
Role: message.Role,
Parts: []GeminiPart{
{
Text: message.StringContent(),
},
},
}
// isToolCall := false
if message.ToolCalls != nil {
// message.Role = "model"
// isToolCall = true
for _, call := range message.ParseToolCalls() {
args := map[string]interface{}{}
if call.Function.Arguments != "" {
if json.Unmarshal([]byte(call.Function.Arguments), &args) != nil {
return nil, fmt.Errorf("invalid arguments for function %s, args: %s", call.Function.Name, call.Function.Arguments)
}
}
toolCall := GeminiPart{
FunctionCall: &FunctionCall{
FunctionName: call.Function.Name,
Arguments: args,
},
}
parts = append(parts, toolCall)
tool_call_ids[call.ID] = call.Function.Name
}
}
openaiContent := message.ParseContent()
var parts []GeminiPart
imageNum := 0
for _, part := range openaiContent {
if part.Type == dto.ContentTypeText {
if part.Text == "" {
continue
}
parts = append(parts, GeminiPart{
Text: part.Text,
})
} else if part.Type == dto.ContentTypeImageURL {
imageNum += 1
if imageNum > GeminiVisionMaxImageNum {
continue
if constant.GeminiVisionMaxImageNum != -1 && imageNum > constant.GeminiVisionMaxImageNum {
return nil, fmt.Errorf("too many images in the message, max allowed is %d", constant.GeminiVisionMaxImageNum)
}
// 判断是否是url
if strings.HasPrefix(part.ImageUrl.(dto.MessageImageUrl).Url, "http") {
@@ -96,7 +202,7 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) *GeminiChatReques
} else {
_, format, base64String, err := service.DecodeBase64ImageData(part.ImageUrl.(dto.MessageImageUrl).Url)
if err != nil {
continue
return nil, fmt.Errorf("decode base64 image data failed: %s", err.Error())
}
parts = append(parts, GeminiPart{
InlineData: &GeminiInlineData{
@@ -107,59 +213,86 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) *GeminiChatReques
}
}
}
content.Parts = parts
// there's no assistant role in gemini and API shall vomit if Role is not user or model
if content.Role == "assistant" {
content.Role = "model"
}
// Converting system prompt to prompt from user for the same reason
if content.Role == "system" {
content.Role = "user"
shouldAddDummyModelMessage = true
}
geminiRequest.Contents = append(geminiRequest.Contents, content)
}
// If a system message is the last message, we need to add a dummy model message to make gemini happy
if shouldAddDummyModelMessage {
geminiRequest.Contents = append(geminiRequest.Contents, GeminiChatContent{
Role: "model",
Parts: []GeminiPart{
{
Text: "Okay",
},
if len(system_content) > 0 {
geminiRequest.SystemInstructions = &GeminiChatContent{
Parts: []GeminiPart{
{
Text: strings.Join(system_content, "\n"),
},
})
shouldAddDummyModelMessage = false
},
}
}
return &geminiRequest
return &geminiRequest, nil
}
func (g *GeminiChatResponse) GetResponseText() string {
if g == nil {
return ""
func removeAdditionalPropertiesWithDepth(schema interface{}, depth int) interface{} {
if depth >= 5 {
return schema
}
if len(g.Candidates) > 0 && len(g.Candidates[0].Content.Parts) > 0 {
return g.Candidates[0].Content.Parts[0].Text
v, ok := schema.(map[string]interface{})
if !ok || len(v) == 0 {
return schema
}
return ""
// 删除所有的title字段
delete(v, "title")
// 如果type不为object和array则直接返回
if typeVal, exists := v["type"]; !exists || (typeVal != "object" && typeVal != "array") {
return schema
}
switch v["type"] {
case "object":
delete(v, "additionalProperties")
// 处理 properties
if properties, ok := v["properties"].(map[string]interface{}); ok {
for key, value := range properties {
properties[key] = removeAdditionalPropertiesWithDepth(value, depth+1)
}
}
for _, field := range []string{"allOf", "anyOf", "oneOf"} {
if nested, ok := v[field].([]interface{}); ok {
for i, item := range nested {
nested[i] = removeAdditionalPropertiesWithDepth(item, depth+1)
}
}
}
case "array":
if items, ok := v["items"].(map[string]interface{}); ok {
v["items"] = removeAdditionalPropertiesWithDepth(items, depth+1)
}
}
return v
}
func getToolCalls(candidate *GeminiChatCandidate) []dto.ToolCall {
var toolCalls []dto.ToolCall
// func (g *GeminiChatResponse) GetResponseText() string {
// if g == nil {
// return ""
// }
// if len(g.Candidates) > 0 && len(g.Candidates[0].Content.Parts) > 0 {
// return g.Candidates[0].Content.Parts[0].Text
// }
// return ""
// }
item := candidate.Content.Parts[0]
if item.FunctionCall == nil {
return toolCalls
}
func getToolCall(item *GeminiPart) *dto.ToolCall {
argsBytes, err := json.Marshal(item.FunctionCall.Arguments)
if err != nil {
//common.SysError("getToolCalls failed: " + err.Error())
return toolCalls
//common.SysError("getToolCall failed: " + err.Error())
return nil
}
toolCall := dto.ToolCall{
return &dto.ToolCall{
ID: fmt.Sprintf("call_%s", common.GetUUID()),
Type: "function",
Function: dto.FunctionCall{
@@ -167,10 +300,32 @@ func getToolCalls(candidate *GeminiChatCandidate) []dto.ToolCall {
Name: item.FunctionCall.FunctionName,
},
}
toolCalls = append(toolCalls, toolCall)
return toolCalls
}
// func getToolCalls(candidate *GeminiChatCandidate, index int) []dto.ToolCall {
// var toolCalls []dto.ToolCall
// item := candidate.Content.Parts[index]
// if item.FunctionCall == nil {
// return toolCalls
// }
// argsBytes, err := json.Marshal(item.FunctionCall.Arguments)
// if err != nil {
// //common.SysError("getToolCalls failed: " + err.Error())
// return toolCalls
// }
// toolCall := dto.ToolCall{
// ID: fmt.Sprintf("call_%s", common.GetUUID()),
// Type: "function",
// Function: dto.FunctionCall{
// Arguments: string(argsBytes),
// Name: item.FunctionCall.FunctionName,
// },
// }
// toolCalls = append(toolCalls, toolCall)
// return toolCalls
// }
func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResponse {
fullTextResponse := dto.OpenAITextResponse{
Id: fmt.Sprintf("chatcmpl-%s", common.GetUUID()),
@@ -179,49 +334,127 @@ func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResp
Choices: make([]dto.OpenAITextResponseChoice, 0, len(response.Candidates)),
}
content, _ := json.Marshal("")
for i, candidate := range response.Candidates {
is_tool_call := false
for _, candidate := range response.Candidates {
choice := dto.OpenAITextResponseChoice{
Index: i,
Index: int(candidate.Index),
Message: dto.Message{
Role: "assistant",
Content: content,
},
FinishReason: relaycommon.StopFinishReason,
FinishReason: constant.FinishReasonStop,
}
if len(candidate.Content.Parts) > 0 {
if candidate.Content.Parts[0].FunctionCall != nil {
choice.Message.ToolCalls = getToolCalls(&candidate)
} else {
choice.Message.SetStringContent(candidate.Content.Parts[0].Text)
var texts []string
var tool_calls []dto.ToolCall
for _, part := range candidate.Content.Parts {
if part.FunctionCall != nil {
choice.FinishReason = constant.FinishReasonToolCalls
if call := getToolCall(&part); call != nil {
tool_calls = append(tool_calls, *call)
}
} else {
if part.ExecutableCode != nil {
texts = append(texts, "```"+part.ExecutableCode.Language+"\n"+part.ExecutableCode.Code+"\n```")
} else if part.CodeExecutionResult != nil {
texts = append(texts, "```output\n"+part.CodeExecutionResult.Output+"\n```")
} else {
// 过滤掉空行
if part.Text != "\n" {
texts = append(texts, part.Text)
}
}
}
}
if len(tool_calls) > 0 {
choice.Message.SetToolCalls(tool_calls)
is_tool_call = true
}
// 过滤掉空行
choice.Message.SetStringContent(strings.Join(texts, "\n"))
}
if candidate.FinishReason != nil {
switch *candidate.FinishReason {
case "STOP":
choice.FinishReason = constant.FinishReasonStop
case "MAX_TOKENS":
choice.FinishReason = constant.FinishReasonLength
default:
choice.FinishReason = constant.FinishReasonContentFilter
}
}
if is_tool_call {
choice.FinishReason = constant.FinishReasonToolCalls
}
fullTextResponse.Choices = append(fullTextResponse.Choices, choice)
}
return &fullTextResponse
}
func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) *dto.ChatCompletionsStreamResponse {
var choice dto.ChatCompletionsStreamResponseChoice
//choice.Delta.SetContentString(geminiResponse.GetResponseText())
if len(geminiResponse.Candidates) > 0 && len(geminiResponse.Candidates[0].Content.Parts) > 0 {
respFirst := geminiResponse.Candidates[0].Content.Parts[0]
if respFirst.FunctionCall != nil {
// function response
choice.Delta.ToolCalls = getToolCalls(&geminiResponse.Candidates[0])
} else {
// text response
choice.Delta.SetContentString(respFirst.Text)
func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) (*dto.ChatCompletionsStreamResponse, bool) {
choices := make([]dto.ChatCompletionsStreamResponseChoice, 0, len(geminiResponse.Candidates))
is_stop := false
for _, candidate := range geminiResponse.Candidates {
if candidate.FinishReason != nil && *candidate.FinishReason == "STOP" {
is_stop = true
candidate.FinishReason = nil
}
choice := dto.ChatCompletionsStreamResponseChoice{
Index: int(candidate.Index),
Delta: dto.ChatCompletionsStreamResponseChoiceDelta{
Role: "assistant",
},
}
var texts []string
isTools := false
if candidate.FinishReason != nil {
// p := GeminiConvertFinishReason(*candidate.FinishReason)
switch *candidate.FinishReason {
case "STOP":
choice.FinishReason = &constant.FinishReasonStop
case "MAX_TOKENS":
choice.FinishReason = &constant.FinishReasonLength
default:
choice.FinishReason = &constant.FinishReasonContentFilter
}
}
for _, part := range candidate.Content.Parts {
if part.FunctionCall != nil {
isTools = true
if call := getToolCall(&part); call != nil {
choice.Delta.ToolCalls = append(choice.Delta.ToolCalls, *call)
}
} else {
if part.ExecutableCode != nil {
texts = append(texts, "```"+part.ExecutableCode.Language+"\n"+part.ExecutableCode.Code+"\n```\n")
} else if part.CodeExecutionResult != nil {
texts = append(texts, "```output\n"+part.CodeExecutionResult.Output+"\n```\n")
} else {
if part.Text != "\n" {
texts = append(texts, part.Text)
}
}
}
}
choice.Delta.SetContentString(strings.Join(texts, "\n"))
if isTools {
choice.FinishReason = &constant.FinishReasonToolCalls
}
choices = append(choices, choice)
}
var response dto.ChatCompletionsStreamResponse
response.Object = "chat.completion.chunk"
response.Model = "gemini"
response.Choices = []dto.ChatCompletionsStreamResponseChoice{choice}
return &response
response.Choices = choices
return &response, is_stop
}
func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
responseText := ""
// responseText := ""
id := fmt.Sprintf("chatcmpl-%s", common.GetUUID())
createAt := common.GetTimestamp()
var usage = &dto.Usage{}
@@ -245,13 +478,11 @@ func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycom
continue
}
response := streamResponseGeminiChat2OpenAI(&geminiResponse)
if response == nil {
continue
}
response, is_stop := streamResponseGeminiChat2OpenAI(&geminiResponse)
response.Id = id
response.Created = createAt
responseText += response.Choices[0].Delta.GetContentString()
response.Model = info.UpstreamModelName
// responseText += response.Choices[0].Delta.GetContentString()
if geminiResponse.UsageMetadata.TotalTokenCount != 0 {
usage.PromptTokens = geminiResponse.UsageMetadata.PromptTokenCount
usage.CompletionTokens = geminiResponse.UsageMetadata.CandidatesTokenCount
@@ -260,12 +491,17 @@ func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycom
if err != nil {
common.LogError(c, err.Error())
}
if is_stop {
response := service.GenerateStopResponse(id, createAt, info.UpstreamModelName, constant.FinishReasonStop)
service.ObjectData(c, response)
}
}
response := service.GenerateStopResponse(id, createAt, info.UpstreamModelName, relaycommon.StopFinishReason)
service.ObjectData(c, response)
var response *dto.ChatCompletionsStreamResponse
usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens
usage.PromptTokensDetails.TextTokens = usage.PromptTokens
usage.CompletionTokenDetails.TextTokens = usage.CompletionTokens
if info.ShouldIncludeUsage {
response = service.GenerateFinalUsageResponse(id, createAt, info.UpstreamModelName, *usage)
@@ -279,7 +515,7 @@ func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycom
return nil, usage
}
func GeminiChatHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
func GeminiChatHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
@@ -305,6 +541,7 @@ func GeminiChatHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWit
}, nil
}
fullTextResponse := responseGeminiChat2OpenAI(&geminiResponse)
fullTextResponse.Model = info.UpstreamModelName
usage := dto.Usage{
PromptTokens: geminiResponse.UsageMetadata.PromptTokenCount,
CompletionTokens: geminiResponse.UsageMetadata.CandidatesTokenCount,

View File

@@ -106,15 +106,22 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, re
if request == nil {
return nil, errors.New("request is nil")
}
if info.ChannelType != common.ChannelTypeOpenAI {
if info.ChannelType != common.ChannelTypeOpenAI && info.ChannelType != common.ChannelTypeAzure {
request.StreamOptions = nil
}
if strings.HasPrefix(request.Model, "o1-") {
if strings.HasPrefix(request.Model, "o1") {
if request.MaxCompletionTokens == 0 && request.MaxTokens != 0 {
request.MaxCompletionTokens = request.MaxTokens
request.MaxTokens = 0
}
}
if request.Model == "o1" || request.Model == "o1-2024-12-17" {
//修改第一个Message的内容将system改为developer
if len(request.Messages) > 0 && request.Messages[0].Role == "system" {
request.Messages[0].Role = "developer"
}
}
return request, nil
}

View File

@@ -13,6 +13,7 @@ var ModelList = []string{
"gpt-4o-mini", "gpt-4o-mini-2024-07-18",
"o1-preview", "o1-preview-2024-09-12",
"o1-mini", "o1-mini-2024-09-12",
"o1", "o1-2024-12-17",
"gpt-4o-audio-preview", "gpt-4o-audio-preview-2024-10-01",
"gpt-4o-realtime-preview", "gpt-4o-realtime-preview-2024-10-01",
"text-embedding-ada-002", "text-embedding-3-small", "text-embedding-3-large",

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())
}
@@ -105,7 +133,7 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
}
}
if shouldSendLastResp {
service.StringData(c, lastStreamData)
sendStreamData(c, lastStreamData, forceFormat)
}
// 计算token
@@ -375,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
@@ -390,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():
@@ -445,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():
@@ -568,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

@@ -135,7 +135,10 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, re
c.Set("request_model", request.Model)
return vertexClaudeReq, nil
} else if a.RequestMode == RequestModeGemini {
geminiRequest := gemini.CovertGemini2OpenAI(*request)
geminiRequest, err := gemini.CovertGemini2OpenAI(*request)
if err != nil {
return nil, err
}
c.Set("request_model", request.Model)
return geminiRequest, nil
} else if a.RequestMode == RequestModeLlama {
@@ -167,7 +170,7 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
case RequestModeClaude:
err, usage = claude.ClaudeHandler(c, resp, claude.RequestModeMessage, info)
case RequestModeGemini:
err, usage = gemini.GeminiChatHandler(c, resp)
err, usage = gemini.GeminiChatHandler(c, resp, info)
case RequestModeLlama:
err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.OriginModelName)
}
@@ -176,7 +179,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,13 +1,15 @@
package common
import (
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"one-api/common"
"one-api/constant"
"one-api/dto"
"one-api/relay/constant"
relayconstant "one-api/relay/constant"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
type RelayInfo struct {
@@ -43,6 +45,7 @@ type RelayInfo struct {
RealtimeTools []dto.RealTimeTool
IsFirstRequest bool
AudioUsage bool
ChannelSetting map[string]interface{}
}
func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo {
@@ -57,19 +60,20 @@ 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")
startTime := time.Now()
startTime := c.GetTime(constant.ContextKeyRequestStartTime)
// firstResponseTime = time.Now() - 1 second
apiType, _ := constant.ChannelType2APIType(channelType)
apiType, _ := relayconstant.ChannelType2APIType(channelType)
info := &RelayInfo{
RelayMode: constant.Path2RelayMode(c.Request.URL.Path),
RelayMode: relayconstant.Path2RelayMode(c.Request.URL.Path),
BaseUrl: c.GetString("base_url"),
RequestURLPath: c.Request.URL.String(),
ChannelType: channelType,
@@ -87,6 +91,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
@@ -104,7 +109,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
}
if info.ChannelType == common.ChannelTypeOpenAI || info.ChannelType == common.ChannelTypeAnthropic ||
info.ChannelType == common.ChannelTypeAws || info.ChannelType == common.ChannelTypeGemini ||
info.ChannelType == common.ChannelCloudflare {
info.ChannelType == common.ChannelCloudflare || info.ChannelType == common.ChannelTypeAzure {
info.SupportStreamOptions = true
}
return info
@@ -154,10 +159,10 @@ func GenTaskRelayInfo(c *gin.Context) *TaskRelayInfo {
group := c.GetString("group")
startTime := time.Now()
apiType, _ := constant.ChannelType2APIType(channelType)
apiType, _ := relayconstant.ChannelType2APIType(channelType)
info := &TaskRelayInfo{
RelayMode: constant.Path2RelayMode(c.Request.URL.Path),
RelayMode: relayconstant.Path2RelayMode(c.Request.URL.Path),
BaseUrl: c.GetString("base_url"),
RequestURLPath: c.Request.URL.String(),
ChannelType: channelType,

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

@@ -7,12 +7,12 @@ import (
"github.com/gin-gonic/gin"
"net/http"
"one-api/common"
"one-api/constant"
"one-api/dto"
"one-api/model"
relaycommon "one-api/relay/common"
relayconstant "one-api/relay/constant"
"one-api/service"
"one-api/setting"
)
func getAndValidAudioRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.AudioRequest, error) {
@@ -26,7 +26,7 @@ func getAndValidAudioRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.
if audioRequest.Model == "" {
return nil, errors.New("model is required")
}
if constant.ShouldCheckPromptSensitive() {
if setting.ShouldCheckPromptSensitive() {
err := service.CheckSensitiveInput(audioRequest.Input)
if err != nil {
return nil, err
@@ -74,7 +74,7 @@ func AudioHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
}
modelRatio := common.GetModelRatio(audioRequest.Model)
groupRatio := common.GetGroupRatio(relayInfo.Group)
groupRatio := setting.GetGroupRatio(relayInfo.Group)
ratio := modelRatio * groupRatio
preConsumedQuota := int(float64(preConsumedTokens) * ratio)
userQuota, err := model.CacheGetUserQuota(relayInfo.UserId)

View File

@@ -9,11 +9,11 @@ import (
"io"
"net/http"
"one-api/common"
"one-api/constant"
"one-api/dto"
"one-api/model"
relaycommon "one-api/relay/common"
"one-api/service"
"one-api/setting"
"strings"
)
@@ -59,7 +59,7 @@ func getAndValidImageRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.
//if imageRequest.N != 0 && (imageRequest.N < 1 || imageRequest.N > 10) {
// return service.OpenAIErrorWrapper(errors.New("n must be between 1 and 10"), "invalid_field_value", http.StatusBadRequest)
//}
if constant.ShouldCheckPromptSensitive() {
if setting.ShouldCheckPromptSensitive() {
err := service.CheckSensitiveInput(imageRequest.Prompt)
if err != nil {
return nil, err
@@ -99,7 +99,7 @@ func ImageHelper(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusCode {
modelPrice = 0.0025 * modelRatio
}
groupRatio := common.GetGroupRatio(relayInfo.Group)
groupRatio := setting.GetGroupRatio(relayInfo.Group)
userQuota, err := model.CacheGetUserQuota(relayInfo.UserId)
sizeRatio := 1.0

View File

@@ -15,6 +15,7 @@ import (
relaycommon "one-api/relay/common"
relayconstant "one-api/relay/constant"
"one-api/service"
"one-api/setting"
"strconv"
"strings"
"time"
@@ -111,8 +112,8 @@ func coverMidjourneyTaskDto(c *gin.Context, originTask *model.Midjourney) (midjo
midjourneyTask.StartTime = originTask.StartTime
midjourneyTask.FinishTime = originTask.FinishTime
midjourneyTask.ImageUrl = ""
if originTask.ImageUrl != "" && constant.MjForwardUrlEnabled {
midjourneyTask.ImageUrl = constant.ServerAddress + "/mj/image/" + originTask.MjId
if originTask.ImageUrl != "" && setting.MjForwardUrlEnabled {
midjourneyTask.ImageUrl = setting.ServerAddress + "/mj/image/" + originTask.MjId
if originTask.Status != "SUCCESS" {
midjourneyTask.ImageUrl += "?rand=" + strconv.FormatInt(time.Now().UnixNano(), 10)
}
@@ -167,7 +168,7 @@ func RelaySwapFace(c *gin.Context) *dto.MidjourneyResponse {
modelPrice = defaultPrice
}
}
groupRatio := common.GetGroupRatio(group)
groupRatio := setting.GetGroupRatio(group)
ratio := modelPrice * groupRatio
userQuota, err := model.CacheGetUserQuota(userId)
if err != nil {
@@ -207,7 +208,8 @@ func RelaySwapFace(c *gin.Context) *dto.MidjourneyResponse {
other := make(map[string]interface{})
other["model_price"] = modelPrice
other["group_ratio"] = groupRatio
model.RecordConsumeLog(ctx, userId, channelId, 0, 0, modelName, tokenName, quota, logContent, tokenId, userQuota, 0, false, other)
model.RecordConsumeLog(ctx, userId, channelId, 0, 0, modelName, tokenName,
quota, logContent, tokenId, userQuota, 0, false, group, other)
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
channelId := c.GetInt("channel_id")
model.UpdateChannelUsedQuota(channelId, quota)
@@ -421,7 +423,7 @@ func RelayMidjourneySubmit(c *gin.Context, relayMode int) *dto.MidjourneyRespons
if originTask == nil {
return service.MidjourneyErrorWrapper(constant.MjRequestError, "task_not_found")
} else { //原任务的Status=SUCCESS则可以做放大UPSCALE、变换VARIATION等动作此时必须使用原来的请求地址才能正确处理
if constant.MjActionCheckSuccessEnabled {
if setting.MjActionCheckSuccessEnabled {
if originTask.Status != "SUCCESS" && relayMode != relayconstant.RelayModeMidjourneyModal {
return service.MidjourneyErrorWrapper(constant.MjRequestError, "task_status_not_success")
}
@@ -472,7 +474,7 @@ func RelayMidjourneySubmit(c *gin.Context, relayMode int) *dto.MidjourneyRespons
modelPrice = defaultPrice
}
}
groupRatio := common.GetGroupRatio(group)
groupRatio := setting.GetGroupRatio(group)
ratio := modelPrice * groupRatio
userQuota, err := model.CacheGetUserQuota(userId)
if err != nil {
@@ -512,7 +514,8 @@ func RelayMidjourneySubmit(c *gin.Context, relayMode int) *dto.MidjourneyRespons
other := make(map[string]interface{})
other["model_price"] = modelPrice
other["group_ratio"] = groupRatio
model.RecordConsumeLog(ctx, userId, channelId, 0, 0, modelName, tokenName, quota, logContent, tokenId, userQuota, 0, false, other)
model.RecordConsumeLog(ctx, userId, channelId, 0, 0, modelName, tokenName,
quota, logContent, tokenId, userQuota, 0, false, group, other)
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
channelId := c.GetInt("channel_id")
model.UpdateChannelUsedQuota(channelId, quota)

View File

@@ -15,11 +15,10 @@ import (
relaycommon "one-api/relay/common"
relayconstant "one-api/relay/constant"
"one-api/service"
"one-api/setting"
"strings"
"time"
"github.com/bytedance/sonic"
"github.com/gin-gonic/gin"
)
@@ -95,24 +94,31 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
}
relayInfo.UpstreamModelName = textRequest.Model
modelPrice, getModelPriceSuccess := common.GetModelPrice(textRequest.Model, false)
groupRatio := common.GetGroupRatio(relayInfo.Group)
groupRatio := setting.GetGroupRatio(relayInfo.Group)
var preConsumedQuota int
var ratio float64
var modelRatio float64
//err := service.SensitiveWordsCheck(textRequest)
if constant.ShouldCheckPromptSensitive() {
if setting.ShouldCheckPromptSensitive() {
err = checkRequestSensitive(textRequest, relayInfo)
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "sensitive_words_detected", http.StatusBadRequest)
}
}
promptTokens, err := getPromptTokens(textRequest, relayInfo)
// count messages token error 计算promptTokens错误
if err != nil {
return service.OpenAIErrorWrapper(err, "count_token_messages_failed", http.StatusInternalServerError)
// 获取 promptTokens如果上下文中已经存在则直接使用
var promptTokens int
if value, exists := c.Get("prompt_tokens"); exists {
promptTokens = value.(int)
} else {
promptTokens, err = getPromptTokens(textRequest, relayInfo)
// count messages token error 计算promptTokens错误
if err != nil {
return service.OpenAIErrorWrapper(err, "count_token_messages_failed", http.StatusInternalServerError)
}
c.Set("prompt_tokens", promptTokens)
}
if !getModelPriceSuccess {
@@ -180,7 +186,7 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "convert_request_failed", http.StatusInternalServerError)
}
jsonData, err := sonic.Marshal(convertedRequest)
jsonData, err := json.Marshal(convertedRequest)
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError)
}
@@ -386,7 +392,7 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelN
}
other := service.GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, modelPrice)
model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, promptTokens, completionTokens, logModel,
tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, other)
tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, relayInfo.Group, other)
//if quota != 0 {
//

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

@@ -10,6 +10,7 @@ import (
"one-api/dto"
relaycommon "one-api/relay/common"
"one-api/service"
"one-api/setting"
)
func getRerankPromptToken(rerankRequest dto.RerankRequest) int {
@@ -57,7 +58,7 @@ func RerankHelper(c *gin.Context, relayMode int) (openaiErr *dto.OpenAIErrorWith
relayInfo.UpstreamModelName = rerankRequest.Model
modelPrice, success := common.GetModelPrice(rerankRequest.Model, false)
groupRatio := common.GetGroupRatio(relayInfo.Group)
groupRatio := setting.GetGroupRatio(relayInfo.Group)
var preConsumedQuota int
var ratio float64

View File

@@ -16,6 +16,7 @@ import (
relaycommon "one-api/relay/common"
relayconstant "one-api/relay/constant"
"one-api/service"
"one-api/setting"
)
/*
@@ -48,7 +49,7 @@ func RelayTaskSubmit(c *gin.Context, relayMode int) (taskErr *dto.TaskError) {
}
// 预扣
groupRatio := common.GetGroupRatio(relayInfo.Group)
groupRatio := setting.GetGroupRatio(relayInfo.Group)
ratio := modelPrice * groupRatio
userQuota, err := model.CacheGetUserQuota(relayInfo.UserId)
if err != nil {
@@ -126,7 +127,8 @@ func RelayTaskSubmit(c *gin.Context, relayMode int) (taskErr *dto.TaskError) {
other := make(map[string]interface{})
other["model_price"] = modelPrice
other["group_ratio"] = groupRatio
model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, 0, 0, modelName, tokenName, quota, logContent, relayInfo.TokenId, userQuota, 0, false, other)
model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, 0, 0,
modelName, tokenName, quota, logContent, relayInfo.TokenId, userQuota, 0, false, relayInfo.Group, other)
model.UpdateUserUsedQuotaAndRequestCount(relayInfo.UserId, quota)
model.UpdateChannelUsedQuota(relayInfo.ChannelId, quota)
}

View File

@@ -10,6 +10,7 @@ import (
"one-api/dto"
relaycommon "one-api/relay/common"
"one-api/service"
"one-api/setting"
)
//func getAndValidateWssRequest(c *gin.Context, ws *websocket.Conn) (*dto.RealtimeEvent, error) {
@@ -57,7 +58,7 @@ func WssHelper(c *gin.Context, ws *websocket.Conn) (openaiErr *dto.OpenAIErrorWi
}
//relayInfo.UpstreamModelName = textRequest.Model
modelPrice, getModelPriceSuccess := common.GetModelPrice(relayInfo.UpstreamModelName, false)
groupRatio := common.GetGroupRatio(relayInfo.Group)
groupRatio := setting.GetGroupRatio(relayInfo.Group)
var preConsumedQuota int
var ratio float64

View File

@@ -28,10 +28,10 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.GET("/oauth/linuxdo", middleware.CriticalRateLimit(), controller.LinuxdoOAuth)
apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), controller.GenerateOAuthCode)
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth)
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind)
apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind)
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), controller.WeChatBind)
apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), controller.EmailBind)
apiRouter.GET("/oauth/telegram/login", middleware.CriticalRateLimit(), controller.TelegramLogin)
apiRouter.GET("/oauth/telegram/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.TelegramBind)
apiRouter.GET("/oauth/telegram/bind", middleware.CriticalRateLimit(), controller.TelegramBind)
userRoute := apiRouter.Group("/user")
{
@@ -98,7 +98,8 @@ func SetApiRouter(router *gin.Engine) {
channelRoute.POST("/batch", controller.DeleteChannelBatch)
channelRoute.POST("/fix", controller.FixChannelsAbilities)
channelRoute.GET("/fetch_models/:id", controller.FetchUpstreamModels)
channelRoute.POST("/fetch_models", controller.FetchModels)
channelRoute.POST("/batch/tag", controller.BatchSetChannelTag)
}
tokenRoute := apiRouter.Group("/token")
tokenRoute.Use(middleware.UserAuth())

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

@@ -1,12 +1,12 @@
package service
import (
"one-api/constant"
"one-api/setting"
)
func GetCallbackAddress() string {
if constant.CustomCallbackAddress == "" {
return constant.ServerAddress
if setting.CustomCallbackAddress == "" {
return setting.ServerAddress
}
return constant.CustomCallbackAddress
return setting.CustomCallbackAddress
}

View File

@@ -11,6 +11,7 @@ import (
"one-api/constant"
"one-api/dto"
relayconstant "one-api/relay/constant"
"one-api/setting"
"strconv"
"strings"
"time"
@@ -167,16 +168,16 @@ func DoMidjourneyHttpRequest(c *gin.Context, timeout time.Duration, fullRequestU
if err != nil {
return MidjourneyErrorWithStatusCodeWrapper(constant.MjErrorUnknown, "read_request_body_failed", http.StatusInternalServerError), nullBytes, err
}
if !constant.MjAccountFilterEnabled {
if !setting.MjAccountFilterEnabled {
delete(mapResult, "accountFilter")
}
if !constant.MjNotifyEnabled {
if !setting.MjNotifyEnabled {
delete(mapResult, "notifyHook")
}
//req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
// make new request with mapResult
}
if constant.MjModeClearEnabled {
if setting.MjModeClearEnabled {
if prompt, ok := mapResult["prompt"].(string); ok {
prompt = strings.Replace(prompt, "--fast", "", -1)
prompt = strings.Replace(prompt, "--relax", "", -1)

View File

@@ -9,6 +9,7 @@ import (
"one-api/dto"
"one-api/model"
relaycommon "one-api/relay/common"
"one-api/setting"
"strings"
"time"
)
@@ -36,7 +37,7 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag
completionRatio := common.GetCompletionRatio(modelName)
audioRatio := common.GetAudioRatio(relayInfo.UpstreamModelName)
audioCompletionRatio := common.GetAudioCompletionRatio(modelName)
groupRatio := common.GetGroupRatio(relayInfo.Group)
groupRatio := setting.GetGroupRatio(relayInfo.Group)
modelRatio := common.GetModelRatio(modelName)
ratio := groupRatio * modelRatio
@@ -139,7 +140,7 @@ func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, mod
}
other := GenerateWssOtherInfo(ctx, relayInfo, usage, modelRatio, groupRatio, completionRatio, audioRatio, audioCompletionRatio, modelPrice)
model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, usage.InputTokens, usage.OutputTokens, logModel,
tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, other)
tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, relayInfo.Group, other)
}
func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
@@ -208,5 +209,5 @@ func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
}
other := GenerateAudioOtherInfo(ctx, relayInfo, usage, modelRatio, groupRatio, completionRatio, audioRatio, audioCompletionRatio, modelPrice)
model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, usage.PromptTokens, usage.CompletionTokens, logModel,
tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, other)
tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, relayInfo.Group, other)
}

View File

@@ -3,8 +3,8 @@ package service
import (
"errors"
"fmt"
"one-api/constant"
"one-api/dto"
"one-api/setting"
"strings"
)
@@ -56,7 +56,7 @@ func CheckSensitiveInput(input any) error {
// SensitiveWordContains 是否包含敏感词,返回是否包含敏感词和敏感词列表
func SensitiveWordContains(text string) (bool, []string) {
if len(constant.SensitiveWords) == 0 {
if len(setting.SensitiveWords) == 0 {
return false, nil
}
checkText := strings.ToLower(text)
@@ -75,7 +75,7 @@ func SensitiveWordContains(text string) (bool, []string) {
// SensitiveWordReplace 敏感词替换,返回是否包含敏感词和替换后的文本
func SensitiveWordReplace(text string, returnImmediately bool) (bool, []string, string) {
if len(constant.SensitiveWords) == 0 {
if len(setting.SensitiveWords) == 0 {
return false, nil, text
}
checkText := strings.ToLower(text)

View File

@@ -4,7 +4,7 @@ import (
"bytes"
"fmt"
goahocorasick "github.com/anknown/ahocorasick"
"one-api/constant"
"one-api/setting"
"strings"
)
@@ -70,7 +70,7 @@ func InitAc() *goahocorasick.Machine {
func readRunes() [][]rune {
var dict [][]rune
for _, word := range constant.SensitiveWords {
for _, word := range setting.SensitiveWords {
word = strings.ToLower(word)
l := bytes.TrimSpace([]byte(word))
dict = append(dict, bytes.Runes(l))

View File

@@ -19,42 +19,40 @@ import (
// tokenEncoderMap won't grow after initialization
var tokenEncoderMap = map[string]*tiktoken.Tiktoken{}
var defaultTokenEncoder *tiktoken.Tiktoken
var cl200kTokenEncoder *tiktoken.Tiktoken
var o200kTokenEncoder *tiktoken.Tiktoken
func InitTokenEncoders() {
common.SysLog("initializing token encoders")
gpt35TokenEncoder, err := tiktoken.EncodingForModel("gpt-3.5-turbo")
cl100TokenEncoder, err := tiktoken.GetEncoding(tiktoken.MODEL_CL100K_BASE)
if err != nil {
common.FatalLog(fmt.Sprintf("failed to get gpt-3.5-turbo token encoder: %s", err.Error()))
}
defaultTokenEncoder = gpt35TokenEncoder
gpt4TokenEncoder, err := tiktoken.EncodingForModel("gpt-4")
if err != nil {
common.FatalLog(fmt.Sprintf("failed to get gpt-4 token encoder: %s", err.Error()))
}
cl200kTokenEncoder, err = tiktoken.EncodingForModel("gpt-4o")
defaultTokenEncoder = cl100TokenEncoder
o200kTokenEncoder, err = tiktoken.GetEncoding(tiktoken.MODEL_O200K_BASE)
if err != nil {
common.FatalLog(fmt.Sprintf("failed to get gpt-4o token encoder: %s", err.Error()))
}
for model, _ := range common.GetDefaultModelRatioMap() {
if strings.HasPrefix(model, "gpt-3.5") {
tokenEncoderMap[model] = gpt35TokenEncoder
tokenEncoderMap[model] = cl100TokenEncoder
} else if strings.HasPrefix(model, "gpt-4") {
if strings.HasPrefix(model, "gpt-4o") {
tokenEncoderMap[model] = cl200kTokenEncoder
tokenEncoderMap[model] = o200kTokenEncoder
} else {
tokenEncoderMap[model] = gpt4TokenEncoder
tokenEncoderMap[model] = defaultTokenEncoder
}
} else if strings.HasPrefix(model, "o1") {
tokenEncoderMap[model] = o200kTokenEncoder
} else {
tokenEncoderMap[model] = nil
tokenEncoderMap[model] = defaultTokenEncoder
}
}
common.SysLog("token encoders initialized")
}
func getModelDefaultTokenEncoder(model string) *tiktoken.Tiktoken {
if strings.HasPrefix(model, "gpt-4o") || strings.HasPrefix(model, "chatgpt-4o") {
return cl200kTokenEncoder
if strings.HasPrefix(model, "gpt-4o") || strings.HasPrefix(model, "chatgpt-4o") || strings.HasPrefix(model, "o1") {
return o200kTokenEncoder
}
return defaultTokenEncoder
}
@@ -92,11 +90,11 @@ func getImageToken(imageUrl *dto.MessageImageUrl, model string, stream bool) (in
}
// TODO: 非流模式下不计算图片token数量
if !constant.GetMediaTokenNotStream && !stream {
return 1000, nil
return 256, nil
}
// 是否统计图片token
if !constant.GetMediaToken {
return 1000, nil
return 256, nil
}
// 同步One API的图片计费逻辑
if imageUrl.Detail == "auto" || imageUrl.Detail == "" {

View File

@@ -5,20 +5,20 @@ import (
"fmt"
"net/http"
"one-api/common"
"one-api/constant"
"one-api/setting"
"strings"
)
func DoImageRequest(originUrl string) (resp *http.Response, err error) {
if constant.EnableWorker() {
if setting.EnableWorker() {
common.SysLog(fmt.Sprintf("downloading image from worker: %s", originUrl))
workerUrl := constant.WorkerUrl
workerUrl := setting.WorkerUrl
if !strings.HasSuffix(workerUrl, "/") {
workerUrl += "/"
}
// post request to worker
data := []byte(`{"url":"` + originUrl + `","key":"` + constant.WorkerValidKey + `"}`)
return http.Post(constant.WorkerUrl, "application/json", bytes.NewBuffer(data))
data := []byte(`{"url":"` + originUrl + `","key":"` + setting.WorkerValidKey + `"}`)
return http.Post(setting.WorkerUrl, "application/json", bytes.NewBuffer(data))
} else {
common.SysLog(fmt.Sprintf("downloading image from origin: %s", originUrl))
return http.Get(originUrl)

View File

@@ -1,4 +1,4 @@
package constant
package setting
import (
"encoding/json"

View File

@@ -1,33 +1,47 @@
package common
package setting
import (
"encoding/json"
"errors"
"one-api/common"
)
var GroupRatio = map[string]float64{
var groupRatio = map[string]float64{
"default": 1,
"vip": 1,
"svip": 1,
}
func GetGroupRatioCopy() map[string]float64 {
groupRatioCopy := make(map[string]float64)
for k, v := range groupRatio {
groupRatioCopy[k] = v
}
return groupRatioCopy
}
func ContainsGroupRatio(name string) bool {
_, ok := groupRatio[name]
return ok
}
func GroupRatio2JSONString() string {
jsonBytes, err := json.Marshal(GroupRatio)
jsonBytes, err := json.Marshal(groupRatio)
if err != nil {
SysError("error marshalling model ratio: " + err.Error())
common.SysError("error marshalling model ratio: " + err.Error())
}
return string(jsonBytes)
}
func UpdateGroupRatioByJSONString(jsonStr string) error {
GroupRatio = make(map[string]float64)
return json.Unmarshal([]byte(jsonStr), &GroupRatio)
groupRatio = make(map[string]float64)
return json.Unmarshal([]byte(jsonStr), &groupRatio)
}
func GetGroupRatio(name string) float64 {
ratio, ok := GroupRatio[name]
ratio, ok := groupRatio[name]
if !ok {
SysError("group ratio not found: " + name)
common.SysError("group ratio not found: " + name)
return 1
}
return ratio

7
setting/midjourney.go Normal file
View File

@@ -0,0 +1,7 @@
package setting
var MjNotifyEnabled = false
var MjAccountFilterEnabled = false
var MjModeClearEnabled = false
var MjForwardUrlEnabled = true
var MjActionCheckSuccessEnabled = true

View File

@@ -1,4 +1,4 @@
package constant
package setting
var PayAddress = ""
var CustomCallbackAddress = ""

View File

@@ -1,4 +1,4 @@
package constant
package setting
import "strings"

View File

@@ -1,4 +1,4 @@
package constant
package setting
var ServerAddress = "http://localhost:3000"
var WorkerUrl = ""

View File

@@ -0,0 +1,52 @@
package setting
import (
"encoding/json"
"one-api/common"
)
var userUsableGroups = map[string]string{
"default": "默认分组",
"vip": "vip分组",
}
func GetUserUsableGroupsCopy() map[string]string {
copyUserUsableGroups := make(map[string]string)
for k, v := range userUsableGroups {
copyUserUsableGroups[k] = v
}
return copyUserUsableGroups
}
func UserUsableGroups2JSONString() string {
jsonBytes, err := json.Marshal(userUsableGroups)
if err != nil {
common.SysError("error marshalling user groups: " + err.Error())
}
return string(jsonBytes)
}
func UpdateUserUsableGroupsByJSONString(jsonStr string) error {
userUsableGroups = make(map[string]string)
return json.Unmarshal([]byte(jsonStr), &userUsableGroups)
}
func GetUserUsableGroups(userGroup string) map[string]string {
groupsCopy := GetUserUsableGroupsCopy()
if userGroup == "" {
if _, ok := groupsCopy["default"]; !ok {
groupsCopy["default"] = "default"
}
}
// 如果userGroup不在UserUsableGroups中返回UserUsableGroups + userGroup
if _, ok := groupsCopy[userGroup]; !ok {
groupsCopy[userGroup] = "用户分组"
}
// 如果userGroup在UserUsableGroups中返回UserUsableGroups
return groupsCopy
}
func GroupInUserUsableGroups(groupName string) bool {
_, ok := userUsableGroups[groupName]
return ok
}

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>

View File

@@ -36,43 +36,111 @@ import { IconList, IconTreeTriangleDown } from '@douyinfe/semi-icons';
import { loadChannelModels } from './utils.js';
import EditTagModal from '../pages/Channel/EditTagModal.js';
import TextNumberInput from './custom/TextNumberInput.js';
import { useTranslation } from 'react-i18next';
function renderTimestamp(timestamp) {
return <>{timestamp2string(timestamp)}</>;
}
let type2label = undefined;
function renderType(type) {
if (!type2label) {
type2label = new Map();
for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
}
type2label[0] = { value: 0, text: '未知类型', color: 'grey' };
}
return (
<Tag size="large" color={type2label[type]?.color}>
{type2label[type]?.text}
</Tag>
);
}
function renderTagType(type) {
return (
<Tag
color='light-blue'
prefixIcon={<IconList />}
size='large'
shape='circle'
type='light'
>
标签聚合
</Tag>
);
}
const ChannelsTable = () => {
const { t } = useTranslation();
let type2label = undefined;
const renderType = (type) => {
if (!type2label) {
type2label = new Map();
for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
}
type2label[0] = { value: 0, text: t('未知类型'), color: 'grey' };
}
return (
<Tag size="large" color={type2label[type]?.color}>
{type2label[type]?.text}
</Tag>
);
};
const renderTagType = () => {
return (
<Tag
color='light-blue'
prefixIcon={<IconList />}
size='large'
shape='circle'
type='light'
>
{t('标签聚合')}
</Tag>
);
};
const renderStatus = (status) => {
switch (status) {
case 1:
return (
<Tag size="large" color="green">
{t('已启用')}
</Tag>
);
case 2:
return (
<Tag size="large" color="yellow">
{t('已禁用')}
</Tag>
);
case 3:
return (
<Tag size="large" color="yellow">
{t('自动禁用')}
</Tag>
);
default:
return (
<Tag size="large" color="grey">
{t('未知状态')}
</Tag>
);
}
};
const renderResponseTime = (responseTime) => {
let time = responseTime / 1000;
time = time.toFixed(2) + t(' 秒');
if (responseTime === 0) {
return (
<Tag size="large" color="grey">
{t('未测试')}
</Tag>
);
} else if (responseTime <= 1000) {
return (
<Tag size="large" color="green">
{time}
</Tag>
);
} else if (responseTime <= 3000) {
return (
<Tag size="large" color="lime">
{time}
</Tag>
);
} else if (responseTime <= 5000) {
return (
<Tag size="large" color="yellow">
{time}
</Tag>
);
} else {
return (
<Tag size="large" color="red">
{time}
</Tag>
);
}
};
const columns = [
// {
// title: '',
@@ -80,41 +148,47 @@ const ChannelsTable = () => {
// className: 'checkbox',
// },
{
title: 'ID',
title: t('ID'),
dataIndex: 'id'
},
{
title: '名称',
title: t('名称'),
dataIndex: 'name'
},
{
title: '分组',
title: t('分组'),
dataIndex: 'group',
render: (text, record, index) => {
return (
<div>
<Space spacing={2}>
{text?.split(',').map((item, index) => {
return renderGroup(item);
})}
{text?.split(',')
.sort((a, b) => {
if (a === 'default') return -1;
if (b === 'default') return 1;
return a.localeCompare(b);
})
.map((item, index) => {
return renderGroup(item);
})}
</Space>
</div>
);
}
},
{
title: '类型',
title: t('类型'),
dataIndex: 'type',
render: (text, record, index) => {
if (record.children === undefined) {
return <>{renderType(text)}</>;
} else {
return <>{renderTagType(0)}</>;
return <>{renderTagType()}</>;
}
}
},
{
title: '状态',
title: t('状态'),
dataIndex: 'status',
render: (text, record, index) => {
if (text === 3) {
@@ -126,7 +200,7 @@ const ChannelsTable = () => {
let time = otherInfo['status_time'];
return (
<div>
<Tooltip content={'原因:' + reason + ',时间:' + timestamp2string(time)}>
<Tooltip content={t('原因:') + reason + t(',时间:') + timestamp2string(time)}>
{renderStatus(text)}
</Tooltip>
</div>
@@ -137,26 +211,26 @@ const ChannelsTable = () => {
}
},
{
title: '响应时间',
title: t('响应时间'),
dataIndex: 'response_time',
render: (text, record, index) => {
return <div>{renderResponseTime(text)}</div>;
}
},
{
title: '已用/剩余',
title: t('已用/剩余'),
dataIndex: 'expired_time',
render: (text, record, index) => {
if (record.children === undefined) {
return (
<div>
<Space spacing={1}>
<Tooltip content={'已用额度'}>
<Tooltip content={t('已用额度')}>
<Tag color="white" type="ghost" size="large">
{renderQuota(record.used_quota)}
</Tag>
</Tooltip>
<Tooltip content={'剩余额度' + record.balance + ',点击更新'}>
<Tooltip content={t('剩余额度') + record.balance + t(',点击更新')}>
<Tag
color="white"
type="ghost"
@@ -172,7 +246,7 @@ const ChannelsTable = () => {
</div>
);
} else {
return <Tooltip content={'已用额度'}>
return <Tooltip content={t('已用额度')}>
<Tag color="white" type="ghost" size="large">
{renderQuota(record.used_quota)}
</Tag>
@@ -181,7 +255,7 @@ const ChannelsTable = () => {
}
},
{
title: '优先级',
title: t('优先级'),
dataIndex: 'priority',
render: (text, record, index) => {
if (record.children === undefined) {
@@ -208,8 +282,8 @@ const ChannelsTable = () => {
keepFocus={true}
onBlur={(e) => {
Modal.warning({
title: '修改子渠道优先级',
content: '确定要修改所有子渠道优先级为 ' + e.target.value + ' 吗?',
title: t('修改子渠道优先级'),
content: t('确定要修改所有子渠道优先级为 ') + e.target.value + t(' 吗?'),
onOk: () => {
if (e.target.value === '') {
return;
@@ -230,7 +304,7 @@ const ChannelsTable = () => {
}
},
{
title: '权重',
title: t('权重'),
dataIndex: 'weight',
render: (text, record, index) => {
if (record.children === undefined) {
@@ -257,8 +331,8 @@ const ChannelsTable = () => {
keepFocus={true}
onBlur={(e) => {
Modal.warning({
title: '修改子渠道权重',
content: '确定要修改所有子渠道权重为 ' + e.target.value + ' 吗?',
title: t('修改子渠道权重'),
content: t('确定要修改所有子渠道权重为 ') + e.target.value + t(' 吗?'),
onOk: () => {
if (e.target.value === '') {
return;
@@ -287,7 +361,7 @@ const ChannelsTable = () => {
<div>
<SplitButtonGroup
style={{ marginRight: 1 }}
aria-label="测试单个渠道操作项目组"
aria-label={t('测试单个渠道操作项目组')}
>
<Button
theme="light"
@@ -295,7 +369,7 @@ const ChannelsTable = () => {
testChannel(record, '');
}}
>
测试
{t('测试')}
</Button>
<Dropdown
trigger="click"
@@ -309,10 +383,9 @@ const ChannelsTable = () => {
></Button>
</Dropdown>
</SplitButtonGroup>
{/*<Button theme='light' type='primary' style={{marginRight: 1}} onClick={()=>testChannel(record)}>测试</Button>*/}
<Popconfirm
title="确定是否要删除此渠道?"
content="此修改将不可逆"
title={t('确定是否要删除此渠道?')}
content={t('此修改将不可逆')}
okType={'danger'}
position={'left'}
onConfirm={() => {
@@ -322,7 +395,7 @@ const ChannelsTable = () => {
}}
>
<Button theme="light" type="danger" style={{ marginRight: 1 }}>
删除
{t('删除')}
</Button>
</Popconfirm>
{record.status === 1 ? (
@@ -334,7 +407,7 @@ const ChannelsTable = () => {
manageChannel(record.id, 'disable', record);
}}
>
禁用
{t('禁用')}
</Button>
) : (
<Button
@@ -345,7 +418,7 @@ const ChannelsTable = () => {
manageChannel(record.id, 'enable', record);
}}
>
启用
{t('启用')}
</Button>
)}
<Button
@@ -357,11 +430,11 @@ const ChannelsTable = () => {
setShowEdit(true);
}}
>
编辑
{t('编辑')}
</Button>
<Popconfirm
title="确定是否要复制此渠道?"
content="复制渠道的所有信息"
title={t('确定是否要复制此渠道?')}
content={t('复制渠道的所有信息')}
okType={'danger'}
position={'left'}
onConfirm={async () => {
@@ -369,7 +442,7 @@ const ChannelsTable = () => {
}}
>
<Button theme="light" type="primary" style={{ marginRight: 1 }}>
复制
{t('复制')}
</Button>
</Popconfirm>
</div>
@@ -385,7 +458,7 @@ const ChannelsTable = () => {
manageTag(record.key, 'enable');
}}
>
启用全部
{t('启用全部')}
</Button>
<Button
theme="light"
@@ -395,7 +468,7 @@ const ChannelsTable = () => {
manageTag(record.key, 'disable');
}}
>
禁用全部
{t('禁用全部')}
</Button>
<Button
theme="light"
@@ -406,7 +479,7 @@ const ChannelsTable = () => {
setEditingTag(record.key);
}}
>
编辑
{t('编辑')}
</Button>
</>
);
@@ -440,6 +513,8 @@ const ChannelsTable = () => {
const [selectedChannels, setSelectedChannels] = useState([]);
const [showEditPriority, setShowEditPriority] = useState(false);
const [enableTagMode, setEnableTagMode] = useState(false);
const [showBatchSetTag, setShowBatchSetTag] = useState(false);
const [batchSetTagValue, setBatchSetTagValue] = useState('');
const removeRecord = (record) => {
@@ -470,21 +545,21 @@ const ChannelsTable = () => {
let channelTags = {};
for (let i = 0; i < channels.length; i++) {
channels[i].key = '' + channels[i].id;
if (!enableTagMode) {
let test_models = [];
channels[i].models.split(',').forEach((item, index) => {
test_models.push({
node: 'item',
name: item,
onClick: () => {
testChannel(channels[i], item);
}
});
let test_models = [];
channels[i].models.split(',').forEach((item, index) => {
test_models.push({
node: 'item',
name: item,
onClick: () => {
testChannel(channels[i], item);
}
});
channels[i].test_models = test_models;
});
channels[i].test_models = test_models;
if (!enableTagMode) {
channelDates.push(channels[i]);
} else {
let tag = channels[i].tag;
let tag = channels[i].tag?channels[i].tag:"";
// find from channelTags
let tagIndex = channelTags[tag];
let tagChannelDates = undefined;
@@ -579,25 +654,25 @@ const ChannelsTable = () => {
const copySelectedChannel = async (record) => {
const channelToCopy = record
channelToCopy.name += '_复制';
channelToCopy.name += t('_复制');
channelToCopy.created_time = null;
channelToCopy.balance = 0;
channelToCopy.used_quota = 0;
if (!channelToCopy) {
showError('渠道未找到,请刷新页面后重试。');
showError(t('渠道未找到,请刷新页面后重试。'));
return;
}
try {
const newChannel = { ...channelToCopy, id: undefined };
const response = await API.post('/api/channel/', newChannel);
if (response.data.success) {
showSuccess('渠道复制成功');
showSuccess(t('渠道复制成功'));
await refresh();
} else {
showError(response.data.message);
}
} catch (error) {
showError('渠道复制失败: ' + error.message);
showError(t('渠道复制失败: ') + error.message);
}
};
@@ -656,7 +731,7 @@ const ChannelsTable = () => {
}
const { success, message } = res.data;
if (success) {
showSuccess('操作成功完成!');
showSuccess(t('操作成功完成!'));
let channel = res.data.data;
let newChannels = [...channels];
if (action === 'delete') {
@@ -703,72 +778,7 @@ const ChannelsTable = () => {
}
};
const renderStatus = (status) => {
switch (status) {
case 1:
return (
<Tag size="large" color="green">
已启用
</Tag>
);
case 2:
return (
<Tag size="large" color="yellow">
已禁用
</Tag>
);
case 3:
return (
<Tag size="large" color="yellow">
自动禁用
</Tag>
);
default:
return (
<Tag size="large" color="grey">
未知状态
</Tag>
);
}
};
const renderResponseTime = (responseTime) => {
let time = responseTime / 1000;
time = time.toFixed(2) + ' 秒';
if (responseTime === 0) {
return (
<Tag size="large" color="grey">
未测试
</Tag>
);
} else if (responseTime <= 1000) {
return (
<Tag size="large" color="green">
{time}
</Tag>
);
} else if (responseTime <= 3000) {
return (
<Tag size="large" color="lime">
{time}
</Tag>
);
} else if (responseTime <= 5000) {
return (
<Tag size="large" color="yellow">
{time}
</Tag>
);
} else {
return (
<Tag size="large" color="red">
{time}
</Tag>
);
}
};
const searchChannels = async (searchKeyword, searchGroup, searchModel) => {
const searchChannels = async (searchKeyword, searchGroup, searchModel, enableTagMode) => {
if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
await loadChannels(0, pageSize, idSort, enableTagMode);
setActivePage(1);
@@ -780,12 +790,7 @@ const ChannelsTable = () => {
);
const { success, message, data } = res.data;
if (success) {
if (enableTagMode) {
setChannelFormat(data, enableTagMode);
} else {
setChannels(data.map(channel => ({...channel, key: '' + channel.id})));
setChannelCount(data.length);
}
setChannelFormat(data, enableTagMode);
setActivePage(1);
} else {
showError(message);
@@ -799,7 +804,7 @@ const ChannelsTable = () => {
if (success) {
record.response_time = time * 1000;
record.test_time = Date.now() / 1000;
showInfo(`通道 ${record.name} 测试成功,耗时 ${time.toFixed(2)} 秒。`);
showInfo(t('通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。').replace('${name}', record.name).replace('${time.toFixed(2)}', time.toFixed(2)));
} else {
showError(message);
}
@@ -809,7 +814,7 @@ const ChannelsTable = () => {
const res = await API.get(`/api/channel/test`);
const { success, message } = res.data;
if (success) {
showInfo('已成功开始测试所有通道,请刷新页面查看结果。');
showInfo(t('已成功开始测试所有已启用通道,请刷新页面查看结果。'));
} else {
showError(message);
}
@@ -819,7 +824,7 @@ const ChannelsTable = () => {
const res = await API.delete(`/api/channel/disabled`);
const { success, message, data } = res.data;
if (success) {
showSuccess(`已删除所有禁用渠道,共计 ${data}`);
showSuccess(t('已删除所有禁用渠道,共计 ${data} 个').replace('${data}', data));
await refresh();
} else {
showError(message);
@@ -832,7 +837,7 @@ const ChannelsTable = () => {
if (success) {
record.balance = balance;
record.balance_updated_time = Date.now() / 1000;
showInfo(`通道 ${record.name} 余额更新成功!`);
showInfo(t('通道 ${name} 余额更新成功!').replace('${name}', record.name));
} else {
showError(message);
}
@@ -843,7 +848,7 @@ const ChannelsTable = () => {
const res = await API.get(`/api/channel/update_balance`);
const { success, message } = res.data;
if (success) {
showInfo('已更新完毕所有已启用通道余额!');
showInfo(t('已更新完毕所有已启用通道余额!'));
} else {
showError(message);
}
@@ -852,7 +857,7 @@ const ChannelsTable = () => {
const batchDeleteChannels = async () => {
if (selectedChannels.length === 0) {
showError('请先选择要删除的通道!');
showError(t('请先选择要删除的通道!'));
return;
}
setLoading(true);
@@ -863,7 +868,7 @@ const ChannelsTable = () => {
const res = await API.post(`/api/channel/batch`, { ids: ids });
const { success, message, data } = res.data;
if (success) {
showSuccess(`已删除 ${data} 个通道!`);
showSuccess(t('已删除 ${data} 个通道!').replace('${data}', data));
await refresh();
} else {
showError(message);
@@ -875,7 +880,7 @@ const ChannelsTable = () => {
const res = await API.post(`/api/channel/fix`);
const { success, message, data } = res.data;
if (success) {
showSuccess(`已修复 ${data} 个通道!`);
showSuccess(t('已修复 ${data} 个通道!').replace('${data}', data));
await refresh();
} else {
showError(message);
@@ -971,6 +976,29 @@ const ChannelsTable = () => {
}
};
const batchSetChannelTag = async () => {
if (selectedChannels.length === 0) {
showError(t('请先选择要设置标签的渠道!'));
return;
}
if (batchSetTagValue === '') {
showError(t('标签不能为空!'));
return;
}
let ids = selectedChannels.map(channel => channel.id);
const res = await API.post('/api/channel/batch/tag', {
ids: ids,
tag: batchSetTagValue === '' ? null : batchSetTagValue
});
if (res.data.success) {
showSuccess(t('已为 ${count} 个渠道设置标签!').replace('${count}', res.data.data));
await refresh();
setShowBatchSetTag(false);
} else {
showError(res.data.message);
}
};
return (
<>
<EditTagModal
@@ -987,7 +1015,7 @@ const ChannelsTable = () => {
/>
<Form
onSubmit={() => {
searchChannels(searchKeyword, searchGroup, searchModel);
searchChannels(searchKeyword, searchGroup, searchModel, enableTagMode);
}}
labelPosition="left"
>
@@ -995,8 +1023,8 @@ const ChannelsTable = () => {
<Space>
<Form.Input
field="search_keyword"
label="搜索渠道关键词"
placeholder="ID名称和密钥 ..."
label={t('搜索渠道关键词')}
placeholder={t('搜索渠道的 ID名称和密钥 ...')}
value={searchKeyword}
loading={searching}
onChange={(v) => {
@@ -1005,8 +1033,8 @@ const ChannelsTable = () => {
/>
<Form.Input
field="search_model"
label="模型"
placeholder="模型关键字"
label={t('模型')}
placeholder={t('模型关键字')}
value={searchModel}
loading={searching}
onChange={(v) => {
@@ -1015,22 +1043,22 @@ const ChannelsTable = () => {
/>
<Form.Select
field="group"
label="分组"
optionList={[{ label: '选择分组', value: null }, ...groupOptions]}
label={t('分组')}
optionList={[{ label: t('选择分组'), value: null }, ...groupOptions]}
initValue={null}
onChange={(v) => {
setSearchGroup(v);
searchChannels(searchKeyword, v, searchModel);
searchChannels(searchKeyword, v, searchModel, enableTagMode);
}}
/>
<Button
label="查询"
label={t('查询')}
type="primary"
htmlType="submit"
className="btn-margin-right"
style={{ marginRight: 8 }}
>
查询
{t('查询')}
</Button>
</Space>
</div>
@@ -1047,12 +1075,12 @@ const ChannelsTable = () => {
<Space
style={{ pointerEvents: 'auto', marginTop: isMobile() ? 0 : 45 }}
>
<Typography.Text strong>使用ID排序</Typography.Text>
<Typography.Text strong>{t('使用ID排序')}</Typography.Text>
<Switch
checked={idSort}
label="使用ID排序"
uncheckedText="关"
aria-label="是否用ID排序"
label={t('使用ID排序')}
uncheckedText={t('关')}
aria-label={t('是否用ID排序')}
onChange={(v) => {
localStorage.setItem('id-sort', v + '');
setIdSort(v);
@@ -1074,35 +1102,35 @@ const ChannelsTable = () => {
setShowEdit(true);
}}
>
添加渠道
{t('添加渠道')}
</Button>
<Popconfirm
title="确定?"
title={t('确定?')}
okType={'warning'}
onConfirm={testAllChannels}
position={isMobile() ? 'top' : 'top'}
>
<Button theme="light" type="warning" style={{ marginRight: 8 }}>
测试所有通道
{t('测试所有通道')}
</Button>
</Popconfirm>
<Popconfirm
title="确定?"
title={t('确定?')}
okType={'secondary'}
onConfirm={updateAllChannelsBalance}
>
<Button theme="light" type="secondary" style={{ marginRight: 8 }}>
更新所有已启用通道余额
{t('更新所有已启用通道余额')}
</Button>
</Popconfirm>
<Popconfirm
title="确定是否要删除禁用通道?"
content="此修改将不可逆"
title={t('确定是否要删除禁用通道?')}
content={t('此修改将不可逆')}
okType={'danger'}
onConfirm={deleteAllDisabledChannels}
>
<Button theme="light" type="danger" style={{ marginRight: 8 }}>
删除禁用通道
{t('删除禁用通道')}
</Button>
</Popconfirm>
@@ -1112,24 +1140,24 @@ const ChannelsTable = () => {
style={{ marginRight: 8 }}
onClick={refresh}
>
刷新
{t('刷新')}
</Button>
</Space>
</div>
<div style={{ marginTop: 20 }}>
<Space>
<Typography.Text strong>开启批量删除</Typography.Text>
<Typography.Text strong>{t('开启批量操作')}</Typography.Text>
<Switch
label="开启批量删除"
uncheckedText="关"
aria-label="是否开启批量删除"
label={t('开启批量操作')}
uncheckedText={t('关')}
aria-label={t('是否开启批量操作')}
onChange={(v) => {
setEnableBatchDelete(v);
}}
></Switch>
<Popconfirm
title="确定是否要删除所选通道?"
content="此修改将不可逆"
title={t('确定是否要删除所选通道?')}
content={t('此修改将不可逆')}
okType={'danger'}
onConfirm={batchDeleteChannels}
disabled={!enableBatchDelete}
@@ -1141,37 +1169,46 @@ const ChannelsTable = () => {
type="danger"
style={{ marginRight: 8 }}
>
删除所选通道
{t('删除所选通道')}
</Button>
</Popconfirm>
<Popconfirm
title="确定是否要修复数据库一致性?"
content="进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用"
title={t('确定是否要修复数据库一致性?')}
content={t('进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用')}
okType={'warning'}
onConfirm={fixChannelsAbilities}
position={'top'}
>
<Button theme="light" type="secondary" style={{ marginRight: 8 }}>
修复数据库一致性
{t('修复数据库一致性')}
</Button>
</Popconfirm>
</Space>
</div>
<div style={{ marginTop: 20 }}>
<Space>
<Typography.Text strong>标签聚合模式</Typography.Text>
<Typography.Text strong>{t('标签聚合模式')}</Typography.Text>
<Switch
checked={enableTagMode}
label="标签聚合模式"
uncheckedText="关"
aria-label="是否启用标签聚合"
label={t('标签聚合模式')}
uncheckedText={t('关')}
aria-label={t('是否启用标签聚合')}
onChange={(v) => {
setEnableTagMode(v);
// 切换模式时重新加载数据
loadChannels(0, pageSize, idSort, v);
}}
/>
<Button
disabled={!enableBatchDelete}
theme="light"
type="primary"
style={{ marginRight: 8 }}
onClick={() => setShowBatchSetTag(true)}
>
{t('批量设置标签')}
</Button>
</Space>
</div>
@@ -1205,6 +1242,23 @@ const ChannelsTable = () => {
: null
}
/>
<Modal
title={t('批量设置标签')}
visible={showBatchSetTag}
onOk={batchSetChannelTag}
onCancel={() => setShowBatchSetTag(false)}
maskClosable={false}
centered={true}
>
<div style={{ marginBottom: 20 }}>
<Typography.Text>{t('请输入要设置的标签名称')}</Typography.Text>
</div>
<Input
placeholder={t('请输入标签名称')}
value={batchSetTagValue}
onChange={(v) => setBatchSetTagValue(v)}
/>
</Modal>
</>
);
};

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,62 @@ import '../index.css';
import fireworks from 'react-fireworks';
import {
IconClose,
IconHelpCircle,
IconHome,
IconHomeStroked,
IconKey,
IconHomeStroked, IconIndentLeft,
IconComment,
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');
@@ -95,49 +88,101 @@ const HeaderBar = () => {
useEffect(() => {
if (theme === 'dark') {
document.body.setAttribute('theme-mode', 'dark');
} else {
document.body.removeAttribute('theme-mode');
}
// 发送当前主题模式给子页面
const iframe = document.querySelector('iframe');
if (iframe) {
iframe.contentWindow.postMessage({ themeMode: theme }, '*');
}
if (isNewYear) {
console.log('Happy New Year!');
}
}, []);
}, [theme]);
useEffect(() => {
const handleLanguageChanged = (lng) => {
setCurrentLang(lng);
const iframe = document.querySelector('iframe');
if (iframe) {
iframe.contentWindow.postMessage({ lang: 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: '/',
chat: '/chat',
};
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 +202,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 +255,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({
@@ -45,6 +46,7 @@ const LoginForm = () => {
let navigate = useNavigate();
const [status, setStatus] = useState({});
const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false);
const { t } = useTranslation();
const logo = getLogo();
@@ -55,7 +57,7 @@ const LoginForm = () => {
useEffect(() => {
if (searchParams.get('expired')) {
showError('未登录或登录已过期,请重新登录');
showError(t('未登录或登录已过期,请重新登录'));
}
let status = localStorage.getItem('status');
if (status) {
@@ -182,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)}
@@ -209,7 +211,7 @@ const LoginForm = () => {
htmlType={'submit'}
onClick={handleSubmit}
>
登录
{t('登录')}
</Button>
</Form>
<div
@@ -220,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 ||
@@ -232,7 +234,7 @@ const LoginForm = () => {
status.linuxdo_oauth ? (
<>
<Divider margin='12px' align='center'>
第三方登录
{t('第三方登录')}
</Divider>
<div
style={{
@@ -296,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}
>
@@ -316,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 React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
API,
copy,
@@ -24,14 +25,15 @@ import {
} from '@douyinfe/semi-ui';
import { ITEMS_PER_PAGE } from '../constants';
import {
renderAudioModelPrice,
renderModelPrice,
renderAudioModelPrice, renderGroup,
renderModelPrice, renderModelPriceSimple,
renderNumber,
renderQuota,
stringToColor
} from '../helpers/render';
import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
import { getLogOther } from '../helpers/other.js';
import { StyleContext } from '../context/Style/index.js';
const { Header } = Layout;
@@ -40,8 +42,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 +64,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 +175,7 @@ const LogsTable = () => {
},
},
{
title: '用户',
title: t('用户'),
dataIndex: 'username',
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
@@ -226,7 +197,7 @@ const LogsTable = () => {
},
},
{
title: '令牌',
title: t('令牌'),
dataIndex: 'token_name',
render: (text, record, index) => {
return record.type === 0 || record.type === 2 ? (
@@ -239,7 +210,7 @@ const LogsTable = () => {
}}
>
{' '}
{text}{' '}
{t(text)}{' '}
</Tag>
</div>
) : (
@@ -248,14 +219,45 @@ const LogsTable = () => {
},
},
{
title: '类型',
title: t('分组'),
dataIndex: 'group',
render: (text, record, index) => {
if (record.type === 0 || record.type === 2) {
if (record.group) {
return (
<>
{renderGroup(record.group)}
</>
);
} else {
let other = JSON.parse(record.other);
if (other === null) {
return <></>;
}
if (other.group !== undefined) {
return (
<>
{renderGroup(other.group)}
</>
);
} else {
return <></>;
}
}
} else {
return <></>;
}
},
},
{
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 +279,7 @@ const LogsTable = () => {
},
},
{
title: '用时/首字',
title: t('用时/首字'),
dataIndex: 'use_time',
render: (text, record, index) => {
if (record.is_stream) {
@@ -304,7 +306,7 @@ const LogsTable = () => {
},
},
{
title: '提示',
title: t('提示'),
dataIndex: 'prompt_tokens',
render: (text, record, index) => {
return record.type === 0 || record.type === 2 ? (
@@ -315,7 +317,7 @@ const LogsTable = () => {
},
},
{
title: '补全',
title: t('补全'),
dataIndex: 'completion_tokens',
render: (text, record, index) => {
return parseInt(text) > 0 &&
@@ -327,7 +329,7 @@ const LogsTable = () => {
},
},
{
title: '花费',
title: t('花费'),
dataIndex: 'quota',
render: (text, record, index) => {
return record.type === 0 || record.type === 2 ? (
@@ -338,11 +340,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 +359,7 @@ const LogsTable = () => {
// channel id array
let useChannel = other.admin_info.use_channel;
let useChannelStr = useChannel.join('->');
content = `渠道${useChannelStr}`;
content = t('渠道') + `${useChannelStr}`;
}
}
}
@@ -365,7 +367,7 @@ const LogsTable = () => {
},
},
{
title: '详情',
title: t('详情'),
dataIndex: 'content',
render: (text, record, index) => {
let other = getLogOther(record.other);
@@ -386,14 +388,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,13 +400,14 @@ const LogsTable = () => {
}}
style={{ maxWidth: 240 }}
>
调用消费
{content}
</Paragraph>
);
},
},
];
const [styleState, styleDispatch] = useContext(StyleContext);
const [logs, setLogs] = useState([]);
const [expandData, setExpandData] = useState({});
const [showStat, setShowStat] = useState(false);
@@ -427,6 +427,7 @@ const LogsTable = () => {
start_timestamp: timestamp2string(getTodayStartTimestamp()),
end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
channel: '',
group: '',
});
const {
username,
@@ -435,6 +436,7 @@ const LogsTable = () => {
start_timestamp,
end_timestamp,
channel,
group,
} = inputs;
const [stat, setStat] = useState({
@@ -443,13 +445,13 @@ const LogsTable = () => {
});
const handleInputChange = (value, name) => {
setInputs((inputs) => ({ ...inputs, [name]: value }));
setInputs(inputs => ({ ...inputs, [name]: value }));
};
const getLogSelfStat = async () => {
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
let url = `/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
let url = `/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
url = encodeURI(url);
let res = await API.get(url);
const { success, message, data } = res.data;
@@ -463,7 +465,7 @@ const LogsTable = () => {
const getLogStat = async () => {
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
let url = `/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`;
let url = `/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`;
url = encodeURI(url);
let res = await API.get(url);
const { success, message, data } = res.data;
@@ -496,13 +498,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 +542,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 +588,7 @@ const LogsTable = () => {
);
}
expandDataLocal.push({
key: '计费过程',
key: t('计费过程'),
value: content,
});
}
@@ -606,9 +608,9 @@ const LogsTable = () => {
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
if (isAdminUser) {
url = `/api/log/?p=${startIdx}&page_size=${pageSize}&type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`;
url = `/api/log/?p=${startIdx}&page_size=${pageSize}&type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`;
} else {
url = `/api/log/self/?p=${startIdx}&page_size=${pageSize}&type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
url = `/api/log/self/?p=${startIdx}&page_size=${pageSize}&type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
}
url = encodeURI(url);
const res = await API.get(url);
@@ -679,7 +681,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}
@@ -692,69 +694,96 @@ const LogsTable = () => {
</Header>
<Form layout='horizontal' style={{ marginTop: 10 }}>
<>
<Form.Section>
<div style={{ marginBottom: 10 }}>
{
styleState.isMobile ? (
<div>
<Form.DatePicker
field='start_timestamp'
label={t('起始时间')}
style={{ width: 272 }}
initValue={start_timestamp}
type='dateTime'
onChange={(value) => {
console.log(value);
handleInputChange(value, 'start_timestamp')
}}
/>
<Form.DatePicker
field='end_timestamp'
fluid
label={t('结束时间')}
style={{ width: 272 }}
initValue={end_timestamp}
type='dateTime'
onChange={(value) => handleInputChange(value, 'end_timestamp')}
/>
</div>
) : (
<Form.DatePicker
field="range_timestamp"
label={t('时间范围')}
initValue={[start_timestamp, end_timestamp]}
type="dateTimeRange"
name="range_timestamp"
onChange={(value) => {
if (Array.isArray(value) && value.length === 2) {
handleInputChange(value[0], 'start_timestamp');
handleInputChange(value[1], 'end_timestamp');
}
}}
/>
)
}
</div>
</Form.Section>
<Form.Input
field='token_name'
label='令牌名称'
style={{ width: 176 }}
label={t('令牌名称')}
value={token_name}
placeholder={'可选值'}
placeholder={t('可选值')}
name='token_name'
onChange={(value) => handleInputChange(value, 'token_name')}
/>
<Form.Input
field='model_name'
label='模型名称'
style={{ width: 176 }}
label={t('模型名称')}
value={model_name}
placeholder='可选值'
placeholder={t('可选值')}
name='model_name'
onChange={(value) => handleInputChange(value, 'model_name')}
/>
<Form.DatePicker
field='start_timestamp'
label='起始时间'
style={{ width: 272 }}
initValue={start_timestamp}
value={start_timestamp}
type='dateTime'
name='start_timestamp'
onChange={(value) => handleInputChange(value, 'start_timestamp')}
/>
<Form.DatePicker
field='end_timestamp'
fluid
label='结束时间'
style={{ width: 272 }}
initValue={end_timestamp}
value={end_timestamp}
type='dateTime'
name='end_timestamp'
onChange={(value) => handleInputChange(value, 'end_timestamp')}
<Form.Input
field='group'
label={t('分组')}
value={group}
placeholder={t('可选值')}
name='group'
onChange={(value) => handleInputChange(value, 'group')}
/>
{isAdminUser && (
<>
<Form.Input
field='channel'
label='渠道 ID'
style={{ width: 176 }}
label={t('渠道 ID')}
value={channel}
placeholder='可选值'
placeholder={t('可选值')}
name='channel'
onChange={(value) => handleInputChange(value, 'channel')}
/>
<Form.Input
field='username'
label='用户名称'
style={{ width: 176 }}
label={t('用户名称')}
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 +791,7 @@ const LogsTable = () => {
loading={loading}
style={{ marginTop: 24 }}
>
查询
{t('查询')}
</Button>
<Form.Section></Form.Section>
</>
@@ -776,11 +805,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,51 @@ 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 (
<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>
)
}
const columns = [
{
title: '可用性',
title: t('可用性'),
dataIndex: 'available',
render: (text, record, index) => {
// if record.enable_groups contains selectedGroup, then available is true
@@ -127,20 +112,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 +134,7 @@ const ModelPricing = () => {
filteredValue,
},
{
title: '计费类型',
title: t('计费类型'),
dataIndex: 'quota_type',
render: (text, record, index) => {
return renderQuotaType(parseInt(text));
@@ -169,36 +142,42 @@ 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
return (
<Space>
{text.map((group) => {
if (group === selectedGroup) {
return (
<Tag
color='blue'
size='large'
prefixIcon={<IconVerify />}
>
{group}
</Tag>
);
} else {
return (
<Tag
color='blue'
size='large'
onClick={() => {
setSelectedGroup(group);
showInfo('当前查看的分组为:' + group + ',倍率为:' + groupRatio[group]);
}}
>
{group}
</Tag>
);
if (usableGroup[group]) {
if (group === selectedGroup) {
return (
<Tag
color='blue'
size='large'
prefixIcon={<IconVerify />}
>
{group}
</Tag>
);
} else {
return (
<Tag
color='blue'
size='large'
onClick={() => {
setSelectedGroup(group);
showInfo(t('当前查看的分组为:{{group}},倍率为:{{ratio}}', {
group: group,
ratio: groupRatio[group]
}));
}}
>
{group}
</Tag>
);
}
}
})}
</Space>
@@ -208,10 +187,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 +219,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 +243,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>;
},
@@ -279,6 +261,7 @@ const ModelPricing = () => {
const [loading, setLoading] = useState(true);
const [userState, userDispatch] = useContext(UserContext);
const [groupRatio, setGroupRatio] = useState({});
const [usableGroup, setUsableGroup] = useState({});
const setModelsFormat = (models, groupRatio) => {
for (let i = 0; i < models.length; i++) {
@@ -313,9 +296,10 @@ const ModelPricing = () => {
let url = '';
url = `/api/pricing`;
const res = await API.get(url);
const { success, message, data, group_ratio } = res.data;
const { success, message, data, group_ratio, usable_group } = res.data;
if (success) {
setGroupRatio(group_ratio);
setUsableGroup(usable_group);
setSelectedGroup(userState.user ? userState.user.group : 'default')
setModelsFormat(data, group_ratio);
} else {
@@ -349,41 +333,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

@@ -25,6 +25,7 @@ import {
Space,
Tag,
Typography,
Collapsible,
} from '@douyinfe/semi-ui';
import {
getQuotaPerUnit,
@@ -33,10 +34,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: '',
@@ -62,6 +65,8 @@ const PersonalSetting = () => {
const [models, setModels] = useState([]);
const [openTransfer, setOpenTransfer] = useState(false);
const [transferAmount, setTransferAmount] = useState(0);
const [isModelsExpanded, setIsModelsExpanded] = useState(false);
const MODELS_DISPLAY_COUNT = 10; // 默认显示的模型数量
useEffect(() => {
// let user = localStorage.getItem('user');
@@ -110,7 +115,7 @@ const PersonalSetting = () => {
if (success) {
setSystemToken(data);
await copy(data);
showSuccess(`令牌已重置并已复制到剪贴板`);
showSuccess(t('令牌已重置并已复制到剪贴板'));
} else {
showError(message);
}
@@ -151,18 +156,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 +175,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 +192,7 @@ const PersonalSetting = () => {
);
const {success, message} = res.data;
if (success) {
showSuccess('微信账户绑定成功!');
showSuccess(t('微信账户绑定成功!'));
setShowWeChatBindModal(false);
} else {
showError(message);
@@ -196,7 +201,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 +209,7 @@ const PersonalSetting = () => {
});
const {success, message} = res.data;
if (success) {
showSuccess('密码修改成功!');
showSuccess(t('密码修改成功!'));
setShowWeChatBindModal(false);
} else {
showError(message);
@@ -214,7 +219,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 +237,7 @@ const PersonalSetting = () => {
const sendVerificationCode = async () => {
if (inputs.email === '') {
showError('请输入邮箱!');
showError(t('请输入邮箱!'));
return;
}
setDisableButton(true);
@@ -246,7 +251,7 @@ const PersonalSetting = () => {
);
const {success, message} = res.data;
if (success) {
showSuccess('验证码发送成功,请检查邮箱!');
showSuccess(t('验证码发送成功,请检查邮箱!'));
} else {
showError(message);
}
@@ -255,7 +260,7 @@ const PersonalSetting = () => {
const bindEmail = async () => {
if (inputs.email_verification_code === '') {
showError('请输入邮箱验证码!');
showError(t('请输入邮箱验证码!'));
return;
}
setLoading(true);
@@ -264,7 +269,7 @@ const PersonalSetting = () => {
);
const {success, message} = res.data;
if (success) {
showSuccess('邮箱账户绑定成功!');
showSuccess(t('邮箱账户绑定成功!'));
setShowEmailBindModal(false);
userState.user.email = inputs.email;
} else {
@@ -287,10 +292,10 @@ const PersonalSetting = () => {
const copyText = async (text) => {
if (await copy(text)) {
showSuccess('已复制:' + text);
showSuccess(t('已复制:') + text);
} else {
// setSearchKeyword(text);
Modal.error({title: '无法复制到剪贴板,请手动复制', content: text});
Modal.error({title: t('无法复制到剪贴板,请手动复制'), content: text});
}
};
@@ -299,7 +304,7 @@ const PersonalSetting = () => {
<Layout>
<Layout.Content>
<Modal
title='请输入要划转的数量'
title={t('请输入要划转的数量')}
visible={openTransfer}
onOk={transfer}
onCancel={handleCancel}
@@ -308,7 +313,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 +322,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 +352,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>
@@ -364,41 +368,97 @@ const PersonalSetting = () => {
</>
}
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>
<>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Typography.Title heading={6}>{t('可用模型')}</Typography.Title>
</div>
<div style={{marginTop: 10}}>
{models.length <= MODELS_DISPLAY_COUNT ? (
<Space wrap>
{models.map((model) => (
<Tag
key={model}
color='cyan'
onClick={() => {
copyText(model);
}}
>
{model}
</Tag>
))}
</Space>
) : (
<>
<Collapsible isOpen={isModelsExpanded}>
<Space wrap>
{models.map((model) => (
<Tag
key={model}
color='cyan'
onClick={() => {
copyText(model);
}}
>
{model}
</Tag>
))}
<Tag
color='blue'
type="light"
style={{ cursor: 'pointer' }}
onClick={() => setIsModelsExpanded(false)}
>
{t('收起')}
</Tag>
</Space>
</Collapsible>
{!isModelsExpanded && (
<Space wrap>
{models.slice(0, MODELS_DISPLAY_COUNT).map((model) => (
<Tag
key={model}
color='cyan'
onClick={() => {
copyText(model);
}}
>
{model}
</Tag>
))}
<Tag
color='blue'
type="light"
style={{ cursor: 'pointer' }}
onClick={() => setIsModelsExpanded(true)}
>
{t('更多')} {models.length - MODELS_DISPLAY_COUNT} {t('个模型')}
</Tag>
</Space>
)}
</>
)}
</div>
</>
}
>
<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 +468,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 +505,7 @@ const PersonalSetting = () => {
value={
userState.user && userState.user.email !== ''
? userState.user.email
: '未绑定'
: t('未绑定')
}
readonly={true}
></Input>
@@ -457,14 +517,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 +532,8 @@ const PersonalSetting = () => {
<Input
value={
userState.user && userState.user.wechat_id !== ''
? '已绑定'
: '未绑定'
? t('已绑定')
: t('未绑定')
}
readonly={true}
></Input>
@@ -485,13 +545,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 +560,7 @@ const PersonalSetting = () => {
value={
userState.user && userState.user.github_id !== ''
? userState.user.github_id
: '未绑定'
: t('未绑定')
}
readonly={true}
></Input>
@@ -515,13 +575,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 +590,7 @@ const PersonalSetting = () => {
value={
userState.user && userState.user.telegram_id !== ''
? userState.user.telegram_id
: '未绑定'
: t('未绑定')
}
readonly={true}
></Input>
@@ -538,7 +598,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 +606,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 +621,7 @@ const PersonalSetting = () => {
value={
userState.user && userState.user.linux_do_id !== ''
? userState.user.linux_do_id
: '未绑定'
: t('未绑定')
}
readonly={true}
></Input>
@@ -576,7 +636,7 @@ const PersonalSetting = () => {
!status.linuxdo_oauth
}
>
{status.linuxdo_oauth ? '绑定' : '未启用'}
{status.linuxdo_oauth ? t('绑定') : t('未启用')}
</Button>
</div>
</div>
@@ -584,14 +644,14 @@ const PersonalSetting = () => {
<div style={{marginTop: 10}}>
<Space>
<Button onClick={generateAccessToken}>
生成系统访问令牌
{t('生成系统访问令牌')}
</Button>
<Button
onClick={() => {
setShowChangePasswordModal(true);
}}
>
修改密码
{t('修改密码')}
</Button>
<Button
type={'danger'}
@@ -599,7 +659,7 @@ const PersonalSetting = () => {
setShowAccountDeleteModal(true);
}}
>
删除个人账户
{t('删除个人账户')}
</Button>
</Space>
@@ -617,7 +677,7 @@ const PersonalSetting = () => {
setShowWeChatBindModal(true);
}}
>
绑定微信账号
{t('绑定微信账号')}
</Button>
)}
<Modal
@@ -641,7 +701,7 @@ const PersonalSetting = () => {
}
/>
<Button color='' fluid size='large' onClick={bindWeChat}>
绑定
{t('绑定')}
</Button>
</Modal>
</div>
@@ -655,7 +715,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 +807,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 +816,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)

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