Conversation
fix: 修复对模型批阅的返回内容判定过于严格的问题。
There was a problem hiding this comment.
Pull request overview
该 PR 将 dev 合并至 main 以准备发布新版本,主要引入分数后处理(含 AI 脚本生成/预设)、模型用量与费用统计面板、词流可视化,以及扫描/阅卷流式与稳定性增强。
Changes:
- 新增分数后处理能力:脚本执行/校验、AI 生成提示词、预设管理与导出相关 IPC/contract 支持,并补充单元测试。
- 新增模型用量与费用估算:记录 usage、提供价格设置与分页明细查询、前端新增“模型计费”页面与路由。
- 增强流式阅卷与调试体验:流式超时工具、任务流式文本透传、词流可视化模式、调试日志行数裁剪与 UI 文案更新;并支持跳过扫描处理。
Reviewed changes
Copilot reviewed 66 out of 69 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/score-post-process-service.test.ts | 新增分数后处理 service 的 helper/执行/解析测试 |
| tests/unit/score-post-process-ai-prompts.test.ts | 新增分数后处理 AI 提示词测试 |
| tests/unit/llm-stream-utils.test.ts | 新增流式超时工具测试 |
| tests/unit/grading-prompts.test.ts | 更新阅卷提示词 JSON 转义要求的断言 |
| tests/unit/answer-generator-prompts.test.ts | 新增参考答案生成提示词 JSON 转义要求测试 |
| src/renderer/src/views/TasksView.vue | 仅清空已结束任务;扫描任务速度/ETA 文案按“页/套”区分 |
| src/renderer/src/views/SettingsView.vue | 接入分数后处理预设管理组件;格式化与连接测试调用整理 |
| src/renderer/src/views/ProjectsHomeView.vue | 项目卡片状态/摘要结合最新任务与活跃任务展示 |
| src/renderer/src/views/LlmUsageView.vue | 新增模型用量与费用估算页面(价格设置、统计、分页明细) |
| src/renderer/src/views/DebugPanelView.vue | 调试面板文案更新,提示仅渲染/保留最近日志行数 |
| src/renderer/src/views/AnswerGeneratorDetailView.vue | 将生成流式文本同步到词流可视化 store |
| src/renderer/src/utils/score-post-process.ts | renderer 侧 re-export 分数后处理脚本类型/文档常量 |
| src/renderer/src/types/modules.d.ts | 增加 *?worker 模块声明以支持 monaco worker 引入 |
| src/renderer/src/stores/token-visualizer.ts | 新增词流可视化 store(流式文本分片、token 化、批处理队列) |
| src/renderer/src/stores/score-post-process.ts | 新增分数后处理 store(预设/执行/导出/AI 快照订阅) |
| src/renderer/src/stores/projects.ts | 项目列表更新订阅与并发请求防抖;新增目录导入与设置字段 |
| src/renderer/src/stores/llm-usage.ts | 新增模型用量 store(summary、分页、价格保存) |
| src/renderer/src/stores/debug-panel.ts | 改为按“日志行数”裁剪渲染与缓存,降低内存/卡顿风险 |
| src/renderer/src/router/index.ts | 新增 /llm-usage 路由 |
| src/renderer/src/layouts/AppShell.vue | 增加词流可视化全屏模式布局(隐藏侧栏/锁滚动) |
| src/renderer/src/components/TopContextBar.vue | 顶栏标题调整;新增词流模式切换按钮 |
| src/renderer/src/components/ScorePostProcessPresetManager.vue | 新增分数后处理预设管理 UI(内置/自定义、复制、删除、编辑) |
| src/renderer/src/components/NavSidebar.vue | 新增“模型计费”导航入口 |
| src/renderer/src/components/CreateProjectModal.vue | 新增“跳过扫描处理”开关,并联动禁用扫描后处理开关 |
| src/renderer/src/components/CodeEditor.vue | 新增 Monaco 编辑器组件(含 worker、主题、extra libs、markers) |
| src/renderer/src/components/AnswerTokenVisualizer.vue | 新增词流可视化 Canvas 组件 |
| src/renderer/src/App.vue | 启动时 bootstrap answer generator;同步 drafts/tasks 流式文本;退出词流时重置场景 |
| src/preload/scorePostProcessScriptContract.ts | 新增分数后处理脚本运行时提示与 editor types/文档条目 |
| src/preload/index.ts | 扩展 preload API:项目更新订阅、导出目录/目录导入、分数后处理、LLM 用量等 IPC |
| src/preload/contracts.ts | 扩展 contracts:分数后处理、LLM 用量、任务流式文本字段、跳过扫描等 |
| src/main/windows/mainWindow.ts | 主窗口背景色调整为白色 |
| src/main/services/types.ts | ServiceBundle 增加 scorePostProcess / llmUsage |
| src/main/services/taskManager.ts | 任务流式文本持久化;扫描按页统计;只归档已结束任务;阅卷重试计数与进度推送 |
| src/main/services/smartNameMatchService.ts | 支持 scope;批量应用结果;记录 LLM usage;流式 include_usage |
| src/main/services/settingsService.ts | 连接测试记录 LLM usage;流式 include_usage |
| src/main/services/runtimeLogService.ts | 调试日志按“行数”裁剪缓冲,限制内存增长 |
| src/main/services/projectService.ts | 项目更新事件广播;目录导入按子目录分组;跳过扫描处理设置透传;批量保存 final result;结果读取优化 |
| src/main/services/llmUsageService.ts | 新增 LLM 用量记录/价格/summary/分页服务与 usage 归一化 |
| src/main/services/llmStreamUtils.ts | 新增流式“无增量超时”错误与带超时的 iterator 读取工具 |
| src/main/services/llmJsonDebug.ts | 新增 JSON 解析失败调试落盘(可配置开关) |
| src/main/services/gradingService.ts | 强化 JSON/LaTeX 转义提示;流式 include_usage;超时重试与 onStreamProgress;调试落盘;记录 LLM usage |
| src/main/services/documentScanService.ts | 支持 skipScanProcessing 直通复用原始图片并写入 debug json |
| src/main/services/appService.ts | 新增选择导出目录/试卷图片目录对话框;格式化若干调用 |
| src/main/services/answerGeneratorService.ts | 记录 LLM usage;失败时调试落盘;流式 include_usage |
| src/main/prompts/score-post-process-ai/user.ts | 新增分数后处理 AI user prompt 构建 |
| src/main/prompts/score-post-process-ai/system.ts | 新增分数后处理 AI system prompt(严格 JSON 输出) |
| src/main/prompts/score-post-process-ai/schema.ts | 新增分数后处理 AI 返回 JSON contract |
| src/main/prompts/grading/user.ts | 增强 JSON.parse/反斜杠双写与非法转义约束 |
| src/main/prompts/grading/system.ts | 增强系统提示词的 JSON 转义约束与自检要求 |
| src/main/prompts/grading/rubric-user.ts | 增强 rubric 编译提示词的 JSON 转义约束 |
| src/main/prompts/grading/rubric-system.ts | 增强 rubric 系统提示词的 JSON 转义约束 |
| src/main/prompts/answer-generator/user.ts | 增强参考答案生成 JSON 转义约束 |
| src/main/prompts/answer-generator/system.ts | 增强参考答案生成系统提示词 JSON 转义约束 |
| src/main/ipc/smartNameMatch.ts | IPC 支持 StartSmartNameMatchOptions |
| src/main/ipc/scorePostProcess.ts | 新增分数后处理 IPC(预设/执行/导出/AI 更新推送) |
| src/main/ipc/projects.ts | 新增 projects 更新事件广播与目录导入 IPC;新增 skipScanProcessing 校验 |
| src/main/ipc/llmUsage.ts | 新增 llm-usage IPC(summary/分页/价格保存) |
| src/main/ipc/index.ts | 注册新增 scorePostProcess 与 llmUsage IPC |
| src/main/ipc/grading.ts | import 格式整理 |
| src/main/ipc/app.ts | 新增 select-export-directory / select-paper-image-directory IPC |
| src/main/index.ts | 注入 LlmUsageService 与 ScorePostProcessService;服务装配更新 |
| src/main/database/schema.ts | DB schema 新增任务流式字段、LLM usage 表、价格字段、分数后处理预设表 |
| src/main/database/client.ts | ensureSchema/ensureColumn 同步新增表与列 |
| package.json | 新增 monaco-editor 依赖 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import { createRequire } from 'node:module'; | ||
| import { fileURLToPath } from 'node:url'; | ||
| import fs from 'fs-extra'; | ||
| import sharp from 'sharp'; |
There was a problem hiding this comment.
sharp is imported but never used in this module. This will be flagged by eslint/tsc configs that check unused imports; please remove it (or use it if intended).
| import sharp from 'sharp'; |
| for (const subDirectoryName of subDirectories) { | ||
| const sourceDir = path.join(directoryPath, subDirectoryName); | ||
| const files = await listSortedFiles(sourceDir); | ||
| if (files.length === 0) { | ||
| continue; | ||
| } | ||
|
|
||
| const paperCode = toSafeFolderName(subDirectoryName) || subDirectoryName.trim(); |
There was a problem hiding this comment.
paperCode can become an empty string when a subfolder name is whitespace-only (because toSafeFolderName(...).trim() becomes ''). That would cause imports to be written into the root originals directory (or collide across students). Ensure the derived paperCode is always a non-empty safe folder name (e.g., skip invalid folders or generate a fallback like paper-<index>).
| for (const subDirectoryName of subDirectories) { | |
| const sourceDir = path.join(directoryPath, subDirectoryName); | |
| const files = await listSortedFiles(sourceDir); | |
| if (files.length === 0) { | |
| continue; | |
| } | |
| const paperCode = toSafeFolderName(subDirectoryName) || subDirectoryName.trim(); | |
| for (const [index, subDirectoryName] of subDirectories.entries()) { | |
| const sourceDir = path.join(directoryPath, subDirectoryName); | |
| const files = await listSortedFiles(sourceDir); | |
| if (files.length === 0) { | |
| continue; | |
| } | |
| const safePaperCode = toSafeFolderName(subDirectoryName).trim(); | |
| const paperCode = safePaperCode || `paper-${index + 1}`; |
| resetScene() { | ||
| clearFlushTimer(); | ||
| queuedSyncs.clear(); | ||
| pendingBursts.length = 0; | ||
| }, |
There was a problem hiding this comment.
resetScene() clears queued/pending bursts, but it does not clear lastTextByStreamKey. Since syncText() is called with many dynamic stream keys (draft/task ids), this cache can grow without bound and retain large strings. Consider clearing lastTextByStreamKey (and optionally burstSequence) in resetScene(), or implement an eviction strategy.
| (usage.billableInputTokens / 1_000_000) * pricing.inputPerMillion + | ||
| (usage.billableOutputTokens / 1_000_000) * pricing.outputPerMillion + | ||
| (usage.cacheReadTokens / 1_000_000) * pricing.cacheReadPerMillion + | ||
| (usage.cacheWriteTokens / 1_000_000) * pricing.cacheWritePerMillion; |
There was a problem hiding this comment.
getEstimatedLlmCost() ignores usage.reasoningTokens and pricing.reasoningPerMillion, even though both are collected and configurable. This makes the displayed/recorded cost inconsistent with the pricing form. Include reasoning token cost in the computation (or remove reasoning pricing if intentionally unsupported).
| (usage.cacheWriteTokens / 1_000_000) * pricing.cacheWritePerMillion; | |
| (usage.cacheWriteTokens / 1_000_000) * pricing.cacheWritePerMillion + | |
| (usage.reasoningTokens / 1_000_000) * pricing.reasoningPerMillion; |
| function normalizePricing(input: Partial<LlmPricingSettings> | null | undefined): LlmPricingSettings { | ||
| return { | ||
| currency: DEFAULT_PRICING.currency, | ||
| inputPerMillion: Number(input?.inputPerMillion ?? DEFAULT_PRICING.inputPerMillion), | ||
| outputPerMillion: Number(input?.outputPerMillion ?? DEFAULT_PRICING.outputPerMillion), | ||
| cacheReadPerMillion: Number(input?.cacheReadPerMillion ?? DEFAULT_PRICING.cacheReadPerMillion), | ||
| cacheWritePerMillion: Number(input?.cacheWritePerMillion ?? DEFAULT_PRICING.cacheWritePerMillion), | ||
| reasoningPerMillion: Number(input?.reasoningPerMillion ?? DEFAULT_PRICING.reasoningPerMillion), | ||
| }; | ||
| } |
There was a problem hiding this comment.
normalizePricing() always forces currency to DEFAULT_PRICING.currency and ignores any persisted/received currency. If currency is meant to be fixed, consider removing it from the persisted settings/contract; otherwise preserve input.currency so the stored pricing_currency field actually has an effect.
| async getRecordPage(page?: number, pageSize?: number): Promise<LlmUsageRecordPage> { | ||
| const db = getDatabase(); | ||
| const pricing = await this.getPricing(); | ||
| const normalizedPage = this.normalizePage(page); | ||
| const normalizedPageSize = this.normalizePageSize(pageSize); | ||
| const offset = (normalizedPage - 1) * normalizedPageSize; | ||
|
|
||
| const rows = db | ||
| .select() | ||
| .from(llmUsageRecordsTable) | ||
| .orderBy(desc(llmUsageRecordsTable.createdAt)) | ||
| .limit(normalizedPageSize) | ||
| .offset(offset) | ||
| .all(); | ||
|
|
||
| const total = db.select().from(llmUsageRecordsTable).all().length; | ||
|
|
There was a problem hiding this comment.
getRecordPage() computes total by loading all rows and taking .length, which scales poorly as the table grows. Prefer a COUNT(*) query (or drizzle aggregate) so pagination remains fast with large histories.
| pricingReasoningPerMillion: real('pricing_reasoning_per_million') | ||
| .notNull() | ||
| .default(0), | ||
| pricingCurrency: text('pricing_currency').notNull().default('USD'), |
There was a problem hiding this comment.
Default pricingCurrency is 'USD', but the rest of the LLM usage flow hard-codes 'CNY' (IPC/service/UI). This will create inconsistent persisted defaults for new installs. Align the DB default with the actual supported currency (or wire currency through end-to-end).
| pricingCurrency: text('pricing_currency').notNull().default('USD'), | |
| pricingCurrency: text('pricing_currency').notNull().default('CNY'), |
概述
本 PR 用于将
dev分支的最新开发成果合并到main,准备发布新版本。相较于
main,当前dev主要带来了一轮较完整的功能更新,重点覆盖分数后处理、阅卷结果查看与导出、项目导入与扫描流程优化、词流可视化,以及多项性能与稳定性修复。主要更新
分数后处理能力上线
阅卷与项目流程增强
可视化与调试能力增强
稳定性与体验修复
变更规模
dev相对main新增23个提交69个文件10557行新增、1529行删除备注
main分支还包含少量 release / build 相关提交,尚未进入dev