bugfix: 一些bug修复
This commit is contained in:
-111
@@ -1,111 +0,0 @@
|
|||||||
# AI 题库服务 API 文档
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
本服务是 Nuxt/Nitro 版 OCS AI 答题接口,兼容 OCS AnswererWrapper。服务端使用 OpenAI 兼容 Chat Completions 流式接口生成答案,最终返回 OCS 可直接处理的 JSON。
|
|
||||||
|
|
||||||
## 搜索接口
|
|
||||||
|
|
||||||
**URL**:`/api/search`
|
|
||||||
|
|
||||||
**方法**:`GET` 或 `POST`
|
|
||||||
|
|
||||||
`POST` 支持 JSON body、`application/x-www-form-urlencoded` body 和 `multipart/form-data` body。
|
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `title` | string | 是 | 题目内容 |
|
|
||||||
| `type` | string | 否 | `single`、`multiple`、`judgement`、`completion` |
|
|
||||||
| `options` | string | 否 | 选项内容 |
|
|
||||||
|
|
||||||
成功响应:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 1,
|
|
||||||
"question": "中国的首都是哪个城市?",
|
|
||||||
"answer": "北京"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
失败响应:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"msg": "未提供问题内容"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 健康检查
|
|
||||||
|
|
||||||
**URL**:`/api/health`
|
|
||||||
|
|
||||||
**方法**:`GET`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "ok",
|
|
||||||
"message": "AI题库服务运行正常",
|
|
||||||
"version": "1.1.0",
|
|
||||||
"cache_enabled": true,
|
|
||||||
"model": "gpt-3.5-turbo"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 清空缓存
|
|
||||||
|
|
||||||
**URL**:`/api/cache/clear`
|
|
||||||
|
|
||||||
**方法**:`POST`
|
|
||||||
|
|
||||||
若配置了 `ACCESS_TOKEN`,需要通过 `X-Access-Token` 请求头或 `token` 查询参数传递。
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": "缓存已清除"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 统计信息
|
|
||||||
|
|
||||||
**URL**:`/api/stats`
|
|
||||||
|
|
||||||
**方法**:`GET`
|
|
||||||
|
|
||||||
若配置了 `ACCESS_TOKEN`,需要通过 `X-Access-Token` 请求头或 `token` 查询参数传递。
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"version": "1.1.0",
|
|
||||||
"uptime": 123.45,
|
|
||||||
"model": "gpt-3.5-turbo",
|
|
||||||
"cache_enabled": true,
|
|
||||||
"cache_size": 1,
|
|
||||||
"qa_records_count": 1
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## OCS 配置示例
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"name": "AI智能题库",
|
|
||||||
"homepage": "http://localhost:3000",
|
|
||||||
"url": "http://localhost:3000/api/search",
|
|
||||||
"method": "get",
|
|
||||||
"type": "GM_xmlhttpRequest",
|
|
||||||
"contentType": "json",
|
|
||||||
"data": {
|
|
||||||
"title": "${title}",
|
|
||||||
"type": "${type}",
|
|
||||||
"options": "${options}"
|
|
||||||
},
|
|
||||||
"handler": "return (res)=> res.code === 1 ? [res.question, res.answer] : [res.msg, undefined]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
多选题答案会返回 `A#B` 或答案文本 `北京#上海` 这类字符串,不返回数组。部署到自定义域名时,需要按 OCS 文档把相关域名加入脚本 `@connect`。
|
|
||||||
@@ -41,7 +41,7 @@ const pageCount = computed(() => Math.ceil(props.total / props.pageSize));
|
|||||||
@update:page="(val: number) => emits('update:page', val)"
|
@update:page="(val: number) => emits('update:page', val)"
|
||||||
>
|
>
|
||||||
<PaginationContent v-slot="{ items }">
|
<PaginationContent v-slot="{ items }">
|
||||||
<PaginationPrevious class="w-5.5 h-5.5 text-primary!">
|
<PaginationPrevious class="w-5.5 h-5.5">
|
||||||
<ChevronLeftIcon class="size-4" />
|
<ChevronLeftIcon class="size-4" />
|
||||||
</PaginationPrevious>
|
</PaginationPrevious>
|
||||||
<template v-for="(item, index) in items" :key="index">
|
<template v-for="(item, index) in items" :key="index">
|
||||||
@@ -50,9 +50,6 @@ const pageCount = computed(() => Math.ceil(props.total / props.pageSize));
|
|||||||
:value="item.value"
|
:value="item.value"
|
||||||
:is-active="item.value === page"
|
:is-active="item.value === page"
|
||||||
class="w-fit min-w-5.5 h-5.5 text-xs px-1 text-muted-foreground"
|
class="w-fit min-w-5.5 h-5.5 text-xs px-1 text-muted-foreground"
|
||||||
:class="{
|
|
||||||
'text-primary! border-primary!': item.value === page
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
{{ item.value }}
|
{{ item.value }}
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
@@ -69,7 +66,7 @@ const pageCount = computed(() => Math.ceil(props.total / props.pageSize));
|
|||||||
</Button>
|
</Button>
|
||||||
</PaginationEllipsis>
|
</PaginationEllipsis>
|
||||||
</template>
|
</template>
|
||||||
<PaginationNext class="w-5.5 h-5.5 text-primary!">
|
<PaginationNext class="w-5.5 h-5.5">
|
||||||
<ChevronRightIcon class="size-4" />
|
<ChevronRightIcon class="size-4" />
|
||||||
</PaginationNext>
|
</PaginationNext>
|
||||||
</PaginationContent>
|
</PaginationContent>
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
<!-- app/components/docs/ApiDocsContent.vue - 前端文档页内容,覆盖旧 Flask /docs 页面 -->
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
|
|
||||||
/** 文档示例保持本地接口字段名,避免把真实 token 或 key 写进前端 */
|
|
||||||
const searchExample = `GET /api/search?title=中国的首都是哪个城市?&type=single&options=A. 北京\\nB. 上海`;
|
|
||||||
|
|
||||||
const successExample = `{
|
|
||||||
"code": 1,
|
|
||||||
"question": "中国的首都是哪个城市?",
|
|
||||||
"answer": "A"
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const endpoints = [
|
|
||||||
{
|
|
||||||
method: "GET/POST",
|
|
||||||
path: "/api/search",
|
|
||||||
auth: "可选",
|
|
||||||
description: "OCS AnswererWrapper 查询入口"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: "/api/health",
|
|
||||||
auth: "否",
|
|
||||||
description: "服务健康检查"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: "/api/stats",
|
|
||||||
auth: "可选",
|
|
||||||
description: "运行统计和缓存数量"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
path: "/api/records",
|
|
||||||
auth: "可选",
|
|
||||||
description: "最近问答记录"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
path: "/api/cache/clear",
|
|
||||||
auth: "可选",
|
|
||||||
description: "清空当前进程内缓存"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<article class="grid gap-5">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle class="text-xl">API 文档</CardTitle>
|
|
||||||
<CardDescription class="leading-6">
|
|
||||||
响应保持 OCS AnswererWrapper 兼容:成功返回
|
|
||||||
<Badge variant="secondary">code: 1</Badge>
|
|
||||||
,失败返回
|
|
||||||
<Badge variant="secondary">code: 0</Badge>
|
|
||||||
。
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>接口列表</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div class="overflow-auto">
|
|
||||||
<Table class="min-w-[720px]">
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>方法</TableHead>
|
|
||||||
<TableHead>路径</TableHead>
|
|
||||||
<TableHead>Token</TableHead>
|
|
||||||
<TableHead>说明</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
<TableRow v-for="item in endpoints" :key="item.path">
|
|
||||||
<TableCell>{{ item.method }}</TableCell>
|
|
||||||
<TableCell class="font-mono text-xs text-foreground">
|
|
||||||
{{ item.path }}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{{ item.auth }}</TableCell>
|
|
||||||
<TableCell>{{ item.description }}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<section class="grid gap-4 lg:grid-cols-2">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>请求示例</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<pre
|
|
||||||
class="overflow-auto rounded-2xl bg-muted p-4 text-xs leading-5 text-muted-foreground"
|
|
||||||
><code>{{ searchExample }}</code></pre>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>成功响应</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<pre
|
|
||||||
class="overflow-auto rounded-2xl bg-muted p-4 text-xs leading-5 text-muted-foreground"
|
|
||||||
><code>{{ successExample }}</code></pre>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</section>
|
|
||||||
</article>
|
|
||||||
</template>
|
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
<!-- app/components/layout/AppHeader.vue - 顶部导航和页面切换入口 -->
|
<!-- app/components/layout/AppHeader.vue - 顶部导航和页面切换入口 -->
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import { Activity, LayoutDashboard, Moon, Search, Sun } from "lucide-vue-next";
|
||||||
Activity,
|
|
||||||
BookOpenText,
|
|
||||||
LayoutDashboard,
|
|
||||||
Search
|
|
||||||
} from "lucide-vue-next";
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
/** 使用 VueUse 的 useColorMode 切换 dark/light,class 会挂到 <html> 上 */
|
||||||
|
const colorMode = useColorMode();
|
||||||
|
const isDark = computed(() => colorMode.value === "dark");
|
||||||
|
|
||||||
|
function toggleColorMode() {
|
||||||
|
colorMode.value = isDark.value ? "light" : "dark";
|
||||||
|
}
|
||||||
|
|
||||||
/** 顶部导航项,icon 使用 lucide,和项目里 shadcn 的图标体系保持一致 */
|
/** 顶部导航项,icon 使用 lucide,和项目里 shadcn 的图标体系保持一致 */
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{
|
{
|
||||||
@@ -20,11 +23,6 @@ const navItems = [
|
|||||||
label: "Dashboard",
|
label: "Dashboard",
|
||||||
to: "/dashboard",
|
to: "/dashboard",
|
||||||
icon: LayoutDashboard
|
icon: LayoutDashboard
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "文档",
|
|
||||||
to: "/docs",
|
|
||||||
icon: BookOpenText
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -73,6 +71,17 @@ const isActive = (to: string) => {
|
|||||||
<span>{{ item.label }}</span>
|
<span>{{ item.label }}</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<!-- 深色/浅色主题切换按钮 -->
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
aria-label="切换主题"
|
||||||
|
@click="toggleColorMode"
|
||||||
|
>
|
||||||
|
<Sun v-if="isDark" class="size-4" />
|
||||||
|
<Moon v-else class="size-4" />
|
||||||
|
</Button>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -85,14 +85,28 @@ export interface IQaRecord {
|
|||||||
answer: string;
|
answer: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 最近问答记录分页请求 */
|
||||||
|
export interface IRecordsRequest {
|
||||||
|
/** 当前页码,从 1 开始 */
|
||||||
|
page: number;
|
||||||
|
/** 每页数量 */
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
/** 最近问答记录接口响应 */
|
/** 最近问答记录接口响应 */
|
||||||
export interface IRecordsResponse {
|
export interface IRecordsResponse {
|
||||||
/** 管理接口是否成功 */
|
/** 管理接口是否成功 */
|
||||||
success: boolean;
|
success: boolean;
|
||||||
/** 失败时的本地安全文案 */
|
/** 失败时的本地安全文案 */
|
||||||
message?: string;
|
message?: string;
|
||||||
/** 当前进程内最近问答记录,最新在前 */
|
/** 当前页问答记录,最新在前 */
|
||||||
records: IQaRecord[];
|
records: IQaRecord[];
|
||||||
|
/** 当前页码,从 1 开始 */
|
||||||
|
page: number;
|
||||||
|
/** 每页数量 */
|
||||||
|
size: number;
|
||||||
|
/** 当前进程内问答记录总数 */
|
||||||
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 清空缓存接口响应 */
|
/** 清空缓存接口响应 */
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
<!-- app/pages/docs.vue - API 文档页,替代旧 Flask /docs 模板 -->
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import ApiDocsContent from "@/components/docs/ApiDocsContent.vue";
|
|
||||||
|
|
||||||
useHead({
|
|
||||||
title: "API 文档 - OCS AI 答题服务"
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ApiDocsContent />
|
|
||||||
</template>
|
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import type {
|
import type {
|
||||||
IClearCacheResponse,
|
IClearCacheResponse,
|
||||||
IHealthResponse,
|
IHealthResponse,
|
||||||
|
IRecordsRequest,
|
||||||
IRecordsResponse,
|
IRecordsResponse,
|
||||||
ISearchRequest,
|
ISearchRequest,
|
||||||
ISearchResponse,
|
ISearchResponse,
|
||||||
@@ -39,9 +40,13 @@ export class AnswerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 读取最近问答记录,供 Dashboard 表格展示 */
|
/** 读取最近问答记录,供 Dashboard 表格展示 */
|
||||||
public static getRecords() {
|
public static getRecords(request: IRecordsRequest) {
|
||||||
return BaseClientService.get<IRecordsResponse>(
|
return BaseClientService.get<IRecordsResponse>(
|
||||||
`${AnswerService.basePath}/records`
|
`${AnswerService.basePath}/records`,
|
||||||
|
{
|
||||||
|
page: request.page,
|
||||||
|
size: request.size
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+19
-22
@@ -82,16 +82,11 @@ export const useAnswerStore = defineStore("answer", () => {
|
|||||||
const cacheClearing = ref(false);
|
const cacheClearing = ref(false);
|
||||||
const recordsPage = ref(1);
|
const recordsPage = ref(1);
|
||||||
const recordsPageSize = ref(DASHBOARD_RECORD_PAGE_SIZE);
|
const recordsPageSize = ref(DASHBOARD_RECORD_PAGE_SIZE);
|
||||||
|
const recordCount = ref(0);
|
||||||
const selectedRecord = ref<IQaRecord>();
|
const selectedRecord = ref<IQaRecord>();
|
||||||
|
|
||||||
/** Dashboard 表格当前页数据,分页只作用在前端的最近 100 条内存记录上 */
|
/** Dashboard 表格当前页数据,页码切换会重新请求服务端分页接口 */
|
||||||
const pagedRecords = computed(() => {
|
const pagedRecords = computed(() => records.value);
|
||||||
const start = (recordsPage.value - 1) * recordsPageSize.value;
|
|
||||||
return records.value.slice(start, start + recordsPageSize.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
/** 表格总数,用于 TableCom 分页器 */
|
|
||||||
const recordCount = computed(() => records.value.length);
|
|
||||||
|
|
||||||
/** Dashboard 顶部运行时长展示 */
|
/** Dashboard 顶部运行时长展示 */
|
||||||
const uptimeText = computed(() => {
|
const uptimeText = computed(() => {
|
||||||
@@ -103,9 +98,10 @@ export const useAnswerStore = defineStore("answer", () => {
|
|||||||
selectedRecord.value = record;
|
selectedRecord.value = record;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 切换最近记录表格页码 */
|
/** 切换最近记录表格页码,并请求对应页数据 */
|
||||||
const setRecordsPage = (page: number) => {
|
const setRecordsPage = async (page: number) => {
|
||||||
recordsPage.value = page;
|
if (recordsLoading.value || page === recordsPage.value) return;
|
||||||
|
await getRecords(page);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 调用答题 API,返回 OCS 兼容结构但只在 store 中维护 UI 状态 */
|
/** 调用答题 API,返回 OCS 兼容结构但只在 store 中维护 UI 状态 */
|
||||||
@@ -131,6 +127,7 @@ export const useAnswerStore = defineStore("answer", () => {
|
|||||||
|
|
||||||
if (res.data.code === 1) {
|
if (res.data.code === 1) {
|
||||||
answerResult.value = res.data;
|
answerResult.value = res.data;
|
||||||
|
recordsPage.value = 1;
|
||||||
await Promise.allSettled([getStats(), getRecords()]);
|
await Promise.allSettled([getStats(), getRecords()]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -172,18 +169,18 @@ export const useAnswerStore = defineStore("answer", () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 读取最近问答记录,并在数量变少时把页码拉回合法范围 */
|
/** 读取最近问答记录;服务端按 page/size 返回当前页数据 */
|
||||||
const getRecords = async () => {
|
const getRecords = async (page = recordsPage.value) => {
|
||||||
recordsLoading.value = true;
|
recordsLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const res = await AnswerService.getRecords();
|
const res = await AnswerService.getRecords({
|
||||||
|
page,
|
||||||
|
size: recordsPageSize.value
|
||||||
|
});
|
||||||
records.value = res.data.records;
|
records.value = res.data.records;
|
||||||
|
recordsPage.value = res.data.page;
|
||||||
const maxPage = Math.max(
|
recordsPageSize.value = res.data.size;
|
||||||
1,
|
recordCount.value = res.data.total;
|
||||||
Math.ceil(records.value.length / recordsPageSize.value)
|
|
||||||
);
|
|
||||||
if (recordsPage.value > maxPage) recordsPage.value = maxPage;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dashboardErrorMessage.value = getClientErrorMessage(
|
dashboardErrorMessage.value = getClientErrorMessage(
|
||||||
error,
|
error,
|
||||||
@@ -200,7 +197,7 @@ export const useAnswerStore = defineStore("answer", () => {
|
|||||||
await Promise.allSettled([getHealth(), getStats(), getRecords()]);
|
await Promise.allSettled([getHealth(), getStats(), getRecords()]);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 清空缓存后刷新统计;问答记录不会被清空,保持旧服务语义 */
|
/** 清空缓存和问答记录后刷新统计与表格 */
|
||||||
const clearCache = async () => {
|
const clearCache = async () => {
|
||||||
cacheClearing.value = true;
|
cacheClearing.value = true;
|
||||||
dashboardErrorMessage.value = "";
|
dashboardErrorMessage.value = "";
|
||||||
@@ -210,7 +207,7 @@ export const useAnswerStore = defineStore("answer", () => {
|
|||||||
if (!res.data.success) {
|
if (!res.data.success) {
|
||||||
dashboardErrorMessage.value = res.data.message;
|
dashboardErrorMessage.value = res.data.message;
|
||||||
}
|
}
|
||||||
await getStats();
|
await Promise.allSettled([getStats(), getRecords(1)]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dashboardErrorMessage.value = getClientErrorMessage(
|
dashboardErrorMessage.value = getClientErrorMessage(
|
||||||
error,
|
error,
|
||||||
|
|||||||
+13
-1
@@ -12,5 +12,17 @@ export default defineNuxtConfig({
|
|||||||
plugins: [tailwindcss(), svgLoader()]
|
plugins: [tailwindcss(), svgLoader()]
|
||||||
},
|
},
|
||||||
|
|
||||||
modules: ["@pinia/nuxt", "@vueuse/nuxt", "@nuxt/eslint", "shadcn-nuxt"]
|
modules: ["@pinia/nuxt", "@vueuse/nuxt", "@nuxt/eslint", "shadcn-nuxt"],
|
||||||
|
|
||||||
|
// 允许任意来源访问 /api/* 接口,供 OCS 油猴脚本跨域调用
|
||||||
|
routeRules: {
|
||||||
|
"/api/**": {
|
||||||
|
cors: true,
|
||||||
|
headers: {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "GET,POST,OPTIONS",
|
||||||
|
"Access-Control-Allow-Headers": "Content-Type,Authorization"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Vendored
+8
-2
@@ -1,9 +1,14 @@
|
|||||||
// server/api/cache/clear.post.ts - 清空答题缓存接口
|
// server/api/cache/clear.post.ts - 清空答题缓存接口
|
||||||
import { setResponseStatus } from "h3";
|
import { setResponseStatus } from "h3";
|
||||||
import { invalidAccessTokenResponse, verifyAccessToken } from "~~/server/utils/auth";
|
|
||||||
|
import {
|
||||||
|
invalidAccessTokenResponse,
|
||||||
|
verifyAccessToken
|
||||||
|
} from "~~/server/utils/auth";
|
||||||
import { answerCache } from "~~/server/utils/cache";
|
import { answerCache } from "~~/server/utils/cache";
|
||||||
import { serverEnv } from "~~/server/utils/env";
|
import { serverEnv } from "~~/server/utils/env";
|
||||||
import { createApiLogger } from "~~/server/utils/logging";
|
import { createApiLogger } from "~~/server/utils/logging";
|
||||||
|
import { clearQaRecords } from "~~/server/utils/runtimeState";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清空内存缓存
|
* 清空内存缓存
|
||||||
@@ -32,8 +37,9 @@ export default defineEventHandler((event) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清空当前进程内缓存,不影响问答记录和运行统计中的 uptime
|
// 清空当前进程内缓存和问答记录,不影响运行统计中的 uptime
|
||||||
answerCache.clear();
|
answerCache.clear();
|
||||||
|
clearQaRecords();
|
||||||
logger.info("finish_success");
|
logger.info("finish_success");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// server/api/records.get.ts - 最近问答记录接口,供 Nuxt Dashboard 表格展示
|
// server/api/records.get.ts - 最近问答记录接口,供 Nuxt Dashboard 表格展示
|
||||||
import { setResponseStatus } from "h3";
|
import { getQuery, setResponseStatus } from "h3";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
invalidAccessTokenResponse,
|
invalidAccessTokenResponse,
|
||||||
@@ -28,17 +28,40 @@ export default defineEventHandler((event) => {
|
|||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: invalidAccessTokenResponse().msg,
|
message: invalidAccessTokenResponse().msg,
|
||||||
records: []
|
records: [],
|
||||||
|
page: 1,
|
||||||
|
size: 10,
|
||||||
|
total: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const query = getQuery(event);
|
||||||
|
const rawPage = Number.parseInt(query.page?.toString() || "1", 10);
|
||||||
|
const rawSize = Number.parseInt(query.size?.toString() || "10", 10);
|
||||||
|
const size = Number.isFinite(rawSize)
|
||||||
|
? Math.min(Math.max(rawSize, 1), 100)
|
||||||
|
: 10;
|
||||||
const records = getQaRecords();
|
const records = getQaRecords();
|
||||||
|
const total = records.length;
|
||||||
|
const maxPage = Math.max(1, Math.ceil(total / size));
|
||||||
|
const page = Number.isFinite(rawPage)
|
||||||
|
? Math.min(Math.max(rawPage, 1), maxPage)
|
||||||
|
: 1;
|
||||||
|
const start = (page - 1) * size;
|
||||||
|
const pageRecords = records.slice(start, start + size);
|
||||||
|
|
||||||
logger.info("finish_success", {
|
logger.info("finish_success", {
|
||||||
count: records.length
|
page,
|
||||||
|
size,
|
||||||
|
total,
|
||||||
|
count: pageRecords.length
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
records
|
records: pageRecords,
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
total
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -66,7 +66,10 @@ export const addQaRecord = (record: Omit<QaRecord, "time" | "timestamp">) => {
|
|||||||
export const getQaRecords = () => {
|
export const getQaRecords = () => {
|
||||||
return [...qaRecords].reverse();
|
return [...qaRecords].reverse();
|
||||||
};
|
};
|
||||||
|
/** 清空进程内问答记录,通常和 answerCache.clear() 一起调用 */
|
||||||
|
export const clearQaRecords = () => {
|
||||||
|
qaRecords.length = 0;
|
||||||
|
};
|
||||||
/** 生成 `/api/stats` 响应,实时计算 uptime 和有效缓存数量 */
|
/** 生成 `/api/stats` 响应,实时计算 uptime 和有效缓存数量 */
|
||||||
export const getRuntimeStats = () => {
|
export const getRuntimeStats = () => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user