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)" > - + - + 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 @@ - - - - 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 {