From d3515562938202d24a7c891f8c7fa653c5778692 Mon Sep 17 00:00:00 2001 From: yu <191972707@qq.com> Date: Thu, 29 Oct 2026 10:58:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8E=A5=E5=85=A5=20AI=20=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E5=AE=B6=E6=97=8F=EF=BC=88OpenAI=20=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E5=8D=8F=E8=AE=AE=EF=BC=8C17=20id=EF=BC=89=E4=B8=8E=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=20OCR=EF=BC=88Tesseract.js=EF=BC=8C4=20id=EF=BC=89?= =?UTF-8?q?=EF=BC=9B=E8=AE=BE=E7=BD=AE=E9=A1=B5=E6=96=B0=E5=A2=9E=20AI=20?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 115 ++++++++++++++++++++++++ package.json | 1 + src/main/capabilities/ai-caps.ts | 56 ++++++++++++ src/main/capabilities/ai-service.ts | 32 +++++++ src/main/capabilities/index.ts | 3 + src/main/capabilities/ocr-caps.ts | 51 +++++++++++ src/main/settings.ts | 11 ++- src/renderer/src/pages/SettingsPage.tsx | 50 ++++++++++- src/shared/types.ts | 7 ++ 9 files changed, 322 insertions(+), 4 deletions(-) create mode 100644 src/main/capabilities/ai-caps.ts create mode 100644 src/main/capabilities/ai-service.ts create mode 100644 src/main/capabilities/ocr-caps.ts diff --git a/package-lock.json b/package-lock.json index 5d1eb09..1a2b044 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "pngjs": "^7.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "tesseract.js": "^7.0.0", "zustand": "^5.0.2" }, "devDependencies": { @@ -3375,6 +3376,12 @@ "dev": true, "license": "MIT" }, + "node_modules/bmp-js": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/bmp-js/-/bmp-js-0.1.0.tgz", + "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==", + "license": "MIT" + }, "node_modules/boolean": { "version": "3.2.0", "resolved": "https://registry.npmmirror.com/boolean/-/boolean-3.2.0.tgz", @@ -5525,6 +5532,12 @@ "node": ">= 14" } }, + "node_modules/idb-keyval": { + "version": "6.3.0", + "resolved": "https://registry.npmmirror.com/idb-keyval/-/idb-keyval-6.3.0.tgz", + "integrity": "sha512-um+2dgAWmYsu615EXpWVwSmapJhON0G43t3Ka/EVaohzPQXSMqKEqeDK/oIW3Ow+BXaF2PvSc+oBTFp793A5Ow==", + "license": "Apache-2.0" + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", @@ -5886,6 +5899,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "license": "MIT" + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -6519,6 +6538,26 @@ "semver": "^7.3.5" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-gyp": { "version": "12.4.0", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", @@ -6684,6 +6723,15 @@ "wrappy": "1" } }, + "node_modules/opencollective-postinstall": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", + "license": "MIT", + "bin": { + "opencollective-postinstall": "index.js" + } + }, "node_modules/own-keys": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/own-keys/-/own-keys-1.0.2.tgz", @@ -7171,6 +7219,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -7980,6 +8034,30 @@ "rimraf": "bin.js" } }, + "node_modules/tesseract.js": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/tesseract.js/-/tesseract.js-7.0.0.tgz", + "integrity": "sha512-exPBkd+z+wM1BuMkx/Bjv43OeLBxhL5kKWsz/9JY+DXcXdiBjiAch0V49QR3oAJqCaL5qURE0vx9Eo+G5YE7mA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "bmp-js": "^0.1.0", + "idb-keyval": "^6.2.0", + "is-url": "^1.2.4", + "node-fetch": "^2.6.9", + "opencollective-postinstall": "^2.0.3", + "regenerator-runtime": "^0.13.3", + "tesseract.js-core": "^7.0.0", + "wasm-feature-detect": "^1.8.0", + "zlibjs": "^0.3.1" + } + }, + "node_modules/tesseract.js-core": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/tesseract.js-core/-/tesseract.js-core-7.0.0.tgz", + "integrity": "sha512-WnNH518NzmbSq9zgTPeoF8c+xmilS8rFIl1YKbk/ptuuc7p6cLNELNuPAzcmsYw450ca6bLa8j3t0VAtq435Vw==", + "license": "Apache-2.0" + }, "node_modules/tiny-async-pool": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", @@ -8080,6 +8158,12 @@ "tmp": "^0.2.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/traverse": { "version": "0.3.10", "resolved": "https://registry.npmmirror.com/traverse/-/traverse-0.3.10.tgz", @@ -9596,6 +9680,12 @@ } } }, + "node_modules/wasm-feature-detect": { + "version": "1.9.0", + "resolved": "https://registry.npmmirror.com/wasm-feature-detect/-/wasm-feature-detect-1.9.0.tgz", + "integrity": "sha512-zonE+xlIIYtxPy++L24ow0hAD8CICb4+FgPyROd3buyXIqsJvUEDkBgfCCoXOd1Hu3DUr0GOfnPIdcGV+YpNaA==", + "license": "Apache-2.0" + }, "node_modules/webcrypto-core": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.9.2.tgz", @@ -9617,6 +9707,22 @@ "dev": true, "license": "0BSD" }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", @@ -9880,6 +9986,15 @@ "node": ">= 10" } }, + "node_modules/zlibjs": { + "version": "0.3.1", + "resolved": "https://registry.npmmirror.com/zlibjs/-/zlibjs-0.3.1.tgz", + "integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/zustand": { "version": "5.0.16", "resolved": "https://registry.npmmirror.com/zustand/-/zustand-5.0.16.tgz", diff --git a/package.json b/package.json index 538f3a5..11c219a 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "pngjs": "^7.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "tesseract.js": "^7.0.0", "zustand": "^5.0.2" }, "devDependencies": { diff --git a/src/main/capabilities/ai-caps.ts b/src/main/capabilities/ai-caps.ts new file mode 100644 index 0000000..3ae7d27 --- /dev/null +++ b/src/main/capabilities/ai-caps.ts @@ -0,0 +1,56 @@ +import { register } from "../engine/registry"; +import { RuntimeError } from "../engine/types"; +import { chatCompletion, requireAiConfig } from "./ai-service"; + +function inputText(args: Record, nodeId: string): string { + for (const key of ["input", "text", "content", "sentence", "prompt", "sourceFilePath", "filePath", "source", "document"]) { + const v = String(args[key] ?? "").trim(); + if (v) return v; + } + throw new RuntimeError("validation_error", "输入文本为空", { nodeId, componentId: nodeId }); +} + +async function readMaybeFile(value: string): Promise { + if (/\.(txt|md|csv|json|log)$/i.test(value) && value.includes("/") && value.length < 260) { + try { + const fs = await import("node:fs"); + if (fs.existsSync(value)) return fs.readFileSync(value, "utf8"); + } catch { /* not a file */ } + } + return value; +} + +async function complete(prompt: string, input: string, nodeId: string, temperature = 0.3): Promise { + if (!(await Promise.resolve(true))) return ""; + void nodeId; + const config = requireAiConfig(); + return chatCompletion([ + { role: "system", content: prompt }, + { role: "user", content: input } + ], { temperature }); +} + +const TEXT_TASKS: Array<{ ids: string[]; system: string; task: string }> = [ + { ids: ["DataService_AIGenTxt"], system: "你是写作助手,根据用户要求生成高质量中文内容。", task: "AI 生成文本" }, + { ids: ["DataService_LargeModel", "DataService_ZBrain", "DataService_AgentCall", "NLP_XunFei_NLPBaseServer"], system: "你是智能助手,尽力完成用户的任务。", task: "通用模型调用" }, + { ids: ["DataService_ChatBot"], system: "你是对话机器人,用中文自然对话。", task: "对话机器人" }, + { ids: ["DataService_KeyWord", "NLP_KeywordExtraction"], system: "从文本中提取关键词,输出 JSON 数组(字符串列表),不要输出其他内容。", task: "关键词抽取" }, + { ids: ["DataService_Correct"], system: "修正文本中的错别字与语病,直接输出修正后的全文。", task: "文本纠错" }, + { ids: ["DataService_Review"], system: '审核文本内容是否合规,输出 JSON:{\"pass\":true/false,\"reason\":\"原因\"}。', task: "文本审核" }, + { ids: ["DataService_TextComparison", "Scenario_TextComparison"], system: "比较两段文本差异,输出简明差异摘要。", task: "文档比对" }, + { ids: ["NLP_SplitWords"], system: '对文本分词,输出 JSON 数组(字符串列表)。', task: "分词" }, + { ids: ["NLP_SentimentAnalysis"], system: '分析文本情感倾向,输出 JSON:{\"label\":\"正面/负面/中性\",\"score\":0.99}。', task: "情感分析" }, + { ids: ["NLP_SemanticSimilarity"], system: '判断两段文本语义相似度,输出 JSON:{\"score\":0.99}(0-1)。输入格式:文本1\n---\n文本2。', task: "语义相似度" }, + { ids: ["NLP_PerspectiveAnalysis"], system: '分析文本中的观点与立场,输出 JSON:{\"perspectives\":[{\"view\":\"...\",\"stance\":\"支持/反对/中立\"}]}。', task: "观点分析" }, + { ids: ["NLP_AnalyseProductReview"], system: '分析商品评论,输出 JSON:{\"summary\":\"...\",\"sentiment\":\"...\",\"issues\":[]}。', task: "商品评论分析" }, +]; + +for (const task of TEXT_TASKS) { + register(task.ids, async (ctx, node, args) => { + const rawInput = inputText(args, node.id); + const input = await readMaybeFile(rawInput); + const result = await complete(task.system, input, node.id); + ctx.log(task.task + "完成", "info", node.id); + return result; + }); +} diff --git a/src/main/capabilities/ai-service.ts b/src/main/capabilities/ai-service.ts new file mode 100644 index 0000000..0eb7176 --- /dev/null +++ b/src/main/capabilities/ai-service.ts @@ -0,0 +1,32 @@ +import { loadSettings } from "../settings"; +import { RuntimeError } from "../engine/types"; + +export interface ChatMessage { role: "system" | "user" | "assistant"; content: string } + +export function getAiConfig(): { baseUrl: string; apiKey: string; model: string } { + return loadSettings().aiConfig; +} + +export function requireAiConfig() { + const config = getAiConfig(); + if (!config.baseUrl || !config.model) { + throw new RuntimeError("ai_not_configured", "AI 服务未配置", { recommendation: "到 设置 → AI 服务 填写 Base URL 与模型名(兼容 OpenAI 协议)" }); + } + return config; +} + +export async function chatCompletion(messages: ChatMessage[], options: { temperature?: number } = {}): Promise { + const config = requireAiConfig(); + const base = config.baseUrl.replace(/\/+$/, ""); + const response = await fetch(base + "/chat/completions", { + method: "POST", + headers: { "Content-Type": "application/json", Authorization: "Bearer " + config.apiKey }, + body: JSON.stringify({ model: config.model, messages, temperature: options.temperature ?? 0.3 }) + }); + if (!response.ok) { + const detail = (await response.text()).slice(0, 300); + throw new RuntimeError("ai_request_failed", "AI 请求失败(HTTP " + response.status + "):" + detail); + } + const data = (await response.json()) as { choices?: Array<{ message?: { content?: string } }> }; + return data.choices?.[0]?.message?.content ?? ""; +} diff --git a/src/main/capabilities/index.ts b/src/main/capabilities/index.ts index 2fa9c50..524807e 100644 --- a/src/main/capabilities/index.ts +++ b/src/main/capabilities/index.ts @@ -16,6 +16,9 @@ import "./human-dialog"; import "./time-caps"; import "./windows-system"; import "./ftp"; +import "./ai-service"; +import "./ai-caps"; +import "./ocr-caps"; import "./pdf"; import "./word"; import "./word-extended"; diff --git a/src/main/capabilities/ocr-caps.ts b/src/main/capabilities/ocr-caps.ts new file mode 100644 index 0000000..1bd4552 --- /dev/null +++ b/src/main/capabilities/ocr-caps.ts @@ -0,0 +1,51 @@ +import Tesseract from "tesseract.js"; +import fs from "node:fs"; +import { register } from "../engine/registry"; +import { RuntimeError } from "../engine/types"; + +function requireImage(args: Record, nodeId: string): string { + for (const key of ["image_path", "imagePath", "file_path", "filePath", "path", "image"]) { + const v = String(args[key] ?? "").trim(); + if (v) return v; + } + throw new RuntimeError("validation_error", "图片路径为空", { nodeId, componentId: nodeId, recommendation: "填写本地图片路径(png/jpg/bmp)" }); +} + +async function recognize(imagePath: string, nodeId: string) { + if (!fs.existsSync(imagePath)) { + throw new RuntimeError("file_not_found", "图片不存在:" + imagePath, { nodeId, componentId: nodeId }); + } + const worker = await Tesseract.createWorker("chi_sim+eng"); + try { + return await worker.recognize(imagePath); + } finally { + await worker.terminate(); + } +} + +register("OcrOffline_GetOcrTextNew", async (ctx, node, args) => { + const imagePath = requireImage(args, node.id); + const result = await recognize(imagePath, node.id); + const data = result.data as Tesseract.Page & { words?: Array<{ text: string; bbox: { x0: number; y0: number; x1: number; y1: number }; confidence: number }>; lines?: Array<{ words: Array<{ text: string }> }> }; + const text = data.text.trim(); + ctx.log("OCR 识别完成:" + text.length + " 字符", "info", node.id); + return text; +}); + +register("OcrOffline_GetOcrTextPosition", async (ctx, node, args) => { + const imagePath = requireImage(args, node.id); + const result = await recognize(imagePath, node.id); + const data = result.data as Tesseract.Page & { words?: Array<{ text: string; bbox: { x0: number; y0: number; x1: number; y1: number }; confidence: number }>; lines?: Array<{ words: Array<{ text: string }> }> }; + const words = (data.words ?? []).map(w => ({ text: w.text, x: w.bbox.x0, y: w.bbox.y0, width: w.bbox.x1 - w.bbox.x0, height: w.bbox.y1 - w.bbox.y0, confidence: w.confidence })); + ctx.log("OCR 定位完成:" + words.length + " 个词", "info", node.id); + return words; +}); + +register("OcrOffline_GetOcrTable", async (ctx, node, args) => { + const imagePath = requireImage(args, node.id); + const result = await recognize(imagePath, node.id); + const data = result.data as Tesseract.Page & { words?: Array<{ text: string; bbox: { x0: number; y0: number; x1: number; y1: number }; confidence: number }>; lines?: Array<{ words: Array<{ text: string }> }> }; + const lines = (data.lines ?? []).map((line: { words: Array<{ text: string }> }) => line.words.map((w: { text: string }) => w.text).join("\t")); + ctx.log("OCR 表格识别完成:" + lines.length + " 行", "info", node.id); + return lines; +}); diff --git a/src/main/settings.ts b/src/main/settings.ts index 4f7d206..136b5c6 100644 --- a/src/main/settings.ts +++ b/src/main/settings.ts @@ -4,7 +4,8 @@ import path from "node:path"; import type { AppSettings } from "../shared/types"; export const DEFAULT_APP_SETTINGS: AppSettings = { - browserElementWaitMs: 5000 + browserElementWaitMs: 5000, + aiConfig: { baseUrl: "", apiKey: "", model: "" } }; let cache: AppSettings | null = null; @@ -16,10 +17,16 @@ function settingsPath(): string { function normalizeSettings(value: unknown): AppSettings { const source = (value && typeof value === "object" ? value : {}) as Partial; const wait = Number(source.browserElementWaitMs); + const ai = (source.aiConfig && typeof source.aiConfig === "object" ? source.aiConfig : {}) as Partial; return { browserElementWaitMs: Number.isFinite(wait) ? Math.min(600000, Math.max(0, Math.round(wait))) - : DEFAULT_APP_SETTINGS.browserElementWaitMs + : DEFAULT_APP_SETTINGS.browserElementWaitMs, + aiConfig: { + baseUrl: String(ai.baseUrl ?? ""), + apiKey: String(ai.apiKey ?? ""), + model: String(ai.model ?? "") + } }; } diff --git a/src/renderer/src/pages/SettingsPage.tsx b/src/renderer/src/pages/SettingsPage.tsx index 6ca1a6b..5df35ba 100644 --- a/src/renderer/src/pages/SettingsPage.tsx +++ b/src/renderer/src/pages/SettingsPage.tsx @@ -1,10 +1,10 @@ import { useEffect, useMemo, useState } from "react"; -import { Clock, Cpu, Database, FolderCog, Globe2, HardDrive, Info, Save, ShieldCheck } from "lucide-react"; +import { Clock, Cpu, Database, FolderCog, Globe2, HardDrive, Info, Save, ShieldCheck, Sparkles } from "lucide-react"; import type { LucideIcon } from "lucide-react"; import type { ReactNode } from "react"; import type { AppInfo, AppSettings, Catalog } from "@shared/types"; -type SettingsTab = "about" | "catalog" | "execution" | "browser"; +type SettingsTab = "about" | "catalog" | "execution" | "browser" | "ai"; export function SettingsPage({ catalog }: { catalog: Catalog }) { const [tab, setTab] = useState("about"); @@ -14,6 +14,9 @@ export function SettingsPage({ catalog }: { catalog: Catalog }) { const [waitDraft, setWaitDraft] = useState("5000"); const [waitSaving, setWaitSaving] = useState(false); const [waitMessage, setWaitMessage] = useState(null); + const [aiDraft, setAiDraft] = useState({ baseUrl: "", apiKey: "", model: "" }); + const [aiSaving, setAiSaving] = useState(false); + const [aiMessage, setAiMessage] = useState(null); useEffect(() => { void window.rpa.app.info().then(setInfo).catch(() => setInfo(null)); @@ -21,6 +24,7 @@ export function SettingsPage({ catalog }: { catalog: Catalog }) { void window.rpa.settings.get().then((value) => { setSettings(value); setWaitDraft(String(value.browserElementWaitMs)); + setAiDraft(value.aiConfig ?? { baseUrl: "", apiKey: "", model: "" }); }).catch(() => setSettings(null)); }, []); @@ -43,6 +47,20 @@ export function SettingsPage({ catalog }: { catalog: Catalog }) { setWaitSaving(false); } }; + const saveAiConfig = async () => { + setAiSaving(true); + setAiMessage(null); + try { + const saved = await window.rpa.settings.update({ aiConfig: aiDraft }); + setSettings(saved); + setAiMessage("AI 服务配置已保存"); + } catch (error) { + setAiMessage(error instanceof Error ? error.message : String(error)); + } finally { + setAiSaving(false); + } + }; + const executable = catalog.components.filter((item) => item.implemented).length; const groups = useMemo(() => catalog.groups.map((group) => { @@ -58,6 +76,7 @@ export function SettingsPage({ catalog }: { catalog: Catalog }) { +
@@ -117,6 +136,33 @@ export function SettingsPage({ catalog }: { catalog: Catalog }) {

浏览器导航不限制域名。只拒绝 HTTP/HTTPS 以外的协议;文件协议、脚本协议和任意本地路径不会由浏览器导航组件打开。

)} + + {tab === "ai" && ( + <> + +
+ + setAiDraft({ ...aiDraft, baseUrl: event.target.value })} placeholder="https://api.openai.com/v1" /> +
+
+ + setAiDraft({ ...aiDraft, apiKey: event.target.value })} placeholder="sk-..." /> +
+
+ + setAiDraft({ ...aiDraft, model: event.target.value })} placeholder="gpt-4o-mini / deepseek-chat / qwen-plus" /> +
+
+ + {aiMessage && {aiMessage}} +
+ 兼容 OpenAI Chat Completions 协议(含本地部署服务)。配置后 AI 文本生成、关键词、纠错、审核、情感分析等能力即可用。 +
+ + )}
); diff --git a/src/shared/types.ts b/src/shared/types.ts index 55b384c..e174e55 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -179,8 +179,15 @@ export interface AppInfo { fixtureUrl?: string; } +export interface AiServiceConfig { + baseUrl: string; + apiKey: string; + model: string; +} + export interface AppSettings { browserElementWaitMs: number; + aiConfig: AiServiceConfig; } export interface AppSettings {