From b02f15f735bd164dfcdb8eb769a66e3b734eec59 Mon Sep 17 00:00:00 2001
From: Marcus <1922576605@qq.com>
Date: Fri, 22 May 2026 17:45:58 +0800
Subject: [PATCH] =?UTF-8?q?bugfix:=20=E4=B8=80=E4=BA=9Bbug=E4=BF=AE?=
=?UTF-8?q?=E5=A4=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
api_docs.md | 111 ----------------
app/components/Table/NewTablePagination.vue | 7 +-
app/components/docs/ApiDocsContent.vue | 132 --------------------
app/components/layout/AppHeader.vue | 31 +++--
app/interfaces/answer.ts | 16 ++-
app/pages/docs.vue | 12 --
app/services/answer_service.ts | 9 +-
app/stores/answer.ts | 41 +++---
nuxt.config.ts | 14 ++-
server/api/cache/clear.post.ts | 10 +-
server/api/records.get.ts | 31 ++++-
server/utils/runtimeState.ts | 5 +-
12 files changed, 115 insertions(+), 304 deletions(-)
delete mode 100644 api_docs.md
delete mode 100644 app/components/docs/ApiDocsContent.vue
delete mode 100644 app/pages/docs.vue
diff --git a/api_docs.md b/api_docs.md
deleted file mode 100644
index 81ae871..0000000
--- a/api_docs.md
+++ /dev/null
@@ -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`。
diff --git a/app/components/Table/NewTablePagination.vue b/app/components/Table/NewTablePagination.vue
index 386f1c2..2f3810d 100644
--- a/app/components/Table/NewTablePagination.vue
+++ b/app/components/Table/NewTablePagination.vue
@@ -41,7 +41,7 @@ const pageCount = computed(() => Math.ceil(props.total / props.pageSize));
@update:page="(val: number) => emits('update:page', val)"
>
-
+
@@ -50,9 +50,6 @@ const pageCount = computed(() => Math.ceil(props.total / props.pageSize));
:value="item.value"
:is-active="item.value === page"
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 }}
@@ -69,7 +66,7 @@ const pageCount = computed(() => Math.ceil(props.total / props.pageSize));
-
+
diff --git a/app/components/docs/ApiDocsContent.vue b/app/components/docs/ApiDocsContent.vue
deleted file mode 100644
index 658da80..0000000
--- a/app/components/docs/ApiDocsContent.vue
+++ /dev/null
@@ -1,132 +0,0 @@
-
-
-
-
-
-
-
- API 文档
-
- 响应保持 OCS AnswererWrapper 兼容:成功返回
- code: 1
- ,失败返回
- code: 0
- 。
-
-
-
-
-
-
- 接口列表
-
-
-
-
-
-
- 方法
- 路径
- Token
- 说明
-
-
-
-
- {{ item.method }}
-
- {{ item.path }}
-
- {{ item.auth }}
- {{ item.description }}
-
-
-
-
-
-
-
-
-
-
- 请求示例
-
-
- {{ searchExample }}
-
-
-
-
-
- 成功响应
-
-
- {{ successExample }}
-
-
-
-
-
diff --git a/app/components/layout/AppHeader.vue b/app/components/layout/AppHeader.vue
index abef252..c2d5336 100644
--- a/app/components/layout/AppHeader.vue
+++ b/app/components/layout/AppHeader.vue
@@ -1,14 +1,17 @@
-
-
-
-
diff --git a/app/services/answer_service.ts b/app/services/answer_service.ts
index f03c13f..a52f072 100644
--- a/app/services/answer_service.ts
+++ b/app/services/answer_service.ts
@@ -2,6 +2,7 @@
import type {
IClearCacheResponse,
IHealthResponse,
+ IRecordsRequest,
IRecordsResponse,
ISearchRequest,
ISearchResponse,
@@ -39,9 +40,13 @@ export class AnswerService {
}
/** 读取最近问答记录,供 Dashboard 表格展示 */
- public static getRecords() {
+ public static getRecords(request: IRecordsRequest) {
return BaseClientService.get(
- `${AnswerService.basePath}/records`
+ `${AnswerService.basePath}/records`,
+ {
+ page: request.page,
+ size: request.size
+ }
);
}
diff --git a/app/stores/answer.ts b/app/stores/answer.ts
index 0c92d2a..fca7854 100644
--- a/app/stores/answer.ts
+++ b/app/stores/answer.ts
@@ -82,16 +82,11 @@ export const useAnswerStore = defineStore("answer", () => {
const cacheClearing = ref(false);
const recordsPage = ref(1);
const recordsPageSize = ref(DASHBOARD_RECORD_PAGE_SIZE);
+ const recordCount = ref(0);
const selectedRecord = ref();
- /** Dashboard 表格当前页数据,分页只作用在前端的最近 100 条内存记录上 */
- const pagedRecords = computed(() => {
- const start = (recordsPage.value - 1) * recordsPageSize.value;
- return records.value.slice(start, start + recordsPageSize.value);
- });
-
- /** 表格总数,用于 TableCom 分页器 */
- const recordCount = computed(() => records.value.length);
+ /** Dashboard 表格当前页数据,页码切换会重新请求服务端分页接口 */
+ const pagedRecords = computed(() => records.value);
/** Dashboard 顶部运行时长展示 */
const uptimeText = computed(() => {
@@ -103,9 +98,10 @@ export const useAnswerStore = defineStore("answer", () => {
selectedRecord.value = record;
};
- /** 切换最近记录表格页码 */
- const setRecordsPage = (page: number) => {
- recordsPage.value = page;
+ /** 切换最近记录表格页码,并请求对应页数据 */
+ const setRecordsPage = async (page: number) => {
+ if (recordsLoading.value || page === recordsPage.value) return;
+ await getRecords(page);
};
/** 调用答题 API,返回 OCS 兼容结构但只在 store 中维护 UI 状态 */
@@ -131,6 +127,7 @@ export const useAnswerStore = defineStore("answer", () => {
if (res.data.code === 1) {
answerResult.value = res.data;
+ recordsPage.value = 1;
await Promise.allSettled([getStats(), getRecords()]);
return;
}
@@ -172,18 +169,18 @@ export const useAnswerStore = defineStore("answer", () => {
}
};
- /** 读取最近问答记录,并在数量变少时把页码拉回合法范围 */
- const getRecords = async () => {
+ /** 读取最近问答记录;服务端按 page/size 返回当前页数据 */
+ const getRecords = async (page = recordsPage.value) => {
recordsLoading.value = true;
try {
- const res = await AnswerService.getRecords();
+ const res = await AnswerService.getRecords({
+ page,
+ size: recordsPageSize.value
+ });
records.value = res.data.records;
-
- const maxPage = Math.max(
- 1,
- Math.ceil(records.value.length / recordsPageSize.value)
- );
- if (recordsPage.value > maxPage) recordsPage.value = maxPage;
+ recordsPage.value = res.data.page;
+ recordsPageSize.value = res.data.size;
+ recordCount.value = res.data.total;
} catch (error) {
dashboardErrorMessage.value = getClientErrorMessage(
error,
@@ -200,7 +197,7 @@ export const useAnswerStore = defineStore("answer", () => {
await Promise.allSettled([getHealth(), getStats(), getRecords()]);
};
- /** 清空缓存后刷新统计;问答记录不会被清空,保持旧服务语义 */
+ /** 清空缓存和问答记录后刷新统计与表格 */
const clearCache = async () => {
cacheClearing.value = true;
dashboardErrorMessage.value = "";
@@ -210,7 +207,7 @@ export const useAnswerStore = defineStore("answer", () => {
if (!res.data.success) {
dashboardErrorMessage.value = res.data.message;
}
- await getStats();
+ await Promise.allSettled([getStats(), getRecords(1)]);
} catch (error) {
dashboardErrorMessage.value = getClientErrorMessage(
error,
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 40d1abc..65c3b6e 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -12,5 +12,17 @@ export default defineNuxtConfig({
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"
+ }
+ }
+ }
});
diff --git a/server/api/cache/clear.post.ts b/server/api/cache/clear.post.ts
index 8d56e42..24149ac 100644
--- a/server/api/cache/clear.post.ts
+++ b/server/api/cache/clear.post.ts
@@ -1,9 +1,14 @@
// server/api/cache/clear.post.ts - 清空答题缓存接口
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 { serverEnv } from "~~/server/utils/env";
import { createApiLogger } from "~~/server/utils/logging";
+import { clearQaRecords } from "~~/server/utils/runtimeState";
/**
* 清空内存缓存
@@ -32,8 +37,9 @@ export default defineEventHandler((event) => {
};
}
- // 清空当前进程内缓存,不影响问答记录和运行统计中的 uptime
+ // 清空当前进程内缓存和问答记录,不影响运行统计中的 uptime
answerCache.clear();
+ clearQaRecords();
logger.info("finish_success");
return {
diff --git a/server/api/records.get.ts b/server/api/records.get.ts
index 0f4f2e3..2150240 100644
--- a/server/api/records.get.ts
+++ b/server/api/records.get.ts
@@ -1,5 +1,5 @@
// server/api/records.get.ts - 最近问答记录接口,供 Nuxt Dashboard 表格展示
-import { setResponseStatus } from "h3";
+import { getQuery, setResponseStatus } from "h3";
import {
invalidAccessTokenResponse,
@@ -28,17 +28,40 @@ export default defineEventHandler((event) => {
return {
success: false,
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 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", {
- count: records.length
+ page,
+ size,
+ total,
+ count: pageRecords.length
});
return {
success: true,
- records
+ records: pageRecords,
+ page,
+ size,
+ total
};
});
diff --git a/server/utils/runtimeState.ts b/server/utils/runtimeState.ts
index 4d128b0..778b827 100644
--- a/server/utils/runtimeState.ts
+++ b/server/utils/runtimeState.ts
@@ -66,7 +66,10 @@ export const addQaRecord = (record: Omit) => {
export const getQaRecords = () => {
return [...qaRecords].reverse();
};
-
+/** 清空进程内问答记录,通常和 answerCache.clear() 一起调用 */
+export const clearQaRecords = () => {
+ qaRecords.length = 0;
+};
/** 生成 `/api/stats` 响应,实时计算 uptime 和有效缓存数量 */
export const getRuntimeStats = () => {
return {