Compare commits
2 Commits
6e569ff478
...
6d9acd7252
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d9acd7252 | |||
| a71bde3452 |
@@ -0,0 +1,476 @@
|
||||
/**
|
||||
* IDE 协作开发题库 — 种子脚本
|
||||
* 运行方式:cd D:/AuraK/server && node scripts/seed-ide-questions.js
|
||||
*/
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const crypto = require('crypto');
|
||||
const path = require('path');
|
||||
|
||||
const DB_PATH = path.join(__dirname, '..', 'data', 'metadata.db');
|
||||
const BANK_ID = crypto.randomUUID();
|
||||
const TENANT_ID = 'a140a68e-f70a-44d3-b753-fa33d48cf234'; // 现有tenant
|
||||
const ADMIN_USER_ID = '1cf8ba6d-d184-4055-ab58-99c6f38bbf93'; // admin用户
|
||||
|
||||
const db = new DatabaseSync(DB_PATH);
|
||||
|
||||
function uuid() {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
function now() {
|
||||
return new Date().toISOString().replace('T', ' ').substring(0, 22) + '000';
|
||||
}
|
||||
|
||||
const ts = now();
|
||||
|
||||
// ===== 创建题库 =====
|
||||
console.log('Creating IDE question bank...');
|
||||
db.prepare(`
|
||||
INSERT INTO question_banks (id, tenant_id, template_id, name, description, status, created_by, reviewed_by, reviewed_at, review_comment, created_at, updated_at)
|
||||
VALUES (?, ?, NULL, ?, ?, 'PUBLISHED', ?, ?, ?, ?, ?, ?)
|
||||
`).run(BANK_ID, TENANT_ID, 'IDE协作开发题库', 'L1课程四:IDE协作开发考核题库,包含Copilot、Claude Code、OpenCode三种工具的50道题目', ADMIN_USER_ID, ADMIN_USER_ID, ts, 'approve', ts, ts);
|
||||
|
||||
// ===== 生成题目 =====
|
||||
const items = [];
|
||||
|
||||
function addItem(questionText, questionType, options, correctAnswer, keyPoints, difficulty, dimension, basis, judgment) {
|
||||
items.push({
|
||||
questionText,
|
||||
questionType,
|
||||
options: options ? JSON.stringify(options) : null,
|
||||
correctAnswer,
|
||||
keyPoints: JSON.stringify(keyPoints),
|
||||
difficulty: difficulty || 'STANDARD',
|
||||
dimension: dimension || 'IDE',
|
||||
basis: basis || null,
|
||||
judgment: judgment || null,
|
||||
});
|
||||
}
|
||||
|
||||
// ================================================
|
||||
// 一、GitHub Copilot — 智能代码补全(4题)
|
||||
// ================================================
|
||||
|
||||
addItem(
|
||||
'以下说法是否正确?(✓ / ✗)\n\n看到 Copilot 给出的灰色补全建议后,按 Tab 键可以接受建议。',
|
||||
'TRUE_FALSE', null, '✓',
|
||||
['Tab键接受补全建议', 'Copilot基础操作'],
|
||||
'STANDARD', 'IDE', 'GitHub Copilot 使用指南 - 第3章'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'以下说法是否正确?(✓ / ✗)\n\n看到 Copilot 给出的补全建议后,按 Esc 键可以拒绝这个建议。',
|
||||
'TRUE_FALSE', null, '✓',
|
||||
['Esc键拒绝补全建议', 'Copilot基础操作'],
|
||||
'STANDARD', 'IDE', 'GitHub Copilot 使用指南 - 第3章'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'以下说法是否正确?(✓ / ✗)\n\n写函数开头后,Copilot 会自动逐行补全后续逻辑。',
|
||||
'TRUE_FALSE', null, '✓',
|
||||
['Copilot智能补全功能', '注释驱动代码生成'],
|
||||
'STANDARD', 'IDE', 'GitHub Copilot 使用指南 - 第3章'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'小张用智能补全生成了一段代码,看起来功能正常。\n\n问:在正式使用这段代码前,他应该先做什么?',
|
||||
'SHORT_ANSWER', null, '审查代码逻辑,确认没有语法错误或逻辑漏洞后再使用。AI 生成的代码需要人工审核。',
|
||||
['人工审核AI生成代码', '代码审查', '不直接使用AI生成的代码'],
|
||||
'STANDARD', 'IDE', 'GitHub Copilot 使用指南 - 红线警告'
|
||||
);
|
||||
|
||||
// ================================================
|
||||
// 二、GitHub Copilot — Chat 三种模式(5题)
|
||||
// ================================================
|
||||
|
||||
addItem(
|
||||
'小张接手了一个老项目,打开 OrderService.java 发现有段逻辑看不太懂。他打开 Copilot Chat,想先问问这段代码是干什么的。\n\nCopilot Chat 有以下三种模式:Ask / Plan / Agent\n\n问:小张应该选择哪种模式?',
|
||||
'SHORT_ANSWER', null, 'Ask(问答模式)。Ask 模式只回答问题,不修改代码。',
|
||||
['Ask模式适用场景', '不修改代码的对话方式'],
|
||||
'STANDARD', 'IDE', 'GitHub Copilot 使用指南 - 第3章 三种对话模式'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'小李需要在三个文件中新增一个「批量删除用户」的功能,希望 AI 直接帮他完成代码修改。\n\nCopilot Chat 有以下三种模式:Ask / Plan / Agent\n\n问:小李应该选择哪种模式?',
|
||||
'SHORT_ANSWER', null, 'Agent(智能代理模式)。Agent 模式可以自动跨文件修改代码。',
|
||||
['Agent模式适用场景', '跨文件修改任务'],
|
||||
'STANDARD', 'IDE', 'GitHub Copilot 使用指南 - 第3章 三种对话模式'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'小赵想在项目中新增一个功能,但不知道涉及哪些文件、影响范围多大,想让 AI 先扫描整个项目给出方案。\n\nCopilot Chat 有以下三种模式:Ask / Plan / Agent\n\n问:小赵应该选择哪种模式?',
|
||||
'SHORT_ANSWER', null, 'Plan(计划模式)。Plan 模式只出方案不动代码,适合先评估再动手。',
|
||||
['Plan模式适用场景', '先规划再执行'],
|
||||
'STANDARD', 'IDE', 'GitHub Copilot 使用指南 - 第3章 三种对话模式'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'以下说法是否正确?(✓ / ✗)\n\nAsk 模式下,Copilot 只会回答问题,不会修改用户的代码。',
|
||||
'TRUE_FALSE', null, '✓',
|
||||
['Ask模式只回答不修改'],
|
||||
'STANDARD', 'IDE', 'GitHub Copilot 使用指南 - 第3章'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'以下说法是否正确?(✓ / ✗)\n\nAgent 模式下,Copilot 可以跨多个文件修改代码。',
|
||||
'TRUE_FALSE', null, '✓',
|
||||
['Agent模式跨文件修改'],
|
||||
'STANDARD', 'IDE', 'GitHub Copilot 使用指南 - 第3章'
|
||||
);
|
||||
|
||||
// ================================================
|
||||
// 三、GitHub Copilot — CLI 使用(3题)
|
||||
// ================================================
|
||||
|
||||
addItem(
|
||||
'小刘想用 Copilot CLI 重构一个 Python 脚本,过程中要多次对话、逐步调优。\n\nCopilot CLI 有以下两种使用方式:交互模式(copilot)/ 非交互模式(copilot -p "指令")\n\n问:小刘应该选择哪种方式?',
|
||||
'SHORT_ANSWER', null, '交互模式(copilot)。交互模式支持多轮对话,适合需要迭代的复杂任务。',
|
||||
['CLI交互模式适用场景', '多轮对话重构'],
|
||||
'STANDARD', 'IDE', 'GitHub Copilot 使用指南 - CLI 使用'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'小钱想用 Copilot CLI 快速解释一下 git diff 的结果,不想进入交互式对话。\n\nCopilot CLI 有以下两种使用方式:交互模式(copilot)/ 非交互模式(copilot -p "指令")\n\n问:小钱应该选择哪种方式?',
|
||||
'SHORT_ANSWER', null, '非交互模式(copilot -p "指令")。非交互模式适合一次性任务,快速获得结果后退出。',
|
||||
['CLI非交互模式适用场景', '一次性任务'],
|
||||
'STANDARD', 'IDE', 'GitHub Copilot 使用指南 - CLI使用'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'小赵在 Copilot CLI 交互模式中,想清空当前对话上下文重新开始。\n\nCopilot CLI 中常用的斜杠命令有:/clear / /model / /session / /exit\n\n问:小赵应该使用哪个命令?',
|
||||
'SHORT_ANSWER', null, '/clear',
|
||||
['CLI斜杠命令', '/clear清空对话'],
|
||||
'STANDARD', 'IDE', 'GitHub Copilot 使用指南 - CLI斜杠命令'
|
||||
);
|
||||
|
||||
// ================================================
|
||||
// 四、Claude Code — 四种交互方式(6题)
|
||||
// ================================================
|
||||
|
||||
addItem(
|
||||
'以下说法是否正确?(✓ / ✗)\n\n在 Claude Code 中输入 @src/utils.js 可以让 AI 读取该文件。',
|
||||
'TRUE_FALSE', null, '✓',
|
||||
['Claude Code文件引用', '@语法'],
|
||||
'STANDARD', 'IDE', 'Claude Code 使用指南 - 第3章 文件引用'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'以下说法是否正确?(✓ / ✗)\n\n在 Claude Code 中输入 /clear 可以清空当前对话。',
|
||||
'TRUE_FALSE', null, '✓',
|
||||
['Claude Code斜杠命令', '/clear清屏'],
|
||||
'STANDARD', 'IDE', 'Claude Code 使用指南 - 第3章 斜杠命令'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'以下说法是否正确?(✓ / ✗)\n\n在 Claude Code 中输入 !git status 可以查看 Git 状态。',
|
||||
'TRUE_FALSE', null, '✓',
|
||||
['Claude Code Bash模式', '!语法执行系统命令'],
|
||||
'STANDARD', 'IDE', 'Claude Code 使用指南 - 第3章 Bash模式'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'以下说法是否正确?(✓ / ✗)\n\n在 Claude Code 中所有操作都必须用特殊符号,自然语言输入不能完成任何功能。',
|
||||
'TRUE_FALSE', null, '✗。自然语言也可以完成大部分功能,特殊符号用于特定场景。',
|
||||
['自然语言可用', '特殊符号的定位'],
|
||||
'STANDARD', 'IDE', 'Claude Code 使用指南 - 第3章 交互方式'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'以下说法是否正确?(✓ / ✗)\n\n在 Claude Code 中输入 !npm run dev 可以启动开发服务器。',
|
||||
'TRUE_FALSE', null, '✓',
|
||||
['Claude Code Bash模式', '!语法启动开发服务器'],
|
||||
'STANDARD', 'IDE', 'Claude Code 使用指南 - 第3章 Bash模式'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'以下说法是否正确?(✓ / ✗)\n\n在 Claude Code 中输入 /help 可以查看所有可用命令。',
|
||||
'TRUE_FALSE', null, '✓',
|
||||
['Claude Code斜杠命令', '/help帮助'],
|
||||
'STANDARD', 'IDE', 'Claude Code 使用指南 - 第3章 斜杠命令'
|
||||
);
|
||||
|
||||
// ================================================
|
||||
// 五、Claude Code — 模型选择与切换(3题)
|
||||
// ================================================
|
||||
|
||||
addItem(
|
||||
'Claude Code 的三个模型特点如下:\n- Sonnet:主力工程师,日常编码首选\n- Haiku:响应极快、成本低,适合简单任务\n- Opus:处理超级复杂的难题,智商最高\n\n小陈需要修复一个非常复杂的系统架构 Bug。\n\n问:他应该选择哪个模型?',
|
||||
'SHORT_ANSWER', null, 'Opus',
|
||||
['Opus模型适用场景', '复杂难题'],
|
||||
'STANDARD', 'IDE', 'Claude Code 使用指南 - 第3章 模型选择'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'Claude Code 的三个模型特点如下:\n- Sonnet:主力工程师,日常编码首选\n- Haiku:响应极快、成本低,适合简单任务\n- Opus:处理超级复杂的难题,智商最高\n\n小陈在做日常的 CRUD 接口开发。\n\n问:他应该选择哪个模型?',
|
||||
'SHORT_ANSWER', null, 'Sonnet',
|
||||
['Sonnet模型适用场景', '日常编码'],
|
||||
'STANDARD', 'IDE', 'Claude Code 使用指南 - 第3章 模型选择'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'Claude Code 的三个模型特点如下:\n- Sonnet:主力工程师,日常编码首选\n- Haiku:响应极快、成本低,适合简单任务\n- Opus:处理超级复杂的难题,智商最高\n\n小陈想快速查一下某个 JavaScript 数组方法的语法。\n\n问:他应该选择哪个模型?',
|
||||
'SHORT_ANSWER', null, 'Haiku',
|
||||
['Haiku模型适用场景', '快速简单任务'],
|
||||
'STANDARD', 'IDE', 'Claude Code 使用指南 - 第3章 模型选择'
|
||||
);
|
||||
|
||||
// ================================================
|
||||
// 六、Claude Code — CLI 命令(3题)
|
||||
// ================================================
|
||||
|
||||
addItem(
|
||||
'小赵的 Claude Code 会话因为终端意外关闭了,想接着刚才的对话继续。\n\nClaude CLI 有以下命令:claude / claude --continue / claude --resume\n\n问:小赵应该用哪个命令?',
|
||||
'SHORT_ANSWER', null, 'claude --continue(或 claude -c)。该命令用于恢复上次意外关闭的会话。',
|
||||
['恢复会话', '--continue参数'],
|
||||
'STANDARD', 'IDE', 'Claude Code 使用指南 - 第3章 CLI命令'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'小钱想用 Claude Code 快速解释一下 git diff 的结果,不想进入交互式对话。\n\nClaude CLI 有以下命令:claude / claude -p "指令" / claude --resume\n\n问:小钱应该用哪个命令?',
|
||||
'SHORT_ANSWER', null, 'claude -p "指令"。-p 参数用于一次性任务,适合快速执行。',
|
||||
['一次性任务', '-p参数'],
|
||||
'STANDARD', 'IDE', 'Claude Code 使用指南 - 第3章 CLI命令'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'以下说法是否正确?(✓ / ✗)\n\nclaude --resume 可以从历史会话列表中选择恢复。',
|
||||
'TRUE_FALSE', null, '✓',
|
||||
['恢复历史会话', '--resume参数'],
|
||||
'STANDARD', 'IDE', 'Claude Code 使用指南 - 第3章 CLI命令'
|
||||
);
|
||||
|
||||
// ================================================
|
||||
// 七、OpenCode — 整体认知(3题)
|
||||
// ================================================
|
||||
|
||||
addItem(
|
||||
'以下说法是否正确?(✓ / ✗)\n\nOpenCode 像一位身边的搭档,可以直接读取项目文件、修改代码、执行命令。',
|
||||
'TRUE_FALSE', null, '✓',
|
||||
['OpenCode核心定位', 'AI编码代理'],
|
||||
'STANDARD', 'IDE', 'OpenCode 使用指南 - 第1章 核心定位'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'以下说法是否正确?(✓ / ✗)\n\n传统 AI 像远程顾问,给你建议但需要你自己动手;OpenCode 可以直接帮你操作。',
|
||||
'TRUE_FALSE', null, '✓',
|
||||
['OpenCode与传统AI区别', '直接操作能力'],
|
||||
'STANDARD', 'IDE', 'OpenCode 使用指南 - 第1章'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'小周想用 OpenCode 读取包含客户个人信息的代码文件,让 AI 帮忙优化。\n\n问:这种做法是否合适?为什么?',
|
||||
'SHORT_ANSWER', null, '不合适。客户个人信息属于敏感数据,严禁输入任何公共 AI 工具。应该先对数据进行脱敏处理,用虚构数据或占位符替代后再使用。',
|
||||
['安全合规', '敏感数据保护', '数据脱敏'],
|
||||
'STANDARD', 'IDE', 'OpenCode 使用指南 - 红线警告'
|
||||
);
|
||||
|
||||
// ================================================
|
||||
// 八、OpenCode — 安装与使用方式(4题)
|
||||
// ================================================
|
||||
|
||||
addItem(
|
||||
'OpenCode 有以下四种使用方式:\n- 终端版 — 轻量启动快,适合有基础的用户\n- 桌面应用 — 界面直观,适合新手\n- IDE 扩展 — 深度绑定编辑器\n- Web 版 — 浏览器访问,可远程部署\n\n小周是新手,不喜欢操作命令行,想找一个界面直观的方式。\n\n问:他应该选择哪种方式?',
|
||||
'SHORT_ANSWER', null, '桌面应用',
|
||||
['OpenCode使用方式选择', '桌面应用适合新手'],
|
||||
'STANDARD', 'IDE', 'OpenCode 使用指南 - 第2章 使用方式'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'OpenCode 有以下四种使用方式:终端版 / 桌面应用 / IDE 扩展 / Web 版\n\n小刘平时用 VS Code 写代码,希望不离开编辑器就能用 OpenCode。\n\n问:他应该选择哪种方式?',
|
||||
'SHORT_ANSWER', null, 'IDE 扩展',
|
||||
['OpenCode IDE扩展', '编辑器集成'],
|
||||
'STANDARD', 'IDE', 'OpenCode 使用指南 - 第2章 使用方式'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'OpenCode 有以下四种使用方式:终端版 / 桌面应用 / IDE 扩展 / Web 版\n\n小马需要在远程服务器上开发,只能通过命令行操作。\n\n问:他应该选择哪种方式?',
|
||||
'SHORT_ANSWER', null, '终端版',
|
||||
['OpenCode终端版', '远程服务器开发'],
|
||||
'STANDARD', 'IDE', 'OpenCode 使用指南 - 第2章 使用方式'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'以下说法是否正确?(✓ / ✗)\n\n在终端中输入 opencode 可以启动 OpenCode。',
|
||||
'TRUE_FALSE', null, '✓',
|
||||
['OpenCode启动命令'],
|
||||
'STANDARD', 'IDE', 'OpenCode 使用指南 - 第2章 安装'
|
||||
);
|
||||
|
||||
// ================================================
|
||||
// 九、OpenCode — Plan / Build 工作模式(5题)
|
||||
// ================================================
|
||||
|
||||
addItem(
|
||||
'小周接手了一个新项目,想先让 OpenCode 分析项目结构,还不想修改任何文件。\n\nOpenCode 有以下两种工作模式:Plan / Build\n\n问:小周应该选择哪种模式?',
|
||||
'SHORT_ANSWER', null, 'Plan(计划模式)。Plan 模式下 AI 只能读取文件,不会修改代码。',
|
||||
['Plan模式', '只读分析'],
|
||||
'STANDARD', 'IDE', 'OpenCode 使用指南 - 第3章 Plan模式'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'小周已经确认了修改方案,想让 OpenCode 开始实际修改代码。\n\nOpenCode 有以下两种工作模式:Plan / Build\n\n问:小周应该选择哪种模式?',
|
||||
'SHORT_ANSWER', null, 'Build(构建模式)。Build 模式下 AI 可以编辑文件和执行命令。',
|
||||
['Build模式', '实际代码修改'],
|
||||
'STANDARD', 'IDE', 'OpenCode 使用指南 - 第3章 Build模式'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'以下说法是否正确?(✓ / ✗)\n\nPlan 模式下 AI 只能读取文件,不会修改任何代码。',
|
||||
'TRUE_FALSE', null, '✓',
|
||||
['Plan模式只读特性'],
|
||||
'STANDARD', 'IDE', 'OpenCode 使用指南 - 第3章'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'以下说法是否正确?(✓ / ✗)\n\nBuild 模式下 AI 可以编辑文件和执行命令。',
|
||||
'TRUE_FALSE', null, '✓',
|
||||
['Build模式可写特性'],
|
||||
'STANDARD', 'IDE', 'OpenCode 使用指南 - 第3章'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'小周让 OpenCode 在 Build 模式下修改了多个文件。\n\n问:修改完成后,他应该先做什么?',
|
||||
'SHORT_ANSWER', null, '审查 AI 修改的代码,确认逻辑正确后再使用。AI 生成的代码不能直接部署到生产环境。',
|
||||
['代码审查', 'AI生成代码需人工审核'],
|
||||
'STANDARD', 'IDE', 'OpenCode 使用指南 - 安全原则'
|
||||
);
|
||||
|
||||
// ================================================
|
||||
// 十、OpenCode — 常用命令(5题)
|
||||
// ================================================
|
||||
|
||||
addItem(
|
||||
'小周用 OpenCode 修改了代码,但发现改错了,想撤销刚才的修改。\n\nOpenCode 中有以下命令:/undo / /redo / /clear / /init\n\n问:他应该使用哪个命令?',
|
||||
'SHORT_ANSWER', null, '/undo',
|
||||
['OpenCode撤销命令', '/undo'],
|
||||
'STANDARD', 'IDE', 'OpenCode 使用指南 - 第3章 撤销更改'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'小周撤销了修改后又觉得还是刚才改得好,想恢复回来。\n\nOpenCode 中有以下命令:/undo / /redo / /clear / /init\n\n问:他应该使用哪个命令?',
|
||||
'SHORT_ANSWER', null, '/redo',
|
||||
['OpenCode重做命令', '/redo'],
|
||||
'STANDARD', 'IDE', 'OpenCode 使用指南 - 第3章'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'小周想在新项目目录中创建 AGENTS.md 文件,让 OpenCode 了解项目结构。\n\nOpenCode 中有以下命令:/undo / /redo / /clear / /init\n\n问:他应该使用哪个命令?',
|
||||
'SHORT_ANSWER', null, '/init',
|
||||
['OpenCode初始化', '/init命令'],
|
||||
'STANDARD', 'IDE', 'OpenCode 使用指南 - 第4章 项目初始化'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'小周想看看 OpenCode 当前有哪些可用的斜杠命令和快捷键。\n\nOpenCode 中有以下命令:/help / /models / /connect / /exit\n\n问:他应该使用哪个命令?',
|
||||
'SHORT_ANSWER', null, '/help',
|
||||
['OpenCode帮助', '/help命令'],
|
||||
'STANDARD', 'IDE', 'OpenCode 使用指南 - 第3章 斜杠命令'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'小周想切换 OpenCode 正在使用的 AI 模型。\n\nOpenCode 中有以下命令:/help / /models / /connect / /exit\n\n问:他应该使用哪个命令?',
|
||||
'SHORT_ANSWER', null, '/models',
|
||||
['OpenCode模型切换', '/models命令'],
|
||||
'STANDARD', 'IDE', 'OpenCode 使用指南 - 第3章 模型选择'
|
||||
);
|
||||
|
||||
// ================================================
|
||||
// 十一、OpenCode — 模型选择(2题)
|
||||
// ================================================
|
||||
|
||||
addItem(
|
||||
'以下说法是否正确?(✓ / ✗)\n\nOpenCode 内置多款免费模型,启动后可以直接选择使用,无需配置 API 密钥。',
|
||||
'TRUE_FALSE', null, '✓',
|
||||
['OpenCode免费模型', '无需API密钥'],
|
||||
'STANDARD', 'IDE', 'OpenCode 使用指南 - 第3章 模型'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'以下说法是否正确?(✓ / ✗)\n\n使用第三方 LLM 提供商(如 OpenAI、Anthropic)需要自行承担 API 费用。',
|
||||
'TRUE_FALSE', null, '✓',
|
||||
['第三方LLM费用', 'API成本'],
|
||||
'STANDARD', 'IDE', 'OpenCode 使用指南 - 第3章 模型'
|
||||
);
|
||||
|
||||
// ================================================
|
||||
// 十二、Debug — 调试助手(7题)
|
||||
// ================================================
|
||||
|
||||
addItem(
|
||||
'小吴的代码运行时报错了,不知道问题出在哪,想让 Copilot Chat 帮他定位和解决 Bug。\n\nCopilot Chat 中有以下命令:/fix / /tests / /explain / /debug\n\n问:他应该使用哪个命令?',
|
||||
'SHORT_ANSWER', null, '/debug。该命令专用于帮助定位和解决 Bug。',
|
||||
['/debug命令', '定位解决Bug'],
|
||||
'STANDARD', 'IDE', 'GitHub Copilot 使用指南 - 第3章 内置命令'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'小吴已经知道问题在哪了,想让 Copilot 直接修复选中的代码。\n\nCopilot Chat 中有以下命令:/fix / /tests / /explain / /debug\n\n问:他应该使用哪个命令?',
|
||||
'SHORT_ANSWER', null, '/fix。该命令用于自动修复代码问题。',
|
||||
['/fix命令', '自动修复代码'],
|
||||
'STANDARD', 'IDE', 'GitHub Copilot 使用指南 - 第3章 内置命令'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'以下说法是否正确?(✓ / ✗)\n\n在 Copilot Chat 中输入 /tests 可以生成选中代码的单元测试。',
|
||||
'TRUE_FALSE', null, '✓',
|
||||
['/tests命令', '生成单元测试'],
|
||||
'STANDARD', 'IDE', 'GitHub Copilot 使用指南 - 第3章 内置命令'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'以下说法是否正确?(✓ / ✗)\n\n在 Copilot Chat 中输入 /explain 可以让 AI 解释选中代码的逻辑。',
|
||||
'TRUE_FALSE', null, '✓',
|
||||
['/explain命令', '解释代码逻辑'],
|
||||
'STANDARD', 'IDE', 'GitHub Copilot 使用指南 - 第3章 内置命令'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'以下说法是否正确?(✓ / ✗)\n\n/debug 命令可以帮助定位和解决 Bug。',
|
||||
'TRUE_FALSE', null, '✓',
|
||||
['/debug命令功能'],
|
||||
'STANDARD', 'IDE', 'GitHub Copilot 使用指南 - 第3章 内置命令'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'小吴用 /fix 命令让 Copilot 自动修复了代码。\n\n问:修复完成后,他应该先做什么?',
|
||||
'SHORT_ANSWER', null, '审查修复后的代码,确认修改正确、逻辑无误后再使用。',
|
||||
['代码审查', '修复后验证'],
|
||||
'STANDARD', 'IDE', 'GitHub Copilot 使用指南 - 安全原则'
|
||||
);
|
||||
|
||||
addItem(
|
||||
'小吴遇到一个 Bug,想把包含数据库连接串的配置文件贴到 Copilot Chat 中用 /debug 分析。\n\n问:这种做法是否合适?为什么?',
|
||||
'SHORT_ANSWER', null, '不合适。数据库连接串属于敏感信息,严禁输入公共 AI 工具。应该用脱敏数据或占位符替代后再进行分析。',
|
||||
['安全合规', '敏感数据保护', '脱敏处理'],
|
||||
'STANDARD', 'IDE', 'GitHub Copilot 使用指南 - 红线警告'
|
||||
);
|
||||
|
||||
// ===== 批量插入题目 =====
|
||||
console.log(`Inserting ${items.length} questions...`);
|
||||
|
||||
const insertItem = db.prepare(`
|
||||
INSERT INTO question_bank_items (id, bank_id, question_text, questionType, options, correctAnswer, key_points, difficulty, dimension, basis, judgment, status, created_by, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'PUBLISHED', ?, ?, ?)
|
||||
`);
|
||||
|
||||
for (const item of items) {
|
||||
insertItem.run(
|
||||
uuid(),
|
||||
BANK_ID,
|
||||
item.questionText,
|
||||
item.questionType,
|
||||
item.options,
|
||||
item.correctAnswer,
|
||||
item.keyPoints,
|
||||
item.difficulty,
|
||||
item.dimension,
|
||||
item.basis,
|
||||
item.judgment,
|
||||
ADMIN_USER_ID,
|
||||
ts,
|
||||
ts
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Successfully inserted ${items.length} IDE questions into bank ${BANK_ID}`);
|
||||
db.close();
|
||||
@@ -231,7 +231,7 @@ export class AssessmentController {
|
||||
@ApiOperation({ summary: 'Batch delete assessment sessions (admin only)' })
|
||||
async batchDelete(@Request() req: any, @Body() body: { ids: string[] }) {
|
||||
const user = req.user;
|
||||
const isAdmin = user.role === 'super_admin' || user.role === 'admin';
|
||||
const isAdmin = user.role?.toLowerCase() === 'super_admin' || user.role?.toLowerCase() === 'admin';
|
||||
if (!isAdmin) {
|
||||
throw new ForbiddenException('Only admin can batch delete');
|
||||
}
|
||||
@@ -286,7 +286,7 @@ export class AssessmentController {
|
||||
@Request() req: any,
|
||||
) {
|
||||
const { id: userId, tenantId, role } = req.user;
|
||||
const isAdmin = role === 'super_admin' || role === 'admin';
|
||||
const isAdmin = role?.toLowerCase() === 'super_admin' || role?.toLowerCase() === 'admin';
|
||||
if (!isAdmin) {
|
||||
throw new ForbiddenException('Only admin can force end assessment');
|
||||
}
|
||||
|
||||
@@ -189,15 +189,16 @@ private async getModel(tenantId: string): Promise<ChatOpenAI> {
|
||||
|
||||
this.logger.debug('[calculateScores] Scoring debug:', { promptAvg, otherDimsWithScores, otherAvg, workCapability: dimensionAverages.workCapability });
|
||||
|
||||
const allScores: number[] = [];
|
||||
questions.forEach((q: any) => {
|
||||
const score = scores[q.id || questions.indexOf(q).toString()] || 0;
|
||||
allScores.push(score);
|
||||
});
|
||||
|
||||
const finalScore = allScores.length > 0
|
||||
? allScores.reduce((a, b) => a + b, 0) / allScores.length
|
||||
: 0;
|
||||
// Weighted final score using weightConfig
|
||||
let finalScore: number;
|
||||
if (promptAvg > 0 && otherAvg > 0) {
|
||||
const totalWeight = (weightConfig?.prompt ?? 50) + (weightConfig?.other ?? 50);
|
||||
finalScore = totalWeight > 0
|
||||
? (promptAvg * (weightConfig?.prompt ?? 50) + otherAvg * (weightConfig?.other ?? 50)) / totalWeight
|
||||
: (promptAvg + otherAvg) / 2;
|
||||
} else {
|
||||
finalScore = promptAvg || otherAvg || 0;
|
||||
}
|
||||
|
||||
const radarData: Record<string, number> = {};
|
||||
Object.keys(dimensionAverages).forEach(dim => {
|
||||
@@ -430,33 +431,54 @@ private async getModel(tenantId: string): Promise<ChatOpenAI> {
|
||||
|
||||
// Use kbId if provided, otherwise fall back to template's group ID
|
||||
const activeKbId = kbId || template?.knowledgeGroupId;
|
||||
this.logger.log(`[startSession] activeKbId resolved to: ${activeKbId}`);
|
||||
if (!activeKbId) {
|
||||
|
||||
// If no knowledge source, check if template has a question bank first
|
||||
let hasBankQuestions = false;
|
||||
if (!activeKbId && templateId && template) {
|
||||
try {
|
||||
const targetCount = template.questionCount || 5;
|
||||
const linkedBanks = await this.questionBankRepository.find({
|
||||
where: { templateId },
|
||||
});
|
||||
if (linkedBanks.length > 0) {
|
||||
const bankIds = linkedBanks.map(b => b.id);
|
||||
const count = await this.questionBankItemRepository.count({
|
||||
where: { bankId: In(bankIds), status: QuestionBankItemStatus.PUBLISHED },
|
||||
});
|
||||
if (count >= targetCount) {
|
||||
hasBankQuestions = true;
|
||||
this.logger.log(`[startSession] Template has ${count} published questions, skipping KB check`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.warn(`[startSession] Bank pre-check failed: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!activeKbId && !hasBankQuestions) {
|
||||
this.logger.error(`[startSession] No knowledge source resolved`);
|
||||
throw new BadRequestException('Knowledge source (ID or Template) must be provided.');
|
||||
}
|
||||
|
||||
// Try to determine if it's a KB or Group and check permissions
|
||||
// Determine if it's a KB or Group (only when activeKbId exists)
|
||||
let isKb = false;
|
||||
try {
|
||||
await this.kbService.findOne(activeKbId, userId, tenantId);
|
||||
isKb = true;
|
||||
} catch (kbError) {
|
||||
if (kbError instanceof NotFoundException) {
|
||||
// Try finding it as a Group
|
||||
try {
|
||||
await this.groupService.findOne(activeKbId, userId, tenantId);
|
||||
} catch (groupError) {
|
||||
this.logger.error(
|
||||
`[startSession] Knowledge source ${activeKbId} not found as KB or Group`,
|
||||
);
|
||||
throw new NotFoundException(
|
||||
this.i18nService.getMessage('knowledgeSourceNotFound') ||
|
||||
'Knowledge source not found',
|
||||
);
|
||||
if (activeKbId) {
|
||||
try {
|
||||
await this.kbService.findOne(activeKbId, userId, tenantId);
|
||||
isKb = true;
|
||||
} catch (kbError) {
|
||||
if (kbError instanceof NotFoundException) {
|
||||
try {
|
||||
await this.groupService.findOne(activeKbId, userId, tenantId);
|
||||
} catch (groupError) {
|
||||
this.logger.error(`[startSession] Knowledge source ${activeKbId} not found`);
|
||||
throw new NotFoundException(
|
||||
this.i18nService.getMessage('knowledgeSourceNotFound') || 'Knowledge source not found',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw kbError;
|
||||
}
|
||||
} else {
|
||||
throw kbError; // e.g. ForbiddenException
|
||||
}
|
||||
}
|
||||
this.logger.debug(`[startSession] isKb: ${isKb}`);
|
||||
@@ -503,6 +525,7 @@ private async getModel(tenantId: string): Promise<ChatOpenAI> {
|
||||
const selectedItems = await this.questionBankService.selectQuestions(
|
||||
bankId,
|
||||
targetCount,
|
||||
template?.dimensions,
|
||||
);
|
||||
|
||||
questionsFromBank = selectedItems.map(item => {
|
||||
@@ -586,9 +609,9 @@ private async getModel(tenantId: string): Promise<ChatOpenAI> {
|
||||
};
|
||||
|
||||
// Skip content check if questions are loaded from the question bank
|
||||
const hasBankQuestions = questionsFromBank.length > 0;
|
||||
const hasBankContent = questionsFromBank.length > 0;
|
||||
|
||||
if (!hasBankQuestions) {
|
||||
if (!hasBankContent) {
|
||||
const content = await this.getSessionContent(sessionData);
|
||||
|
||||
if (!content || content.trim().length < 10) {
|
||||
@@ -787,7 +810,7 @@ const initialState: Partial<EvaluationState> = {
|
||||
const scores = finalData.scores;
|
||||
const questions = finalData.questions || [];
|
||||
const weightConfig = session.templateJson?.weightConfig || { prompt: 50, other: 50 };
|
||||
const passingScore = (session.templateJson?.passingScore ?? 90) / 10;
|
||||
const passingScore = (session.templateJson?.passingScore ?? 60) / 10;
|
||||
|
||||
if (questions.length > 0 && Object.keys(scores).length > 0) {
|
||||
const { finalScore, dimensionScores, radarData } = this.calculateScores(
|
||||
@@ -882,7 +905,7 @@ const initialState: Partial<EvaluationState> = {
|
||||
|
||||
let finalResult: any = null;
|
||||
const weightConfig = session.templateJson?.weightConfig || { prompt: 50, other: 50 };
|
||||
const passingScore = (session.templateJson?.passingScore ?? 90) / 10;
|
||||
const passingScore = (session.templateJson?.passingScore ?? 60) / 10;
|
||||
|
||||
// Resume from the last interrupt (typically after interviewer)
|
||||
const stream = await this.graph.stream(null, {
|
||||
@@ -935,7 +958,7 @@ const initialState: Partial<EvaluationState> = {
|
||||
const scores = finalResult.scores as Record<string, number>;
|
||||
const questions = finalResult.questions || [];
|
||||
const weightConfig = session.templateJson?.weightConfig || { prompt: 50, other: 50 };
|
||||
const passingScore = (session.templateJson?.passingScore ?? 90) / 10;
|
||||
const passingScore = (session.templateJson?.passingScore ?? 60) / 10;
|
||||
|
||||
if (questions.length > 0 && Object.keys(scores).length > 0) {
|
||||
const { finalScore, dimensionScores, radarData } = this.calculateScores(
|
||||
@@ -1158,7 +1181,7 @@ const initialState: Partial<EvaluationState> = {
|
||||
const scores = finalData.scores;
|
||||
const questions = finalData.questions || [];
|
||||
const weightConfig = session.templateJson?.weightConfig || { prompt: 50, other: 50 };
|
||||
const passingScore = (session.templateJson?.passingScore ?? 90) / 10;
|
||||
const passingScore = (session.templateJson?.passingScore ?? 60) / 10;
|
||||
|
||||
if (questions.length > 0 && Object.keys(scores).length > 0) {
|
||||
const { finalScore, dimensionScores, radarData } = this.calculateScores(
|
||||
@@ -1364,17 +1387,27 @@ const initialState: Partial<EvaluationState> = {
|
||||
const content = await this.getSessionContent(session);
|
||||
const model = await this.getModel(session.tenantId);
|
||||
|
||||
const existingQuestions = session.questions_json || [];
|
||||
const hasQuestionsFromBank = existingQuestions.length > 0;
|
||||
const isZh = (session.language || 'en') === 'zh';
|
||||
const isJa = session.language === 'ja';
|
||||
|
||||
const initialState: Partial<EvaluationState> = {
|
||||
assessmentSessionId: sessionId,
|
||||
knowledgeBaseId:
|
||||
session.knowledgeBaseId || session.knowledgeGroupId || '',
|
||||
messages: [],
|
||||
messages: hasQuestionsFromBank
|
||||
? [new HumanMessage(
|
||||
isZh ? '我已准备好回答问题。' : isJa ? '質問への回答準備ができています。' : 'I am ready to answer the questions.',
|
||||
)]
|
||||
: [],
|
||||
questionCount: session.templateJson?.questionCount,
|
||||
difficultyDistribution: session.templateJson?.difficultyDistribution,
|
||||
style: session.templateJson?.style,
|
||||
keywords: session.templateJson?.keywords,
|
||||
questionAnswerKey: session.templateJson?.questionAnswerKey,
|
||||
language: session.language || 'en',
|
||||
questions: hasQuestionsFromBank ? existingQuestions : undefined,
|
||||
};
|
||||
|
||||
this.logger.log(
|
||||
@@ -1504,7 +1537,8 @@ const initialState: Partial<EvaluationState> = {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const level = this.determineLevel(session.finalScore || 0);
|
||||
const passingThreshold = (session.templateJson?.passingScore ?? 60) / 10;
|
||||
const level = this.determineLevel(session.finalScore || 0, !!(session as any).passed, passingThreshold);
|
||||
const qrCode = `cert://${sessionId}-${Date.now()}`;
|
||||
|
||||
const questionDetails = (session.questions_json || []).map((q: any, i: number) => ({
|
||||
@@ -1535,11 +1569,11 @@ const initialState: Partial<EvaluationState> = {
|
||||
} as any;
|
||||
}
|
||||
|
||||
private determineLevel(score: number): string {
|
||||
private determineLevel(score: number, passed: boolean, passingThreshold: number): string {
|
||||
if (!passed) return 'Novice';
|
||||
if (score >= 9) return 'Expert';
|
||||
if (score >= 7.5) return 'Advanced';
|
||||
if (score >= 6) return 'Proficient';
|
||||
return 'Novice';
|
||||
if (score >= 7) return 'Advanced';
|
||||
return 'Proficient';
|
||||
}
|
||||
|
||||
async getStats(
|
||||
@@ -1723,7 +1757,7 @@ const initialState: Partial<EvaluationState> = {
|
||||
}
|
||||
|
||||
session.finalScore = newScore;
|
||||
const passingScore = (session.templateJson?.passingScore ?? 90) / 10;
|
||||
const passingScore = (session.templateJson?.passingScore ?? 60) / 10;
|
||||
(session as any).passed = newScore >= passingScore;
|
||||
session.reviewedBy = reviewerId;
|
||||
session.reviewedAt = new Date();
|
||||
|
||||
@@ -100,13 +100,11 @@ export class CreateTemplateDto {
|
||||
|
||||
@IsInt()
|
||||
@Min(60)
|
||||
@Max(7200)
|
||||
@IsOptional()
|
||||
@Max(86400)
|
||||
totalTimeLimit?: number;
|
||||
|
||||
@IsInt()
|
||||
@Min(30)
|
||||
@Max(1800)
|
||||
@IsOptional()
|
||||
@Max(3600)
|
||||
perQuestionTimeLimit?: number;
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ export class AssessmentTemplate {
|
||||
@Column({ type: 'int', name: 'question_count_max', default: 10 })
|
||||
questionCountMax: number;
|
||||
|
||||
@Column({ type: 'int', name: 'passing_score', default: 90 })
|
||||
@Column({ type: 'int', name: 'passing_score', default: 60 })
|
||||
passingScore: number;
|
||||
|
||||
@Column({ type: 'int', name: 'total_time_limit', default: 1800 })
|
||||
|
||||
@@ -56,7 +56,12 @@ const scoreSummary = Object.entries(scores)
|
||||
1. **你必须使用以下语言生成报告:中文 (Simplified Chinese)**。
|
||||
2. **严禁夹杂日文**。即使对话记录中包含日文,报告内容也必须全中文。
|
||||
3. 报告的第一行必须严格遵守此格式:"LEVEL: [Novice/Proficient/Advanced/Expert]"。
|
||||
4. 必须保持客观。如果用户没有提供有效的回答或得分为 0,你必须将其识别为 'Novice',并明确指出他们尚未证明其掌握程度。
|
||||
4. **等级判定必须遵循以下分数阈值**:
|
||||
- 总体平均分 >= 9 → Expert(专家)
|
||||
- 总体平均分 >= 7 → Advanced(高级)
|
||||
- 已通过(有有效回答且得分 > 0)→ Proficient(熟练)
|
||||
- 未通过(无有效回答或得分为 0)→ Novice(新手)
|
||||
即使得分很高,也要确保等级与上述阈值匹配。不要随意提高或降低等级。
|
||||
5. 不要虚构或幻想优点(如"潜力"或"好奇心"),如果用户明确表示"不知道"或未提供实质内容。
|
||||
6. 专注于对话记录中已证明的事实。
|
||||
|
||||
@@ -87,8 +92,13 @@ ${messages
|
||||
2. **中国語を混ぜないでください**。会話ログに中国語が含まれていても、レポートの内容はすべて日本語で記述してください。
|
||||
3. レポートの最初の行は, 必ず次の形式に従ってください:"LEVEL: [Novice/Proficient/Advanced/Expert]"。
|
||||
4. 客観的であること。ユーザーが有効な回答を提供しなかった場合、またはスコアが 0 の場合、'Novice' と判定し、習熟度が証明されていないことを明示してください。
|
||||
5. ユーザーが「わからない」と言ったり、内容を提供しなかった場合に、長所(「ポテンシャル」や「好奇心」など)を捏造しないでください。
|
||||
6. 会話ログで証明された事実に集中してください。
|
||||
5. **レベル判定は以下のスコアしきい値に従うこと**:
|
||||
- 平均スコア >= 9 → Expert
|
||||
- 平均スコア >= 7 → Advanced
|
||||
- 合格(有効な回答がありスコア > 0)→ Proficient
|
||||
- 不合格(有効な回答なし、またはスコア 0)→ Novice
|
||||
6. ユーザーが「わからない」と言ったり、内容を提供しなかった場合に、長所(「ポテンシャル」や「好奇心」など)を捏造しないでください。
|
||||
7. 会話ログで証明された事実に集中してください。
|
||||
|
||||
各ディメンションスコア:
|
||||
${dimensionAvg}
|
||||
@@ -115,8 +125,13 @@ IMPORTANT:
|
||||
1. **You MUST generate the report strictly in English.**
|
||||
2. START the report with exactly this format: "LEVEL: [Novice/Proficient/Advanced/Expert]" on the first line.
|
||||
3. Be OBJECTIVE. If the user provided no valid answers or scores are 0, you MUST identify them as 'Novice' and explicitly state they have NOT demonstrated mastery.
|
||||
4. DO NOT invent or hallucinate strengths (like 'potential' or 'curiosity') if the user explicitly said "I don't know" or provided no content.
|
||||
5. Focus on what was PROVEN in the conversation logs.
|
||||
4. **Level assignment MUST follow these score thresholds**:
|
||||
- Average score >= 9 → Expert
|
||||
- Average score >= 7 → Advanced
|
||||
- Passed (has valid answers with score > 0) → Proficient
|
||||
- Not passed (no valid answers or score is 0) → Novice
|
||||
5. DO NOT invent or hallucinate strengths (like 'potential' or 'curiosity') if the user explicitly said "I don't know" or provided no content.
|
||||
6. Focus on what was PROVEN in the conversation logs.
|
||||
|
||||
DIMENSION SCORES:
|
||||
${dimensionAvg}
|
||||
|
||||
@@ -90,34 +90,83 @@ export const questionGeneratorNode = async (
|
||||
.map((q, i) => `Q${i + 1}: ${q.questionText}`)
|
||||
.join('\n');
|
||||
|
||||
const systemPromptZh = `你是一个信息提取工具。严格按以下步骤操作。
|
||||
const systemPromptZh = `你是一个出题工具。严格按以下规则生成题目。
|
||||
|
||||
### 第一步:提取知识点
|
||||
阅读下方 Human 消息中的【知识库内容】,逐条列出其中包含的所有可考核知识点。
|
||||
每条以"知识点N:"开头,引用原文语句。如果不足,诚实报告。
|
||||
每条以"知识点N:"开头,引用原文语句。
|
||||
|
||||
### 第二步:从知识点生成考题
|
||||
仅用第一步提取的知识点生成 1 道题。必须引用知识点编号。
|
||||
### 第二步:基于知识点出题
|
||||
仅用第一步提取的知识点生成题目。必须引用知识点编号。
|
||||
如果知识点数量不足(少于3个),输出空数组 [] 并停止。
|
||||
|
||||
### 题型分配规则
|
||||
每生成 3 道题:
|
||||
- 第1、4、7...道:选择题(MULTIPLE_CHOICE),占 1/3
|
||||
- 第2、3、5、6...道:对话简答题(SHORT_ANSWER),占 2/3
|
||||
严格按照这个顺序循环,不要自行调整比例。
|
||||
|
||||
### 出题范围限制
|
||||
出题内容必须严格限制在知识库范围内。每道题必须有知识点编号引用。
|
||||
以下情况绝对禁止:
|
||||
- 使用 LLM 自身知识编题
|
||||
- 引用知识库中不存在的概念
|
||||
- 题目内容超出知识库覆盖的主题
|
||||
|
||||
### 选择题出题标准
|
||||
- 必须是场景驱动:描述一个真实工作场景,让用户判断最佳做法
|
||||
- 四个选项(A/B/C/D),只有一个正确,另外三个要有迷惑性
|
||||
- 难度:不是考概念背诵,是考实际应用判断
|
||||
- 正确答案必须附带解析,说明为什么对、错在哪
|
||||
- 出题依据必须引用第一步提取的知识点编号
|
||||
|
||||
### 对话简答题出题标准
|
||||
- 开放式场景问题,不预设标准答案
|
||||
- 考察用户的理解深度和表达能力
|
||||
- 适合多轮追问展开讨论
|
||||
- 出题依据必须引用第一步提取的知识点编号
|
||||
|
||||
### 绝对禁止:
|
||||
- 禁止使用知识库内容中不存在的任何概念、术语、数据
|
||||
- 禁止使用你自己的知识
|
||||
${existingQuestionsText ? `- 禁止与已出题目重复:${existingQuestionsText}` : ''}
|
||||
- 禁止出纯概念题(如"提示词六要素是什么")
|
||||
- 禁止出需要记忆具体数据的题
|
||||
- 禁止使用知识库之外的知识
|
||||
- 禁止生成与知识库主题无关的题目
|
||||
${existingQuestionsText ? `- 禁止与已出题目概念重复:${existingQuestionsText}` : ''}
|
||||
|
||||
### 输出(纯 JSON 数组):
|
||||
[
|
||||
{
|
||||
"knowledge_points": ["知识点引用"],
|
||||
"question_text": "基于知识点的题目",
|
||||
"key_points": ["评分要点"],
|
||||
"difficulty": "STANDARD|ADVANCED|SPECIALIST",
|
||||
"dimension": "prompt|llm|ide|devPattern|workCapability",
|
||||
"basis": "知识库原文"
|
||||
}
|
||||
]`;
|
||||
// dimension取值:prompt=提示词, llm=LLM原理, ide=IDE协作, devPattern=开发范式, workCapability=工作能力
|
||||
### 输出格式(严格遵循)
|
||||
选择题完整格式:
|
||||
{
|
||||
"question_type": "MULTIPLE_CHOICE",
|
||||
"question_text": "场景描述+问题,不超过120字",
|
||||
"options": ["A) 选项1", "B) 选项2", "C) 选项3", "D) 选项4"],
|
||||
"correct_answer": "A",
|
||||
"judgment": "解析:为什么对、为什么错,不超过200字",
|
||||
"key_points": ["考核要点", "2-3个"],
|
||||
"difficulty": "STANDARD",
|
||||
"dimension": "prompt",
|
||||
"basis": "知识点N:参考来源"
|
||||
}
|
||||
|
||||
const systemPromptJa = `あなたは情報抽出ツールです。以下の手順に厳密に従ってください。
|
||||
对话简答题完整格式:
|
||||
{
|
||||
"question_type": "SHORT_ANSWER",
|
||||
"question_text": "开放式场景问题,不超过120字",
|
||||
"key_points": ["期望的回答方向", "2-3个"],
|
||||
"difficulty": "STANDARD",
|
||||
"dimension": "prompt",
|
||||
"basis": "知识点N:参考来源"
|
||||
}
|
||||
|
||||
### 输出要求
|
||||
- 只输出 JSON 数组,不要其他文字
|
||||
- question_type 必须为 MULTIPLE_CHOICE 或 SHORT_ANSWER
|
||||
- dimension 只能取以下值之一:prompt、llm、ide、devPattern、workCapability
|
||||
- 每次生成 1 道题,以 JSON 数组格式输出
|
||||
- 选择题必须包含全部8个字段:question_text、options、correct_answer、judgment、key_points、difficulty、dimension、basis
|
||||
- 对话简答题必须包含全部6个字段:question_text、key_points、difficulty、dimension、basis
|
||||
- 每个字段的值不能为空`;
|
||||
|
||||
const systemPromptJa = `あなたは問題作成ツールです。以下の手順に厳密に従ってください。
|
||||
|
||||
### 第一歩:知識ポイントの抽出
|
||||
Human メッセージ内の【ナレッジベース内容】を読み、含まれるすべての評価可能な知識ポイントを箇条書きで抽出。
|
||||
@@ -126,48 +175,76 @@ Human メッセージ内の【ナレッジベース内容】を読み、含ま
|
||||
### 第二歩:知識ポイントから問題を作成
|
||||
第一歩で抽出した知識ポイントのみを使用して 1 問作成。知識ポイント番号を引用すること。
|
||||
|
||||
### 問題タイプの割合
|
||||
3問中、約1問を選択問題、2問を対話式記述問題にしてください。全体で約30%/70%の割合。
|
||||
|
||||
### 出題方向
|
||||
「AI協作スキル」に関する問題:
|
||||
- プロンプトの書き方(役割、タスク、背景、制約)
|
||||
- 複数ラウンドの対話テクニック
|
||||
- AIに先に質問させる方法
|
||||
- セッション管理(いつ継続、いつ新規)
|
||||
- よくある間違いと自己チェック
|
||||
- セキュリティ意識(機密データの取扱い)
|
||||
|
||||
### 選択問題の基準
|
||||
- シナリオ駆動:実務シーンを想定
|
||||
- 4択(A/B/C/D)、正解は1つ
|
||||
- 正解には必ず解説を含める
|
||||
|
||||
### 対話式記述問題の基準
|
||||
- オープンクエスチョン、正解なし
|
||||
- 理解の深さと表現力を評価
|
||||
|
||||
### 絶対禁止:
|
||||
- ナレッジベースに存在しない概念、用語、データの使用
|
||||
- 自身の知識の使用
|
||||
${existingQuestionsText ? `- 作成済み問題との重複禁止:${existingQuestionsText}` : ''}
|
||||
- 暗記問題の禁止
|
||||
- 知識ベースにない概念の使用禁止
|
||||
${existingQuestionsText ? `- 既出問題との重複禁止:${existingQuestionsText}` : ''}
|
||||
|
||||
### 出力(純粋な JSON 配列):
|
||||
[
|
||||
{
|
||||
"knowledge_points": ["知識ポイント参照"],
|
||||
"question_text": "知識ポイントに基づく問題",
|
||||
"key_points": ["採点ポイント"],
|
||||
"difficulty": "STANDARD|ADVANCED|SPECIALIST",
|
||||
"dimension": "prompt|llm|ide|devPattern|workCapability",
|
||||
"basis": "ナレッジベースの原文"
|
||||
}
|
||||
]`;
|
||||
### 出力
|
||||
JSON 配列のみ出力:
|
||||
選択問題:{"question_type":"MULTIPLE_CHOICE","question_text":"...","options":["A)...","B)...","C)...","D)..."],"correct_answer":"A","judgment":"...","key_points":["..."],"difficulty":"STANDARD","dimension":"prompt|llm|ide|devPattern|workCapability","basis":"..."}
|
||||
記述問題:{"question_type":"SHORT_ANSWER","question_text":"...","key_points":["..."],"difficulty":"STANDARD","dimension":"prompt|llm|ide|devPattern|workCapability","basis":"..."}`;
|
||||
|
||||
const systemPromptEn = `You are an information extraction tool. Follow these steps exactly.
|
||||
const systemPromptEn = `You are a question generation tool. Follow these steps exactly.
|
||||
|
||||
### Step 1: Extract Knowledge Points
|
||||
Read the knowledge base content in the Human message. List ALL assessable knowledge points found.
|
||||
Read the knowledge base content in the Human message. List ALL assessable knowledge points.
|
||||
Each point must start with "KP N:" and quote the source text. If insufficient, honestly report.
|
||||
|
||||
### Step 2: Generate Question from Points
|
||||
Use ONLY the knowledge points from Step 1 to generate 1 question. Must reference KP numbers.
|
||||
|
||||
### Absolutely Forbidden:
|
||||
- Using any concept, term, or data NOT present in the knowledge base content
|
||||
- Using your own knowledge
|
||||
${existingQuestionsText ? `- Repeating previous questions: ${existingQuestionsText}` : ''}
|
||||
### Type Mix
|
||||
Out of every 3 questions, approximately 1 should be MULTIPLE_CHOICE and 2 should be SHORT_ANSWER (dialogue-style). Roughly 30%/70% split.
|
||||
|
||||
### Output (pure JSON array only):
|
||||
[
|
||||
{
|
||||
"knowledge_points": ["KP reference"],
|
||||
"question_text": "Question based on the knowledge points",
|
||||
"key_points": ["scoring points"],
|
||||
"difficulty": "STANDARD|ADVANCED|SPECIALIST",
|
||||
"dimension": "prompt|llm|ide|devPattern|workCapability",
|
||||
"basis": "Source text from knowledge base"
|
||||
}
|
||||
]`;
|
||||
### Topics
|
||||
AI collaboration skills:
|
||||
- Writing good prompts (role, task, context, constraints)
|
||||
- Multi-turn iteration techniques
|
||||
- Letting AI ask clarifying questions first
|
||||
- Session management (continue vs new window)
|
||||
- Common mistakes and self-review
|
||||
- Security awareness (handling sensitive data)
|
||||
|
||||
### MC Standards
|
||||
- Scenario-driven: describe a real work scenario
|
||||
- 4 options (A/B/C/D), one correct
|
||||
- Must include judgment explaining why correct/incorrect
|
||||
|
||||
### SA Standards
|
||||
- Open-ended, no predefined answer
|
||||
- Tests understanding depth and expression
|
||||
|
||||
### Forbidden:
|
||||
- Pure concept recall questions
|
||||
- Questions requiring memorization of specific data
|
||||
${existingQuestionsText ? `- Repeating previous question concepts: ${existingQuestionsText}` : ''}
|
||||
|
||||
### Output
|
||||
JSON array only. One question at a time.
|
||||
MC: {"question_type":"MULTIPLE_CHOICE","question_text":"...","options":["A)...","B)...","C)...","D)..."],"correct_answer":"A","judgment":"...","key_points":["..."],"difficulty":"STANDARD","dimension":"prompt|llm|ide|devPattern|workCapability","basis":"..."}
|
||||
SA: {"question_type":"SHORT_ANSWER","question_text":"...","key_points":["..."],"difficulty":"STANDARD","dimension":"prompt|llm|ide|devPattern|workCapability","basis":"..."}`;
|
||||
|
||||
// dimension values: prompt=prompt engineering, llm=LLM principles, ide=IDE collaboration, devPattern=development paradigm, workCapability=work capability
|
||||
|
||||
@@ -201,6 +278,42 @@ ${existingQuestionsText ? `- Repeating previous questions: ${existingQuestionsTe
|
||||
newQuestions = [newQuestions];
|
||||
}
|
||||
|
||||
// === 代码级校验:确保 LLM 输出符合规范 ===
|
||||
const VALID_DIMENSIONS = ['prompt', 'llm', 'ide', 'devPattern', 'workCapability'];
|
||||
const VALID_TYPES = ['MULTIPLE_CHOICE', 'SHORT_ANSWER'];
|
||||
|
||||
const validatedQuestions = newQuestions.filter((q: any) => {
|
||||
const qType = q.question_type;
|
||||
const dim = q.dimension?.toString().toLowerCase().trim();
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!VALID_TYPES.includes(qType)) errors.push(`invalid question_type: ${qType}`);
|
||||
if (!dim || !VALID_DIMENSIONS.includes(dim)) errors.push(`invalid dimension: ${q.dimension}`);
|
||||
if (!q.question_text || q.question_text.length < 5) errors.push('question_text missing or too short');
|
||||
|
||||
if (qType === 'MULTIPLE_CHOICE') {
|
||||
if (!Array.isArray(q.options) || q.options.length < 2) errors.push('options missing or insufficient');
|
||||
if (!q.correct_answer) errors.push('correct_answer missing');
|
||||
if (!q.judgment) errors.push('judgment missing');
|
||||
} else if (qType === 'SHORT_ANSWER') {
|
||||
if (!Array.isArray(q.key_points) || q.key_points.length === 0) errors.push('key_points missing');
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.warn('[GeneratorNode] Validation failed for question:', errors.join('; '));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (validatedQuestions.length === 0) {
|
||||
console.warn('[GeneratorNode] All generated questions failed validation, using existing questions only');
|
||||
return { questions: existingQuestions };
|
||||
}
|
||||
|
||||
// 只取验证通过的题目
|
||||
newQuestions = validatedQuestions;
|
||||
|
||||
const dimensionMap: Record<string, string> = {
|
||||
// 中文
|
||||
'技术能力-提示词': 'prompt',
|
||||
@@ -228,15 +341,27 @@ ${existingQuestionsText ? `- Repeating previous questions: ${existingQuestionsTe
|
||||
inferredDimension = dimensionMap[dimValue] || 'workCapability';
|
||||
console.log('[GeneratorNode] Dimension mapping:', { original: q.dimension, mapped: inferredDimension });
|
||||
}
|
||||
return {
|
||||
|
||||
const qType = q.question_type === 'MULTIPLE_CHOICE' ? 'MULTIPLE_CHOICE' : 'SHORT_ANSWER';
|
||||
const base = {
|
||||
id: (existingQuestions.length + 1).toString(),
|
||||
questionText: q.question_text,
|
||||
questionType: 'SHORT_ANSWER',
|
||||
keyPoints: q.key_points,
|
||||
difficulty: q.difficulty,
|
||||
basis: q.basis,
|
||||
questionType: qType,
|
||||
keyPoints: q.key_points || [],
|
||||
difficulty: q.difficulty || 'STANDARD',
|
||||
basis: q.basis || '',
|
||||
dimension: inferredDimension,
|
||||
};
|
||||
|
||||
if (qType === 'MULTIPLE_CHOICE') {
|
||||
return {
|
||||
...base,
|
||||
options: q.options || [],
|
||||
correctAnswer: q.correct_answer || '',
|
||||
judgment: q.judgment || '',
|
||||
};
|
||||
}
|
||||
return base;
|
||||
});
|
||||
|
||||
const questionsToGenerate = Math.max(1, limitCount - existingQuestions.length);
|
||||
|
||||
@@ -91,6 +91,72 @@ export const graderNode = async (
|
||||
};
|
||||
}
|
||||
|
||||
// ── Rule-based grading: use structured followupMapping if available ──
|
||||
if (currentQuestion.followupHints) {
|
||||
let mapping: any = null;
|
||||
if (typeof currentQuestion.followupHints === 'string') {
|
||||
try { mapping = JSON.parse(currentQuestion.followupHints); } catch {}
|
||||
} else if (typeof currentQuestion.followupHints === 'object') {
|
||||
mapping = currentQuestion.followupHints;
|
||||
}
|
||||
if (mapping && Array.isArray(mapping.branches)) {
|
||||
const userAnswerText = typeof lastUserMessage.content === 'string'
|
||||
? lastUserMessage.content : JSON.stringify(lastUserMessage.content);
|
||||
|
||||
// Score based on keyword coverage
|
||||
let bestScore = mapping.defaultScore ?? 5;
|
||||
let matchedFollowup = mapping.defaultFollowup || '';
|
||||
let matchedAll = true;
|
||||
const maxFollowUps = mapping.maxFollowups ?? 2;
|
||||
|
||||
for (const branch of mapping.branches) {
|
||||
const kws = branch.keywords || [];
|
||||
const matchCount = kws.filter((kw: string) => userAnswerText.toLowerCase().includes(kw.toLowerCase())).length;
|
||||
if (kws.length > 0 && matchCount >= kws.length * 0.5) {
|
||||
const branchScore = branch.score ?? 7;
|
||||
if (branchScore > bestScore) bestScore = branchScore;
|
||||
if (branch.followup) matchedFollowup = branch.followup;
|
||||
} else if (kws.length > 0 && matchCount === 0) {
|
||||
matchedAll = false;
|
||||
}
|
||||
}
|
||||
|
||||
const completionThreshold = mapping.completionThreshold ?? 80;
|
||||
const tooShort = userAnswerText.trim().length < 8;
|
||||
const saysIDontKnow = userAnswerText.trim().length < 10 && (
|
||||
userAnswerText.includes('不知道') || userAnswerText.includes("don't know") || userAnswerText.includes('わかりません')
|
||||
);
|
||||
|
||||
let shouldFollowUp: boolean;
|
||||
if (saysIDontKnow || tooShort) {
|
||||
shouldFollowUp = false;
|
||||
bestScore = Math.min(bestScore, 2);
|
||||
} else if (bestScore >= completionThreshold / 10) {
|
||||
shouldFollowUp = false;
|
||||
} else if (currentFollowUpCount >= maxFollowUps) {
|
||||
shouldFollowUp = false;
|
||||
} else {
|
||||
shouldFollowUp = true;
|
||||
}
|
||||
|
||||
const feedbackMessage = new AIMessage(`Score: ${bestScore}/10\n\nFeedback: ${shouldFollowUp ? matchedFollowup : '回答已覆盖关键点。'}`);
|
||||
|
||||
const feedbackHistoryMessages = shouldFollowUp && matchedFollowup
|
||||
? [feedbackMessage, new AIMessage(matchedFollowup)]
|
||||
: [feedbackMessage];
|
||||
|
||||
console.log('[GraderNode] Rule grading:', { score: bestScore, shouldFollowUp, matchedAll, followup: matchedFollowup?.substring(0, 60) });
|
||||
|
||||
return {
|
||||
feedbackHistory: feedbackHistoryMessages,
|
||||
scores: { [currentQuestion.id || currentQuestionIndex.toString()]: bestScore },
|
||||
shouldFollowUp,
|
||||
followUpCount: shouldFollowUp ? currentFollowUpCount + 1 : 0,
|
||||
currentQuestionIndex: shouldFollowUp ? currentQuestionIndex : currentQuestionIndex + 1,
|
||||
} as any;
|
||||
}
|
||||
}
|
||||
|
||||
const systemPromptZh = `你是一位考官。请评分并给出反馈。
|
||||
|
||||
规则:
|
||||
@@ -100,8 +166,10 @@ export const graderNode = async (
|
||||
问题:${currentQuestion.questionText}
|
||||
关键点:${currentQuestion.keyPoints.join(', ')}
|
||||
|
||||
评分标准:准确性、完整性、深度。
|
||||
部分正确也给分(5-7分),完全不沾边才0-2分。
|
||||
评分标准:不要求深度,不要求使用特定术语,只看用户是否理解了概念。
|
||||
用户理解核心概念就给分。即使没有使用关键点中的原词,只要意思到位就算覆盖。
|
||||
例如关键点是"上下文窗口有限",用户说"信息太多超过AI处理长度"也是覆盖。
|
||||
评分原则:往宽了给分,不确定时就给高分。明显正确就给8-10分,部分正确5-7分,完全不沾边才0-2分。
|
||||
|
||||
返回JSON:
|
||||
- score: 0-10
|
||||
|
||||
@@ -456,10 +456,6 @@ export class QuestionBankService {
|
||||
): Promise<QuestionBankItem[]> {
|
||||
const bank = await this.findOne(bankId);
|
||||
|
||||
if (bank.status !== QuestionBankStatus.DRAFT) {
|
||||
throw new ForbiddenException('仅草稿状态的题库可生成题目');
|
||||
}
|
||||
|
||||
if (count <= 0 || count > 50) {
|
||||
throw new BadRequestException('生成数量必须在 1-50 之间');
|
||||
}
|
||||
@@ -523,6 +519,7 @@ export class QuestionBankService {
|
||||
async selectQuestions(
|
||||
bankId: string,
|
||||
count: number,
|
||||
dimensionWeights?: Array<{ name: string; weight: number }>,
|
||||
): Promise<QuestionBankItem[]> {
|
||||
const bank = await this.findOne(bankId);
|
||||
|
||||
@@ -537,40 +534,51 @@ export class QuestionBankService {
|
||||
|
||||
const usedIds = new Set<string>();
|
||||
const selected: QuestionBankItem[] = [];
|
||||
const availableItems = [...allItems];
|
||||
let availableItems = [...allItems];
|
||||
|
||||
let dimIdx = 0;
|
||||
while (selected.length < count && availableItems.length > 0) {
|
||||
const dim = DIMENSIONS[dimIdx % DIMENSIONS.length];
|
||||
dimIdx++;
|
||||
|
||||
const available = availableItems.filter(
|
||||
(i) => i.dimension === dim && !usedIds.has(i.id),
|
||||
);
|
||||
|
||||
if (available.length > 0) {
|
||||
const idx = Math.floor(Math.random() * available.length);
|
||||
const item = available[idx];
|
||||
selected.push(item);
|
||||
usedIds.add(item.id);
|
||||
const actualIdx = availableItems.findIndex(i => i.id === item.id);
|
||||
if (actualIdx > -1) {
|
||||
availableItems.splice(actualIdx, 1);
|
||||
if (dimensionWeights && dimensionWeights.length > 0) {
|
||||
const totalWeight = dimensionWeights.reduce((s, d) => s + d.weight, 0);
|
||||
for (const dw of dimensionWeights) {
|
||||
const dimName = dw.name as QuestionDimension;
|
||||
const targetForDim = Math.round(count * dw.weight / totalWeight);
|
||||
const pool = availableItems.filter(i => i.dimension === dimName && !usedIds.has(i.id));
|
||||
this.shuffleArray(pool);
|
||||
const take = Math.min(targetForDim, pool.length);
|
||||
for (let i = 0; i < take; i++) {
|
||||
selected.push(pool[i]);
|
||||
usedIds.add(pool[i].id);
|
||||
}
|
||||
}
|
||||
|
||||
if (dimIdx >= DIMENSIONS.length * 3) {
|
||||
break;
|
||||
availableItems = availableItems.filter(i => !usedIds.has(i.id));
|
||||
this.shuffleArray(availableItems);
|
||||
while (selected.length < count && availableItems.length > 0) {
|
||||
const item = availableItems.pop()!;
|
||||
selected.push(item);
|
||||
usedIds.add(item.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (selected.length < count && availableItems.length > 0) {
|
||||
const shuffled = this.shuffleArray([...availableItems]);
|
||||
for (const item of shuffled) {
|
||||
if (selected.length >= count) break;
|
||||
if (!usedIds.has(item.id)) {
|
||||
} else {
|
||||
let dimIdx = 0;
|
||||
while (selected.length < count && availableItems.length > 0) {
|
||||
const dim = DIMENSIONS[dimIdx % DIMENSIONS.length];
|
||||
dimIdx++;
|
||||
const pool = availableItems.filter(i => i.dimension === dim && !usedIds.has(i.id));
|
||||
if (pool.length > 0) {
|
||||
const idx = Math.floor(Math.random() * pool.length);
|
||||
const item = pool[idx];
|
||||
selected.push(item);
|
||||
usedIds.add(item.id);
|
||||
availableItems = availableItems.filter(i => i.id !== item.id);
|
||||
}
|
||||
if (dimIdx >= DIMENSIONS.length * 3) break;
|
||||
}
|
||||
if (selected.length < count && availableItems.length > 0) {
|
||||
this.shuffleArray(availableItems);
|
||||
for (const item of availableItems) {
|
||||
if (selected.length >= count) break;
|
||||
if (!usedIds.has(item.id)) {
|
||||
selected.push(item);
|
||||
usedIds.add(item.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ export class TemplateService {
|
||||
const { ...data } = createDto;
|
||||
const template = this.templateRepository.create({
|
||||
...data,
|
||||
isActive: data.isActive !== undefined ? data.isActive : true,
|
||||
createdBy: userId,
|
||||
tenantId,
|
||||
});
|
||||
|
||||
@@ -76,7 +76,7 @@ export const AssessmentTemplateManager: React.FC = () => {
|
||||
: (template.difficultyDistribution || ''),
|
||||
style: template.style || 'Professional',
|
||||
knowledgeGroupId: template.knowledgeGroupId || '',
|
||||
passingScore: template.passingScore ? template.passingScore / 10 : 6,
|
||||
passingScore: template.passingScore !== null && template.passingScore !== undefined ? template.passingScore / 10 : 6,
|
||||
totalTimeLimit: template.totalTimeLimit ?? 1800,
|
||||
perQuestionTimeLimit: template.perQuestionTimeLimit ?? 300,
|
||||
});
|
||||
@@ -436,7 +436,7 @@ export const AssessmentTemplateManager: React.FC = () => {
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1 ml-1 flex items-center gap-2">
|
||||
<Hash size={12} className="text-indigo-500" />总时间限制 (秒)
|
||||
</label>
|
||||
<input type="number" min="60" max="7200" step="60"
|
||||
<input type="number" min="60" max="86400" step="60"
|
||||
className="w-full px-5 py-4 bg-slate-50 border border-slate-200 rounded-[1.25rem] text-sm font-bold focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500/50 outline-none transition-all"
|
||||
value={formData.totalTimeLimit}
|
||||
onChange={e => setFormData({ ...formData, totalTimeLimit: parseInt(e.target.value) || 1800 })}
|
||||
@@ -446,7 +446,7 @@ export const AssessmentTemplateManager: React.FC = () => {
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1 ml-1 flex items-center gap-2">
|
||||
<Hash size={12} className="text-indigo-500" />单题时间限制 (秒)
|
||||
</label>
|
||||
<input type="number" min="30" max="1800" step="30"
|
||||
<input type="number" min="30" max="3600" step="30"
|
||||
className="w-full px-5 py-4 bg-slate-50 border border-slate-200 rounded-[1.25rem] text-sm font-bold focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500/50 outline-none transition-all"
|
||||
value={formData.perQuestionTimeLimit}
|
||||
onChange={e => setFormData({ ...formData, perQuestionTimeLimit: parseInt(e.target.value) || 300 })}
|
||||
|
||||
@@ -296,6 +296,8 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
|
||||
if (event.data.status === 'COMPLETED') {
|
||||
setSession(prev => prev ? { ...prev, status: 'COMPLETED' } : null);
|
||||
fetchHistory();
|
||||
} else if (event.data.currentQuestionIndex !== undefined) {
|
||||
assessmentService.nextQuestion(session.id).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -620,7 +622,6 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
|
||||
{currentQuestion.options.map((opt: string, i: number) => {
|
||||
const letter = optionLabels[i];
|
||||
const isSelected = selectedChoice === letter;
|
||||
const displayText = opt.slice(1);
|
||||
return (
|
||||
<button
|
||||
key={letter}
|
||||
@@ -634,10 +635,7 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
|
||||
isTimedOut && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
<span className="inline-flex items-center justify-center w-7 h-7 rounded-xl text-xs font-black mr-3 shrink-0 border-2 border-current">
|
||||
{letter}
|
||||
</span>
|
||||
{displayText}
|
||||
{opt}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -662,7 +660,7 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey && !isTimedOut) {
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey) && !isTimedOut) {
|
||||
e.preventDefault();
|
||||
handleSubmitAnswer();
|
||||
}
|
||||
@@ -867,16 +865,15 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
|
||||
{q.options?.map((opt: string, oi: number) => {
|
||||
const letter = String.fromCharCode(65 + oi);
|
||||
const isAnswer = letter === q.correctAnswer;
|
||||
const displayText = opt.slice(1);
|
||||
return (
|
||||
<span key={oi} className={cn(
|
||||
"px-3 py-1 rounded-lg font-medium",
|
||||
isAnswer ? "bg-emerald-100 text-emerald-700 border border-emerald-200" : "bg-slate-50 text-slate-500"
|
||||
)}>
|
||||
{letter}. {displayText}
|
||||
{opt}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{q.judgment && (
|
||||
|
||||
@@ -147,6 +147,11 @@ export class AssessmentService {
|
||||
return data;
|
||||
}
|
||||
|
||||
async nextQuestion(sessionId: string): Promise<{ success: boolean }> {
|
||||
const { data } = await apiClient.post<{ success: boolean }>(`/assessment/${sessionId}/next-question`, {});
|
||||
return data;
|
||||
}
|
||||
|
||||
async forceEnd(sessionId: string): Promise<AssessmentSession> {
|
||||
const { data } = await apiClient.post<AssessmentSession>(`/assessment/${sessionId}/force-end`, {});
|
||||
return data;
|
||||
|
||||
Reference in New Issue
Block a user