From 3f15dd0ea3681ceca517977e6eeb9f5693982a18 Mon Sep 17 00:00:00 2001 From: db_huang <18006480240@163.com> Date: Thu, 17 Sep 2026 10:51:00 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=89=B9=E9=87=8F=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E5=90=8E=E4=B8=8D=E8=83=BD=E5=8F=91=E5=B8=83=E7=9A=84?= =?UTF-8?q?BUG=EF=BC=8C=E4=BC=98=E5=8C=96=E5=8F=91=E5=B8=83=E6=97=B6?= =?UTF-8?q?=E5=A1=AB=E5=86=99=E4=BF=A1=E6=81=AF=EF=BC=8C=E8=AE=A4=E9=A2=86?= =?UTF-8?q?=E4=B8=8A=E9=99=90=E6=94=B9=E6=88=9020?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/main.py | 180 +++++++++++++++++++++++---- backend/tests/test_auth.py | 81 ++++++++++-- frontend/src/AnswerUserPortal.jsx | 10 +- frontend/src/AskUserPortal.jsx | 8 +- frontend/src/CreateAskTasksPage.jsx | 183 ++++++++++++++-------------- frontend/src/admin.css | 27 ++++ frontend/src/main.jsx | 4 +- frontend/src/productIntent.js | 4 +- 8 files changed, 363 insertions(+), 134 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index 80dfd63..e0f9198 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -44,6 +44,13 @@ app.add_middleware( # confirms a selection, the upload screen has a separate thirty-minute window. ASK_SELECTION_TIMEOUT = timedelta(minutes=1) ASK_UPLOAD_TIMEOUT = timedelta(minutes=30) +# Bumping this prefix invalidates every previously issued public-user token. +# Only Taobao-ID login can issue a token accepted by the current user portals. +PUBLIC_USER_SESSION_PREFIX = "tb2_" + + +def _new_public_user_token() -> str: + return f"{PUBLIC_USER_SESSION_PREFIX}{uuid.uuid4().hex}" def _release_expired_ask_assignments(db: Session, task: AskTask | None = None) -> int: @@ -444,9 +451,11 @@ def _public_answer_task(task: dict) -> dict: def _answer_user_from_token(token: str, db: Session) -> AnswerUser: + if not str(token or "").startswith(PUBLIC_USER_SESSION_PREFIX): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用户登录已失效,请使用真实淘宝ID重新登录") user = db.scalar(select(AnswerUser).where(AnswerUser.auth_token == token)) - if not user: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="回答用户登录已失效") + if not user or not user.taobao_id: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用户登录已失效,请使用真实淘宝ID重新登录") db.expunge(user) db.rollback() return user @@ -523,13 +532,13 @@ def _taobao_login(payload: dict, db: Session) -> AnswerUser: public_id=f"U{uuid.uuid4().int % 10_000_000:07d}", phone=legacy_phone_key, taobao_id=taobao_id, - auth_token=uuid.uuid4().hex, + auth_token=_new_public_user_token(), ) db.add(user) else: user.last_login_at = datetime.utcnow() - if not user.auth_token: - user.auth_token = uuid.uuid4().hex + if not user.auth_token.startswith(PUBLIC_USER_SESSION_PREFIX): + user.auth_token = _new_public_user_token() db.commit(); db.refresh(user) return user @@ -2408,13 +2417,8 @@ def generate_ask_task(payload: dict, user: AdminUser = Depends(current_user), db payload.get("product_qr_image"), payload.get("product_qr_image_name") ) product_name = str(payload.get("product_name", "")).strip() - if not shop_name: - raise HTTPException(status_code=400, detail="店铺名称为必填项") if len(shop_name) > 255: raise HTTPException(status_code=400, detail="店铺名称不能超过255个字符") - product_token_error = _product_token_error(product_link, product_qr_data) - if product_token_error: - raise HTTPException(status_code=400, detail=product_token_error) intent_error = _product_intent_error([product_name, payload.get("category"), payload.get("benefits"), payload.get("selling_points")]) if intent_error: raise HTTPException(status_code=400, detail=intent_error) @@ -2427,16 +2431,10 @@ def generate_ask_task(payload: dict, user: AdminUser = Depends(current_user), db raise HTTPException(status_code=400, detail="提问生成条数必须是正整数") if count < 1: raise HTTPException(status_code=400, detail="提问生成条数必须大于 0") - raw_claim_limit = payload.get("claim_questions_per_user", "") - if str(raw_claim_limit).strip(): - try: - claim_limit = int(raw_claim_limit) - except (TypeError, ValueError): - raise HTTPException(status_code=400, detail="每个用户可认领的问题数量必须是 2-5 的整数") - if claim_limit < 2 or claim_limit > 5: - raise HTTPException(status_code=400, detail="每个用户可认领的问题数量必须在 2-5 条之间") - else: - claim_limit = 5 + # Claim limits belong to the publish workflow and must never prevent AI + # generation. Drafts keep the neutral default until the publish dialog is + # confirmed. + claim_limit = 5 existing_task_id = payload.get("task_id") try: task = db.get(AskTask, int(existing_task_id)) if existing_task_id else None @@ -2601,15 +2599,149 @@ def delete_ask_question(task_id: int, question_id: int, user: AdminUser = Depend return {"success": True, "data": {"id": question_id}} +def _ask_publish_fields(payload: dict) -> tuple[str, str, bytes | None, str | None, str | None, int]: + shop_name = str(payload.get("shop_name", "")).strip() + if not shop_name: + raise HTTPException(status_code=400, detail="请填写店铺名称") + if len(shop_name) > 255: + raise HTTPException(status_code=400, detail="店铺名称不能超过255个字符") + product_link = str(payload.get("product_link", "")).strip() + qr_data, qr_content_type, qr_name = _decode_product_qr_image( + payload.get("product_qr_image"), payload.get("product_qr_image_name") + ) + product_token_error = _product_token_error(product_link, qr_data) + if product_token_error: + raise HTTPException(status_code=400, detail=product_token_error) + raw_claim_limit = payload.get("claim_questions_per_user", "") + if not str(raw_claim_limit).strip(): + raise HTTPException(status_code=400, detail="请填写每个用户可领取的问题数量") + try: + claim_limit = int(raw_claim_limit) + except (TypeError, ValueError): + raise HTTPException(status_code=400, detail="每个用户可领取的问题数量必须是 2-20 的整数") + if claim_limit < 2 or claim_limit > 20: + raise HTTPException(status_code=400, detail="每个用户可领取的问题数量必须在 2-20 条之间") + return shop_name, product_link, qr_data, qr_content_type, qr_name, claim_limit + + +def _apply_ask_publish_fields(task: AskTask, payload: dict) -> None: + shop_name, product_link, qr_data, qr_content_type, qr_name, claim_limit = _ask_publish_fields(payload) + task.shop_name = shop_name + task.product_link = product_link + task.product_qr_image_data = qr_data + task.product_qr_image_content_type = qr_content_type + task.product_qr_image_name = qr_name + task.claim_questions_per_user = claim_limit + + +@app.post("/api/admin/ask-tasks/publish") +def create_and_publish_ask_task(payload: dict, user: AdminUser = Depends(current_user), db: Session = Depends(get_db)): + """Publish the questions currently visible in the editor, with or without an AI draft.""" + raw_questions = payload.get("questions") + if not isinstance(raw_questions, list): + raise HTTPException(status_code=400, detail="请至少选择一个提问") + question_rows = [] + seen = set() + for raw in raw_questions: + item = raw if isinstance(raw, dict) else {"question": raw} + question_text = re.sub(r"\s+", " ", str(item.get("question", "")).strip()) + if not question_text or len(question_text) > 500 or question_text in seen: + continue + seen.add(question_text) + source = str(item.get("source", "自主添加")).strip() + if source not in {"自主添加", "问答库", "批量导入", "AI生成"}: + source = "自主添加" + question_rows.append((item, question_text, source)) + if not question_rows: + raise HTTPException(status_code=400, detail="请至少选择一个提问") + if len(question_rows) > 500: + raise HTTPException(status_code=400, detail="单个任务最多发布 500 条问题") + + raw_task_id = payload.get("task_id") + try: + task = db.get(AskTask, int(raw_task_id)) if raw_task_id else None + except (TypeError, ValueError): + task = None + if raw_task_id and not task: + raise HTTPException(status_code=404, detail="提问任务草稿不存在") + if task: + _require_ask_task_access(task, user) + if task.status != "草稿": + raise HTTPException(status_code=409, detail="任务已经发布") + else: + task = AskTask( + task_no=f"TASK{datetime.utcnow().strftime('%Y%m%d%H%M%S')}{uuid.uuid4().hex[:4].upper()}", + owner_id=user.id, + shop_name="", + product_link="", + product_name=str(payload.get("product_name", "")).strip() or "待补充商品", + category=str(payload.get("category", "")), + benefits=str(payload.get("benefits", "")), + selling_points=str(payload.get("selling_points", "")), + language_style=str(payload.get("language_style", "")), + target_group=str(payload.get("target_group", "")), + usage_scene=str(payload.get("usage_scene", "")), + purchase_channel=str(payload.get("purchase_channel", "")), + claim_questions_per_user=5, + status="草稿", + ) + db.add(task) + db.flush() + + _apply_ask_publish_fields(task, payload) + if task.questions: + by_id = {question.id: question for question in task.questions} + selected_ids = set() + for item, question_text, _source in question_rows: + try: + question_id = int(item.get("id")) + except (TypeError, ValueError): + continue + question = by_id.get(question_id) + if question: + question.question_text = question_text + question.selected = True + selected_ids.add(question.id) + for question in task.questions: + question.selected = question.id in selected_ids + existing_by_text = {question.question_text: question for question in task.questions} + next_sequence = max((question.sequence_no for question in task.questions), default=0) + 1 + for _item, question_text, source in question_rows: + existing_question = existing_by_text.get(question_text) + if existing_question: + existing_question.selected = True + continue + question = AskQuestion(task_id=task.id, sequence_no=next_sequence, question_text=question_text, source=source, selected=True) + db.add(question) + existing_by_text[question_text] = question + next_sequence += 1 + else: + db.add_all([ + AskQuestion(task_id=task.id, sequence_no=index, question_text=question_text, source=source, selected=True) + for index, (_item, question_text, source) in enumerate(question_rows, start=1) + ]) + if not task.public_token: + task.public_token = _new_ask_public_token(db) + task.status = "已发布" + db.commit() + db.refresh(task) + return {"success": True, "data": _ask_task_data(task)} + + @app.post("/api/admin/ask-tasks/{task_id}/publish") -def publish_ask_task(task_id: int, user: AdminUser = Depends(current_user), db: Session = Depends(get_db)): +def publish_ask_task(task_id: int, payload: dict = Body(default={}), user: AdminUser = Depends(current_user), db: Session = Depends(get_db)): task = db.get(AskTask, task_id) _require_ask_task_access(task, user) if task.status != "草稿": raise HTTPException(status_code=409, detail="任务已经发布") _dedupe_draft_ask_questions(task, db) - product_token_error = _product_token_error(task.product_link, task.product_qr_image_data) - if product_token_error: - raise HTTPException(status_code=400, detail=f"无法发布任务:{product_token_error}") + if payload: + _apply_ask_publish_fields(task, payload) + else: + product_token_error = _product_token_error(task.product_link, task.product_qr_image_data) + if product_token_error: + raise HTTPException(status_code=400, detail=f"无法发布任务:{product_token_error}") + if not task.shop_name: + raise HTTPException(status_code=400, detail="请填写店铺名称") if not any(q.selected for q in task.questions): raise HTTPException(status_code=400, detail="请至少选择一个提问") if not task.public_token: task.public_token = _new_ask_public_token(db) diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py index b00775d..a738ad2 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -164,6 +164,7 @@ def test_taobao_id_login_is_shared_by_both_user_portals(): assert answer_session["taobao_id"] == taobao_id assert answer_session["account"] == taobao_id assert answer_session["phone"] == "" + assert answer_session["token"].startswith("tb2_") ask_login = client.post( "/api/ask/auth/taobao-login", @@ -179,6 +180,32 @@ def test_taobao_id_login_is_shared_by_both_user_portals(): assert invalid.status_code == 400 +def test_legacy_public_user_sessions_are_invalidated_and_rotated_on_taobao_login(): + taobao_id = f"tb{uuid4().hex[:14]}" + legacy_token = uuid4().hex + with main_module.SessionLocal() as db: + user = main_module.AnswerUser( + public_id=f"U{uuid4().int % 10_000_000:07d}", + phone=f"tb_{uuid4().hex[:16]}", + taobao_id=taobao_id, + auth_token=legacy_token, + ) + db.add(user) + db.commit() + + with TestClient(app) as client: + expired = client.get(f"/api/ask/me/assignments?token={legacy_token}") + assert expired.status_code == 401 + assert "淘宝ID重新登录" in expired.json()["detail"] + + renewed = client.post("/api/ask/auth/taobao-login", json={"taobao_id": taobao_id}) + assert renewed.status_code == 200 + renewed_token = renewed.json()["data"]["token"] + assert renewed_token.startswith("tb2_") + assert renewed_token != legacy_token + assert client.get(f"/api/ask/me/assignments?token={renewed_token}").status_code == 200 + + def test_answer_task_publish_remembers_operator_shop_name(): unique = uuid4().hex[:10] remembered_shop = f"记忆店铺-{unique}" @@ -508,15 +535,12 @@ def test_ask_task_publish_appears_on_task_board(monkeypatch): monkeypatch.setattr(main_module, "generate_questions", fake_ask_questions) with TestClient(app) as client: assert client.post("/api/admin/auth/login", json={"username": "operator", "password": "operator123456"}).status_code == 200 - missing_link = client.post("/api/admin/ask-tasks/generate", json={"product_name": "测试商品"}) - assert missing_link.status_code == 400 - missing_product_access = client.post( + generation_without_publish_fields = client.post( "/api/admin/ask-tasks/generate", - json={"shop_name": "测试店铺", "product_name": "测试商品", "question_count": 3}, + json={"product_name": "测试商品", "question_count": 3}, ) - assert missing_product_access.status_code == 400 - missing_shop = client.post("/api/admin/ask-tasks/generate", json={"product_link": "https://item.example.com/1", "product_name": "测试商品", "question_count": 3}) - assert missing_shop.status_code == 400 + assert generation_without_publish_fields.status_code == 200 + assert generation_without_publish_fields.json()["data"]["status"] == "草稿" generated = client.post("/api/admin/ask-tasks/generate", json={"shop_name": "测试店铺", "product_link": "https://item.example.com/1", "product_name": "测试商品", "question_count": 3}) assert generated.status_code == 200 task = generated.json()["data"] @@ -540,6 +564,49 @@ def test_ask_task_publish_appears_on_task_board(monkeypatch): assert detail.json()["data"]["shop_name"] == "测试店铺" +def test_imported_questions_can_publish_without_ai_generation(): + with TestClient(app) as client: + assert client.post("/api/admin/auth/login", json={"username": "operator", "password": "operator123456"}).status_code == 200 + published = client.post( + "/api/admin/ask-tasks/publish", + json={ + "shop_name": "直接发布测试店铺", + "product_link": "TOKEN-DIRECT-PUBLISH", + "product_name": "直接发布测试商品", + "claim_questions_per_user": 20, + "questions": [ + {"id": "new-import-1", "question": "批量导入问题一?", "source": "批量导入"}, + {"id": "new-manual-2", "question": "自主添加问题二?", "source": "自主添加"}, + ], + }, + ) + assert published.status_code == 200 + data = published.json()["data"] + assert data["status"] == "已发布" + assert data["shop_name"] == "直接发布测试店铺" + assert data["claim_questions_per_user"] == 20 + assert [item["question"] for item in data["selected_questions"]] == ["批量导入问题一?", "自主添加问题二?"] + assert [item["source"] for item in data["selected_questions"]] == ["批量导入", "自主添加"] + + missing_fields = client.post( + "/api/admin/ask-tasks/publish", + json={"questions": [{"question": "不能缺少发布信息?", "source": "批量导入"}]}, + ) + assert missing_fields.status_code == 400 + + over_limit = client.post( + "/api/admin/ask-tasks/publish", + json={ + "shop_name": "领取上限测试店铺", + "product_link": "TOKEN-CLAIM-LIMIT", + "claim_questions_per_user": 21, + "questions": [{"question": "超过领取上限?", "source": "自主添加"}], + }, + ) + assert over_limit.status_code == 400 + assert over_limit.json()["detail"] == "每个用户可领取的问题数量必须在 2-20 条之间" + + def test_ask_task_generation_honors_question_count(monkeypatch): monkeypatch.setattr(main_module, "generate_questions", fake_ask_questions) with TestClient(app) as client: diff --git a/frontend/src/AnswerUserPortal.jsx b/frontend/src/AnswerUserPortal.jsx index 1bc9168..13a83d1 100644 --- a/frontend/src/AnswerUserPortal.jsx +++ b/frontend/src/AnswerUserPortal.jsx @@ -1,7 +1,8 @@ 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' -const SESSION_KEY = 'answer_user_taobao_session' +const SESSION_KEY = 'answer_user_taobao_session_v2' +const LEGACY_SESSION_KEYS = ['answer_user_taobao_session', 'answer_user_session'] const STATUS_TEXT = { '待筛选': '截图审核中', '识别失败': '运营处理中', '待运营派发': '截图审核中', '已派发': '截图审核中', '待用户完成': '待完成回答', '待审核': '完成审核中', '已通过': '已完成', '已完成': '已返款', '未通过': '审核不通过', '已撤销': '任务已撤销' } const accountName = session => session?.taobao_id || session?.account || session?.user_id || '' const formatTime = value => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '--' @@ -29,7 +30,7 @@ function Login({ api, onLogin, linkToken }) { 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) } catch (err) { setError(err.message) } finally { setBusy(false) } } - return
淘宝问答运营官方承接通道官方认证

淘宝账号登录

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

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

淘宝账号登录

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

{error &&
{error}
}
} function UploadStage({ api, session, onUploaded, setError, linkToken }) { @@ -128,7 +129,7 @@ function HistoryPage({ records, close, onOpen, openingId }) { } export default function AnswerUserPortalV2({ api, linkToken = '' }) { - const [session, setSession] = useState(() => { try { return JSON.parse(localStorage.getItem(SESSION_KEY) || 'null') } catch { return null } }) + const [session, setSession] = useState(() => { try { LEGACY_SESSION_KEYS.forEach(key => localStorage.removeItem(key)); return JSON.parse(localStorage.getItem(SESSION_KEY) || 'null') } catch { return null } }) const [records, setRecords] = useState([]); const [error, setError] = useState(''); const [busy, setBusy] = useState(false); const [loaded, setLoaded] = useState(false); const [historyOpen, setHistoryOpen] = useState(false); const [retrying, setRetrying] = useState(false); const [newTaskMode, setNewTaskMode] = useState(false); const [activeRecordId, setActiveRecordId] = useState(null); const [optimisticStatuses, setOptimisticStatuses] = useState({}); const [openingId, setOpeningId] = useState(null) const imageLoadPromises = useRef(new Map()) const loadVersion = useRef(0) @@ -173,6 +174,7 @@ export default function AnswerUserPortalV2({ api, linkToken = '' }) { }) setError('') } catch (err) { + if (err.message.includes('登录已失效')) { logout(); return } if (version === loadVersion.current) setError(err.message) } finally { if (version === loadVersion.current) { setLoaded(true); if (!silent) setBusy(false) } @@ -214,7 +216,7 @@ export default function AnswerUserPortalV2({ api, linkToken = '' }) { } const latestRecord = records.find(record => record.id === activeRecordId) || records[0] const latest = latestRecord && optimisticStatuses[latestRecord.id] ? { ...latestRecord, status: optimisticStatuses[latestRecord.id] } : latestRecord - const logout = () => { localStorage.removeItem(SESSION_KEY); setSession(null); setRecords([]); setLoaded(false); setOptimisticStatuses({}) } + const logout = () => { localStorage.removeItem(SESSION_KEY); LEGACY_SESSION_KEYS.forEach(key => localStorage.removeItem(key)); setSession(null); setRecords([]); setLoaded(false); setOptimisticStatuses({}) } function applySubmittedRecord(submitted) { if (!submitted?.id) return setOptimisticStatuses(current => ({ ...current, [submitted.id]: submitted.status })) diff --git a/frontend/src/AskUserPortal.jsx b/frontend/src/AskUserPortal.jsx index 123911c..e2d72ca 100644 --- a/frontend/src/AskUserPortal.jsx +++ b/frontend/src/AskUserPortal.jsx @@ -12,7 +12,8 @@ import { Trash2, } from "lucide-react"; -const SESSION_KEY = "ask_user_taobao_session"; +const SESSION_KEY = "ask_user_taobao_session_v2"; +const LEGACY_SESSION_KEYS = ["ask_user_taobao_session", "ask_user_session"]; const EXHAUSTED_MESSAGE = "本次任务已被领取完,请关注下一个任务!"; const COMPLETED_TASK_MESSAGE = "本次提问任务已全部完成,可查看历史状态"; const INVALID_TASK_MESSAGE = "提问任务链接无效、未发布或已结束"; @@ -132,6 +133,7 @@ function TaobaoLogin({ api, onLogin }) { autoCorrect="off" placeholder="例如:tb1008611" /> + 为了运营更好地核对信息,请填写真实的淘宝ID {error &&
{error}
} -
- - selectProductQr(event.target.files?.[0] || null)} - /> - {form.product_qr_image ? ( -
- 商品二维码预览 -
- {form.product_qr_image_name || "商品二维码"} - 已上传商品二维码,可与淘口令同时使用 - -
-
- ) : ( - - )} - 淘口令和二维码至少填写一种,也可以两者都填写 -

商品信息

-
- - -
+

生成偏好与数量

-
- - -
+
@@ -836,6 +794,43 @@ export default function CreateAskTasksPage({ api, librarySelection = [], onLibra )} {addDialog === "manual" && setAddDialog(null)} onAdd={addManualQuestion} />} {addDialog === "import" && setAddDialog(null)} onImport={addImportedQuestions} />} + {publishOpen && ( +
event.target === event.currentTarget && !busy && setPublishOpen(false)}> +
{ event.preventDefault(); publish(); }}> +
+

发布提问任务

本次将发布 {questions.some((item) => item.selected) ? questions.filter((item) => item.selected).length : questions.length} 条问题

+ +
+ +
+ + selectProductQr(event.target.files?.[0] || null)} /> + {form.product_qr_image ? ( +
+ 商品二维码预览 +
{form.product_qr_image_name || "商品二维码"}已上传商品二维码
+
+ ) : ( + + )} + 淘口令和二维码至少填写一种,也可以两者都填写 +
+ + {error &&
{error}
} +
+
+
+ )} ); } diff --git a/frontend/src/admin.css b/frontend/src/admin.css index 36e0c14..bf70282 100644 --- a/frontend/src/admin.css +++ b/frontend/src/admin.css @@ -3871,6 +3871,12 @@ color: #466181; font-size: 12px; } +.user-login-notice { + color: #d47a00; + font-size: 10px; + font-weight: 500; + line-height: 1.5; +} .answer-v2-login input { padding: 12px; border: 1px solid #c5d5e8; @@ -5133,6 +5139,27 @@ padding: 3px 7px; font-size: 10px; } +.ask-publish-dialog { + width: min(520px, calc(100vw - 28px)); +} +.ask-publish-dialog .manual-modal-header > div { + min-width: 0; +} +.ask-publish-dialog .manual-modal-header p { + margin: 4px 0 0; + color: #8194aa; + font-size: 11px; +} +.ask-publish-dialog > label > small { + color: #8296ad; + font-size: 10px; +} +.ask-publish-dialog .ask-product-token-field { + margin-top: 12px; +} +.ask-publish-dialog .form-error { + margin-top: 14px; +} .ask-results { min-width: 0; } diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 981165e..8809bc3 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -3854,7 +3854,7 @@ function GlobalImageViewer() { function Dashboard({ user, onLogout }) { const [active, setActive] = useState("assistant"); - const [createMode, setCreateMode] = useState("answer"); + const [createMode, setCreateMode] = useState("ask"); const [reviewBadge, setReviewBadge] = useState(0); const [librarySelection, setLibrarySelection] = useState([]); async function loadReviewBadge() { @@ -3917,7 +3917,7 @@ function Dashboard({ user, onLogout }) { const selectPage = (next) => { if (user.permissions.includes(next)) { setActive(next); - if (next === "create") setCreateMode("answer"); + if (next === "create") setCreateMode("ask"); if (next === "review") loadReviewBadge(); } }; diff --git a/frontend/src/productIntent.js b/frontend/src/productIntent.js index 104ef24..7000fb0 100644 --- a/frontend/src/productIntent.js +++ b/frontend/src/productIntent.js @@ -38,8 +38,8 @@ export function validateGenerationOptions(form, includeAnswers = true) { } if (String(form.claim_questions_per_user ?? "").trim()) { const claimCount = Number(form.claim_questions_per_user); - if (!Number.isInteger(claimCount) || claimCount < 2 || claimCount > 5) { - return "每个用户可认领的问题数量必须在 2-5 条之间"; + if (!Number.isInteger(claimCount) || claimCount < 2 || claimCount > 20) { + return "每个用户可认领的问题数量必须在 2-20 条之间"; } } return "";