usage-context.js 3.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. 'use strict';
  2. const crypto = require('crypto');
  3. const fs = require('fs');
  4. const path = require('path');
  5. const MAX_LABEL = 120;
  6. const ID_RE = /^[A-Za-z0-9._:-]{1,120}$/;
  7. function cleanLabel(value, fallback = '') {
  8. const text = String(value == null ? '' : value).trim().replace(/[\r\n\t]+/g, ' ');
  9. return text.slice(0, MAX_LABEL) || fallback;
  10. }
  11. function newId(prefix = '') {
  12. const id = crypto.randomUUID();
  13. return prefix ? `${cleanLabel(prefix).replace(/[^A-Za-z0-9_-]/g, '')}_${id}` : id;
  14. }
  15. function validId(value) {
  16. return ID_RE.test(String(value || '').trim());
  17. }
  18. function createUsageContext(input = {}) {
  19. const usageTraceId = validId(input.usageTraceId) ? String(input.usageTraceId).trim() : newId('usage');
  20. const reportId = input.reportId == null || input.reportId === ''
  21. ? null
  22. : (validId(input.reportId) ? String(input.reportId).trim() : newId('report'));
  23. return {
  24. usageTraceId,
  25. reportId,
  26. skillId: cleanLabel(input.skillId, 'voc-api-catalog'),
  27. workflowType: cleanLabel(input.workflowType, 'voc_custom_workflow'),
  28. title: cleanLabel(input.title || input.task),
  29. clientId: cleanLabel(input.clientId),
  30. stepId: cleanLabel(input.stepId),
  31. budgetLimitCny: Number.isFinite(Number(input.budgetLimitCny ?? input.budgetCny))
  32. ? Math.max(0, Number(input.budgetLimitCny ?? input.budgetCny))
  33. : null,
  34. pricingVersion: cleanLabel(input.pricingVersion),
  35. createdAt: input.createdAt || new Date().toISOString()
  36. };
  37. }
  38. function createStepContext(context, stepId, extra = {}) {
  39. const base = createUsageContext({ ...context, stepId });
  40. return {
  41. ...base,
  42. requestId: validId(extra.requestId) ? String(extra.requestId).trim() : newId('request'),
  43. ...(extra.entityId ? { entityId: cleanLabel(extra.entityId) } : {}),
  44. ...(extra.batchIndex !== undefined ? { batchIndex: Number(extra.batchIndex) || 0 } : {})
  45. };
  46. }
  47. function usageHeaders(context = {}) {
  48. const headers = {};
  49. const values = {
  50. 'X-Fmode-Usage-Trace-Id': context.usageTraceId,
  51. 'X-Fmode-Report-Id': context.reportId,
  52. 'X-Fmode-Skill-Id': context.skillId,
  53. 'X-Fmode-Workflow-Type': context.workflowType,
  54. 'X-Fmode-Step-Id': context.stepId,
  55. 'X-Fmode-Client-Id': context.clientId,
  56. 'X-Fmode-Request-Id': context.requestId
  57. };
  58. for (const [name, value] of Object.entries(values)) {
  59. if (value && validId(value)) headers[name] = String(value);
  60. }
  61. return headers;
  62. }
  63. function contextForTool(input = {}) {
  64. return createUsageContext({
  65. ...input,
  66. budgetLimitCny: input.budgetLimitCny ?? input.budgetCny
  67. });
  68. }
  69. function writeUsageContext(context, outputDir) {
  70. if (!outputDir) return null;
  71. const dir = path.resolve(String(outputDir));
  72. fs.mkdirSync(dir, { recursive: true });
  73. const filePath = path.join(dir, 'usage-context.json');
  74. fs.writeFileSync(filePath, JSON.stringify(context, null, 2));
  75. return filePath;
  76. }
  77. module.exports = {
  78. MAX_LABEL,
  79. cleanLabel,
  80. newId,
  81. validId,
  82. createUsageContext,
  83. createStepContext,
  84. contextForTool,
  85. usageHeaders,
  86. writeUsageContext
  87. };