Compare commits
5 Commits
ab703e6176
...
2b30f0ffe3
| Author | SHA1 | Date | |
|---|---|---|---|
|
2b30f0ffe3
|
|||
|
c8c474ecfd
|
|||
|
567c146fb8
|
|||
|
7df66bbc61
|
|||
|
45e255856a
|
-149
@@ -1,149 +0,0 @@
|
|||||||
# A generic, single database configuration.
|
|
||||||
|
|
||||||
[alembic]
|
|
||||||
# path to migration scripts.
|
|
||||||
# this is typically a path given in POSIX (e.g. forward slashes)
|
|
||||||
# format, relative to the token %(here)s which refers to the location of this
|
|
||||||
# ini file
|
|
||||||
script_location = %(here)s/alembic
|
|
||||||
|
|
||||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
|
||||||
# Uncomment the line below if you want the files to be prepended with date and time
|
|
||||||
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
|
||||||
# for all available tokens
|
|
||||||
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
|
||||||
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
|
|
||||||
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
|
|
||||||
|
|
||||||
# sys.path path, will be prepended to sys.path if present.
|
|
||||||
# defaults to the current working directory. for multiple paths, the path separator
|
|
||||||
# is defined by "path_separator" below.
|
|
||||||
prepend_sys_path = .
|
|
||||||
|
|
||||||
|
|
||||||
# timezone to use when rendering the date within the migration file
|
|
||||||
# as well as the filename.
|
|
||||||
# If specified, requires the tzdata library which can be installed by adding
|
|
||||||
# `alembic[tz]` to the pip requirements.
|
|
||||||
# string value is passed to ZoneInfo()
|
|
||||||
# leave blank for localtime
|
|
||||||
# timezone =
|
|
||||||
|
|
||||||
# max length of characters to apply to the "slug" field
|
|
||||||
# truncate_slug_length = 40
|
|
||||||
|
|
||||||
# set to 'true' to run the environment during
|
|
||||||
# the 'revision' command, regardless of autogenerate
|
|
||||||
# revision_environment = false
|
|
||||||
|
|
||||||
# set to 'true' to allow .pyc and .pyo files without
|
|
||||||
# a source .py file to be detected as revisions in the
|
|
||||||
# versions/ directory
|
|
||||||
# sourceless = false
|
|
||||||
|
|
||||||
# version location specification; This defaults
|
|
||||||
# to <script_location>/versions. When using multiple version
|
|
||||||
# directories, initial revisions must be specified with --version-path.
|
|
||||||
# The path separator used here should be the separator specified by "path_separator"
|
|
||||||
# below.
|
|
||||||
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
|
|
||||||
|
|
||||||
# path_separator; This indicates what character is used to split lists of file
|
|
||||||
# paths, including version_locations and prepend_sys_path within configparser
|
|
||||||
# files such as alembic.ini.
|
|
||||||
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
|
|
||||||
# to provide os-dependent path splitting.
|
|
||||||
#
|
|
||||||
# Note that in order to support legacy alembic.ini files, this default does NOT
|
|
||||||
# take place if path_separator is not present in alembic.ini. If this
|
|
||||||
# option is omitted entirely, fallback logic is as follows:
|
|
||||||
#
|
|
||||||
# 1. Parsing of the version_locations option falls back to using the legacy
|
|
||||||
# "version_path_separator" key, which if absent then falls back to the legacy
|
|
||||||
# behavior of splitting on spaces and/or commas.
|
|
||||||
# 2. Parsing of the prepend_sys_path option falls back to the legacy
|
|
||||||
# behavior of splitting on spaces, commas, or colons.
|
|
||||||
#
|
|
||||||
# Valid values for path_separator are:
|
|
||||||
#
|
|
||||||
# path_separator = :
|
|
||||||
# path_separator = ;
|
|
||||||
# path_separator = space
|
|
||||||
# path_separator = newline
|
|
||||||
#
|
|
||||||
# Use os.pathsep. Default configuration used for new projects.
|
|
||||||
path_separator = os
|
|
||||||
|
|
||||||
# set to 'true' to search source files recursively
|
|
||||||
# in each "version_locations" directory
|
|
||||||
# new in Alembic version 1.10
|
|
||||||
# recursive_version_locations = false
|
|
||||||
|
|
||||||
# the output encoding used when revision files
|
|
||||||
# are written from script.py.mako
|
|
||||||
# output_encoding = utf-8
|
|
||||||
|
|
||||||
# database URL. This is consumed by the user-maintained env.py script only.
|
|
||||||
# other means of configuring database URLs may be customized within the env.py
|
|
||||||
# file.
|
|
||||||
sqlalchemy.url = sqlite:///.nyahome/nyahome.db
|
|
||||||
|
|
||||||
|
|
||||||
[post_write_hooks]
|
|
||||||
# post_write_hooks defines scripts or Python functions that are run
|
|
||||||
# on newly generated revision scripts. See the documentation for further
|
|
||||||
# detail and examples
|
|
||||||
|
|
||||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
|
||||||
# hooks = black
|
|
||||||
# black.type = console_scripts
|
|
||||||
# black.entrypoint = black
|
|
||||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
|
||||||
|
|
||||||
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
|
|
||||||
# hooks = ruff
|
|
||||||
# ruff.type = module
|
|
||||||
# ruff.module = ruff
|
|
||||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
|
||||||
|
|
||||||
# Alternatively, use the exec runner to execute a binary found on your PATH
|
|
||||||
# hooks = ruff
|
|
||||||
# ruff.type = exec
|
|
||||||
# ruff.executable = ruff
|
|
||||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
|
||||||
|
|
||||||
# Logging configuration. This is also consumed by the user-maintained
|
|
||||||
# env.py script only.
|
|
||||||
[loggers]
|
|
||||||
keys = root,sqlalchemy,alembic
|
|
||||||
|
|
||||||
[handlers]
|
|
||||||
keys = console
|
|
||||||
|
|
||||||
[formatters]
|
|
||||||
keys = generic
|
|
||||||
|
|
||||||
[logger_root]
|
|
||||||
level = WARNING
|
|
||||||
handlers = console
|
|
||||||
qualname =
|
|
||||||
|
|
||||||
[logger_sqlalchemy]
|
|
||||||
level = WARNING
|
|
||||||
handlers =
|
|
||||||
qualname = sqlalchemy.engine
|
|
||||||
|
|
||||||
[logger_alembic]
|
|
||||||
level = INFO
|
|
||||||
handlers =
|
|
||||||
qualname = alembic
|
|
||||||
|
|
||||||
[handler_console]
|
|
||||||
class = StreamHandler
|
|
||||||
args = (sys.stderr,)
|
|
||||||
level = NOTSET
|
|
||||||
formatter = generic
|
|
||||||
|
|
||||||
[formatter_generic]
|
|
||||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
||||||
datefmt = %H:%M:%S
|
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
from sqlalchemy import engine_from_config, pool
|
from sqlalchemy import engine_from_config, pool
|
||||||
from sqlmodel import SQLModel
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
from alembic import context
|
from alembic import context
|
||||||
|
from nyahome.cli.cli import ENV_PATH
|
||||||
|
|
||||||
|
load_dotenv(ENV_PATH)
|
||||||
|
|
||||||
from nyahome.database import ( # noqa
|
from nyahome.database import ( # noqa
|
||||||
AiiModel,
|
AiiModel,
|
||||||
AiiModelPublic,
|
AiiModelPublic,
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ from typing import Sequence, Union
|
|||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import sqlmodel
|
import sqlmodel
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from nyahome.cli.cli import ENV_PATH
|
||||||
|
|
||||||
|
load_dotenv(ENV_PATH)
|
||||||
|
|
||||||
${imports if imports else ""}
|
${imports if imports else ""}
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from sqlmodel import SQLModel
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
from .engine import engine
|
from .engine import engine
|
||||||
from .model_aii import AiiModel, AiiModelPublic, AiiProvider, AiiProviderPublic, z_aii_model, z_aii_provider
|
from .model_aii import AiiModel, AiiModelPublic, AiiProvider, AiiProviderPublic, AiiProviderPublicWithoutKey
|
||||||
from .model_story import (
|
from .model_story import (
|
||||||
Chatroom,
|
Chatroom,
|
||||||
ChatroomChat,
|
ChatroomChat,
|
||||||
@@ -29,6 +29,7 @@ __all__ = [
|
|||||||
AiiModelPublic,
|
AiiModelPublic,
|
||||||
AiiProvider,
|
AiiProvider,
|
||||||
AiiProviderPublic,
|
AiiProviderPublic,
|
||||||
|
AiiProviderPublicWithoutKey,
|
||||||
ChatScript,
|
ChatScript,
|
||||||
Chatroom,
|
Chatroom,
|
||||||
ChatroomChat,
|
ChatroomChat,
|
||||||
@@ -42,6 +43,4 @@ __all__ = [
|
|||||||
async_get_session,
|
async_get_session,
|
||||||
create_db,
|
create_db,
|
||||||
get_session,
|
get_session,
|
||||||
z_aii_model,
|
|
||||||
z_aii_provider,
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, SerializerFunctionWrapHandler, model_serializer
|
||||||
from sqlmodel import Field, Relationship, SQLModel
|
from sqlmodel import Field, Relationship, SQLModel
|
||||||
|
|
||||||
|
|
||||||
@@ -16,6 +16,18 @@ class AiiProvider(SQLModel, table=True):
|
|||||||
|
|
||||||
aii_models: list["AiiModel"] = Relationship(back_populates="aii_provider")
|
aii_models: list["AiiModel"] = Relationship(back_populates="aii_provider")
|
||||||
|
|
||||||
|
@model_serializer(mode="wrap")
|
||||||
|
def serialize_provider(self, handler: SerializerFunctionWrapHandler) -> dict[str, Any]:
|
||||||
|
data: dict = handler(self)
|
||||||
|
data.pop("api_key", None)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class AiiProviderPublicWithoutKey(BaseModel):
|
||||||
|
id: Optional[int] = None
|
||||||
|
name: str
|
||||||
|
base_url: str
|
||||||
|
|
||||||
|
|
||||||
class AiiProviderPublic(BaseModel):
|
class AiiProviderPublic(BaseModel):
|
||||||
id: Optional[int] = None
|
id: Optional[int] = None
|
||||||
@@ -32,36 +44,27 @@ class AiiModel(SQLModel, table=True):
|
|||||||
id: Optional[int] = Field(default=None, primary_key=True)
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
model_name: str
|
model_name: str
|
||||||
max_context_length: int
|
max_context_length: int
|
||||||
|
reasonable: Optional[bool] = Field(default=None, nullable=True, description="模型是否具备思考能力")
|
||||||
|
|
||||||
aii_provider_id: int = Field(default=None, foreign_key="aiiprovider.id")
|
aii_provider_id: int = Field(default=None, foreign_key="aiiprovider.id")
|
||||||
aii_provider: AiiProvider = Relationship(back_populates="aii_models")
|
aii_provider: AiiProvider = Relationship(back_populates="aii_models")
|
||||||
|
|
||||||
chatrooms: list["Chatroom"] = Relationship(back_populates="default_model")
|
chatrooms: list["Chatroom"] = Relationship(back_populates="default_model")
|
||||||
|
|
||||||
|
@model_serializer(mode="wrap")
|
||||||
|
def serialize_model(self, handler: SerializerFunctionWrapHandler) -> dict[str, Any]:
|
||||||
|
data: dict = handler(self)
|
||||||
|
data["reasonable"] = bool(data.get("reasonable"))
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class AiiModelPublic(BaseModel):
|
class AiiModelPublic(BaseModel):
|
||||||
id: Optional[int] = None
|
id: Optional[int] = None
|
||||||
model_name: str
|
model_name: str
|
||||||
max_context_length: int
|
max_context_length: int
|
||||||
|
reasonable: bool
|
||||||
|
|
||||||
aii_provider_id: int
|
aii_provider_id: int
|
||||||
|
|
||||||
|
|
||||||
def z_aii_model(am: AiiModel) -> dict:
|
|
||||||
return {
|
|
||||||
"id": am.id,
|
|
||||||
"model_name": am.model_name,
|
|
||||||
"max_context_length": am.max_context_length,
|
|
||||||
"aii_provider_id": am.aii_provider_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def z_aii_provider(ap: AiiProvider) -> dict:
|
|
||||||
return {
|
|
||||||
"id": ap.id,
|
|
||||||
"name": ap.name,
|
|
||||||
"base_url": ap.base_url,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
from .model_story import Chatroom # noqa: E402
|
from .model_story import Chatroom # noqa: E402
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ class ChatroomChat(BaseModel):
|
|||||||
prefix: str
|
prefix: str
|
||||||
mode: Literal["continue", "expand"]
|
mode: Literal["continue", "expand"]
|
||||||
model_id: int
|
model_id: int
|
||||||
|
enable_thinking: bool
|
||||||
|
|
||||||
|
|
||||||
class ChatroomChatAccept(BaseModel):
|
class ChatroomChatAccept(BaseModel):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated, Sequence
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from sqlalchemy.exc import NoResultFound
|
from sqlalchemy.exc import NoResultFound
|
||||||
@@ -9,10 +9,9 @@ from nyahome.database import (
|
|||||||
AiiModelPublic,
|
AiiModelPublic,
|
||||||
AiiProvider,
|
AiiProvider,
|
||||||
AiiProviderPublic,
|
AiiProviderPublic,
|
||||||
|
AiiProviderPublicWithoutKey,
|
||||||
ModelUser,
|
ModelUser,
|
||||||
get_session,
|
get_session,
|
||||||
z_aii_model,
|
|
||||||
z_aii_provider,
|
|
||||||
)
|
)
|
||||||
from nyahome.service.aii_service import apply_get_models, s_check_remote_model, s_list_remote_provider_models
|
from nyahome.service.aii_service import apply_get_models, s_check_remote_model, s_list_remote_provider_models
|
||||||
|
|
||||||
@@ -23,16 +22,15 @@ aii_router = APIRouter(tags=["Aii"], prefix="/aii")
|
|||||||
|
|
||||||
|
|
||||||
@aii_router.get("/model/", name="获取模型列表")
|
@aii_router.get("/model/", name="获取模型列表")
|
||||||
async def get_all_model(session: Annotated[Session, Depends(get_session)]) -> ReturnDto:
|
async def get_all_model(session: Annotated[Session, Depends(get_session)]) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
获取 AI 模型列表。
|
获取 AI 模型列表。
|
||||||
此接口无需用户登录即可访问。
|
此接口无需用户登录即可访问。
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
被 ReturnDto 包裹的 AiiModel 列表
|
AiiModel 列表
|
||||||
"""
|
"""
|
||||||
final_model_list = apply_get_models(session)
|
return apply_get_models(session)
|
||||||
return ReturnDto(result=final_model_list)
|
|
||||||
|
|
||||||
|
|
||||||
@aii_router.post("/model/", name="添加模型")
|
@aii_router.post("/model/", name="添加模型")
|
||||||
@@ -40,18 +38,18 @@ async def add_model(
|
|||||||
model: AiiModelPublic,
|
model: AiiModelPublic,
|
||||||
user: Annotated[ModelUser, Depends(verify_token)],
|
user: Annotated[ModelUser, Depends(verify_token)],
|
||||||
session: Annotated[Session, Depends(get_session)],
|
session: Annotated[Session, Depends(get_session)],
|
||||||
) -> ReturnDto:
|
) -> AiiModel:
|
||||||
"""
|
"""
|
||||||
添加新的 AI 模型。需要基于已添加的模型提供商。
|
添加新的 AI 模型。需要基于已添加的模型提供商。
|
||||||
此接口需要管理员访问。
|
此接口需要管理员访问。
|
||||||
添加模型时不会进行可用性检查,因此 WebUI 在前端实现了检查按钮。此端点不会负责检查。
|
不会进行可用性检查,因此 WebUI 在前端实现了检查按钮。此端点不会负责检查。
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPException: 401 用户无权限管理模型(未登录或非管理员)
|
HTTPException: 401 用户无权限管理模型(未登录或非管理员)
|
||||||
HTTPException: 404 模型提供商不存在
|
HTTPException: 404 模型提供商不存在
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
被 ReturnDto 包裹的、添加的 AiiModel
|
AiiModel
|
||||||
"""
|
"""
|
||||||
if not user.is_admin:
|
if not user.is_admin:
|
||||||
raise HTTPException(status_code=401, detail="用户无权限管理模型。") from None
|
raise HTTPException(status_code=401, detail="用户无权限管理模型。") from None
|
||||||
@@ -69,11 +67,57 @@ async def add_model(
|
|||||||
session.add(am)
|
session.add(am)
|
||||||
session.commit()
|
session.commit()
|
||||||
session.refresh(am)
|
session.refresh(am)
|
||||||
return ReturnDto(result=z_aii_model(am))
|
return am
|
||||||
|
|
||||||
|
|
||||||
|
@aii_router.post("/model/{id_}", name="修改模型")
|
||||||
|
async def edit_model(
|
||||||
|
id_: int,
|
||||||
|
model: AiiModelPublic,
|
||||||
|
user: Annotated[ModelUser, Depends(verify_token)],
|
||||||
|
session: Annotated[Session, Depends(get_session)],
|
||||||
|
) -> AiiModel:
|
||||||
|
"""
|
||||||
|
修改已添加的 AI 模型。
|
||||||
|
此接口需要管理员访问。
|
||||||
|
不会进行可用性检查,因此 WebUI 在前端实现了检查按钮。此端点不会负责检查。
|
||||||
|
**只允许修改模型的名称、最大上下文长度和是否支持思考。**
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: 400 模型提供商 ID 不匹配
|
||||||
|
HTTPException: 401 用户无权限管理模型(未登录或非管理员)
|
||||||
|
HTTPException: 404 模型提供商不存在
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AiiModel
|
||||||
|
"""
|
||||||
|
if not user.is_admin:
|
||||||
|
raise HTTPException(status_code=401, detail="用户无权限管理模型。") from None
|
||||||
|
|
||||||
|
try:
|
||||||
|
ap: AiiProvider = session.exec(select(AiiProvider).where(AiiProvider.id == model.aii_provider_id)).one()
|
||||||
|
except NoResultFound:
|
||||||
|
raise HTTPException(status_code=404, detail="Provider 不存在。") from None
|
||||||
|
|
||||||
|
try:
|
||||||
|
am: AiiModel = session.exec(select(AiiModel).where(AiiModel.id == id_)).one()
|
||||||
|
except NoResultFound:
|
||||||
|
raise HTTPException(status_code=404, detail="模型不存在。") from None
|
||||||
|
|
||||||
|
if ap.id != am.aii_provider_id:
|
||||||
|
raise HTTPException(status_code=400, detail="模型提供商 ID 不匹配。") from None
|
||||||
|
|
||||||
|
am.model_name = model.model_name
|
||||||
|
am.max_context_length = model.max_context_length
|
||||||
|
am.reasonable = model.reasonable
|
||||||
|
session.add(am)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(am)
|
||||||
|
return am
|
||||||
|
|
||||||
|
|
||||||
@aii_router.get("/provider/", name="获取提供商列表")
|
@aii_router.get("/provider/", name="获取提供商列表")
|
||||||
async def get_all_provider(session: Annotated[Session, Depends(get_session)]) -> ReturnDto:
|
async def get_all_provider(session: Annotated[Session, Depends(get_session)]) -> Sequence[AiiProvider]:
|
||||||
"""
|
"""
|
||||||
获取 AI 模型提供商列表。
|
获取 AI 模型提供商列表。
|
||||||
此接口无需用户登录即可访问。
|
此接口无需用户登录即可访问。
|
||||||
@@ -81,8 +125,7 @@ async def get_all_provider(session: Annotated[Session, Depends(get_session)]) ->
|
|||||||
Returns:
|
Returns:
|
||||||
被 ReturnDto 包裹的 AiiProvider 列表
|
被 ReturnDto 包裹的 AiiProvider 列表
|
||||||
"""
|
"""
|
||||||
aii_providers = session.exec(select(AiiProvider)).all()
|
return session.exec(select(AiiProvider)).all()
|
||||||
return ReturnDto(result=[z_aii_provider(ap) for ap in aii_providers])
|
|
||||||
|
|
||||||
|
|
||||||
@aii_router.post("/provider/", name="添加提供商")
|
@aii_router.post("/provider/", name="添加提供商")
|
||||||
@@ -90,11 +133,11 @@ async def add_provider(
|
|||||||
provider: AiiProviderPublic,
|
provider: AiiProviderPublic,
|
||||||
user: Annotated[ModelUser, Depends(verify_token)],
|
user: Annotated[ModelUser, Depends(verify_token)],
|
||||||
session: Annotated[Session, Depends(get_session)],
|
session: Annotated[Session, Depends(get_session)],
|
||||||
) -> ReturnDto:
|
) -> AiiProvider:
|
||||||
"""
|
"""
|
||||||
添加新的 AI 模型提供商。
|
添加新的 AI 模型提供商。
|
||||||
此接口需要管理员才能访问。
|
此接口需要管理员才能访问。
|
||||||
添加提供商时不会进行可用性检查,因此 WebUI 在前端实现了检查按钮。此端点不会负责检查。
|
不会进行可用性检查,因此 WebUI 在前端实现了检查按钮。此端点不会负责检查。
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPException: 401 表示用户未登录或非管理员。
|
HTTPException: 401 表示用户未登录或非管理员。
|
||||||
@@ -108,7 +151,47 @@ async def add_provider(
|
|||||||
session.add(ap)
|
session.add(ap)
|
||||||
session.commit()
|
session.commit()
|
||||||
session.refresh(ap)
|
session.refresh(ap)
|
||||||
return ReturnDto(result=z_aii_provider(ap))
|
return ap
|
||||||
|
|
||||||
|
|
||||||
|
@aii_router.post("/provider/{id_}/", name="修改提供商")
|
||||||
|
async def edit_provider(
|
||||||
|
id_: int,
|
||||||
|
provider: AiiProviderPublicWithoutKey,
|
||||||
|
user: Annotated[ModelUser, Depends(verify_token)],
|
||||||
|
session: Annotated[Session, Depends(get_session)],
|
||||||
|
) -> AiiProvider:
|
||||||
|
"""
|
||||||
|
修改 AI 模型提供商。
|
||||||
|
此接口需要管理员才能访问。
|
||||||
|
不会进行可用性检查,因此 WebUI 在前端实现了检查按钮。此端点不会负责检查。
|
||||||
|
**只允许修改模型提供商的名称和 Base URL。**
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: 400 模型提供商 ID 不匹配。
|
||||||
|
HTTPException: 401 表示用户未登录或非管理员。
|
||||||
|
HTTPException: 404 提供商不存在。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
被 ReturnDto 包裹的、添加的 AiiProvider
|
||||||
|
"""
|
||||||
|
if not user.is_admin:
|
||||||
|
raise HTTPException(status_code=401, detail="用户无权限管理模型。") from None
|
||||||
|
|
||||||
|
if provider.id != id_:
|
||||||
|
raise HTTPException(status_code=400, detail="模型提供商 ID 不匹配。") from None
|
||||||
|
|
||||||
|
try:
|
||||||
|
ap: AiiProvider = session.exec(select(AiiProvider).where(AiiProvider.id == id_)).one()
|
||||||
|
except NoResultFound:
|
||||||
|
raise HTTPException(status_code=404, detail="提供商不存在。") from None
|
||||||
|
|
||||||
|
ap.name = provider.name
|
||||||
|
ap.base_url = provider.base_url
|
||||||
|
session.add(ap)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(ap)
|
||||||
|
return ap
|
||||||
|
|
||||||
|
|
||||||
@aii_router.get("/provider/{id_}/remote/models/", name="获取提供商远端模型")
|
@aii_router.get("/provider/{id_}/remote/models/", name="获取提供商远端模型")
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app_: FastAPI) -> AsyncGenerator[None, Any]:
|
async def lifespan(_: FastAPI) -> AsyncGenerator[None, Any]:
|
||||||
# 在生命周期函数内先加载环境变量,再局部导入 nyahome 核心模块
|
# 在生命周期函数内先加载环境变量,再局部导入 nyahome 核心模块
|
||||||
logger.info("🚀 服务启动中...")
|
logger.info("🚀 服务启动中...")
|
||||||
|
|
||||||
@@ -50,6 +50,7 @@ app.include_router(aii_router, prefix="/api")
|
|||||||
app.mount("/nyahome", StaticFiles(directory=Path.cwd() / "public"), name="public")
|
app.mount("/nyahome", StaticFiles(directory=Path.cwd() / "public"), name="public")
|
||||||
app.mount("/download", StaticFiles(directory=Path.cwd() / ".nyahome/contents"), name="upload")
|
app.mount("/download", StaticFiles(directory=Path.cwd() / ".nyahome/contents"), name="upload")
|
||||||
|
|
||||||
|
# noinspection PyTypeChecker
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=["*"],
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from typing import Sequence
|
||||||
|
|
||||||
import openai
|
import openai
|
||||||
from openai import AsyncOpenAI
|
from openai import AsyncOpenAI
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
@@ -16,17 +18,18 @@ def apply_get_models(session: Session) -> list[dict]:
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
aii_models = session.exec(select(AiiModel).options(joinedload(AiiModel.aii_provider))).all() # type: ignore[arg-type]
|
aii_models: Sequence[AiiModel] = session.exec(select(AiiModel).options(joinedload(AiiModel.aii_provider))).all() # type: ignore[arg-type]
|
||||||
|
|
||||||
final_model_list = []
|
final_model_list = []
|
||||||
for aii_model in aii_models:
|
for aii_model in aii_models:
|
||||||
final_model_list.append({
|
final_model_list.append({
|
||||||
"id": aii_model.id,
|
"id": aii_model.id,
|
||||||
"model_name": aii_model.model_name,
|
"model_name": aii_model.model_name,
|
||||||
"max_content_length": aii_model.max_context_length,
|
"max_context_length": aii_model.max_context_length,
|
||||||
"provider_id": aii_model.id,
|
"provider_id": aii_model.aii_provider_id,
|
||||||
"provider_name": aii_model.aii_provider.name,
|
"provider_name": aii_model.aii_provider.name,
|
||||||
"base_url": aii_model.aii_provider.base_url,
|
"base_url": aii_model.aii_provider.base_url,
|
||||||
|
"reasonable": bool(aii_model.reasonable), # 数据库中的 reasonable 字段可能为 None,在这里归一为 False
|
||||||
})
|
})
|
||||||
|
|
||||||
return final_model_list
|
return final_model_list
|
||||||
|
|||||||
@@ -105,11 +105,12 @@ def apply_chat(id_: int, user_id: int, chat: ChatroomChat, session: Session) ->
|
|||||||
"api_key": model.aii_provider.api_key,
|
"api_key": model.aii_provider.api_key,
|
||||||
"model_name": model.model_name,
|
"model_name": model.model_name,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
|
"enable_thinking": chat.enable_thinking,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def s_start_async_streaming_chat(
|
async def s_start_async_streaming_chat(
|
||||||
base_url: str, api_key: str, model_name: str, messages: list
|
base_url: str, api_key: str, model_name: str, messages: list, enable_thinking: bool
|
||||||
) -> AsyncGenerator[str, None]:
|
) -> AsyncGenerator[str, None]:
|
||||||
client = AsyncOpenAI(base_url=base_url, api_key=api_key)
|
client = AsyncOpenAI(base_url=base_url, api_key=api_key)
|
||||||
stream = await client.chat.completions.create(
|
stream = await client.chat.completions.create(
|
||||||
@@ -117,6 +118,7 @@ async def s_start_async_streaming_chat(
|
|||||||
model=model_name,
|
model=model_name,
|
||||||
stream=True,
|
stream=True,
|
||||||
reasoning_effort="high",
|
reasoning_effort="high",
|
||||||
|
extra_body={"thinking": {"type": "enabled" if enable_thinking else "disabled"}},
|
||||||
)
|
)
|
||||||
|
|
||||||
# AI 说 SSE 好喵,推荐我用 SSE 喵,我不知道喵
|
# AI 说 SSE 好喵,推荐我用 SSE 喵,我不知道喵
|
||||||
@@ -135,6 +137,7 @@ async def s_start_async_streaming_chat(
|
|||||||
yield f"data: {json.dumps({'text': cd, 'type': 'output'}, ensure_ascii=False)}\n\n"
|
yield f"data: {json.dumps({'text': cd, 'type': 'output'}, ensure_ascii=False)}\n\n"
|
||||||
logger.info(f"AI 完成输出 : {aii_message}")
|
logger.info(f"AI 完成输出 : {aii_message}")
|
||||||
try:
|
try:
|
||||||
|
# noinspection PyUnboundLocalVariable
|
||||||
yield f"data: {json.dumps({'type': 'usage', **chuck.usage.model_dump()})}\n\n" # type: ignore[union-attr]
|
yield f"data: {json.dumps({'type': 'usage', **chuck.usage.model_dump()})}\n\n" # type: ignore[union-attr]
|
||||||
finally:
|
finally:
|
||||||
yield "data: [DONE]\n\n"
|
yield "data: [DONE]\n\n"
|
||||||
|
|||||||
Vendored
+8
-4
@@ -12,8 +12,10 @@ export {}
|
|||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
AiiModelAddModal: typeof import('./src/components/chatroom/AiiModelAddModal.vue')['default']
|
AiiModelAddModal: typeof import('./src/components/aii/AiiModelAddModal.vue')['default']
|
||||||
AiiProviderAddModal: typeof import('./src/components/chatroom/AiiProviderAddModal.vue')['default']
|
AiiModelEditModal: typeof import('./src/components/aii/AiiModelEditModal.vue')['default']
|
||||||
|
AiiProviderAddModal: typeof import('./src/components/aii/AiiProviderAddModal.vue')['default']
|
||||||
|
AiiProviderEditModal: typeof import('./src/components/aii/AiiProviderEditModal.vue')['default']
|
||||||
ChangeEmailModal: typeof import('./src/components/admin/ChangeEmailModal.vue')['default']
|
ChangeEmailModal: typeof import('./src/components/admin/ChangeEmailModal.vue')['default']
|
||||||
ChangePhoneModal: typeof import('./src/components/admin/ChangePhoneModal.vue')['default']
|
ChangePhoneModal: typeof import('./src/components/admin/ChangePhoneModal.vue')['default']
|
||||||
ChatControlPanel: typeof import('./src/components/chatroom/ChatControlPanel.vue')['default']
|
ChatControlPanel: typeof import('./src/components/chatroom/ChatControlPanel.vue')['default']
|
||||||
@@ -88,8 +90,10 @@ declare module 'vue' {
|
|||||||
|
|
||||||
// For TSX support
|
// For TSX support
|
||||||
declare global {
|
declare global {
|
||||||
const AiiModelAddModal: typeof import('./src/components/chatroom/AiiModelAddModal.vue')['default']
|
const AiiModelAddModal: typeof import('./src/components/aii/AiiModelAddModal.vue')['default']
|
||||||
const AiiProviderAddModal: typeof import('./src/components/chatroom/AiiProviderAddModal.vue')['default']
|
const AiiModelEditModal: typeof import('./src/components/aii/AiiModelEditModal.vue')['default']
|
||||||
|
const AiiProviderAddModal: typeof import('./src/components/aii/AiiProviderAddModal.vue')['default']
|
||||||
|
const AiiProviderEditModal: typeof import('./src/components/aii/AiiProviderEditModal.vue')['default']
|
||||||
const ChangeEmailModal: typeof import('./src/components/admin/ChangeEmailModal.vue')['default']
|
const ChangeEmailModal: typeof import('./src/components/admin/ChangeEmailModal.vue')['default']
|
||||||
const ChangePhoneModal: typeof import('./src/components/admin/ChangePhoneModal.vue')['default']
|
const ChangePhoneModal: typeof import('./src/components/admin/ChangePhoneModal.vue')['default']
|
||||||
const ChatControlPanel: typeof import('./src/components/chatroom/ChatControlPanel.vue')['default']
|
const ChatControlPanel: typeof import('./src/components/chatroom/ChatControlPanel.vue')['default']
|
||||||
|
|||||||
+1
-1
@@ -33,7 +33,7 @@ onMounted(async () => {
|
|||||||
<page-header />
|
<page-header />
|
||||||
</div>
|
</div>
|
||||||
<div class="content-container">
|
<div class="content-container">
|
||||||
<n-message-provider>
|
<n-message-provider :duration="6000">
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</n-message-provider>
|
</n-message-provider>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,3 +48,7 @@ div.nyahome-card {
|
|||||||
border: 1px solid rgb(44 44 44 / 0.4);
|
border: 1px solid rgb(44 44 44 / 0.4);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.in-form-alert {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
<div id="page-header">
|
<div id="page-header">
|
||||||
<n-text class="nav-text">🌸 Nya Home ~</n-text>
|
<n-text class="nav-text">🌸 Nya Home ~</n-text>
|
||||||
<router-link to="/" style="margin-left: auto">
|
<router-link to="/" style="margin-left: auto">
|
||||||
<n-button secondary type="tertiary" size="large">首页</n-button>
|
<n-button quaternary size="large">首页</n-button>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/chatroom">
|
<router-link to="/chatroom">
|
||||||
<n-button secondary type="tertiary" size="large">聊天室</n-button>
|
<n-button quaternary size="large">聊天室</n-button>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/marketplace">
|
<router-link to="/marketplace">
|
||||||
<n-button secondary type="tertiary" size="large">剧本市场</n-button>
|
<n-button quaternary size="large">剧本市场</n-button>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+54
-38
@@ -2,15 +2,21 @@
|
|||||||
import { type SelectOption, useMessage } from 'naive-ui'
|
import { type SelectOption, useMessage } from 'naive-ui'
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
import AiiProviderAddModal from '@/components/chatroom/AiiProviderAddModal.vue'
|
import AiiProviderAddModal from '@/components/aii/AiiProviderAddModal.vue'
|
||||||
|
import { aiiModelRules, check_remote_model } from '@/tools/avaliable-check.ts'
|
||||||
import { api } from '@/tools/web.js'
|
import { api } from '@/tools/web.js'
|
||||||
import type { AiiProviderPublicWithoutKey } from '@/types/aii.js'
|
import type { AiiModelPublic, AiiProviderPublicWithoutKey } from '@/types/aii.js'
|
||||||
import type { ReturnDto } from '@/types/response.js'
|
import type { ReturnDto } from '@/types/response.js'
|
||||||
|
|
||||||
const MESSAGE = useMessage()
|
const MESSAGE = useMessage()
|
||||||
|
|
||||||
const showModal = defineModel<boolean>('showModal', { required: true })
|
const showModal = defineModel<boolean>('showModal', { required: true })
|
||||||
|
|
||||||
|
const { reload } = defineProps<{
|
||||||
|
noAddProvider?: boolean
|
||||||
|
reload?: () => void
|
||||||
|
}>()
|
||||||
|
|
||||||
const showAddProviderModal = ref(false)
|
const showAddProviderModal = ref(false)
|
||||||
const selectProvider = ref<number | null>(null)
|
const selectProvider = ref<number | null>(null)
|
||||||
const providers = ref<AiiProviderPublicWithoutKey[]>([])
|
const providers = ref<AiiProviderPublicWithoutKey[]>([])
|
||||||
@@ -20,6 +26,7 @@ const addModelForm = ref({
|
|||||||
id: 0,
|
id: 0,
|
||||||
model_name: '',
|
model_name: '',
|
||||||
max_context_length: 0,
|
max_context_length: 0,
|
||||||
|
reasonable: false,
|
||||||
aii_provider_id: selectProvider.value,
|
aii_provider_id: selectProvider.value,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -30,14 +37,7 @@ watch(selectProvider, (newValue) => {
|
|||||||
function loadProviders() {
|
function loadProviders() {
|
||||||
api
|
api
|
||||||
.get('/aii/provider/')
|
.get('/aii/provider/')
|
||||||
.then((res) => res.data as ReturnDto)
|
.then((res) => res.data as AiiProviderPublicWithoutKey[])
|
||||||
.then((data) => {
|
|
||||||
if (data.success) {
|
|
||||||
return data.result as AiiProviderPublicWithoutKey[]
|
|
||||||
} else {
|
|
||||||
throw TypeError('因未知原因,后端业务失败。')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
providers.value = result
|
providers.value = result
|
||||||
MESSAGE.success(`成功加载了 ${result.length} 个模型提供商。`)
|
MESSAGE.success(`成功加载了 ${result.length} 个模型提供商。`)
|
||||||
@@ -82,33 +82,26 @@ function onGetRemoteModels() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCheck() {
|
async function onCheck() {
|
||||||
api
|
if (selectProvider.value) {
|
||||||
.get(`/aii/provider/${selectProvider.value}/remote/model/${addModelForm.value.model_name}/`)
|
if (await check_remote_model(selectProvider.value, addModelForm.value.model_name)) {
|
||||||
.then((res) => res.data as ReturnDto)
|
MESSAGE.success(`提供商的模型 ${addModelForm.value.model_name} 可用。`)
|
||||||
.then((data) => {
|
} else {
|
||||||
if (data.success) {
|
MESSAGE.warning(`提供商的模型 ${addModelForm.value.model_name} 不可用。`)
|
||||||
MESSAGE.success(`检测成功,模型 ${addModelForm.value.model_name} 可用。`)
|
}
|
||||||
} else {
|
} else {
|
||||||
MESSAGE.warning(`检测完成,模型 ${addModelForm.value.model_name} 不可用。`)
|
MESSAGE.warning('请选择模型提供商。')
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
MESSAGE.error(`检测过程出现问题:${err}`)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onConfirm() {
|
function onConfirm() {
|
||||||
api
|
api
|
||||||
.post('/aii/model/', JSON.stringify(addModelForm.value))
|
.post('/aii/model/', JSON.stringify(addModelForm.value))
|
||||||
.then((res) => res.data as ReturnDto)
|
.then((res) => res.data as AiiModelPublic)
|
||||||
.then((data) => {
|
.then(() => {
|
||||||
if (data.success) {
|
MESSAGE.success(`模型 ${addModelForm.value.model_name} 成功添加。`)
|
||||||
MESSAGE.success(`模型 ${addModelForm.value.model_name} 成功添加。`)
|
showModal.value = false
|
||||||
showModal.value = false
|
if (reload) reload()
|
||||||
} else {
|
|
||||||
throw TypeError('因未知原因,后端业务失败。')
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
MESSAGE.error(`添加模型失败:${err}`)
|
MESSAGE.error(`添加模型失败:${err}`)
|
||||||
@@ -118,16 +111,36 @@ function onConfirm() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-modal v-model:show="showModal" preset="card" title="添加模型">
|
<n-modal v-model:show="showModal" preset="card" title="添加模型">
|
||||||
<n-form :model="addModelForm" label-placement="left" label-width="auto" label-align="right">
|
<n-form
|
||||||
|
:model="addModelForm"
|
||||||
|
label-placement="left"
|
||||||
|
label-width="auto"
|
||||||
|
label-align="right"
|
||||||
|
:rules="aiiModelRules"
|
||||||
|
>
|
||||||
<n-form-item label="模型提供商" path="aii_provider_id">
|
<n-form-item label="模型提供商" path="aii_provider_id">
|
||||||
<n-flex style="width: 100%" justify="right" align="center">
|
<n-flex style="width: 100%" justify="right" align="center">
|
||||||
<n-select v-model:value="selectProvider" :options="providerOptions" />
|
<n-select v-model:value="selectProvider" :options="providerOptions" />
|
||||||
<n-tag round type="info">修改已添加的提供商?请前往管理中心</n-tag>
|
<n-tag round type="info" v-if="!noAddProvider">修改已添加的提供商?请前往管理中心</n-tag>
|
||||||
<n-button secondary type="success" size="small" round @click="loadProviders()"
|
<n-button
|
||||||
>刷新
|
secondary
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
round
|
||||||
|
@click="loadProviders()"
|
||||||
|
v-if="!noAddProvider"
|
||||||
|
>
|
||||||
|
刷新
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button secondary type="warning" size="small" round @click="showAddProviderModal = true"
|
<n-button
|
||||||
>添加
|
secondary
|
||||||
|
type="warning"
|
||||||
|
size="small"
|
||||||
|
round
|
||||||
|
@click="showAddProviderModal = true"
|
||||||
|
v-if="!noAddProvider"
|
||||||
|
>
|
||||||
|
添加
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
@@ -156,6 +169,9 @@ function onConfirm() {
|
|||||||
<template #suffix>K</template>
|
<template #suffix>K</template>
|
||||||
</n-input-number>
|
</n-input-number>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
<n-form-item label="支持思考">
|
||||||
|
<n-switch v-model:value="addModelForm.reasonable" />
|
||||||
|
</n-form-item>
|
||||||
<n-form-item label="添加完成">
|
<n-form-item label="添加完成">
|
||||||
<n-flex>
|
<n-flex>
|
||||||
<n-button secondary type="info" @click="onCheck()">检测</n-button>
|
<n-button secondary type="info" @click="onCheck()">检测</n-button>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import { aiiModelRules, check_remote_model } from '@/tools/avaliable-check.ts'
|
||||||
|
import { api } from '@/tools/web.ts'
|
||||||
|
import type { AiiModelPublic } from '@/types/aii.ts'
|
||||||
|
|
||||||
|
const MESSAGE = useMessage()
|
||||||
|
|
||||||
|
const showModal = defineModel('showModal', { required: true })
|
||||||
|
|
||||||
|
const { model } = defineProps<{
|
||||||
|
model: AiiModelPublic
|
||||||
|
reload: () => void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const provider = computed(() => {
|
||||||
|
return `[${model.provider_id}] ${model.provider_name}`
|
||||||
|
})
|
||||||
|
|
||||||
|
async function onCheck() {
|
||||||
|
if (await check_remote_model(model.provider_id, model.model_name)) {
|
||||||
|
MESSAGE.success(`提供商的模型 ${model.model_name} 可用。`)
|
||||||
|
} else {
|
||||||
|
MESSAGE.warning(`提供商的模型 ${model.model_name} 不可用。`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSave() {
|
||||||
|
api
|
||||||
|
.post(
|
||||||
|
`/aii/model/${model.id}`,
|
||||||
|
JSON.stringify({
|
||||||
|
model_name: model.model_name,
|
||||||
|
max_context_length: model.max_context_length,
|
||||||
|
reasonable: model.reasonable,
|
||||||
|
aii_provider_id: model.provider_id,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.then((res) => res.data as AiiModelPublic)
|
||||||
|
.then((data) => {
|
||||||
|
MESSAGE.success(
|
||||||
|
`提供商 [${model.provider_id}] ${model.provider_name} 的模型 ${data.model_name} 已更新。`,
|
||||||
|
)
|
||||||
|
showModal.value = false
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
MESSAGE.error(`更新模型失败:${err}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-modal v-model:show="showModal" preset="card" title="修改模型">
|
||||||
|
<n-alert type="warning" class="in-form-alert">
|
||||||
|
不支持更换 API Key。如果需要更换 Key,请移除并重新添加模型提供商与模型。
|
||||||
|
</n-alert>
|
||||||
|
<n-form
|
||||||
|
label-width="auto"
|
||||||
|
label-align="right"
|
||||||
|
label-placement="left"
|
||||||
|
:model="model"
|
||||||
|
:rules="aiiModelRules"
|
||||||
|
>
|
||||||
|
<n-form-item label="模型提供商">
|
||||||
|
<n-input v-model:value="provider" readonly />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="模型名称" path="model_name">
|
||||||
|
<n-input v-model:value="model.model_name" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="最大上下文长度" path="max_context_length">
|
||||||
|
<n-input-number v-model:value="model.max_context_length">
|
||||||
|
<template #suffix>k</template>
|
||||||
|
</n-input-number>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="支持思考" path="reasonable">
|
||||||
|
<n-switch v-model:value="model.reasonable" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="操作">
|
||||||
|
<n-flex>
|
||||||
|
<n-button type="info" secondary @click="onCheck()">检测</n-button>
|
||||||
|
<n-button type="primary" secondary @click="onSave()">确认</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
+20
-26
@@ -2,13 +2,18 @@
|
|||||||
import { useMessage } from 'naive-ui'
|
import { useMessage } from 'naive-ui'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { check_remote_provider } from '@/tools/avaliable-check.ts'
|
||||||
import { api } from '@/tools/web.js'
|
import { api } from '@/tools/web.js'
|
||||||
import type { ReturnDto } from '@/types/response.js'
|
import type { AiiProviderPublicWithoutKey } from '@/types/aii.ts'
|
||||||
|
|
||||||
const MESSAGE = useMessage()
|
const MESSAGE = useMessage()
|
||||||
|
|
||||||
const showModal = defineModel('showModal', { required: true })
|
const showModal = defineModel('showModal', { required: true })
|
||||||
|
|
||||||
|
const { reload } = defineProps<{
|
||||||
|
reload?: () => void
|
||||||
|
}>()
|
||||||
|
|
||||||
const addProviderForm = ref({
|
const addProviderForm = ref({
|
||||||
id: 0,
|
id: 0,
|
||||||
name: '',
|
name: '',
|
||||||
@@ -16,33 +21,22 @@ const addProviderForm = ref({
|
|||||||
api_key: '',
|
api_key: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
function onCheck() {
|
async function onCheck() {
|
||||||
api
|
if (await check_remote_provider(addProviderForm.value)) {
|
||||||
.post('/aii/remote/provider/check/', JSON.stringify(addProviderForm.value))
|
MESSAGE.success(`检查模型提供商 ${addProviderForm.value.name} 可用性成功。`)
|
||||||
.then((res) => res.data as ReturnDto)
|
} else {
|
||||||
.then((data) => {
|
MESSAGE.success(`检查模型提供商 ${addProviderForm.value.name} 可用性失败?`)
|
||||||
if (data.success) {
|
}
|
||||||
MESSAGE.success(`模型提供商检测成功,探测到 ${data.result} 个可用模型。`)
|
|
||||||
} else {
|
|
||||||
MESSAGE.warning('模型提供商检测失败,请确认 Base URI 与 API key 是否正确。')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
MESSAGE.error(`检测模型提供商时遇到未知的异常,请检查后端业务:${err}`)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onConfirm() {
|
function onConfirm() {
|
||||||
api
|
api
|
||||||
.post('/aii/provider/', JSON.stringify(addProviderForm.value))
|
.post('/aii/provider/', JSON.stringify(addProviderForm.value))
|
||||||
.then((res) => res.data as ReturnDto)
|
.then((res) => res.data as AiiProviderPublicWithoutKey)
|
||||||
.then((data) => {
|
.then(() => {
|
||||||
if (data.success) {
|
MESSAGE.success(`已添加模型提供商 ${addProviderForm.value.name} 。`)
|
||||||
MESSAGE.success(`已添加模型提供商 ${addProviderForm.value.name} 。`)
|
showModal.value = false
|
||||||
showModal.value = false
|
if (reload) reload()
|
||||||
} else {
|
|
||||||
throw TypeError('后端业务表示添加模型提供商失败,但未提供原因。')
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
MESSAGE.error(`添加模型提供商失败:${err}`)
|
MESSAGE.error(`添加模型提供商失败:${err}`)
|
||||||
@@ -53,13 +47,13 @@ function onConfirm() {
|
|||||||
<template>
|
<template>
|
||||||
<n-modal v-model:show="showModal" preset="card" title="添加模型提供商">
|
<n-modal v-model:show="showModal" preset="card" title="添加模型提供商">
|
||||||
<n-form :model="addProviderForm" label-placement="left" label-width="auto" label-align="right">
|
<n-form :model="addProviderForm" label-placement="left" label-width="auto" label-align="right">
|
||||||
<n-form-item label="名称" path="name">
|
<n-form-item label="名称" path="name" :rule="{ required: true, trigger: 'blur' }">
|
||||||
<n-input v-model:value="addProviderForm.name" />
|
<n-input v-model:value="addProviderForm.name" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="Base URL" path="base_url">
|
<n-form-item label="Base URL" path="base_url" :rule="{ required: true, trigger: 'blur' }">
|
||||||
<n-input v-model:value="addProviderForm.base_url" />
|
<n-input v-model:value="addProviderForm.base_url" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="API Key" path="api_key">
|
<n-form-item label="API Key" path="api_key" :rule="{ required: true, trigger: 'blur' }">
|
||||||
<n-input v-model:value="addProviderForm.api_key" />
|
<n-input v-model:value="addProviderForm.api_key" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="添加完成">
|
<n-form-item label="添加完成">
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {useMessage} from 'naive-ui'
|
||||||
|
|
||||||
|
import {api} from '@/tools/web.ts'
|
||||||
|
import type {AiiProviderPublicWithoutKey} from '@/types/aii.ts'
|
||||||
|
|
||||||
|
const MESSAGE = useMessage()
|
||||||
|
|
||||||
|
const showModal = defineModel('showModal', { required: true })
|
||||||
|
|
||||||
|
const { provider, reload } = defineProps<{
|
||||||
|
provider: AiiProviderPublicWithoutKey
|
||||||
|
reload: () => void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function onSave() {
|
||||||
|
api.post(`/aii/provider/${provider.id}/`, JSON.stringify(provider)).then(() => {
|
||||||
|
MESSAGE.success(`模型提供商 [${provider.id}]${provider.name} 成功保存~`)
|
||||||
|
showModal.value = false
|
||||||
|
reload()
|
||||||
|
}).catch((err) => {
|
||||||
|
MESSAGE.error(`修改提供商信息失败:${err}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-modal v-model:show="showModal" preset="card" title="修改模型提供商">
|
||||||
|
<n-alert type="warning" class="in-form-alert">
|
||||||
|
不支持更换 API Key。如果需要更换 Key,请移除并重新添加模型提供商与模型。
|
||||||
|
</n-alert>
|
||||||
|
<n-form label-placement="left" label-align="right" label-width="auto" :model="provider">
|
||||||
|
<n-form-item label="提供商名称" path="name" :rule="{ required: true, trigger: 'blur' }">
|
||||||
|
<n-input v-model:value="provider.name" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="Base URL" path="base_url" :rule="{ required: true, trigger: 'blur' }">
|
||||||
|
<n-input v-model:value="provider.base_url" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="操作">
|
||||||
|
<n-button secondary type="primary" @click="onSave()">确认</n-button>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type SelectOption, useMessage } from 'naive-ui'
|
import { NTag, type SelectOption, useMessage } from 'naive-ui'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, h, onMounted, ref, type VNode, watch } from 'vue'
|
||||||
|
|
||||||
import AiiModelAddModal from '@/components/chatroom/AiiModelAddModal.vue'
|
|
||||||
import ChatPromptQuicker from '@/components/chatroom/ChatPromptQuicker.vue'
|
import ChatPromptQuicker from '@/components/chatroom/ChatPromptQuicker.vue'
|
||||||
import ChatroomEditorModal from '@/components/chatroom/ChatroomEditorModal.vue'
|
import ChatroomEditorModal from '@/components/chatroom/ChatroomEditorModal.vue'
|
||||||
import ScriptDrawer from '@/components/chatroom/ScriptDrawer.vue'
|
import ScriptDrawer from '@/components/chatroom/ScriptDrawer.vue'
|
||||||
@@ -10,12 +9,14 @@ import { useNowUser } from '@/stores/now-user.ts'
|
|||||||
import { api } from '@/tools/web.js'
|
import { api } from '@/tools/web.js'
|
||||||
import type { AiiModelPublic } from '@/types/aii.js'
|
import type { AiiModelPublic } from '@/types/aii.js'
|
||||||
import type { Chatroom, ChatroomPublic } from '@/types/chatroom.ts'
|
import type { Chatroom, ChatroomPublic } from '@/types/chatroom.ts'
|
||||||
import type { ReturnDto } from '@/types/response.js'
|
|
||||||
|
import AiiModelAddModal from '../aii/AiiModelAddModal.vue'
|
||||||
|
|
||||||
const NOWUSER = useNowUser()
|
const NOWUSER = useNowUser()
|
||||||
const MESSAGE = useMessage()
|
const MESSAGE = useMessage()
|
||||||
|
|
||||||
const selectedModel = defineModel<number | null>('selectModel', { required: true })
|
const selectedModelId = defineModel<number | null>('selectModelId', { required: true })
|
||||||
|
const selectedModel = defineModel<AiiModelPublic | null>('selectModel', { required: true })
|
||||||
const quickerPrompt = defineModel<string>('quickerPrompt', { required: true })
|
const quickerPrompt = defineModel<string>('quickerPrompt', { required: true })
|
||||||
|
|
||||||
const { chatroom, loadPage } = defineProps<{
|
const { chatroom, loadPage } = defineProps<{
|
||||||
@@ -32,23 +33,34 @@ const modelOptions = computed(() => {
|
|||||||
for (const model of models.value) {
|
for (const model of models.value) {
|
||||||
options.push({
|
options.push({
|
||||||
value: model.id,
|
value: model.id,
|
||||||
label: `[${model.provider_name}] ${model.model_name}`,
|
label: model.model_name,
|
||||||
|
provider: model.provider_name,
|
||||||
|
reasonable: model.reasonable ? '思考' : '非思考',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return options
|
return options
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 在选中的模型 ID 以及请求得到的模型列表出现变化时,重新确定当前选中模型。
|
||||||
|
// 此处选中的模型会同步到上级组件 Chatroom1Page,然后同步给 ChatTable。
|
||||||
|
// 从而,ChatTable 能够提供思考开关以及更多设置。
|
||||||
|
watch(
|
||||||
|
[selectedModelId, models],
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal[0]) {
|
||||||
|
const newModel = models.value.find((v) => v.id === newVal[0])
|
||||||
|
if (newModel) {
|
||||||
|
selectedModel.value = newModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
function loadModels() {
|
function loadModels() {
|
||||||
api
|
api
|
||||||
.get('/aii/model')
|
.get('/aii/model/')
|
||||||
.then((res) => res.data as ReturnDto)
|
.then((res) => (models.value = res.data as AiiModelPublic[]))
|
||||||
.then((data) => {
|
|
||||||
if (data.success) {
|
|
||||||
models.value = data.result as AiiModelPublic[]
|
|
||||||
} else {
|
|
||||||
throw TypeError('获取模型列表失败……')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
MESSAGE.error(`加载模型列表失败:${err}`)
|
MESSAGE.error(`加载模型列表失败:${err}`)
|
||||||
})
|
})
|
||||||
@@ -57,7 +69,7 @@ function loadModels() {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadModels()
|
loadModels()
|
||||||
if (chatroom.default_model_id) {
|
if (chatroom.default_model_id) {
|
||||||
selectedModel.value = chatroom.default_model_id
|
selectedModelId.value = chatroom.default_model_id
|
||||||
} else {
|
} else {
|
||||||
MESSAGE.info(
|
MESSAGE.info(
|
||||||
'此聊天室还未设置默认模型。你需要选择一个模型然后开始聊天,或者现在就保存一个默认模型嘛?',
|
'此聊天室还未设置默认模型。你需要选择一个模型然后开始聊天,或者现在就保存一个默认模型嘛?',
|
||||||
@@ -77,8 +89,8 @@ const chatroomInfo = ref<ChatroomPublic>({
|
|||||||
})
|
})
|
||||||
|
|
||||||
function saveDefaultModel() {
|
function saveDefaultModel() {
|
||||||
if (selectedModel.value) {
|
if (selectedModelId.value) {
|
||||||
chatroomInfo.value.default_model_id = selectedModel.value
|
chatroomInfo.value.default_model_id = selectedModelId.value
|
||||||
api
|
api
|
||||||
.post(`/chatroom/${chatroom.id}/`, JSON.stringify(chatroomInfo.value))
|
.post(`/chatroom/${chatroom.id}/`, JSON.stringify(chatroomInfo.value))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -91,6 +103,41 @@ function saveDefaultModel() {
|
|||||||
MESSAGE.warning('请先选择一个模型哦~')
|
MESSAGE.warning('请先选择一个模型哦~')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderLabel(option: SelectOption): VNode {
|
||||||
|
return h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: '5px',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h(
|
||||||
|
NTag,
|
||||||
|
{
|
||||||
|
type: 'primary',
|
||||||
|
size: 'small',
|
||||||
|
round: true,
|
||||||
|
},
|
||||||
|
option.provider as string,
|
||||||
|
),
|
||||||
|
option.label as string,
|
||||||
|
h(
|
||||||
|
NTag,
|
||||||
|
{
|
||||||
|
type: 'info',
|
||||||
|
size: 'small',
|
||||||
|
round: true,
|
||||||
|
},
|
||||||
|
option.reasonable as string,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -99,16 +146,6 @@ function saveDefaultModel() {
|
|||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<n-flex>
|
<n-flex>
|
||||||
<n-button secondary type="info" size="small" round @click="loadModels()">刷新</n-button>
|
<n-button secondary type="info" size="small" round @click="loadModels()">刷新</n-button>
|
||||||
<n-button
|
|
||||||
v-if="NOWUSER.is_admin"
|
|
||||||
secondary
|
|
||||||
type="warning"
|
|
||||||
size="small"
|
|
||||||
round
|
|
||||||
@click="showAddModelModal = true"
|
|
||||||
>
|
|
||||||
添加
|
|
||||||
</n-button>
|
|
||||||
<n-button-group>
|
<n-button-group>
|
||||||
<n-button secondary type="primary" size="small" round @click="saveDefaultModel()">
|
<n-button secondary type="primary" size="small" round @click="saveDefaultModel()">
|
||||||
保存
|
保存
|
||||||
@@ -127,7 +164,23 @@ function saveDefaultModel() {
|
|||||||
</n-button-group>
|
</n-button-group>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</template>
|
</template>
|
||||||
<n-select v-model:value="selectedModel" :options="modelOptions" />
|
<n-select v-model:value="selectedModelId" :options="modelOptions" :render-label="renderLabel">
|
||||||
|
<template #action>
|
||||||
|
<n-flex>
|
||||||
|
<n-button
|
||||||
|
v-if="NOWUSER.is_admin"
|
||||||
|
secondary
|
||||||
|
type="warning"
|
||||||
|
size="small"
|
||||||
|
round
|
||||||
|
@click="showAddModelModal = true"
|
||||||
|
>
|
||||||
|
添加
|
||||||
|
</n-button>
|
||||||
|
<n-tag type="info" round v-if="NOWUSER.is_admin">前往管理后端修改已有模型</n-tag>
|
||||||
|
</n-flex>
|
||||||
|
</template>
|
||||||
|
</n-select>
|
||||||
<aii-model-add-modal v-model:show-modal="showAddModelModal" />
|
<aii-model-add-modal v-model:show-modal="showAddModelModal" />
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { createChatTableMessages } from '@/components/chatroom/chat-table-messages.js'
|
import { createChatTableMessages } from '@/components/chatroom/chat-table-messages.js'
|
||||||
import { md } from '@/tools/md.js'
|
import { md } from '@/tools/md.js'
|
||||||
|
import type { AiiModelPublic } from '@/types/aii.ts'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
content: string | null
|
content: string | null
|
||||||
aiiThinking: string
|
aiiThinking: string
|
||||||
aiiMessage: string | null
|
aiiMessage: string | null
|
||||||
aiiTokenInfo: string
|
aiiTokenInfo: string
|
||||||
|
model: AiiModelPublic | null
|
||||||
onSendMessage: () => void
|
onSendMessage: () => void
|
||||||
onAccept: () => void
|
onAccept: () => void
|
||||||
onRewrite: () => void
|
onRewrite: () => void
|
||||||
@@ -35,8 +37,13 @@ const mode = defineModel<'continue' | 'expand'>('mode', { required: true })
|
|||||||
|
|
||||||
<div v-if="aiiMessage === null" class="editor">
|
<div v-if="aiiMessage === null" class="editor">
|
||||||
<n-input v-model:value="message" type="textarea" :resizable="false" />
|
<n-input v-model:value="message" type="textarea" :resizable="false" />
|
||||||
<n-flex justify="right" align="center">
|
<n-flex justify="right" align="center" size="small" v-if="model">
|
||||||
<n-button type="tertiary" size="small" circle>!</n-button>
|
<n-button type="tertiary" size="small" circle>!</n-button>
|
||||||
|
<n-switch size="large" v-if="model.reasonable">
|
||||||
|
<template #checked>开启思考</template>
|
||||||
|
<template #unchecked>关闭思考</template>
|
||||||
|
<template #icon>💡</template>
|
||||||
|
</n-switch>
|
||||||
<n-switch
|
<n-switch
|
||||||
v-model:value="mode"
|
v-model:value="mode"
|
||||||
size="large"
|
size="large"
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { useMessage } from 'naive-ui'
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
import InputFile from '@/components/file/InputFile.vue'
|
import InputFile from '@/components/file/InputFile.vue'
|
||||||
import SelectFileModal from '@/components/file/SelectFileModal.vue'
|
|
||||||
import UploadFileModal from '@/components/file/UploadFileModal.vue'
|
|
||||||
import { api } from '@/tools/web.js'
|
import { api } from '@/tools/web.js'
|
||||||
import type { ChatroomPublic } from '@/types/chatroom.js'
|
import type { ChatroomPublic } from '@/types/chatroom.js'
|
||||||
import type { ReturnDto } from '@/types/response.js'
|
import type { ReturnDto } from '@/types/response.js'
|
||||||
@@ -65,15 +63,6 @@ function onSubmit() {
|
|||||||
<n-button secondary type="primary" @click="onSubmit()">确认!</n-button>
|
<n-button secondary type="primary" @click="onSubmit()">确认!</n-button>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
|
|
||||||
<select-file-modal
|
|
||||||
:max="1"
|
|
||||||
:extensions="['png', 'jpeg', 'jpg']"
|
|
||||||
:load-files="loadFiles"
|
|
||||||
v-model:show-modal="showSelectModal"
|
|
||||||
v-model:select-files="selectFiles"
|
|
||||||
/>
|
|
||||||
<upload-file-modal v-model:show-modal="showUploadModal" :after-leave="loadFiles" />
|
|
||||||
</n-modal>
|
</n-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import ChatControlPanel from '@/components/chatroom/ChatControlPanel.vue'
|
|||||||
import ChatroomCard from '@/components/chatroom/ChatroomCard.vue'
|
import ChatroomCard from '@/components/chatroom/ChatroomCard.vue'
|
||||||
import ChatTable from '@/components/chatroom/ChatTable.vue'
|
import ChatTable from '@/components/chatroom/ChatTable.vue'
|
||||||
import { api } from '@/tools/web.ts'
|
import { api } from '@/tools/web.ts'
|
||||||
import type { AiiTokenInfo } from '@/types/aii.ts'
|
import type { AiiModelPublic, AiiTokenInfo } from '@/types/aii.ts'
|
||||||
import type { Chatroom } from '@/types/chatroom.ts'
|
import type { Chatroom } from '@/types/chatroom.ts'
|
||||||
import type { ReturnDto } from '@/types/response.ts'
|
import type { ReturnDto } from '@/types/response.ts'
|
||||||
import { SEE_YOU_TOMORROW } from '@/types/syt.ts'
|
import { SEE_YOU_TOMORROW } from '@/types/syt.ts'
|
||||||
@@ -26,7 +26,8 @@ const MESSAGE = useMessage()
|
|||||||
|
|
||||||
const chatroom = ref<Chatroom | null>(null)
|
const chatroom = ref<Chatroom | null>(null)
|
||||||
|
|
||||||
const selectedModel = ref<number | null>(null)
|
const selectedModelId = ref<number | null>(null)
|
||||||
|
const selectedModel = ref<AiiModelPublic | null>(null)
|
||||||
const quickerPrompt = ref('')
|
const quickerPrompt = ref('')
|
||||||
const inputMessage = ref<string>('')
|
const inputMessage = ref<string>('')
|
||||||
const inputMode = ref<'continue' | 'expand'>('expand')
|
const inputMode = ref<'continue' | 'expand'>('expand')
|
||||||
@@ -67,7 +68,7 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
function chat() {
|
function chat() {
|
||||||
if (!selectedModel.value) {
|
if (!selectedModelId.value) {
|
||||||
MESSAGE.warning('未选择模型,无法开始创作喵!')
|
MESSAGE.warning('未选择模型,无法开始创作喵!')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -91,7 +92,7 @@ function chat() {
|
|||||||
message: inputMessage.value,
|
message: inputMessage.value,
|
||||||
prefix: quickerPrompt.value,
|
prefix: quickerPrompt.value,
|
||||||
mode: inputMode.value,
|
mode: inputMode.value,
|
||||||
model_id: selectedModel.value,
|
model_id: selectedModelId.value,
|
||||||
}),
|
}),
|
||||||
openWhenHidden: true, // 此开关控制在浏览器失去焦点时是否保持连接开启。默认为 false 会导致焦点转移时流式传输中断然后重连,很怪
|
openWhenHidden: true, // 此开关控制在浏览器失去焦点时是否保持连接开启。默认为 false 会导致焦点转移时流式传输中断然后重连,很怪
|
||||||
|
|
||||||
@@ -249,6 +250,7 @@ function enableSidebar() {
|
|||||||
:aii-thinking
|
:aii-thinking
|
||||||
:aii-message
|
:aii-message
|
||||||
:aii-token-info
|
:aii-token-info
|
||||||
|
:model="selectedModel"
|
||||||
v-model:message="inputMessage"
|
v-model:message="inputMessage"
|
||||||
v-model:mode="inputMode"
|
v-model:mode="inputMode"
|
||||||
:on-send-message="chat"
|
:on-send-message="chat"
|
||||||
@@ -265,6 +267,7 @@ function enableSidebar() {
|
|||||||
:chatroom="chatroom"
|
:chatroom="chatroom"
|
||||||
:load-page="load"
|
:load-page="load"
|
||||||
v-model:quicker-prompt="quickerPrompt"
|
v-model:quicker-prompt="quickerPrompt"
|
||||||
|
v-model:select-model-id="selectedModelId"
|
||||||
v-model:select-model="selectedModel"
|
v-model:select-model="selectedModel"
|
||||||
/>
|
/>
|
||||||
<div id="sidebar-toggle" @click="disableSidebar" />
|
<div id="sidebar-toggle" @click="disableSidebar" />
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ref } from 'vue'
|
|||||||
|
|
||||||
import ConfigCard from '@/components/admin/ConfigCard.vue'
|
import ConfigCard from '@/components/admin/ConfigCard.vue'
|
||||||
import InDev from '@/components/InDev.vue'
|
import InDev from '@/components/InDev.vue'
|
||||||
|
import AdminAii from '@/pages/nyahome/AdminAii.vue'
|
||||||
import { api } from '@/tools/web.ts'
|
import { api } from '@/tools/web.ts'
|
||||||
import type { ReturnDto } from '@/types/response.ts'
|
import type { ReturnDto } from '@/types/response.ts'
|
||||||
|
|
||||||
@@ -101,6 +102,10 @@ function sendTestMail() {
|
|||||||
</config-card>
|
</config-card>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<n-tab-pane name="aii" tab="AII" display-directive="show">
|
||||||
|
<admin-aii />
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
<n-tab-pane name="site_info" tab="站点信息" display-directive="show">
|
<n-tab-pane name="site_info" tab="站点信息" display-directive="show">
|
||||||
<n-flex vertical>
|
<n-flex vertical>
|
||||||
<config-card title="基本信息">
|
<config-card title="基本信息">
|
||||||
@@ -233,8 +238,4 @@ function sendTestMail() {
|
|||||||
<n-empty size="large" v-else description="请尝试手动获取设置..." />
|
<n-empty size="large" v-else description="请尝试手动获取设置..." />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
.in-form-alert {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type DataTableColumns, NButton, NTag } from 'naive-ui'
|
||||||
|
import { h, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
import ConfigCard from '@/components/admin/ConfigCard.vue'
|
||||||
|
import AiiModelAddModal from '@/components/aii/AiiModelAddModal.vue'
|
||||||
|
import AiiModelEditModal from '@/components/aii/AiiModelEditModal.vue'
|
||||||
|
import AiiProviderAddModal from '@/components/aii/AiiProviderAddModal.vue'
|
||||||
|
import AiiProviderEditModal from '@/components/aii/AiiProviderEditModal.vue'
|
||||||
|
import { api } from '@/tools/web.ts'
|
||||||
|
import type { AiiModelPublic, AiiProviderPublicWithoutKey } from '@/types/aii.ts'
|
||||||
|
|
||||||
|
const showProviderAddModal = ref(false)
|
||||||
|
const showProviderEditModal = ref(false)
|
||||||
|
const showModelAddModal = ref(false)
|
||||||
|
const showModelEditModal = ref(false)
|
||||||
|
|
||||||
|
function createProviderColumns(): DataTableColumns<AiiProviderPublicWithoutKey> {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
key: 'id',
|
||||||
|
render(row) {
|
||||||
|
return h(
|
||||||
|
NTag,
|
||||||
|
{
|
||||||
|
type: 'error',
|
||||||
|
round: true,
|
||||||
|
},
|
||||||
|
row.id,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '提供商名称',
|
||||||
|
key: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Base URL',
|
||||||
|
key: 'base_url',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
render(row) {
|
||||||
|
return h(
|
||||||
|
NButton,
|
||||||
|
{
|
||||||
|
type: 'warning',
|
||||||
|
secondary: true,
|
||||||
|
round: true,
|
||||||
|
onClick() {
|
||||||
|
selectedProvider.value = row
|
||||||
|
showProviderEditModal.value = true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'修改',
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function createModelColumns(): DataTableColumns<AiiModelPublic> {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
key: 'id',
|
||||||
|
render(row) {
|
||||||
|
return h(
|
||||||
|
NTag,
|
||||||
|
{
|
||||||
|
type: 'primary',
|
||||||
|
round: true,
|
||||||
|
},
|
||||||
|
row.id,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '模型名称',
|
||||||
|
key: 'model_name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '最大上下文长度(k)',
|
||||||
|
key: 'max_context_length',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '支持思考',
|
||||||
|
key: 'reasonable',
|
||||||
|
render(row) {
|
||||||
|
return h(
|
||||||
|
NTag,
|
||||||
|
{
|
||||||
|
type: 'info',
|
||||||
|
round: true,
|
||||||
|
},
|
||||||
|
row.reasonable ? '思考' : '非思考',
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '所属提供商',
|
||||||
|
key: 'provider_id',
|
||||||
|
render(row) {
|
||||||
|
return h(
|
||||||
|
NTag,
|
||||||
|
{
|
||||||
|
type: 'error',
|
||||||
|
round: true,
|
||||||
|
},
|
||||||
|
row.provider_id,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
render(row) {
|
||||||
|
return h(
|
||||||
|
NButton,
|
||||||
|
{
|
||||||
|
type: 'warning',
|
||||||
|
secondary: true,
|
||||||
|
round: true,
|
||||||
|
onClick() {
|
||||||
|
selectedModel.value = row
|
||||||
|
showModelEditModal.value = true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'修改',
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerColumns = createProviderColumns()
|
||||||
|
const modelColumns = createModelColumns()
|
||||||
|
const providers = ref<AiiProviderPublicWithoutKey[]>([])
|
||||||
|
const models = ref<AiiModelPublic[]>([])
|
||||||
|
const selectedModel = ref<AiiModelPublic | null>(null)
|
||||||
|
const selectedProvider = ref<AiiProviderPublicWithoutKey | null>(null)
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
api
|
||||||
|
.get('/aii/provider/')
|
||||||
|
.then((res) => res.data as AiiProviderPublicWithoutKey[])
|
||||||
|
.then((data) => (providers.value = data))
|
||||||
|
api
|
||||||
|
.get('/aii/model/')
|
||||||
|
.then((res) => res.data as AiiModelPublic[])
|
||||||
|
.then((data) => (models.value = data))
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
load()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-flex vertical align="center">
|
||||||
|
<n-card>
|
||||||
|
<n-flex>
|
||||||
|
<n-h4 style="margin: 0">刷新本页信息(如果你正在从其他地方修改)</n-h4>
|
||||||
|
<n-button style="margin-left: auto" type="info" @click="load()">更新</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<config-card title="模型提供商">
|
||||||
|
<template #extra>
|
||||||
|
<n-button round type="info" @click="showProviderAddModal = true">添加</n-button>
|
||||||
|
</template>
|
||||||
|
<n-data-table :columns="providerColumns" :data="providers" />
|
||||||
|
</config-card>
|
||||||
|
|
||||||
|
<config-card title="模型">
|
||||||
|
<template #extra>
|
||||||
|
<n-button round type="info" @click="showModelAddModal = true">添加</n-button>
|
||||||
|
</template>
|
||||||
|
<n-data-table :columns="modelColumns" :data="models" />
|
||||||
|
</config-card>
|
||||||
|
|
||||||
|
<aii-provider-add-modal v-model:show-modal="showProviderAddModal" :reload="load" />
|
||||||
|
<aii-model-add-modal v-model:show-modal="showModelAddModal" no-add-provider :reload="load" />
|
||||||
|
<aii-provider-edit-modal
|
||||||
|
:provider="selectedProvider"
|
||||||
|
v-model:show-modal="showProviderEditModal"
|
||||||
|
:reload="load"
|
||||||
|
/>
|
||||||
|
<aii-model-edit-modal
|
||||||
|
:model="selectedModel"
|
||||||
|
v-model:show-modal="showModelEditModal"
|
||||||
|
:reload="load"
|
||||||
|
/>
|
||||||
|
</n-flex>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import type { FormRules } from 'naive-ui'
|
||||||
|
|
||||||
|
import { api } from '@/tools/web.ts'
|
||||||
|
import type { AiiProviderPublic } from '@/types/aii.ts'
|
||||||
|
import type { ReturnDto } from '@/types/response.ts'
|
||||||
|
|
||||||
|
export async function check_remote_model(provider_id: number, model_name: string) {
|
||||||
|
try {
|
||||||
|
return await api
|
||||||
|
.get(`/aii/provider/${provider_id}/remote/model/${model_name}/`)
|
||||||
|
.then((res) => res.data as ReturnDto)
|
||||||
|
.then((data) => data.success)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('检测远端模型可用性时出现问题:', provider_id, model_name, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function check_remote_provider(provider: AiiProviderPublic) {
|
||||||
|
try {
|
||||||
|
return await api
|
||||||
|
.post('/aii/remote/provider/check/', JSON.stringify(provider))
|
||||||
|
.then((res) => res.data as ReturnDto)
|
||||||
|
.then((data) => data.success)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`检查远端模型提供商可用性时出现问题:`, provider, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const aiiModelRules: FormRules = {
|
||||||
|
aii_provider_id: {
|
||||||
|
required: true,
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
model_name: {
|
||||||
|
required: true,
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
max_context_length: {
|
||||||
|
required: true,
|
||||||
|
trigger: ['change', 'blur'],
|
||||||
|
message: '最大上下文长度需要合理设置。大部分模型的上下文长度在数百到一千 k 左右',
|
||||||
|
validator(_, value) {
|
||||||
|
if (typeof value !== 'number') return new Error('非数字')
|
||||||
|
if (value < 20) return new Error('上下文长度过小,不太合理?')
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ export interface AiiModelPublic {
|
|||||||
provider_id: number
|
provider_id: number
|
||||||
provider_name: string
|
provider_name: string
|
||||||
base_url: string
|
base_url: string
|
||||||
|
reasonable: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AiiProviderPublic {
|
export interface AiiProviderPublic {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {resolve} from 'path'
|
|||||||
import AutoImport from 'unplugin-auto-import/vite'
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
import {NaiveUiResolver} from 'unplugin-vue-components/resolvers'
|
import {NaiveUiResolver} from 'unplugin-vue-components/resolvers'
|
||||||
import Components from 'unplugin-vue-components/vite'
|
import Components from 'unplugin-vue-components/vite'
|
||||||
import {unheadVueComposablesImports} from '@unhead/vue'
|
import {unheadVueComposablesImports} from '@unhead/vue' // 从 package.json 里搞到 WebUI 版本号
|
||||||
|
|
||||||
// 从 package.json 里搞到 WebUI 版本号
|
// 从 package.json 里搞到 WebUI 版本号
|
||||||
const pkg = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf-8'))
|
const pkg = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf-8'))
|
||||||
@@ -19,7 +19,9 @@ export default defineConfig({
|
|||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
vueJsx(),
|
vueJsx(),
|
||||||
vueDevTools(),
|
vueDevTools({
|
||||||
|
launchEditor: 'pycharm',
|
||||||
|
}),
|
||||||
AutoImport({
|
AutoImport({
|
||||||
imports: [
|
imports: [
|
||||||
'vue',
|
'vue',
|
||||||
|
|||||||
Reference in New Issue
Block a user