#!/usr/bin/env node import crypto from 'node:crypto'; import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { verifyKnowledgePlatform } from './verify-knowledge-platform.mjs'; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const baseUrl = process.env.AIHR_BASE_URL || 'https://wygj-api.localhost'; const frontendEnv = readEnv(path.join(root, 'frontend/.env.development')); const adminClientId = frontendEnv.VITE_APP_CLIENT_ID; const requestPublicKey = pem('PUBLIC KEY', frontendEnv.VITE_APP_RSA_PUBLIC_KEY); const responsePrivateKey = pem('PRIVATE KEY', frontendEnv.VITE_APP_RSA_PRIVATE_KEY); if (new URL(baseUrl).hostname.endsWith('.localhost')) { process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; } function readEnv(filePath) { const values = {}; for (const line of fs.readFileSync(filePath, 'utf8').split(/\r?\n/)) { const match = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.+?)\s*$/); if (match) values[match[1]] = match[2].replace(/^['"]|['"]$/g, ''); } return values; } function pem(label, body) { return `-----BEGIN ${label}-----\n${body.match(/.{1,64}/g).join('\n')}\n-----END ${label}-----`; } function encryptPayload(payload) { const keyText = crypto.randomBytes(24).toString('base64').slice(0, 32); const cipher = crypto.createCipheriv('aes-256-ecb', Buffer.from(keyText, 'utf8'), null); cipher.setAutoPadding(true); const body = Buffer.concat([cipher.update(JSON.stringify(payload), 'utf8'), cipher.final()]).toString('base64'); const encryptedKey = crypto.publicEncrypt( { key: requestPublicKey, padding: crypto.constants.RSA_PKCS1_PADDING }, Buffer.from(Buffer.from(keyText, 'utf8').toString('base64'), 'utf8') ).toString('base64'); return { body, encryptedKey }; } function decryptResponse(text, encryptedKey) { if (!encryptedKey) return JSON.parse(text); const keyBase64 = crypto.privateDecrypt( { key: responsePrivateKey, padding: crypto.constants.RSA_PKCS1_PADDING }, Buffer.from(encryptedKey, 'base64') ).toString('utf8'); const decipher = crypto.createDecipheriv('aes-256-ecb', Buffer.from(keyBase64, 'base64'), null); decipher.setAutoPadding(true); return JSON.parse(Buffer.concat([decipher.update(Buffer.from(text, 'base64')), decipher.final()]).toString('utf8')); } async function request(method, endpoint, body, { encrypted = false, clientId = adminClientId } = {}) { const headers = { clientid: clientId, 'Content-Language': 'zh_CN' }; let payload; if (body !== undefined) { headers['Content-Type'] = 'application/json;charset=utf-8'; if (encrypted) { const value = encryptPayload(body); headers['encrypt-key'] = value.encryptedKey; payload = value.body; } else { payload = JSON.stringify(body); } } const response = await fetch(`${baseUrl}${endpoint}`, { method, headers, body: payload, signal: AbortSignal.timeout(20_000) }); const text = await response.text(); return encrypted ? decryptResponse(text, response.headers.get('encrypt-key')) : JSON.parse(text); } function docker(container, command) { return execFileSync('docker', ['exec', container, ...command], { encoding: 'utf8' }).trim(); } function mysql(sql) { return docker('wygj-mysql', ['mysql', '-uroot', '-proot', '--default-character-set=utf8mb4', 'ry-vue', '-Nse', sql]); } async function adminLogin() { const captcha = await request('GET', '/auth/code'); const uuid = captcha.data?.uuid; if (!uuid) throw new Error('本地管理员登录未返回验证码UUID'); const redis = docker('wygj-redis', ['redis-cli', '-a', 'ruoyi123', '--raw', 'get', `global:captcha_codes:${uuid}`]); const code = redis.split('\n').pop().replace(/^"|"$/g, ''); const response = await request('POST', '/auth/login', { tenantId: '000000', username: process.env.AIHR_VERIFY_USER || 'admin', password: process.env.AIHR_VERIFY_PASSWORD || 'admin123', rememberMe: false, code, uuid, clientId: adminClientId, grantType: 'password' }, { encrypted: true }); if (!response.data?.access_token) throw new Error(`本地管理员登录失败: ${response.msg || response.code}`); return response.data.access_token; } async function mobileLogin(phonenumber) { const mobileClientId = '428a8310cd442757ae699df5d894f051'; const sent = await request('GET', `/resource/sms/code?phonenumber=${encodeURIComponent(phonenumber)}`, undefined, { clientId: mobileClientId }); if (Number(sent.code) !== 200) throw new Error(`本地手机号 ${phonenumber} 验证码准备失败: ${sent.msg || sent.code}`); const response = await request('POST', '/auth/mobile/sms-login', { phonenumber, smsCode: process.env.AIHR_VERIFY_SMS_CODE || '123456', tenantId: '000000' }, { clientId: mobileClientId }); if (!response.data?.access_token) throw new Error(`本地手机号 ${phonenumber} 登录失败: ${response.msg || response.code}`); return response.data.access_token; } async function prepareSharedDocumentFixture(adminToken, fileName, marker) { const form = new FormData(); form.append('file', new Blob([`${marker}\n员工应遵守基本制度,包括信息保密、考勤和服务规范。\n报修受理后第一步应登记业主信息、报修事项和联系方式并生成工单。\n该文件仅用于验证同一OSS在两个知识空间中的独立成员关系。\n`], { type: 'text/plain;charset=utf-8' }), fileName); form.append('spaceCodes', 'yc_public_policy'); form.append('spaceCodes', 'yc_property_sop'); const response = await fetch(`${baseUrl}/api/knowledge/doc/upload`, { method: 'POST', headers: { Authorization: `Bearer ${adminToken}`, clientid: adminClientId }, body: form, signal: AbortSignal.timeout(60_000) }); const payload = await response.json(); if (Number(payload.code) !== 200) throw new Error(`双空间测试文档准备失败: ${payload.msg || payload.code}`); console.log('PASS 双空间测试文档已准备'); } function installTemporaryMeituToken() { const token = `ak_mt_card_miniapp_${crypto.randomBytes(32).toString('base64url')}`; const tokenHash = crypto.createHash('sha256').update(token).digest('hex'); mysql(`update aihr_knowledge_app set token_hash='${tokenHash}', status='ACTIVE', rate_limit_per_minute=10, expires_time=now()+interval 1 day where tenant_id='100001' and app_code='mt_card_miniapp' and auth_type='API_TOKEN'`); return token; } function disableTemporaryMeituToken() { mysql("update aihr_knowledge_app set token_hash=null, status='DISABLED', rate_limit_per_minute=60, expires_time=null where tenant_id='100001' and app_code='mt_card_miniapp'"); } function clearLocalAppRateLimit() { const output = docker('wygj-redis', ['redis-cli', '-a', 'ruoyi123', '--scan', '--pattern', '*knowledge:app:*']); for (const key of output.split('\n').map((value) => value.trim()).filter(Boolean)) { docker('wygj-redis', ['redis-cli', '-a', 'ruoyi123', 'del', key]); } } function clearLocalMobileAuthFixture() { const output = docker('wygj-redis', ['redis-cli', '-a', 'ruoyi123', '--scan', '--pattern', '*1390000000*']); for (const key of output.split('\n').map((value) => value.trim()).filter(Boolean)) { docker('wygj-redis', ['redis-cli', '-a', 'ruoyi123', 'del', key]); } } async function expectExternalRejected(token, label) { const response = await fetch(`${baseUrl}/api/open/knowledge/query`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ queryText: '认证状态验证', spaceCodes: ['mt_customer_service'] }), signal: AbortSignal.timeout(20_000) }); const payload = await response.json(); if (Number(payload.code) !== 401) throw new Error(`${label}: 期望业务码401,实际${payload.code}`); console.log(`PASS ${label}`); } async function main() { const externalToken = installTemporaryMeituToken(); clearLocalAppRateLimit(); clearLocalMobileAuthFixture(); try { const [adminToken, employeeToken, supervisorToken] = await Promise.all([ adminLogin(), mobileLogin('13900000001'), mobileLogin('13900000002') ]); const sharedDocumentName = process.env.AIHR_SHARED_DOC_NAME || 'knowledge-platform-shared-verification.txt'; const sharedDocumentQuery = process.env.AIHR_SHARED_DOC_QUERY || '知识空间共享验证标记'; if (process.env.AIHR_VERIFY_UNBIND === 'true') { await prepareSharedDocumentFixture(adminToken, sharedDocumentName, sharedDocumentQuery); } const results = await verifyKnowledgePlatform({ AIHR_BASE_URL: baseUrl, AIHR_SILVER_ADMIN_TOKEN: adminToken, AIHR_SILVER_EMPLOYEE_TOKEN: employeeToken, AIHR_SILVER_SUPERVISOR_TOKEN: supervisorToken, AIHR_MEITU_APP_TOKEN: externalToken, AIHR_SHARED_DOC_QUERY: sharedDocumentQuery, AIHR_MEITU_QUERY: process.env.AIHR_MEITU_QUERY || 'MEITU_VERIFY_20260716', AIHR_MEITU_RATE_LIMIT: 10, AIHR_VERIFY_RATE_LIMIT: true, AIHR_VERIFY_UNBIND: process.env.AIHR_VERIFY_UNBIND === 'true', AIHR_SHARED_DOC_NAME: sharedDocumentName, AIHR_VERIFY_TIMEOUT_MS: 20_000 }); mysql("update aihr_knowledge_app set status='DISABLED' where tenant_id='100001' and app_code='mt_card_miniapp'"); await expectExternalRejected(externalToken, '停用应用令牌被拒绝'); mysql("update aihr_knowledge_app set status='ACTIVE', expires_time=now()-interval 1 minute where tenant_id='100001' and app_code='mt_card_miniapp'"); await expectExternalRejected(externalToken, '过期应用令牌被拒绝'); console.log(`LOCAL_HTTP_RESULT ${results.length + 2}/${results.length + 2}`); } finally { disableTemporaryMeituToken(); } } main().catch((error) => { console.error(`本地知识平台验证失败: ${error.message}`); process.exitCode = 1; });