Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,31 @@ https://github.com/MAX-API-Next/MAX-API
Modified versions must not misrepresent the origin of the software and must
mark their changes in accordance with AGPLv3 Section 7(c).

==== Preserved New API Upstream Notice ====

new-api
Copyright (c) QuantumNous and contributors.

This project includes software derived from New API, which is licensed under
the GNU Affero General Public License v3.0. The following additional terms from
the New API NOTICE are preserved:

Pursuant to Section 7(b) of the GNU Affero General Public License version 3,
the following reasonable legal notice and author attribution must be preserved
by modified versions in the Appropriate Legal Notices and in any prominent
about, legal, footer, or attribution location presented by the user interface:

"Frontend design and development by New API contributors."

Modified versions that present a user interface must also preserve a visible
link to the original project in a prominent about, legal, footer, or
attribution location:

https://github.com/QuantumNous/new-api

Modified versions must not misrepresent the origin of the software and must
mark their changes in accordance with AGPLv3 Section 7(c).

==== Third-Party Notices ====

This product includes third-party open source software. Copyright notices and
Expand Down
863 changes: 225 additions & 638 deletions README.en.md

Large diffs are not rendered by default.

804 changes: 237 additions & 567 deletions README.fr.md

Large diffs are not rendered by default.

825 changes: 237 additions & 588 deletions README.ja.md

Large diffs are not rendered by default.

865 changes: 226 additions & 639 deletions README.md

Large diffs are not rendered by default.

872 changes: 226 additions & 646 deletions README.zh_CN.md

Large diffs are not rendered by default.

865 changes: 227 additions & 638 deletions README.zh_TW.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ version: '3.4' # For compatibility with older Docker versions

services:
max-api:
image: cscitechtop/max-api:latest
image: cscitechtop/max-api:latest@sha256:006d5d86887a261baab4d71ec3797d429e3771a4836e5899734aee0e7f66f2ab
container_name: max-api
restart: always
command: --log-dir /app/logs
ports:
- "3000:3000"
- "127.0.0.1:3000:3000"
volumes:
- ./data:/data
- ./logs:/app/logs
Expand Down
Binary file modified docs/images/MAX-API架构图.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/images/aionui.png
Binary file not shown.
Binary file removed docs/images/aliyun.png
Binary file not shown.
Binary file removed docs/images/cherry-studio.png
Binary file not shown.
Binary file removed docs/images/io-net.png
Binary file not shown.
Binary file removed docs/images/pku.png
Binary file not shown.
Binary file removed docs/images/ucloud.png
Binary file not shown.
25 changes: 12 additions & 13 deletions docs/installation/BT.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@
3. 点击 **安装**
4. 配置以下基本选项:
- **容器名称**:可自定义,默认为 `max-api`
- **端口映射**:默认为 `3000:3000`
- **端口映射**:设置为 `127.0.0.1:3000:3000`,仅允许服务器本机和反向代理访问
- **环境变量**:
- `SESSION_SECRET`:会话密钥(**必填**,多机部署时必须一致)
- `CRYPTO_SECRET`:加密密钥(使用 Redis 时必填)
- `CRYPTO_SECRET`:可选的加密密钥覆盖项;未设置时回退使用 `SESSION_SECRET`,如单独设置则多机/Redis 节点必须一致
5. 点击 **确认** 开始安装
6. 等待安装完成后,访问 `http://您的服务器IP:3000` 即可使用
6. 等待安装完成后,为站点配置 HTTPS 反向代理,再通过 `https://您的域名` 登录;`http://127.0.0.1:3000` 仅用于服务器本机检查,不应直接暴露到公网
Comment thread
coderabbitai[bot] marked this conversation as resolved.

### 方法二:使用 Docker Compose

Expand All @@ -57,11 +57,11 @@
version: '3'
services:
max-api:
image: cscitechtop/max-api:latest
image: cscitechtop/max-api:latest@sha256:006d5d86887a261baab4d71ec3797d429e3771a4836e5899734aee0e7f66f2ab
container_name: max-api
restart: always
ports:
- "3000:3000"
- "127.0.0.1:3000:3000"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
volumes:
- ./data:/data
environment:
Expand All @@ -85,7 +85,7 @@ docker-compose up -d
| 变量名 | 说明 | 是否必填 |
| ------------------- | ------------------ | ------ |
| `SESSION_SECRET` | 会话密钥,多机部署必须一致 | **必填** |
| `CRYPTO_SECRET` | 加密密钥,使用 Redis 时必填 | 条件必填 |
| `CRYPTO_SECRET` | 可选的加密密钥覆盖项;未设置时回退使用 `SESSION_SECRET`,显式设置时多机/Redis 节点需一致 | 可选 |
| `SQL_DSN` | 数据库连接字符串(使用外部数据库时) | 可选 |
| `REDIS_CONN_STRING` | Redis 连接字符串 | 可选 |

Expand All @@ -103,11 +103,11 @@ head -c 16 /dev/urandom | xxd -p

## 常见问题

### Q1:无法访问 3000 端口?
### Q1:无法通过 HTTPS 域名访问?

1. 检查服务器防火墙是否开放 3000 端口
2. 在宝塔面板 **安全** 中放行 3000 端口
3. 检查云服务器安全组是否开放端口
1. 在服务器本机执行 `curl http://127.0.0.1:3000`,确认 MAX API 容器正常响应
2. 检查宝塔站点的 HTTPS 反向代理目标是否为 `http://127.0.0.1:3000`
3. 检查域名解析、TLS 证书和 443 端口;不要在防火墙或云安全组中向公网开放 3000 端口

### Q2:登录后提示会话失效?

Expand All @@ -125,8 +125,8 @@ volumes:
### Q4:如何更新版本?

```bash
# 拉取最新镜像
docker pull cscitechtop/max-api:latest
# 拉取经过校验的固定镜像
docker pull cscitechtop/max-api:latest@sha256:006d5d86887a261baab4d71ec3797d429e3771a4836e5899734aee0e7f66f2ab

# 重启容器
docker-compose down && docker-compose up -d
Expand All @@ -148,4 +148,3 @@ docker-compose down && docker-compose up -d
![宝塔面板 Docker 安装](https://github.com/user-attachments/assets/7a6fc03e-c457-45e4-b8f9-184508fc26b0)

> ⚠️ 注意:密钥为环境变量 `SESSION_SECRET`,请务必设置!

38 changes: 35 additions & 3 deletions web/default/src/components/data-table/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ export type DataTableToolbarProps<TData> = {
* (form-mode workflow).
*/
onSearch?: () => void
/**
* Controlled value for a submit-mode global search input. When provided,
* typing updates this value without changing the table filter until
* {@link onSearch} is invoked.
*/
searchValue?: string
/** Change handler for a controlled submit-mode global search input. */
onSearchValueChange?: (value: string) => void
/**
* Loading state for the explicit Search button.
*/
Expand Down Expand Up @@ -144,12 +152,14 @@ export function DataTableToolbar<TData>(props: DataTableToolbarProps<TData>) {
const isFiltered =
props.table.getState().columnFilters.length > 0 ||
!!props.table.getState().globalFilter ||
!!props.searchValue?.trim() ||
!!props.hasAdditionalFilters

const placeholder = props.searchPlaceholder ?? t('Filter...')

const searchInput = props.searchKey ? (
<Input
aria-label={placeholder}
placeholder={placeholder}
value={
(props.table.getColumn(props.searchKey)?.getFilterValue() as string) ??
Expand All @@ -164,9 +174,27 @@ export function DataTableToolbar<TData>(props: DataTableToolbarProps<TData>) {
/>
) : (
<Input
aria-label={placeholder}
placeholder={placeholder}
value={props.table.getState().globalFilter ?? ''}
onChange={(event) => props.table.setGlobalFilter(event.target.value)}
value={props.searchValue ?? props.table.getState().globalFilter ?? ''}
onChange={(event) => {
const value = event.target.value
if (props.onSearchValueChange) {
props.onSearchValueChange(value)
} else {
props.table.setGlobalFilter(value)
}
}}
onKeyDown={(event) => {
if (
event.key === 'Enter' &&
!event.nativeEvent.isComposing &&
props.onSearch
) {
event.preventDefault()
props.onSearch()
}
}}
className='w-full sm:w-[200px] lg:w-[240px]'
/>
)
Expand All @@ -187,7 +215,11 @@ export function DataTableToolbar<TData>(props: DataTableToolbarProps<TData>) {

const handleReset = () => {
props.table.resetColumnFilters()
props.table.setGlobalFilter('')
if (props.onSearchValueChange) {
props.onSearchValueChange('')
} else {
props.table.setGlobalFilter('')
}
props.onReset?.()
}

Expand Down
80 changes: 33 additions & 47 deletions web/default/src/features/channels/components/channels-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.

For commercial licensing, please contact https://github.com/MAX-API-Next/MAX-API/issues
*/
import {
useState,
useMemo,
useEffect,
useRef,
type CompositionEvent,
} from 'react'
import { useState, useMemo, useEffect } from 'react'
import { useQuery } from '@tanstack/react-query'
import { getRouteApi } from '@tanstack/react-router'
import {
Expand All @@ -35,7 +29,7 @@ import {
type ExpandedState,
type Row,
} from '@tanstack/react-table'
import { useDebounce, useMediaQuery } from '@/hooks'
import { useMediaQuery } from '@/hooks'
import { useTranslation } from 'react-i18next'
import { getLobeIcon } from '@/lib/lobe-icon'
import { useTableUrlState } from '@/hooks/use-table-url-state'
Expand Down Expand Up @@ -98,6 +92,10 @@ export function ChannelsTable() {
const {
globalFilter,
onGlobalFilterChange,
globalFilterInput,
onGlobalFilterInputChange,
applyGlobalFilter,
resetGlobalFilter,
columnFilters,
onColumnFiltersChange,
pagination,
Expand All @@ -110,7 +108,7 @@ export function ChannelsTable() {
defaultPage: 1,
defaultPageSize: isMobile ? 10 : DEFAULT_PAGE_SIZE,
},
globalFilter: { enabled: true, key: 'filter' },
globalFilter: { enabled: true, key: 'filter', mode: 'submit' },
columnFilters: [
{ columnId: 'status', searchKey: 'status', type: 'array' },
{ columnId: 'type', searchKey: 'type', type: 'array' },
Expand All @@ -122,60 +120,37 @@ export function ChannelsTable() {
// Extract filters from column filters
const statusFilter =
(columnFilters.find((f) => f.id === 'status')?.value as string[]) || []
const typeFilter =
(columnFilters.find((f) => f.id === 'type')?.value as string[]) || []
const typeFilter = useMemo(
() => (columnFilters.find((f) => f.id === 'type')?.value as string[]) || [],
[columnFilters]
)
const groupFilter =
(columnFilters.find((f) => f.id === 'group')?.value as string[]) || []
const modelFilterFromUrl =
(columnFilters.find((f) => f.id === 'model')?.value as string) || ''

// Local state for immediate input feedback
// Keep the model filter local until the toolbar Search action is submitted.
const [modelFilterInput, setModelFilterInput] = useState(modelFilterFromUrl)
const [pendingModelFilter, setPendingModelFilter] =
useState(modelFilterFromUrl)
const isModelFilterComposingRef = useRef(false)
const debouncedModelFilter = useDebounce(pendingModelFilter, 500)

// Sync local input with URL when URL changes (e.g., from back/forward navigation)
useEffect(() => {
if (!isModelFilterComposingRef.current) {
setModelFilterInput(modelFilterFromUrl)
}
setPendingModelFilter(modelFilterFromUrl)
setModelFilterInput(modelFilterFromUrl)
}, [modelFilterFromUrl])

const handleModelFilterChange = (value: string) => {
const handleModelFilterChange = (value: string): void => {
setModelFilterInput(value)
if (!isModelFilterComposingRef.current) {
setPendingModelFilter(value)
}
}

const handleModelFilterCompositionStart = () => {
isModelFilterComposingRef.current = true
const handleSearch = (): void => {
const modelFilter = modelFilterInput.trim()
applyGlobalFilter?.({ model: modelFilter || undefined })
}

const handleModelFilterCompositionEnd = (
event: CompositionEvent<HTMLInputElement>
) => {
isModelFilterComposingRef.current = false
const value = event.currentTarget.value
setModelFilterInput(value)
setPendingModelFilter(value)
const handleReset = (): void => {
setModelFilterInput('')
resetGlobalFilter?.({ model: undefined })
}

// Update URL when debounced value changes
useEffect(() => {
if (debouncedModelFilter !== modelFilterFromUrl) {
onColumnFiltersChange((prev) => {
const filtered = prev.filter((f) => f.id !== 'model')
return debouncedModelFilter
? [...filtered, { id: 'model', value: debouncedModelFilter }]
: filtered
})
}
}, [debouncedModelFilter, modelFilterFromUrl, onColumnFiltersChange])

const modelFilter = modelFilterFromUrl

// Determine whether to use search or regular list API
Expand Down Expand Up @@ -417,13 +392,24 @@ export function ChannelsTable() {
applyHeaderSize
toolbarProps={{
searchPlaceholder: t('Filter by name, ID, or key...'),
onSearch: handleSearch,
searchValue: globalFilterInput,
onSearchValueChange: onGlobalFilterInputChange,
onReset: handleReset,
hasAdditionalFilters: Boolean(modelFilterInput.trim()),
searchLoading: isFetching,
additionalSearch: (
<Input
placeholder={t('Filter by model...')}
aria-label={t('Filter by model...')}
value={modelFilterInput}
onChange={(e) => handleModelFilterChange(e.target.value)}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
onCompositionStart={handleModelFilterCompositionStart}
onCompositionEnd={handleModelFilterCompositionEnd}
onKeyDown={(event) => {
if (event.key === 'Enter' && !event.nativeEvent.isComposing) {
event.preventDefault()
handleSearch()
}
}}
className='w-full sm:w-[150px] lg:w-[180px]'
/>
),
Expand Down
Loading
Loading