data: add merged question banks for all three roles

This commit is contained in:
薇星幻海
2026-10-10 04:25:51 +08:00
parent ed24cf18d0
commit 3969c233b3
9 changed files with 18836 additions and 2 deletions
+2
View File
@@ -0,0 +1,2 @@
# Preserve CSV BOM, record separators, and embedded line breaks byte for byte.
题库/*.csv -text whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol
+1 -1
View File
@@ -1,4 +1,4 @@
# 原始下载网页、压缩包和题库不提交到源代码仓库。 # 原始下载网页、压缩包和临时导出不提交;题库/ 中的岗位合并 CSV 提交到仓库。
网页样本/ 网页样本/
导出结果/ 导出结果/
*.zip *.zip
+11 -1
View File
@@ -15,6 +15,16 @@
## 导入题库与搜索答案 ## 导入题库与搜索答案
仓库已提供按岗位合并的题库,可直接下载并导入,文件内已标注岗位:
| 岗位 | 合并文件 | 记录数 |
|---|---|---:|
| 副操 | [副操_合并题库.csv](题库/副操_合并题库.csv) | 1,560 |
| 主操 | [主操_合并题库.csv](题库/主操_合并题库.csv) | 1,620 |
| 班长 | [班长_合并题库.csv](题库/班长_合并题库.csv) | 2,340 |
可选单个岗位文件,也可同时选择三个文件导入。合并保留全部原记录、课程名称、原题号、参考答案和来源网址,并增加岗位及来源文件列;不同课程的重复题或答案差异保留。详细来源和重新生成方法见 [题库说明](题库/README.md)。
- ZIP 内按“主操 / 班长 / 副操”目录自动归类;成功导入后全量替换已有题库。提供的 `题库.zip` 已核对为 28 个 CSV、5,520 条记录:主操 1,620、班长 2,340、副操 1,560。 - ZIP 内按“主操 / 班长 / 副操”目录自动归类;成功导入后全量替换已有题库。提供的 `题库.zip` 已核对为 28 个 CSV、5,520 条记录:主操 1,620、班长 2,340、副操 1,560。
- 单个或多个 CSV / JSON 建议先选择岗位;文件缺少岗位信息时,使用当前选择的岗位。导入会更新相同岗位、课程和来源的数据,保留其他岗位及其他来源。 - 单个或多个 CSV / JSON 建议先选择岗位;文件缺少岗位信息时,使用当前选择的岗位。导入会更新相同岗位、课程和来源的数据,保留其他岗位及其他来源。
- 所有文件先解析和验证,再保存。题干、选项或参考答案无效时,会提示文件及题目位置,原题库保留。 - 所有文件先解析和验证,再保存。题干、选项或参考答案无效时,会提示文件及题目位置,原题库保留。
@@ -75,6 +85,6 @@ $env:PLAYWRIGHT_CHANNEL = 'edge'
npm test npm test
``` ```
验证使用本地模拟页面,不连接谊安学服务器;真实登录后的线上使用仍需在实际环境验收。本地存在题库 ZIP 或保存网页样本时,还会核对对应内容。原始 ZIP、解压网页及实际导出题库均由 `.gitignore` 排除,不提交到源码仓库。 验证使用本地模拟页面,不连接谊安学服务器;真实登录后的线上使用仍需在实际环境验收。本地存在题库 ZIP 或保存网页样本时,还会核对对应内容。原始 ZIP、解压网页及临时导出由 `.gitignore` 排除;`题库/` 中的三个岗位合并 CSV 随仓库提供。
远程仓库:`ssh://git@192.168.10.31:2222/qishizhe/yianxue-shauti.git`。 远程仓库:`ssh://git@192.168.10.31:2222/qishizhe/yianxue-shauti.git`。
+1
View File
@@ -5,6 +5,7 @@
"description": "不绑定网址的谊安学题库助手:岗位搜索、允许复制和练习自动选择", "description": "不绑定网址的谊安学题库助手:岗位搜索、允许复制和练习自动选择",
"scripts": { "scripts": {
"build": "node build-userscript.mjs", "build": "node build-userscript.mjs",
"merge:banks": "node scripts/merge-question-banks.cjs",
"pretest": "node build-userscript.mjs", "pretest": "node build-userscript.mjs",
"test": "node tests/run.cjs", "test": "node tests/run.cjs",
"test:assistant": "node tests/assistant.cjs" "test:assistant": "node tests/assistant.cjs"
+111
View File
@@ -0,0 +1,111 @@
/* Merge each role's CSV files without losing course-specific question records. */
'use strict';
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const rootDir = path.resolve(__dirname, '..');
const JSZip = require(path.join(rootDir, 'vendor', 'jszip.min.js'));
const bankSource = fs.readFileSync(path.join(rootDir, 'bank-store.js'), 'utf8');
const roles = ['副操', '主操', '班长'];
const originalHeaders = ['题号', '题型', '题目', ...'ABCDEFGH'.split('').map(label => '选项' + label), '全部选项', '参考答案', '答案对应选项', '题目解析', '图片链接', '来源网址', '题库名称', '完整性', '网页题数', '缺失答案题号'];
function runtime() {
const environment = { TextDecoder, JSZip, GM_getValue: (_key, fallback) => fallback, GM_setValue: () => {} };
vm.createContext(environment);
vm.runInContext(bankSource, environment);
return environment.YianBankStore;
}
function file(name, bytes) {
return { name, size: bytes.length, arrayBuffer: async () => bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) };
}
function decode(bytes) {
try { return new TextDecoder('utf-8', { fatal: true }).decode(bytes); }
catch (_) { return new TextDecoder('gb18030', { fatal: true }).decode(bytes); }
}
function csv(rows) {
const cell = value => '"' + String(value ?? '').replace(/"/g, '""') + '"';
return '\uFEFF' + rows.map(row => row.map(cell).join(',')).join('\r\n') + '\r\n';
}
function comparable(records) {
// sourceFile changes to the merged filename on CSV import; all answer data must remain identical.
return Array.from(records, ({ sourceFile, ...record }) => JSON.stringify(record)).sort();
}
async function main() {
const inputPath = path.resolve(process.argv[2] || path.join(rootDir, '题库.zip'));
const outputDir = path.resolve(process.argv[3] || path.join(rootDir, '题库'));
const input = fs.readFileSync(inputPath);
const zip = await JSZip.loadAsync(input);
const entries = Object.values(zip.files).filter(entry => !entry.dir && /\.csv$/i.test(entry.name)
&& !entry.name.replace(/\\/g, '/').split('/').some(part => /^(?:__MACOSX|\.DS_Store|\.~lock\.|~\$|\._)/i.test(part)))
.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
if (!entries.length) throw new Error('ZIP 内没有 CSV 题库');
const parser = runtime();
const groups = new Map(roles.map(role => [role, { files: [], rows: [] }]));
const headers = ['岗位', '来源文件', ...originalHeaders];
for (const entry of entries) {
const role = entry.name.replace(/\\/g, '/').split('/').find(part => roles.includes(part));
if (!role) throw new Error('题库文件没有岗位目录:' + entry.name);
const rows = parser.parseCSV(decode(await entry.async('uint8array')));
if (rows.length < 2) throw new Error('CSV 没有题目:' + entry.name);
const sourceHeaders = rows[0].map(value => value.replace(/^\uFEFF/, '').trim());
if (new Set(sourceHeaders).size !== sourceHeaders.length) throw new Error('CSV 列名重复:' + entry.name);
if (sourceHeaders.includes('岗位') || sourceHeaders.includes('来源文件')) throw new Error('请使用原始导出 CSV;文件已有合并列:' + entry.name);
sourceHeaders.forEach(name => { if (!headers.includes(name)) headers.push(name); });
const group = groups.get(role);
group.files.push({ name: entry.name, total: rows.length - 1 });
for (const [index, row] of rows.slice(1).entries()) {
if (row.length !== sourceHeaders.length) throw new Error(entry.name + ' 第 ' + (index + 2) + ' 条记录列数不匹配');
group.rows.push({ 岗位: role, 来源文件: entry.name, ...Object.fromEntries(sourceHeaders.map((name, column) => [name, row[column]])) });
}
}
// Use the assistant's actual import validator to compare the source ZIP with all three results.
const original = await runtime().importFiles([file(path.basename(inputPath), input)]);
const outputs = roles.map(role => {
const group = groups.get(role);
if (!group.rows.length) throw new Error('缺少岗位题库:' + role);
return { role, name: role + '_合并题库.csv', bytes: Buffer.from(csv([headers, ...group.rows.map(row => headers.map(name => row[name] ?? ''))])) };
});
const merged = await runtime().importFiles(outputs.map(output => file(output.name, output.bytes)));
const sourceRowCount = roles.reduce((total, role) => total + groups.get(role).rows.length, 0);
assert.equal(merged.total, sourceRowCount, '合并后导入丢失了题目记录');
const originalRecords = comparable(original.records);
assert(comparable(merged.records).every((record, index) => record === originalRecords[index]), '合并前后题干、选项、答案或课程来源发生变化');
const report = [
'# 按岗位合并题库', '',
'由原始 `题库.zip` 中各岗位的 CSV 合并生成。三个文件均可在“谊安学助手 2.0”的“导入题库”中直接选择,也可同时导入;文件内已有岗位字段,无需靠当前岗位选择归类。', '',
'| 文件 | 原文件数 | 记录数 | 单选 | 多选 | 判断 |',
'|---|---:|---:|---:|---:|---:|'
];
for (const output of outputs) {
const records = merged.records.filter(record => record.role === output.role);
const count = type => records.filter(record => record.type === type).length;
report.push('| [' + output.name + '](' + output.name + ') | ' + groups.get(output.role).files.length + ' | ' + records.length + ' | ' + count('单选') + ' | ' + count('多选') + ' | ' + count('判断') + ' |');
}
report.push('', '合计 ' + merged.total + ' 条记录。统一保留选项 A–H,原文件没有的选项为空;原字段逐格保留,并增加“岗位”和“来源文件”。题号保持原课程题号,不同课程可有相同题号。CSV 使用 UTF-8 BOM,可用 Excel 打开。', '',
'合并保留全部记录,不按题干去重,避免丢失课程来源或不同选项。同题在不同课程出现答案差异时也原样保留;搜索时可按课程核对。“常压罐组收料作业中,以下哪些属于工程控制措施?”在风险管理题库中为 ABC,在重大危险源题库中为 ABCD。', '',
'负数等原有防公式单引号转义保持不变,助手导入时会按原规则恢复选项文字。', '',
'## 来源文件', '', '| 岗位 | 原 CSV | 记录数 |', '|---|---|---:|');
for (const role of roles) {
for (const source of groups.get(role).files) report.push('| ' + role + ' | ' + source.name.replace(/\|/g, '\\|') + ' | ' + source.total + ' |');
}
report.push('', '## 重新生成', '', '在仓库根目录运行(原始 ZIP 不提交到 Git):', '', '```powershell', 'npm run merge:banks', '```', '',
'也可指定输入和输出目录:', '', '```powershell', 'node scripts/merge-question-banks.cjs "题库.zip" "题库"', '```', '',
'脚本在写入前调用助手的实际导入逻辑,逐条核对合并前后的题干、选项、答案、岗位、题号及课程来源。无需安装额外依赖。', '',
'原始 ZIP SHA-256:`' + crypto.createHash('sha256').update(input).digest('hex') + '`。', '');
fs.mkdirSync(outputDir, { recursive: true });
outputs.forEach(output => fs.writeFileSync(path.join(outputDir, output.name), output.bytes));
fs.writeFileSync(path.join(outputDir, 'README.md'), report.join('\n'));
console.log('已验证并合并 ' + entries.length + ' 个原始 CSV,共 ' + merged.total + ' 条记录:');
outputs.forEach(output => console.log(output.role + ':' + merged.counts[output.role] + ' 条 → ' + path.join(outputDir, output.name)));
}
main().catch(error => { console.error(error.message); process.exitCode = 1; });
+66
View File
@@ -0,0 +1,66 @@
# 按岗位合并题库
由原始 `题库.zip` 中各岗位的 CSV 合并生成。三个文件均可在“谊安学助手 2.0”的“导入题库”中直接选择,也可同时导入;文件内已有岗位字段,无需靠当前岗位选择归类。
| 文件 | 原文件数 | 记录数 | 单选 | 多选 | 判断 |
|---|---:|---:|---:|---:|---:|
| [副操_合并题库.csv](副操_合并题库.csv) | 9 | 1560 | 520 | 520 | 520 |
| [主操_合并题库.csv](主操_合并题库.csv) | 9 | 1620 | 540 | 540 | 540 |
| [班长_合并题库.csv](班长_合并题库.csv) | 10 | 2340 | 780 | 780 | 780 |
合计 5520 条记录。统一保留选项 A–H,原文件没有的选项为空;原字段逐格保留,并增加“岗位”和“来源文件”。题号保持原课程题号,不同课程可有相同题号。CSV 使用 UTF-8 BOM,可用 Excel 打开。
合并保留全部记录,不按题干去重,避免丢失课程来源或不同选项。同题在不同课程出现答案差异时也原样保留;搜索时可按课程核对。“常压罐组收料作业中,以下哪些属于工程控制措施?”在风险管理题库中为 ABC,在重大危险源题库中为 ABCD。
负数等原有防公式单引号转义保持不变,助手导入时会按原规则恢复选项文字。
## 来源文件
| 岗位 | 原 CSV | 记录数 |
|---|---|---:|
| 副操 | 题库/副操/事故事件-1_题库_60题.csv | 60 |
| 副操 | 题库/副操/作业许可-1_题库_540题.csv | 540 |
| 副操 | 题库/副操/合规管理_题库_60题.csv | 60 |
| 副操 | 题库/副操/安全生产信息-罐区_题库_240题.csv | 240 |
| 副操 | 题库/副操/安全生产责任制-副操_题库_120题.csv | 120 |
| 副操 | 题库/副操/安全领导力-1_题库_120题.csv | 120 |
| 副操 | 题库/副操/应急管理-罐区_题库_240题.csv | 240 |
| 副操 | 题库/副操/重大危险源安全管理-1-罐区_题库_60题.csv | 60 |
| 副操 | 题库/副操/风险管理-1-罐区_题库_120题.csv | 120 |
| 主操 | 题库/主操/事故事件-1_题库_60题.csv | 60 |
| 主操 | 题库/主操/作业许可-1_题库_540题.csv | 540 |
| 主操 | 题库/主操/合规管理_题库_60题.csv | 60 |
| 主操 | 题库/主操/安全生产信息-罐区_题库_240题.csv | 240 |
| 主操 | 题库/主操/安全生产责任制-主操_题库_120题.csv | 120 |
| 主操 | 题库/主操/安全领导力-1_题库_120题.csv | 120 |
| 主操 | 题库/主操/应急管理-罐区_题库_240题.csv | 240 |
| 主操 | 题库/主操/重大危险源安全管理-1-罐区_题库_60题.csv | 60 |
| 主操 | 题库/主操/风险管理-2-罐区_题库_180题.csv | 180 |
| 班长 | 题库/班长/事故事件-2_题库_120题.csv | 120 |
| 班长 | 题库/班长/作业许可-2_题库_660题.csv | 660 |
| 班长 | 题库/班长/合规管理_题库_60题.csv | 60 |
| 班长 | 题库/班长/安全生产信息-罐区_题库_240题.csv | 240 |
| 班长 | 题库/班长/安全生产责任制-班长_题库_120题.csv | 120 |
| 班长 | 题库/班长/安全领导力-2_题库_240题.csv | 240 |
| 班长 | 题库/班长/应急管理-罐区_题库_240题.csv | 240 |
| 班长 | 题库/班长/承包商安全管理-1_题库_120题.csv | 120 |
| 班长 | 题库/班长/重大危险源安全管理-2-罐区_题库_120题.csv | 120 |
| 班长 | 题库/班长/风险管理-3-罐区_题库_420题.csv | 420 |
## 重新生成
在仓库根目录运行(原始 ZIP 不提交到 Git):
```powershell
npm run merge:banks
```
也可指定输入和输出目录:
```powershell
node scripts/merge-question-banks.cjs "题库.zip" "题库"
```
脚本在写入前调用助手的实际导入逻辑,逐条核对合并前后的题干、选项、答案、岗位、题号及课程来源。无需安装额外依赖。
原始 ZIP SHA-256:`21758bc40614fc84a5a10fad25cd891ab9775d80ef12427e5cead5b62bf3ea09`。
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff