diff --git a/ui/src/api/review.js b/ui/src/api/review.js index e4419a1..ca4bddb 100644 --- a/ui/src/api/review.js +++ b/ui/src/api/review.js @@ -6,7 +6,7 @@ // 而 http.js 只对非 2xx 抛 ApiError —— 200 信封会正常 resolve。 // unwrap() 把两条错误路径(200 信封 / 非 2xx http.js 抛出)归一成 ApiError, // 调用方只需 catch + reviewErrorMessage(err) 一个口径。 -import { get, post, ApiError } from './http.js'; +import { get, post, patch, del, ApiError } from './http.js'; const BASE = '/api/review'; @@ -133,3 +133,71 @@ export function reviewErrorMessage(err) { } return '服务异常,请稍后重试'; } + +/* —— Rebuttal Phase 1(审稿回复助手,10 端点)—— */ +const RB = `${BASE}/rebuttal`; + +/** POST /rebuttal/jobs {text} → {ok, job, meta:{reviewers,comments}} 文本粘贴主入口 */ +export function rebuttalCreateJob(text, opts = {}) { + return unwrap(post(`${RB}/jobs`, { text }, opts)); +} + +/** POST /rebuttal/jobs/pdf(multipart)→ 同上;PDF 次要入口(批注版审稿 PDF) */ +export function rebuttalCreateFromPdf(formData, opts = {}) { + return unwrap(post(`${RB}/jobs/pdf`, formData, opts)); +} + +/** GET /rebuttal/jobs/{id} → {ok, job} 全量还原 */ +export function rebuttalGetJob(id, opts = {}) { + return unwrap(get(`${RB}/jobs/${id}`, {}, opts)); +} + +/** PATCH /rebuttal/jobs/{id}/comments/{cid}/response {parts} → {ok, job} */ +export function rebuttalSaveResponse(id, cid, parts, opts = {}) { + return unwrap(patch(`${RB}/jobs/${id}/comments/${cid}/response`, { parts }, opts)); +} + +/** POST /rebuttal/jobs/{id}/comments/{cid}/draft → {ok, job} 或校验失败 + * {ok:false, error:{code:'VALIDATION_FAILED'}, job, meta:{validation}}(校验C前置)。 + * 不走 unwrap:校验失败信封里 job/meta 也是有效载荷,需原样返回给面板。 */ +export function rebuttalDraft(id, cid, opts = {}) { + return post(`${RB}/jobs/${id}/comments/${cid}/draft`, {}, opts).then((payload) => { + if (payload && payload.ok === false && payload.error) { + const err = new ApiError(payload.error.message || '草稿生成失败', { + code: payload.error.code || 'INTERNAL', + status: 200, + degraded: !!payload.error.degraded, + }); + err.payload = payload; // 信封全量(含 job + meta.validation) + throw err; + } + return payload; + }); +} + +/** PATCH /rebuttal/jobs/{id}/comments/{cid}/confirm {confirmed} → {ok, job, meta.human_ack} */ +export function rebuttalConfirm(id, cid, confirmed, opts = {}) { + return unwrap(patch(`${RB}/jobs/${id}/comments/${cid}/confirm`, { confirmed }, opts)); +} + +/** POST /rebuttal/jobs/{id}/letter → {ok, job}(job.letter_md 已拼装) */ +export function rebuttalLetter(id, opts = {}) { + return unwrap(post(`${RB}/jobs/${id}/letter`, {}, opts)); +} + +/** GET /rebuttal/jobs/{id}/letter.md —— 纯文本下载(human_ack 门槛,非信封)。 + * 返回 Promise;400 时抛 ApiError(code 取自响应文本前缀)。 */ +export async function rebuttalDownloadLetterMd(id) { + const resp = await fetch(`${RB}/jobs/${id}/letter.md`); + if (!resp.ok) { + const text = await resp.text().catch(() => ''); + const code = text.startsWith('HUMAN_ACK_REQUIRED') ? 'HUMAN_ACK_REQUIRED' : 'DOWNLOAD_FAILED'; + throw new ApiError(text || `下载失败(HTTP ${resp.status})`, { code, status: resp.status }); + } + return resp.text(); +} + +/** DELETE /rebuttal/jobs/{id} → {ok:true, meta:{deleted}} 手动清(无 TTL) */ +export function rebuttalDeleteJob(id, opts = {}) { + return unwrap(del(`${RB}/jobs/${id}`, opts)); +} diff --git a/ui/src/components/edu/EduSideNav.vue b/ui/src/components/edu/EduSideNav.vue index 4e89d4f..1219aa0 100644 --- a/ui/src/components/edu/EduSideNav.vue +++ b/ui/src/components/edu/EduSideNav.vue @@ -16,7 +16,8 @@ const ITEMS = [ { key: 'report', label: '专业审稿意见报告', icon: '📝' }, { key: 'format', label: '论文排版与规范', icon: '📐' }, { key: 'reformat', label: '论文自动重排版', icon: '🧩' }, - { key: 'tracker', label: '投稿进度与返修', icon: '📮' }, + // SVG 内联(U+1F4AC/U+2709 在用户 Ubuntu 浏览器均无字形 → 方框;SVG 不依赖字体) + { key: 'rebuttal', label: '审稿回复助手', icon: '' }, { key: 'calendar', label: '会议与投稿日历', icon: '📅' }, ]; @@ -35,7 +36,9 @@ const ITEMS = [ :title="it.label" @click="edu.setSection(it.key)" > - + +