| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161 |
- import fs from 'node:fs';
- import path from 'node:path';
- import crypto from 'node:crypto';
- import { fileURLToPath, pathToFileURL } from 'node:url';
- const skillDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
- const runtimeDir = path.resolve(process.env.KSF_ADAPT_DIR || '/opt/data/ksf-adapt-training-20261031');
- const r7Dir = path.resolve(process.env.KSF_R7_SKILL_DIR || '/opt/data/ksf-baseline/r7/unpacked/skill');
- const hash = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
- const base = {
- skill: 'ksf-workshop-runtime', invocationId: crypto.randomUUID(), at: new Date().toISOString(),
- entry: fileURLToPath(import.meta.url), entrySha256: hash(fs.readFileSync(fileURLToPath(import.meta.url))),
- };
- const fail = (code, message, details) => { throw Object.assign(new Error(message), { code, details }); };
- const object = value => value && typeof value === 'object' && !Array.isArray(value);
- function argsOf(argv) {
- const [command, ...rest] = argv;
- if (!['status', 'demo', 'connection', 'ingest', 'report-preview', 'report-submit'].includes(command)) {
- fail('USAGE', '用法:ksf.mjs status|demo|connection|ingest|report-preview|report-submit [--input 文件] [--request-id 消息标记]');
- }
- const args = { command };
- for (let i = 0; i < rest.length; i += 2) {
- const key = rest[i];
- if (!['--input', '--request-id'].includes(key) || !rest[i + 1] || rest[i + 1].startsWith('--') || args[key] != null) {
- fail('USAGE', '参数名称、参数值或重复参数不正确');
- }
- args[key] = rest[i + 1];
- }
- if (args['--request-id'] && !/^[\p{L}\p{N}_:@.-]{1,100}$/u.test(args['--request-id'])) fail('USAGE', '消息标记只接受100字以内的文字、数字和 _ : @ . -');
- if (['ingest', 'report-preview', 'report-submit'].includes(command) !== Boolean(args['--input'])) fail('USAGE', '写入命令必须有 --input;只读命令不接受 --input');
- return args;
- }
- function verifyTree(root, entries) {
- return Object.entries(entries).map(([file, expected]) => {
- const target = path.resolve(root, file);
- if (!target.startsWith(root + path.sep)) fail('BAD_LOCK', '指纹清单包含越界路径');
- let actual = null;
- try { actual = hash(fs.readFileSync(target)); } catch { /* 缺失与不符都阻止加载 */ }
- return { file, ok: actual === expected, ...(actual === expected ? {} : { expected, actual }) };
- });
- }
- function verifyInstallation() {
- const lock = JSON.parse(fs.readFileSync(path.join(skillDir, 'runtime-lock.json'), 'utf8'));
- const runtime = verifyTree(runtimeDir, lock.runtime), r7 = verifyTree(r7Dir, lock.r7);
- const errors = [...runtime.filter(x => !x.ok).map(x => ({ tree: 'runtime', ...x })),
- ...r7.filter(x => !x.ok).map(x => ({ tree: 'r7', ...x }))];
- return { verified: !errors.length, runtimeDir, r7Dir,
- runtimeFiles: { passed: runtime.filter(x => x.ok).length, total: runtime.length },
- r7Files: { passed: r7.filter(x => x.ok).length, total: r7.length }, errors };
- }
- function requireService() {
- if (!process.env.KSF_SERVICE_TOKEN?.trim()) fail('MISSING_SERVICE_TOKEN', '未配置 KSF_SERVICE_TOKEN;请在命令进程环境中注入,勿发送到聊天');
- }
- function readTask(file, allowed) {
- let task;
- try { task = JSON.parse(fs.readFileSync(path.resolve(file), 'utf8')); }
- catch { fail('INVALID_INPUT', '输入文件无法读取或不是有效 JSON'); }
- if (!object(task)) fail('INVALID_INPUT', '任务根节点必须是对象');
- if (Object.keys(task).some(key => !allowed.includes(key))) fail('INVALID_INPUT', '任务包含本命令不接受的根字段,请按技能中的输入约定提供');
- return task;
- }
- async function moduleAt(file) {
- return import(pathToFileURL(path.join(runtimeDir, file)).href);
- }
- async function reportTask(file, command) {
- const task = readTask(file, ['reportDir', 'seriesKey', 'version', 'reportType', 'dataMode', 'stageCode', 'artifactSubtype', 'previewId']);
- const codes = await moduleAt('client/contract-constants.mjs');
- if (typeof task.reportDir !== 'string' || !path.isAbsolute(task.reportDir)
- || typeof task.seriesKey !== 'string' || !task.seriesKey.trim()
- || !Number.isInteger(task.version) || task.version < 1
- || !codes.REPORT_TYPES.includes(task.reportType) || !codes.DATA_MODES.includes(task.dataMode)
- || !codes.STAGE_CODES.includes(task.stageCode)
- || (task.reportType === codes.AUXILIARY_TYPE && !task.artifactSubtype)) {
- fail('INVALID_REPORT', '报告目录、系列键、版本、报告类型、数据模式、阶段或附属类型不完整/不合法');
- }
- if (!fs.existsSync(path.join(task.reportDir, 'report.html'))) fail('MISSING_REPORT', '报告目录中没有 report.html');
- if (command === 'report-submit' && task.dataMode === 'sample') fail('SAMPLE_DISPLAY_ONLY', '本轮培训 sample 报告只作展示,不能提交到正式看板;不得更改数据标签绕过');
- if (command === 'report-submit' && (typeof task.previewId !== 'string' || !task.previewId)) fail('MISSING_PREVIEW', '须提供服务端返回的 previewId,且由员工在确认页完成确认');
- requireService();
- const bridge = await moduleAt('client/report-bridge.mjs');
- if (command === 'report-preview') {
- const preview = await bridge.previewReport(task);
- return { ok: true, operation: 'preview-created', employeeActionRequired: true,
- previewId: preview.previewId, confirmationUrl: preview.confirmationUrl,
- reportIdentity: preview.reportIdentity, dataMode: task.dataMode, ingested: false, published: false };
- }
- const result = await bridge.submitConfirmedReport({ ...task, skillDir: r7Dir });
- return { ok: !result.fatal, operation: 'report-submit', steps: result.steps,
- jobId: result.artifacts?.jobId, artifactId: result.artifacts?.artifactId,
- cardId: result.artifacts?.cardId, guard: result.guard,
- ingested: result.steps?.import === 'ok', published: result.steps?.publish === 'ok',
- ...(result.fatal ? { error: { code: result.fatal.code, step: result.fatal.step, message: result.fatal.reason } } : {}) };
- }
- async function run(args) {
- const installation = verifyInstallation();
- if (args.command === 'status') return {
- ok: installation.verified, readOnly: true, networkRequests: 0, installation,
- serviceTokenConfigured: Boolean(process.env.KSF_SERVICE_TOKEN?.trim()),
- connectionVerified: false, note: '本次仅检查本地安装;凭据是否有效需另跑 connection',
- };
- if (!installation.verified) fail('INSTALLATION_MISMATCH', '文件缺失或指纹不符,未加载连接模块、未发出网络请求', installation);
- if (args.command === 'demo') {
- const metadata = JSON.parse(fs.readFileSync(path.join(runtimeDir, 'training-demo/报告样例/metadata.json'), 'utf8'));
- return { ok: true, readOnly: true, networkRequests: 0, dataMode: 'sample',
- report: { localFile: path.join(runtimeDir, 'training-demo/报告样例/report.html'),
- url: 'https://s3.fmode.cn/user/DpPsnlBm0K/ksf-training-20261031/report-demo/index.html',
- sha256: metadata.reportContentHash, sizeBytes: metadata.sizeBytes,
- preparedBeforeConversation: true, hasRealResearchEvidence: false, publishedToDashboard: false },
- dashboard: ['https://kangshifu.fmode.cn/', 'https://kangshifu.fmode.cn/pool',
- 'https://kangshifu.fmode.cn/stages', 'https://kangshifu.fmode.cn/report'],
- linksCheckedThisRun: false, note: '这是预先准备的样例;链接来自既有培训配置,本命令未访问网页或核查现有看板记录' };
- }
- if (args.command.startsWith('report-')) return reportTask(args['--input'], args.command);
- requireService();
- if (args.command === 'connection') {
- const { createGuardedClient } = await moduleAt('client/guarded-client.mjs');
- const contract = await createGuardedClient({ sessionToken: '' }).invoke('ksfGetIngestContract', {});
- if (!Array.isArray(contract?.allowedClasses) || !contract.allowedClasses.length) fail('CONTRACT_MISSING', '未读到受限身份合同');
- return { ok: true, readOnly: true, identity: 'restricted-service', schemaVersion: contract.schemaVersion,
- release: contract.release, allowedClassCount: contract.allowedClasses.length,
- employeeAccountsProvisioned: contract.capabilities?.employeeAccountsProvisioned };
- }
- const task = readTask(args['--input'], ['requestKey', 'source', 'runKey', 'run', 'records', 'producer']);
- const { ingestCollectedRecords } = await moduleAt('client/collection-bridge.mjs');
- const result = await ingestCollectedRecords(task);
- return { ok: true, operation: 'collection-ingested', sourceId: result.sourceId, runId: result.runId,
- jobId: result.jobId, status: result.status, counts: result.counts,
- items: result.items.map(({ itemKey, status, objectId }) => ({ itemKey, status, objectId })), published: false };
- }
- function redact(value) {
- let text = JSON.stringify(value, null, 2);
- for (const key of ['KSF_SERVICE_TOKEN', 'KSF_PARSE_SESSION_TOKEN', 'KSF_PARSE_MASTER_KEY']) {
- const secret = process.env[key];
- if (secret) text = text.split(JSON.stringify(secret).slice(1, -1)).join('[已隐藏]');
- }
- return text;
- }
- try {
- const args = argsOf(process.argv.slice(2));
- base.command = args.command;
- if (args['--request-id']) base.requestId = args['--request-id'];
- const result = await run(args);
- console.log(redact({ ...base, ...result }));
- if (!result.ok) process.exitCode = 1;
- } catch (error) {
- console.error(redact({ ...base, ok: false, error: {
- code: typeof error.code === 'string' ? error.code : 'EXECUTION_FAILED',
- message: error.message || '执行失败', ...(error.details ? { details: error.details } : {}),
- } }));
- process.exitCode = 1;
- }
|