mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-27 16:19:50 +00:00
* feat(infra): add runtime package support for skills Install nodejs, npm, pandoc, github-cli + pre-install Python packages (openpyxl, pandas, python-pptx, markitdown) and Node packages (docx, pptxgenjs). Configure runtime dirs for agent pip/npm installs with PIP_TARGET, NPM_CONFIG_PREFIX, NODE_PATH to enable dynamic package installation in read-only container environment. * feat(infra): add bundled skills with runtime package support - Add 5 bundled skills: docx, pdf, pptx, xlsx, skill-creator from container skills-store - Wire GOCLAW_BUILTIN_SKILLS_DIR env var in gateway and CLI - Support optional runtime packages alongside dynamic skill loading - Update Dockerfile to COPY bundled-skills at /app/bundled-skills/ - Add PIP_CACHE_DIR in docker-entrypoint.sh for clean pip installs - Document bundled skills in 14-skills-runtime.md section 6 * feat(infra): remove ai-multimodal skill directory from bundled skills Remove the ai-multimodal skill package as part of consolidating runtime package support for bundled skills. This directory is no longer needed in the bundled skills structure. * feat(ci): add semantic release and Docker Hub publishing Add go-semantic-release workflow to auto-create semver tags on merge to main. Extend docker-publish to push all variants to both GHCR and Docker Hub (digitop/goclaw). * feat(skills): add system skills infrastructure with is_system column, dep scanning, and seeder - Migration 000017: add is_system boolean column with partial index - Store layer: UpsertSystemSkill, delete protection, IsSystemSkill - ListAccessible auto-includes system skills (no grants needed) - ListWithGrantStatus returns is_system field - Dependency scanner: auto-detect deps from scripts/ or skill-manifest.json - Dependency checker: verify system binaries, Python/Node packages - Seeder: seed bundled skills into DB on startup (idempotent via hash) - Gateway wiring: GOCLAW_BUNDLED_SKILLS_DIR env for bundled skills - HTTP: delete guard (403), slug conflict check (409), rescan-deps endpoint - UI: System badge, hide delete for system skills, rescan deps button - Agent skills tab: "Always available" for system skills - i18n: en/vi/zh keys for system skills, deps scanning * feat(skills): conditional system prompt, skill manifests, and Zip Slip fix - System prompt: only show package list when python3/node are available - Add skill-manifest.json for pdf, docx, xlsx, pptx bundled skills - Fix Zip Slip vulnerability in office/unpack.py (all 3 copies) * refactor(skills): extract shared office code to _shared/ and deduplicate Move office scripts (pack, unpack, validate, schemas, validators) from duplicated copies in docx/xlsx/pptx to skills/_shared/office/ with symlinks. Remove soffice.py (non-functional in containers) and update SKILL.md references to use soffice binary directly. Update seeder copyDir to follow symlinks. Removes ~45K lines of duplicate code across 3 skills. * fix(skills): address code review findings for system skills integration - H1: Remove dead symlink branch in copyDir (filepath.Walk follows symlinks) - H3: Fix rescan-deps to query ALL skills (including archived) and re-activate when deps become available; add ListAllSkills() + Status field to SkillInfo - H4: Add Status field to SkillCreateParams, stop overloading Visibility - M1: Batch Python/Node dep checks into single subprocess per runtime - M4: Add rows.Err() check in ListSkills to prevent caching partial results * feat(skills): async dep checking with realtime WS events Split Seed() into sync DB upsert + async CheckDepsAsync() goroutine. Gateway startup no longer blocks on Python/Node subprocess dep checks. - Seed() returns seeded skills list, all initially status="active" - CheckDepsAsync() runs in background, emits skill.deps.checked per-skill - skill.deps.complete event emitted when all checks finish - Each failed dep check: archives skill + BumpVersion() for immediate cache invalidation so next agent turn picks up the change - UI: use-query-invalidation listens to skill.deps.* events → auto-refresh skills list in realtime * feat(skills): system skills integration with toggle, dep checking, and per-item install - Add is_system, deps, enabled columns to skills table (migration 017) - Seed bundled core skills (pdf, docx, pptx, xlsx, skill-creator) on startup - PYTHONPATH-based dep detection — eliminates false positives from local modules - Per-item dep install UI with individual status (installing/success/error) - Enable/disable toggle for core and custom skills (independent of dep status) - Re-run dep check when skill is toggled back on - Inline skill thresholds: 40 skills / 5000 tokens before switching to search mode - Fix UpsertSystemSkill: backfill null file_hash without bumping DB version - Remove redundant skill-manifest.json files (replaced by deps JSONB column) - Show author from frontmatter in custom skills tab - Runtime checker for python3/pip3/node/npm availability - WS events for dep checking/installing progress - docs: add 15-core-skills-system.md, 16-skill-publishing.md --------- Co-authored-by: Goon <duy@wearetopgroup.com>
147 lines
6.9 KiB
HTML
147 lines
6.9 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Eval Set Review - __SKILL_NAME_PLACEHOLDER__</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@500;600&family=Lora:wght@400;500&display=swap" rel="stylesheet">
|
|
<style>
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body { font-family: 'Lora', Georgia, serif; background: #faf9f5; padding: 2rem; color: #141413; }
|
|
h1 { font-family: 'Poppins', sans-serif; margin-bottom: 0.5rem; font-size: 1.5rem; }
|
|
.description { color: #b0aea5; margin-bottom: 1.5rem; font-style: italic; max-width: 900px; }
|
|
.controls { margin-bottom: 1rem; display: flex; gap: 0.5rem; }
|
|
.btn { font-family: 'Poppins', sans-serif; padding: 0.5rem 1rem; border: none; border-radius: 6px; cursor: pointer; font-size: 0.875rem; font-weight: 500; }
|
|
.btn-add { background: #6a9bcc; color: white; }
|
|
.btn-add:hover { background: #5889b8; }
|
|
.btn-export { background: #d97757; color: white; }
|
|
.btn-export:hover { background: #c4613f; }
|
|
table { width: 100%; max-width: 1100px; border-collapse: collapse; background: white; border-radius: 6px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
|
|
th { font-family: 'Poppins', sans-serif; background: #141413; color: #faf9f5; padding: 0.75rem 1rem; text-align: left; font-size: 0.875rem; }
|
|
td { padding: 0.75rem 1rem; border-bottom: 1px solid #e8e6dc; vertical-align: top; }
|
|
tr:nth-child(even) td { background: #faf9f5; }
|
|
tr:hover td { background: #f3f1ea; }
|
|
.section-header td { background: #e8e6dc; font-family: 'Poppins', sans-serif; font-weight: 500; font-size: 0.8rem; color: #141413; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
.query-input { width: 100%; padding: 0.4rem; border: 1px solid #e8e6dc; border-radius: 4px; font-size: 0.875rem; font-family: 'Lora', Georgia, serif; resize: vertical; min-height: 60px; }
|
|
.query-input:focus { outline: none; border-color: #d97757; box-shadow: 0 0 0 2px rgba(217,119,87,0.15); }
|
|
.toggle { position: relative; display: inline-block; width: 44px; height: 24px; }
|
|
.toggle input { opacity: 0; width: 0; height: 0; }
|
|
.toggle .slider { position: absolute; inset: 0; background: #b0aea5; border-radius: 24px; cursor: pointer; transition: 0.2s; }
|
|
.toggle .slider::before { content: ""; position: absolute; width: 18px; height: 18px; left: 3px; bottom: 3px; background: white; border-radius: 50%; transition: 0.2s; }
|
|
.toggle input:checked + .slider { background: #d97757; }
|
|
.toggle input:checked + .slider::before { transform: translateX(20px); }
|
|
.btn-delete { background: #c44; color: white; padding: 0.3rem 0.6rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.75rem; font-family: 'Poppins', sans-serif; }
|
|
.btn-delete:hover { background: #a33; }
|
|
.summary { margin-top: 1rem; color: #b0aea5; font-size: 0.875rem; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Eval Set Review: <span id="skill-name">__SKILL_NAME_PLACEHOLDER__</span></h1>
|
|
<p class="description">Current description: <span id="skill-desc">__SKILL_DESCRIPTION_PLACEHOLDER__</span></p>
|
|
|
|
<div class="controls">
|
|
<button class="btn btn-add" onclick="addRow()">+ Add Query</button>
|
|
<button class="btn btn-export" onclick="exportEvalSet()">Export Eval Set</button>
|
|
</div>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th style="width:65%">Query</th>
|
|
<th style="width:18%">Should Trigger</th>
|
|
<th style="width:10%">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="eval-body"></tbody>
|
|
</table>
|
|
|
|
<p class="summary" id="summary"></p>
|
|
|
|
<script>
|
|
const EVAL_DATA = __EVAL_DATA_PLACEHOLDER__;
|
|
|
|
let evalItems = [...EVAL_DATA];
|
|
|
|
function render() {
|
|
const tbody = document.getElementById('eval-body');
|
|
tbody.innerHTML = '';
|
|
|
|
// Sort: should-trigger first, then should-not-trigger
|
|
const sorted = evalItems
|
|
.map((item, origIdx) => ({ ...item, origIdx }))
|
|
.sort((a, b) => (b.should_trigger ? 1 : 0) - (a.should_trigger ? 1 : 0));
|
|
|
|
let lastGroup = null;
|
|
sorted.forEach(item => {
|
|
const group = item.should_trigger ? 'trigger' : 'no-trigger';
|
|
if (group !== lastGroup) {
|
|
const headerRow = document.createElement('tr');
|
|
headerRow.className = 'section-header';
|
|
headerRow.innerHTML = `<td colspan="3">${item.should_trigger ? 'Should Trigger' : 'Should NOT Trigger'}</td>`;
|
|
tbody.appendChild(headerRow);
|
|
lastGroup = group;
|
|
}
|
|
|
|
const idx = item.origIdx;
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td><textarea class="query-input" onchange="updateQuery(${idx}, this.value)">${escapeHtml(item.query)}</textarea></td>
|
|
<td>
|
|
<label class="toggle">
|
|
<input type="checkbox" ${item.should_trigger ? 'checked' : ''} onchange="updateTrigger(${idx}, this.checked)">
|
|
<span class="slider"></span>
|
|
</label>
|
|
<span style="margin-left:8px;font-size:0.8rem;color:#b0aea5">${item.should_trigger ? 'Yes' : 'No'}</span>
|
|
</td>
|
|
<td><button class="btn-delete" onclick="deleteRow(${idx})">Delete</button></td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
});
|
|
updateSummary();
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function updateQuery(idx, value) { evalItems[idx].query = value; updateSummary(); }
|
|
function updateTrigger(idx, value) { evalItems[idx].should_trigger = value; render(); }
|
|
function deleteRow(idx) { evalItems.splice(idx, 1); render(); }
|
|
|
|
function addRow() {
|
|
evalItems.push({ query: '', should_trigger: true });
|
|
render();
|
|
const inputs = document.querySelectorAll('.query-input');
|
|
inputs[inputs.length - 1].focus();
|
|
}
|
|
|
|
function updateSummary() {
|
|
const trigger = evalItems.filter(i => i.should_trigger).length;
|
|
const noTrigger = evalItems.filter(i => !i.should_trigger).length;
|
|
document.getElementById('summary').textContent =
|
|
`${evalItems.length} queries total: ${trigger} should trigger, ${noTrigger} should not trigger`;
|
|
}
|
|
|
|
function exportEvalSet() {
|
|
const valid = evalItems.filter(i => i.query.trim() !== '');
|
|
const data = valid.map(i => ({ query: i.query.trim(), should_trigger: i.should_trigger }));
|
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'eval_set.json';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
render();
|
|
</script>
|
|
</body>
|
|
</html>
|