From 3ab8b525935970bb9ffb5f72381e698790f1c6c9 Mon Sep 17 00:00:00 2001 From: db_huang <18006480240@163.com> Date: Thu, 17 Sep 2026 14:30:10 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=99=BB=E5=BD=95=E6=97=B6?= =?UTF-8?q?=E6=B7=98=E5=AE=9DID=E7=9A=84=E8=BE=93=E5=85=A5=E5=90=88?= =?UTF-8?q?=E6=B3=95=E6=80=A7=EF=BC=8C=E9=97=AE=E7=AD=94=E5=8A=A9=E6=89=8B?= =?UTF-8?q?=E5=8F=AF=E6=89=B9=E9=87=8F=E5=AF=BC=E5=85=A5=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=9B=9E=E7=AD=94=EF=BC=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/llm.py | 98 +++++++++++++++- backend/app/main.py | 26 ++++- backend/app/schemas.py | 15 +++ backend/tests/test_auth.py | 63 ++++++++++ frontend/src/AnswerUserPortal.jsx | 8 +- frontend/src/AskUserPortal.jsx | 16 ++- frontend/src/admin.css | 51 +++++++++ frontend/src/main.jsx | 184 +++++++++++++++++++++++++++++- frontend/src/taobaoId.js | 11 ++ 9 files changed, 453 insertions(+), 19 deletions(-) create mode 100644 frontend/src/taobaoId.js diff --git a/backend/app/llm.py b/backend/app/llm.py index c25b722..581089b 100644 --- a/backend/app/llm.py +++ b/backend/app/llm.py @@ -13,7 +13,7 @@ from PIL import Image, ImageOps from fastapi import HTTPException, status from .config import settings -from .schemas import QAGenerateRequest +from .schemas import QABulkAnswerRequest, QAGenerateRequest logger = logging.getLogger(__name__) @@ -119,6 +119,102 @@ def generate_qa(payload: QAGenerateRequest) -> list[dict]: return items[:target] +def _bulk_answers_prompt(payload: QABulkAnswerRequest, questions: list[str]) -> str: + style = "带真实体验的轻度种草语气" if payload.seed_style else "客观自然,不刻意种草" + question_lines = "\n".join(f"{index}. {question}" for index, question in enumerate(questions, start=1)) + return f"""你是电商问大家回答助手。请严格按照给定问题逐条生成参考回答,不要新增、删除、合并或改写问题。 +商品名称:{payload.product_name or '未提供'} +商品品类:{payload.category or '未提供'} +商品功效:{payload.benefits or '未提供'} +商品卖点:{payload.selling_points or '未提供'} +语言风格:{payload.language_style or '客观自然'} +目标人群:{payload.target_group or '普通消费者'} +使用场景:{payload.usage_scene or '日常使用'} +购买渠道:{payload.purchase_channel or '电商平台'} +回答要求:{payload.answer_requirement or '直接回应问题,避免夸大功效'} +回答语气:{style} + +问题列表: +{question_lines} + +每个问题生成 {payload.answers_per_question} 条不同角度的回答。回答尽量不超过50字,不虚构成分、检测、认证和医疗功效。 +只返回严格 JSON,不要 Markdown 或解释。必须按问题序号原顺序返回,格式: +{{"items":[{{"index":1,"answers":["回答1","回答2"]}}]}}""" + + +def _generate_bulk_answers_batch(payload: QABulkAnswerRequest, questions: list[str]) -> list[list[str]]: + if not settings.llm_api_key: + raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="尚未配置大模型 API Key,请先设置 LLM_API_KEY") + url = settings.llm_base_url.rstrip("/") + "/chat/completions" + request_body = { + "model": settings.llm_model, + "temperature": 0.75, + "messages": [ + {"role": "system", "content": "你只输出合法 JSON,并严格保持输入问题的数量和顺序。"}, + {"role": "user", "content": _bulk_answers_prompt(payload, questions)}, + ], + } + request = urllib.request.Request( + url, + data=json.dumps(request_body).encode("utf-8"), + headers={"Content-Type": "application/json", "Authorization": f"Bearer {settings.llm_api_key}"}, + method="POST", + ) + try: + with urllib.request.urlopen(request, timeout=settings.llm_timeout_seconds) as response: + result = json.loads(response.read().decode("utf-8")) + content = str(result["choices"][0]["message"]["content"]).strip() + cleaned = re.sub(r"^```(?:json)?\s*|\s*```$", "", content, flags=re.I | re.S).strip() + parsed = json.loads(cleaned) + raw_items = parsed.get("items") if isinstance(parsed, dict) else parsed + if not isinstance(raw_items, list): + raise ValueError("items is not a list") + by_index: dict[int, list[str]] = {} + for raw in raw_items: + if not isinstance(raw, dict): + continue + try: + index = int(raw.get("index")) + except (TypeError, ValueError): + continue + answers = raw.get("answers") or [] + if isinstance(answers, str): + answers = [answers] + clean_answers = [str(answer).strip() for answer in answers if str(answer).strip()] + if len(clean_answers) >= payload.answers_per_question: + by_index[index] = clean_answers[:payload.answers_per_question] + if any(index not in by_index for index in range(1, len(questions) + 1)): + raise ValueError("answers are incomplete") + return [by_index[index] for index in range(1, len(questions) + 1)] + except urllib.error.HTTPError as exc: + logger.error("LLM bulk-answer HTTP error: status=%s url=%s", exc.code, url) + raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f"大模型服务返回错误(HTTP {exc.code})") from exc + except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, KeyError, IndexError, TypeError, ValueError) as exc: + logger.exception("LLM bulk-answer generation failed") + raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail="批量回答生成不完整,请重试") from exc + + +def generate_answers_for_questions(payload: QABulkAnswerRequest, questions: list[str]) -> list[dict]: + """Generate answers for exact imported questions without rewriting them.""" + items: list[dict] = [] + # Keep each response small enough that the provider can return every + # requested answer without truncating the JSON payload. + batch_size = max(1, min(10, 30 // payload.answers_per_question)) + for offset in range(0, len(questions), batch_size): + batch = questions[offset:offset + batch_size] + answer_groups = _generate_bulk_answers_batch(payload, batch) + for question, answers in zip(batch, answer_groups): + items.append({ + "id": f"Q{len(items) + 1:03d}", + "question": question, + "answers": answers, + "source": "批量导入(AI回答)", + "status": "待采纳", + "accepted": False, + }) + return items + + def _generate_questions_batch(payload: dict, count: int, reference_qa: str = "") -> list[str]: """Generate only consumer questions for the ask-task workflow.""" if not settings.llm_api_key: diff --git a/backend/app/main.py b/backend/app/main.py index e0f9198..8546208 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -24,9 +24,9 @@ from sqlalchemy.orm import Session, defer, noload, selectinload from .config import settings from .database import Base, SessionLocal, engine, get_db -from .llm import extract_questions_from_images, generate_answer_for_question, generate_questions, generate_qa +from .llm import extract_questions_from_images, generate_answer_for_question, generate_answers_for_questions, generate_questions, generate_qa from .models import AdminLoginLog, AdminUser, AnswerRecognizedQuestion, AnswerReviewDecision, AnswerScreenshot, AnswerSubmission, AnswerUser, AskAssignment, AskAssignmentBatch, AskAssignmentBatchReview, AskAssignmentBatchScreenshot, AskAssignmentQuestion, AskAssignmentReview, AskAssignmentScreenshot, AskAssignmentUsedQuestion, AskQuestion, AskTask, QALibraryEntry, RefundUploadSession -from .schemas import AdminUserCreate, AdminUserUpdate, LoginRequest, QAGenerateRequest, RegisterRequest +from .schemas import AdminUserCreate, AdminUserUpdate, LoginRequest, QABulkAnswerRequest, QAGenerateRequest, RegisterRequest from .security import create_session_token, decode_session_token, hash_password, verify_password @@ -513,10 +513,10 @@ def _phone_login(payload: dict, db: Session) -> AnswerUser: def _taobao_id(payload: dict) -> str: value = str(payload.get("taobao_id", "")).strip() - if not re.fullmatch(r"[A-Za-z0-9_.-]{3,64}", value): + if not re.fullmatch(r"[\u3400-\u4dbf\u4e00-\u9fffA-Za-z0-9_.-]{2,64}", value): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="请输入正确的淘宝ID(3-64位字母、数字、下划线、点或短横线)", + detail="请输入正确的淘宝ID(2-64位中文、英文字母、数字、下划线、点或短横线)", ) return value @@ -3542,6 +3542,24 @@ def qa_generate(payload: QAGenerateRequest, _: AdminUser = Depends(current_user) raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail="问答生成服务异常,请检查大模型配置后重试") from exc +@app.post("/api/admin/qa/generate-answers") +def qa_generate_answers(payload: QABulkAnswerRequest, _: AdminUser = Depends(current_user)): + intent_error = _product_intent_error([payload.product_name, payload.category, payload.benefits, payload.selling_points]) + if intent_error: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=intent_error) + questions = _dedupe_import_questions(payload.questions) + if not questions: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="请至少导入一个有效问题") + try: + return {"success": True, "data": {"items": generate_answers_for_questions(payload, questions)}} + except HTTPException: + raise + except Exception as exc: + import logging + logging.getLogger(__name__).exception("Bulk answer generation failed") + raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail="批量回答生成服务异常,请稍后重试") from exc + + def _qa_library_data(entry: QALibraryEntry) -> dict: try: answers = json.loads(entry.answers_json) diff --git a/backend/app/schemas.py b/backend/app/schemas.py index e488582..ee12fbd 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -42,6 +42,21 @@ class QAGenerateRequest(BaseModel): seed_style: bool = True +class QABulkAnswerRequest(BaseModel): + product_name: str = Field(default="", max_length=200) + category: str = Field(default="", max_length=100) + benefits: str = Field(default="", max_length=1000) + selling_points: str = Field(default="", max_length=2000) + language_style: str = Field(default="口语(亲切随性)", max_length=40) + target_group: str = Field(default="女性", max_length=100) + usage_scene: str = Field(default="居家", max_length=100) + purchase_channel: str = Field(default="电商平台", max_length=100) + answers_per_question: int = Field(default=2, ge=1, le=20) + answer_requirement: str = Field(default="", max_length=1000) + seed_style: bool = True + questions: list[str] = Field(min_length=1, max_length=500) + + class UserOut(BaseModel): id: int username: str diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py index a738ad2..e691c06 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -89,6 +89,48 @@ def test_qa_generation_requires_product_and_model_key(): assert client.post("/api/admin/qa/generate", json={"question_count": 2}).status_code == 400 +def test_imported_questions_can_generate_configured_answer_count(monkeypatch): + captured = {} + + def fake_bulk_answers(payload, questions): + captured["questions"] = questions + captured["answers_per_question"] = payload.answers_per_question + return [ + { + "id": f"Q{index:03d}", + "question": question, + "answers": [f"回答{answer_index}" for answer_index in range(1, payload.answers_per_question + 1)], + "source": "批量导入(AI回答)", + "status": "待采纳", + "accepted": False, + } + for index, question in enumerate(questions, start=1) + ] + + monkeypatch.setattr(main_module, "generate_answers_for_questions", fake_bulk_answers) + payload = { + "product_name": "批量回答测试商品", + "questions": ["导入问题一?", "导入问题二?", "导入问题一?"], + "answers_per_question": 3, + } + with TestClient(app) as client: + assert client.post("/api/admin/qa/generate-answers", json=payload).status_code == 401 + assert client.post("/api/admin/auth/login", json={"username": "operator", "password": "operator123456"}).status_code == 200 + assert client.post( + "/api/admin/qa/generate-answers", + json={"questions": ["缺少商品信息?"], "answers_per_question": 2}, + ).status_code == 400 + response = client.post("/api/admin/qa/generate-answers", json=payload) + assert response.status_code == 200 + assert captured == { + "questions": ["导入问题一?", "导入问题二?"], + "answers_per_question": 3, + } + items = response.json()["data"]["items"] + assert [item["question"] for item in items] == captured["questions"] + assert all(len(item["answers"]) == 3 for item in items) + + def test_accepted_qa_is_saved_to_library_without_duplicates(): unique_product = f"问答库测试商品-{uuid4().hex[:8]}" payload = { @@ -179,6 +221,27 @@ def test_taobao_id_login_is_shared_by_both_user_portals(): ) assert invalid.status_code == 400 + chinese_taobao_id = f"淘宝用户{uuid4().hex[:6]}" + chinese_login = client.post( + "/api/ask/auth/taobao-login", + json={"taobao_id": chinese_taobao_id}, + ) + assert chinese_login.status_code == 200 + assert chinese_login.json()["data"]["taobao_id"] == chinese_taobao_id + + answer_chinese_login = client.post( + "/api/answer/auth/taobao-login", + json={"taobao_id": chinese_taobao_id, "owner_token": owner_token}, + ) + assert answer_chinese_login.status_code == 200 + assert answer_chinese_login.json()["data"]["token"] == chinese_login.json()["data"]["token"] + + invalid_symbol = client.post( + "/api/ask/auth/taobao-login", + json={"taobao_id": "淘宝用户@测试"}, + ) + assert invalid_symbol.status_code == 400 + def test_legacy_public_user_sessions_are_invalidated_and_rotated_on_taobao_login(): taobao_id = f"tb{uuid4().hex[:14]}" diff --git a/frontend/src/AnswerUserPortal.jsx b/frontend/src/AnswerUserPortal.jsx index 13a83d1..1165370 100644 --- a/frontend/src/AnswerUserPortal.jsx +++ b/frontend/src/AnswerUserPortal.jsx @@ -1,5 +1,6 @@ import React, { useEffect, useMemo, useRef, useState } from 'react' import { Check, ClipboardCheck, Clock3, Copy, Eye, FileCheck2, History, ImagePlus, LogOut, MessageCircle, ReceiptText, RefreshCw, ShieldCheck } from 'lucide-react' +import { isValidTaobaoId, normalizeTaobaoId, TAOBAO_ID_HINT } from './taobaoId.js' const SESSION_KEY = 'answer_user_taobao_session_v2' const LEGACY_SESSION_KEYS = ['answer_user_taobao_session', 'answer_user_session'] @@ -25,12 +26,13 @@ function Login({ api, onLogin, linkToken }) { const [taobaoId, setTaobaoId] = useState(''); const [busy, setBusy] = useState(false); const [error, setError] = useState('') async function submit(event) { event.preventDefault(); setError('') - if (!/^[A-Za-z0-9_.-]{3,64}$/.test(taobaoId.trim())) return setError('请输入正确的淘宝ID') + const account = normalizeTaobaoId(taobaoId) + if (!isValidTaobaoId(account)) return setError(TAOBAO_ID_HINT) setBusy(true) - try { const result = await api('/api/answer/auth/taobao-login', { method: 'POST', body: JSON.stringify({ taobao_id: taobaoId.trim(), owner_token: linkToken || undefined }) }); localStorage.setItem(SESSION_KEY, JSON.stringify(result.data)); onLogin(result.data) } + try { const result = await api('/api/answer/auth/taobao-login', { method: 'POST', body: JSON.stringify({ taobao_id: account, owner_token: linkToken || undefined }) }); localStorage.setItem(SESSION_KEY, JSON.stringify(result.data)); onLogin(result.data) } catch (err) { setError(err.message) } finally { setBusy(false) } } - return
淘宝问答运营官方承接通道官方认证

淘宝账号登录

输入淘宝ID即可进入任务中心

{error &&
{error}
}
+ return
淘宝问答运营官方承接通道官方认证

淘宝账号登录

输入淘宝ID即可进入任务中心

{error &&
{error}
}
} function UploadStage({ api, session, onUploaded, setError, linkToken }) { diff --git a/frontend/src/AskUserPortal.jsx b/frontend/src/AskUserPortal.jsx index e2d72ca..d94a01c 100644 --- a/frontend/src/AskUserPortal.jsx +++ b/frontend/src/AskUserPortal.jsx @@ -11,6 +11,7 @@ import { ShieldCheck, Trash2, } from "lucide-react"; +import { isValidTaobaoId, normalizeTaobaoId, TAOBAO_ID_HINT } from "./taobaoId.js"; const SESSION_KEY = "ask_user_taobao_session_v2"; const LEGACY_SESSION_KEYS = ["ask_user_taobao_session", "ask_user_session"]; @@ -86,8 +87,9 @@ function TaobaoLogin({ api, onLogin }) { async function submit(event) { event.preventDefault(); - if (!/^[A-Za-z0-9_.-]{3,64}$/.test(taobaoId.trim())) { - setError("请输入正确的淘宝ID"); + const account = normalizeTaobaoId(taobaoId); + if (!isValidTaobaoId(account)) { + setError(TAOBAO_ID_HINT); return; } setBusy(true); @@ -95,7 +97,7 @@ function TaobaoLogin({ api, onLogin }) { try { const result = await api("/api/ask/auth/taobao-login", { method: "POST", - body: JSON.stringify({ taobao_id: taobaoId.trim() }), + body: JSON.stringify({ taobao_id: account }), }); localStorage.setItem(SESSION_KEY, JSON.stringify(result.data)); onLogin(result.data); @@ -128,12 +130,14 @@ function TaobaoLogin({ api, onLogin }) { autoFocus maxLength="64" value={taobaoId} - onChange={(event) => setTaobaoId(event.target.value.replace(/\s/g, ""))} + onChange={(event) => setTaobaoId(event.target.value.slice(0, 64))} + inputMode="text" + lang="zh-CN" autoCapitalize="none" autoCorrect="off" - placeholder="例如:tb1008611" + placeholder="例如:tb1008611 或 淘宝用户" /> - 为了运营更好地核对信息,请填写真实的淘宝ID + 为了运营更好地核对信息,请填写真实的淘宝ID;支持中文、字母和数字 {error &&
{error}
} + @@ -930,12 +933,13 @@ function InlineQAItem({ item, onToggle, onDelete, onSave }) { ) : ( <>

问题:{item.question}

- {item.answers.map((answer, index) => ( + {(item.answers || []).map((answer, index) => (

回答{index + 1}: {answer}

))} + {!(item.answers || []).length &&

尚未生成回答,请点击“编辑”补充

} )} @@ -1029,6 +1033,107 @@ function ManualQAForm({ onClose, onSave }) { ); } +function AssistantQuestionImportModal({ form, onClose, onImport }) { + const inputRef = useRef(null); + const [file, setFile] = useState(null); + const [questions, setQuestions] = useState([]); + const [generateAnswers, setGenerateAnswers] = useState(true); + const [answersPerQuestion, setAnswersPerQuestion] = useState( + String(Number(form.answers_per_question) || 2), + ); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + + async function selectFile(nextFile) { + setFile(nextFile || null); + setQuestions([]); + setError(""); + if (!nextFile) return; + setBusy(true); + try { + const body = new FormData(); + body.append("file", nextFile); + const result = await api("/api/admin/ask-questions/import", { method: "POST", body }); + setQuestions(result.data.questions || []); + } catch (err) { + setError(err.message || "文件解析失败"); + } finally { + setBusy(false); + } + } + + async function submit(event) { + event.preventDefault(); + if (!questions.length || busy) return; + const answerCount = Number(answersPerQuestion); + if (generateAnswers && (!Number.isInteger(answerCount) || answerCount < 1 || answerCount > 20)) { + setError("每个问题的回答条数必须是 1-20 的整数"); + return; + } + setBusy(true); + setError(""); + try { + await onImport(questions, generateAnswers, answerCount); + } catch (err) { + setError(err.message || "批量导入失败"); + setBusy(false); + } + } + + return ( +
event.target === event.currentTarget && !busy && onClose()}> +
+
+

批量导入问题

+ +
+

导入本地问题文件,并选择是否为每个问题自动生成参考回答。

+ selectFile(event.target.files?.[0] || null)} + /> + + TXT、Markdown 每行一个问题;表格文件优先读取“问题”列。 +
+ 是否自动生成回答 +
+ + +
+ {generateAnswers && ( + + )} +
+ {busy && !questions.length &&
正在解析文件…
} + {error &&
{error}
} + {questions.length > 0 && ( +
+
识别到 {questions.length} 条问题重复问题已自动排除
+
    {questions.slice(0, 8).map((question, index) =>
  1. {question}
  2. )}
+ {questions.length > 8 && 还有 {questions.length - 8} 条未展开显示} +
+ )} +
+ + +
+
+
+ ); +} + function AssistantPageV2() { const [form, setForm] = useState({ product_name: "", @@ -1587,6 +1692,7 @@ function AssistantPageFinal() { const [savedMessage, setSavedMessage] = useState(""); const [adoptedDialog, setAdoptedDialog] = useState(null); const [manualOpen, setManualOpen] = useState(false); + const [importOpen, setImportOpen] = useState(false); const [referenceFile, setReferenceFile] = useState(null); const [referenceFileName, setReferenceFileName] = useState(cached.referenceFileName || ""); const [referenceQa, setReferenceQa] = useState(cached.referenceQa || ""); @@ -1657,6 +1763,13 @@ function AssistantPageFinal() { } async function adopt(targetItems, showDialog = false) { if (!targetItems.length || saving) return; + const readyItems = targetItems.filter((item) => + (item.answers || []).some((answer) => String(answer || "").trim()), + ); + if (!readyItems.length) { + setError("请先为问题补充回答,再采纳到问答库"); + return; + } setSaving(true); setSavedMessage(""); try { @@ -1667,14 +1780,14 @@ function AssistantPageFinal() { category: form.category, benefits: form.benefits, selling_points: form.selling_points, - items: targetItems.map(({ question, answers, source }) => ({ + items: readyItems.map(({ question, answers, source }) => ({ question, answers, source, })), }), }); - const ids = new Set(targetItems.map((item) => item.id)); + const ids = new Set(readyItems.map((item) => item.id)); setItems((current) => current.map((item) => ids.has(item.id) @@ -1683,7 +1796,7 @@ function AssistantPageFinal() { ), ); const { added_count: added, skipped_count: skipped } = result.data; - if (showDialog) setAdoptedDialog({ count: targetItems.length, added, skipped }); + if (showDialog) setAdoptedDialog({ count: readyItems.length, added, skipped }); setSavedMessage( added > 0 ? `已将 ${added} 条问答存入问答库${skipped ? `,${skipped} 条重复内容已跳过` : ""}` @@ -1700,6 +1813,10 @@ function AssistantPageFinal() { const target = items.find((item) => item.id === id); if (!target) return; if (target.accepted) return; + if (!(target.answers || []).some((answer) => String(answer || "").trim())) { + setError("请先为该问题补充回答,再采纳到问答库"); + return; + } adopt([target]); } const acceptAll = () => adopt(items, true); @@ -1727,6 +1844,54 @@ function AssistantPageFinal() { ]); setManualOpen(false); }; + async function addImportedQuestions(questions, generateAnswers, answersPerQuestion) { + const existing = new Set(items.map((item) => String(item.question || "").trim().toLocaleLowerCase())); + const newQuestions = questions.filter((question) => !existing.has(String(question || "").trim().toLocaleLowerCase())); + if (!newQuestions.length) { + setSavedMessage("导入的问题已全部存在于当前列表中"); + setImportOpen(false); + return; + } + let importedItems; + if (generateAnswers) { + const intentError = analyzeProductIntent([ + form.product_name, + form.category, + form.benefits, + form.selling_points, + ]); + if (intentError) throw new Error(`自动生成回答前,${intentError}`); + const result = await api("/api/admin/qa/generate-answers", { + method: "POST", + body: JSON.stringify({ + ...form, + questions: newQuestions, + answers_per_question: answersPerQuestion, + }), + }); + importedItems = result.data.items || []; + } else { + importedItems = newQuestions.map((question) => ({ + question, + answers: [], + source: "批量导入", + status: "待补充回答", + accepted: false, + })); + } + const idPrefix = `IMP${Date.now()}`; + setItems((current) => [ + ...current, + ...importedItems.map((item, index) => ({ ...item, id: `${idPrefix}-${index + 1}` })), + ]); + setSavedMessage( + generateAnswers + ? `已导入 ${importedItems.length} 条问题,并生成每题 ${answersPerQuestion} 条回答` + : `已导入 ${importedItems.length} 条问题,可点击“编辑”补充回答`, + ); + setError(""); + setImportOpen(false); + } const selected = items.filter((item) => item.accepted).length; return ( <> @@ -1920,6 +2085,12 @@ function AssistantPageFinal() { > + 手动添加Q&A +