Compare commits
14 Commits
1f1ac5f87a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
23f03822f6
|
|||
|
ad3bafcd35
|
|||
|
82723038c3
|
|||
|
03928c6c59
|
|||
|
ee81ccefc5
|
|||
|
2b30f0ffe3
|
|||
|
c8c474ecfd
|
|||
|
567c146fb8
|
|||
|
7df66bbc61
|
|||
|
45e255856a
|
|||
|
ab703e6176
|
|||
|
884cea53a1
|
|||
|
52f6904bef
|
|||
|
a7140ea5c1
|
@@ -22,3 +22,5 @@ alembic/versions/
|
|||||||
|
|
||||||
.idea
|
.idea
|
||||||
.codemoss
|
.codemoss
|
||||||
|
|
||||||
|
alembic.ini
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ NyaHome 是由 FastAPI 后端、Vue WebUI 实现的在线 AI 文学创作平台
|
|||||||
| 功能 | 阶段 | 优先级 |
|
| 功能 | 阶段 | 优先级 |
|
||||||
|---------------------------|-----|-----|
|
|---------------------------|-----|-----|
|
||||||
| **剧本市场** - 剧本分享、聊天室导出为剧本 | 规划中 | 低 |
|
| **剧本市场** - 剧本分享、聊天室导出为剧本 | 规划中 | 低 |
|
||||||
| **用户功能** - 绑定手机号、接收收集验证码 | 规划中 | 低 |
|
| **用户功能** - 绑定手机号、接收手机验证码 | 规划中 | 低 |
|
||||||
| **用户功能** - 第三方账户 Oauth 登录 | 规划中 | 低 |
|
| **用户功能** - 第三方账户 Oauth 登录 | 规划中 | 低 |
|
||||||
|
|
||||||
## 代码规范
|
## 代码规范
|
||||||
|
|||||||
-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.
|
||||||
|
|||||||
@@ -36,8 +36,29 @@
|
|||||||
scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
|
scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* vitepress-openapi */
|
/* ===== vitepress-oepnapi ===== */
|
||||||
|
|
||||||
|
/* Details */
|
||||||
|
details {
|
||||||
|
padding: 10px 6px;
|
||||||
|
border-width: 1px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-color: transparent;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
details[open] {
|
||||||
|
border-color: rgba(255, 255, 255, 0.3)
|
||||||
|
}
|
||||||
|
|
||||||
|
details>summary {
|
||||||
|
text-align: center;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部 */
|
||||||
div.vitepress-openapi {
|
div.vitepress-openapi {
|
||||||
|
background: linear-gradient(45deg, hsla(58, 100%, 92%, 0.6), hsla(128, 100%, 75%, 0.5));
|
||||||
border: 1px solid #64ffc4;
|
border: 1px solid #64ffc4;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
margin: 48px auto 0;
|
margin: 48px auto 0;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const operationId = route.data.params.operationId
|
|||||||
<template #branding>
|
<template #branding>
|
||||||
<div class="vitepress-openapi">
|
<div class="vitepress-openapi">
|
||||||
<p>API 文档是基于最新代码自动生成的</p>
|
<p>API 文档是基于最新代码自动生成的</p>
|
||||||
<p>由 VitePress OpenAPI 提供文档支持</p>
|
<p>由 <a href="https://vitepress-openapi.vercel.app/" target="_blank">VitePress OpenAPI</a> 提供文档支持</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</OAOperation>
|
</OAOperation>
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
version: 1
|
|
||||||
disable_existing_loggers: false
|
|
||||||
|
|
||||||
formatters:
|
|
||||||
default:
|
|
||||||
"()": uvicorn.logging.DefaultFormatter
|
|
||||||
fmt: "%(asctime)s | %(levelprefix)s %(name)s | %(message)s"
|
|
||||||
use_colors: true
|
|
||||||
|
|
||||||
access:
|
|
||||||
"()": uvicorn.logging.AccessFormatter
|
|
||||||
fmt: '%(asctime)s | %(client_addr)s - "%(request_line)s" %(status_code)s'
|
|
||||||
use_colors: true
|
|
||||||
|
|
||||||
handlers:
|
|
||||||
default:
|
|
||||||
formatter: default
|
|
||||||
class: logging.StreamHandler
|
|
||||||
stream: ext://sys.stderr
|
|
||||||
|
|
||||||
access:
|
|
||||||
formatter: access
|
|
||||||
class: logging.StreamHandler
|
|
||||||
stream: ext://sys.stdout
|
|
||||||
|
|
||||||
file:
|
|
||||||
formatter: default
|
|
||||||
class: logging.handlers.RotatingFileHandler
|
|
||||||
filename: .nyahome/app.log
|
|
||||||
maxBytes: 10485760
|
|
||||||
backupCount: 5
|
|
||||||
encoding: utf8
|
|
||||||
|
|
||||||
loggers:
|
|
||||||
uvicorn:
|
|
||||||
handlers: [ default, file ]
|
|
||||||
level: INFO
|
|
||||||
propagate: false
|
|
||||||
|
|
||||||
uvicorn.error:
|
|
||||||
handlers: [ default, file ]
|
|
||||||
level: INFO
|
|
||||||
propagate: false
|
|
||||||
|
|
||||||
uvicorn.access:
|
|
||||||
handlers: [ access, file ]
|
|
||||||
level: INFO
|
|
||||||
propagate: false
|
|
||||||
|
|
||||||
nyahome:
|
|
||||||
handlers: [ default, file ]
|
|
||||||
level: DEBUG
|
|
||||||
propagate: false
|
|
||||||
+23
-9
@@ -6,32 +6,46 @@ readme = "README.md"
|
|||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aiofiles>=25.1.0",
|
"aiofiles>=25.1.0",
|
||||||
"aiosmtplib>=5.1.0",
|
"aiosmtplib>=5.1.1",
|
||||||
"alembic>=1.18.4",
|
"alembic>=1.18.4",
|
||||||
"argon2-cffi>=25.1.0",
|
"argon2-cffi>=25.1.0",
|
||||||
"fastapi>=0.136.1",
|
"fastapi>=0.136.3",
|
||||||
"httpx>=0.28.1",
|
"httpx>=0.28.1",
|
||||||
"jinja2>=3.1.6",
|
"jinja2>=3.1.6",
|
||||||
"openai>=2.38.0",
|
"openai>=2.40.0",
|
||||||
"passlib[bcrypt]>=1.7.4",
|
"passlib[bcrypt]>=1.7.4",
|
||||||
"psycopg[binary]>=3.3.4",
|
|
||||||
"pydantic>=2.13.4",
|
"pydantic>=2.13.4",
|
||||||
"python-dotenv>=1.2.2",
|
"python-dotenv>=1.2.2",
|
||||||
"python-jose[cryptography]>=3.5.0",
|
"python-jose[cryptography]>=3.5.0",
|
||||||
"python-multipart>=0.0.29",
|
"python-multipart>=0.0.30",
|
||||||
"pyyaml>=6.0.3",
|
"pyyaml>=6.0.3",
|
||||||
"rich>=15.0.0",
|
"rich>=15.0.0",
|
||||||
"sqlalchemy>=2.0.49",
|
"sqlalchemy>=2.0.50",
|
||||||
"sqlmodel>=0.0.38",
|
"sqlmodel>=0.0.38",
|
||||||
"typer>=0.25.1",
|
"typer>=0.26.5",
|
||||||
"uvicorn>=0.47.0",
|
"uvicorn>=0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
mysql = [
|
||||||
|
"pymysql>=1.2.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
postgresql = [
|
||||||
|
"psycopg[binary]>=3.3.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
all = [
|
||||||
|
"nyahome[mysql]",
|
||||||
|
"nyahome[postgresql]",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
|
"nyahome[all]",
|
||||||
"docstring-parser>=0.18.0",
|
"docstring-parser>=0.18.0",
|
||||||
"mypy>=2.1.0",
|
"mypy>=2.1.0",
|
||||||
"ruff>=0.15.14",
|
"ruff>=0.15.15",
|
||||||
"taskipy>=1.14.1",
|
"taskipy>=1.14.1",
|
||||||
"types-aiofiles>=25.1.0.20260518",
|
"types-aiofiles>=25.1.0.20260518",
|
||||||
"types-passlib>=1.7.7.20260211",
|
"types-passlib>=1.7.7.20260211",
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
DATA_DIR = Path.cwd() / ".nyahome"
|
||||||
|
ENV_PATH = DATA_DIR / ".env"
|
||||||
|
LOGGING_YAML = DATA_DIR / "logging.yaml"
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
from typing import Annotated, Sequence
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
|
from .cli import ENV_PATH, console
|
||||||
|
|
||||||
|
aii_app = typer.Typer()
|
||||||
|
|
||||||
|
|
||||||
|
@aii_app.command(name="list")
|
||||||
|
def list_all_provider() -> None:
|
||||||
|
"""
|
||||||
|
列出已设置的所有提供商和模型。
|
||||||
|
"""
|
||||||
|
load_dotenv(ENV_PATH)
|
||||||
|
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
|
from nyahome.database import AiiProvider, engine
|
||||||
|
|
||||||
|
table = Table(title="AI 模型提供商与已录入模型")
|
||||||
|
table.add_column("ID", style="cyan", no_wrap=True)
|
||||||
|
table.add_column("提供商名称", style="white", no_wrap=True)
|
||||||
|
table.add_column("Base URL", style="white", no_wrap=True)
|
||||||
|
table.add_column("录入的模型", style="bright_black")
|
||||||
|
|
||||||
|
with Session(engine) as session:
|
||||||
|
aps: Sequence[AiiProvider] = session.exec(select(AiiProvider)).all()
|
||||||
|
for ap in aps:
|
||||||
|
table.add_row(str(ap.id), ap.name, ap.base_url, str(ap.aii_models))
|
||||||
|
|
||||||
|
console.print(table)
|
||||||
|
|
||||||
|
|
||||||
|
@aii_app.command()
|
||||||
|
def add_provider(
|
||||||
|
name: Annotated[str, typer.Argument(help="提供商名称")],
|
||||||
|
base_url: Annotated[str, typer.Argument(help="提供商 Base URL(OpenAI 兼容端点)")],
|
||||||
|
api_key: Annotated[
|
||||||
|
str,
|
||||||
|
typer.Option(
|
||||||
|
"--api-key",
|
||||||
|
"-k",
|
||||||
|
help="提供商 API Key",
|
||||||
|
prompt=True,
|
||||||
|
hide_input=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
添加 AI 提供商。需要提供商名称、Base URL 和 API Key。
|
||||||
|
"""
|
||||||
|
load_dotenv(ENV_PATH)
|
||||||
|
|
||||||
|
from sqlmodel import Session
|
||||||
|
|
||||||
|
from nyahome.database import AiiProvider, engine
|
||||||
|
|
||||||
|
console.print(f"[cyan]正在添加模型提供商 [{name}]({base_url})[/cyan]")
|
||||||
|
|
||||||
|
with Session(engine) as session:
|
||||||
|
ap = AiiProvider(name=name, base_url=base_url, api_key=api_key)
|
||||||
|
session.add(ap)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(ap)
|
||||||
|
|
||||||
|
console.print(f"[cyan]添加完成 [{ap.id}][{ap.name}]({ap.base_url})[/cyan]")
|
||||||
|
|
||||||
|
|
||||||
|
@aii_app.command()
|
||||||
|
def add_model(
|
||||||
|
model_name: Annotated[str, typer.Argument(help="模型名称(需准确填写)")],
|
||||||
|
max_context_length: Annotated[int, typer.Argument(help="最大上下文长度(单位为 k)")],
|
||||||
|
provider_id: Annotated[
|
||||||
|
int,
|
||||||
|
typer.Option(
|
||||||
|
"--provider-id",
|
||||||
|
"-p",
|
||||||
|
help="该模型所属于的模型提供商 ID",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
reasonable: Annotated[
|
||||||
|
bool,
|
||||||
|
typer.Option(
|
||||||
|
"--reasonable",
|
||||||
|
"-r",
|
||||||
|
help="支持思考",
|
||||||
|
),
|
||||||
|
] = False,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
添加 AI 模型。在此之前需要先添加该模型的提供商。
|
||||||
|
"""
|
||||||
|
load_dotenv(ENV_PATH)
|
||||||
|
|
||||||
|
from sqlmodel import Session
|
||||||
|
|
||||||
|
from nyahome.database import AiiModel, engine
|
||||||
|
|
||||||
|
with Session(engine) as session:
|
||||||
|
am = AiiModel(
|
||||||
|
model_name=model_name,
|
||||||
|
max_context_length=max_context_length,
|
||||||
|
aii_provider_id=provider_id,
|
||||||
|
reasonable=reasonable,
|
||||||
|
)
|
||||||
|
session.add(am)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(am)
|
||||||
|
|
||||||
|
console.print(f"[cyan]已添加模型 [{am.id}][{am.model_name}]({am.aii_provider_id})[/cyan]")
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import json
|
||||||
|
from importlib.metadata import distribution
|
||||||
|
from importlib.util import find_spec
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Mapping
|
||||||
|
|
||||||
|
from nyahome.cli.cli import console
|
||||||
|
from nyahome.data import db_driver_available, db_type_allowlist
|
||||||
|
|
||||||
|
|
||||||
|
class CliWarning:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.counter = 0
|
||||||
|
|
||||||
|
def info(self, description: str) -> None:
|
||||||
|
console.print(f"INFO - {description}")
|
||||||
|
|
||||||
|
def warning(self, description: str) -> None:
|
||||||
|
self.counter += 1
|
||||||
|
console.print(f"[yellow]WARNING {self.counter}[/yellow] - {description}")
|
||||||
|
|
||||||
|
|
||||||
|
cw = CliWarning()
|
||||||
|
|
||||||
|
|
||||||
|
def check_database_connector() -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
检查是否安装用于数据库连接的各种驱动库。只有安装对应的驱动库之后才可以连接到对应的数据库。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{驱动库: 可用状态描述}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _(value: bool, description: str) -> str:
|
||||||
|
return f"{'[green]可用[/green]' if value else '[yellow]不可用[/yellow]'} - {description}"
|
||||||
|
|
||||||
|
result: dict[str, str] = {
|
||||||
|
"sqlite3": _(bool(find_spec("sqlite3")), "Python 标准库,支持 sqlite"),
|
||||||
|
"pymysql": _(bool(find_spec("pymysql")), "社区维护的 MySQL 驱动库"),
|
||||||
|
"psycopg": _(bool(find_spec("psycopg")), "更先进的 PostgreSQL 驱动库"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def check_database_type(environ: Mapping[str, str | None]) -> None:
|
||||||
|
db_type = environ.get("NYAHOME_DB_TYPE")
|
||||||
|
db_driver = environ.get("NYAHOME_DB_DRIVER")
|
||||||
|
db_name = environ.get("NYAHOME_DB_NAME")
|
||||||
|
db_user = environ.get("NYAHOME_DB_USER")
|
||||||
|
db_password = environ.get("NYAHOME_DB_PASSWORD")
|
||||||
|
db_host = environ.get("NYAHOME_DB_HOST")
|
||||||
|
db_port = environ.get("NYAHOME_DB_PORT")
|
||||||
|
|
||||||
|
if not db_type:
|
||||||
|
cw.warning("NYAHOME_DB_TYPE 未设置,将回退到默认数据库 sqlite。")
|
||||||
|
elif db_type not in db_type_allowlist:
|
||||||
|
cw.warning(f"NYAHOME_DB_TYPE 的值 {db_type} 不受 NyaHome 官方支持。")
|
||||||
|
|
||||||
|
if not db_driver:
|
||||||
|
cw.warning("NYAHOME_DB_DRIVER 未设置,将使用 SQLModel 的默认驱动库。")
|
||||||
|
elif not find_spec(db_driver):
|
||||||
|
cw.warning(f"NYAHOME_DB_DRIVER 的值 {db_driver} 未在当前 NyaHome 中安装。")
|
||||||
|
elif db_type and (db_driver not in db_driver_available.get(db_type, [])):
|
||||||
|
cw.warning(f"NYAHOME_DB_DRIVER 的值 {db_driver} 是为数据库 {db_type} 准备的吗?")
|
||||||
|
|
||||||
|
if db_driver and db_type != "sqlite": # 对于 sqlite 数据库,不需要设置凭证
|
||||||
|
if not db_name:
|
||||||
|
cw.warning("NYAHOME_DB_NAME 未设置,将使用 [cyan]nyahome[/cyan] 作为默认值。")
|
||||||
|
if not db_user:
|
||||||
|
cw.warning("NYAHOME_DB_USER 未设置,将使用 [cyan]nyahome[/cyan] 作为默认值。")
|
||||||
|
if not db_password:
|
||||||
|
cw.warning("NYAHOME_DB_PASSWORD 未设置,将使用 [cyan]nyahome[/cyan] 作为默认值。")
|
||||||
|
if not db_host:
|
||||||
|
cw.warning("NYAHOME_DB_HOST 未设置,将使用 [cyan]localhost[/cyan] 作为默认值。")
|
||||||
|
if not db_port:
|
||||||
|
cw.warning("NYAHOME_DB_PORT 未设置,将使用 [cyan]3306[/cyan] 作为默认值。")
|
||||||
|
cw.info("自检未检查数据库状态是否可用。")
|
||||||
|
else:
|
||||||
|
cw.info("使用 sqlite 数据库,跳过数据库凭证检查。")
|
||||||
|
|
||||||
|
|
||||||
|
def check_uvicorn(environ: Mapping[str, str | None]) -> None:
|
||||||
|
un_host = environ.get("NYAHOME_UVICORN_HOST")
|
||||||
|
un_port = environ.get("NYAHOME_UVICORN_PORT")
|
||||||
|
un_reload = environ.get("NYAHOME_UVICORN_RELOAD")
|
||||||
|
|
||||||
|
if not un_host:
|
||||||
|
cw.warning("NYAHOME_UVICORN_HOST 未设置,将使用 [cyan]0.0.0.0[/cyan] 作为默认值。")
|
||||||
|
if not un_port:
|
||||||
|
cw.warning("NYAHOME_UVICORN_PORT 未设置,将使用 [cyan]9000[/cyan] 作为默认值。")
|
||||||
|
if not un_reload:
|
||||||
|
cw.warning("NYAHOME_UVICORN_RELOAD 未设置,将使用 [cyan]false[/cyan] 作为默认值。")
|
||||||
|
else:
|
||||||
|
if un_reload in ["True", "true", "1"]:
|
||||||
|
cw.warning("NYAHOME_UVICORN_RELOAD 设置为 [cyan]true[/cyan],在生产环境中不应如此。")
|
||||||
|
|
||||||
|
|
||||||
|
def check_nyahome_status() -> None:
|
||||||
|
# 检查是否以可编辑模式安装 NyaHome
|
||||||
|
dist = distribution("nyahome")
|
||||||
|
try:
|
||||||
|
f = dist.read_text("direct_url.json")
|
||||||
|
if f:
|
||||||
|
data = json.loads(f)
|
||||||
|
if data.get("dir_info", {}).get("editable", False):
|
||||||
|
cw.warning("当前 NyaHome 以可编辑模式安装。在生产环境中不应如此。")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 检查 NyaHome 是否受 git 管理
|
||||||
|
git_dir = Path.cwd() / ".git"
|
||||||
|
if git_dir.is_dir():
|
||||||
|
cw.warning("当前 NyaHome 受版本控制系统管理。在生产环境中不应如此。")
|
||||||
|
|
||||||
|
|
||||||
|
LOGGING_YAML_CONTENT = """version: 1
|
||||||
|
disable_existing_loggers: false
|
||||||
|
|
||||||
|
formatters:
|
||||||
|
default:
|
||||||
|
"()": uvicorn.logging.DefaultFormatter
|
||||||
|
fmt: "%(asctime)s | %(levelprefix)s %(name)s | %(message)s"
|
||||||
|
use_colors: true
|
||||||
|
|
||||||
|
access:
|
||||||
|
"()": uvicorn.logging.AccessFormatter
|
||||||
|
fmt: '%(asctime)s | %(client_addr)s - "%(request_line)s" %(status_code)s'
|
||||||
|
use_colors: true
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
default:
|
||||||
|
formatter: default
|
||||||
|
class: logging.StreamHandler
|
||||||
|
stream: ext://sys.stderr
|
||||||
|
|
||||||
|
access:
|
||||||
|
formatter: access
|
||||||
|
class: logging.StreamHandler
|
||||||
|
stream: ext://sys.stdout
|
||||||
|
|
||||||
|
file:
|
||||||
|
formatter: default
|
||||||
|
class: logging.handlers.RotatingFileHandler
|
||||||
|
filename: .nyahome/app.log
|
||||||
|
maxBytes: 10485760
|
||||||
|
backupCount: 5
|
||||||
|
encoding: utf8
|
||||||
|
|
||||||
|
loggers:
|
||||||
|
uvicorn:
|
||||||
|
handlers: [ default, file ]
|
||||||
|
level: INFO
|
||||||
|
propagate: false
|
||||||
|
|
||||||
|
uvicorn.error:
|
||||||
|
handlers: [ default, file ]
|
||||||
|
level: INFO
|
||||||
|
propagate: false
|
||||||
|
|
||||||
|
uvicorn.access:
|
||||||
|
handlers: [ access, file ]
|
||||||
|
level: INFO
|
||||||
|
propagate: false
|
||||||
|
|
||||||
|
nyahome:
|
||||||
|
handlers: [ default, file ]
|
||||||
|
level: DEBUG
|
||||||
|
propagate: false"""
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
|
from nyahome.config import Config, config_manager
|
||||||
|
|
||||||
|
from .cli import console
|
||||||
|
|
||||||
|
config_app = typer.Typer()
|
||||||
|
|
||||||
|
|
||||||
|
@config_app.command(name="list")
|
||||||
|
def list_all_configs() -> None:
|
||||||
|
"""
|
||||||
|
列出所有 NyaHome 定义的设置项。直接输出,可能包含敏感信息。
|
||||||
|
|
||||||
|
同时包含默认值和当前值。在 NyaHome 首次运行时,所有设置项都会以默认值存储。
|
||||||
|
"""
|
||||||
|
config_manager.sync_load_config()
|
||||||
|
ci = Config()
|
||||||
|
|
||||||
|
table = Table(title="NyaHome 设置")
|
||||||
|
table.add_column("设置键名", style="cyan", no_wrap=True)
|
||||||
|
table.add_column("值类型", style="bright_black", no_wrap=True)
|
||||||
|
table.add_column("当前值", style="white")
|
||||||
|
table.add_column("默认值", style="bright_black")
|
||||||
|
|
||||||
|
for key, value in config_manager.get_config().items():
|
||||||
|
default_value = getattr(ci, key)
|
||||||
|
table.add_row(key, type(default_value).__name__, str(value), str(default_value))
|
||||||
|
|
||||||
|
console.print(table)
|
||||||
|
|
||||||
|
|
||||||
|
@config_app.command(name="set")
|
||||||
|
def set_config_item(
|
||||||
|
key: Annotated[str, typer.Argument(help="设置键名")],
|
||||||
|
value: Annotated[list[str], typer.Argument(help="设置键新值,类型会自动转换,多个输入将被视作列表")],
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
修改一项设置。
|
||||||
|
|
||||||
|
目前,NyaHome 的设置键所支持的值类型包括:str int bool list
|
||||||
|
"""
|
||||||
|
config_manager.sync_load_config()
|
||||||
|
if len(value) == 1:
|
||||||
|
value: str | int | bool = value[0] # type: ignore[no-redef]
|
||||||
|
try:
|
||||||
|
config_manager.set(key, value)
|
||||||
|
except AttributeError:
|
||||||
|
console.print(f"[yellow]设置失败,设置键 [cyan]{key}[/cyan] 不存在。[/yellow]")
|
||||||
|
config_manager.sync_save_config()
|
||||||
|
console.print(f"已经将设置项 [cyan]{key}[/cyan] 的值设置为 [cyan]{value}[/cyan]")
|
||||||
|
|
||||||
|
|
||||||
|
@config_app.command(name="reset")
|
||||||
|
def reset_config_item(
|
||||||
|
key: Annotated[str, typer.Argument(help="设置键名")],
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
重置一项设置至默认值。
|
||||||
|
"""
|
||||||
|
config_manager.sync_load_config()
|
||||||
|
try:
|
||||||
|
config_manager.reset(key)
|
||||||
|
except AttributeError:
|
||||||
|
console.print(f"[yellow]设置失败,设置键 [cyan]{key}[/cyan] 不存在。[/yellow]")
|
||||||
|
config_manager.sync_save_config()
|
||||||
|
console.print(f"已经将设置项 [cyan]{key}[/cyan] 的值重置为默认值。")
|
||||||
@@ -1,19 +1,15 @@
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
from dotenv import load_dotenv, set_key, unset_key
|
from dotenv import load_dotenv, set_key, unset_key
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
|
||||||
from .cli import console
|
from .cli import ENV_PATH, console
|
||||||
|
|
||||||
env_app = typer.Typer()
|
env_app = typer.Typer()
|
||||||
|
|
||||||
|
|
||||||
ENV_PATH = Path.cwd() / ".nyahome" / ".env"
|
|
||||||
|
|
||||||
|
|
||||||
@env_app.command(name="list")
|
@env_app.command(name="list")
|
||||||
def list_all_envs() -> None:
|
def list_all_envs() -> None:
|
||||||
"""
|
"""
|
||||||
@@ -46,8 +42,11 @@ def set_env(
|
|||||||
|
|
||||||
保存在 .nyahome 内的 .env 文件。
|
保存在 .nyahome 内的 .env 文件。
|
||||||
"""
|
"""
|
||||||
set_key(ENV_PATH, f"NYAHOME_{key.upper()}", value)
|
key = key.upper()
|
||||||
console.print(f"[cyan]已设置环境变量 NYAHOME_{key}。[/cyan]")
|
if not key.startswith("NYAHOME_"):
|
||||||
|
key = f"NYAHOME_{key}"
|
||||||
|
set_key(ENV_PATH, key, value)
|
||||||
|
console.print(f"[cyan]已设置环境变量 {key}。[/cyan]")
|
||||||
|
|
||||||
|
|
||||||
@env_app.command(name="unset")
|
@env_app.command(name="unset")
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
from .config import Config
|
||||||
from .manager import config_manager
|
from .manager import config_manager
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
Config,
|
||||||
config_manager,
|
config_manager,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -11,10 +11,15 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
CONFIG_PATH = Path.cwd() / ".nyahome" / "config.json"
|
CONFIG_PATH = Path.cwd() / ".nyahome" / "config.json"
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T", str, int, bool, list)
|
||||||
|
|
||||||
|
|
||||||
class ConfigManager:
|
class ConfigManager:
|
||||||
|
"""
|
||||||
|
ConfigManager 携带一个初始化的 Config 实例。在 Config 初始化时,所有的默认设置键的值就都已经加载。
|
||||||
|
因此,如果不 load_config,ConfigManager 也将持有一套默认设置。
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
CONFIG_PATH.parent.mkdir(exist_ok=True)
|
CONFIG_PATH.parent.mkdir(exist_ok=True)
|
||||||
self._config = Config()
|
self._config = Config()
|
||||||
@@ -76,6 +81,53 @@ class ConfigManager:
|
|||||||
"""
|
"""
|
||||||
return getattr(self._config, key, default) # type: ignore[return-value]
|
return getattr(self._config, key, default) # type: ignore[return-value]
|
||||||
|
|
||||||
|
def set(self, key: str, value: T) -> None:
|
||||||
|
"""
|
||||||
|
设置配置项。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: 配置键名
|
||||||
|
value: 配置键的新值,可以是(且仅支持)字符串、整型以及列表。
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AttributeError: 配置键名错误
|
||||||
|
TypeError: 配置键值类型错误
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
old_value = self.get(key)
|
||||||
|
except AttributeError as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
match old_value:
|
||||||
|
case str():
|
||||||
|
new_value = str(value)
|
||||||
|
case int():
|
||||||
|
new_value = int(value)
|
||||||
|
case bool():
|
||||||
|
new_value = bool(value)
|
||||||
|
case list():
|
||||||
|
new_value = list(value)
|
||||||
|
case _:
|
||||||
|
raise TypeError(f"不支持 {type(old_value).__name__} 类型的设置项。({key})")
|
||||||
|
setattr(self._config, key, new_value)
|
||||||
|
|
||||||
|
def reset(self, key: str) -> None:
|
||||||
|
"""
|
||||||
|
将配置项恢复至默认值。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: 配置键名
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AttributeError: 配置键名错误
|
||||||
|
"""
|
||||||
|
ci = Config()
|
||||||
|
try:
|
||||||
|
default_value = getattr(ci, key)
|
||||||
|
except AttributeError as e:
|
||||||
|
raise e
|
||||||
|
setattr(self._config, key, default_value)
|
||||||
|
|
||||||
def get_config(self) -> dict[str, Any]:
|
def get_config(self) -> dict[str, Any]:
|
||||||
config = {}
|
config = {}
|
||||||
for attr in dir(self._config):
|
for attr in dir(self._config):
|
||||||
|
|||||||
@@ -42,17 +42,17 @@ class OtpMemoryStore(ABC):
|
|||||||
async def _cleanup(self) -> None:
|
async def _cleanup(self) -> None:
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(60)
|
await asyncio.sleep(60)
|
||||||
logger.debug(f"[{self.type_name}] 开始定时清理过期验证码。")
|
# logger.debug(f"[{self.type_name}] 开始定时清理过期验证码。")
|
||||||
expires = []
|
expires = []
|
||||||
count = 0
|
count = 0
|
||||||
for address, item in self._store.items():
|
for address, item in self._store.items():
|
||||||
if item.expire_time < time.time():
|
if item.expire_time < time.time():
|
||||||
logger.debug(f"[{self.type_name}] 移除过期的 {address}")
|
# logger.debug(f"[{self.type_name}] 移除过期的 {address}")
|
||||||
expires.append(address)
|
expires.append(address)
|
||||||
count += 1
|
count += 1
|
||||||
for address in expires:
|
for address in expires:
|
||||||
self._store.pop(address)
|
self._store.pop(address)
|
||||||
logger.debug(f"[{self.type_name}] 清理完成,清理了 {count} 个过期验证码。")
|
# logger.debug(f"[{self.type_name}] 清理完成,清理了 {count} 个过期验证码。")
|
||||||
|
|
||||||
def verify(self, address: str, user_id: int, verify_code: str) -> bool:
|
def verify(self, address: str, user_id: int, verify_code: str) -> bool:
|
||||||
item = self._store.get(address)
|
item = self._store.get(address)
|
||||||
|
|||||||
@@ -95,10 +95,10 @@ class EmailSenderQueue(TaskQueue):
|
|||||||
body=item.body,
|
body=item.body,
|
||||||
sender=config_manager.get("smtp_sender"),
|
sender=config_manager.get("smtp_sender"),
|
||||||
hostname=config_manager.get("smtp_hostname"),
|
hostname=config_manager.get("smtp_hostname"),
|
||||||
port=config_manager.get("smtp_port"),
|
port=config_manager.get("smtp_port", 465),
|
||||||
username=config_manager.get("smtp_username"),
|
username=config_manager.get("smtp_username"),
|
||||||
password=config_manager.get("smtp_password"),
|
password=config_manager.get("smtp_password"),
|
||||||
use_tls=config_manager.get("smtp_use_tls"),
|
use_tls=config_manager.get("smtp_use_tls", True),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
db_driver_available = {
|
||||||
|
"sqlite": ["sqlite3"],
|
||||||
|
"mysql": ["pymysql"],
|
||||||
|
"postgresql": ["psycopg"],
|
||||||
|
}
|
||||||
|
db_type_allowlist = ["sqlite", "mysql", "postgresql"]
|
||||||
@@ -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,
|
||||||
@@ -18,7 +18,10 @@ from .session import async_get_session, get_session
|
|||||||
|
|
||||||
# 创建数据库连接和数据库文件
|
# 创建数据库连接和数据库文件
|
||||||
def create_db() -> None: # noqa: RUF067
|
def create_db() -> None: # noqa: RUF067
|
||||||
|
try:
|
||||||
SQLModel.metadata.create_all(engine)
|
SQLModel.metadata.create_all(engine)
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(f"连接或创建数据库失败:{e}") from e
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -26,6 +29,7 @@ __all__ = [
|
|||||||
AiiModelPublic,
|
AiiModelPublic,
|
||||||
AiiProvider,
|
AiiProvider,
|
||||||
AiiProviderPublic,
|
AiiProviderPublic,
|
||||||
|
AiiProviderPublicWithoutKey,
|
||||||
ChatScript,
|
ChatScript,
|
||||||
Chatroom,
|
Chatroom,
|
||||||
ChatroomChat,
|
ChatroomChat,
|
||||||
@@ -39,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,5 +1,34 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
|
from sqlalchemy import Engine
|
||||||
from sqlmodel import create_engine
|
from sqlmodel import create_engine
|
||||||
|
|
||||||
engine = create_engine(os.environ["NYAHOME_SQL_URL"])
|
from nyahome.data import db_driver_available, db_type_allowlist
|
||||||
|
|
||||||
|
|
||||||
|
def build_engine() -> Engine:
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
db_type = os.environ.get("NYAHOME_DB_TYPE", "sqlite")
|
||||||
|
db_driver = os.environ.get("NYAHOME_DB_DRIVER")
|
||||||
|
if db_type not in db_type_allowlist:
|
||||||
|
logger.warning(f"数据库类型 {db_type} 不受 NyaHome 官方支持,建议改用受支持的数据库:{db_type_allowlist}")
|
||||||
|
else:
|
||||||
|
if db_driver is None:
|
||||||
|
db_driver = db_driver_available[db_type][0]
|
||||||
|
|
||||||
|
if db_type == "sqlite":
|
||||||
|
if db_driver == "sqlite3": # fix: sqlalchemy 中使用 pysqlite 表示默认的 sqlite3 标准库驱动,气得我直接缺省算惹
|
||||||
|
return create_engine("sqlite:///.nyahome/nyahome.db")
|
||||||
|
return create_engine(f"sqlite+{db_driver}:///.nyahome/nyahome.db")
|
||||||
|
db_name = os.environ.get("NYAHOME_DB_NAME", "nyahome")
|
||||||
|
db_user = os.environ.get("NYAHOME_DB_USER", "nyahome")
|
||||||
|
db_password = os.environ.get("NYAHOME_DB_PASSWORD", "nyahome")
|
||||||
|
db_host = os.environ.get("NYAHOME_DB_HOST", "localhost")
|
||||||
|
db_port = os.environ.get("NYAHOME_DB_PORT", "3306")
|
||||||
|
return create_engine(f"{db_type}+{db_driver}://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}")
|
||||||
|
|
||||||
|
|
||||||
|
engine = build_engine()
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
from pydantic import BaseModel
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, SerializerFunctionWrapHandler, model_serializer
|
||||||
from sqlmodel import Field, Relationship, SQLModel
|
from sqlmodel import Field, Relationship, SQLModel
|
||||||
|
|
||||||
|
|
||||||
@@ -7,16 +9,28 @@ class AiiProvider(SQLModel, table=True):
|
|||||||
模型提供商。
|
模型提供商。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id: int | None = Field(default=None, primary_key=True)
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
name: str
|
name: str
|
||||||
base_url: str
|
base_url: str
|
||||||
api_key: str
|
api_key: str
|
||||||
|
|
||||||
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: int | None = None
|
id: Optional[int] = None
|
||||||
name: str
|
name: str
|
||||||
base_url: str
|
base_url: str
|
||||||
api_key: str
|
api_key: str
|
||||||
@@ -27,34 +41,30 @@ class AiiModel(SQLModel, table=True):
|
|||||||
模型。
|
模型。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id: int | None = 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")
|
||||||
|
|
||||||
|
@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: int | None = 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:
|
from .model_story import Chatroom # noqa: E402
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,33 +16,40 @@ class Chatroom(SQLModel, table=True):
|
|||||||
规定 creator_id 为 0 的聊天室为公共聊天室,其权限由配置文件决定。
|
规定 creator_id 为 0 的聊天室为公共聊天室,其权限由配置文件决定。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id: int | None = Field(default=None, primary_key=True)
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
feature_image: str = Field(
|
feature_image: str = Field(
|
||||||
default=f"{config_manager.get('site_url', 'http://localhost:9000')}/nyahome/normal-thumbnail.png"
|
default=f"{config_manager.get('site_url', 'http://localhost:9000')}/nyahome/normal-thumbnail.png"
|
||||||
)
|
)
|
||||||
content: str
|
content: str = Field(description="聊天室中的聊天内容。作为已序列化的 json 格式字符串存储,请参阅数据保存。")
|
||||||
script: str
|
script: str = Field(description="聊天室中的世界书内容。作为已序列化的 json 格式字符串存储,请参阅数据保存。")
|
||||||
|
|
||||||
script_template_id: int | None = Field(
|
script_template_id: Optional[int] = Field(
|
||||||
default=None, sa_column=Column(ForeignKey("scripttemplate.id", name="fk_chatroom_script_template"))
|
default=None, sa_column=Column(ForeignKey("scripttemplate.id", name="fk_chatroom_script_template"))
|
||||||
)
|
)
|
||||||
script_template_version: str | None
|
script_template_version: Optional[str]
|
||||||
script_template: "ScriptTemplate" = Relationship()
|
script_template: "ScriptTemplate" = Relationship()
|
||||||
|
|
||||||
creator_id: int = Field(sa_column=Column(ForeignKey("modeluser.id", name="fk_chatroom_creator")))
|
creator_id: int = Field(sa_column=Column(ForeignKey("modeluser.id", name="fk_chatroom_creator")))
|
||||||
creator: Optional["ModelUser"] = Relationship(back_populates="chatrooms")
|
creator: "ModelUser" = Relationship(back_populates="chatrooms")
|
||||||
|
|
||||||
|
default_model_id: Optional[int] = Field(
|
||||||
|
sa_column=Column(ForeignKey("aiimodel.id", name="fk_chatroom_default_model"))
|
||||||
|
)
|
||||||
|
default_model: Optional["AiiModel"] = Relationship(back_populates="chatrooms")
|
||||||
|
|
||||||
|
|
||||||
class ChatroomPublic(BaseModel):
|
class ChatroomPublic(BaseModel):
|
||||||
id: int | None = None
|
id: Optional[int] = None
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
feature_image: str
|
feature_image: str
|
||||||
|
|
||||||
script_template_id: int | None = None
|
script_template_id: Optional[int] = None
|
||||||
script_template_version: str | None
|
script_template_version: Optional[str] = None
|
||||||
|
|
||||||
|
default_model_id: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
class ScriptTemplate(SQLModel, table=True):
|
class ScriptTemplate(SQLModel, table=True):
|
||||||
@@ -85,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):
|
||||||
@@ -104,4 +112,5 @@ class ChatroomChatDelete(BaseModel):
|
|||||||
change: Literal["user", "aii"]
|
change: Literal["user", "aii"]
|
||||||
|
|
||||||
|
|
||||||
|
from .model_aii import AiiModel # noqa: E402
|
||||||
from .model_user import ModelUser # noqa: E402
|
from .model_user import ModelUser # noqa: E402
|
||||||
|
|||||||
+146
-3
@@ -3,12 +3,16 @@
|
|||||||
避免在此文件中引用 router 和 service 模块内的代码。
|
避免在此文件中引用 router 和 service 模块内的代码。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
from nyahome import __version__
|
from nyahome import __version__
|
||||||
from nyahome.cli.cli import console
|
from nyahome.cli.cli import console
|
||||||
|
from nyahome.cli.cli_aii import aii_app
|
||||||
|
from nyahome.cli.cli_config import config_app
|
||||||
from nyahome.cli.cli_env import ENV_PATH, env_app
|
from nyahome.cli.cli_env import ENV_PATH, env_app
|
||||||
|
|
||||||
app = typer.Typer(
|
app = typer.Typer(
|
||||||
@@ -35,7 +39,7 @@ def main(
|
|||||||
is_eager=True,
|
is_eager=True,
|
||||||
),
|
),
|
||||||
) -> None:
|
) -> None:
|
||||||
console.print("[bright_black]NyaHome 仍然处于极早期的阶段。如果遇到任何问题,请告诉芒果帆帆喵![/bright_black]")
|
console.print("(!) [bright_black]NyaHome 仍然处于极早期的阶段。如果遇到任何问题,请告诉芒果帆帆喵![/bright_black]")
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
@@ -56,7 +60,7 @@ def run() -> None:
|
|||||||
host=os.getenv("NYAHOME_UVICORN_HOST", "0.0.0.0"),
|
host=os.getenv("NYAHOME_UVICORN_HOST", "0.0.0.0"),
|
||||||
port=int(os.getenv("NYAHOME_UVICORN_PORT", "9000")),
|
port=int(os.getenv("NYAHOME_UVICORN_PORT", "9000")),
|
||||||
timeout_graceful_shutdown=2,
|
timeout_graceful_shutdown=2,
|
||||||
log_config="logging.yaml",
|
log_config=".nyahome/logging.yaml",
|
||||||
log_level="debug",
|
log_level="debug",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -68,13 +72,152 @@ def openapi(
|
|||||||
"""
|
"""
|
||||||
根据代码导出 NyaHome 的 openapi.json 。
|
根据代码导出 NyaHome 的 openapi.json 。
|
||||||
"""
|
"""
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv(ENV_PATH)
|
||||||
|
|
||||||
from nyahome.server import save_openapi_json
|
from nyahome.server import save_openapi_json
|
||||||
|
|
||||||
save_openapi_json(path)
|
save_openapi_json(path)
|
||||||
console.print(f"[cyan]已经保存 openapi.json 到 {path} 。[/cyan]")
|
console.print(f"[cyan]已经保存 openapi.json 到 {path} 。[/cyan]")
|
||||||
|
|
||||||
|
|
||||||
app.add_typer(env_app, name="env", no_args_is_help=True, help="设置 NyaHome 应用的环境变量。")
|
@app.command()
|
||||||
|
def init() -> None:
|
||||||
|
"""
|
||||||
|
交互式初始化 NyaHome。
|
||||||
|
"""
|
||||||
|
from dotenv import set_key
|
||||||
|
from rich.prompt import Confirm, IntPrompt, Prompt
|
||||||
|
|
||||||
|
from nyahome.cli.cli import DATA_DIR, ENV_PATH, LOGGING_YAML
|
||||||
|
from nyahome.cli.cli_check import LOGGING_YAML_CONTENT
|
||||||
|
from nyahome.config import config_manager
|
||||||
|
from nyahome.data import db_driver_available, db_type_allowlist
|
||||||
|
|
||||||
|
console.print("\n准备初始化 NyaHome。")
|
||||||
|
|
||||||
|
# 1.数据目录初始化
|
||||||
|
if DATA_DIR.is_dir():
|
||||||
|
console.print("\n1.数据目录 [cyan].nyahome[/cyan] 已存在,跳过创建。")
|
||||||
|
else:
|
||||||
|
DATA_DIR.mkdir()
|
||||||
|
console.print("\n1.已创建数据目录 [cyan].nyahome[/cyan]。")
|
||||||
|
|
||||||
|
# 2.日志配置文件初始化
|
||||||
|
if LOGGING_YAML.is_file():
|
||||||
|
console.print("\n2.日志配置文件 [cyan]logging.yaml[/cyan] 已存在,跳过创建。")
|
||||||
|
if Confirm.ask("需要[yellow]覆盖其至默认值[/yellow]吗?", default=False):
|
||||||
|
with open(LOGGING_YAML, "w") as f:
|
||||||
|
f.write(LOGGING_YAML_CONTENT)
|
||||||
|
console.print("已覆盖至默认值。")
|
||||||
|
else:
|
||||||
|
with open(LOGGING_YAML, "w") as f:
|
||||||
|
f.write(LOGGING_YAML_CONTENT)
|
||||||
|
console.print("\n2.已创建日志配置文件 [cyan]logging.yaml[/cyan]。")
|
||||||
|
|
||||||
|
# 3.环境变量初始化
|
||||||
|
console.print("\n3.一些必须的环境变量需要设置。")
|
||||||
|
if Confirm.ask("\n设置[yellow]数据库连接[/yellow](环境变量)?", default=True):
|
||||||
|
db_type = Prompt.ask(
|
||||||
|
"NYAHOME_DB_TYPE - 数据库协议", default="sqlite", choices=db_type_allowlist, console=console
|
||||||
|
)
|
||||||
|
al = db_driver_available.get(db_type, [])
|
||||||
|
db_driver = Prompt.ask("NYAHOME_DB_DRIVER - 数据库驱动库", default=al[0], choices=al, console=console)
|
||||||
|
set_key(ENV_PATH, "NYAHOME_DB_TYPE", db_type)
|
||||||
|
set_key(ENV_PATH, "NYAHOME_DB_DRIVER", db_driver)
|
||||||
|
console.print("已设置数据库类型和驱动程序。")
|
||||||
|
if db_type == "sqlite":
|
||||||
|
console.print("采用 [cyan]sqlite[/cyan] 数据库,无需再额外配置。")
|
||||||
|
else:
|
||||||
|
console.print("接下来,需要继续设置数据库的连接凭证。")
|
||||||
|
db_name = Prompt.ask("NYAHOME_DB_NAME - 数据库名称", default="nyahome", console=console)
|
||||||
|
db_user = Prompt.ask("NYAHOME_DB_USER - 数据库用户", default="nyahome", console=console)
|
||||||
|
db_password = Prompt.ask("NYAHOME_DB_PASSWORD - 密码", default="nyahome", console=console)
|
||||||
|
db_host = Prompt.ask("NYAHOME_DB_HOST - 主机名", default="localhost", console=console)
|
||||||
|
db_port = Prompt.ask("NYAHOME_DB_PORT - 端口", default="3006", console=console)
|
||||||
|
if db_password == "nyahome":
|
||||||
|
console.print("[yellow]使用了默认数据库密码。如果是生产环境,建议更换。[/yellow]")
|
||||||
|
set_key(ENV_PATH, "NYAHOME_DB_NAME", db_name)
|
||||||
|
set_key(ENV_PATH, "NYAHOME_DB_USER", db_user)
|
||||||
|
set_key(ENV_PATH, "NYAHOME_DB_PASSWORD", db_password)
|
||||||
|
set_key(ENV_PATH, "NYAHOME_DB_HOST", db_host)
|
||||||
|
set_key(ENV_PATH, "NYAHOME_DB_PORT", db_port)
|
||||||
|
console.print("已设置数据库连接凭证。")
|
||||||
|
if Confirm.ask("\n设置 [yellow]uvicorn[/yellow] 启动配置?", default=True):
|
||||||
|
un_host = Prompt.ask("NYAHOME_UVICORN_HOST - 绑定主机名", default="0.0.0.0", console=console)
|
||||||
|
un_port = IntPrompt.ask("NYAHOME_UVICORN_PORT - 绑定端口", default=9000, console=console)
|
||||||
|
un_reload = Confirm.ask("NYAHOME_UVICORN_RELOAD - 自动重载", default=False, console=console)
|
||||||
|
if un_reload:
|
||||||
|
console.print("[yellow]启用了 uvicorn reload。如果是生产环境,建议关闭。[/yellow]")
|
||||||
|
set_key(ENV_PATH, "NYAHOME_UVICORN_HOST", un_host)
|
||||||
|
set_key(ENV_PATH, "NYAHOME_UVICORN_PORT", str(un_port))
|
||||||
|
set_key(ENV_PATH, "NYAHOME_UVICORN_RELOAD", "true" if un_reload else "false")
|
||||||
|
console.print("已设置 uvicorn 启动配置。")
|
||||||
|
|
||||||
|
# 4.NyaHome 设置初始化
|
||||||
|
console.print("\n4. NyaHome 设置初始化")
|
||||||
|
try:
|
||||||
|
config_manager.sync_load_config()
|
||||||
|
except FileNotFoundError:
|
||||||
|
console.print("配置文件 [cyan].nyahome/config.json[/cyan] 不存在,创建默认配置。")
|
||||||
|
config_manager.sync_save_config()
|
||||||
|
else:
|
||||||
|
console.print("配置文件已存在,跳过。")
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def check() -> None:
|
||||||
|
"""
|
||||||
|
详细自检查环境变量与设置,得到检查报告,可能有用。
|
||||||
|
"""
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
from nyahome.cli.cli import DATA_DIR, ENV_PATH
|
||||||
|
from nyahome.cli.cli_check import (
|
||||||
|
check_database_connector,
|
||||||
|
check_database_type,
|
||||||
|
check_nyahome_status,
|
||||||
|
check_uvicorn,
|
||||||
|
cw,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _(step: int, description: str) -> str:
|
||||||
|
return f"\n[cyan]> Step {step}[/cyan]: {description}"
|
||||||
|
|
||||||
|
console.print(_(1, "检查可用的数据库驱动程序"))
|
||||||
|
table1 = Table(title="数据库驱动库")
|
||||||
|
table1.add_column("驱动库", style="cyan")
|
||||||
|
table1.add_column("状态与描述")
|
||||||
|
database_connectors = check_database_connector()
|
||||||
|
for key, value in database_connectors.items():
|
||||||
|
table1.add_row(key, value)
|
||||||
|
console.print(table1)
|
||||||
|
|
||||||
|
console.print(_(2, "检查环境变量"))
|
||||||
|
load_dotenv(ENV_PATH)
|
||||||
|
check_database_type(os.environ)
|
||||||
|
check_uvicorn(os.environ)
|
||||||
|
|
||||||
|
console.print(_(3, "检查 NyaHome 安装模式与运行环境"))
|
||||||
|
check_nyahome_status()
|
||||||
|
|
||||||
|
console.print(_(4, "检查 NyaHome 数据目录可用性"))
|
||||||
|
if not DATA_DIR.is_dir():
|
||||||
|
cw.warning("NyaHome 数据目录 .nyahome 不存在。")
|
||||||
|
else:
|
||||||
|
if not (DATA_DIR / "logging.yaml").is_file():
|
||||||
|
cw.warning(".nyahome/logging.yaml 日志配置文件不存在。")
|
||||||
|
if not (DATA_DIR / "contents").is_dir():
|
||||||
|
cw.warning(".nyahome/contents 上传目录不存在。")
|
||||||
|
cw.info("可以运行 [cyan]nyahome init[/cyan] 命令来重新初始化数据目录。")
|
||||||
|
|
||||||
|
console.print(f"\n[yellow]完成自检,共有 {cw.counter} 个警告。[/yellow]")
|
||||||
|
|
||||||
|
|
||||||
|
app.add_typer(config_app, name="config", no_args_is_help=True, help="设置 NyaHome 的设置。(需要初始化)")
|
||||||
|
app.add_typer(env_app, name="env", no_args_is_help=True, help="设置 NyaHome 应用的环境变量。(需要初始化)")
|
||||||
|
app.add_typer(aii_app, name="aii", no_args_is_help=True, help="添加、设置、修改 AI 提供商和模型。(需要初始化)")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -49,8 +49,17 @@ class VerifyEmail(BaseModel):
|
|||||||
verify_code: str
|
verify_code: str
|
||||||
|
|
||||||
|
|
||||||
@admin_router.post("/login/name/")
|
@admin_router.post("/login/name/", name="用户登录")
|
||||||
async def nyahome_login_name(user: UserLogin, session: Annotated[Session, Depends(get_session)]) -> ReturnDto:
|
async def nyahome_login_name(user: UserLogin, session: Annotated[Session, Depends(get_session)]) -> ReturnDto:
|
||||||
|
"""
|
||||||
|
使用用户名密码登录。
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: 404 表示尝试登录的用户不存在。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ReturnDto,其中 result 字段包含 `user_id` 和 `access_token` 两个字段。
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
u: ModelUser = session.exec(select(ModelUser).where(ModelUser.name == user.username)).one()
|
u: ModelUser = session.exec(select(ModelUser).where(ModelUser.name == user.username)).one()
|
||||||
except NoResultFound:
|
except NoResultFound:
|
||||||
@@ -75,15 +84,28 @@ async def nyahome_login_name(user: UserLogin, session: Annotated[Session, Depend
|
|||||||
raise HTTPException(status_code=401, detail="验证失败,请检查用户名和密码是否正确")
|
raise HTTPException(status_code=401, detail="验证失败,请检查用户名和密码是否正确")
|
||||||
|
|
||||||
|
|
||||||
@admin_router.get("/me/")
|
@admin_router.get("/me/", name="获取登录用户信息")
|
||||||
async def nyahome_get_me(user: Annotated[ModelUser, Depends(verify_token)]) -> ModelUser:
|
async def nyahome_get_me(user: Annotated[ModelUser, Depends(verify_token)]) -> ModelUser:
|
||||||
|
"""
|
||||||
|
获取当前登录的用户的详细信息。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ModelUser
|
||||||
|
"""
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
@admin_router.post("/me/")
|
@admin_router.post("/me/", name="修改登录用户信息")
|
||||||
async def nyahome_post_me(
|
async def nyahome_post_me(
|
||||||
info: UserInfo, user: Annotated[ModelUser, Depends(verify_token)], session: Annotated[Session, Depends(get_session)]
|
info: UserInfo, user: Annotated[ModelUser, Depends(verify_token)], session: Annotated[Session, Depends(get_session)]
|
||||||
) -> ModelUser:
|
) -> ModelUser:
|
||||||
|
"""
|
||||||
|
修改当前登录的用户的详细信息。
|
||||||
|
此端点可以修改除了用户密码、邮箱、手机号之外的大部分用户信息。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ModelUser
|
||||||
|
"""
|
||||||
user.name = info.name
|
user.name = info.name
|
||||||
user.display_name = info.display_name
|
user.display_name = info.display_name
|
||||||
user.avatar_url = info.avatar_url
|
user.avatar_url = info.avatar_url
|
||||||
@@ -95,12 +117,21 @@ async def nyahome_post_me(
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
@admin_router.post("/me/password/")
|
@admin_router.post("/me/password/", name="修改用户密码")
|
||||||
async def nyahome_change_password(
|
async def nyahome_change_password(
|
||||||
change: ChangePassword,
|
change: ChangePassword,
|
||||||
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:
|
) -> ReturnDto:
|
||||||
|
"""
|
||||||
|
修改用户密码。
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: 400 提供的旧密码错误。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
不重要的 ReturnDto,无异常本身即表示修改成功。
|
||||||
|
"""
|
||||||
if verify_password(change.old_password, user.password):
|
if verify_password(change.old_password, user.password):
|
||||||
user.password = save_password(change.new_password)
|
user.password = save_password(change.new_password)
|
||||||
change_ = SecureChange(
|
change_ = SecureChange(
|
||||||
@@ -116,12 +147,20 @@ async def nyahome_change_password(
|
|||||||
raise HTTPException(status_code=400, detail="修改密码需要提供旧的密码,但提供的旧密码错误。") from None
|
raise HTTPException(status_code=400, detail="修改密码需要提供旧的密码,但提供的旧密码错误。") from None
|
||||||
|
|
||||||
|
|
||||||
@admin_router.post("/me/email-verify/")
|
@admin_router.post("/me/email-verify/", name="验证并修改用户邮箱")
|
||||||
async def nyahome_verify_email(
|
async def nyahome_verify_email(
|
||||||
to: VerifyEmail,
|
to: VerifyEmail,
|
||||||
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:
|
) -> ReturnDto:
|
||||||
|
"""
|
||||||
|
验证用户提供的邮箱以及验证码。
|
||||||
|
需要先通过 `/me/email-verify/send/` 发送验证码。验证码有五分钟有效期,为六位数字,需以字符串形式提供。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ReturnDto,其中 success 字段表明是否成功。如果成功,则用户邮箱已被修改。
|
||||||
|
不返回完整的 ModelUser,WebUI 自行负责前端用户信息更新。
|
||||||
|
"""
|
||||||
success = await s_verify_email(user_id=user.id, address=to.to, verify_code=to.verify_code)
|
success = await s_verify_email(user_id=user.id, address=to.to, verify_code=to.verify_code)
|
||||||
if success:
|
if success:
|
||||||
old_email = user.email
|
old_email = user.email
|
||||||
@@ -141,23 +180,37 @@ async def nyahome_verify_email(
|
|||||||
return ReturnDto(success=success)
|
return ReturnDto(success=success)
|
||||||
|
|
||||||
|
|
||||||
@admin_router.post("/me/email-verify/send/")
|
@admin_router.post("/me/email-verify/send/", name="发送修改邮箱验证码")
|
||||||
async def nyahome_verify_email_send(to: SendEmail, user: Annotated[ModelUser, Depends(verify_token)]) -> ReturnDto:
|
async def nyahome_verify_email_send(to: SendEmail, user: Annotated[ModelUser, Depends(verify_token)]) -> ReturnDto:
|
||||||
|
"""
|
||||||
|
请求对新的邮箱发送验证码。验证码有五分钟有效期,为六位数字。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ReturnDto,其中 success 字段表明是否成功。
|
||||||
|
"""
|
||||||
success = await s_send_verify_email(user.id, to.to)
|
success = await s_send_verify_email(user.id, to.to)
|
||||||
return ReturnDto(success=success)
|
return ReturnDto(success=success)
|
||||||
|
|
||||||
|
|
||||||
@admin_router.get("/me/secure_changes/")
|
@admin_router.get("/me/secure_changes/", name="获取用户安全变更记录")
|
||||||
async def nyahome_get_secure_changes(
|
async def nyahome_get_secure_changes(
|
||||||
user: Annotated[ModelUser, Depends(verify_token)],
|
user: Annotated[ModelUser, Depends(verify_token)],
|
||||||
) -> list[SecureChange]:
|
) -> list[SecureChange]:
|
||||||
|
"""
|
||||||
|
获取用户的安全变更记录。
|
||||||
|
安全变更记录包括:登录、修改密码、修改邮箱、修改手机号。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
SecureChange 列表。
|
||||||
|
"""
|
||||||
return json.loads(user.secure_changes) # type: ignore[no-any-return]
|
return json.loads(user.secure_changes) # type: ignore[no-any-return]
|
||||||
|
|
||||||
|
|
||||||
@admin_router.get("/site_config/")
|
@admin_router.get("/site_config/", name="获取 NyaHome 设置")
|
||||||
async def get_site_config(user: Annotated[ModelUser, Depends(verify_token)]) -> dict[str, Any]:
|
async def get_site_config(user: Annotated[ModelUser, Depends(verify_token)]) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
获取 NyaHome 的设置。
|
获取 NyaHome 的设置。
|
||||||
|
需要管理员权限才能访问。
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPException: 403 表示请求用户非管理员。
|
HTTPException: 403 表示请求用户非管理员。
|
||||||
@@ -170,13 +223,14 @@ async def get_site_config(user: Annotated[ModelUser, Depends(verify_token)]) ->
|
|||||||
return config_manager.get_config()
|
return config_manager.get_config()
|
||||||
|
|
||||||
|
|
||||||
@admin_router.post("/site_config/")
|
@admin_router.post("/site_config/", name="修改 NyaHome 设置")
|
||||||
async def set_site_config(
|
async def set_site_config(
|
||||||
user: Annotated[ModelUser, Depends(verify_token)],
|
user: Annotated[ModelUser, Depends(verify_token)],
|
||||||
config_: dict[str, Any],
|
config_: dict[str, Any],
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
设置 NyaHome 的设置。
|
设置 NyaHome 的设置。
|
||||||
|
需要管理员权限才能访问。
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPException: 403 表示请求用户非管理员。
|
HTTPException: 403 表示请求用户非管理员。
|
||||||
@@ -191,7 +245,7 @@ async def set_site_config(
|
|||||||
return final_config
|
return final_config
|
||||||
|
|
||||||
|
|
||||||
@admin_router.post("/email-test/")
|
@admin_router.post("/email-test/", name="测试邮件发送")
|
||||||
async def nyahome_test_email(to: SendEmail, user: Annotated[ModelUser, Depends(verify_token)]) -> ReturnDto:
|
async def nyahome_test_email(to: SendEmail, user: Annotated[ModelUser, Depends(verify_token)]) -> ReturnDto:
|
||||||
"""
|
"""
|
||||||
NyaHome 管理员面板中的测试邮件端点。
|
NyaHome 管理员面板中的测试邮件端点。
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -22,18 +21,36 @@ from .response_model import ReturnDto
|
|||||||
aii_router = APIRouter(tags=["Aii"], prefix="/aii")
|
aii_router = APIRouter(tags=["Aii"], prefix="/aii")
|
||||||
|
|
||||||
|
|
||||||
@aii_router.get("/model/")
|
@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]:
|
||||||
final_model_list = apply_get_models(session)
|
"""
|
||||||
return ReturnDto(result=final_model_list)
|
获取 AI 模型列表。
|
||||||
|
此接口无需用户登录即可访问。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AiiModel 列表
|
||||||
|
"""
|
||||||
|
return apply_get_models(session)
|
||||||
|
|
||||||
|
|
||||||
@aii_router.post("/model/")
|
@aii_router.post("/model/", name="添加模型")
|
||||||
async def add_model(
|
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 模型。需要基于已添加的模型提供商。
|
||||||
|
此接口需要管理员访问。
|
||||||
|
不会进行可用性检查,因此 WebUI 在前端实现了检查按钮。此端点不会负责检查。
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: 401 用户无权限管理模型(未登录或非管理员)
|
||||||
|
HTTPException: 404 模型提供商不存在
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
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
|
||||||
|
|
||||||
@@ -50,34 +67,147 @@ 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.get("/provider/")
|
@aii_router.post("/model/{id_}", name="修改模型")
|
||||||
async def get_all_provider(session: Annotated[Session, Depends(get_session)]) -> ReturnDto:
|
async def edit_model(
|
||||||
aii_providers = session.exec(select(AiiProvider)).all()
|
id_: int,
|
||||||
return ReturnDto(result=[z_aii_provider(ap) for ap in aii_providers])
|
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.post("/provider/")
|
@aii_router.get("/provider/", name="获取提供商列表")
|
||||||
|
async def get_all_provider(session: Annotated[Session, Depends(get_session)]) -> Sequence[AiiProvider]:
|
||||||
|
"""
|
||||||
|
获取 AI 模型提供商列表。
|
||||||
|
此接口无需用户登录即可访问。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
被 ReturnDto 包裹的 AiiProvider 列表
|
||||||
|
"""
|
||||||
|
return session.exec(select(AiiProvider)).all()
|
||||||
|
|
||||||
|
|
||||||
|
@aii_router.post("/provider/", name="添加提供商")
|
||||||
async def add_provider(
|
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 模型提供商。
|
||||||
|
此接口需要管理员才能访问。
|
||||||
|
不会进行可用性检查,因此 WebUI 在前端实现了检查按钮。此端点不会负责检查。
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: 401 表示用户未登录或非管理员。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
被 ReturnDto 包裹的、添加的 AiiProvider
|
||||||
|
"""
|
||||||
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
|
||||||
ap = AiiProvider(name=provider.name, base_url=provider.base_url, api_key=provider.api_key)
|
ap = AiiProvider(name=provider.name, base_url=provider.base_url, api_key=provider.api_key)
|
||||||
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.get("/provider/{id_}/remote/models/")
|
@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="获取提供商远端模型")
|
||||||
async def get_provider_remote_models(
|
async def get_provider_remote_models(
|
||||||
id_: int, user: Annotated[ModelUser, Depends(verify_token)], session: Annotated[Session, Depends(get_session)]
|
id_: int, user: Annotated[ModelUser, Depends(verify_token)], session: Annotated[Session, Depends(get_session)]
|
||||||
) -> ReturnDto:
|
) -> ReturnDto:
|
||||||
|
"""
|
||||||
|
查看指定模型提供商提供的远端模型列表。并非添加到 NyaHome 的模型列表。
|
||||||
|
此接口需要管理员才能访问。
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: 401 表示用户未登录或非管理员。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
被 ReturnDto 包裹的、模型名称字符串列表
|
||||||
|
"""
|
||||||
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
|
||||||
try:
|
try:
|
||||||
@@ -89,16 +219,12 @@ async def get_provider_remote_models(
|
|||||||
return ReturnDto(result=[m["id"] for m in models])
|
return ReturnDto(result=[m["id"] for m in models])
|
||||||
|
|
||||||
|
|
||||||
@aii_router.get("/provider/{id_}/remote/model/{model_name}/")
|
@aii_router.get("/provider/{id_}/remote/model/{model_name}/", name="检查指定远端模型可用性")
|
||||||
async def check_remote_provider_model(
|
async def check_remote_provider_model(
|
||||||
id_: int, model_name: str, session: Annotated[Session, Depends(get_session)]
|
id_: int, model_name: str, session: Annotated[Session, Depends(get_session)]
|
||||||
) -> ReturnDto:
|
) -> ReturnDto:
|
||||||
"""
|
"""
|
||||||
检测指定提供商的指定名称模型是否可用。
|
检测指定提供商的指定名称远端模型是否可用。
|
||||||
Args:
|
|
||||||
id_: 模型提供商 ID。
|
|
||||||
model_name: 模型名称。
|
|
||||||
session: 数据库连接对象。
|
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPException: 404 表明提供商 ID 未找到。
|
HTTPException: 404 表明提供商 ID 未找到。
|
||||||
@@ -113,8 +239,14 @@ async def check_remote_provider_model(
|
|||||||
return ReturnDto(result=await s_check_remote_model(model_name, ap.base_url, ap.api_key))
|
return ReturnDto(result=await s_check_remote_model(model_name, ap.base_url, ap.api_key))
|
||||||
|
|
||||||
|
|
||||||
@aii_router.post("/remote/provider/check/")
|
@aii_router.post("/remote/provider/check/", name="检查指定提供商可用性")
|
||||||
async def check_remote_provider(provider: AiiProviderPublic) -> ReturnDto:
|
async def check_remote_provider(provider: AiiProviderPublic) -> ReturnDto:
|
||||||
|
"""
|
||||||
|
检查指定提供商是否可用。会返回提供商提供的模型数量作为测试。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ReturnDto,其中 success 字段为布尔值,表明可用状态;如果为真,result 字段是整型模型数量。
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
count = len(await s_list_remote_provider_models(provider.base_url, provider.api_key))
|
count = len(await s_list_remote_provider_models(provider.base_url, provider.api_key))
|
||||||
return ReturnDto(result=count)
|
return ReturnDto(result=count)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ from .response_model import ReturnDto
|
|||||||
chatroom_router = APIRouter(tags=["Chatroom"], prefix="/chatroom")
|
chatroom_router = APIRouter(tags=["Chatroom"], prefix="/chatroom")
|
||||||
|
|
||||||
|
|
||||||
@chatroom_router.get("/{id_}/")
|
@chatroom_router.get("/{id_}/", name="获取指定聊天室")
|
||||||
async def get_chatroom(
|
async def get_chatroom(
|
||||||
id_: int, user: Annotated[ModelUser, Depends(verify_token)], session: Annotated[Session, Depends(get_session)]
|
id_: int, user: Annotated[ModelUser, Depends(verify_token)], session: Annotated[Session, Depends(get_session)]
|
||||||
) -> ReturnDto:
|
) -> ReturnDto:
|
||||||
@@ -54,7 +54,7 @@ async def get_chatroom(
|
|||||||
return ReturnDto(result=cr.model_dump())
|
return ReturnDto(result=cr.model_dump())
|
||||||
|
|
||||||
|
|
||||||
@chatroom_router.get("/")
|
@chatroom_router.get("/", name="获取聊天室列表")
|
||||||
async def get_all_chatroom(
|
async def get_all_chatroom(
|
||||||
user: Annotated[ModelUser, Depends(verify_token)], session: Annotated[Session, Depends(get_session)]
|
user: Annotated[ModelUser, Depends(verify_token)], session: Annotated[Session, Depends(get_session)]
|
||||||
) -> ReturnDto:
|
) -> ReturnDto:
|
||||||
@@ -68,7 +68,7 @@ async def get_all_chatroom(
|
|||||||
return ReturnDto(result=[cr.model_dump(exclude={"content", "script"}) for cr in crs])
|
return ReturnDto(result=[cr.model_dump(exclude={"content", "script"}) for cr in crs])
|
||||||
|
|
||||||
|
|
||||||
@chatroom_router.post("/")
|
@chatroom_router.post("/", name="创建聊天室")
|
||||||
async def create_chatroom(
|
async def create_chatroom(
|
||||||
chatroom: ChatroomPublic,
|
chatroom: ChatroomPublic,
|
||||||
user: Annotated[ModelUser, Depends(verify_token)],
|
user: Annotated[ModelUser, Depends(verify_token)],
|
||||||
@@ -96,7 +96,7 @@ async def create_chatroom(
|
|||||||
return ReturnDto(result=cr.model_dump())
|
return ReturnDto(result=cr.model_dump())
|
||||||
|
|
||||||
|
|
||||||
@chatroom_router.post("/{id_}/")
|
@chatroom_router.post("/{id_}/", name="修改指定聊天室")
|
||||||
async def edit_chatroom(
|
async def edit_chatroom(
|
||||||
id_: int,
|
id_: int,
|
||||||
chatroom: ChatroomPublic,
|
chatroom: ChatroomPublic,
|
||||||
@@ -131,13 +131,14 @@ async def edit_chatroom(
|
|||||||
cr.feature_image = chatroom.feature_image
|
cr.feature_image = chatroom.feature_image
|
||||||
cr.script_template_id = chatroom.script_template_id
|
cr.script_template_id = chatroom.script_template_id
|
||||||
cr.script_template_version = chatroom.script_template_version
|
cr.script_template_version = chatroom.script_template_version
|
||||||
|
cr.default_model_id = chatroom.default_model_id
|
||||||
session.add(cr)
|
session.add(cr)
|
||||||
session.commit()
|
session.commit()
|
||||||
session.refresh(cr)
|
session.refresh(cr)
|
||||||
return ReturnDto(result=cr.model_dump())
|
return ReturnDto(result=cr.model_dump())
|
||||||
|
|
||||||
|
|
||||||
@chatroom_router.post("/{id_}/script/")
|
@chatroom_router.post("/{id_}/script/", name="修改聊天室脚本")
|
||||||
async def update_chatroom_script(
|
async def update_chatroom_script(
|
||||||
id_: int,
|
id_: int,
|
||||||
script: ChatScript,
|
script: ChatScript,
|
||||||
@@ -172,7 +173,7 @@ async def update_chatroom_script(
|
|||||||
return ReturnDto(result=script.model_dump())
|
return ReturnDto(result=script.model_dump())
|
||||||
|
|
||||||
|
|
||||||
@chatroom_router.post("/{id_}/chat/")
|
@chatroom_router.post("/{id_}/chat/", name="聊天室发起模型创作")
|
||||||
async def post_chatroom_chat(
|
async def post_chatroom_chat(
|
||||||
id_: int,
|
id_: int,
|
||||||
chat: ChatroomChat,
|
chat: ChatroomChat,
|
||||||
@@ -181,6 +182,7 @@ async def post_chatroom_chat(
|
|||||||
) -> StreamingResponse:
|
) -> StreamingResponse:
|
||||||
"""
|
"""
|
||||||
在聊天室中发送新的用户消息,流式返回 AI 调用结果。
|
在聊天室中发送新的用户消息,流式返回 AI 调用结果。
|
||||||
|
即:调用模型发起创作。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
id_: (路径参数)聊天室 ID
|
id_: (路径参数)聊天室 ID
|
||||||
@@ -203,7 +205,7 @@ async def post_chatroom_chat(
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
@chatroom_router.post("/{id_}/chat/accept/")
|
@chatroom_router.post("/{id_}/chat/accept/", name="聊天室保存模型创作")
|
||||||
async def accept_chatroom_chat(
|
async def accept_chatroom_chat(
|
||||||
id_: int,
|
id_: int,
|
||||||
accept: ChatroomChatAccept,
|
accept: ChatroomChatAccept,
|
||||||
@@ -212,6 +214,7 @@ async def accept_chatroom_chat(
|
|||||||
) -> ReturnDto:
|
) -> ReturnDto:
|
||||||
"""
|
"""
|
||||||
此端点不负责调用 AI 生成输出,而是用于保存一对用户消息和 AI 输出到聊天室 content 的最后。
|
此端点不负责调用 AI 生成输出,而是用于保存一对用户消息和 AI 输出到聊天室 content 的最后。
|
||||||
|
需要提供用户消息、AI 消息和创作模式。
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPException: 404 表明未找到聊天室。
|
HTTPException: 404 表明未找到聊天室。
|
||||||
@@ -232,7 +235,7 @@ async def accept_chatroom_chat(
|
|||||||
return ReturnDto(result=cr.model_dump())
|
return ReturnDto(result=cr.model_dump())
|
||||||
|
|
||||||
|
|
||||||
@chatroom_router.post("/{id_}/chat/edit/")
|
@chatroom_router.post("/{id_}/chat/edit/", name="聊天室编辑消息")
|
||||||
async def edit_chatroom_chat(
|
async def edit_chatroom_chat(
|
||||||
id_: int,
|
id_: int,
|
||||||
edit: ChatroomChatEdit,
|
edit: ChatroomChatEdit,
|
||||||
@@ -241,6 +244,7 @@ async def edit_chatroom_chat(
|
|||||||
) -> ReturnDto:
|
) -> ReturnDto:
|
||||||
"""
|
"""
|
||||||
此端点不负责调用 AI 生成输出,而是用于修改一条已经保存在聊天记录中的消息。
|
此端点不负责调用 AI 生成输出,而是用于修改一条已经保存在聊天记录中的消息。
|
||||||
|
需要提供消息类型(用户/AI)、旧消息和新消息,以便进行替换。
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPException: 404 表明未找到聊天室,400 表明聊天记录匹配失败,未更新。
|
HTTPException: 404 表明未找到聊天室,400 表明聊天记录匹配失败,未更新。
|
||||||
@@ -264,7 +268,7 @@ async def edit_chatroom_chat(
|
|||||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
@chatroom_router.post("/{id_}/chat/delete/")
|
@chatroom_router.post("/{id_}/chat/delete/", name="聊天室删除消息")
|
||||||
async def delete_chatroom_chat(
|
async def delete_chatroom_chat(
|
||||||
id_: int,
|
id_: int,
|
||||||
delete: ChatroomChatDelete,
|
delete: ChatroomChatDelete,
|
||||||
@@ -273,6 +277,7 @@ async def delete_chatroom_chat(
|
|||||||
) -> ReturnDto:
|
) -> ReturnDto:
|
||||||
"""
|
"""
|
||||||
此端点不负责调用 AI 生成输出,而是用于删除一条已经保存在聊天记录中的消息。关联的 user 或 aii 消息会一并删除。
|
此端点不负责调用 AI 生成输出,而是用于删除一条已经保存在聊天记录中的消息。关联的 user 或 aii 消息会一并删除。
|
||||||
|
需要提供消息和消息类型(用户/AI)。用户消息和 AI 消息是一对一成对的,所以总是会删除关联的一对(两条)消息。
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPException: 404 表明未找到聊天室,400 表明聊天记录匹配失败,未更新。
|
HTTPException: 404 表明未找到聊天室,400 表明聊天记录匹配失败,未更新。
|
||||||
|
|||||||
@@ -13,11 +13,17 @@ from .auth import verify_token
|
|||||||
file_router = APIRouter(tags=["File"], prefix="/file")
|
file_router = APIRouter(tags=["File"], prefix="/file")
|
||||||
|
|
||||||
|
|
||||||
@file_router.get("/")
|
@file_router.get("/", name="获取文件列表")
|
||||||
async def get_files(
|
async def get_files(
|
||||||
user: Annotated[ModelUser, Depends(verify_token)],
|
user: Annotated[ModelUser, Depends(verify_token)],
|
||||||
session: Annotated[Session, Depends(get_session)],
|
session: Annotated[Session, Depends(get_session)],
|
||||||
) -> Sequence[ModelUploadFile]:
|
) -> Sequence[ModelUploadFile]:
|
||||||
|
"""
|
||||||
|
获取用户上传的文件列表。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ModelUploadFile 列表。
|
||||||
|
"""
|
||||||
files: Sequence[ModelUploadFile] = session.exec(
|
files: Sequence[ModelUploadFile] = session.exec(
|
||||||
select(ModelUploadFile).where(ModelUploadFile.uploader_id == user.id)
|
select(ModelUploadFile).where(ModelUploadFile.uploader_id == user.id)
|
||||||
).all()
|
).all()
|
||||||
@@ -25,12 +31,29 @@ async def get_files(
|
|||||||
return files
|
return files
|
||||||
|
|
||||||
|
|
||||||
@file_router.post("/upload/")
|
@file_router.post("/upload/", name="上传文件")
|
||||||
async def file_upload(
|
async def file_upload(
|
||||||
file: Annotated[UploadFile, File()],
|
file: Annotated[UploadFile, File()],
|
||||||
user: Annotated[ModelUser, Depends(verify_token)],
|
user: Annotated[ModelUser, Depends(verify_token)],
|
||||||
session: Annotated[Session, Depends(get_session)],
|
session: Annotated[Session, Depends(get_session)],
|
||||||
) -> ModelUploadFile:
|
) -> ModelUploadFile:
|
||||||
|
"""
|
||||||
|
仅允许单文件上传。
|
||||||
|
文件存储在 `.nyahome/contents` 目录下,由 uuid4 重命名,保留原拓展名。
|
||||||
|
允许上传的文件拓展名由 NyaHome 设置 `allow_upload_file_extensions` 约束。
|
||||||
|
对于不允许上传的文件类型,将抛出 400 错误。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file: 文件对象
|
||||||
|
user: 经验证的用户
|
||||||
|
session: 数据库连接对象
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: 400 表示上传的文件类型不允许。文件类型仅由拓展名判断,不检查 MIME。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ModelUploadFile
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
safe_name = s_get_safe_filename(file.filename) # type: ignore[arg-type]
|
safe_name = s_get_safe_filename(file.filename) # type: ignore[arg-type]
|
||||||
dest_path = UPLOAD_DIR / safe_name
|
dest_path = UPLOAD_DIR / safe_name
|
||||||
|
|||||||
+13
-8
@@ -5,25 +5,26 @@ from contextlib import asynccontextmanager
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, AsyncGenerator
|
from typing import Any, AsyncGenerator
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from nyahome.config import config_manager
|
|
||||||
from nyahome.core.otp_store import email_otp_memory_store
|
|
||||||
from nyahome.core.send_email import email_sender_queue
|
|
||||||
from nyahome.core.task import init_admin_user
|
|
||||||
from nyahome.database import create_db
|
|
||||||
from nyahome.router import admin_router, aii_router, chatroom_router, file_router, webui_router
|
from nyahome.router import admin_router, aii_router, chatroom_router, file_router, webui_router
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app_: FastAPI) -> AsyncGenerator[None, Any]:
|
async def lifespan(_: FastAPI) -> AsyncGenerator[None, Any]:
|
||||||
load_dotenv(Path.cwd() / ".nyahome" / ".env")
|
# 在生命周期函数内先加载环境变量,再局部导入 nyahome 核心模块
|
||||||
logger.info("🚀 服务启动中...")
|
logger.info("🚀 服务启动中...")
|
||||||
|
|
||||||
|
from nyahome.config import config_manager
|
||||||
|
from nyahome.core.otp_store import email_otp_memory_store
|
||||||
|
from nyahome.core.send_email import email_sender_queue
|
||||||
|
from nyahome.core.task import init_admin_user
|
||||||
|
from nyahome.database import create_db
|
||||||
|
|
||||||
create_db()
|
create_db()
|
||||||
await asyncio.gather(init_admin_user(), config_manager.async_load_config())
|
await asyncio.gather(init_admin_user(), config_manager.async_load_config())
|
||||||
email_sender_queue.start()
|
email_sender_queue.start()
|
||||||
@@ -49,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=["*"],
|
||||||
@@ -59,7 +61,10 @@ app.add_middleware(
|
|||||||
|
|
||||||
|
|
||||||
def save_openapi_json(save_path: str | Path) -> None:
|
def save_openapi_json(save_path: str | Path) -> None:
|
||||||
|
try:
|
||||||
from docstring_parser import DocstringStyle
|
from docstring_parser import DocstringStyle
|
||||||
|
except ImportError as e:
|
||||||
|
raise RuntimeError("开发依赖 docstring_parser 不存在,请使用 git clone 方式克隆 NyaHome。") from e
|
||||||
|
|
||||||
from nyahome.cli.openapi_docstring import enrich_openapi_from_docstrings
|
from nyahome.cli.openapi_docstring import enrich_openapi_from_docstrings
|
||||||
|
|
||||||
|
|||||||
@@ -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 喵,我不知道喵
|
||||||
@@ -126,15 +128,16 @@ async def s_start_async_streaming_chat(
|
|||||||
td = getattr(chuck.choices[0].delta, "reasoning_content", None)
|
td = getattr(chuck.choices[0].delta, "reasoning_content", None)
|
||||||
cd = chuck.choices[0].delta.content
|
cd = chuck.choices[0].delta.content
|
||||||
if td:
|
if td:
|
||||||
logger.debug(f"reasoning 流式输出:{cd}")
|
# logger.debug(f"reasoning 流式输出:{cd}")
|
||||||
aii_thinking += td
|
aii_thinking += td
|
||||||
yield f"data: {json.dumps({'text': td, 'type': 'thinking'}, ensure_ascii=False)}\n\n"
|
yield f"data: {json.dumps({'text': td, 'type': 'thinking'}, ensure_ascii=False)}\n\n"
|
||||||
if cd:
|
if cd:
|
||||||
logger.debug(f"content 流式输出:{cd}")
|
# logger.debug(f"content 流式输出:{cd}")
|
||||||
aii_message += cd
|
aii_message += cd
|
||||||
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"
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiosmtplib"
|
name = "aiosmtplib"
|
||||||
version = "5.1.0"
|
version = "5.1.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e7/ad/240a7ce4e50713b111dff8b781a898d8d4770e5d6ad4899103f84c86005c/aiosmtplib-5.1.0.tar.gz", hash = "sha256:2504a23b2b63c9de6bc4ea719559a38996dba68f73f6af4eb97be20ee4c5e6c4", size = 66176, upload-time = "2026-01-25T01:51:11.408Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/39/ba/34f2fef90d13e21ae3f1b360da98d825c40832bb232613513be92457ff65/aiosmtplib-5.1.1.tar.gz", hash = "sha256:d9a35e9d170bc1a9f66e2fdfe7fd212f7eebb8c1581c621f79395d0bcaba7a68", size = 68123, upload-time = "2026-05-31T17:25:36.298Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/37/82/70f2c452acd7ed18c558c8ace9a8cf4fdcc70eae9a41749b5bdc53eb6f45/aiosmtplib-5.1.0-py3-none-any.whl", hash = "sha256:368029440645b486b69db7029208a7a78c6691b90d24a5332ddba35d9109d55b", size = 27778, upload-time = "2026-01-25T01:51:10.026Z" },
|
{ url = "https://files.pythonhosted.org/packages/56/97/d1030d897e96c79cf0682ff93c11a2118085b3af4c27993675eda9e55da3/aiosmtplib-5.1.1-py3-none-any.whl", hash = "sha256:9d384f0c3d8906f745c1cf6819f073145bb2de8b10407905f5e2ee3389bfe6c7", size = 27937, upload-time = "2026-05-31T17:25:35.283Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -378,7 +378,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi"
|
name = "fastapi"
|
||||||
version = "0.136.1"
|
version = "0.136.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "annotated-doc" },
|
{ name = "annotated-doc" },
|
||||||
@@ -387,9 +387,9 @@ dependencies = [
|
|||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
{ name = "typing-inspection" },
|
{ name = "typing-inspection" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/5d/45/c130091c2dfa061bbfe3150f2a5091ef1adf149f2a8d2ae769ecaf6e99a2/fastapi-0.136.1.tar.gz", hash = "sha256:7af665ad7acfa0a3baf8983d393b6b471b9da10ede59c60045f49fbc89a0fa7f", size = 397448, upload-time = "2026-04-23T16:49:44.046Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/81/2d/ff8d91d7b564d464629a0fd50a4489c97fcb836ac230bf3a7269232a9b1f/fastapi-0.136.3.tar.gz", hash = "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab", size = 396410, upload-time = "2026-05-23T18:53:15.192Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" },
|
{ url = "https://files.pythonhosted.org/packages/e0/82/45359b62a067409bd929ae8a56b8ed13e5a8c8a61194b3c236920999ab83/fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", size = 117481, upload-time = "2026-05-23T18:53:16.924Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -736,7 +736,6 @@ dependencies = [
|
|||||||
{ name = "jinja2" },
|
{ name = "jinja2" },
|
||||||
{ name = "openai" },
|
{ name = "openai" },
|
||||||
{ name = "passlib", extra = ["bcrypt"] },
|
{ name = "passlib", extra = ["bcrypt"] },
|
||||||
{ name = "psycopg", extra = ["binary"] },
|
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
{ name = "python-jose", extra = ["cryptography"] },
|
{ name = "python-jose", extra = ["cryptography"] },
|
||||||
@@ -749,10 +748,23 @@ dependencies = [
|
|||||||
{ name = "uvicorn" },
|
{ name = "uvicorn" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
all = [
|
||||||
|
{ name = "psycopg", extra = ["binary"] },
|
||||||
|
{ name = "pymysql" },
|
||||||
|
]
|
||||||
|
mysql = [
|
||||||
|
{ name = "pymysql" },
|
||||||
|
]
|
||||||
|
postgresql = [
|
||||||
|
{ name = "psycopg", extra = ["binary"] },
|
||||||
|
]
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "docstring-parser" },
|
{ name = "docstring-parser" },
|
||||||
{ name = "mypy" },
|
{ name = "mypy" },
|
||||||
|
{ name = "nyahome", extra = ["all"] },
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
{ name = "taskipy" },
|
{ name = "taskipy" },
|
||||||
{ name = "types-aiofiles" },
|
{ name = "types-aiofiles" },
|
||||||
@@ -763,32 +775,37 @@ dev = [
|
|||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "aiofiles", specifier = ">=25.1.0" },
|
{ name = "aiofiles", specifier = ">=25.1.0" },
|
||||||
{ name = "aiosmtplib", specifier = ">=5.1.0" },
|
{ name = "aiosmtplib", specifier = ">=5.1.1" },
|
||||||
{ name = "alembic", specifier = ">=1.18.4" },
|
{ name = "alembic", specifier = ">=1.18.4" },
|
||||||
{ name = "argon2-cffi", specifier = ">=25.1.0" },
|
{ name = "argon2-cffi", specifier = ">=25.1.0" },
|
||||||
{ name = "fastapi", specifier = ">=0.136.1" },
|
{ name = "fastapi", specifier = ">=0.136.3" },
|
||||||
{ name = "httpx", specifier = ">=0.28.1" },
|
{ name = "httpx", specifier = ">=0.28.1" },
|
||||||
{ name = "jinja2", specifier = ">=3.1.6" },
|
{ name = "jinja2", specifier = ">=3.1.6" },
|
||||||
{ name = "openai", specifier = ">=2.38.0" },
|
{ name = "nyahome", extras = ["mysql"], marker = "extra == 'all'" },
|
||||||
|
{ name = "nyahome", extras = ["postgresql"], marker = "extra == 'all'" },
|
||||||
|
{ name = "openai", specifier = ">=2.40.0" },
|
||||||
{ name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" },
|
{ name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" },
|
||||||
{ name = "psycopg", extras = ["binary"], specifier = ">=3.3.4" },
|
{ name = "psycopg", extras = ["binary"], marker = "extra == 'postgresql'", specifier = ">=3.3.4" },
|
||||||
{ name = "pydantic", specifier = ">=2.13.4" },
|
{ name = "pydantic", specifier = ">=2.13.4" },
|
||||||
|
{ name = "pymysql", marker = "extra == 'mysql'", specifier = ">=1.2.0" },
|
||||||
{ name = "python-dotenv", specifier = ">=1.2.2" },
|
{ name = "python-dotenv", specifier = ">=1.2.2" },
|
||||||
{ name = "python-jose", extras = ["cryptography"], specifier = ">=3.5.0" },
|
{ name = "python-jose", extras = ["cryptography"], specifier = ">=3.5.0" },
|
||||||
{ name = "python-multipart", specifier = ">=0.0.29" },
|
{ name = "python-multipart", specifier = ">=0.0.30" },
|
||||||
{ name = "pyyaml", specifier = ">=6.0.3" },
|
{ name = "pyyaml", specifier = ">=6.0.3" },
|
||||||
{ name = "rich", specifier = ">=15.0.0" },
|
{ name = "rich", specifier = ">=15.0.0" },
|
||||||
{ name = "sqlalchemy", specifier = ">=2.0.49" },
|
{ name = "sqlalchemy", specifier = ">=2.0.50" },
|
||||||
{ name = "sqlmodel", specifier = ">=0.0.38" },
|
{ name = "sqlmodel", specifier = ">=0.0.38" },
|
||||||
{ name = "typer", specifier = ">=0.25.1" },
|
{ name = "typer", specifier = ">=0.26.5" },
|
||||||
{ name = "uvicorn", specifier = ">=0.47.0" },
|
{ name = "uvicorn", specifier = ">=0.48.0" },
|
||||||
]
|
]
|
||||||
|
provides-extras = ["mysql", "postgresql", "all"]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "docstring-parser", specifier = ">=0.18.0" },
|
{ name = "docstring-parser", specifier = ">=0.18.0" },
|
||||||
{ name = "mypy", specifier = ">=2.1.0" },
|
{ name = "mypy", specifier = ">=2.1.0" },
|
||||||
{ name = "ruff", specifier = ">=0.15.14" },
|
{ name = "nyahome", extras = ["all"] },
|
||||||
|
{ name = "ruff", specifier = ">=0.15.15" },
|
||||||
{ name = "taskipy", specifier = ">=1.14.1" },
|
{ name = "taskipy", specifier = ">=1.14.1" },
|
||||||
{ name = "types-aiofiles", specifier = ">=25.1.0.20260518" },
|
{ name = "types-aiofiles", specifier = ">=25.1.0.20260518" },
|
||||||
{ name = "types-passlib", specifier = ">=1.7.7.20260211" },
|
{ name = "types-passlib", specifier = ">=1.7.7.20260211" },
|
||||||
@@ -797,7 +814,7 @@ dev = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openai"
|
name = "openai"
|
||||||
version = "2.38.0"
|
version = "2.40.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "anyio" },
|
{ name = "anyio" },
|
||||||
@@ -809,9 +826,9 @@ dependencies = [
|
|||||||
{ name = "tqdm" },
|
{ name = "tqdm" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/8f/12/cfa322c5f5dd8fa21aab9a7a8e979e7a11123800f86ca8d82eb68a83d213/openai-2.38.0.tar.gz", hash = "sha256:798694c6cf74145541fda94325b6f8f72d8e1fd0262cc137c8d728177a6a4ce3", size = 772764, upload-time = "2026-05-21T21:23:42.105Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/9f/136562ec6c3b1a50fe06eb0bb34ed21f0d7426ec0140e5cc43ac785b69a5/openai-2.40.0.tar.gz", hash = "sha256:9a756f91f274a24ad6026cbcb2042fd356c8d4a10e8f347b08d34465e585f7a2", size = 781177, upload-time = "2026-06-01T21:48:23.878Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/0a/bf/ccff9be562e24207716d04ef9dc931c76aff0c89a7265da43e2104d7fe06/openai-2.38.0-py3-none-any.whl", hash = "sha256:ec6661c57b2dcc47414a767e6e3335c7ed3d19c9696999283a3c82e95c756a3c", size = 1344910, upload-time = "2026-05-21T21:23:39.636Z" },
|
{ url = "https://files.pythonhosted.org/packages/f6/46/180e14be801a75bc13f234cb1b594b232adeb9c84e60a9ab1832e8333591/openai-2.40.0-py3-none-any.whl", hash = "sha256:2b205637ff214477f9ce9ab035e9f494db0e3fa8f1e599008953735fbf6ff1ff", size = 1350935, upload-time = "2026-06-01T21:48:21.462Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -996,6 +1013,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pymysql"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c9/bc/1c6a92f385940f727daeecf3bacaf186e03875dff57197801046c583bcf0/pymysql-1.2.0.tar.gz", hash = "sha256:6c7b17ca686988104d7426c27895b455cdeea3e9d3ceb1270f0c3704fead8c33", size = 49021, upload-time = "2026-05-19T08:26:22.302Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/bd/2534e130295c8cfd4f0a2e31623baab7502278f1e97bcfe61db75656a77f/pymysql-1.2.0-py3-none-any.whl", hash = "sha256:62169ce6d5510f08e140c5e7990ee884a9764024e4a9a27b2cc11f1099322ae0", size = 45716, upload-time = "2026-05-19T08:26:20.974Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
@@ -1026,11 +1052,11 @@ cryptography = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-multipart"
|
name = "python-multipart"
|
||||||
version = "0.0.29"
|
version = "0.0.30"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/4e/fe/70bd71a6738b09a0bdf6480ca6436b167469ca4578b2a0efbe390b4b0e70/python_multipart-0.0.29.tar.gz", hash = "sha256:643e93849196645e2dbdd81a0f8829a23123ad7f797a84a364c6fb3563f18904", size = 45678, upload-time = "2026-05-17T17:29:47.654Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/4b/82/c8cd43a6e0719bf5a3b034f6726dd701f75829c08944c83d4b95d02ed0e8/python_multipart-0.0.30.tar.gz", hash = "sha256:0edfe0475c1f46ddd3ff7785a626f6118af32bdcf359bb21260367313bb32118", size = 46316, upload-time = "2026-05-31T19:24:55.198Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/8f/cb/769cfc37177252872a45a71f3fbdde9d51b471a3f3c14bfe95dde3407386/python_multipart-0.0.29-py3-none-any.whl", hash = "sha256:2ddcc971cef266225f54f552d8fa10bcfbb1f14446caec199060daac59ff2d69", size = 29640, upload-time = "2026-05-17T17:29:45.69Z" },
|
{ url = "https://files.pythonhosted.org/packages/1c/fd/0318007beb234790993d3ec5afd051d1dbceb733e81e3afe2b981ece3f37/python_multipart-0.0.30-py3-none-any.whl", hash = "sha256:830964def8c90607ac5daa00514e3987815865713ade8d20febc9177ac0c3c5b", size = 29730, upload-time = "2026-05-31T19:24:53.814Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1096,27 +1122,27 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.15.14"
|
version = "0.15.15"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/8a/8bce2894573e9dae6ff4d77fe34ad727d79b9e6238ad288c5638990d90f6/ruff-0.15.14.tar.gz", hash = "sha256:48e866b165be4a9bdbf310f7d3c9a07edef2fe8cd63ffeb4e00bb590506ebf9f", size = 4700910, upload-time = "2026-05-21T14:34:55.177Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/b9/c8/74a92c6ff9fcfb4f1f947126d3ebee8389276e161ecc85de5bda7cda51bd/ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108", size = 10739177, upload-time = "2026-05-21T14:34:37.332Z" },
|
{ url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/45/91/254a35c20acc38a7223c9d2d594af12e794432464f2cdeb52af1dc4a892d/ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b", size = 11144969, upload-time = "2026-05-21T14:34:43.978Z" },
|
{ url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/56/9e/d13e40f83b8d0a94430e6778ce1d94a43b38cf2efe63278bdd2b4c65abbf/ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f", size = 10478207, upload-time = "2026-05-21T14:34:48.378Z" },
|
{ url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8d/f1/b15a7839fa4f332f8acec78e20564f26bb2d866e3d21710b877fd0263000/ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d", size = 10818459, upload-time = "2026-05-21T14:34:22.318Z" },
|
{ url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/45/33/53d651177f84f94b400a0e27f8824eeada3dddc9d5ee8aeb048f4352a520/ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4", size = 10541800, upload-time = "2026-05-21T14:34:20.209Z" },
|
{ url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/a6/868f87e0bf9786ed24b5d0d0ad8676b8a94fd1912f42cddf9cfc7857818a/ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542", size = 11342149, upload-time = "2026-05-21T14:34:46.365Z" },
|
{ url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a7/8b/38cd5c19faffdcc05a408d2b78edccc69492ab9720eadb49ea15ef80d768/ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f", size = 12212563, upload-time = "2026-05-21T14:34:28.579Z" },
|
{ url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/4d/a3c5b874a556d5731e3e657aaf04311bb76f0a5c3ec220ed43051be6b64b/ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf", size = 11493299, upload-time = "2026-05-21T14:34:41.836Z" },
|
{ url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/c0/56472c251d09858a53e51efbd485b09e1995d8731668b76d52e5dd6ee0f1/ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba", size = 11455931, upload-time = "2026-05-21T14:34:57.276Z" },
|
{ url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/4a/e2e7b4d8dbf233d4eace59c75bc3435fa6d8bd3bae82d351d4e4300c0fd1/ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f", size = 11400794, upload-time = "2026-05-21T14:34:39.773Z" },
|
{ url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/c7/83c0539fe34c3e09136204d1e75d6052492364e0b3cb05e9465423f567d7/ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581", size = 10804759, upload-time = "2026-05-21T14:34:31.045Z" },
|
{ url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/a6/18f2bfc095a2ab4a78745644e428205532ce6653a5d0fa8501572891534d/ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93", size = 10539517, upload-time = "2026-05-21T14:34:53.064Z" },
|
{ url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/3a/5a8b3b69c654d4e4bf1d246ac5b49cbcdac6eaab6905925f8915f31e3b80/ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61", size = 11065169, upload-time = "2026-05-21T14:34:24.484Z" },
|
{ url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ed/c5/8864e4e7925b836ea354b31d57641ec03830564e281a8b6f061f8c3e0ec1/ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553", size = 11560214, upload-time = "2026-05-21T14:34:50.975Z" },
|
{ url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/38/012bf76752e1f89ed50b77b99532d90f3a3e287bc7918e1fc0948ac866ac/ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6", size = 10805548, upload-time = "2026-05-21T14:34:33.453Z" },
|
{ url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/b7/4ea2c170f10ad760fff2a5250beb18897719dc8b52b53a24cddbb9dd3f19/ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902", size = 11939523, upload-time = "2026-05-21T14:34:18.077Z" },
|
{ url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" },
|
{ url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1148,41 +1174,36 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlalchemy"
|
name = "sqlalchemy"
|
||||||
version = "2.0.49"
|
version = "2.0.50"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
|
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/57/da/6fbf010c8ebb347679d0d100b22fe9ba5e13fd04046c5df7280d2f0bf706/sqlalchemy-2.0.50.tar.gz", hash = "sha256:af5607d11ef90fd6a5c0549fe0045dce1663d427426bcfb506dcb5346a85a3b9", size = 9907424, upload-time = "2026-05-24T19:20:04.018Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547, upload-time = "2026-04-03T16:53:08.64Z" },
|
{ url = "https://files.pythonhosted.org/packages/0b/c4/c42356b527296e9862f67990efce31ef78b4cf69cd3f80873a528a060320/sqlalchemy-2.0.50-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:06a9210bdc5f4298cff0781087e2ff45683922252dacc452846373a58761f093", size = 2156697, upload-time = "2026-05-24T19:27:54.764Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782, upload-time = "2026-04-03T17:07:43.508Z" },
|
{ url = "https://files.pythonhosted.org/packages/60/a1/b1a70e3c4365ac7fe9e347f3710f19b562c866fb96d45e3c891588789a7b/sqlalchemy-2.0.50-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b53784972ade4f8174b9aa661f31a06f8a936d2cfdd602913ff3c6dd40ae873", size = 3284260, upload-time = "2026-05-24T20:09:34.195Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156, upload-time = "2026-04-03T17:12:27.697Z" },
|
{ url = "https://files.pythonhosted.org/packages/3f/4a/f3ac3caa19f263d57b0a47f8c91bbf56583dc2d3fc63acfbf644abb24fe0/sqlalchemy-2.0.50-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31648fa14460537e768a7303b078e4344d208e0d23e06867c1f376a227ed82db", size = 3302280, upload-time = "2026-05-24T20:17:17.825Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832, upload-time = "2026-04-03T17:07:45.38Z" },
|
{ url = "https://files.pythonhosted.org/packages/66/55/ccada3e3d62254587819749a0bc69f41173eb48a6e385d10e66d32a9c88e/sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03f4323c980ad0e918cc9e5369b015f759f4e534db5bbaf4dc36832c10d05064", size = 3231580, upload-time = "2026-05-24T20:09:36.406Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000, upload-time = "2026-04-03T17:12:29.657Z" },
|
{ url = "https://files.pythonhosted.org/packages/05/f6/6809349130a2de0e109e7f00fd7d431da9565b9b2868b32ee684754f672b/sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2b9dcc43afef8ac157cd92fce96985d6b8b0cfbd3df4d666f66b4d55a75d202f", size = 3269375, upload-time = "2026-05-24T20:17:20.34Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641, upload-time = "2026-04-03T17:05:43.989Z" },
|
{ url = "https://files.pythonhosted.org/packages/48/84/278a811ef4e07be9c89dc5cdd7be833268509a66a68c4897cf585e67428f/sqlalchemy-2.0.50-cp313-cp313-win32.whl", hash = "sha256:60922d6599065ddca2c6f376b9aa2f41a6b85a271725e0909490bbc50b1998a5", size = 2117229, upload-time = "2026-05-24T19:50:08.215Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498, upload-time = "2026-04-03T17:05:45.7Z" },
|
{ url = "https://files.pythonhosted.org/packages/f6/1c/067cc6187ed32d2ec222fe6d2643acc1659a6d0659f8a7cbc5ad3ae83280/sqlalchemy-2.0.50-cp313-cp313-win_amd64.whl", hash = "sha256:287086e67275a212c4582d166a6fb03a65ccc5551d80866270ce0dd9f34eccd3", size = 2143126, upload-time = "2026-05-24T19:50:09.691Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807, upload-time = "2026-04-03T16:58:31.666Z" },
|
{ url = "https://files.pythonhosted.org/packages/df/32/10ac51b4be7cdecd7e93d069251c86dfbf70b7adbd7c67b48ccea6c49e1c/sqlalchemy-2.0.50-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c966932507a4d7d0a37314927dbfcd89720e3f37d2a1e3352e7ae7939fa8e8a0", size = 2158519, upload-time = "2026-05-24T19:27:56.472Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481, upload-time = "2026-04-03T17:06:00.155Z" },
|
{ url = "https://files.pythonhosted.org/packages/5a/76/e703d2f7681d7d66c4c891af3f07c7ccf4c76ad7f18351de035b5eda007a/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:faffef4bcc20a1892e65e155293d99d60855bbbc79250ab712819cfd56a8e6bb", size = 3282063, upload-time = "2026-05-24T20:09:38.57Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565, upload-time = "2026-04-03T16:58:33.414Z" },
|
{ url = "https://files.pythonhosted.org/packages/31/26/ef168b184a25701f9995e8fb7e503fafd7a99c1c77cda1bc1a26ea2ed486/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c206aec519a2e7bd08abbfb33436e325fd22c632d9c21a9047e376ce241646e", size = 3287069, upload-time = "2026-05-24T20:17:21.942Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769, upload-time = "2026-04-03T17:06:02.787Z" },
|
{ url = "https://files.pythonhosted.org/packages/c2/15/765acc2bc693bccc43ca4a95d5b69750da8aaf6db1b5c616536e087f8920/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bef4ac756363227ef6402a75fee025a4bc690f92328e825868939b3b3a446a6d", size = 3230453, upload-time = "2026-05-24T20:09:40.398Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319, upload-time = "2026-04-03T17:02:04.328Z" },
|
{ url = "https://files.pythonhosted.org/packages/63/61/08e03c3adbf5db0087a0b6816746fec8f3032fb2f7fc899a9bb9b2a48ce4/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96fbee6b19c19cd1556c8bf9419447cf2ec149ffcab7ab64348c23e54ef8547f", size = 3252413, upload-time = "2026-05-24T20:17:24.067Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104, upload-time = "2026-04-03T17:02:05.989Z" },
|
{ url = "https://files.pythonhosted.org/packages/03/0c/370a1f2db38436c615e10134c8a37de3688e74084792380695f3f5083860/sqlalchemy-2.0.50-cp314-cp314-win32.whl", hash = "sha256:8f00e3eb43ba30eb1b238ee03a8a62309486d1321eda3328bb611e0340033ad8", size = 2120063, upload-time = "2026-05-24T19:50:11.08Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e", size = 2156356, upload-time = "2026-04-03T16:53:09.914Z" },
|
{ url = "https://files.pythonhosted.org/packages/7f/a0/fe92bb9817863bc13ba093bda931979a26cc2ca69f8e8f26d07add3d7c6f/sqlalchemy-2.0.50-cp314-cp314-win_amd64.whl", hash = "sha256:15708c613cd5005b7dffe1f66ee6a63ee8f5e46799f71c70ebad74178c676a39", size = 2145830, upload-time = "2026-05-24T19:50:12.452Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/a7/5f476227576cb8644650eff68cc35fa837d3802b997465c96b8340ced1e2/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a", size = 3276486, upload-time = "2026-04-03T17:07:46.9Z" },
|
{ url = "https://files.pythonhosted.org/packages/cc/ff/e5640a98a0b2f491eb8fde10fb6c773621a2e44340de231fafcc9370f4a9/sqlalchemy-2.0.50-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3699dac4be410e97049a1658e9480da9cde956594aa0f3aebc60b88f21c5ba70", size = 2178435, upload-time = "2026-05-24T19:42:58.889Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066", size = 3281479, upload-time = "2026-04-03T17:12:32.226Z" },
|
{ url = "https://files.pythonhosted.org/packages/b7/85/337116e186f1236375b5fb70c21cfac98e8e8ab0d3a47be838dc47a59e08/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f96233858e3df43932ac11589e22520da6e8aeb624b03fedfeebb0e8ea213086", size = 3566059, upload-time = "2026-05-24T20:01:20.848Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/68/bb406fa4257099c67bd75f3f2261b129c63204b9155de0d450b37f004698/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187", size = 3226269, upload-time = "2026-04-03T17:07:48.678Z" },
|
{ url = "https://files.pythonhosted.org/packages/96/34/bb0e190e161c3c2c24314a65add57218be14a4a9486886b7f5047c1ff7c8/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4e70c46fad30c3bcc6a4708bc0130a3173e11a5b25f0ea4a9d8911b450f1f52", size = 3535366, upload-time = "2026-05-24T20:03:56.768Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/84/acb56c00cca9f251f437cb49e718e14f7687505749ea9255d7bd8158a6df/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401", size = 3248260, upload-time = "2026-04-03T17:12:34.381Z" },
|
{ url = "https://files.pythonhosted.org/packages/df/5a/a7f759f97e4fd499c5d4e4488c760d5a7fbecf3028b465a04274fcd52384/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1918a3cf564d16d95bca7301005f41ab2ad50b07cd3b9da50d3ed986db148d6a", size = 3474879, upload-time = "2026-05-24T20:01:23.058Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/56/19/6a20ea25606d1efd7bd1862149bb2a22d1451c3f851d23d887969201633f/sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5", size = 2118463, upload-time = "2026-04-03T17:05:47.093Z" },
|
{ url = "https://files.pythonhosted.org/packages/9d/d9/2907ea38eb60687d297bf9c39e5ee58053c87b57fe8a9cae97090cecbf10/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b00098cdbdbd38c7be3d568b0c9c3122b8c0ec62b911b57cd5e6e0254d60a76d", size = 3486117, upload-time = "2026-05-24T20:03:59.052Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5", size = 2144204, upload-time = "2026-04-03T17:05:48.694Z" },
|
{ url = "https://files.pythonhosted.org/packages/f2/e3/5aa06f167559f8c0bdae487e297d23ba548150ab016a3418265d617a4985/sqlalchemy-2.0.50-cp314-cp314t-win32.whl", hash = "sha256:1fbd55a969d7ac44a98e3dec75016074f809fa08f871585ace58dde110d1bf3e", size = 2150823, upload-time = "2026-05-24T20:08:58.644Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/33/95e7216df810c706e0cd3655a778604bbd319ed4f43333127d465a46862d/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977", size = 3565474, upload-time = "2026-04-03T16:58:35.128Z" },
|
{ url = "https://files.pythonhosted.org/packages/65/9b/112fb8f977582d7489d036e409e3723948bcf5320b3ac465f3c481bbe8f9/sqlalchemy-2.0.50-cp314-cp314t-win_amd64.whl", hash = "sha256:c5c3cdb753a9004183e1ccb634b41611654c989e61bc68617ce878e46d6f1e51", size = 2185794, upload-time = "2026-05-24T20:09:00.319Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/a4/ed7b18d8ccf7f954a83af6bb73866f5bc6f5636f44c7731fbb741f72cc4f/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01", size = 3530567, upload-time = "2026-04-03T17:06:04.587Z" },
|
{ url = "https://files.pythonhosted.org/packages/d0/10/f7220e9b784d295d241c86ed99aeb537f92afcd469a64861f2717e9bb077/sqlalchemy-2.0.50-py3-none-any.whl", hash = "sha256:92064363517a3ff8212b5a93b8c62876579d8dfd1ca5b561335f30152d884fa9", size = 1943861, upload-time = "2026-05-24T19:59:01.119Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/73/a3/20faa869c7e21a827c4a2a42b41353a54b0f9f5e96df5087629c306df71e/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61", size = 3474282, upload-time = "2026-04-03T16:58:37.131Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b7/50/276b9a007aa0764304ad467eceb70b04822dc32092492ee5f322d559a4dc/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a", size = 3480406, upload-time = "2026-04-03T17:06:07.176Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/c3/c80fcdb41905a2df650c2a3e0337198b6848876e63d66fe9188ef9003d24/sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158", size = 2149151, upload-time = "2026-04-03T17:02:07.281Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/52/9f1a62feab6ed368aff068524ff414f26a6daebc7361861035ae00b05530/sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7", size = 2184178, upload-time = "2026-04-03T17:02:08.623Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1276,17 +1297,17 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typer"
|
name = "typer"
|
||||||
version = "0.25.1"
|
version = "0.26.5"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "annotated-doc" },
|
{ name = "annotated-doc" },
|
||||||
{ name = "click" },
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
{ name = "rich" },
|
{ name = "rich" },
|
||||||
{ name = "shellingham" },
|
{ name = "shellingham" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/eb/1a/2cf40b65b1d9c254fe5814bb0519f9b8f2ac38059df0810f9b866300c04a/typer-0.26.5.tar.gz", hash = "sha256:9b9b39e35c3afc9e1e51a06f21155246e457c0911279b09b35d8210ca74b935c", size = 201494, upload-time = "2026-06-01T14:42:49.744Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" },
|
{ url = "https://files.pythonhosted.org/packages/ec/d6/baac76fc04a6532883de3d8722c7f921dae94d10965e7ffba9e38e42a251/typer-0.26.5-py3-none-any.whl", hash = "sha256:4bfd901d564e41608920134aa5d4481200f4ba76d98e982d9f9d32dcb7b84da0", size = 122451, upload-time = "2026-06-01T14:42:51.021Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1360,13 +1381,13 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uvicorn"
|
name = "uvicorn"
|
||||||
version = "0.47.0"
|
version = "0.48.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
{ name = "h11" },
|
{ name = "h11" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/b1/8e7077a8641086aea449e1b5752a570f1b5906c64e0a33cd6d93b63a066b/uvicorn-0.47.0.tar.gz", hash = "sha256:7c9a0ea1a9414106bbab7324609c162d8fa0cdcdcb703060987269d77c7bb533", size = 90582, upload-time = "2026-05-14T18:16:54.455Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/e6/bf/f6544ba992ddb9a6077343a576f9844f7f8f06ab819aefd00206e9255f18/uvicorn-0.48.0.tar.gz", hash = "sha256:a5504207195d08c2511bf9125ede5ac4a4b71725d519e758d01dcf0bc2d31c37", size = 91074, upload-time = "2026-05-24T12:08:41.925Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/15/41/ac2dfdbc1f60c7af4f994c7a335cfa7040c01642b605d65f611cecc2a1e4/uvicorn-0.47.0-py3-none-any.whl", hash = "sha256:2c5715bc12d1892d84752049f400cd1c3cb018514967fdfeb97640443a6a9432", size = 71301, upload-time = "2026-05-14T18:16:51.762Z" },
|
{ url = "https://files.pythonhosted.org/packages/01/be/72532be3da7acc5fdfbccdb95215cd04f995a0886532a5b423f929cda4cc/uvicorn-0.48.0-py3-none-any.whl", hash = "sha256:48097851328b87ec36117d3d575234519eb58c2b22d79666e9bbc6c49a761dad", size = 71410, upload-time = "2026-05-24T12:08:40.258Z" },
|
||||||
]
|
]
|
||||||
|
|||||||
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>
|
||||||
|
|||||||
+50
-34
@@ -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) => {
|
|
||||||
if (data.success) {
|
|
||||||
MESSAGE.success(`检测成功,模型 ${addModelForm.value.model_name} 可用。`)
|
|
||||||
} else {
|
} else {
|
||||||
MESSAGE.warning(`检测完成,模型 ${addModelForm.value.model_name} 不可用。`)
|
MESSAGE.warning(`提供商的模型 ${addModelForm.value.model_name} 不可用。`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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
|
||||||
} else {
|
if (reload) reload()
|
||||||
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>
|
||||||
+16
-22
@@ -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)
|
|
||||||
.then((data) => {
|
|
||||||
if (data.success) {
|
|
||||||
MESSAGE.success(`模型提供商检测成功,探测到 ${data.result} 个可用模型。`)
|
|
||||||
} else {
|
} else {
|
||||||
MESSAGE.warning('模型提供商检测失败,请确认 Base URI 与 API key 是否正确。')
|
MESSAGE.success(`检查模型提供商 ${addProviderForm.value.name} 可用性失败?`)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.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
|
||||||
} else {
|
if (reload) reload()
|
||||||
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