feat(aihr): complete phase two pilot readiness
This commit is contained in:
@@ -3,6 +3,7 @@ import { execFileSync } from 'node:child_process';
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { verifyPilotExportContract } from './lib/pilot-export-contract.mjs';
|
||||
|
||||
const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
|
||||
const args = new Map(process.argv.slice(2).map((arg) => {
|
||||
@@ -21,6 +22,10 @@ const file = path.resolve(root, args.get('file') || 'docs/20260708/发布会演
|
||||
const prefixes = (args.get('prefix') || 'Q').split(',').map((item) => item.trim()).filter(Boolean);
|
||||
const ids = new Set((args.get('ids') || '').split(',').map((item) => item.trim()).filter(Boolean));
|
||||
const limit = Number(args.get('limit') || 0);
|
||||
const orgDryRun = args.has('org-dry-run');
|
||||
const pilotExport = args.has('pilot-export');
|
||||
const pilotStartDate = args.get('start-date') || '';
|
||||
const pilotEndDate = args.get('end-date') || '';
|
||||
|
||||
function readEnv(filePath) {
|
||||
const env = {};
|
||||
@@ -100,6 +105,26 @@ async function request(method, endpoint, body, token, encrypted = false) {
|
||||
}
|
||||
}
|
||||
|
||||
async function requestText(method, endpoint, token) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 70000);
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}${endpoint}`, {
|
||||
method,
|
||||
headers: { clientid, 'Content-Language': 'zh_CN', Authorization: `Bearer ${token}` },
|
||||
signal: controller.signal
|
||||
});
|
||||
return {
|
||||
status: response.status,
|
||||
contentType: response.headers.get('content-type') || '',
|
||||
disposition: response.headers.get('content-disposition') || '',
|
||||
text: await response.text()
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
async function login() {
|
||||
if (process.env.AIHR_VERIFY_TOKEN) return process.env.AIHR_VERIFY_TOKEN;
|
||||
const codeResponse = await request('GET', '/auth/code');
|
||||
@@ -160,13 +185,79 @@ function evidenceTitles(result) {
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
const token = await login();
|
||||
if (pilotExport) {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(pilotStartDate) || !/^\d{4}-\d{2}-\d{2}$/.test(pilotEndDate)) {
|
||||
throw new Error('--pilot-export requires --start-date=YYYY-MM-DD and --end-date=YYYY-MM-DD');
|
||||
}
|
||||
const missingWindow = await requestText('GET', '/api/train/practice/export', token);
|
||||
let missingWindowData;
|
||||
try {
|
||||
missingWindowData = JSON.parse(missingWindow.text);
|
||||
} catch {
|
||||
missingWindowData = null;
|
||||
}
|
||||
if (missingWindow.status < 400 && (missingWindow.contentType.includes('text/csv') || missingWindowData?.code === 200)) {
|
||||
console.error('FAIL: pilot export accepted a request without an explicit date window');
|
||||
process.exit(1);
|
||||
}
|
||||
const endpoint = `/api/train/practice/export?startDate=${encodeURIComponent(pilotStartDate)}&endDate=${encodeURIComponent(pilotEndDate)}`;
|
||||
const response = await requestText('GET', endpoint, token);
|
||||
if (response.status !== 200 || !response.contentType.includes('text/csv')) {
|
||||
console.error(`FAIL: pilot export HTTP ${response.status}, content-type=${response.contentType}`);
|
||||
if (verbose) console.error(response.text.slice(0, 1000));
|
||||
process.exit(1);
|
||||
}
|
||||
let verifiedExport;
|
||||
try {
|
||||
verifiedExport = verifyPilotExportContract({
|
||||
csv: response.text,
|
||||
startDate: pilotStartDate,
|
||||
endDate: pilotEndDate,
|
||||
contentDisposition: response.disposition
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`FAIL: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const { metrics } = verifiedExport;
|
||||
console.log(
|
||||
`PILOT_EXPORT window=${pilotStartDate}..${pilotEndDate}`
|
||||
+ ` org_login=${metrics.get('formal_org_login_count')}`
|
||||
+ ` people=${metrics.get('formal_pilot_people_count')}`
|
||||
+ ` projects=${metrics.get('formal_pilot_project_count')}`
|
||||
+ ` ten_sessions=${metrics.get('completion_people_count')}/${metrics.get('completion_people_total')}`
|
||||
+ ` calibration=${metrics.get('calibration_sample_count')}`
|
||||
+ ` sop=${metrics.get('sop_review_sample_count')}`
|
||||
+ ` ready=${metrics.get('pilot_metric_ready')}`
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
if (orgDryRun) {
|
||||
const response = await request('POST', '/api/aihr/org/sync', {
|
||||
dryRun: true,
|
||||
replaceExisting: true,
|
||||
pageSize: 500,
|
||||
maxPages: 200
|
||||
}, token);
|
||||
const data = response.data?.data;
|
||||
if (response.status !== 200 || response.data?.code !== 200 || !data?.dryRun) {
|
||||
console.error(`FAIL: org dry-run failed: ${response.data?.msg || response.status}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`ORG_DRY_RUN employees=${data.employeeCount} phone_linked=${data.phoneLinked} masked_phone=${data.maskedPhone} suspect_text=${data.suspectText} synced=${data.syncedCount}`);
|
||||
for (const warning of data.warnings || []) {
|
||||
console.log(`WARN ${warning}`);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const questions = parseQuestions(fs.readFileSync(file, 'utf8'));
|
||||
if (!questions.length) {
|
||||
console.error(`FAIL: no questions found in ${file} for prefix ${prefixes.join(',')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const token = await login();
|
||||
const vector = await request('GET', '/api/knowledge/doc/vector-index-status', undefined, token);
|
||||
const vectorStatus = vector.data?.data;
|
||||
if (!vectorStatus?.matched) {
|
||||
|
||||
Reference in New Issue
Block a user