Compare commits

...

27 Commits

Author SHA1 Message Date
MangoFanFanw 23f03822f6 fix(backend): 累积的错误修复和细节修正 2026-06-06 10:12:08 +08:00
MangoFanFanw ad3bafcd35 feat(cli): 实现 nyahome config 命令
允许通过 nyahome 终端命令修改设置
同时再次调整数据库有关的内部常量的位置
2026-06-06 10:04:26 +08:00
MangoFanFanw 82723038c3 fix: sqlalchemy 中使用 pysqlite 表示默认的 sqlite3 标准库驱动 2026-06-06 10:03:31 +08:00
MangoFanFanw 03928c6c59 feat(cli,database): 更新数据库引擎创建模式 2026-06-04 18:51:09 +08:00
MangoFanFanw ee81ccefc5 feat(cli): 部分完成 nyahome init 和 check 命令 2026-06-03 23:02:59 +08:00
MangoFanFanw 2b30f0ffe3 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	src/nyahome/database/model_aii.py
2026-06-01 20:48:22 +08:00
MangoFanFanw c8c474ecfd fix(all): 累积的错误修复和细节修正 2026-06-01 20:46:58 +08:00
MangoFanFanw 567c146fb8 feat(nyahome): 支持模型的思考模式(DS)与编辑模型
增加了控制模型是否支持思考以及是否在调用时启用思考的开关,目前为 DeepSeek 适配。
WebUI 进行了同步的更新。
2026-06-01 20:45:45 +08:00
MangoFanFanw 7df66bbc61 feat(webui): WebUI 管理后台新增 AII 管理栏目
在 WebUI NyaHome 管理后台中实现 AII 管理栏目,用于在线修改模型设置。
同时在后端补全了两个路由端点。
2026-06-01 20:42:16 +08:00
MangoFanFanw 45e255856a fix(backend): 累积的错误修复和细节修正 2026-05-31 15:24:20 +08:00
MangoFanFanw ab703e6176 fix(backend): 累积的错误修复和细节修正 2026-05-31 13:49:31 +08:00
MangoFanFanw 884cea53a1 docs: 为已有的路由编写完整名称和文档 2026-05-30 00:27:23 +08:00
MangoFanFanw 52f6904bef fix: 加载环境变量,避免 FastAPI app 无法组装 2026-05-29 23:25:38 +08:00
MangoFanFanw a7140ea5c1 feat(manage/cli): 增加 nyahome 命令行添加提供商和模型的功能 2026-05-28 22:25:55 +08:00
MangoFanFanw 1f1ac5f87a style(webui): 在 Oxfmt 中配置导入排序功能,并对全 webui 目录进行格式化 2026-05-28 21:03:11 +08:00
MangoFanFanw d62a9d9304 feat: 切换数据库至 PostgreSQL、支持环境变量
未来计划支持多种数据库,从 PostgreSQL 开始!
支持从环境变量读取启动设置,添加 nyahome env 命令用来持久化环境变量。

BREAKING CHANGE: 切换开发阶段的数据库,从 SQLite 到 PostgreSQL。
2026-05-28 16:12:35 +08:00
MangoFanFanw 21cb4ee8c1 fix(alembic/env.py): 修复数据库更新脚本生成错误的问题
该问题是由于代码质量工具自动优化了 env.py 文件头部的未使用导入引起的。
2026-05-27 15:44:33 +08:00
MangoFanFanw 58012e43db style: Oxc 代码格式调整 2026-05-27 14:22:21 +08:00
MangoFanFanw 0796250df8 fix: 修补暂未实现的功能、添加开发中占位
修改手机号、剧本市场
2026-05-27 14:20:59 +08:00
MangoFanFanw 1c7d932e31 feat(NyaHome设置): 增加 NyaHome 设置项
允许上传的文件拓展名、四处默认图片
2026-05-27 14:18:59 +08:00
MangoFanFanw 4deee00bcb docs: 将 openapi.json 移出版本管理
作为文档构建所需的生成文件(而非运行时所需),可以放心交由构建步骤生成。
2026-05-25 23:11:07 +08:00
MangoFanFanw ff2074b400 style: 一些复杂而综合的细节修正 2026-05-25 21:11:29 +08:00
MangoFanFanw 8efb55827c docs: 写一点文档咯 2026-05-25 20:55:09 +08:00
MangoFanFanw dcaa1fddf2 feat: 对未实现的功能增加占位信息
1. 修改手机号
2. 剧本市场
2026-05-25 20:54:24 +08:00
MangoFanFanw e29f27e2eb feat: 从 FastAPI 导出 openapi.json 并渲染为 API 文档
通过 vitepress-openapi 插件,在 VitePress 文档中实现基于 openapi.json 的 API 文档。
这样实现的 API 文档可以从最新版本的代码库中提取路由信息,继而实现自动集成。
---
同时,通过 Kimi 和 Deepseek 实现并审查了一个对 Google 风格 docstring 的解析函数。
该函数可以从 Google 风格的 docstring 中提取参数文档并按 openapi 规范重新整理它们。
---
增加了 nyahome openapi 命令用来导出 openapi.json。
增加了 task openapi-docs 命令用来准备未来的持续集成。
2026-05-25 14:56:36 +08:00
MangoFanFanw 3117af670b style(webui): Oxc format 已有代码 2026-05-24 16:50:44 +08:00
MangoFanFanw ab396b01f2 style(otp.py): 将清理验证码的定期日志降级为 debug,且增加清理计数 2026-05-24 14:59:45 +08:00
98 changed files with 3721 additions and 1158 deletions
+3
View File
@@ -12,6 +12,7 @@ wheels/
# VitePress # VitePress
docs/.vitepress/dist docs/.vitepress/dist
docs/.vitepress/cache docs/.vitepress/cache
docs/.vitepress/theme/openapi.json
node_modules/ node_modules/
@@ -21,3 +22,5 @@ alembic/versions/
.idea .idea
.codemoss .codemoss
alembic.ini
+1 -1
View File
@@ -22,7 +22,7 @@ NyaHome 是由 FastAPI 后端、Vue WebUI 实现的在线 AI 文学创作平台
| 功能 | 阶段 | 优先级 | | 功能 | 阶段 | 优先级 |
|---------------------------|-----|-----| |---------------------------|-----|-----|
| **剧本市场** - 剧本分享、聊天室导出为剧本 | 规划中 | 低 | | **剧本市场** - 剧本分享、聊天室导出为剧本 | 规划中 | 低 |
| **用户功能** - 绑定手机号、接收收集验证码 | 规划中 | 低 | | **用户功能** - 绑定手机号、接收手机验证码 | 规划中 | 低 |
| **用户功能** - 第三方账户 Oauth 登录 | 规划中 | 低 | | **用户功能** - 第三方账户 Oauth 登录 | 规划中 | 低 |
## 代码规范 ## 代码规范
-149
View File
@@ -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
+21
View File
@@ -1,9 +1,30 @@
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
AiiModel,
AiiModelPublic,
AiiProvider,
AiiProviderPublic,
Chatroom,
ChatroomChat,
ChatroomChatAccept,
ChatroomChatDelete,
ChatroomChatEdit,
ChatroomPublic,
ChatScript,
ModelUploadFile,
ModelUser,
ScriptTemplate,
)
# this is the Alembic Config object, which provides # this is the Alembic Config object, which provides
# access to the values within the .ini file in use. # access to the values within the .ini file in use.
+6
View File
@@ -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.
+10
View File
@@ -0,0 +1,10 @@
[*]
insert_final_newline = true
[*.{js,ts,mjs,mts}]
indent_style = space
indent_size = 2
[*.md]
indent_style = space
indent_size = 2
+36 -11
View File
@@ -1,4 +1,12 @@
import {defineConfig} from 'vitepress' import { defineConfig } from 'vitepress'
import { useSidebar } from 'vitepress-openapi'
import spec from './theme/openapi.json' with { type: 'json' }
const sidebar = useSidebar({
spec,
linkPrefix: '/dev/api-docs/'
})
// https://vitepress.dev/reference/site-config // https://vitepress.dev/reference/site-config
export default defineConfig({ export default defineConfig({
@@ -7,29 +15,46 @@ export default defineConfig({
themeConfig: { themeConfig: {
// https://vitepress.dev/reference/default-theme-config // https://vitepress.dev/reference/default-theme-config
nav: [ nav: [
{text: '主页', link: '/'}, { text: '主页', link: '/' },
{text: '使用', link: '/use/start'} { text: '使用', link: '/use/start' },
{ text: '开发', link: '/dev/start' },
], ],
sidebar: [ sidebar: {
{ 'use': [{
text: '使用', text: '使用',
items: [ items: [
{text: '开始', link: '/use/start'}, { text: '开始', link: '/use/start' },
{text: '默认行为', link: '/use/default-action'}, { text: '默认行为', link: '/use/default_action' },
{text: '用户系统', link: '/use/user-system'}, { text: '用户系统', link: '/use/user_system' },
] ]
}, },
],
'dev': [
{ {
text: '开发', text: '开发',
items: [ items: [
{text: '后端响应', link: '/dev/backend-response'}, { text: '开始', link: '/dev/start' },
{ text: '后端响应', link: '/dev/backend_response' },
{ text: '邮件模板', link: '/dev/email_templates' },
] ]
}, },
], {
text: 'API 文档',
items: [
{ text: 'API', link: '/dev/api-docs/api' },
...sidebar.generateSidebarGroups({}),
]
},]
},
socialLinks: [ socialLinks: [
{icon: 'github', link: 'https://github.com/vuejs/vitepress'} { icon: 'github', link: 'https://github.com/vuejs/vitepress' }
] ]
},
sitemap: {
hostname: 'https://docs.nyahome.cn'
} }
}) })
+68
View File
@@ -2,3 +2,71 @@
--vp-c-brand-1: #64ffc4; --vp-c-brand-1: #64ffc4;
--vp-c-brand-2: #9354ff; --vp-c-brand-2: #9354ff;
} }
/* ===== 简约滚动条美化 ===== */
/* 适用于 Webkit 内核(Chrome/Edge/Safari */
::-webkit-scrollbar {
width: 6px;
/* 垂直滚动条宽度 */
height: 6px;
/* 水平滚动条高度 */
}
::-webkit-scrollbar-track {
background: transparent;
/* 轨道透明,极简 */
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
/* 滑块半透明灰 */
border-radius: 3px;
/* 小圆角 */
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.25);
/* 悬停稍微深一点 */
}
/* Firefox 兼容 */
* {
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
}
/* ===== 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 {
background: linear-gradient(45deg, hsla(58, 100%, 92%, 0.6), hsla(128, 100%, 75%, 0.5));
border: 1px solid #64ffc4;
border-radius: 6px;
margin: 48px auto 0;
padding: 6px 20px;
text-align: center;
}
div.vitepress-openapi p {
line-height: 8px;
}
+22 -1
View File
@@ -1,5 +1,26 @@
import DefaultTheme from 'vitepress/theme' import DefaultTheme from 'vitepress/theme'
import { theme, useOpenapi } from 'vitepress-openapi/client'
import 'vitepress-openapi/dist/style.css'
// @ts-ignore // @ts-ignore
import './custom.css' import './custom.css'
import { Theme } from 'vitepress'
export default DefaultTheme import spec from './openapi.json' with { type: 'json' }
export default {
extends: DefaultTheme,
async enhanceApp({ app }) {
useOpenapi({
spec,
config: {
codeSamples: {
defaultLang: 'python',
},
server: {
allowCustomServer: true,
},
}
})
theme.enhanceApp({ app })
}
} satisfies Theme
+29
View File
@@ -0,0 +1,29 @@
---
aside: false
outline: false
title: NyaHome API Docs
---
<script setup lang="ts">
import { useTheme, locals } from 'vitepress-openapi/client'
import { useRoute } from 'vitepress'
const route = useRoute()
useTheme({
i18n: {
locale: 'zh',
},
})
const operationId = route.data.params.operationId
</script>
<OAOperation :operationId="operationId">
<template #branding>
<div class="vitepress-openapi">
<p>API 文档是基于最新代码自动生成的</p>
<p>由 <a href="https://vitepress-openapi.vercel.app/" target="_blank">VitePress OpenAPI</a> 提供文档支持</p>
</div>
</template>
</OAOperation>
+17
View File
@@ -0,0 +1,17 @@
import { usePaths } from 'vitepress-openapi'
import spec from '../../.vitepress/theme/openapi.json' with {type: 'json'}
export default {
paths() {
return usePaths({ spec })
.getPathsByVerbs()
.map(({ operationId, summary }) => {
return {
params: {
operationId,
pageTitle: `${summary} - NyaHome API`,
},
}
})
},
}
+36
View File
@@ -0,0 +1,36 @@
# API
以下是自动生成的 API 文档,包含 NyaHome 的 FastAPI 后端拥有的所有路由端点的有关信息。
API 文档会跟随代码仓库主分支的最新状况进行同步。也就是说,此处的 API 文档永远显示最新版本。
## 查看 NyaHome Swagger UI 或 ReDoc
FastAPI 提供内置的文档功能,位于开发服务器的 `/docs``/redoc` 两处路径。
NyaHome 提供关闭它们的选项(并且默认关闭),但如果你有开发需求,打开它们自然是比较方便的。
由 FastAPI 打开的文档是由当前代码生成的,可以反应开发中的最新更改,或者查看当前版本的路由信息(如果你没有使用最新版本)。
## 导出 openapi.json
你可以从一个部署完成的 NyaHome 实例中导出 openapi.json。
```bash
uv run nyahome openapi [path]
```
以上命令会生成 openapi.json 文件。`path` 参数的默认值是 `openapi.json`,意味着 openapi.json 会被保存在执行位置(仓库根目录)。
你也可以从一个正在运行的 NyaHome 实例访问 `/openapi.json` 来在线查看 openapi.json,此功能也默认关闭。
## 构建文档自动集成
`pyproject.toml` 中配置了一个项目任务,来方便将最新的 openapi.json 导出到 `/docs` 路径下的指定位置,从而与 VitePress 进行集成。
```bash
uv run task openapi-docs
```
这会将 openapi.json 保存到 `/docs/.vitepress/theme/openapi.json`。事实上这是对上面的 `nyahome openapi` 命令的封装。
此命令一般适合在 CI/CD 流水线中运行比较合适,或者在开发阶段需要预览 API 文档的渲染效果。
+1
View File
@@ -0,0 +1 @@
# 后端响应
+19
View File
@@ -13,6 +13,8 @@ HTML。
pnpm mjml mjml/filename.mjml -o public/templates/filename.j2 pnpm mjml mjml/filename.mjml -o public/templates/filename.j2
``` ```
NyaHome 的开发状况稳定之后,可能会再优化邮件模板的编译流程。
Jinja2 在 NyaHome 进程中读取模板,渲染变量,然后由 aiosmptplib 发送渲染好的 HTML 邮件。 Jinja2 在 NyaHome 进程中读取模板,渲染变量,然后由 aiosmptplib 发送渲染好的 HTML 邮件。
## 在 PyCharm 中预览 mjml 源文件 ## 在 PyCharm 中预览 mjml 源文件
@@ -22,4 +24,21 @@ Jinja2 在 NyaHome 进程中读取模板,渲染变量,然后由 aiosmptplib
:::warning :::warning
MJML Support 插件不支持 JetBrains 远程开发。在远程开发(包括使用 Gateway 进行 WSL 开发)中,IDE client 无法对 mjml MJML Support 插件不支持 JetBrains 远程开发。在远程开发(包括使用 Gateway 进行 WSL 开发)中,IDE client 无法对 mjml
源文件进行实时预览。 源文件进行实时预览。
MJML Support 的开发者表示该插件不会支持远程开发。
::: :::
## 渲染变量
Jinja2 负责将需要动态渲染的变量渲染到经过变异的 Jinja2 模板中。如果你碰过 Django 或者 Flask,应该会对这种格式很熟悉。
```html
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Maple Mono CN;font-size:13px;line-height:1;text-align:left;color:#000000;">{{ site_name }} 的管理员选择向此邮箱发送了一封测试邮件。此邮件不含有任何有效内容。</div>
</td>
</tr>
```
本文档不过多设计 Jinja2 的模板变量写法。只需要知道,这些变量会在发送前被填入具体的内容。例如,<code v-pre>{{ site_name }}</code>
会被渲染成 `config_manager.get("site_name", "Nya Home")`
+3
View File
@@ -0,0 +1,3 @@
# 开始
## 从代码仓库克隆项目
+11
View File
@@ -0,0 +1,11 @@
# 开始
## 从 uv 安装
NyaHome 作为一个 Python 模块发布在了 pypi 上,因此可以使用 uv 将 NyaHome 作为工具安装,然后随处运行。
你需要选定一个合适的目录,并在每次都从此处运行 NyaHome。NyaHome 需要在一个指定目录存放数据文件。
```bash
uv tool install nyahome
```
-53
View File
@@ -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
+4 -2
View File
@@ -1,10 +1,12 @@
{ {
"type": "module",
"devDependencies": { "devDependencies": {
"mjml": "^5.2.2", "mjml": "^5.2.2",
"vitepress": "2.0.0-alpha.17" "vitepress": "2.0.0-alpha.17",
"vitepress-openapi": "^0.2.0"
}, },
"scripts": { "scripts": {
"dev": "vitepress dev docs", "dev": "vitepress dev docs --port 6173",
"build": "vitepress build docs", "build": "vitepress build docs",
"preview": "vitepress preview docs" "preview": "vitepress preview docs"
} }
+178
View File
@@ -14,6 +14,9 @@ importers:
vitepress: vitepress:
specifier: 2.0.0-alpha.17 specifier: 2.0.0-alpha.17
version: 2.0.0-alpha.17(postcss@8.5.15) version: 2.0.0-alpha.17(postcss@8.5.15)
vitepress-openapi:
specifier: ^0.2.0
version: 0.2.0(vitepress@2.0.0-alpha.17(postcss@8.5.15))(vue@3.5.34)
packages: packages:
@@ -210,12 +213,30 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@floating-ui/core@1.7.5':
resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
'@floating-ui/dom@1.7.6':
resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==}
'@floating-ui/utils@0.2.11':
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
'@floating-ui/vue@1.1.11':
resolution: {integrity: sha512-HzHKCNVxnGS35r9fCHBc3+uCnjw9IWIlCPL683cGgM9Kgj2BiAl8x1mS7vtvP6F9S/e/q4O6MApwSHj8hNLGfw==}
'@iconify-json/simple-icons@1.2.83': '@iconify-json/simple-icons@1.2.83':
resolution: {integrity: sha512-6Pp9V++XisT9RKH7FB4RLPqUDzcmLtSma0ovOEIoEWGrXtHwBFsH7oN1z8vvCVCb95fb87QgR46/zRLyN9Y3kg==} resolution: {integrity: sha512-6Pp9V++XisT9RKH7FB4RLPqUDzcmLtSma0ovOEIoEWGrXtHwBFsH7oN1z8vvCVCb95fb87QgR46/zRLyN9Y3kg==}
'@iconify/types@2.0.0': '@iconify/types@2.0.0':
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
'@internationalized/date@3.12.1':
resolution: {integrity: sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==}
'@internationalized/number@3.6.6':
resolution: {integrity: sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ==}
'@isaacs/cliui@8.0.2': '@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -395,6 +416,17 @@ packages:
'@shikijs/vscode-textmate@10.0.2': '@shikijs/vscode-textmate@10.0.2':
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
'@swc/helpers@0.5.21':
resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==}
'@tanstack/virtual-core@3.15.0':
resolution: {integrity: sha512-0AwPGx0I8QxPYjAxShT/+z+ZOe9u8mW5rsXvivCTjRfRmz9a43+3mRyi4wwlyoUqOC56q/jatKa0Bh9M99BEHQ==}
'@tanstack/vue-virtual@3.13.25':
resolution: {integrity: sha512-/ez+t68a5O4CgVysvk7Bav0XbSYSYufOVHZveXF+DYO9hvtg2UheYzR0YkniCeUtXmMjDne1dDqwBMkOmEUOow==}
peerDependencies:
vue: ^2.7.0 || ^3.0.0
'@types/estree@1.0.8': '@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
@@ -556,6 +588,10 @@ packages:
argparse@2.0.1: argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
aria-hidden@1.2.6:
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
engines: {node: '>=10'}
balanced-match@1.0.2: balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -616,10 +652,17 @@ packages:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'} engines: {node: '>= 8.10.0'}
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
cliui@8.0.1: cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
color-convert@2.0.1: color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'} engines: {node: '>=7.0.0'}
@@ -719,6 +762,9 @@ packages:
csstype@3.2.3: csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
defu@6.1.7:
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
dequal@2.0.3: dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -986,12 +1032,21 @@ packages:
lru-cache@10.4.3: lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
lucide-vue-next@1.0.0:
resolution: {integrity: sha512-V6SPvx1IHTj/UY+FrIYWV5faISsPSb8BnWSFDxAtezWKvWc9ZZ40PDrdu1/Qb5vg4lHWr1hs1BAMGVGm6V1Xdg==}
deprecated: Package deprecated. Please use @lucide/vue instead.
peerDependencies:
vue: '>=3.0.1'
magic-string@0.30.21: magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
mark.js@8.11.1: mark.js@8.11.1:
resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==}
markdown-it-link-attributes@4.0.1:
resolution: {integrity: sha512-pg5OK0jPLg62H4k7M9mRJLT61gUp9nvG0XveKYHMOOluASo9OEF13WlXrpAp2aj35LbedAy3QOCgQCw0tkLKAQ==}
mdast-util-to-hast@13.2.1: mdast-util-to-hast@13.2.1:
resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==}
@@ -1151,6 +1206,9 @@ packages:
nth-check@2.1.1: nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
ohash@2.0.11:
resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
oniguruma-parser@0.12.2: oniguruma-parser@0.12.2:
resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==}
@@ -1403,6 +1461,11 @@ packages:
regex@6.1.0: regex@6.1.0:
resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==}
reka-ui@2.9.8:
resolution: {integrity: sha512-7dxaBJ6nQ0zOQZXPV45219tTEgZPstmihBLS9ABPhSiPiJ8SiF0sacfZHFaBptS0v9N4tzsevq+8MNBpE4p5JQ==}
peerDependencies:
vue: '>= 3.4.0'
require-directory@2.1.1: require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -1497,6 +1560,9 @@ packages:
trim-lines@3.0.1: trim-lines@3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
undici@6.25.0: undici@6.25.0:
resolution: {integrity: sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==} resolution: {integrity: sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==}
engines: {node: '>=18.17'} engines: {node: '>=18.17'}
@@ -1575,6 +1641,13 @@ packages:
yaml: yaml:
optional: true optional: true
vitepress-openapi@0.2.0:
resolution: {integrity: sha512-suCYD59HG0P+XKauEm2GayqeXiVqBnDfaltmwcqNS3zOUDgI/nDCN75z4zZqLGHLLw/YhryNko8LOykTaUGmtQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0}
peerDependencies:
vitepress: '>=1.0.0'
vue: ^3.0.0
vitepress@2.0.0-alpha.17: vitepress@2.0.0-alpha.17:
resolution: {integrity: sha512-Z3VPUpwk/bHYqt1uMVOOK1/4xFiWQov1GNc2FvMdz6kvje4JRXEOngVI9C+bi5jeedMSHiA4dwKkff1NCvbZ9Q==} resolution: {integrity: sha512-Z3VPUpwk/bHYqt1uMVOOK1/4xFiWQov1GNc2FvMdz6kvje4JRXEOngVI9C+bi5jeedMSHiA4dwKkff1NCvbZ9Q==}
hasBin: true hasBin: true
@@ -1590,6 +1663,17 @@ packages:
postcss: postcss:
optional: true optional: true
vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
engines: {node: '>=12'}
hasBin: true
peerDependencies:
'@vue/composition-api': ^1.0.0-rc.1
vue: ^3.0.0-0 || ^2.6.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
vue@3.5.34: vue@3.5.34:
resolution: {integrity: sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==} resolution: {integrity: sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==}
peerDependencies: peerDependencies:
@@ -1748,12 +1832,40 @@ snapshots:
'@esbuild/win32-x64@0.27.7': '@esbuild/win32-x64@0.27.7':
optional: true optional: true
'@floating-ui/core@1.7.5':
dependencies:
'@floating-ui/utils': 0.2.11
'@floating-ui/dom@1.7.6':
dependencies:
'@floating-ui/core': 1.7.5
'@floating-ui/utils': 0.2.11
'@floating-ui/utils@0.2.11': {}
'@floating-ui/vue@1.1.11(vue@3.5.34)':
dependencies:
'@floating-ui/dom': 1.7.6
'@floating-ui/utils': 0.2.11
vue-demi: 0.14.10(vue@3.5.34)
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@iconify-json/simple-icons@1.2.83': '@iconify-json/simple-icons@1.2.83':
dependencies: dependencies:
'@iconify/types': 2.0.0 '@iconify/types': 2.0.0
'@iconify/types@2.0.0': {} '@iconify/types@2.0.0': {}
'@internationalized/date@3.12.1':
dependencies:
'@swc/helpers': 0.5.21
'@internationalized/number@3.6.6':
dependencies:
'@swc/helpers': 0.5.21
'@isaacs/cliui@8.0.2': '@isaacs/cliui@8.0.2':
dependencies: dependencies:
string-width: 5.1.2 string-width: 5.1.2
@@ -1885,6 +1997,17 @@ snapshots:
'@shikijs/vscode-textmate@10.0.2': {} '@shikijs/vscode-textmate@10.0.2': {}
'@swc/helpers@0.5.21':
dependencies:
tslib: 2.8.1
'@tanstack/virtual-core@3.15.0': {}
'@tanstack/vue-virtual@3.13.25(vue@3.5.34)':
dependencies:
'@tanstack/virtual-core': 3.15.0
vue: 3.5.34
'@types/estree@1.0.8': {} '@types/estree@1.0.8': {}
'@types/hast@3.0.4': '@types/hast@3.0.4':
@@ -2027,6 +2150,10 @@ snapshots:
argparse@2.0.1: {} argparse@2.0.1: {}
aria-hidden@1.2.6:
dependencies:
tslib: 2.8.1
balanced-match@1.0.2: {} balanced-match@1.0.2: {}
baseline-browser-mapping@2.10.31: {} baseline-browser-mapping@2.10.31: {}
@@ -2105,12 +2232,18 @@ snapshots:
optionalDependencies: optionalDependencies:
fsevents: 2.3.3 fsevents: 2.3.3
class-variance-authority@0.7.1:
dependencies:
clsx: 2.1.1
cliui@8.0.1: cliui@8.0.1:
dependencies: dependencies:
string-width: 4.2.3 string-width: 4.2.3
strip-ansi: 6.0.1 strip-ansi: 6.0.1
wrap-ansi: 7.0.0 wrap-ansi: 7.0.0
clsx@2.1.1: {}
color-convert@2.0.1: color-convert@2.0.1:
dependencies: dependencies:
color-name: 1.1.4 color-name: 1.1.4
@@ -2229,6 +2362,8 @@ snapshots:
csstype@3.2.3: {} csstype@3.2.3: {}
defu@6.1.7: {}
dequal@2.0.3: {} dequal@2.0.3: {}
detect-node@2.1.0: {} detect-node@2.1.0: {}
@@ -2502,12 +2637,18 @@ snapshots:
lru-cache@10.4.3: {} lru-cache@10.4.3: {}
lucide-vue-next@1.0.0(vue@3.5.34):
dependencies:
vue: 3.5.34
magic-string@0.30.21: magic-string@0.30.21:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
mark.js@8.11.1: {} mark.js@8.11.1: {}
markdown-it-link-attributes@4.0.1: {}
mdast-util-to-hast@13.2.1: mdast-util-to-hast@13.2.1:
dependencies: dependencies:
'@types/hast': 3.0.4 '@types/hast': 3.0.4
@@ -3026,6 +3167,8 @@ snapshots:
dependencies: dependencies:
boolbase: 1.0.0 boolbase: 1.0.0
ohash@2.0.11: {}
oniguruma-parser@0.12.2: {} oniguruma-parser@0.12.2: {}
oniguruma-to-es@4.3.6: oniguruma-to-es@4.3.6:
@@ -3270,6 +3413,22 @@ snapshots:
dependencies: dependencies:
regex-utilities: 2.3.0 regex-utilities: 2.3.0
reka-ui@2.9.8(vue@3.5.34):
dependencies:
'@floating-ui/dom': 1.7.6
'@floating-ui/vue': 1.1.11(vue@3.5.34)
'@internationalized/date': 3.12.1
'@internationalized/number': 3.6.6
'@tanstack/vue-virtual': 3.13.25(vue@3.5.34)
'@vueuse/core': 14.3.0(vue@3.5.34)
'@vueuse/shared': 14.3.0(vue@3.5.34)
aria-hidden: 1.2.6
defu: 6.1.7
ohash: 2.0.11
vue: 3.5.34
transitivePeerDependencies:
- '@vue/composition-api'
require-directory@2.1.1: {} require-directory@2.1.1: {}
resolve-from@4.0.0: {} resolve-from@4.0.0: {}
@@ -3390,6 +3549,8 @@ snapshots:
trim-lines@3.0.1: {} trim-lines@3.0.1: {}
tslib@2.8.1: {}
undici@6.25.0: {} undici@6.25.0: {}
unist-util-is@6.0.1: unist-util-is@6.0.1:
@@ -3446,6 +3607,19 @@ snapshots:
optionalDependencies: optionalDependencies:
fsevents: 2.3.3 fsevents: 2.3.3
vitepress-openapi@0.2.0(vitepress@2.0.0-alpha.17(postcss@8.5.15))(vue@3.5.34):
dependencies:
'@vueuse/core': 14.3.0(vue@3.5.34)
class-variance-authority: 0.7.1
clsx: 2.1.1
lucide-vue-next: 1.0.0(vue@3.5.34)
markdown-it-link-attributes: 4.0.1
reka-ui: 2.9.8(vue@3.5.34)
vitepress: 2.0.0-alpha.17(postcss@8.5.15)
vue: 3.5.34
transitivePeerDependencies:
- '@vue/composition-api'
vitepress@2.0.0-alpha.17(postcss@8.5.15): vitepress@2.0.0-alpha.17(postcss@8.5.15):
dependencies: dependencies:
'@docsearch/css': 4.6.3 '@docsearch/css': 4.6.3
@@ -3494,6 +3668,10 @@ snapshots:
- universal-cookie - universal-cookie
- yaml - yaml
vue-demi@0.14.10(vue@3.5.34):
dependencies:
vue: 3.5.34
vue@3.5.34: vue@3.5.34:
dependencies: dependencies:
'@vue/compiler-dom': 3.5.34 '@vue/compiler-dom': 3.5.34
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

+27 -8
View File
@@ -6,29 +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",
"pydantic>=2.13.4", "pydantic>=2.13.4",
"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",
"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",
@@ -110,3 +127,5 @@ migrate = "alembic upgrade head"
rollback = "alembic downgrade -1" rollback = "alembic downgrade -1"
# 查看数据库版本 # 查看数据库版本
current = "alembic current" current = "alembic current"
# 为 docs 文档生成最新 openapi.json
openapi-docs = "nyahome openapi docs/.vitepress/theme/openapi.json"
+9
View File
@@ -0,0 +1,9 @@
from pathlib import Path
from rich.console import Console
console = Console()
DATA_DIR = Path.cwd() / ".nyahome"
ENV_PATH = DATA_DIR / ".env"
LOGGING_YAML = DATA_DIR / "logging.yaml"
+113
View File
@@ -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 URLOpenAI 兼容端点)")],
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]")
+169
View File
@@ -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"""
+70
View File
@@ -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] 的值重置为默认值。")
+60
View File
@@ -0,0 +1,60 @@
import os
from typing import Annotated
import typer
from dotenv import load_dotenv, set_key, unset_key
from rich.table import Table
from .cli import ENV_PATH, console
env_app = typer.Typer()
@env_app.command(name="list")
def list_all_envs() -> None:
"""
列出所有以 NYAHOME_ 开头的环境变量。
这些变量可能配置在 .nyahome 内的 .env 文件中,或者通过其他方式预先设置。
"""
if not load_dotenv(ENV_PATH):
console.print(
"[bright_black]未在 .nyahome 目录下读取到任何环境变量。如果这是有意为之,则请无需担心。[/bright_black]"
)
table = Table(title="NyaHome 应用的环境变量")
table.add_column("Key", style="cyan", no_wrap=True)
table.add_column("Value", style="white")
for env in os.environ.items():
if env[0].startswith("NYAHOME_"):
table.add_row(env[0], env[1])
console.print(table)
@env_app.command(name="set")
def set_env(
key: Annotated[str, typer.Argument(help="不包含 NYAHOME_ 的键名")],
value: Annotated[str, typer.Argument(help="环境变量值")],
) -> None:
"""
设置 NYAHOME_ 环境变量。请参考 NyaHome 文档以了解使用方法。
保存在 .nyahome 内的 .env 文件。
"""
key = key.upper()
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")
def unset_env(key: Annotated[str, typer.Argument(help="不包含 NYAHOME_ 的键名")]) -> None:
"""
删除 NYAHOME_ 环境变量。
操作在 .nyahome 内的 .env 文件。
"""
unset_key(ENV_PATH, f"NYAHOME_{key.upper()}")
console.print(f"[cyan]已删除环境变量 NYAHOME_{key}。[/cyan]")
+339
View File
@@ -0,0 +1,339 @@
"""
FastAPI Docstring-to-OpenAPI Enricher
=====================================
自动解析 Google Style docstring,将 Args / Returns / Raises 注入到 OpenAPI Schema 的对应位置,
让 vitepress-openapi 等工具能够正确渲染参数表格和返回值说明。
功能特性
--------
1. 参数描述注入:路径参数、查询参数、Header、Cookie、请求体字段
2. 返回值描述注入:自动写入 2xx 响应的 description 及 schema.description
3. 异常描述注入:以 Markdown 格式附加到 operation description
4. 嵌套 Schema 递归处理:支持 Pydantic v1/v2 的嵌套模型、List、Optional 等
5. \f 截断兼容:与 FastAPI 原生行为保持一致
6. 全局 Schema 补全:为 components/schemas 中缺少描述的字段补充 docstring 说明
7. 零侵入:只需替换 ``app.openapi``,业务代码无需任何修改
依赖安装
--------
pip install docstring-parser
使用方式
--------
from fastapi import FastAPI
from fastapi_docstring_openapi import enrich_openapi_from_docstrings
app = FastAPI()
enrich_openapi_from_docstrings(app)
# 你的路由注册...
完整示例见文件底部 ``if __name__ == "__main__":`` 块。
"""
from __future__ import annotations
import inspect
import logging
from typing import Any, Dict, Optional, Set
from docstring_parser import Docstring, DocstringStyle, ParseError, parse
from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi
logger = logging.getLogger(__name__)
def _parse_docstring(func: Any, style: DocstringStyle = DocstringStyle.AUTO) -> Optional[Docstring]: # noqa ANN401 有意为之
"""
解析函数的 Google Style docstring,失败时返回 None。
Returns:
成功解析时返回 docstring_parser.Docstring。
失败时返回 None。
"""
doc = inspect.getdoc(func)
if not doc:
return None
if "\f" in doc:
doc = doc.split("\f")[0].strip()
try:
return parse(doc, style=style)
except ParseError:
return None
except Exception:
logger.warning(
"Unexpected error parsing docstring for %s",
getattr(func, "__name__", func),
exc_info=True,
)
return None
def _build_param_lookup(parsed_doc: Docstring) -> Dict[str, str]:
"""
从解析后的 docstring 构建 {参数名: 描述} 映射。
Returns:
{参数名: 描述}
"""
lookup: Dict[str, str] = {}
for param in getattr(parsed_doc, "params", []) or []:
name = getattr(param, "arg_name", None)
desc = getattr(param, "description", None) or ""
if name:
# 合并多行描述并清理缩进
lookup[name] = _dedent_description(desc)
return lookup
def _build_returns_description(parsed_doc: Docstring) -> Optional[str]:
"""
从解析后的 docstring 构建返回值描述。
Returns:
可能的字符串为返回值描述。
"""
ret = getattr(parsed_doc, "returns", None)
if not ret:
return None
type_name = getattr(ret, "type_name", None) or ""
desc = getattr(ret, "description", None) or ""
if type_name and desc:
return f"**{type_name}**: {desc}"
return desc or None
def _build_raises_description(parsed_doc: Docstring) -> Optional[str]:
"""
从解析后的 docstring 构建异常描述(Markdown 列表)。
Returns:
可能的字符串为异常描述。
"""
raises = getattr(parsed_doc, "raises", None)
if not raises:
return None
parts: list[str] = []
for exc in raises:
type_name = getattr(exc, "type_name", None) or ""
desc = getattr(exc, "description", None) or ""
if type_name and desc:
parts.append(f"- **{type_name}**: {desc}")
elif type_name:
parts.append(f"- **{type_name}**")
elif desc:
parts.append(f"- {desc}")
return "\n".join(parts) if parts else None
def _dedent_description(text: str) -> str:
"""
清理 docstring 描述中的多余缩进,保留段落结构。
Returns:
docstring 字符串
"""
if not text:
return ""
lines = text.splitlines()
# 找到最小公共缩进(排除空行)
min_indent = min(
(len(line) - len(line.lstrip()) for line in lines if line.strip()),
default=0,
)
cleaned = [line[min_indent:] if line.strip() else "" for line in lines]
return "\n".join(cleaned).strip()
def _get_clean_operation_description(parsed_doc: Docstring) -> str:
"""
生成干净的 operation description。
Returns:
策略:保留 summary + long_description,去掉 Args/Returns/Raises 等机器块。
"""
parts: list[str] = []
short = getattr(parsed_doc, "short_description", None)
long_ = getattr(parsed_doc, "long_description", None)
if short:
parts.append(short)
if long_:
parts.append(long_)
return "\n\n".join(parts).strip()
def _inject_schema_properties(
schema: Dict[str, Any],
param_lookup: Dict[str, str],
visited_refs: Optional[Set[str]] = None,
openapi_schema: Optional[Dict[str, Any]] = None,
) -> None:
"""
递归注入 schema properties 的描述。
支持对象、数组、allOf/anyOf/oneOf、$ref 引用(全局 components/schemas 补全)。
"""
if visited_refs is None:
visited_refs = set()
if not isinstance(schema, dict):
return
# 处理 $ref:在 components/schemas 中查找并补全
ref = schema.get("$ref")
if ref and openapi_schema:
if ref not in visited_refs:
visited_refs.add(ref)
# 提取 schema 名,例如 "#/components/schemas/User" -> "User"
schema_name = ref.split("/")[-1]
components = openapi_schema.get("components", {}).get("schemas", {})
target = components.get(schema_name)
if target:
_inject_schema_properties(target, param_lookup, visited_refs, openapi_schema)
return
# 处理 properties(对象字段)
properties = schema.get("properties")
if isinstance(properties, dict):
for prop_name, prop_schema in properties.items():
if prop_name in param_lookup: # noqa SIM102
# 只有当 docstring 描述非空时才写入
if param_lookup[prop_name]:
prop_schema["description"] = param_lookup[prop_name]
# 递归处理子 schema
_inject_schema_properties(prop_schema, param_lookup, visited_refs, openapi_schema)
# 处理 items(数组类型)
items = schema.get("items")
if items:
_inject_schema_properties(items, param_lookup, visited_refs, openapi_schema)
# 处理 allOf / anyOf / oneOf
for key in ("allOf", "anyOf", "oneOf"):
for sub in schema.get(key, []):
_inject_schema_properties(sub, param_lookup, visited_refs, openapi_schema)
# 处理 additionalProperties
additional = schema.get("additionalProperties")
if isinstance(additional, dict):
_inject_schema_properties(additional, param_lookup, visited_refs, openapi_schema)
def enrich_openapi_from_docstrings(
app: FastAPI,
*,
prefer_docstring_over_field: bool = True,
append_raises_to_description: bool = True,
docstring_style: DocstringStyle = DocstringStyle.AUTO,
) -> None:
"""
为 FastAPI 应用启用 docstring 驱动的 OpenAPI 增强。
参数
----
app : FastAPI
目标应用实例。
prefer_docstring_over_field : bool, 默认 True
当 docstring 中的参数描述与 Pydantic Field(description=...) 冲突时,
是否优先使用 docstring 的描述。
append_raises_to_description : bool, 默认 True
是否将 Raises 段落以 Markdown 格式追加到 operation description。
docstring_style : DocstringStyle, 默认 AUTO
解析风格。团队统一用 Google 时可显式传入 ``DocstringStyle.GOOGLE``。
"""
# 可选择保存原始的 openapi 函数(如果有)
# original_openapi = app.openapi
def custom_openapi() -> Dict[str, Any]:
# 如果已经有缓存,直接返回
if app.openapi_schema:
return app.openapi_schema
# 生成基础 schema
openapi_schema = get_openapi(
title=app.title,
version=app.version,
openapi_version=app.openapi_version,
description=app.description,
routes=app.routes,
)
for route in app.routes:
if not hasattr(route, "endpoint") or not route.endpoint:
continue
path = getattr(route, "path", None)
methods = [m.lower() for m in route.methods] if hasattr(route, "methods") else []
if not path or not methods:
continue
# 解析 docstring
parsed = _parse_docstring(route.endpoint, style=docstring_style)
if not parsed:
continue
param_lookup = _build_param_lookup(parsed)
returns_desc = _build_returns_description(parsed)
raises_desc = _build_raises_description(parsed)
clean_desc = _get_clean_operation_description(parsed)
for method in methods:
if method == "head":
# FastAPI 通常不把 HEAD 单独写入 OpenAPI,或者与 GET 共享
continue
if method not in openapi_schema.get("paths", {}).get(path, {}):
continue
op = openapi_schema["paths"][path][method]
# ---------- 1. 更新 operation description ----------
if clean_desc:
op["description"] = clean_desc
# ---------- 2. 注入参数描述(路径/查询/Header/Cookie----------
for param in op.get("parameters", []):
param_name = param.get("name")
if param_name and param_name in param_lookup:
existing = param.get("description", "")
new_desc = param_lookup[param_name]
if new_desc and (prefer_docstring_over_field or not existing):
param["description"] = new_desc
# ---------- 3. 注入请求体 schema 字段描述 ----------
if "requestBody" in op:
content = op["requestBody"].get("content", {})
for media_obj in content.values():
schema = media_obj.get("schema", {})
if schema:
_inject_schema_properties(schema, param_lookup, openapi_schema=openapi_schema)
# ---------- 4. 注入返回值描述 ----------
if returns_desc:
for code, resp in op.get("responses", {}).items():
if code.startswith("2"): # 2xx 成功响应
# 更新响应级 description
resp["description"] = returns_desc
# 同时注入到响应 schema 的 description(如果存在)
for media_obj in resp.get("content", {}).values():
schema = media_obj.get("schema", {})
if schema and not schema.get("description"):
schema["description"] = returns_desc
# 递归注入 schema 内部字段
_inject_schema_properties(schema, param_lookup, openapi_schema=openapi_schema)
# ---------- 5. 异常描述追加 ----------
if append_raises_to_description and raises_desc:
current_desc = op.get("description", "")
raises_md = f"## 异常\n\n{raises_desc}"
# 避免重复追加
if raises_md not in current_desc:
op["description"] = f"{current_desc}\n\n{raises_md}".strip()
app.openapi_schema = openapi_schema
return app.openapi_schema
app.openapi = custom_openapi # type: ignore[method-assign]
+2
View File
@@ -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,
] ]
+6
View File
@@ -4,7 +4,13 @@ class Config:
self.site_url = "http://localhost:5173" self.site_url = "http://localhost:5173"
self.backend_url = "http://localhost:9000" self.backend_url = "http://localhost:9000"
self.default_user_avatar_url = "/nyahome/normal-avatar.png"
self.default_user_background_url = "/nyahome/normal-background.png"
self.default_chatroom_script_cover_url = "/nyahome/normal-thumbnail.png"
self.default_page_cover_url = "/nyahome/normal-header.png"
self.jwt_secret_key = "see you tomorrow" self.jwt_secret_key = "see you tomorrow"
self.allow_upload_file_extensions = [".jpg", ".jpeg", ".png"]
self.smtp_enable = False self.smtp_enable = False
self.smtp_sender = "" self.smtp_sender = ""
+53 -1
View File
@@ -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_configConfigManager 也将持有一套默认设置。
"""
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):
+5 -3
View File
@@ -42,15 +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.info(f"[{self.type_name}] 开始定时清理过期验证码。") # logger.debug(f"[{self.type_name}] 开始定时清理过期验证码。")
expires = [] expires = []
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
for address in expires: for address in expires:
self._store.pop(address) self._store.pop(address)
logger.info(f"[{self.type_name}] 清理完成。") # 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)
+2 -2
View File
@@ -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),
) )
+6
View File
@@ -0,0 +1,6 @@
db_driver_available = {
"sqlite": ["sqlite3"],
"mysql": ["pymysql"],
"postgresql": ["psycopg"],
}
db_type_allowlist = ["sqlite", "mysql", "postgresql"]
+5 -3
View File
@@ -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,
] ]
+30 -3
View File
@@ -1,7 +1,34 @@
from pathlib import Path import os
from sqlalchemy import Engine
from sqlmodel import create_engine from sqlmodel import create_engine
sqlite_file_path = Path.cwd() / ".nyahome" / "nyahome.db" from nyahome.data import db_driver_available, db_type_allowlist
engine = create_engine(f"sqlite:///{sqlite_file_path!s}", connect_args={"check_same_thread": False})
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()
+30 -20
View File
@@ -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,
}
+18 -9
View File
@@ -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
+176 -10
View File
@@ -3,14 +3,20 @@
避免在此文件中引用 router 和 service 模块内的代码。 避免在此文件中引用 router 和 service 模块内的代码。
""" """
import os
from typing import Annotated
import typer import typer
from rich.console import Console from rich.table import Table
from nyahome import __version__ from nyahome import __version__
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
console = Console()
app = typer.Typer( app = typer.Typer(
name="Nya Home", name="NyaHome",
help="🌸 为你而存在的故事之家 ~", help="🌸 为你而存在的故事之家 ~",
rich_markup_mode="rich", rich_markup_mode="rich",
no_args_is_help=True, no_args_is_help=True,
@@ -19,7 +25,7 @@ app = typer.Typer(
def version_callback(value: bool = False) -> None: def version_callback(value: bool = False) -> None:
if value: if value:
console.print(f"[green]Nya Home[/green] version {__version__}") console.print(f"[green]NyaHome[/green] version {__version__}")
@app.callback(invoke_without_command=True) @app.callback(invoke_without_command=True)
@@ -33,26 +39,186 @@ def main(
is_eager=True, is_eager=True,
), ),
) -> None: ) -> None:
console.print("[bright_black]Nya Home 仍然处于极早期的阶段。如果遇到任何问题,请告诉芒果帆帆喵![/bright_black]") console.print("(!) [bright_black]NyaHome 仍然处于极早期的阶段。如果遇到任何问题,请告诉芒果帆帆喵![/bright_black]")
@app.command() @app.command()
def run() -> None: def run() -> None:
""" """
运行 Nya Home。 运行 NyaHome。
""" """
import os
import uvicorn import uvicorn
from dotenv import load_dotenv
load_dotenv(ENV_PATH)
uvicorn.run( uvicorn.run(
"nyahome.server:app", "nyahome.server:app",
reload=False, reload=os.getenv("NYAHOME_UVICORN_RELOAD", "false") in ["True", "true", "1"],
host="0.0.0.0", host=os.getenv("NYAHOME_UVICORN_HOST", "0.0.0.0"),
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",
) )
@app.command()
def openapi(
path: Annotated[str, typer.Argument(help="导出的 json 格式 openapi.json 应该保存为……")] = "openapi.json",
) -> None:
"""
根据代码导出 NyaHome 的 openapi.json 。
"""
from dotenv import load_dotenv
load_dotenv(ENV_PATH)
from nyahome.server import save_openapi_json
save_openapi_json(path)
console.print(f"[cyan]已经保存 openapi.json 到 {path} 。[/cyan]")
@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__":
app() app()
+65 -11
View File
@@ -19,7 +19,7 @@ from .response_model import ReturnDto
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
admin_router = APIRouter(tags=["admin"], prefix="/admin") admin_router = APIRouter(tags=["Admin"], prefix="/admin")
class UserLogin(BaseModel): class UserLogin(BaseModel):
@@ -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 管理员面板中的测试邮件端点。
+157 -25
View File
@@ -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)
+14 -9
View File
@@ -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 表明聊天记录匹配失败,未更新。
+25 -2
View File
@@ -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
+26 -6
View File
@@ -1,4 +1,5 @@
import asyncio import asyncio
import json
import logging import logging
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pathlib import Path from pathlib import Path
@@ -8,19 +9,22 @@ 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]:
# 在生命周期函数内先加载环境变量,再局部导入 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()
@@ -46,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=["*"],
@@ -53,3 +58,18 @@ app.add_middleware(
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
) )
def save_openapi_json(save_path: str | Path) -> None:
try:
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
# 增强 openapi docstring 参数文档提取
enrich_openapi_from_docstrings(app, docstring_style=DocstringStyle.GOOGLE)
with open(save_path, "w") as f:
json.dump(app.openapi(), f, indent=2)
+6 -3
View File
@@ -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
+6 -3
View File
@@ -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"
Generated
+175 -75
View File
@@ -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]]
@@ -355,6 +355,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
] ]
[[package]]
name = "docstring-parser"
version = "0.18.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" },
]
[[package]] [[package]]
name = "ecdsa" name = "ecdsa"
version = "0.19.2" version = "0.19.2"
@@ -369,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" },
@@ -378,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]]
@@ -728,6 +737,7 @@ dependencies = [
{ name = "openai" }, { name = "openai" },
{ name = "passlib", extra = ["bcrypt"] }, { name = "passlib", extra = ["bcrypt"] },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "python-dotenv" },
{ name = "python-jose", extra = ["cryptography"] }, { name = "python-jose", extra = ["cryptography"] },
{ name = "python-multipart" }, { name = "python-multipart" },
{ name = "pyyaml" }, { name = "pyyaml" },
@@ -738,9 +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 = "mypy" }, { name = "mypy" },
{ name = "nyahome", extra = ["all"] },
{ name = "ruff" }, { name = "ruff" },
{ name = "taskipy" }, { name = "taskipy" },
{ name = "types-aiofiles" }, { name = "types-aiofiles" },
@@ -751,29 +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"], 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-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 = "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" },
@@ -782,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" },
@@ -794,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]]
@@ -837,6 +869,52 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/d7/7831438e6c3ebbfa6e01a927127a6cb42ad3ab844247f3c5b96bea25d73d/psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649", size = 254444, upload-time = "2024-12-19T18:22:11.335Z" }, { url = "https://files.pythonhosted.org/packages/7b/d7/7831438e6c3ebbfa6e01a927127a6cb42ad3ab844247f3c5b96bea25d73d/psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649", size = 254444, upload-time = "2024-12-19T18:22:11.335Z" },
] ]
[[package]]
name = "psycopg"
version = "3.3.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" },
]
[package.optional-dependencies]
binary = [
{ name = "psycopg-binary", marker = "implementation_name != 'pypy'" },
]
[[package]]
name = "psycopg-binary"
version = "3.3.4"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/09/43/13e9c406fbbf354580476e248a16b64802a376873ebe6339e30bb655572d/psycopg_binary-3.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7", size = 4590377, upload-time = "2026-05-01T23:29:18.782Z" },
{ url = "https://files.pythonhosted.org/packages/22/be/2923cd7c3683e7afdecf4f10796a18de02f5c5ddc0969aa2ad0a8cdd3bbd/psycopg_binary-3.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31", size = 4669023, upload-time = "2026-05-01T23:29:25.884Z" },
{ url = "https://files.pythonhosted.org/packages/96/a0/2c913d6fe13d6a8bd13597d36739bf47af063ad9399e402cfecab16f3c1e/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70", size = 5467423, upload-time = "2026-05-01T23:29:33.416Z" },
{ url = "https://files.pythonhosted.org/packages/e7/38/205d10bc1ad0df4a21c5c51659126bd3ea0ef98fcad1e852f78c249bb9c3/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3", size = 5151137, upload-time = "2026-05-01T23:29:42.013Z" },
{ url = "https://files.pythonhosted.org/packages/36/fc/f0381ddcd45eff3bb70dbca6823a996048d7f507b2ec3fc92c6fabc0fe87/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c", size = 6736671, upload-time = "2026-05-01T23:29:51.626Z" },
{ url = "https://files.pythonhosted.org/packages/95/40/fa545ae152c24327651e5624e4902121e808270be36c10b12e9939be09bc/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae", size = 4979601, upload-time = "2026-05-01T23:29:56.961Z" },
{ url = "https://files.pythonhosted.org/packages/86/e4/2f8a47ee97f90cd2b933d0463081d35631ff419de2b8c984a5f369857de0/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc", size = 4510513, upload-time = "2026-05-01T23:30:07.243Z" },
{ url = "https://files.pythonhosted.org/packages/0e/0e/94e842ff4a7f98ed162580ca2e8b8864b28c1e0350f2443f8ee47f821167/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf", size = 4187243, upload-time = "2026-05-01T23:30:15.352Z" },
{ url = "https://files.pythonhosted.org/packages/d0/83/fc6c174b672e29b7de996ea77b6cbddf46c891751c3355f6974292baa6b4/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260", size = 3927347, upload-time = "2026-05-01T23:30:21.186Z" },
{ url = "https://files.pythonhosted.org/packages/e9/65/768364d4a97a15b1a7f47ba52688c1686f22941d8332a8398cefc468e25f/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf", size = 4236393, upload-time = "2026-05-01T23:30:26.211Z" },
{ url = "https://files.pythonhosted.org/packages/bd/3b/218efbc9e645becd80cdf651acda05f85cfe546b7a9c0458c7cbc8fe1f74/psycopg_binary-3.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38", size = 3564592, upload-time = "2026-05-01T23:30:31.764Z" },
{ url = "https://files.pythonhosted.org/packages/48/a6/828c9185701dab71b234c2a76c38a08b098ebfec5020716b4e93807492b5/psycopg_binary-3.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:28b7398fdd19db3232c884fb24550bdfe951221f510e195e233299e4c9b78f97", size = 4607292, upload-time = "2026-05-01T23:30:38.962Z" },
{ url = "https://files.pythonhosted.org/packages/92/58/5b40dbc9d839045c9dae956960e4fb6d20bcabe6c59a2aa34fc3a371913f/psycopg_binary-3.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1fbaa292a3c8bb61b45df1ad3da1908ccee7cb889db9425e3557d9e34e2a4829", size = 4687023, upload-time = "2026-05-01T23:30:47.227Z" },
{ url = "https://files.pythonhosted.org/packages/85/a9/793f0ac107a9003b48441d0d1f9f616d96e0f37458dd8dc12528ceff55fb/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94596f9e7633ee3f6440711d43bb70aa31cc0a46a900ab8b4201a366ace5c9e7", size = 5486985, upload-time = "2026-05-01T23:30:55.517Z" },
{ url = "https://files.pythonhosted.org/packages/8f/26/42e8533497e2592334f68ec529cf5f840f7fa4e99575a4bb61aa184dbfbf/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277", size = 5168745, upload-time = "2026-05-01T23:31:01.904Z" },
{ url = "https://files.pythonhosted.org/packages/15/af/b7151776cc08d5935d45c833ec818a9beb417cf7c08239af1aafbdae78ee/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c09aad7051326e7603c14e50636db9c01f78272dc54b3accff03d46370461e6", size = 6761486, upload-time = "2026-05-01T23:31:14.511Z" },
{ url = "https://files.pythonhosted.org/packages/d0/ed/c92533b9124712d592cbf1cd6c76da933a2e0acea81dfe1fbe7e735f0cff/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:514404ed543efd620c85602b747df2a23cf1241b4067199e1a66f2d2757aaa41", size = 4997427, upload-time = "2026-05-01T23:31:20.901Z" },
{ url = "https://files.pythonhosted.org/packages/a2/23/ccadfd0de416aa188356daa199453af24087b042e296088706d190ae0295/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:46893c26858be12cc49ca4226ed6a60b4bfccadd946b3bebb783a60b38788228", size = 4533549, upload-time = "2026-05-01T23:31:26.204Z" },
{ url = "https://files.pythonhosted.org/packages/fd/a0/c8f43cee36386f7bc891ab41a9d31ea07cf9826038e732da79f26b1e5f34/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:df1d567fc430f6df15c9fcf67d87685fc49bdb325adc0db5af1adfb2f44eb5c9", size = 4210256, upload-time = "2026-05-01T23:31:33.884Z" },
{ url = "https://files.pythonhosted.org/packages/4e/2c/c1547871be3790676e8868b38655496422f94f0978dfb66b74bdba2f1676/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6b9016b1714da4dd5ecaaa75b82098aa5a0b87854ce9b092e21c27c4ae23e014", size = 3946204, upload-time = "2026-05-01T23:31:39.626Z" },
{ url = "https://files.pythonhosted.org/packages/c4/b1/f6670f00fa7ea601584623f6c11602ab92117d83eaff885e0210f6de7418/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:47c656a8a7ba6eb0cff1801a4caaa9c8bdc12d03080e273aff1c8ac39971a77e", size = 4255811, upload-time = "2026-05-01T23:31:44.986Z" },
{ url = "https://files.pythonhosted.org/packages/eb/e6/5fff07a70d1f945ed90ae131c3bd76cab32beff7c58c6db15ad5820b6d1f/psycopg_binary-3.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8", size = 3666849, upload-time = "2026-05-01T23:31:51.165Z" },
]
[[package]] [[package]]
name = "pyasn1" name = "pyasn1"
version = "0.6.3" version = "0.6.3"
@@ -935,6 +1013,24 @@ 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]]
name = "python-dotenv"
version = "1.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
]
[[package]] [[package]]
name = "python-jose" name = "python-jose"
version = "3.5.0" version = "3.5.0"
@@ -956,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]]
@@ -1026,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]]
@@ -1078,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]]
@@ -1206,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]]
@@ -1279,15 +1370,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
] ]
[[package]]
name = "tzdata"
version = "2026.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
]
[[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" },
] ]
+3 -1
View File
@@ -1,5 +1,7 @@
{ {
"$schema": "./node_modules/oxfmt/configuration_schema.json", "$schema": "./node_modules/oxfmt/configuration_schema.json",
"semi": false, "semi": false,
"singleQuote": true "singleQuote": true,
"bracketSpacing": true,
"sortImports": true
} }
+22 -4
View File
@@ -12,19 +12,24 @@ 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']
ChatControlPanel: typeof import('./src/components/chatroom/ChatControlPanel.vue')['default'] ChatControlPanel: typeof import('./src/components/chatroom/ChatControlPanel.vue')['default']
ChatMessage: typeof import('./src/components/chatroom/ChatMessage.vue')['default'] ChatMessage: typeof import('./src/components/chatroom/ChatMessage.vue')['default']
ChatPromptQuicker: typeof import('./src/components/chatroom/ChatPromptQuicker.vue')['default'] ChatPromptQuicker: typeof import('./src/components/chatroom/ChatPromptQuicker.vue')['default']
ChatroomCard: typeof import('./src/components/chatroom/ChatroomCard.vue')['default'] ChatroomCard: typeof import('./src/components/chatroom/ChatroomCard.vue')['default']
ChatroomCreatorModal: typeof import('./src/components/chatroom/ChatroomCreatorModal.vue')['default'] ChatroomCreatorModal: typeof import('./src/components/chatroom/ChatroomCreatorModal.vue')['default']
ChatroomEditorModal: typeof import('./src/components/chatroom/ChatroomEditorModal.vue')['default']
ChatTable: typeof import('./src/components/chatroom/ChatTable.vue')['default'] ChatTable: typeof import('./src/components/chatroom/ChatTable.vue')['default']
ConfigCard: typeof import('./src/components/admin/ConfigCard.vue')['default'] ConfigCard: typeof import('./src/components/admin/ConfigCard.vue')['default']
FileModal: typeof import('./src/components/file/FileModal.vue')['default'] FileModal: typeof import('./src/components/file/FileModal.vue')['default']
FileThumbnail: typeof import('./src/components/file/FileThumbnail.vue')['default'] FileThumbnail: typeof import('./src/components/file/FileThumbnail.vue')['default']
InDev: typeof import('./src/components/InDev.vue')['default'] InDev: typeof import('./src/components/InDev.vue')['default']
InputFile: typeof import('./src/components/file/InputFile.vue')['default']
NAlert: typeof import('naive-ui')['NAlert'] NAlert: typeof import('naive-ui')['NAlert']
NAvatar: typeof import('naive-ui')['NAvatar'] NAvatar: typeof import('naive-ui')['NAvatar']
NButton: typeof import('naive-ui')['NButton'] NButton: typeof import('naive-ui')['NButton']
@@ -35,6 +40,7 @@ declare module 'vue' {
NDataTable: typeof import('naive-ui')['NDataTable'] NDataTable: typeof import('naive-ui')['NDataTable']
NDrawer: typeof import('naive-ui')['NDrawer'] NDrawer: typeof import('naive-ui')['NDrawer']
NDrawerContent: typeof import('naive-ui')['NDrawerContent'] NDrawerContent: typeof import('naive-ui')['NDrawerContent']
NDynamicTags: typeof import('naive-ui')['NDynamicTags']
NEllipsis: typeof import('naive-ui')['NEllipsis'] NEllipsis: typeof import('naive-ui')['NEllipsis']
NEmpty: typeof import('naive-ui')['NEmpty'] NEmpty: typeof import('naive-ui')['NEmpty']
NFlex: typeof import('naive-ui')['NFlex'] NFlex: typeof import('naive-ui')['NFlex']
@@ -45,6 +51,7 @@ declare module 'vue' {
NGridItem: typeof import('naive-ui')['NGridItem'] NGridItem: typeof import('naive-ui')['NGridItem']
NH2: typeof import('naive-ui')['NH2'] NH2: typeof import('naive-ui')['NH2']
NH3: typeof import('naive-ui')['NH3'] NH3: typeof import('naive-ui')['NH3']
NH4: typeof import('naive-ui')['NH4']
NImage: typeof import('naive-ui')['NImage'] NImage: typeof import('naive-ui')['NImage']
NInput: typeof import('naive-ui')['NInput'] NInput: typeof import('naive-ui')['NInput']
NInputNumber: typeof import('naive-ui')['NInputNumber'] NInputNumber: typeof import('naive-ui')['NInputNumber']
@@ -54,6 +61,7 @@ declare module 'vue' {
NModal: typeof import('naive-ui')['NModal'] NModal: typeof import('naive-ui')['NModal']
NModalProvider: typeof import('naive-ui')['NModalProvider'] NModalProvider: typeof import('naive-ui')['NModalProvider']
NP: typeof import('naive-ui')['NP'] NP: typeof import('naive-ui')['NP']
NPopover: typeof import('naive-ui')['NPopover']
NRadio: typeof import('naive-ui')['NRadio'] NRadio: typeof import('naive-ui')['NRadio']
NRadioButton: typeof import('naive-ui')['NRadioButton'] NRadioButton: typeof import('naive-ui')['NRadioButton']
NRadioGroup: typeof import('naive-ui')['NRadioGroup'] NRadioGroup: typeof import('naive-ui')['NRadioGroup']
@@ -63,6 +71,7 @@ declare module 'vue' {
NTabs: typeof import('naive-ui')['NTabs'] NTabs: typeof import('naive-ui')['NTabs']
NTag: typeof import('naive-ui')['NTag'] NTag: typeof import('naive-ui')['NTag']
NText: typeof import('naive-ui')['NText'] NText: typeof import('naive-ui')['NText']
NTooltip: typeof import('naive-ui')['NTooltip']
NUpload: typeof import('naive-ui')['NUpload'] NUpload: typeof import('naive-ui')['NUpload']
NUploadDragger: typeof import('naive-ui')['NUploadDragger'] NUploadDragger: typeof import('naive-ui')['NUploadDragger']
PageHeader: typeof import('./src/components/PageHeader.vue')['default'] PageHeader: typeof import('./src/components/PageHeader.vue')['default']
@@ -81,19 +90,24 @@ 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 ChatControlPanel: typeof import('./src/components/chatroom/ChatControlPanel.vue')['default'] const ChatControlPanel: typeof import('./src/components/chatroom/ChatControlPanel.vue')['default']
const ChatMessage: typeof import('./src/components/chatroom/ChatMessage.vue')['default'] const ChatMessage: typeof import('./src/components/chatroom/ChatMessage.vue')['default']
const ChatPromptQuicker: typeof import('./src/components/chatroom/ChatPromptQuicker.vue')['default'] const ChatPromptQuicker: typeof import('./src/components/chatroom/ChatPromptQuicker.vue')['default']
const ChatroomCard: typeof import('./src/components/chatroom/ChatroomCard.vue')['default'] const ChatroomCard: typeof import('./src/components/chatroom/ChatroomCard.vue')['default']
const ChatroomCreatorModal: typeof import('./src/components/chatroom/ChatroomCreatorModal.vue')['default'] const ChatroomCreatorModal: typeof import('./src/components/chatroom/ChatroomCreatorModal.vue')['default']
const ChatroomEditorModal: typeof import('./src/components/chatroom/ChatroomEditorModal.vue')['default']
const ChatTable: typeof import('./src/components/chatroom/ChatTable.vue')['default'] const ChatTable: typeof import('./src/components/chatroom/ChatTable.vue')['default']
const ConfigCard: typeof import('./src/components/admin/ConfigCard.vue')['default'] const ConfigCard: typeof import('./src/components/admin/ConfigCard.vue')['default']
const FileModal: typeof import('./src/components/file/FileModal.vue')['default'] const FileModal: typeof import('./src/components/file/FileModal.vue')['default']
const FileThumbnail: typeof import('./src/components/file/FileThumbnail.vue')['default'] const FileThumbnail: typeof import('./src/components/file/FileThumbnail.vue')['default']
const InDev: typeof import('./src/components/InDev.vue')['default'] const InDev: typeof import('./src/components/InDev.vue')['default']
const InputFile: typeof import('./src/components/file/InputFile.vue')['default']
const NAlert: typeof import('naive-ui')['NAlert'] const NAlert: typeof import('naive-ui')['NAlert']
const NAvatar: typeof import('naive-ui')['NAvatar'] const NAvatar: typeof import('naive-ui')['NAvatar']
const NButton: typeof import('naive-ui')['NButton'] const NButton: typeof import('naive-ui')['NButton']
@@ -104,6 +118,7 @@ declare global {
const NDataTable: typeof import('naive-ui')['NDataTable'] const NDataTable: typeof import('naive-ui')['NDataTable']
const NDrawer: typeof import('naive-ui')['NDrawer'] const NDrawer: typeof import('naive-ui')['NDrawer']
const NDrawerContent: typeof import('naive-ui')['NDrawerContent'] const NDrawerContent: typeof import('naive-ui')['NDrawerContent']
const NDynamicTags: typeof import('naive-ui')['NDynamicTags']
const NEllipsis: typeof import('naive-ui')['NEllipsis'] const NEllipsis: typeof import('naive-ui')['NEllipsis']
const NEmpty: typeof import('naive-ui')['NEmpty'] const NEmpty: typeof import('naive-ui')['NEmpty']
const NFlex: typeof import('naive-ui')['NFlex'] const NFlex: typeof import('naive-ui')['NFlex']
@@ -114,6 +129,7 @@ declare global {
const NGridItem: typeof import('naive-ui')['NGridItem'] const NGridItem: typeof import('naive-ui')['NGridItem']
const NH2: typeof import('naive-ui')['NH2'] const NH2: typeof import('naive-ui')['NH2']
const NH3: typeof import('naive-ui')['NH3'] const NH3: typeof import('naive-ui')['NH3']
const NH4: typeof import('naive-ui')['NH4']
const NImage: typeof import('naive-ui')['NImage'] const NImage: typeof import('naive-ui')['NImage']
const NInput: typeof import('naive-ui')['NInput'] const NInput: typeof import('naive-ui')['NInput']
const NInputNumber: typeof import('naive-ui')['NInputNumber'] const NInputNumber: typeof import('naive-ui')['NInputNumber']
@@ -123,6 +139,7 @@ declare global {
const NModal: typeof import('naive-ui')['NModal'] const NModal: typeof import('naive-ui')['NModal']
const NModalProvider: typeof import('naive-ui')['NModalProvider'] const NModalProvider: typeof import('naive-ui')['NModalProvider']
const NP: typeof import('naive-ui')['NP'] const NP: typeof import('naive-ui')['NP']
const NPopover: typeof import('naive-ui')['NPopover']
const NRadio: typeof import('naive-ui')['NRadio'] const NRadio: typeof import('naive-ui')['NRadio']
const NRadioButton: typeof import('naive-ui')['NRadioButton'] const NRadioButton: typeof import('naive-ui')['NRadioButton']
const NRadioGroup: typeof import('naive-ui')['NRadioGroup'] const NRadioGroup: typeof import('naive-ui')['NRadioGroup']
@@ -132,6 +149,7 @@ declare global {
const NTabs: typeof import('naive-ui')['NTabs'] const NTabs: typeof import('naive-ui')['NTabs']
const NTag: typeof import('naive-ui')['NTag'] const NTag: typeof import('naive-ui')['NTag']
const NText: typeof import('naive-ui')['NText'] const NText: typeof import('naive-ui')['NText']
const NTooltip: typeof import('naive-ui')['NTooltip']
const NUpload: typeof import('naive-ui')['NUpload'] const NUpload: typeof import('naive-ui')['NUpload']
const NUploadDragger: typeof import('naive-ui')['NUploadDragger'] const NUploadDragger: typeof import('naive-ui')['NUploadDragger']
const PageHeader: typeof import('./src/components/PageHeader.vue')['default'] const PageHeader: typeof import('./src/components/PageHeader.vue')['default']
+11 -10
View File
@@ -1,14 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { useHead } from '@unhead/vue'
import { dateZhCN, zhCN } from 'naive-ui'
import { onMounted } from 'vue'
import PageHeader from '@/components/PageHeader.vue' import PageHeader from '@/components/PageHeader.vue'
import {dateZhCN, zhCN} from 'naive-ui' import { useNowUser } from '@/stores/now-user.ts'
import {useNowUser} from '@/stores/now-user.ts'
import {onMounted} from 'vue'
import {useHead} from "@unhead/vue";
const NOWUSER = useNowUser() const NOWUSER = useNowUser()
useHead({ useHead({
titleTemplate: "%s | NayHome" titleTemplate: '%s | NayHome',
}) })
onMounted(async () => { onMounted(async () => {
@@ -18,9 +19,9 @@ onMounted(async () => {
try { try {
await NOWUSER.loadUserInfo(Number(user_id), access_token) await NOWUSER.loadUserInfo(Number(user_id), access_token)
} catch { } catch {
localStorage.removeItem("user-id") localStorage.removeItem('user-id')
localStorage.removeItem('access-token') localStorage.removeItem('access-token')
console.log("已移除 localstorage 中存储的验证信息。") console.log('已移除 localstorage 中存储的验证信息。')
} }
} }
}) })
@@ -29,15 +30,15 @@ onMounted(async () => {
<template> <template>
<n-config-provider id="aapp" :date-locale="dateZhCN" :locale="zhCN"> <n-config-provider id="aapp" :date-locale="dateZhCN" :locale="zhCN">
<div class="header-container"> <div class="header-container">
<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>
<div class="footer-container">🌸 Nya Home ~</div> <div class="footer-container">🌸 Nya Home ~</div>
<n-global-style/> <n-global-style />
</n-config-provider> </n-config-provider>
</template> </template>
+34 -14
View File
@@ -1,6 +1,9 @@
div.message { div.message {
position: relative; position: relative;
padding: 4px 12px; padding: 0 12px;
border-width: 0 0 0 4px;
border-style: solid;
border-radius: 4px;
font-size: 1rem; font-size: 1rem;
height: max-content; height: max-content;
@@ -18,35 +21,52 @@ div.message {
} }
div.user-message { div.user-message {
border: 2px solid #e1ff20; border-color: #e1ff20;
border-radius: 4px; background: #fffbdb;
background: #fffbb1;
} }
div.aii-message { div.aii-message {
border: 2px solid #20ff54; border-color: #20ff54;
border-radius: 4px; background: #e6fff1;
background: #b1ffd0;
} }
div.aii-message-streaming { div.aii-message-streaming {
border: 2px solid #20d2ff; border-color: #20d2ff;
border-radius: 4px; background: #dbfaff;
background: #b1f8ff;
div.thinking { div.thinking {
border: 1px solid #e9ff20; border: solid #5f20ff;
border-width: 0 0 0 2px;
border-radius: 4px; border-radius: 4px;
padding: 4px 8px; padding: 8px 12px;
margin: 6px 3px; margin: 6px 3px;
background: rgb(34 197 94 / 0.2); background: rgb(205 127 255 / 0.2);
min-height: 60px;
} }
} }
// 折叠消息 // 折叠消息
div.collapse { div.collapse {
overflow: hidden; overflow: hidden;
min-height: 80px; min-height: 160px;
&::after {
content: '● ● ●';
text-align: center;
display: block;
position: absolute;
font-size: 18px;
width: 80px;
height: 32px;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
background: rgb(255 255 255 / 0.6);
border: 1px solid rgb(255 255 255);
border-radius: 4px;
}
} }
div.xaml-block { div.xaml-block {
+8
View File
@@ -6,6 +6,10 @@ body {
overflow: hidden; overflow: hidden;
} }
* {
box-sizing: border-box;
}
div#app { div#app {
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
@@ -44,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;
}
+2 -6
View File
@@ -1,13 +1,9 @@
<script setup lang="ts"> <script setup lang="ts"></script>
</script>
<template> <template>
<div class="in-dev"> <div class="in-dev">
<n-text class="in-dev-title">功能开发中</n-text> <n-text class="in-dev-title">功能开发中</n-text>
<n-text class="in-dev-content"> <n-text class="in-dev-content"> 已经被画在饼上辽请耐心等待喵 </n-text>
已经被画在饼上辽请耐心等待喵
</n-text>
</div> </div>
</template> </template>
+3 -3
View File
@@ -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>
+1
View File
@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { createErrorBlock, createXamlBlock, type Xaml } from '@/components/xaml-block.tsx' import { createErrorBlock, createXamlBlock, type Xaml } from '@/components/xaml-block.tsx'
const showModal = defineModel('showModal', { required: true }) const showModal = defineModel('showModal', { required: true })
+23 -15
View File
@@ -1,23 +1,24 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref} from 'vue' import { useMessage } from 'naive-ui'
import {api} from '@/tools/web.ts' import { ref } from 'vue'
import type {ReturnDto} from '@/types/response.ts'
import {useMessage} from 'naive-ui'
import VerifyCodeModal from '@/components/admin/VerifyCodeModal.vue' import VerifyCodeModal from '@/components/admin/VerifyCodeModal.vue'
import {useNowUser} from "@/stores/now-user.ts"; import { useNowUser } from '@/stores/now-user.ts'
import { api } from '@/tools/web.ts'
import type { ReturnDto } from '@/types/response.ts'
const MESSAGE = useMessage() const MESSAGE = useMessage()
const NOWUSER = useNowUser() const NOWUSER = useNowUser()
const showModal = defineModel('showModal', {required: true}) const showModal = defineModel('showModal', { required: true })
const showVerifyCodeModal = ref(false) const showVerifyCodeModal = ref(false)
const newEmail = ref("") const newEmail = ref('')
const verifyCode = ref("") const verifyCode = ref('')
function sendEmail() { function sendEmail() {
api api
.post('/admin/me/email-verify/send/', JSON.stringify({to: newEmail.value})) .post('/admin/me/email-verify/send/', JSON.stringify({ to: newEmail.value }))
.then((res) => res.data as ReturnDto) .then((res) => res.data as ReturnDto)
.then((res) => { .then((res) => {
if (res.success) { if (res.success) {
@@ -33,10 +34,14 @@ function sendEmail() {
} }
function verifyEmail() { function verifyEmail() {
api.post('/admin/me/email-verify/', JSON.stringify({ api
.post(
'/admin/me/email-verify/',
JSON.stringify({
to: newEmail.value, to: newEmail.value,
verify_code: String(verifyCode.value).split(",").join(""), verify_code: String(verifyCode.value).split(',').join(''),
})) }),
)
.then((res) => res.data as ReturnDto) .then((res) => res.data as ReturnDto)
.then((res) => { .then((res) => {
if (res.success) { if (res.success) {
@@ -59,7 +64,7 @@ function verifyEmail() {
<n-form label-placement="left"> <n-form label-placement="left">
<n-p>你需要使用新的邮件地址接收一个验证码来完成修改</n-p> <n-p>你需要使用新的邮件地址接收一个验证码来完成修改</n-p>
<n-form-item path="to" label="新的邮件地址"> <n-form-item path="to" label="新的邮件地址">
<n-input v-model:value="newEmail"/> <n-input v-model:value="newEmail" />
</n-form-item> </n-form-item>
<n-flex> <n-flex>
<n-button type="warning" @click="sendEmail()">获取验证码</n-button> <n-button type="warning" @click="sendEmail()">获取验证码</n-button>
@@ -69,8 +74,11 @@ function verifyEmail() {
</n-form> </n-form>
</n-modal> </n-modal>
<verify-code-modal v-model:show-modal="showVerifyCodeModal" v-model:verify-code="verifyCode" <verify-code-modal
:verify="verifyEmail"/> v-model:show-modal="showVerifyCodeModal"
v-model:verify-code="verifyCode"
:verify="verifyEmail"
/>
</template> </template>
<style scoped></style> <style scoped></style>
@@ -0,0 +1,25 @@
<script setup lang="ts">
import { ref } from 'vue'
const showModal = defineModel('showModal', { required: true })
const newPhone = ref('')
</script>
<template>
<n-modal v-model:show="showModal" preset="card" style="width: 600px" title="修改手机号">
<n-form label-placement="left">
<n-p>你需要使用新的手机号接收一个验证码来完成修改</n-p>
<n-form-item path="to" label="新的手机号">
<n-input v-model:value="newPhone" />
</n-form-item>
<n-flex>
<n-button type="warning" disabled>获取验证码</n-button>
<n-button type="tertiary">暂不支持喵</n-button>
<n-tag type="info">验证码有效期为 5 分钟且不允许多个同时有效</n-tag>
</n-flex>
</n-form>
</n-modal>
</template>
<style scoped></style>
+5 -7
View File
@@ -1,21 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
defineProps<{ defineProps<{
title: string; title: string
}>() }>()
</script> </script>
<template> <template>
<n-card :title> <n-card :title>
<template #header-extra> <template #header-extra>
<slot name="extra"/> <slot name="extra" />
</template> </template>
<slot name="default"/> <slot name="default" />
<template #action> <template #action>
<slot name="action"/> <slot name="action" />
</template> </template>
</n-card> </n-card>
</template> </template>
<style scoped> <style scoped></style>
</style>
+37 -19
View File
@@ -1,10 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref} from 'vue' import { AxiosError } from 'axios'
import {api} from '@/tools/web.js' import { useMessage } from 'naive-ui'
import type {ReturnDto} from '@/types/response.js' import { ref } from 'vue'
import {useMessage} from 'naive-ui'
import {AxiosError} from 'axios' import { useNowUser } from '@/stores/now-user.js'
import {useNowUser} from '@/stores/now-user.js' import { api } from '@/tools/web.js'
import type { ReturnDto } from '@/types/response.js'
const MESSAGE = useMessage() const MESSAGE = useMessage()
const NOWUSER = useNowUser() const NOWUSER = useNowUser()
@@ -20,7 +21,7 @@ const loginForm = ref({
function login() { function login() {
api api
.post(`/admin/login/${loginMethod.value}`, { .post(`/admin/login/${loginMethod.value}/`, {
username: loginForm.value.username, username: loginForm.value.username,
password: loginForm.value.password, password: loginForm.value.password,
}) })
@@ -50,14 +51,21 @@ function login() {
MESSAGE.error(`登录失败:${err_msg}`) MESSAGE.error(`登录失败:${err_msg}`)
}) })
} }
function logout() {
NOWUSER.is_login = false
localStorage.removeItem('user-id')
localStorage.removeItem('access-token')
MESSAGE.success('已注销登录状态……')
}
</script> </script>
<template> <template>
<div> <div>
<div class="user-action nyahome-card" v-if="NOWUSER.isLogin" style="position: relative"> <div class="user-action nyahome-card" v-if="NOWUSER.is_login" style="position: relative">
<img :src="NOWUSER.background_url" alt="User Background" class="user-action-background"> <img :src="NOWUSER.background_url" alt="User Background" class="user-action-background" />
<div class="card-content" style="margin-top: auto; margin-bottom: 20px;"> <div class="card-content" style="margin-top: auto; margin-bottom: 20px">
<n-avatar :size="96" circle :src="NOWUSER.avatar_url"/> <n-avatar :size="96" circle :src="NOWUSER.avatar_url" />
<n-h2 style="margin: 0"> <n-h2 style="margin: 0">
{{ NOWUSER.display_name ? NOWUSER.display_name : NOWUSER.name }} {{ NOWUSER.display_name ? NOWUSER.display_name : NOWUSER.name }}
</n-h2> </n-h2>
@@ -69,9 +77,19 @@ function login() {
<router-link class="card-button" :to="`/user/${NOWUSER.id}`"> <router-link class="card-button" :to="`/user/${NOWUSER.id}`">
<n-button type="info" style="width: 100%" secondary>主页</n-button> <n-button type="info" style="width: 100%" secondary>主页</n-button>
</router-link> </router-link>
<router-link class="card-button" to="#"> <n-popover trigger="click">
<n-button type="error" style="width: 100%" secondary>注销</n-button> <template #trigger>
</router-link> <n-button class="card-button" type="error" style="width: 100%; padding: 0" secondary>
注销
</n-button>
</template>
<n-flex vertical>
<n-alert type="info">
确认注销登录吗注销后你可以重新登录或者切换至其他账号
</n-alert>
<n-button type="warning" secondary @click="logout()">确认注销</n-button>
</n-flex>
</n-popover>
</n-flex> </n-flex>
</div> </div>
</div> </div>
@@ -82,13 +100,13 @@ function login() {
<n-radio-button value="register">注册</n-radio-button> <n-radio-button value="register">注册</n-radio-button>
</n-radio-group> </n-radio-group>
<div class="card-content" v-if="page === 'login'"> <div class="card-content" v-if="page === 'login'">
<n-avatar :size="96" circle/> <n-avatar :size="96" circle />
<n-radio-group v-model:value="loginMethod"> <n-radio-group v-model:value="loginMethod">
<n-radio-button value="name">用户名</n-radio-button> <n-radio-button value="name">用户名</n-radio-button>
<n-radio-button value="email">邮箱</n-radio-button> <n-radio-button value="email">邮箱</n-radio-button>
<n-radio-button value="phone">手机</n-radio-button> <n-radio-button value="phone">手机</n-radio-button>
</n-radio-group> </n-radio-group>
<n-input v-model:value="loginForm.username" class="card-input" placeholder=""/> <n-input v-model:value="loginForm.username" class="card-input" placeholder="" />
<n-input <n-input
v-model:value="loginForm.password" v-model:value="loginForm.password"
class="card-input" class="card-input"
@@ -103,9 +121,9 @@ function login() {
</div> </div>
<div class="card-content" v-else> <div class="card-content" v-else>
<n-avatar :size="96" circle/> <n-avatar :size="96" circle />
<n-input class="card-input" placeholder="用户名"/> <n-input class="card-input" placeholder="用户名" />
<n-input class="card-input" placeholder="密码" type="password" show-password-toggle/> <n-input class="card-input" placeholder="密码" type="password" show-password-toggle />
<n-flex class="card-input"> <n-flex class="card-input">
<n-button type="primary" class="card-button">注册</n-button> <n-button type="primary" class="card-button">注册</n-button>
</n-flex> </n-flex>
@@ -1,15 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref} from "vue"; import { useMessage } from 'naive-ui'
import {api} from "@/tools/web.ts"; import { ref } from 'vue'
import {useNowUser} from "@/stores/now-user.ts"; import { useRouter } from 'vue-router'
import {useMessage} from "naive-ui";
import {useRouter} from "vue-router";
const ROUTER = useRouter(); import { useNowUser } from '@/stores/now-user.ts'
import { api } from '@/tools/web.ts'
const ROUTER = useRouter()
const MESSAGE = useMessage() const MESSAGE = useMessage()
const NOWUSER = useNowUser() const NOWUSER = useNowUser()
const showModal = defineModel("showModal", {required: true}) const showModal = defineModel('showModal', { required: true })
const changeForm = ref({ const changeForm = ref({
old_password: '', old_password: '',
@@ -17,40 +18,41 @@ const changeForm = ref({
}) })
function change() { function change() {
api.post("/admin/me/password/", JSON.stringify(changeForm.value)) api
.post('/admin/me/password/', JSON.stringify(changeForm.value))
.then(() => { .then(() => {
MESSAGE.success("密码修改成功,请重新登录。") MESSAGE.success('密码修改成功,请重新登录。')
NOWUSER.isLogin = false NOWUSER.is_login = false
localStorage.removeItem("user-id") localStorage.removeItem('user-id')
localStorage.removeItem("access-token") localStorage.removeItem('access-token')
ROUTER.push("/") ROUTER.push('/')
}) })
.catch((err) => { .catch((err) => {
MESSAGE.error(`密码修改失败:${err}`) MESSAGE.error(`密码修改失败:${err}`)
MESSAGE.warning("如果您忘记了原密码,请选择「忘记密码」。") MESSAGE.warning('如果您忘记了原密码,请选择「忘记密码」。')
}) })
} }
</script> </script>
<template> <template>
<n-modal style="width: 600px;" v-model:show="showModal" title="修改密码" preset="card"> <n-modal style="width: 600px" v-model:show="showModal" title="修改密码" preset="card">
<n-form label-align="right" label-placement="left" label-width="auto" :model="changeForm"> <n-form label-align="right" label-placement="left" label-width="auto" :model="changeForm">
<n-form-item label="原密码" path="old_password"> <n-form-item label="原密码" path="old_password">
<n-input v-model:value="changeForm.old_password"/> <n-input v-model:value="changeForm.old_password" />
</n-form-item> </n-form-item>
<n-form-item label="新密码" path="new_password"> <n-form-item label="新密码" path="new_password">
<n-input v-model:value="changeForm.new_password" type="password" show-password-toggle/> <n-input v-model:value="changeForm.new_password" type="password" show-password-toggle />
</n-form-item> </n-form-item>
<n-form-item label="确认修改"> <n-form-item label="确认修改">
<n-flex> <n-flex>
<n-button type="error" @click="change()">确认修改</n-button> <n-button type="error" @click="change()">确认修改</n-button>
<n-tag type="warning" size="large">修改密码会注销所有已登录状态您将需要重新登录</n-tag> <n-tag type="warning" size="large"
>修改密码会注销所有已登录状态您将需要重新登录
</n-tag>
</n-flex> </n-flex>
</n-form-item> </n-form-item>
</n-form> </n-form>
</n-modal> </n-modal>
</template> </template>
<style scoped> <style scoped></style>
</style>
@@ -1,14 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import {computed, onMounted, ref, watch} from 'vue' import { type SelectOption, useMessage } from 'naive-ui'
import AiiProviderAddModal from '@/components/chatroom/AiiProviderAddModal.vue' import { computed, onMounted, ref, watch } from 'vue'
import {api} from '@/tools/web.js'
import type {ReturnDto} from '@/types/response.js' import AiiProviderAddModal from '@/components/aii/AiiProviderAddModal.vue'
import {type SelectOption, useMessage} from 'naive-ui' import { aiiModelRules, check_remote_model } from '@/tools/avaliable-check.ts'
import type {AiiProviderPublicWithoutKey} from '@/types/aii.js' import { api } from '@/tools/web.js'
import type { AiiModelPublic, AiiProviderPublicWithoutKey } from '@/types/aii.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)
@@ -19,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,
}) })
@@ -29,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} 个模型提供商。`)
@@ -81,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}`)
@@ -117,24 +111,42 @@ 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
</n-button type="success"
size="small"
round
@click="loadProviders()"
v-if="!noAddProvider"
> >
<n-button secondary type="warning" size="small" round @click="showAddProviderModal = true" 刷新
>添加 </n-button>
</n-button <n-button
secondary
type="warning"
size="small"
round
@click="showAddProviderModal = true"
v-if="!noAddProvider"
> >
添加
</n-button>
</n-flex> </n-flex>
</n-form-item> </n-form-item>
<n-form-item label="模型名称" path="model_name"> <n-form-item label="模型名称" path="model_name">
<n-flex style="width: 100%" justify="right" align="center"> <n-flex style="width: 100%" justify="right" align="center">
<n-input v-model:value="addModelForm.model_name"/> <n-input v-model:value="addModelForm.model_name" />
<n-flex style="overflow: auto"> <n-flex style="overflow: auto">
<n-button <n-button
secondary secondary
@@ -145,13 +157,11 @@ function onConfirm() {
v-bind:key="m" v-bind:key="m"
@click="addModelForm.model_name = m" @click="addModelForm.model_name = m"
>{{ m }} >{{ m }}
</n-button </n-button>
>
</n-flex> </n-flex>
<n-button secondary type="success" size="small" round @click="onGetRemoteModels()" <n-button secondary type="success" size="small" round @click="onGetRemoteModels()"
>获取模型列表 >获取模型列表
</n-button </n-button>
>
</n-flex> </n-flex>
</n-form-item> </n-form-item>
<n-form-item label="最大上下文" path="max_context_length"> <n-form-item label="最大上下文" path="max_context_length">
@@ -159,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>
@@ -166,7 +179,7 @@ function onConfirm() {
</n-flex> </n-flex>
</n-form-item> </n-form-item>
</n-form> </n-form>
<aii-provider-add-modal v-model:show-modal="showAddProviderModal"/> <aii-provider-add-modal v-model:show-modal="showAddProviderModal" />
</n-modal> </n-modal>
</template> </template>
@@ -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>
@@ -0,0 +1,69 @@
<script setup lang="ts">
import { useMessage } from 'naive-ui'
import { ref } from 'vue'
import { check_remote_provider } from '@/tools/avaliable-check.ts'
import { api } from '@/tools/web.js'
import type { AiiProviderPublicWithoutKey } from '@/types/aii.ts'
const MESSAGE = useMessage()
const showModal = defineModel('showModal', { required: true })
const { reload } = defineProps<{
reload?: () => void
}>()
const addProviderForm = ref({
id: 0,
name: '',
base_url: '',
api_key: '',
})
async function onCheck() {
if (await check_remote_provider(addProviderForm.value)) {
MESSAGE.success(`检查模型提供商 ${addProviderForm.value.name} 可用性成功。`)
} else {
MESSAGE.success(`检查模型提供商 ${addProviderForm.value.name} 可用性失败?`)
}
}
function onConfirm() {
api
.post('/aii/provider/', JSON.stringify(addProviderForm.value))
.then((res) => res.data as AiiProviderPublicWithoutKey)
.then(() => {
MESSAGE.success(`已添加模型提供商 ${addProviderForm.value.name}`)
showModal.value = false
if (reload) reload()
})
.catch((err) => {
MESSAGE.error(`添加模型提供商失败:${err}`)
})
}
</script>
<template>
<n-modal v-model:show="showModal" preset="card" title="添加模型提供商">
<n-form :model="addProviderForm" label-placement="left" label-width="auto" label-align="right">
<n-form-item label="名称" path="name" :rule="{ required: true, trigger: 'blur' }">
<n-input v-model:value="addProviderForm.name" />
</n-form-item>
<n-form-item label="Base URL" path="base_url" :rule="{ required: true, trigger: 'blur' }">
<n-input v-model:value="addProviderForm.base_url" />
</n-form-item>
<n-form-item label="API Key" path="api_key" :rule="{ required: true, trigger: 'blur' }">
<n-input v-model:value="addProviderForm.api_key" />
</n-form-item>
<n-form-item label="添加完成">
<n-flex>
<n-button secondary type="info" @click="onCheck()">检测</n-button>
<n-button secondary type="primary" @click="onConfirm()">确认</n-button>
</n-flex>
</n-form-item>
</n-form>
</n-modal>
</template>
<style scoped></style>
@@ -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,74 +0,0 @@
<script setup lang="ts">
import {ref} from 'vue'
import {api} from '@/tools/web.js'
import type {ReturnDto} from '@/types/response.js'
import {useMessage} from 'naive-ui'
const MESSAGE = useMessage()
const showModal = defineModel('showModal', {required: true})
const addProviderForm = ref({
id: 0,
name: '',
base_url: '',
api_key: '',
})
function onCheck() {
api
.post('/aii/remote/provider/check/', JSON.stringify(addProviderForm.value))
.then((res) => res.data as ReturnDto)
.then((data) => {
if (data.success) {
MESSAGE.success(`模型提供商检测成功,探测到 ${data.result} 个可用模型。`)
} else {
MESSAGE.warning('模型提供商检测失败,请确认 Base URI 与 API key 是否正确。')
}
})
.catch((err) => {
MESSAGE.error(`检测模型提供商时遇到未知的异常,请检查后端业务:${err}`)
})
}
function onConfirm() {
api
.post('/aii/provider/', JSON.stringify(addProviderForm.value))
.then((res) => res.data as ReturnDto)
.then((data) => {
if (data.success) {
MESSAGE.success(`已添加模型提供商 ${addProviderForm.value.name}`)
showModal.value = false
} else {
throw TypeError('后端业务表示添加模型提供商失败,但未提供原因。')
}
})
.catch((err) => {
MESSAGE.error(`添加模型提供商失败:${err}`)
})
}
</script>
<template>
<n-modal v-model:show="showModal" preset="card" title="添加模型提供商">
<n-form :model="addProviderForm" label-placement="left" label-width="auto" label-align="right">
<n-form-item label="名称" path="name">
<n-input v-model:value="addProviderForm.name"/>
</n-form-item>
<n-form-item label="Base URL" path="base_url">
<n-input v-model:value="addProviderForm.base_url"/>
</n-form-item>
<n-form-item label="API Key" path="api_key">
<n-input v-model:value="addProviderForm.api_key"/>
</n-form-item>
<n-form-item label="添加完成">
<n-flex>
<n-button secondary type="info" @click="onCheck()">检测</n-button>
<n-button secondary type="primary" @click="onConfirm()">确认</n-button>
</n-flex>
</n-form-item>
</n-form>
</n-modal>
</template>
<style scoped></style>
@@ -1,23 +1,30 @@
<script setup lang="ts"> <script setup lang="ts">
import {computed, onMounted, ref} from 'vue' import { NTag, type SelectOption, useMessage } from 'naive-ui'
import {api} from '@/tools/web.js' import { computed, h, onMounted, ref, type VNode, watch } from 'vue'
import type {ReturnDto} from '@/types/response.js'
import {type SelectOption, useMessage} from 'naive-ui'
import AiiModelAddModal from '@/components/chatroom/AiiModelAddModal.vue'
import type {AiiModelPublic} from '@/types/aii.js'
import ChatPromptQuicker from '@/components/chatroom/ChatPromptQuicker.vue'
import ScriptDrawer from '@/components/chatroom/ScriptDrawer.vue'
import ChatPromptQuicker from '@/components/chatroom/ChatPromptQuicker.vue'
import ChatroomEditorModal from '@/components/chatroom/ChatroomEditorModal.vue'
import ScriptDrawer from '@/components/chatroom/ScriptDrawer.vue'
import { useNowUser } from '@/stores/now-user.ts'
import { api } from '@/tools/web.js'
import type { AiiModelPublic } from '@/types/aii.js'
import type { Chatroom, ChatroomPublic } from '@/types/chatroom.ts'
import AiiModelAddModal from '../aii/AiiModelAddModal.vue'
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 quickerPrompt = defineModel<string>('quickerPrompt', {required: true}) const selectedModel = defineModel<AiiModelPublic | null>('selectModel', { required: true })
const quickerPrompt = defineModel<string>('quickerPrompt', { required: true })
const {script} = defineProps<{ const { chatroom, loadPage } = defineProps<{
script: string chatroom: Chatroom
loadPage: () => void
}>() }>()
const showModal = ref(false) const showAddModelModal = ref(false)
const models = ref<AiiModelPublic[]>([]) const models = ref<AiiModelPublic[]>([])
const showScriptDrawer = ref(false) const showScriptDrawer = ref(false)
@@ -26,31 +33,111 @@ 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
}) })
function load() { // 在选中的模型 ID 以及请求得到的模型列表出现变化时,重新确定当前选中模型。
api // 此处选中的模型会同步到上级组件 Chatroom1Page,然后同步给 ChatTable。
.get('/aii/model') // 从而,ChatTable 能够提供思考开关以及更多设置。
.then((res) => res.data as ReturnDto) watch(
.then((data) => { [selectedModelId, models],
if (data.success) { (newVal) => {
models.value = data.result as AiiModelPublic[] if (newVal[0]) {
} else { const newModel = models.value.find((v) => v.id === newVal[0])
throw TypeError('获取模型列表失败……') if (newModel) {
selectedModel.value = newModel
} }
}) }
},
{ immediate: true },
)
function loadModels() {
api
.get('/aii/model/')
.then((res) => (models.value = res.data as AiiModelPublic[]))
.catch((err) => { .catch((err) => {
MESSAGE.error(`加载模型列表失败:${err}`) MESSAGE.error(`加载模型列表失败:${err}`)
}) })
} }
onMounted(() => { onMounted(() => {
load() loadModels()
if (chatroom.default_model_id) {
selectedModelId.value = chatroom.default_model_id
} else {
MESSAGE.info(
'此聊天室还未设置默认模型。你需要选择一个模型然后开始聊天,或者现在就保存一个默认模型嘛?',
)
}
}) })
const showChatroomInfoModal = ref(false)
const chatroomInfo = ref<ChatroomPublic>({
id: chatroom.id,
name: chatroom.name,
description: chatroom.description,
feature_image: chatroom.feature_image,
default_model_id: chatroom.default_model_id,
script_template_id: chatroom.script_template_id,
script_template_version: chatroom.script_template_version,
})
function saveDefaultModel() {
if (selectedModelId.value) {
chatroomInfo.value.default_model_id = selectedModelId.value
api
.post(`/chatroom/${chatroom.id}/`, JSON.stringify(chatroomInfo.value))
.then(() => {
MESSAGE.success('默认模型设置成功~')
})
.catch((err) => {
MESSAGE.error(`设置默认模型失败:${err}`)
})
} else {
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>
@@ -58,21 +145,46 @@ onMounted(() => {
<n-card title="模型"> <n-card title="模型">
<template #header-extra> <template #header-extra>
<n-flex> <n-flex>
<n-button secondary type="info" size="small" round @click="load()">刷新</n-button> <n-button secondary type="info" size="small" round @click="loadModels()">刷新</n-button>
<n-button secondary type="warning" size="small" round @click="showModal = true">
添加
</n-button>
<n-button-group> <n-button-group>
<n-button secondary type="primary" size="small" round>保存</n-button> <n-button secondary type="primary" size="small" round @click="saveDefaultModel()">
保存
</n-button>
<n-popover>
<template #trigger>
<n-button secondary type="tertiary" size="small" round>?</n-button> <n-button secondary type="tertiary" size="small" round>?</n-button>
</template>
<n-h4>有哪些模型</n-h4>
<n-p>NyaHome 管理员可以添加模型然后所有用户都可以使用这些模型</n-p>
<n-p>NyaHome 不提供模型调用</n-p>
<n-h4>默认模型切换模型</n-h4>
<n-p>可以针对聊天室保存一个默认模型</n-p>
<n-p>你也可以在这里切换其他的模型但在保存之前默认模型不会修改</n-p>
</n-popover>
</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">
<aii-model-add-modal v-model:show-modal="showModal"/> <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" />
</n-card> </n-card>
<chat-prompt-quicker v-model:prompt-prefix="quickerPrompt"/> <chat-prompt-quicker v-model:prompt-prefix="quickerPrompt" />
<n-card title="剧本"> <n-card title="剧本">
<template #header-extra>故事设定 · 世界书</template> <template #header-extra>故事设定 · 世界书</template>
@@ -82,15 +194,22 @@ onMounted(() => {
故事设定 · 世界书 故事设定 · 世界书
</n-button> </n-button>
</n-flex> </n-flex>
<script-drawer :script v-model:show-drawer="showScriptDrawer"/> <script-drawer :script="chatroom.script" v-model:show-drawer="showScriptDrawer" />
</n-card> </n-card>
<n-card title="设置"> <n-card title="设置">
<template #header-extra>也许你不需要修改这里</template> <template #header-extra>也许你不需要修改这里</template>
<n-flex vertical> <n-flex vertical>
<n-button secondary type="primary">聊天室信息</n-button> <n-button secondary type="primary" @click="showChatroomInfoModal = true">
聊天室信息
</n-button>
<n-button secondary type="info">系统设置</n-button> <n-button secondary type="info">系统设置</n-button>
</n-flex> </n-flex>
<chatroom-editor-modal
:chatroom="chatroomInfo"
v-model:show-modal="showChatroomInfoModal"
:reload="loadPage"
/>
</n-card> </n-card>
</n-flex> </n-flex>
</template> </template>
@@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import {md} from '@/tools/md.js' import { onMounted, ref, useTemplateRef } from 'vue'
import {onMounted, ref, useTemplateRef} from 'vue'
const {role, msg} = defineProps<{ import { md } from '@/tools/md.js'
const { role, msg } = defineProps<{
role: 'aii' | 'user' role: 'aii' | 'user'
msg: string msg: string
onMessageEdit: (oldMessage: string, newMessage: string, change: 'aii' | 'user') => void onMessageEdit: (oldMessage: string, newMessage: string, change: 'aii' | 'user') => void
@@ -37,7 +38,7 @@ onMounted(() => {
<template> <template>
<div :class="[`${role}-message`, 'message']" ref="self"> <div :class="[`${role}-message`, 'message']" ref="self">
<p v-html="md.render(msg)"/> <p v-html="md.render(msg)" />
<n-button class="modify-button" secondary type="info" circle @click="showModal = true"> <n-button class="modify-button" secondary type="info" circle @click="showModal = true">
</n-button> </n-button>
@@ -70,7 +71,7 @@ onMounted(() => {
> >
<n-h3 prefix="bar" v-if="showEditor">编辑中</n-h3> <n-h3 prefix="bar" v-if="showEditor">编辑中</n-h3>
<n-input v-if="showEditor" type="textarea" :rows="10" v-model:value="editorMessage"></n-input> <n-input v-if="showEditor" type="textarea" :rows="10" v-model:value="editorMessage"></n-input>
<n-code v-else :code="msg" word-wrap/> <n-code v-else :code="msg" word-wrap />
<!--suppress VueUnrecognizedSlot --> <!--suppress VueUnrecognizedSlot -->
<template #footer> <template #footer>
<n-flex align="center" style="padding-top: 10px"> <n-flex align="center" style="padding-top: 10px">
@@ -1,5 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue' import { useMessage } from 'naive-ui'
import { onMounted, ref, watch } from 'vue'
const MESSAGE = useMessage()
const promptPrefix = defineModel<string>('promptPrefix', { required: true }) const promptPrefix = defineModel<string>('promptPrefix', { required: true })
@@ -8,12 +11,31 @@ const quickerForm = ref({
style: '第三人称全知视角,禁止打破第四面墙。', style: '第三人称全知视角,禁止打破第四面墙。',
}) })
function save() { function saveToBrowser() {
promptPrefix.value = `<要求><输出字数>${quickerForm.value.length}</输出字数><风格约束>${quickerForm.value.style}</风格约束></要求>` localStorage.setItem('prompt-quicker', JSON.stringify(quickerForm.value))
MESSAGE.success('已保存快速提示词至浏览器。')
} }
function loadFromBrowser() {
const temp = localStorage.getItem('prompt-quicker')
if (temp) {
quickerForm.value = JSON.parse(temp)
MESSAGE.success('从浏览器中读取到保存的快速提示词。')
} else {
MESSAGE.info('未找到保存的快速提示词。')
}
}
// 将本组件中进行的对快速提示词的修改自动同步到父级
watch(
() => quickerForm.value,
(newValue) => {
promptPrefix.value = `<要求><输出字数>${newValue.length}</输出字数><风格约束>${newValue.style}</风格约束></要求>`
},
)
onMounted(() => { onMounted(() => {
save() loadFromBrowser()
}) })
</script> </script>
@@ -21,8 +43,17 @@ onMounted(() => {
<n-card title="快速调整"> <n-card title="快速调整">
<template #header-extra> <template #header-extra>
<n-button-group> <n-button-group>
<n-button secondary type="primary" size="small" round @click="save()">保存</n-button> <n-button secondary type="primary" size="small" round @click="saveToBrowser()">
保存
</n-button>
<n-popover>
<template #trigger>
<n-button secondary type="tertiary" size="small" round>?</n-button> <n-button secondary type="tertiary" size="small" round>?</n-button>
</template>
<n-h4>这是什么</n-h4>
<n-p>快速预设一些可能需要随时修改的提示词它们会被结构化地拼接在用户消息中</n-p>
<n-p>您的修改无需保存即可随请求发送保存可以将提示词存储在浏览器中</n-p>
</n-popover>
</n-button-group> </n-button-group>
</template> </template>
<n-form :model="quickerForm"> <n-form :model="quickerForm">
+15 -8
View File
@@ -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
@@ -15,8 +17,8 @@ defineProps<{
onMessageDelete: (message: string, change: 'aii' | 'user') => void onMessageDelete: (message: string, change: 'aii' | 'user') => void
}>() }>()
const message = defineModel<string>('message', {required: true}) const message = defineModel<string>('message', { required: true })
const mode = defineModel<'continue' | 'expand'>('mode', {required: true}) const mode = defineModel<'continue' | 'expand'>('mode', { required: true })
</script> </script>
<template> <template>
@@ -26,17 +28,22 @@ const mode = defineModel<'continue' | 'expand'>('mode', {required: true})
v-if="content !== null" v-if="content !== null"
:is="createChatTableMessages(content, onMessageEdit, onMessageDelete)" :is="createChatTableMessages(content, onMessageEdit, onMessageDelete)"
/> />
<div v-if="aiiMessage !== null" class="user-message message" v-html="md.render(message)"/> <div v-if="aiiMessage !== null" class="user-message message" v-html="md.render(message)" />
<div v-if="aiiMessage !== null" class="aii-message-streaming message"> <div v-if="aiiMessage !== null" class="aii-message-streaming message">
<div class="thinking">{{ aiiThinking }}</div> <div class="thinking">{{ aiiThinking }}</div>
<div v-html="md.render(aiiMessage)"/> <div v-html="md.render(aiiMessage)" />
</div> </div>
</div> </div>
<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"
+17 -4
View File
@@ -10,11 +10,24 @@ defineProps<{
<template> <template>
<div class="chatroom-card"> <div class="chatroom-card">
<n-image v-if="infoMode" class="image" object-fit="cover" preview-disabled :src="feature_image" <n-image
v-if="infoMode"
class="image"
object-fit="cover"
preview-disabled
:src="feature_image"
width="140" width="140"
height="100"/> height="100"
<n-image v-else class="image" object-fit="cover" preview-disabled :src="feature_image" />
width="84" height="60"/> <n-image
v-else
class="image"
object-fit="cover"
preview-disabled
:src="feature_image"
width="84"
height="60"
/>
<div class="card-body"> <div class="card-body">
<n-text class="name">{{ name }}</n-text> <n-text class="name">{{ name }}</n-text>
<n-ellipsis :line-clamp="2" style="max-width: 100%" class="description"> <n-ellipsis :line-clamp="2" style="max-width: 100%" class="description">
@@ -1,42 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import {computed, ref, watch} from 'vue' import { useMessage } from 'naive-ui'
import type {ChatroomPublic} from '@/types/chatroom.js' import { ref } from 'vue'
import {api} from '@/tools/web.js'
import type {ReturnDto} from '@/types/response.js' import InputFile from '@/components/file/InputFile.vue'
import {useMessage} from 'naive-ui' import { api } from '@/tools/web.js'
import UploadFileModal from "@/components/file/UploadFileModal.vue"; import type { ChatroomPublic } from '@/types/chatroom.js'
import SelectFileModal from "@/components/file/SelectFileModal.vue"; import type { ReturnDto } from '@/types/response.js'
import type {UploadFileDto} from "@/types/user.js";
const MESSAGE = useMessage() const MESSAGE = useMessage()
const showModal = defineModel<boolean>('showModal', {required: true}) const showModal = defineModel<boolean>('showModal', { required: true })
const showSelectModal = ref(false)
const showUploadModal = ref(false)
const files = ref<UploadFileDto[]>([])
const selectFiles = ref<UploadFileDto[]>([])
const image_url = computed(() => selectFiles.value.at(0)?.download_url)
const createChatroomForm = ref<ChatroomPublic>({ const createChatroomForm = ref<ChatroomPublic>({
id: 0, id: 0,
name: '', name: '',
description: '', description: '',
feature_image: '', feature_image: '',
script_template_id: 0,
script_template_version: '',
}) })
watch(image_url, () => {
if (image_url.value) {
createChatroomForm.value.feature_image = image_url.value
}
})
async function loadFiles() {
return await api.get("/file/").then(res => files.value = res.data as UploadFileDto[])
}
function onSubmit() { function onSubmit() {
api api
.post('/chatroom/', JSON.stringify(createChatroomForm.value)) .post('/chatroom/', JSON.stringify(createChatroomForm.value))
@@ -56,8 +37,13 @@ function onSubmit() {
</script> </script>
<template> <template>
<n-modal v-model:show="showModal" preset="card" title="创建聊天室" content-scrollable <n-modal
style="width: 800px;"> v-model:show="showModal"
preset="card"
title="创建聊天室"
content-scrollable
style="width: 800px"
>
<n-form <n-form
:model="createChatroomForm" :model="createChatroomForm"
label-placement="left" label-placement="left"
@@ -65,27 +51,18 @@ function onSubmit() {
label-width="auto" label-width="auto"
> >
<n-form-item path="name" label="名称"> <n-form-item path="name" label="名称">
<n-input v-model:value="createChatroomForm.name"/> <n-input v-model:value="createChatroomForm.name" />
</n-form-item> </n-form-item>
<n-form-item path="description" label="简介"> <n-form-item path="description" label="简介">
<n-input type="textarea" v-model:value="createChatroomForm.description"/> <n-input type="textarea" v-model:value="createChatroomForm.description" />
</n-form-item> </n-form-item>
<n-form-item path="feature_image" label="特色图像"> <n-form-item path="feature_image" label="特色图像">
<n-flex style="width: 100%;" :wrap="false"> <input-file v-model:value="createChatroomForm.feature_image" />
<n-input v-model:value="createChatroomForm.feature_image"
placeholder="留空以使用默认图像"/>
<n-button secondary type="info" @click="showSelectModal = true;">选择</n-button>
<n-button secondary type="warning" @click="showUploadModal = true;">上传</n-button>
</n-flex>
</n-form-item> </n-form-item>
<n-form-item label="确认?"> <n-form-item label="确认?">
<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>
@@ -0,0 +1,56 @@
<script setup lang="ts">
import { useMessage } from 'naive-ui'
import InputFile from '@/components/file/InputFile.vue'
import { api } from '@/tools/web.ts'
import type { ChatroomPublic } from '@/types/chatroom.ts'
const MESSAGE = useMessage()
const showModal = defineModel('showModal', { required: true })
const { chatroom, reload } = defineProps<{
chatroom: ChatroomPublic
reload: () => void
}>()
function submit() {
api
.post(`/chatroom/${chatroom.id}/`, JSON.stringify(chatroom))
.then(() => {
MESSAGE.success('成功修改聊天室基本信息~')
showModal.value = false
reload() // 由调用方重新获取聊天室数据
})
.catch((err) => {
MESSAGE.error(`修改聊天室基本信息失败:${err}`)
})
}
</script>
<template>
<n-modal
v-model:show="showModal"
title="修改聊天室信息"
style="width: 800px; max-height: 600px"
content-scrollable
preset="card"
>
<n-form label-placement="left" label-align="right" label-width="auto" :model="chatroom">
<n-form-item label="聊天室名称" path="name">
<n-input v-model:value="chatroom.name" />
</n-form-item>
<n-form-item label="聊天室描述" path="description">
<n-input v-model:value="chatroom.description" type="textarea" :rows="3" />
</n-form-item>
<n-form-item label="特色图片" path="feature_image">
<input-file v-model:value="chatroom.feature_image" />
</n-form-item>
<n-form-item label="保存">
<n-button secondary type="primary" @click="submit()">保存</n-button>
</n-form-item>
</n-form>
</n-modal>
</template>
<style scoped></style>
+12 -12
View File
@@ -1,20 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import {api} from '@/tools/web.js' import { useMessage } from 'naive-ui'
import {ref, watch} from 'vue' import { ref, watch } from 'vue'
import {useRoute} from 'vue-router' import { useRoute } from 'vue-router'
import type {ChatScript} from '@/types/chatroom.js'
import type {ReturnDto} from '@/types/response.js'
import {useMessage} from 'naive-ui'
import XamlModal from '@/components/XamlModal.vue' import XamlModal from '@/components/XamlModal.vue'
import { api } from '@/tools/web.js'
import type { ChatScript } from '@/types/chatroom.js'
import type { ReturnDto } from '@/types/response.js'
const ROUTE = useRoute() const ROUTE = useRoute()
const MESSAGE = useMessage() const MESSAGE = useMessage()
const showDrawer = defineModel('showDrawer', {required: true}) const showDrawer = defineModel('showDrawer', { required: true })
const showXamlModal = ref(false) const showXamlModal = ref(false)
const {script} = defineProps<{ const { script } = defineProps<{
script: string script: string
}>() }>()
@@ -48,10 +49,9 @@ watch(
() => { () => {
try { try {
scriptForm.value = JSON.parse(script) as ChatScript scriptForm.value = JSON.parse(script) as ChatScript
} catch { } catch {}
}
}, },
{immediate: true}, { immediate: true },
) )
</script> </script>
@@ -127,7 +127,7 @@ watch(
</template> </template>
</n-drawer-content> </n-drawer-content>
<xaml-modal v-model:show-modal="showXamlModal"/> <xaml-modal v-model:show-modal="showXamlModal" />
</n-drawer> </n-drawer>
</template> </template>
+10 -8
View File
@@ -1,27 +1,29 @@
<script setup lang="ts"> <script setup lang="ts">
import type {UploadFileDto} from "@/types/user.js"; import { computed } from 'vue'
import {useNowUser} from "@/stores/now-user.js";
import {computed} from "vue"; import { useNowUser } from '@/stores/now-user.js'
import type { UploadFileDto } from '@/types/user.js'
const NOWUSER = useNowUser() const NOWUSER = useNowUser()
const {file} = defineProps<{ const { file } = defineProps<{
file: UploadFileDto file: UploadFileDto
}>() }>()
const is_you = computed(() => NOWUSER.id === file.uploader_id) const is_you = computed(() => NOWUSER.id === file.uploader_id)
const showModal = defineModel("showModal", {required: true}) const showModal = defineModel('showModal', { required: true })
</script> </script>
<template> <template>
<n-modal v-model:show="showModal" preset="card" style="width: 1000px;" title="文件信息"> <n-modal v-model:show="showModal" preset="card" style="width: 1000px" title="文件信息">
<div class="card-content"> <div class="card-content">
<n-image :width="500" :height="500" object-fit="contain" :src="file.download_url"/> <n-image :width="500" :height="500" object-fit="contain" :src="file.download_url" />
<div class="side"> <div class="side">
<n-h3>{{ file.original_name }}</n-h3> <n-h3>{{ file.original_name }}</n-h3>
<n-p>保存文件名{{ file.safe_name }}</n-p> <n-p>保存文件名{{ file.safe_name }}</n-p>
<n-p>上传用户ID{{ file.uploader_id }} <n-p
>上传用户ID{{ file.uploader_id }}
<n-tag v-if="is_you" type="primary"></n-tag> <n-tag v-if="is_you" type="primary"></n-tag>
</n-p> </n-p>
+13 -10
View File
@@ -1,9 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import type {UploadFileDto} from "@/types/user.js"; import { computed, onMounted, ref, useTemplateRef } from 'vue'
import {computed, onMounted, ref, useTemplateRef} from "vue";
import FileModal from "@/components/file/FileModal.vue";
const {file, size, enableSelect, onSelect, onRemove} = defineProps<{ import FileModal from '@/components/file/FileModal.vue'
import type { UploadFileDto } from '@/types/user.js'
const { file, size, enableSelect, onSelect, onRemove } = defineProps<{
file: UploadFileDto file: UploadFileDto
size: number size: number
enableSelect?: boolean enableSelect?: boolean
@@ -11,12 +12,12 @@ const {file, size, enableSelect, onSelect, onRemove} = defineProps<{
onRemove?: (file: UploadFileDto) => boolean onRemove?: (file: UploadFileDto) => boolean
}>() }>()
const th = useTemplateRef("th") const th = useTemplateRef('th')
const showModal = ref(false) const showModal = ref(false)
const selected = ref(false) const selected = ref(false)
const ALLOWED_EXTENSIONS = ["jpg", "jpeg", "png"] const ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png']
onMounted(() => { onMounted(() => {
if (ALLOWED_EXTENSIONS.includes(file.safe_name.split('.').at(-1)!.toLowerCase())) { if (ALLOWED_EXTENSIONS.includes(file.safe_name.split('.').at(-1)!.toLowerCase())) {
@@ -32,13 +33,13 @@ function onClick() {
if (selected.value && onRemove) { if (selected.value && onRemove) {
if (onRemove(file)) { if (onRemove(file)) {
selected.value = false selected.value = false
th.value?.classList.remove("selected") th.value?.classList.remove('selected')
console.log(`选中文件:${file.original_name}`) console.log(`选中文件:${file.original_name}`)
} }
} else if (!selected.value && onSelect) { } else if (!selected.value && onSelect) {
if (onSelect(file)) { if (onSelect(file)) {
selected.value = true selected.value = true
th.value?.classList.add("selected") th.value?.classList.add('selected')
console.log(`取消文件:${file.original_name}`) console.log(`取消文件:${file.original_name}`)
} }
} }
@@ -50,7 +51,7 @@ const size_px = computed(() => `${size}px`)
<template> <template>
<div class="file-thumbnail" ref="th" @click="onClick"></div> <div class="file-thumbnail" ref="th" @click="onClick"></div>
<file-modal :file v-model:show-modal="showModal"/> <file-modal :file v-model:show-modal="showModal" />
</template> </template>
<style scoped> <style scoped>
@@ -64,7 +65,9 @@ div.file-thumbnail {
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover; background-size: cover;
transition: border-color 0.3s, box-shadow 0.3s; transition:
border-color 0.3s,
box-shadow 0.3s;
} }
div.selected { div.selected {
+44
View File
@@ -0,0 +1,44 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import SelectFileModal from '@/components/file/SelectFileModal.vue'
import UploadFileModal from '@/components/file/UploadFileModal.vue'
import { api } from '@/tools/web.ts'
import type { UploadFileDto } from '@/types/user.ts'
const value = defineModel('value', { required: true })
const showSelectModal = ref(false)
const showUploadModal = ref(false)
const selectFiles = ref<UploadFileDto[]>([])
async function loadFiles() {
return await api.get('/file/').then((res) => res.data as UploadFileDto[])
}
watch(
() => selectFiles.value,
() => {
value.value = selectFiles.value[0]?.download_url
},
)
</script>
<template>
<n-flex style="width: 100%" :wrap="false">
<n-input v-model:value="value" placeholder="留空以使用默认图像" />
<n-button secondary type="info" @click="showSelectModal = true">选择</n-button>
<n-button secondary type="warning" @click="showUploadModal = true">上传</n-button>
</n-flex>
<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" />
</template>
<style scoped></style>
+28 -16
View File
@@ -1,29 +1,30 @@
<script setup lang="ts"> <script setup lang="ts">
import {selectFilesCom} from "@/components/file/upload-files.js"; import { useMessage } from 'naive-ui'
import {computed, ref, watch} from "vue"; import { computed, ref, watch } from 'vue'
import type {UploadFileDto} from "@/types/user.js";
import {useMessage} from "naive-ui"; import { selectFilesCom } from '@/components/file/upload-files.js'
import type { UploadFileDto } from '@/types/user.js'
const MESSAGE = useMessage() const MESSAGE = useMessage()
const {max, extensions, loadFiles} = defineProps<{ const { max, extensions, loadFiles } = defineProps<{
max: number max: number
extensions: string[] extensions: string[]
loadFiles: () => Promise<UploadFileDto[]> loadFiles: () => Promise<UploadFileDto[]>
}>() }>()
const showModal = defineModel("showModal", {required: true}) const showModal = defineModel('showModal', { required: true })
const files = ref<UploadFileDto[]>([]) const files = ref<UploadFileDto[]>([])
const tempFiles = ref<UploadFileDto[]>([]) const tempFiles = ref<UploadFileDto[]>([])
const selectFiles = defineModel<UploadFileDto[]>("selectFiles", {required: true}) const selectFiles = defineModel<UploadFileDto[]>('selectFiles', { required: true })
function selectFile(file: UploadFileDto) { function selectFile(file: UploadFileDto) {
if (tempFiles.value.length < max) { if (tempFiles.value.length < max) {
tempFiles.value.push(file) tempFiles.value.push(file)
return true return true
} else { } else {
MESSAGE.warning("可选择文件数量达到上限……") MESSAGE.warning('可选择文件数量达到上限……')
return false return false
} }
} }
@@ -41,27 +42,38 @@ watch(showModal, async () => {
files.value = await loadFiles() files.value = await loadFiles()
}) })
const tip_1 = computed(() => max > 1 ? `请选择至少 ${max} 个文件。` : "请选择一个文件。") const tip_1 = computed(() => (max > 1 ? `请选择至少 ${max} 个文件。` : '请选择一个文件。'))
const tip_2 = computed(() => `允许的文件类型:${extensions.join('、')}`) const tip_2 = computed(() => `允许的文件类型:${extensions.join('、')}`)
</script> </script>
<template> <template>
<n-modal preset="card" style="max-width: 600px; max-height: 600px;" title="选择文件" <n-modal
preset="card"
style="max-width: 600px; max-height: 600px"
title="选择文件"
content-scrollable content-scrollable
v-model:show="showModal"> v-model:show="showModal"
>
<n-flex vertical> <n-flex vertical>
<n-alert type="info"> <n-alert type="info">
{{ tip_1 }} {{ tip_1 }}
{{ tip_2 }} {{ tip_2 }}
</n-alert> </n-alert>
<component :is="selectFilesCom(files, selectFile, removeFile)"/> <component :is="selectFilesCom(files, selectFile, removeFile)" />
<n-button type="primary" secondary @click="selectFiles = tempFiles; showModal = false;"> <n-button
type="primary"
secondary
@click="
() => {
selectFiles = tempFiles
showModal = false
}
"
>
确认选择 确认选择
</n-button> </n-button>
</n-flex> </n-flex>
</n-modal> </n-modal>
</template> </template>
<style scoped> <style scoped></style>
</style>
+35 -23
View File
@@ -1,37 +1,38 @@
<script setup lang="ts"> <script setup lang="ts">
import {type UploadCustomRequestOptions, type UploadFileInfo} from "naive-ui"; import { type UploadCustomRequestOptions, type UploadFileInfo } from 'naive-ui'
import {api} from "@/tools/web.js"; import { shallowRef, useTemplateRef } from 'vue'
import type {UploadFileDto} from "@/types/user.js";
import {shallowRef, useTemplateRef} from "vue"; import { api } from '@/tools/web.js'
import type { UploadFileDto } from '@/types/user.js'
defineProps<{ defineProps<{
afterLeave?: () => void; afterLeave?: () => void
}>() }>()
const showModal = defineModel("showModal", {required: true}) const showModal = defineModel('showModal', { required: true })
const upload = useTemplateRef("upload") const upload = useTemplateRef('upload')
const fileList = shallowRef<UploadFileInfo[]>([]) const fileList = shallowRef<UploadFileInfo[]>([])
async function handle_upload({file, onFinish, onError, onProgress}: UploadCustomRequestOptions) { async function handle_upload({ file, onFinish, onError, onProgress }: UploadCustomRequestOptions) {
const formData = new FormData(); const formData = new FormData()
console.log(file.file) console.log(file.file)
formData.append("file", file.file!) formData.append('file', file.file!)
console.log(formData) console.log(formData)
try { try {
const data = await api.post("/file/upload/", formData, { const data = await api
.post('/file/upload/', formData, {
headers: { headers: {
'Content-Type': undefined // 取消全局默认的 application/json 很重要!!!!!!!! 'Content-Type': undefined, // 取消全局默认的 application/json 很重要!!!!!!!!
}, },
onUploadProgress: (progressEvent) => { onUploadProgress: (progressEvent) => {
const percent = Math.ceil( const percent = Math.ceil((progressEvent.loaded / progressEvent.total!) * 100)
(progressEvent.loaded / progressEvent.total!) * 100 onProgress({ percent }) // 更新进度条
) },
onProgress({percent}) // 更新进度条 })
} .then((res) => res.data as UploadFileDto)
}).then((res) => res.data as UploadFileDto)
file.url = data.download_url file.url = data.download_url
onFinish() onFinish()
@@ -47,12 +48,23 @@ function onUpload() {
</script> </script>
<template> <template>
<n-modal style="width: 600px;" preset="card" v-model:show="showModal" <n-modal
title="上传文件" content-scrollable style="width: 600px"
@after-leave="afterLeave"> preset="card"
v-model:show="showModal"
title="上传文件"
content-scrollable
@after-leave="afterLeave"
>
<n-flex vertical> <n-flex vertical>
<n-upload multiple ref="upload" :default-upload="false" list-type="image" <n-upload
:custom-request="handle_upload" v-model:file-list="fileList"> multiple
ref="upload"
:default-upload="false"
list-type="image"
:custom-request="handle_upload"
v-model:file-list="fileList"
>
<n-upload-dragger> <n-upload-dragger>
<n-p>拖拽文件到此区域可以快速上传</n-p> <n-p>拖拽文件到此区域可以快速上传</n-p>
</n-upload-dragger> </n-upload-dragger>
+23 -11
View File
@@ -1,30 +1,42 @@
import type {UploadFileDto} from "@/types/user.ts"; import { NEmpty, NFlex } from 'naive-ui'
import FileThumbnail from "@/components/file/FileThumbnail.vue";
import {NEmpty, NFlex} from "naive-ui"; import FileThumbnail from '@/components/file/FileThumbnail.vue'
import type { UploadFileDto } from '@/types/user.ts'
export function uploadFilesCom(files: UploadFileDto[]) { export function uploadFilesCom(files: UploadFileDto[]) {
if (files.length === 0) { if (files.length === 0) {
return <NEmpty description="你还没有上传任何文件。" size="large"/> return <NEmpty description="你还没有上传任何文件。" size="large" />
} }
return <NFlex> return (
<NFlex>
{files.map((file: UploadFileDto) => { {files.map((file: UploadFileDto) => {
return <FileThumbnail size={120} file={file}></FileThumbnail>; return <FileThumbnail size={120} file={file}></FileThumbnail>
})} })}
</NFlex> </NFlex>
)
} }
export function selectFilesCom( export function selectFilesCom(
files: UploadFileDto[], files: UploadFileDto[],
onSelect: (file: UploadFileDto) => boolean, onSelect: (file: UploadFileDto) => boolean,
onRemove: (file: UploadFileDto) => boolean onRemove: (file: UploadFileDto) => boolean,
) { ) {
if (files.length === 0) { if (files.length === 0) {
return <NEmpty description="你还没有上传任何文件。" size="large"/> return <NEmpty description="你还没有上传任何文件。" size="large" />
} }
return <NFlex> return (
<NFlex>
{files.map((file: UploadFileDto) => { {files.map((file: UploadFileDto) => {
return <FileThumbnail size={82} file={file} enableSelect onSelect={onSelect} return (
onRemove={onRemove}></FileThumbnail>; <FileThumbnail
size={82}
file={file}
enableSelect
onSelect={onSelect}
onRemove={onRemove}
></FileThumbnail>
)
})} })}
</NFlex> </NFlex>
)
} }
+3 -4
View File
@@ -1,11 +1,10 @@
import {createApp} from 'vue' import { createHead } from '@unhead/vue/client'
import {createPinia} from 'pinia' import { createPinia } from 'pinia'
import {createHead} from "@unhead/vue/client"; import { createApp } from 'vue'
import '@/assets/main.scss' import '@/assets/main.scss'
import '@/assets/beautiful.scss' import '@/assets/beautiful.scss'
import '@/assets/chat.scss' import '@/assets/chat.scss'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
+40 -37
View File
@@ -1,59 +1,60 @@
<script setup lang="ts"> <script setup lang="ts">
import UserAction from "@/components/admin/UserAction.vue"; import { useHead } from '@unhead/vue'
import type {MenuOption} from "naive-ui"; import type { MenuOption } from 'naive-ui'
import {computed, onMounted, ref, useTemplateRef} from "vue"; import { computed, onMounted, ref, useTemplateRef } from 'vue'
import {useRouter} from "vue-router"; import { useRouter } from 'vue-router'
import {useNowUser} from "@/stores/now-user.js";
import {useHead} from "@unhead/vue"; import UserAction from '@/components/admin/UserAction.vue'
import { useNowUser } from '@/stores/now-user.js'
useHead({ useHead({
titleTemplate: "%s | 管理面板 | NayHome" titleTemplate: '%s | 管理面板 | NayHome',
}) })
const ROUTER = useRouter() const ROUTER = useRouter()
const NOWUSER = useNowUser() const NOWUSER = useNowUser()
const menu = useTemplateRef("menu") const menu = useTemplateRef('menu')
const selectOption = ref("") const selectOption = ref('')
const options = computed<MenuOption[]>(() => [ const options = computed<MenuOption[]>(() => [
{ {
label: "总览", label: '总览',
key: "", key: '',
}, },
{ {
label: "用户", label: '用户',
key: "user-basic", key: 'user-basic',
children: [ children: [
{ {
label: "资料", label: '资料',
key: "user-info" key: 'user-info',
}, },
{ {
label: "安全", label: '安全',
key: "user-security" key: 'user-security',
} },
] ],
}, },
{ {
label: "内容", label: '内容',
key: "user-creation", key: 'user-creation',
children: [ children: [
{ {
label: "上传", label: '上传',
key: "user-upload" key: 'user-upload',
}, },
{ {
label: "剧本", label: '剧本',
key: "user-script" key: 'user-script',
} },
] ],
}, },
{ {
label: "NyaHome 管理后台", label: 'NyaHome 管理后台',
key: "nyahome", key: 'nyahome',
show: NOWUSER.is_admin, show: NOWUSER.is_admin,
} },
]) ])
function handleMenuClick(key: string) { function handleMenuClick(key: string) {
@@ -61,12 +62,14 @@ function handleMenuClick(key: string) {
} }
onMounted(() => { onMounted(() => {
const key = ROUTER.currentRoute.value.fullPath.replace("/admin/", "") const key = ROUTER.currentRoute.value.fullPath.replace('/admin/', '')
if (key) { if (key.endsWith('/admin')) {
selectOption.value = ''
} else if (key) {
selectOption.value = key selectOption.value = key
menu.value?.showOption(key) menu.value?.showOption(key)
} else { } else {
selectOption.value = "" selectOption.value = ''
} }
}) })
</script> </script>
@@ -74,16 +77,16 @@ onMounted(() => {
<template> <template>
<div id="user-page"> <div id="user-page">
<div id="user-page-sidebar"> <div id="user-page-sidebar">
<user-action/> <user-action />
<div class="nyahome-card"> <div class="nyahome-card">
<n-menu ref="menu" v-model:value="selectOption" :options @update:value="handleMenuClick"/> <n-menu ref="menu" v-model:value="selectOption" :options @update:value="handleMenuClick" />
</div> </div>
</div> </div>
<router-view v-slot="{Component}"> <router-view v-slot="{ Component }">
<div id="user-page-content"> <div id="user-page-content">
<keep-alive> <keep-alive>
<component :is="Component"/> <component :is="Component" />
</keep-alive> </keep-alive>
</div> </div>
</router-view> </router-view>
+52 -38
View File
@@ -1,27 +1,33 @@
<script setup lang="ts"> <script setup lang="ts">
import {useRoute} from 'vue-router' import { fetchEventSource } from '@microsoft/fetch-event-source'
import {onMounted, ref, useTemplateRef, watch} from 'vue' import { useHead } from '@unhead/vue'
import {api} from '@/tools/web.ts' import { useMessage } from 'naive-ui'
import type {ReturnDto} from '@/types/response.ts' import { onMounted, reactive, ref, useTemplateRef, watch } from 'vue'
import type {Chatroom} from '@/types/chatroom.ts' import { useRoute } from 'vue-router'
import {useMessage} from 'naive-ui'
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 ChatControlPanel from '@/components/chatroom/ChatControlPanel.vue' import { api } from '@/tools/web.ts'
import {fetchEventSource} from '@microsoft/fetch-event-source' import type { AiiModelPublic, AiiTokenInfo } from '@/types/aii.ts'
import type {AiiTokenInfo} from '@/types/aii.ts' import type { Chatroom } from '@/types/chatroom.ts'
import {SEE_YOU_TOMORROW} from '@/types/syt.ts' import type { ReturnDto } from '@/types/response.ts'
import { SEE_YOU_TOMORROW } from '@/types/syt.ts'
const pageHead = reactive({
title: '正在加载聊天室...',
titleTemplate: '%s | 聊天室 | NyaHome',
})
useHead(pageHead)
const ROUTE = useRoute() const ROUTE = useRoute()
const MESSAGE = useMessage() const MESSAGE = useMessage()
const crName = ref('') const chatroom = ref<Chatroom | null>(null)
const crDescription = ref('')
const crFeatureImage = ref('')
const crContent = ref('')
const crScript = ref('')
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')
@@ -45,11 +51,8 @@ function load() {
} }
}) })
.then((cr) => { .then((cr) => {
crName.value = cr.name pageHead.title = cr.name
crDescription.value = cr.description chatroom.value = cr
crFeatureImage.value = cr.feature_image
crContent.value = cr.content
crScript.value = cr.script
}) })
.catch((e) => { .catch((e) => {
MESSAGE.error(`访问聊天室失败:${e}`) MESSAGE.error(`访问聊天室失败:${e}`)
@@ -65,7 +68,7 @@ watch(
) )
function chat() { function chat() {
if (!selectedModel.value) { if (!selectedModelId.value) {
MESSAGE.warning('未选择模型,无法开始创作喵!') MESSAGE.warning('未选择模型,无法开始创作喵!')
return return
} }
@@ -83,12 +86,13 @@ function chat() {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${localStorage.getItem('access-token')}`,
}, },
body: JSON.stringify({ body: JSON.stringify({
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 会导致焦点转移时流式传输中断然后重连,很怪
@@ -145,7 +149,7 @@ function accept() {
} }
}) })
.then((result) => { .then((result) => {
crContent.value = result.content chatroom.value!.content = result.content
}) })
.catch((err) => { .catch((err) => {
MESSAGE.error(`保存失败:${err}`) MESSAGE.error(`保存失败:${err}`)
@@ -180,8 +184,8 @@ function messageEdit(oldMessage: string, newMessage: string, change: 'aii' | 'us
} }
}) })
.then((result) => { .then((result) => {
crContent.value = result.content chatroom.value!.content = result.content
MESSAGE.success('聊天记录已删除,页面已更新~') MESSAGE.success('聊天记录已更新,页面已更新~')
}) })
.catch((err) => { .catch((err) => {
MESSAGE.error(`修改聊天消息失败:${err}`) MESSAGE.error(`修改聊天消息失败:${err}`)
@@ -191,7 +195,7 @@ function messageEdit(oldMessage: string, newMessage: string, change: 'aii' | 'us
function messageDelete(message: string, change: 'aii' | 'user') { function messageDelete(message: string, change: 'aii' | 'user') {
const id = Number(ROUTE.params.id) const id = Number(ROUTE.params.id)
api api
.post(`/chatroom/${id}/chat/delete/`, JSON.stringify({message, change})) .post(`/chatroom/${id}/chat/delete/`, JSON.stringify({ message, change }))
.then((res) => res.data as ReturnDto) .then((res) => res.data as ReturnDto)
.then((data) => { .then((data) => {
if (data.success) { if (data.success) {
@@ -201,7 +205,7 @@ function messageDelete(message: string, change: 'aii' | 'user') {
} }
}) })
.then((result) => { .then((result) => {
crContent.value = result.content chatroom.value!.content = result.content
MESSAGE.success('聊天记录已删除,页面已更新~') MESSAGE.success('聊天记录已删除,页面已更新~')
}) })
.catch((err) => { .catch((err) => {
@@ -233,15 +237,20 @@ function enableSidebar() {
</script> </script>
<template> <template>
<div class="page-container"> <div class="page-container" v-if="chatroom !== null">
<div class="main-column"> <div class="main-column">
<chatroom-card :id="Number(ROUTE.params.id)" :name="crName" :description="crDescription" <chatroom-card
:feature_image="crFeatureImage"/> :id="Number(ROUTE.params.id)"
:name="chatroom.name"
:description="chatroom.description"
:feature_image="chatroom.feature_image"
/>
<chat-table <chat-table
:content="crContent" :content="chatroom.content"
: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"
@@ -251,15 +260,17 @@ function enableSidebar() {
:on-message-edit="messageEdit" :on-message-edit="messageEdit"
:on-message-delete="messageDelete" :on-message-delete="messageDelete"
/> />
<div id="main-toggle" ref="main-toggle" @click="enableSidebar"/> <div id="main-toggle" ref="main-toggle" @click="enableSidebar" />
</div> </div>
<div class="sidebar-column" ref="sidebar"> <div class="sidebar-column" ref="sidebar">
<chat-control-panel <chat-control-panel
:script="crScript" :chatroom="chatroom"
: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" />
</div> </div>
</div> </div>
</template> </template>
@@ -297,7 +308,8 @@ div.page-container {
border-radius: 5px; border-radius: 5px;
opacity: var(--opacity); opacity: var(--opacity);
transition: background-color 0.8s, transition:
background-color 0.8s,
transform 0.5s, transform 0.5s,
opacity 1s, opacity 1s,
height 0.5s, height 0.5s,
@@ -326,7 +338,8 @@ div.page-container {
position: relative; position: relative;
transition: transform 1s, transition:
transform 1s,
opacity 1s, opacity 1s,
flex-basis 1s; flex-basis 1s;
transform: translateX(var(--transform-x)); transform: translateX(var(--transform-x));
@@ -343,7 +356,8 @@ div.page-container {
background: rgb(0 0 0 / 0.1); background: rgb(0 0 0 / 0.1);
border-radius: 5px; border-radius: 5px;
transition: background-color 0.8s, transition:
background-color 0.8s,
transform 0.5s, transform 0.5s,
height 0.5s, height 0.5s,
width 0.5s, width 0.5s,
+18 -8
View File
@@ -1,11 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { useHead } from '@unhead/vue'
import { ref, watch } from 'vue'
import ChatroomCard from '@/components/chatroom/ChatroomCard.vue' import ChatroomCard from '@/components/chatroom/ChatroomCard.vue'
import {ref, watch} from 'vue'
import {api} from '@/tools/web.ts'
import type {ChatroomPublic} from '@/types/chatroom.ts'
import type {ReturnDto} from '@/types/response.ts'
import ChatroomCreatorModal from '@/components/chatroom/ChatroomCreatorModal.vue' import ChatroomCreatorModal from '@/components/chatroom/ChatroomCreatorModal.vue'
import {useNowUser} from "@/stores/now-user.ts"; import { useNowUser } from '@/stores/now-user.ts'
import { api } from '@/tools/web.ts'
import type { ChatroomPublic } from '@/types/chatroom.ts'
import type { ReturnDto } from '@/types/response.ts'
useHead({
title: '聊天室列表',
})
const NOWUSER = useNowUser() const NOWUSER = useNowUser()
@@ -23,9 +29,13 @@ function load() {
}) })
} }
watch(() => NOWUSER.isLogin, () => { watch(
() => NOWUSER.is_login,
() => {
load() load()
}, {immediate: true}) },
{ immediate: true },
)
</script> </script>
<template> <template>
@@ -51,7 +61,7 @@ watch(() => NOWUSER.isLogin, () => {
/> />
</div> </div>
<chatroom-creator-modal v-model:show-modal="showModal"/> <chatroom-creator-modal v-model:show-modal="showModal" />
</template> </template>
<style scoped> <style scoped>
+39
View File
@@ -0,0 +1,39 @@
<script setup lang="ts">
import { useHead } from '@unhead/vue'
import InDev from '@/components/InDev.vue'
useHead({
title: '剧本市场',
})
</script>
<template>
<div class="marketplace-page">
<div class="marketplace-header-card"></div>
<in-dev />
</div>
</template>
<style scoped>
div.marketplace-page {
min-width: min(1200px, 90%);
width: min(1200px, 90%);
min-height: 0;
padding: 6px 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
div.marketplace-header-card {
background-image: url('/nyahome/normal-header.png');
background-repeat: no-repeat;
background-size: cover;
background-position: center;
border-radius: 12px;
width: 100%;
height: 500px;
}
</style>
+18 -3
View File
@@ -1,19 +1,34 @@
<script setup lang="ts"> <script setup lang="ts">
import { useHead } from '@unhead/vue'
import UserAction from '@/components/admin/UserAction.vue' import UserAction from '@/components/admin/UserAction.vue'
useHead({
title: '首页',
})
</script> </script>
<template> <template>
<n-flex vertical style="padding: 6px 20px"> <div class="welcome-page">
<n-flex> <n-flex>
<n-card class="welcome-card" title="Welcome to Welcome!"></n-card> <n-card class="welcome-card" title="Welcome to Welcome!"></n-card>
<div class="user-action-card"> <div class="user-action-card">
<user-action/> <user-action />
</div> </div>
</n-flex> </n-flex>
</n-flex> </div>
</template> </template>
<style scoped> <style scoped>
div.welcome-page {
min-width: min(1200px, 90%);
width: min(1200px, 90%);
min-height: 0;
padding: 6px 20px;
display: flex;
flex-direction: column;
}
.welcome-card { .welcome-card {
flex: 1; flex: 1;
} }
+108 -64
View File
@@ -1,73 +1,79 @@
<script setup lang="ts"> <script setup lang="ts">
import ConfigCard from "@/components/admin/ConfigCard.vue"; import { useHead } from '@unhead/vue'
import {useHead} from "@unhead/vue"; import { useMessage } from 'naive-ui'
import {ref} from "vue"; import { ref } from 'vue'
import {api} from "@/tools/web.ts";
import InDev from "@/components/InDev.vue"; import ConfigCard from '@/components/admin/ConfigCard.vue'
import {useMessage} from "naive-ui"; import InDev from '@/components/InDev.vue'
import type {ReturnDto} from "@/types/response.ts"; import AdminAii from '@/pages/nyahome/AdminAii.vue'
import { api } from '@/tools/web.ts'
import type { ReturnDto } from '@/types/response.ts'
interface SiteConfig { interface SiteConfig {
site_name: string; site_name: string
site_url: string; site_url: string
backend_url: string; backend_url: string
jwt_secret_key: string; default_user_avatar_url: string
default_user_background_url: string
default_chatroom_script_cover_url: string
default_page_cover_url: string
smtp_enable: boolean; jwt_secret_key: string
smtp_sender: string; allow_upload_file_extensions: string[]
smtp_hostname: string;
smtp_port: number; smtp_enable: boolean
smtp_username: string; smtp_sender: string
smtp_password: string; smtp_hostname: string
smtp_use_tls: boolean; smtp_port: number
smtp_username: string
smtp_password: string
smtp_use_tls: boolean
} }
const MESSAGE = useMessage() const MESSAGE = useMessage()
useHead({ useHead({
title: "NyaHome 管理后台" title: 'NyaHome 管理后台',
}) })
const siteConfig = ref<SiteConfig | null>(null); const siteConfig = ref<SiteConfig | null>(null)
function getConfig() { function getConfig() {
api.get("/admin/site_config/") api.get('/admin/site_config/').then((res) => {
.then((res) => {
siteConfig.value = res.data as SiteConfig siteConfig.value = res.data as SiteConfig
MESSAGE.success("成功获取设置~") MESSAGE.success('成功获取设置~')
}) })
} }
function saveConfig() { function saveConfig() {
api.post("/admin/site_config/", JSON.stringify(siteConfig.value)) api.post('/admin/site_config/', JSON.stringify(siteConfig.value)).then((res) => {
.then((res) => {
siteConfig.value = res.data as SiteConfig siteConfig.value = res.data as SiteConfig
MESSAGE.success("保存并刷新设置成功~") MESSAGE.success('保存并刷新设置成功~')
}) })
} }
const testMailTo = ref("25565@qq.com") const testMailTo = ref('25565@qq.com')
function sendTestMail() { function sendTestMail() {
api.post("/admin/email-test/", JSON.stringify({to: testMailTo.value})) api
.then(res => res.data as ReturnDto) .post('/admin/email-test/', JSON.stringify({ to: testMailTo.value }))
.then(data => data.success) .then((res) => res.data as ReturnDto)
.then(success => { .then((data) => data.success)
.then((success) => {
if (success) { if (success) {
MESSAGE.success("邮件发送成功,请稍等片刻,然后检查收件箱~") MESSAGE.success('邮件发送成功,请稍等片刻,然后检查收件箱~')
} else { } else {
MESSAGE.error("后端表示邮件发送失败,请检查日志输出。") MESSAGE.error('后端表示邮件发送失败,请检查日志输出。')
} }
}) })
} }
</script> </script>
<template> <template>
<n-card> <n-card>
<template #header> <template #header>
<n-h3 prefix="bar" style="margin: 0;">NyaHome 管理后台</n-h3> <n-h3 prefix="bar" style="margin: 0">NyaHome 管理后台</n-h3>
</template> </template>
<template #header-extra> <template #header-extra>
<n-flex> <n-flex>
@@ -80,50 +86,79 @@ function sendTestMail() {
<n-tabs type="card" v-if="siteConfig !== null"> <n-tabs type="card" v-if="siteConfig !== null">
<n-tab-pane name="user" tab="用户" display-directive="show"> <n-tab-pane name="user" tab="用户" display-directive="show">
<config-card title="全部用户"> <config-card title="全部用户">
<in-dev/> <in-dev />
</config-card> </config-card>
</n-tab-pane> </n-tab-pane>
<n-tab-pane name="chatroom" tab="聊天室" display-directive="show"> <n-tab-pane name="chatroom" tab="聊天室" display-directive="show">
<config-card title="全部聊天室"> <config-card title="全部聊天室">
<in-dev/> <in-dev />
</config-card> </config-card>
</n-tab-pane> </n-tab-pane>
<n-tab-pane name="script" tab="剧本" display-directive="show"> <n-tab-pane name="script" tab="剧本" display-directive="show">
<config-card title="全部剧本"> <config-card title="全部剧本">
<in-dev/> <in-dev />
</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="基本信息">
<n-form> <n-form>
<n-form-item label="站点名称"> <n-form-item label="站点名称">
<n-input v-model:value="siteConfig.site_name"/> <n-input v-model:value="siteConfig.site_name" />
</n-form-item>
<n-form-item label="站点地址">
<n-input v-model:value="siteConfig.site_url"/>
</n-form-item> </n-form-item>
<n-alert type="info" class="in-form-alert"> <n-alert type="info" class="in-form-alert">
如果您需要将 NyaHome 的前后端分开部署则需要在此设置后端地址您需要自行处理跨域问题 您在浏览器中访问本站点时使用的地址主机名/域名
如果需要前后端分开部署则是前端所在地址<br />
就默认而言这两个地址应当一致
</n-alert>
<n-form-item label="站点地址">
<n-input v-model:value="siteConfig.site_url" />
</n-form-item>
<n-alert type="info" class="in-form-alert">
如果您需要将 NyaHome
的前后端分开部署则需要在此设置后端地址您需要自行处理跨域问题
</n-alert> </n-alert>
<n-form-item label="FastAPI 后端地址"> <n-form-item label="FastAPI 后端地址">
<n-input v-model:value="siteConfig.backend_url"/> <n-input v-model:value="siteConfig.backend_url" />
</n-form-item> </n-form-item>
</n-form> </n-form>
</config-card> </config-card>
<config-card title="搜索引擎设置与 SEO"> <config-card title="搜索引擎设置与 SEO">
<in-dev/> <in-dev />
</config-card> </config-card>
</n-flex> </n-flex>
</n-tab-pane> </n-tab-pane>
<n-tab-pane name="custom" tab="站点定制" display-directive="show">
<config-card title="默认图片">
<n-form>
<n-form-item label="默认用户头像">
<n-input v-model:value="siteConfig.default_user_avatar_url" />
</n-form-item>
<n-form-item label="默认用户背景">
<n-input v-model:value="siteConfig.default_user_background_url" />
</n-form-item>
<n-form-item label="默认聊天室和剧本封面">
<n-input v-model:value="siteConfig.default_chatroom_script_cover_url" />
</n-form-item>
<n-form-item label="默认页面封面">
<n-input v-model:value="siteConfig.default_page_cover_url" />
</n-form-item>
</n-form>
</config-card>
</n-tab-pane>
<n-tab-pane name="permission" tab="权限设置" display-directive="show"> <n-tab-pane name="permission" tab="权限设置" display-directive="show">
<config-card title="用户权限"> <config-card title="用户权限">
<in-dev/> <in-dev />
</config-card> </config-card>
</n-tab-pane> </n-tab-pane>
@@ -132,11 +167,23 @@ function sendTestMail() {
<config-card title="JWT"> <config-card title="JWT">
<n-form> <n-form>
<n-alert type="info" class="in-form-alert"> <n-alert type="info" class="in-form-alert">
JWTJson Web Token签名需要一个密钥你可以手动提供一个或者自行生成一个<br/> JWTJson Web Token签名需要一个密钥你可以手动提供一个或者自行生成一个<br />
修改此密钥会导致所有用户的登录状态丢失你也会请一次性设置一个足够安全的 修改此密钥会导致所有用户的登录状态丢失你也会请一次性设置一个足够安全的
</n-alert> </n-alert>
<n-form-item label="JWT 密钥"> <n-form-item label="JWT 密钥">
<n-input v-model:value="siteConfig.jwt_secret_key"/> <n-input
v-model:value="siteConfig.jwt_secret_key"
type="password"
show-password-toggle
/>
</n-form-item>
</n-form>
</config-card>
<config-card title="文件上传">
<n-form>
<n-form-item label="允许上传的文件类型(拓展名)">
<n-dynamic-tags v-model:value="siteConfig.allow_upload_file_extensions" type="info" />
</n-form-item> </n-form-item>
</n-form> </n-form>
</config-card> </config-card>
@@ -148,37 +195,38 @@ function sendTestMail() {
<config-card title="邮件 SMTP"> <config-card title="邮件 SMTP">
<n-form> <n-form>
<n-alert type="info" class="in-form-alert"> <n-alert type="info" class="in-form-alert">
NayHome 无法自己发送邮件需要配置 SMTP 服务<br/> NayHome 无法自己发送邮件需要配置 SMTP 服务<br />
或者你也可以关闭邮件功能当然芒果还是建议你配置一下的 或者你也可以关闭邮件功能当然芒果还是建议你配置一下的
</n-alert> </n-alert>
<n-form-item label="启用邮件功能(SMTP"> <n-form-item label="启用邮件功能(SMTP">
<n-switch v-model:value="siteConfig.smtp_enable"/> <n-switch v-model:value="siteConfig.smtp_enable" />
</n-form-item> </n-form-item>
<n-form-item label="发件人邮件地址"> <n-form-item label="发件人邮件地址">
<n-input v-model:value="siteConfig.smtp_sender"/> <n-input v-model:value="siteConfig.smtp_sender" />
</n-form-item> </n-form-item>
<n-form-item label="SMTP 主机名"> <n-form-item label="SMTP 主机名">
<n-input v-model:value="siteConfig.smtp_hostname"/> <n-input v-model:value="siteConfig.smtp_hostname" />
</n-form-item> </n-form-item>
<n-form-item label="SMTP 端口"> <n-form-item label="SMTP 端口">
<n-input-number v-model:value="siteConfig.smtp_port"/> <n-input-number v-model:value="siteConfig.smtp_port" />
</n-form-item> </n-form-item>
<n-form-item label="SMTP 用户名"> <n-form-item label="SMTP 用户名">
<n-input v-model:value="siteConfig.smtp_username"/> <n-input v-model:value="siteConfig.smtp_username" />
</n-form-item> </n-form-item>
<n-form-item label="SMTP 密码(一般应当是一个独立的应用程序密码)"> <n-form-item label="SMTP 密码(一般应当是一个独立的应用程序密码)">
<n-input v-model:value="siteConfig.smtp_password"/> <n-input v-model:value="siteConfig.smtp_password" />
</n-form-item> </n-form-item>
<n-form-item label="使用 TLS/SSL 加密"> <n-form-item label="使用 TLS/SSL 加密">
<n-switch v-model:value="siteConfig.smtp_use_tls"/> <n-switch v-model:value="siteConfig.smtp_use_tls" />
</n-form-item> </n-form-item>
</n-form> </n-form>
<template #action> <template #action>
<n-flex vertical> <n-flex vertical>
<n-text>你可以在这里测试 NayHome 的邮件系统能否使用上述 SMTP <n-text
>你可以在这里测试 NayHome 的邮件系统能否使用上述 SMTP
设置工作这会发送一封测试邮件 设置工作这会发送一封测试邮件
</n-text> </n-text>
<n-input v-model:value="testMailTo"/> <n-input v-model:value="testMailTo" />
<n-button secondary type="warning" @click="sendTestMail()">发送测试邮件</n-button> <n-button secondary type="warning" @click="sendTestMail()">发送测试邮件</n-button>
</n-flex> </n-flex>
</template> </template>
@@ -187,11 +235,7 @@ function sendTestMail() {
</n-tab-pane> </n-tab-pane>
</n-tabs> </n-tabs>
<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>
+5 -4
View File
@@ -1,10 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import {useNowUser} from "@/stores/now-user.js"; import { useHead } from '@unhead/vue'
import {computed} from "vue"; import { computed } from 'vue'
import {useHead} from "@unhead/vue";
import { useNowUser } from '@/stores/now-user.js'
useHead({ useHead({
title: "总览" title: '总览',
}) })
const NOWUSER = useNowUser() const NOWUSER = useNowUser()
+13 -8
View File
@@ -1,11 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { useNowUser } from '@/stores/now-user.js' import { useHead } from '@unhead/vue'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import ChangeEmailModal from '@/components/admin/ChangeEmailModal.vue'
import ChangePhoneModal from '@/components/admin/ChangePhoneModal.vue'
import SelectFileModal from '@/components/file/SelectFileModal.vue' import SelectFileModal from '@/components/file/SelectFileModal.vue'
import { useNowUser } from '@/stores/now-user.js'
import { api } from '@/tools/web.js' import { api } from '@/tools/web.js'
import type { UploadFileDto, UserDto } from '@/types/user.js' import type { UploadFileDto, UserDto } from '@/types/user.js'
import { useHead } from '@unhead/vue'
import ChangeEmailModal from '@/components/admin/ChangeEmailModal.vue'
useHead({ useHead({
title: '用户资料', title: '用户资料',
@@ -64,7 +66,7 @@ watch(
) )
watch( watch(
() => NOWUSER.isLogin, () => NOWUSER.is_login,
() => { () => {
reInitForm() reInitForm()
}, },
@@ -146,10 +148,12 @@ async function save() {
</n-form> </n-form>
<n-flex> <n-flex>
<n-button class="ui-button" type="primary" @click="save">保存</n-button> <n-button class="ui-button" type="primary" @click="save">保存</n-button>
<n-button class="ui-button" type="warning" @click="showChangeEmailModal = true" <n-button class="ui-button" type="warning" secondary @click="showChangeEmailModal = true">
>更改邮箱</n-button 更改邮箱
> </n-button>
<n-button class="ui-button" type="warning">更改手机号</n-button> <n-button class="ui-button" type="warning" secondary @click="showChangePhoneModal = true">
更改手机号
</n-button>
<n-button class="ui-button" type="tertiary">重置全部</n-button> <n-button class="ui-button" type="tertiary">重置全部</n-button>
</n-flex> </n-flex>
</div> </div>
@@ -169,6 +173,7 @@ async function save() {
v-model:select-files="background_selectFiles" v-model:select-files="background_selectFiles"
/> />
<change-email-modal v-model:show-modal="showChangeEmailModal" /> <change-email-modal v-model:show-modal="showChangeEmailModal" />
<change-phone-modal v-model:show-modal="showChangePhoneModal" />
</n-card> </n-card>
</template> </template>
+6 -5
View File
@@ -1,19 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import InDev from "@/components/InDev.vue"; import { useHead } from '@unhead/vue'
import {useHead} from "@unhead/vue";
import InDev from '@/components/InDev.vue'
useHead({ useHead({
title: "剧本" title: '剧本',
}) })
</script> </script>
<template> <template>
<n-card> <n-card>
<template #header> <template #header>
<n-h3 prefix="bar" style="margin: 0;">个人剧本库</n-h3> <n-h3 prefix="bar" style="margin: 0">个人剧本库</n-h3>
</template> </template>
<in-dev/> <in-dev />
</n-card> </n-card>
</template> </template>
+39 -43
View File
@@ -1,14 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import {useNowUser} from "@/stores/now-user.js"; import { useHead } from '@unhead/vue'
import UserPasswordModal from "@/components/admin/UserPasswordModal.vue"; import { type DataTableColumn, NTag, NText } from 'naive-ui'
import {h, ref} from "vue"; import { h, ref } from 'vue'
import {api} from "@/tools/web.ts";
import {type DataTableColumn, NTag, NText} from "naive-ui"; import UserPasswordModal from '@/components/admin/UserPasswordModal.vue'
import InDev from "@/components/InDev.vue"; import InDev from '@/components/InDev.vue'
import {useHead} from "@unhead/vue"; import { useNowUser } from '@/stores/now-user.js'
import { api } from '@/tools/web.ts'
useHead({ useHead({
title: "用户安全" title: '用户安全',
}) })
const NOWUSER = useNowUser() const NOWUSER = useNowUser()
@@ -16,8 +17,8 @@ const NOWUSER = useNowUser()
const showPasswordModal = ref(false) const showPasswordModal = ref(false)
interface SecureChange { interface SecureChange {
created_at: number; created_at: number
type: "login" | "change_password" | "change_email" | "change_phone" type: 'login' | 'change_password' | 'change_email' | 'change_phone'
old: string | null old: string | null
new: string | null new: string | null
} }
@@ -26,56 +27,53 @@ const secureChanges = ref<SecureChange[]>([])
const columns = [ const columns = [
{ {
title: "记录时间", title: '记录时间',
key: "created_at", key: 'created_at',
render(row) { render(row) {
const date = new Date(row.created_at * 1000) const date = new Date(row.created_at * 1000)
return h( return h(NText, {}, { default: () => date.toLocaleString() })
NText, },
{},
{default: () => date.toLocaleString()}
)
}
}, },
{ {
title: "类型", title: '类型',
key: "type", key: 'type',
render: (row) => { render: (row) => {
return h( return h(
NTag, NTag,
{ {
type: "info" type: 'info',
}, },
{default: () => row.type} { default: () => row.type },
) )
} },
}, },
{ {
title: "事件之前", title: '事件之前',
key: "old", key: 'old',
}, },
{ {
title: "事件之后", title: '事件之后',
key: "new", key: 'new',
} },
] as DataTableColumn<SecureChange>[] ] as DataTableColumn<SecureChange>[]
function loadSecureChanges() { function loadSecureChanges() {
api.get("/admin/me/secure_changes/") api
.then(res => secureChanges.value = res.data as SecureChange[]) .get('/admin/me/secure_changes/')
.then((res) => (secureChanges.value = res.data as SecureChange[]))
} }
</script> </script>
<template> <template>
<n-card> <n-card>
<template #header> <template #header>
<n-h3 prefix="bar" style="margin: 0;">密码</n-h3> <n-h3 prefix="bar" style="margin: 0">密码</n-h3>
</template> </template>
<n-flex vertical> <n-flex vertical>
<n-alert type="warning" v-if="NOWUSER.id === 1"> <n-alert type="warning" v-if="NOWUSER.id === 1">
您正在使用 NyaHome 初始化时创建的管理员账号此账号的默认密码为 admin 您正在使用 NyaHome 初始化时创建的管理员账号此账号的默认密码为 admin
<strong>您应该及时修改默认密码</strong><br/> <strong>您应该及时修改默认密码</strong><br />
如果您已修改密码请忽略 如果您已修改密码请忽略
</n-alert> </n-alert>
<n-flex> <n-flex>
@@ -84,27 +82,27 @@ function loadSecureChanges() {
</n-flex> </n-flex>
</n-flex> </n-flex>
<user-password-modal v-model:show-modal="showPasswordModal"/> <user-password-modal v-model:show-modal="showPasswordModal" />
</n-card> </n-card>
<n-card> <n-card>
<template #header> <template #header>
<n-h3 prefix="bar" style="margin: 0;">其他登录方式</n-h3> <n-h3 prefix="bar" style="margin: 0">其他登录方式</n-h3>
</template> </template>
<n-flex vertical> <n-flex vertical>
<n-alert type="info"> <n-alert type="info">
在这里连接第三方账户之后可以使用它们进行登录<br/> 在这里连接第三方账户之后可以使用它们进行登录<br />
<strong>必须先在这里连接后才能使用第三方账户进行登录</strong> <strong>必须先在这里连接后才能使用第三方账户进行登录</strong>
</n-alert> </n-alert>
<in-dev/> <in-dev />
</n-flex> </n-flex>
</n-card> </n-card>
<n-card> <n-card>
<template #header> <template #header>
<n-h3 prefix="bar" style="margin: 0;">两步验证</n-h3> <n-h3 prefix="bar" style="margin: 0">两步验证</n-h3>
</template> </template>
<n-flex vertical> <n-flex vertical>
@@ -112,21 +110,19 @@ function loadSecureChanges() {
启用两步验证可以更好地保护您的账户这会强制此账号在登录时进行额外验证 启用两步验证可以更好地保护您的账户这会强制此账号在登录时进行额外验证
</n-alert> </n-alert>
<in-dev/> <in-dev />
</n-flex> </n-flex>
</n-card> </n-card>
<n-card> <n-card>
<template #header> <template #header>
<n-h3 prefix="bar" style="margin: 0;">安全事件记录</n-h3> <n-h3 prefix="bar" style="margin: 0">安全事件记录</n-h3>
</template> </template>
<n-flex vertical> <n-flex vertical>
<n-data-table :columns :data="secureChanges"/> <n-data-table :columns :data="secureChanges" />
<n-button secondary type="warning" @click="loadSecureChanges()">查询更新</n-button> <n-button secondary type="warning" @click="loadSecureChanges()">查询更新</n-button>
</n-flex> </n-flex>
</n-card> </n-card>
</template> </template>
<style scoped> <style scoped></style>
</style>
+31 -26
View File
@@ -1,62 +1,67 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref, watch} from "vue"; import { useHead } from '@unhead/vue'
import UploadFileModal from "@/components/file/UploadFileModal.vue"; import { ref, watch } from 'vue'
import {api} from "@/tools/web.js";
import type {UploadFileDto} from "@/types/user.js"; import { uploadFilesCom } from '@/components/file/upload-files.js'
import {useNowUser} from "@/stores/now-user.js"; import UploadFileModal from '@/components/file/UploadFileModal.vue'
import {uploadFilesCom} from "@/components/file/upload-files.js"; import { useNowUser } from '@/stores/now-user.js'
import {useHead} from "@unhead/vue"; import { api } from '@/tools/web.js'
import type { UploadFileDto } from '@/types/user.js'
useHead({ useHead({
title: "上传" title: '上传',
}) })
const NOWUSER = useNowUser(); const NOWUSER = useNowUser()
const showUploadModal = ref(false) const showUploadModal = ref(false)
const files = ref<UploadFileDto[]>([]) const files = ref<UploadFileDto[]>([])
function load() { function load() {
api.get("/file/") api.get('/file/').then((res) => {
.then(res => {
files.value = res.data as UploadFileDto[] files.value = res.data as UploadFileDto[]
}) })
} }
watch(() => NOWUSER.isLogin, () => { watch(
() => NOWUSER.is_login,
() => {
load() load()
}, {immediate: true}) },
{ immediate: true },
)
</script> </script>
<template> <template>
<n-card> <n-card>
<template #header> <template #header>
<n-h3 prefix="bar" style="margin: 0;">上传文件</n-h3> <n-h3 prefix="bar" style="margin: 0">上传文件</n-h3>
</template> </template>
<n-flex vertical> <n-flex vertical>
<n-alert type="info"> <n-alert type="info"> 接受的文件类型</n-alert>
接受的文件类型
</n-alert>
<n-button @click="showUploadModal = true">打开上传向导</n-button> <n-button @click="showUploadModal = true">打开上传向导</n-button>
<upload-file-modal v-model:show-modal="showUploadModal" :after-leave="() => {load()}"/> <upload-file-modal
v-model:show-modal="showUploadModal"
:after-leave="
() => {
load()
}
"
/>
</n-flex> </n-flex>
</n-card> </n-card>
<n-card> <n-card>
<template #header> <template #header>
<n-h3 prefix="bar" style="margin: 0;">个人文件库</n-h3> <n-h3 prefix="bar" style="margin: 0">个人文件库</n-h3>
</template>
<template #header-extra>
您已经上传的文件都在这里,可以选择性地删除以及重新下载。
</template> </template>
<template #header-extra> 您已经上传的文件都在这里,可以选择性地删除以及重新下载。</template>
<component :is="uploadFilesCom(files)"/> <component :is="uploadFilesCom(files)" />
</n-card> </n-card>
</template> </template>
<style scoped> <style scoped></style>
</style>
+199
View File
@@ -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>
+30 -23
View File
@@ -1,14 +1,16 @@
import {createRouter, createWebHashHistory} from 'vue-router' import { createRouter, createWebHashHistory } from 'vue-router'
import AdminNyahome from '@/pages/admin/AdminNyahome.vue'
import AdminOverview from '@/pages/admin/AdminOverview.vue'
import AdminUserInfo from '@/pages/admin/AdminUserInfo.vue'
import AdminUserScript from '@/pages/admin/AdminUserScript.vue'
import AdminUserSecurity from '@/pages/admin/AdminUserSecurity.vue'
import AdminUserUpload from '@/pages/admin/AdminUserUpload.vue'
import AdminPage from '@/pages/AdminPage.vue'
import Chatroom1Page from '@/pages/Chatroom1Page.vue'
import ChatroomPage from '@/pages/ChatroomPage.vue' import ChatroomPage from '@/pages/ChatroomPage.vue'
import Marketplace from '@/pages/Marketplace.vue'
import WelcomePage from '@/pages/WelcomePage.vue' import WelcomePage from '@/pages/WelcomePage.vue'
import Chatroom1Page from "@/pages/Chatroom1Page.vue";
import AdminPage from "@/pages/AdminPage.vue";
import AdminOverview from "@/pages/admin/AdminOverview.vue";
import AdminUserInfo from "@/pages/admin/AdminUserInfo.vue";
import AdminUserSecurity from "@/pages/admin/AdminUserSecurity.vue";
import AdminUserUpload from "@/pages/admin/AdminUserUpload.vue";
import AdminNyahome from "@/pages/admin/AdminNyahome.vue";
import AdminUserScript from "@/pages/admin/AdminUserScript.vue";
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL), history: createWebHashHistory(import.meta.env.BASE_URL),
@@ -28,42 +30,47 @@ const router = createRouter({
path: '/chatroom', path: '/chatroom',
component: ChatroomPage, component: ChatroomPage,
}, },
{
name: 'marketplace',
path: '/marketplace',
component: Marketplace,
},
{ {
name: 'admin', name: 'admin',
path: '/admin/', path: '/admin/',
component: AdminPage, component: AdminPage,
children: [ children: [
{ {
name: "admin-overview", name: 'admin-overview',
path: "", path: '',
component: AdminOverview, component: AdminOverview,
}, },
{ {
name: "admin-user-info", name: 'admin-user-info',
path: "user-info", path: 'user-info',
component: AdminUserInfo, component: AdminUserInfo,
}, },
{ {
name: "admin-user-security", name: 'admin-user-security',
path: "user-security", path: 'user-security',
component: AdminUserSecurity, component: AdminUserSecurity,
}, },
{ {
name: "admin-user-upload", name: 'admin-user-upload',
path: "user-upload", path: 'user-upload',
component: AdminUserUpload, component: AdminUserUpload,
}, },
{ {
name: "admin-user-script", name: 'admin-user-script',
path: "user-script", path: 'user-script',
component: AdminUserScript, component: AdminUserScript,
}, },
{ {
name: "admin-nyahome", name: 'admin-nyahome',
path: "nyahome", path: 'nyahome',
component: AdminNyahome, component: AdminNyahome,
} },
] ],
}, },
], ],
}) })
+9 -10
View File
@@ -1,10 +1,11 @@
import {defineStore} from 'pinia' import { defineStore } from 'pinia'
import {ref} from 'vue' import { ref } from 'vue'
import {api, setApiToken} from '@/tools/web.ts'
import type {UserDto} from '@/types/user.ts' import { api, setApiToken } from '@/tools/web.ts'
import type { UserDto } from '@/types/user.ts'
export const useNowUser = defineStore('now-user', () => { export const useNowUser = defineStore('now-user', () => {
const isLogin = ref(false) const is_login = ref(false)
const id = ref(0) const id = ref(0)
const name = ref('') const name = ref('')
@@ -22,9 +23,7 @@ export const useNowUser = defineStore('now-user', () => {
let user: UserDto let user: UserDto
try { try {
user = await api user = await api.get('/admin/me/').then((res) => res.data as UserDto)
.get('/admin/me/')
.then((res) => res.data as UserDto)
} catch (err) { } catch (err) {
console.error(`请求用户信息时失败:${err}`) console.error(`请求用户信息时失败:${err}`)
throw err throw err
@@ -43,11 +42,11 @@ export const useNowUser = defineStore('now-user', () => {
description.value = user.description description.value = user.description
is_admin.value = user.is_admin is_admin.value = user.is_admin
isLogin.value = true is_login.value = true
} }
return { return {
isLogin, is_login,
id, id,
name, name,
display_name, display_name,
+50
View File
@@ -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
},
},
}
+1
View File
@@ -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 {
+4 -2
View File
@@ -4,8 +4,10 @@ export interface ChatroomPublic {
description: string description: string
feature_image: string feature_image: string
script_template_id: number script_template_id?: number
script_template_version: string script_template_version?: string
default_model_id?: number
} }
export interface Chatroom extends ChatroomPublic { export interface Chatroom extends ChatroomPublic {
-3
View File
@@ -13,13 +13,10 @@
// Bundler mode provides a smoother developer experience. // Bundler mode provides a smoother developer experience.
"module": "preserve", "module": "preserve",
"moduleResolution": "bundler", "moduleResolution": "bundler",
// Include Node.js types and avoid accidentally including other `@types/*` packages. // Include Node.js types and avoid accidentally including other `@types/*` packages.
"types": ["node"], "types": ["node"],
// Disable emitting output during `vue-tsc --build`, which is used for type-checking only. // Disable emitting output during `vue-tsc --build`, which is used for type-checking only.
"noEmit": true, "noEmit": true,
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking. // `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
// Specified here to keep it out of the root directory. // Specified here to keep it out of the root directory.
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo" "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
+14 -17
View File
@@ -4,42 +4,39 @@ import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx' import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools' import vueDevTools from 'vite-plugin-vue-devtools'
import {readFileSync} from "node:fs"; import {readFileSync} from 'node:fs'
import {resolve} from "path"; 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'))
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vue(), vue(),
vueJsx(), vueJsx(),
vueDevTools(), vueDevTools({
launchEditor: 'pycharm',
}),
AutoImport({ AutoImport({
imports: [ imports: [
'vue', 'vue',
{ {
'naive-ui': [ 'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar'],
'useDialog',
'useMessage',
'useNotification',
'useLoadingBar'
]
}, },
unheadVueComposablesImports, unheadVueComposablesImports,
] ],
}), }),
Components({ Components({
resolvers: [NaiveUiResolver()] resolvers: [NaiveUiResolver()],
}) }),
], ],
define: { define: {
__VERSION__: JSON.stringify(pkg.version) __VERSION__: JSON.stringify(pkg.version),
}, },
build: { build: {
rolldownOptions: { rolldownOptions: {
@@ -47,11 +44,11 @@ export default defineConfig({
index: resolve(__dirname, 'index.html'), index: resolve(__dirname, 'index.html'),
}, },
}, },
outDir: "../src/nyahome/static" outDir: '../src/nyahome/static',
}, },
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)) '@': fileURLToPath(new URL('./src', import.meta.url)),
}, },
}, },
server: { server: {