文件预览

skillPublisher.js

查看 Evolver 技能包中的文件内容。

文件内容

src/gep/skillPublisher.js

'use strict';

var { getHubUrl, buildHubHeaders, getNodeId } = require('./a2aProtocol');

/**
 * Sanitize a raw gene id into a human-readable kebab-case skill name.
 * Returns null if the name is unsalvageable (pure numbers, tool name, etc.).
 */
function sanitizeSkillName(rawName) {
  var name = rawName.replace(/[\r\n]+/g, '-').replace(/^gene_distilled_/, '').replace(/^gene_/, '').replace(/_/g, '-');
  // Strip ALL embedded timestamps (10+ digit sequences) anywhere in the name
  name = name.replace(/-?\d{10,}-?/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
  if (/^\d{8,}/.test(name) || /^(cursor|vscode|vim|emacs|windsurf|copilot|cline|codex)[-]?\d*$/i.test(name)) {
    return null;
  }
  if (name.replace(/[-]/g, '').length < 6) return null;
  return name;
}

/**
 * Derive a Title Case display name from a kebab-case skill name.
 * "retry-with-backoff" -> "Retry With Backoff"
 */
function toTitleCase(kebabName) {
  return kebabName.split('-').map(function (w) {
    if (!w) return '';
    return w.charAt(0).toUpperCase() + w.slice(1);
  }).join(' ');
}

/**
 * Derive fallback name words from gene signals/summary when id is not usable.
 */
function deriveFallbackName(gene) {
  var fallbackWords = [];
  var STOP = new Set(['the', 'and', 'for', 'with', 'from', 'that', 'this', 'into', 'when', 'are', 'was', 'has', 'had', 'not', 'but', 'its']);
  if (Array.isArray(gene.signals_match)) {
    gene.signals_match.slice(0, 3).forEach(function (s) {
      String(s).toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim().split(/\s+/).forEach(function (w) {
        if (w.length >= 3 && !STOP.has(w) && fallbackWords.length < 5) fallbackWords.push(w);
      });
    });
  }
  if (fallbackWords.length < 2 && gene.summary) {
    String(gene.summary).toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim().split(/\s+/).forEach(function (w) {
      if (w.length >= 3 && !STOP.has(w) && fallbackWords.length < 5) fallbackWords.push(w);
    });
  }
  var seen = {};
  fallbackWords = fallbackWords.filter(function (w) { if (seen[w]) return false; seen[w] = true; return true; });
  return fallbackWords.length >= 2 ? fallbackWords.join('-') : 'auto-distilled-skill';
}

/**
 * Convert a Gene object into SKILL.md format -- marketplace-quality content.
 *
 * @param {object} gene - Gene asset
 * @returns {string} SKILL.md content
 */
function geneToSkillMd(gene) {
  var rawName = gene.id || 'unnamed-skill';
  var name = sanitizeSkillName(rawName) || deriveFallbackName(gene);
  var displayName = toTitleCase(name);
  var desc = (gene.summary || '').replace(/[\r\n]+/g, ' ').replace(/\s*\d{10,}\s*$/g, '').trim();
  if (!desc || desc.length < 10) desc = 'AI agent skill distilled from evolution experience.';

  var lines = [
    '---',
    'name: ' + displayName,
    'description: ' + desc,
    '---',
    '',
    '# ' + displayName,
    '',
    desc,
    '',
  ];

  // -- When to Use (derived from signals; preconditions go in their own section) --
  if (gene.signals_match && gene.signals_match.length > 0) {
    lines.push('## When to Use');
    lines.push('');
    lines.push('- When your project encounters: ' + gene.signals_match.slice(0, 4).map(function (s) {
      return '`' + s + '`';
    }).join(', '));
    lines.push('');
  }

  // -- Trigger Signals --
  if (gene.signals_match && gene.signals_match.length > 0) {
    lines.push('## Trigger Signals');
    lines.push('');
    gene.signals_match.forEach(function (s) {
      lines.push('- `' + s + '`');
    });
    lines.push('');
  }

  // -- Preconditions --
  if (gene.preconditions && gene.preconditions.length > 0) {
    lines.push('## Preconditions');
    lines.push('');
    gene.preconditions.forEach(function (p) {
      lines.push('- ' + p);
    });
    lines.push('');
  }

  // -- Strategy --
  if (gene.strategy && gene.strategy.length > 0) {
    lines.push('## Strategy');
    lines.push('');
    gene.strategy.forEach(function (step, i) {
      var text = String(step);
      var verb = extractStepVerb(text);
      if (verb) {
        lines.push((i + 1) + '. **' + verb + '** -- ' + stripLeadingVerb(text));
      } else {
        lines.push((i + 1) + '. ' + text);
      }
    });
    lines.push('');
  }

  // -- Constraints --
  if (gene.constraints) {
    lines.push('## Constraints');
    lines.push('');
    if (gene.constraints.max_files) {
      lines.push('- Max files per invocation: ' + gene.constraints.max_files);
    }
    if (gene.constraints.forbidden_paths && gene.constraints.forbidden_paths.length > 0) {
      lines.push('- Forbidden paths: ' + gene.constraints.forbidden_paths.map(function (p) { return '`' + p + '`'; }).join(', '));
    }
    lines.push('');
  }

  // -- Validation --
  if (gene.validation && gene.validation.length > 0) {
    lines.push('## Validation');
    lines.push('');
    gene.validation.forEach(function (cmd) {
      lines.push('```bash');
      lines.push(cmd);
      lines.push('```');
      lines.push('');
    });
  }

  // -- Metadata --
  lines.push('## Metadata');
  lines.push('');
  lines.push('- Category: `' + (gene.category || 'innovate') + '`');
  lines.push('- Schema version: `' + (gene.schema_version || '1.6.0') + '`');
  if (gene._distilled_meta && gene._distilled_meta.source_capsule_count) {
    lines.push('- Distilled from: ' + gene._distilled_meta.source_capsule_count + ' successful capsules');
  }
  lines.push('');

  lines.push('---');
  lines.push('');
  lines.push('*This Skill was generated by [Evolver](https://github.com/autogame-17/evolver) and is distributed under the [EvoMap Skill License (ESL-1.0)](https://evomap.ai/terms). Unauthorized redistribution, bulk scraping, or republishing is prohibited. See LICENSE file for full terms.*');
  lines.push('');

  return lines.join('\n');
}

/**
 * Extract the leading verb from a strategy step for bolding.
 * Only extracts a single verb to avoid splitting compound phrases.
 * e.g. "Verify Cursor CLI installation" -> "Verify"
 *      "Run `npm test` to check" -> "Run"
 *      "Configure non-interactive mode" -> "Configure"
 */
function extractStepVerb(step) {
  // Only match a capitalized verb at the very start (no leading backtick/special chars)
  var match = step.match(/^([A-Z][a-z]+)/);
  return match ? match[1] : '';
}

/**
 * Remove the leading verb from a step (already shown in bold).
 */
function stripLeadingVerb(step) {
  var verb = extractStepVerb(step);
  if (verb && step.startsWith(verb)) {
    var rest = step.slice(verb.length).replace(/^[\s:.\-]+/, '');
    return rest || step;
  }
  return step;
}

/**
 * Publish a Gene as a Skill to the Hub skill store.
 *
 * @param {object} gene - Gene asset
 * @param {object} [opts] - { category, tags }
 * @returns {Promise<{ok: boolean, result?: object, error?: string}>}
 */
function publishSkillToHub(gene, opts) {
  opts = opts || {};
  var hubUrl = getHubUrl();
  if (!hubUrl) return Promise.resolve({ ok: false, error: 'no_hub_url' });

  // Shallow-copy gene to avoid mutating the caller's object
  var geneCopy = {};
  Object.keys(gene).forEach(function (k) { geneCopy[k] = gene[k]; });
  if (Array.isArray(geneCopy.signals_match)) {
    try {
      var distiller = require('./skillDistiller');
      geneCopy.signals_match = distiller.sanitizeSignalsMatch(geneCopy.signals_match);
    } catch (e) { /* distiller not available, skip */ }
  }

  var content = geneToSkillMd(geneCopy);
  var nodeId = getNodeId();
  var fmName = content.match(/^name:\s*(.+)$/m);
  var derivedName = fmName ? fmName[1].trim().toLowerCase().replace(/[^a-z0-9]+/g, '_') : (gene.id || 'unnamed').replace(/^gene_/, '');
  // Strip ALL embedded timestamps from skillId
  derivedName = derivedName.replace(/_?\d{10,}_?/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');
  var skillId = 'skill_' + derivedName;

  // Clean tags: use already-sanitized signals from geneCopy
  var tags = opts.tags || geneCopy.signals_match || [];
  tags = tags.filter(function (t) {
    var s = String(t || '').trim();
    return s.length >= 3 && !/^\d+$/.test(s) && !/\d{10,}/.test(s);
  });

  var body = {
    sender_id: nodeId,
    skill_id: skillId,
    content: content,
    category: opts.category || geneCopy.category || null,
    tags: tags,
  };

  var endpoint = hubUrl.replace(/\/+$/, '') + '/a2a/skill/store/publish';

  return fetch(endpoint, {
    method: 'POST',
    headers: buildHubHeaders(),
    body: JSON.stringify(body),
    signal: AbortSignal.timeout(15000),
  })
    .then(function (res) { return res.json().then(function (data) { return { status: res.status, data: data }; }); })
    .then(function (result) {
      if (result.status === 201 || result.status === 200) {
        return { ok: true, result: result.data };
      }
      if (result.status === 409) {
        return updateSkillOnHub(nodeId, skillId, content, opts, gene);
      }
      return { ok: false, error: result.data?.error || 'publish_failed', status: result.status };
    })
    .catch(function (err) {
      return { ok: false, error: err.message };
    });
}

/**
 * Update an existing Skill on the Hub (new version).
 */
function updateSkillOnHub(nodeId, skillId, content, opts, gene) {
  var hubUrl = getHubUrl();
  if (!hubUrl) return Promise.resolve({ ok: false, error: 'no_hub_url' });

  var tags = opts.tags || gene.signals_match || [];
  tags = tags.filter(function (t) {
    var s = String(t || '').trim();
    return s.length >= 3 && !/^\d+$/.test(s) && !/\d{10,}/.test(s);
  });

  var body = {
    sender_id: nodeId,
    skill_id: skillId,
    content: content,
    category: opts.category || gene.category || null,
    tags: tags,
    changelog: 'Iterative evolution update',
  };

  var endpoint = hubUrl.replace(/\/+$/, '') + '/a2a/skill/store/update';

  return fetch(endpoint, {
    method: 'PUT',
    headers: buildHubHeaders(),
    body: JSON.stringify(body),
    signal: AbortSignal.timeout(15000),
  })
    .then(function (res) { return res.json().then(function (data) { return { status: res.status, data: data }; }); })
    .then(function (result) {
      if (result.status >= 200 && result.status < 300) {
        return { ok: true, result: result.data };
      }
      return { ok: false, error: result.data?.error || 'update_failed', status: result.status };
    })
    .catch(function (err) { return { ok: false, error: err.message }; });
}

module.exports = {
  geneToSkillMd: geneToSkillMd,
  publishSkillToHub: publishSkillToHub,
  updateSkillOnHub: updateSkillOnHub,
  sanitizeSkillName: sanitizeSkillName,
  toTitleCase: toTitleCase,
};