Skip to content

发布新版本:合并 dev 到 main,包含分数后处理与阅卷流程增强#6

Merged
BobH233 merged 23 commits intomainfrom
dev
Apr 13, 2026
Merged

发布新版本:合并 dev 到 main,包含分数后处理与阅卷流程增强#6
BobH233 merged 23 commits intomainfrom
dev

Conversation

@BobH233
Copy link
Copy Markdown
Owner

@BobH233 BobH233 commented Apr 13, 2026

概述

本 PR 用于将 dev 分支的最新开发成果合并到 main,准备发布新版本。

相较于 main,当前 dev 主要带来了一轮较完整的功能更新,重点覆盖分数后处理、阅卷结果查看与导出、项目导入与扫描流程优化、词流可视化,以及多项性能与稳定性修复。

主要更新

分数后处理能力上线

  • 新增分数后处理功能,支持用户通过自定义代码对成绩结果进行二次处理并导出
  • 支持 AI 生成分数后处理脚本
  • 支持脚本流式预览与执行前校验
  • 支持分数后处理结果排序
  • 支持在分数后处理结果页查看原卷
  • 修复分数后处理界面在窄屏场景下的布局问题
  • 修复分数后处理预设保存相关问题

阅卷与项目流程增强

  • 支持批阅结果模糊搜索
  • 支持按学生文件夹批量导入项目答卷图片
  • 新增单个阅卷结果页面导出能力
  • 支持跳过内置扫描,直接使用原始图片作为最终内容
  • 支持实时刷新批阅进度
  • 保留项目设置编辑草稿,减少误操作带来的内容丢失
  • 为项目详情页增加外层滚动悬浮跳转按钮

可视化与调试能力增强

  • 新增模型用量面板
  • 新增词流/词云可视化能力,用于查看 token 流入情况
  • 优化词流视图卡顿问题
  • 调整词流视图抛射轨迹表现
  • 降低智能核名应用结果时的异常性能开销
  • 新增流式阅卷超时重试逻辑

稳定性与体验修复

  • 修复后台任务清空时误清除进行中任务的问题
  • 强化 JSON 输出约束,并补充解析失败调试落盘
  • 修复项目详情布局与数据显示问题
  • 修复项目详情加载闪回问题
  • 修复开启跳过扫描后扫描仍然较慢的问题
  • 修复项目设置中保存参考答案显示异常的问题
  • 优化导出 PDF 的排版逻辑与色彩表现
  • 优化采分细则显示提示,并放宽对模型批阅返回内容的判定条件

变更规模

  • dev 相对 main 新增 23 个提交
  • 共涉及 69 个文件
  • 代码变更规模为 10557 行新增、1529 行删除

备注

  • 当前 main 分支还包含少量 release / build 相关提交,尚未进入 dev
  • 在正式合并发布前,建议重点检查发布流程、打包配置及相关冲突情况

BobH233 added 23 commits April 5, 2026 12:36
fix: 修复对模型批阅的返回内容判定过于严格的问题。
Copilot AI review requested due to automatic review settings April 13, 2026 15:11
@BobH233 BobH233 merged commit 7de98bb into main Apr 13, 2026
2 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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';
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
import sharp from 'sharp';

Copilot uses AI. Check for mistakes.
Comment on lines +436 to +443
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();
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>).

Suggested change
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}`;

Copilot uses AI. Check for mistakes.
Comment on lines +181 to +185
resetScene() {
clearFlushTimer();
queuedSyncs.clear();
pendingBursts.length = 0;
},
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
(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;
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
(usage.cacheWriteTokens / 1_000_000) * pricing.cacheWritePerMillion;
(usage.cacheWriteTokens / 1_000_000) * pricing.cacheWritePerMillion +
(usage.reasoningTokens / 1_000_000) * pricing.reasoningPerMillion;

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +60
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),
};
}
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +346 to +362
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;

Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
pricingReasoningPerMillion: real('pricing_reasoning_per_million')
.notNull()
.default(0),
pricingCurrency: text('pricing_currency').notNull().default('USD'),
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
pricingCurrency: text('pricing_currency').notNull().default('USD'),
pricingCurrency: text('pricing_currency').notNull().default('CNY'),

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants