'use strict'; const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const MAX_LABEL = 120; const ID_RE = /^[A-Za-z0-9._:-]{1,120}$/; function cleanLabel(value, fallback = '') { const text = String(value == null ? '' : value).trim().replace(/[\r\n\t]+/g, ' '); return text.slice(0, MAX_LABEL) || fallback; } function newId(prefix = '') { const id = crypto.randomUUID(); return prefix ? `${cleanLabel(prefix).replace(/[^A-Za-z0-9_-]/g, '')}_${id}` : id; } function validId(value) { return ID_RE.test(String(value || '').trim()); } function createUsageContext(input = {}) { const usageTraceId = validId(input.usageTraceId) ? String(input.usageTraceId).trim() : newId('usage'); const reportId = input.reportId == null || input.reportId === '' ? null : (validId(input.reportId) ? String(input.reportId).trim() : newId('report')); return { usageTraceId, reportId, skillId: cleanLabel(input.skillId, 'voc-api-catalog'), workflowType: cleanLabel(input.workflowType, 'voc_custom_workflow'), title: cleanLabel(input.title || input.task), clientId: cleanLabel(input.clientId), stepId: cleanLabel(input.stepId), budgetLimitCny: Number.isFinite(Number(input.budgetLimitCny ?? input.budgetCny)) ? Math.max(0, Number(input.budgetLimitCny ?? input.budgetCny)) : null, pricingVersion: cleanLabel(input.pricingVersion), createdAt: input.createdAt || new Date().toISOString() }; } function createStepContext(context, stepId, extra = {}) { const base = createUsageContext({ ...context, stepId }); return { ...base, requestId: validId(extra.requestId) ? String(extra.requestId).trim() : newId('request'), ...(extra.entityId ? { entityId: cleanLabel(extra.entityId) } : {}), ...(extra.batchIndex !== undefined ? { batchIndex: Number(extra.batchIndex) || 0 } : {}) }; } function usageHeaders(context = {}) { const headers = {}; const values = { 'X-Fmode-Usage-Trace-Id': context.usageTraceId, 'X-Fmode-Report-Id': context.reportId, 'X-Fmode-Skill-Id': context.skillId, 'X-Fmode-Workflow-Type': context.workflowType, 'X-Fmode-Step-Id': context.stepId, 'X-Fmode-Client-Id': context.clientId, 'X-Fmode-Request-Id': context.requestId }; for (const [name, value] of Object.entries(values)) { if (value && validId(value)) headers[name] = String(value); } return headers; } function contextForTool(input = {}) { return createUsageContext({ ...input, budgetLimitCny: input.budgetLimitCny ?? input.budgetCny }); } function writeUsageContext(context, outputDir) { if (!outputDir) return null; const dir = path.resolve(String(outputDir)); fs.mkdirSync(dir, { recursive: true }); const filePath = path.join(dir, 'usage-context.json'); fs.writeFileSync(filePath, JSON.stringify(context, null, 2)); return filePath; } module.exports = { MAX_LABEL, cleanLabel, newId, validId, createUsageContext, createStepContext, contextForTool, usageHeaders, writeUsageContext };