← 返回
AI智能 Key

NextCloud AIO OpenClaw

Reliable Nextcloud integration for Notes, Tasks, Calendar, Files, and Contacts (CardDAV-safe vCard handling with robust contact create/update).
可靠的 Nextcloud 集成,支持笔记、任务、日历、文件和联系人(CardDAV 安全的 vCard 处理,稳健的联系人创建与更新)。
juicyroots
AI智能 clawhub v1.1.2 2 版本 100000 Key: 需要
★ 0
Stars
📥 419
下载
💾 5
安装
2
版本
#latest

概述

NextCloud AIO OpenClaw Skill

This skill connects OpenClaw to Nextcloud for:

  • Notes API
  • CalDAV Tasks and Events
  • WebDAV Files
  • CardDAV Contacts

Primary goal: Predictable, loss-resistant Nextcloud reads/writes.

This skill is validated against:

  • Nextcloud Hub 25 Autumn (32.0.6)
  • OpenClaw (2026.3.2)

Required Configuration

Set these environment variables:

  • NEXTCLOUD_URL (example: https://cloud.example.com)
  • NEXTCLOUD_USER
  • NEXTCLOUD_TOKEN (App Password strongly preferred)
  • NEXTCLOUD_TIMEZONE (recommended, IANA timezone like America/Los_Angeles; used for timezone-less date/time inputs)

Runtime and Metadata Contract

This section is the source of truth for registry metadata parity.

  • Required binary: node (Node.js 20+)
  • Required environment variables: NEXTCLOUD_URL, NEXTCLOUD_USER, NEXTCLOUD_TOKEN
  • Optional environment variable: NEXTCLOUD_TIMEZONE
  • Required network target: the host defined by NEXTCLOUD_URL (plus normal redirects from that host)

If a registry entry says "no required binaries" or "no required env vars", treat it as stale metadata and fix before publishing.

Preflight Audit Gate

Before first execution in any new environment:

  • Complete the bundled script review checklist below.
  • Confirm no unexpected endpoints or privileged system calls are present.
  • If checks are incomplete, do not run the skill.
  • Prefer sandbox/container validation and least-privilege app credentials first.

Security and Publish Safety

  • Never hardcode credentials in SKILL.md, scripts, examples, commit messages, or changelog text.
  • Store secrets only in environment variables or local secret stores.
  • Use dedicated app passwords for integrations; rotate if leaked or shared.
  • Keep local .env files out of git (see .gitignore).
  • Before publishing, run a quick scan for obvious secrets and remove any accidental values.

Bundled Script Review Checklist (Before First Use)

The skill ships two scripts: scripts/nextcloud.js (Node.js, text operations) and scripts/files_binary.py (Python 3, binary file transfers). Review both before running in new environments:

  1. Confirm runtime requirements:
    • node --version (must be 20+)
  2. Check obvious outbound endpoints:
    • rg -n "https?://" scripts/nextcloud.js
  3. Check sensitive capability usage:
    • rg -n "child_process|spawn\\(|exec\\(|require\\(\"fs\"\\)|require\\('fs'\\)|fs\\." scripts/nextcloud.js
  4. Verify credentials are env-driven only:
    • rg -n "NEXTCLOUD_URL|NEXTCLOUD_USER|NEXTCLOUD_TOKEN|NEXTCLOUD_TIMEZONE" scripts/nextcloud.js
  5. Run in low-risk mode first:
    • Use a dedicated low-privilege Nextcloud app password
    • Test against non-production data when possible

If review confidence is low, do not run until code is reviewed by a trusted maintainer.

Static audit snapshot (current bundle)

Quick grep-style checks on scripts/nextcloud.js should currently show:

  • Config is read from process.env.NEXTCLOUD_URL|USER|TOKEN|TIMEZONE.
  • Request path is built as ${CONFIG.url}${endpoint} and uses fetch(...).
  • No obvious child_process or fs usage in the bundle entry path.

Quick checks on scripts/files_binary.py:

  • Config is read from os.environ.get("NEXTCLOUD_URL|USER|TOKEN").
  • DAV URL is built as {url}/remote.php/dav/files/{user}/{path}.
  • Uses only stdlib (urllib, base64, xml.etree); no third-party deps.

This is not a full security audit. Re-run checks after every bundle update.

Run

node scripts/nextcloud.js <command> <subcommand> [options]

Commands

Notes

  • notes list
  • notes get --id
  • notes create --title --content <content> [--category <category>]</code></li><li><code>notes edit --id <id> [--title <title>] [--content <content>] [--category <category>]</code></li><li><code>notes delete --id <id></code></li></ul><h3>Tasks</h3><ul><li><code>tasks list [--calendar <calendar-name>]</code></li><li><code>tasks create --title <title> [--calendar <calendar-name>] [--due <iso>] [--priority <0-9>] [--description <text>] [--timezone <IANA>]</code></li><li><code>tasks edit --uid <uid> [--calendar <calendar-name>] [--title <title>] [--due <iso>] [--priority <0-9>] [--description <text>] [--timezone <IANA>]</code></li><li><code>tasks delete --uid <uid> [--calendar <calendar-name>]</code></li><li><code>tasks complete --uid <uid> [--calendar <calendar-name>]</code></li></ul><h3>Calendar Events</h3><ul><li><code>calendar list [--from <iso>] [--to <iso>]</code> (default: next 7 days)</li><li><code>calendar create --summary <summary> --start <iso-or-date> --end <iso-or-date> [--calendar <calendar-name>] [--description <text>] [--timezone <IANA>] [--all-day]</code></li><li><code>calendar edit --uid <uid> [--calendar <calendar-name>] [--summary <summary>] [--start <iso-or-date>] [--end <iso-or-date>] [--description <text>] [--timezone <IANA>] [--all-day]</code></li><li><code>calendar delete --uid <uid> [--calendar <calendar-name>]</code></li></ul><p>Important:</p><ul><li>In many setups, <code>Contact birthdays</code> appears as an events calendar but is read-only.</li><li>Birthdays must be managed through <code>contacts create/edit --birthday ...</code>, not through <code>calendar create/edit</code>.</li><li><code>calendar list</code> returns event <code>summary</code>, <code>start</code>, <code>end</code>, and may include <code>location</code> and <code>description</code> when present.</li></ul><h3>Calendar Discovery</h3><ul><li><code>calendars list [--type <tasks|events>]</code></li></ul><h3>Files (text)</h3><ul><li><code>files list [--path <path>]</code></li><li><code>files search --query <query></code> (supports natural-language input; ranks by filename/path token relevance)</li><li><code>files get --path <path></code> (accepts relative user path or full DAV href)</li><li><code>files upload --path <path> --content <content></code></li><li><code>files delete --path <path></code></li></ul><h3>Files (binary) — ODT, DOCX, PDF, images, etc.</h3><p><code>nextcloud.js</code> passes file content as text strings, which corrupts binary formats.</p><p>Use the companion Python script for any binary file:</p><pre><code>python3 scripts/files_binary.py download <nc_path> <local_path> python3 scripts/files_binary.py upload <local_path> <nc_path> python3 scripts/files_binary.py exists <nc_path> python3 scripts/files_binary.py list [<nc_path>] </code></pre><p>Reads the same <code>NEXTCLOUD_URL</code>, <code>NEXTCLOUD_USER</code>, <code>NEXTCLOUD_TOKEN</code> env vars.</p><p>Auto-detects MIME type from file extension (ODT, DOCX, XLSX, PDF, PNG, JPG, and more).</p><p><strong>When to use which:</strong></p><table><thead><tr><th>Situation</th><th>Command</th></tr></thead><tbody><tr><td>-----------</td><td>---------</td></tr><tr><td>Plain text files (<code>.md</code>, <code>.txt</code>, <code>.csv</code>)</td><td><code>node scripts/nextcloud.js files get/upload</code></td></tr><tr><td>Binary files (<code>.odt</code>, <code>.docx</code>, <code>.pdf</code>, images)</td><td><code>python3 scripts/files_binary.py download/upload</code></td></tr></tbody></table><h3>Contacts</h3><ul><li><code>contacts list [--addressbook <name>]</code></li><li><code>contacts get --uid <uid> [--addressbook <name>]</code></li><li><code>contacts search --query <query> [--addressbook <name>]</code></li><li><code>contacts create [--name <full-name>] [--first-name <given>] [--last-name <family>] [--middle-name <middle>] [--prefix <prefix>] [--suffix <suffix>] [--addressbook <name>] [--email <single>] [--emails <csv>] [--phone <single>] [--phones <csv>] [--organization <org>] [--title <title>] [--note <note>] [--birthday <YYYY-MM-DD|YYYYMMDD|--MM-DD|--MMDD>]</code></li><li><code>contacts edit --uid <uid> [--addressbook <name>] [--name <full-name>] [--first-name <given>] [--last-name <family>] [--middle-name <middle>] [--prefix <prefix>] [--suffix <suffix>] [--email <single>] [--emails <csv>] [--phone <single>] [--phones <csv>] [--organization <org>] [--title <title>] [--note <note>] [--birthday <YYYY-MM-DD|YYYYMMDD|--MM-DD|--MMDD>]</code></li><li><code>contacts delete --uid <uid> [--addressbook <name>]</code></li></ul><h3>Address Book Discovery</h3><ul><li><code>addressbooks list</code></li></ul><h2>Contact Data Contract (Important)</h2><p>Use <code>fullName</code> and <code>structuredName</code> as canonical name fields.</p><p>Contact output includes:</p><ul><li><code>uid</code></li><li><code>addressBook</code></li><li><code>fullName</code></li><li><code>structuredName</code> with:</li><li><code>familyName</code></li><li><code>givenName</code></li><li><code>additionalNames</code></li><li><code>honorificPrefixes</code></li><li><code>honorificSuffixes</code></li><li><code>nameRaw</code> (raw <code>N</code> value for diagnostics only; do not display to users by default)</li><li><code>emails</code> array or <code>null</code></li><li><code>phones</code> array or <code>null</code></li><li><code>organization</code>, <code>title</code>, <code>note</code></li><li><code>birthday</code> normalized for readability (<code>YYYY-MM-DD</code> or <code>--MM-DD</code> when possible)</li><li><code>birthdayRaw</code> (raw CardDAV value)</li></ul><h2>Reliability Rules (Quick Reference)</h2><p>Apply these on every write to prevent corruption and confusion:</p><p>1) Identity and naming</p><ul><li>Never infer <code>UID</code> from URL; always use payload <code>uid</code>.</li><li>Use <code>fullName</code> and <code>structuredName</code> for user-facing names; keep <code>nameRaw</code> diagnostic-only.</li></ul><p>2) Contact and birthday integrity</p><ul><li>Birthday input formats: <code>YYYY-MM-DD</code>, <code>YYYYMMDD</code>, <code>--MM-DD</code>, <code>--MMDD</code>.</li><li>Never add trailing semicolons to <code>BDAY</code>.</li><li>Birthdays are contact fields: use <code>contacts create/edit --birthday ...</code>; never edit birthday-derived calendar entries directly.</li></ul><p>3) Intent-preserving updates</p><ul><li>Modify only fields explicitly requested by the user.</li><li>To clear a value, pass explicit empty values.</li><li>Respect explicit <code>--addressbook</code>; otherwise use remembered default or ask once.</li></ul><p>4) Calendar and task validation</p><ul><li>Validate date/datetime inputs before sending; require <code>end > start</code> for timed events.</li><li>Escape ICS text fields (<code>SUMMARY</code>, <code>DESCRIPTION</code>).</li><li>Validate task priority range (<code>0..9</code>).</li><li>For timezone-less timed input, use <code>--timezone</code> or <code>NEXTCLOUD_TIMEZONE</code>.</li><li>For all-day events, use <code>VALUE=DATE</code> semantics with exclusive <code>DTEND</code>.</li></ul><p>5) Capability and safety checks</p><ul><li>Do not assume listed event calendars are writable.</li><li>Treat <code>Contact birthdays</code> as read-only/system calendar.</li><li>If no writable event calendar exists, explain event mutation is unavailable until writable access exists.</li><li>Some calendars are multi-component (<code>VEVENT</code> + <code>VTODO</code>); keep them eligible for both event/task flows.</li></ul><p>6) Files and post-write verification</p><ul><li>Require non-empty file paths for <code>files upload/get/delete</code>.</li><li>Escape XML filter/search values for CardDAV/WebDAV operations.</li><li>After mutations (<code>create</code>, <code>edit</code>, <code>complete</code>, <code>delete</code>), verify with follow-up read/list when practical.</li><li>For propagation delay, retry around <code>0.5s</code>, <code>1s</code>, <code>2s</code> before declaring failure.</li></ul><h2>Known Nextcloud/CardDAV Behaviors</h2><ul><li>Servers/clients may negotiate or normalize vCard versions (<code>3.0</code> vs <code>4.0</code>) and content on write/read.</li><li><code>FN</code> and <code>N</code> should each appear once in valid contacts.</li><li><code>BDAY</code> failures are often malformed input (for example stray delimiters), not server instability.</li><li>Folded vCard lines must be unfolded before parsing.</li></ul><h2>Error to Action Map (Fast)</h2><ul><li><code>HTTP 403</code> on <code>calendar create/edit/delete</code> -> likely read-only/system calendar or missing permission; switch to writable events calendar.</li><li><code>HTTP 501</code> on <code>files search</code> -> WebDAV <code>SEARCH</code> unsupported; fallback to recursive <code>PROPFIND</code> + client-side filter.</li><li><code>HTTP 415</code> on task/event update -> malformed ICS payload; normalize formatting and retry once.</li><li>Contact write succeeded but UI still stale -> run verification retry cycle before reporting failure.</li></ul><h2>Agent Behavior: Default Calendar Selection</h2><p>When user creates tasks/events without explicit calendar:</p><ol><li>Run <code>calendars list --type tasks</code> or <code>calendars list --type events</code>.</li><li>For <code>events</code>, filter out likely read-only calendars first (<code>Contact birthdays</code>, <code>Holidays</code>, names containing <code>read only</code> or <code>readonly</code>).</li><li>If at least one writable calendar remains, ask for selection.</li><li>Ask whether to remember as default.</li><li>Store in memory.</li><li>If no writable events calendar remains, explain why event mutation is unavailable and suggest creating a writable calendar in Nextcloud Calendar.</li></ol><p>Memory keys:</p><ul><li><code>default_task_calendar</code></li><li><code>default_event_calendar</code></li></ul><h2>Agent Behavior: Default Address Book Selection</h2><p>When user creates contacts without explicit address book:</p><ol><li>Run <code>addressbooks list</code>.</li><li>Ask for selection.</li><li>Ask whether to remember default.</li><li>Store in memory.</li></ol><p>Memory key:</p><ul><li><code>default_addressbook</code></li></ul><h2>Agent Behavior Playbooks (Condensed)</h2><p>These playbooks are execution shortcuts. Reliability and safety rules above still apply.</p><h3>Contacts and Birthdays</h3><ol><li>Resolve address book (<code>--addressbook</code> or remembered default).</li><li>For create/edit, pass only user-requested fields; use explicit empty values when user wants fields cleared.</li><li>Use <code>--name</code> for full-name input; use structured-name flags for split-name input.</li><li>Pass multiple emails/phones as CSV values.</li><li>Birthday changes always go through contacts (<code>contacts create/edit --birthday ...</code>), never calendar mutation commands.</li><li>Verify birthday/name-sensitive writes with <code>contacts get --uid ...</code> when confidence is important.</li></ol><h3>Tasks</h3><ol><li>Resolve task calendar when omitted.</li><li>Validate due and priority (<code>0..9</code>) before write.</li><li>After create/edit/complete/delete, verify via focused list/get when practical.</li></ol><h3>Calendar Events</h3><ol><li>Resolve writable events calendar; treat <code>Contact birthdays</code> and system calendars as read-only.</li><li>Validate <code>start</code>/<code>end</code> and require <code>end > start</code> for timed events.</li><li>For timezone-less date-times, pass <code>--timezone</code> or use <code>NEXTCLOUD_TIMEZONE</code>.</li><li>For all-day intent, use <code>--all-day</code> with date values (not timed midnight ranges).</li><li>Re-list in a focused window after mutation when confidence is important.</li></ol><p>All-day end-date normalization behavior:</p><ul><li><code>start=YYYY-MM-DD</code>, <code>end=YYYY-MM-DD</code> -> inclusive single-day input, normalized.</li><li><code>start=YYYY-MM-DD</code>, <code>end=next-day</code> -> already-exclusive single-day input.</li><li>Multi-day inclusive ranges -> normalized to canonical exclusive <code>DTEND</code>.</li></ul><h3>Appointment briefing workflow (high priority intent)</h3><p>Use this when user asks for appointment details, address, or "what is my appointment today":</p><ol><li>Determine "today" in <code>NEXTCLOUD_TIMEZONE</code> (or ask if timezone is unclear).</li><li>Run <code>calendar list --from <start-of-day-iso> --to <end-of-day-iso></code>.</li><li>Filter events by user terms (for example <code>dermatology</code>, <code>doctor</code>, provider name).</li><li>For the selected event, include <code>summary</code>, time window, <code>location</code>, and key lines from <code>description</code>.</li><li>If multiple events match, ask a quick disambiguation question.</li><li>If none match, clearly say none found for today and offer to check tomorrow or full week.</li><li>Provide a plain-text message suitable for SMS/chat (short lines, no markdown).</li></ol><h3>Files</h3><ol><li>Require explicit file path for mutating operations.</li><li><code>files get/upload/delete</code> accept either a relative user path (for example <code>/Share-Family/file.docx</code>) or a full DAV href returned by search.</li><li>For binary files (ODT, DOCX, PDF, images), use <code>python3 scripts/files_binary.py</code> — never pass binary content through <code>nextcloud.js files upload</code>.</li><li>Use deterministic test paths for temporary validation files.</li><li>Read back uploaded content and then delete test files.</li><li>Use <code>files search</code> with user phrasing directly; it auto-normalizes query text and ranks likely matches.</li><li>On large instances, prefer narrower path-based listing before broad search to reduce load.</li></ol><h3>Notes</h3><ol><li>Create with explicit title/content.</li><li>Read back by id after create/edit.</li><li>Delete temporary test notes after verification.</li></ol><h2>Internal Smoke Test Protocol</h2><p>When validating this skill in a live environment:</p><ol><li>Contacts: create -> get -> edit -> get -> optional delete.</li><li>Notes: create -> get -> edit -> get -> delete.</li><li>Files: upload -> get -> search/list -> delete.</li><li>Tasks: create -> list/locate -> edit -> complete -> delete.</li><li>Calendar: create -> list/locate -> edit -> delete.</li></ol><p>Use a unique timestamped suffix on all test data and clean up all temporary artifacts unless the user requests keeping them.</p><p>Use least-privilege credentials for tests and monitor outbound traffic; the process should only communicate with <code>NEXTCLOUD_URL</code>.</p><p>Calendar exception handling:</p><ul><li>If no writable event calendar exists, mark calendar mutation tests as intentionally skipped (environment limitation), and still run <code>calendar list</code>.</li><li>Do not treat this as a skill failure; treat it as a server capability/permission constraint.</li></ul><h2>Persistent Rules (Store These)</h2><p>Persist these high-value rules in memory/rules:</p><ol><li>Birthdays are contact fields: use <code>contacts create/edit --birthday ...</code>; never mutate <code>Contact birthdays</code> directly.</li><li>For all-day events, always use <code>calendar create/edit --all-day</code> with date values (avoid timed midnight ranges).</li><li>For timezone-less timed values, use <code>--timezone</code> or <code>NEXTCLOUD_TIMEZONE</code>; ask if timezone is unknown.</li><li>Prefer writable user calendars for event mutations; if <code>403</code>, report likely read-only/permission issue.</li><li>Verify event/contact writes with a focused read/list before declaring final success.</li><li>Keep canonical all-day encoding (<code>VALUE=DATE</code>); avoid duplicate compatibility events unless explicitly requested.</li></ol><h2>Troubleshooting Playbook</h2><ul><li>Birthday was updated but not visible in Nextcloud UI yet:</li><li>Run <code>contacts get --uid ...</code> immediately.</li><li>If value is correct in API, advise short wait + UI refresh.</li><li>Re-check after retry intervals before escalating.</li></ul><ul><li><code>contacts get</code> and <code>contacts search</code> disagree briefly:</li><li>Prefer retry cycle and then trust latest consistent result.</li><li>Do not create duplicate contact entries while waiting.</li></ul><ul><li>Event creation keeps failing:</li><li>Run <code>calendars list --type events</code> and verify at least one writable non-system calendar exists.</li><li>If only birthday/system calendars exist, explain limitation and stop mutation attempts.</li></ul><h2>Output Envelope</h2><p>All commands return JSON:</p><p>Success:</p><pre><code>{ "status": "success", "data": {} } </code></pre><p>Error:</p><pre><code>{ "status": "error", "message": "Error description" } </code></pre></div> </div> </div> <div id="tab-versions" class="detail-content"> <div class="detail-section"> <h2>版本历史</h2> <p style="margin-bottom:12px;font-size:14px;color:#94a3b8;">共 2 个版本</p> <ul class="version-list"> <li> <div> <span class="version-tag">v1.1.2</span> <span style="font-size:11px;color:#5b6abf;margin-left:8px;background:#eef0ff;padding:1px 8px;border-radius:10px;">当前</span> </div> <div style="font-size:12px;color:#94a3b8;"> 2026-05-07 04:16 安全 安全 </div> </li> <li> <div> <span class="version-tag">v1.1.1</span> </div> <div style="font-size:12px;color:#94a3b8;"> 2026-03-11 11:53 </div> </li> </ul> </div> </div> <div id="tab-security" class="detail-content"> <div class="detail-section"> <h2>安全检测</h2> <div class="sec-grid"> <div class="sec-card"> <h4>腾讯云安全 (Keen)</h4> <div class="sec-status sec-safe"> 安全,无风险 </div> <a href="https://tix.qq.com/search/skill?keyword=285de4dc7a684607da616b951df8cf10" target="_blank">查看报告</a> </div> <div class="sec-card"> <h4>腾讯云安全 (Sanbu)</h4> <div class="sec-status sec-safe"> 安全,无风险 </div> <a href="https://static.cloudsec.tencent.com/html-report-v2/2026/05/25/404543_89b59346ce4cdcd23e9fc77a360fb3e2.html?q-sign-algorithm=sha1&q-ak=AKID8JMG1bzBC1dz96qNhssfFftujT1NCoFi&q-sign-time=1781294507%3B1812830507&q-key-time=1781294507%3B1812830507&q-header-list=host&q-url-param-list=&q-signature=4b04a34f763ba29035ca31abd8a65f8d6ba7a5e9" target="_blank">查看报告</a> </div> </div> </div> </div> <!-- Recommended Skills --> <div style="margin-top:24px;"> <h2 style="font-size:18px;font-weight:600;margin-bottom:16px;">🔗 相关推荐</h2> <div class="rec-grid"> <div class="rec-card"> <span class="badge-cat" style="margin-bottom:8px;display:inline-block;">ai-intelligence</span> <h3><a href="/s/self-improving-agent">self-improving agent</a></h3> <div class="rec-owner">pskoett</div> <div class="rec-desc">捕获经验教训、错误和纠正,以实现持续改进。使用时机:(1)命令或操作意外失败;(2)用户纠正……</div> <div class="rec-stats"> <span style="color:#f39c12;">★ 4,056</span> <span style="color:#5b6abf;">📥 796,390</span> </div> </div> <div class="rec-card"> <span class="badge-cat" style="margin-bottom:8px;display:inline-block;">ai-intelligence</span> <h3><a href="/s/self-improving">Self-Improving + Proactive Agent</a></h3> <div class="rec-owner">ivangdavila</div> <div class="rec-desc">自我反思+自我批评+自我学习+自组织记忆。智能体评估自身工作、发现错误并持续改进。</div> <div class="rec-stats"> <span style="color:#f39c12;">★ 1,350</span> <span style="color:#5b6abf;">📥 317,745</span> </div> </div> <div class="rec-card"> <span class="badge-cat" style="margin-bottom:8px;display:inline-block;">content-creation</span> <h3><a href="/s/odt-filemgr-oc">ODT File Manager & Editor</a></h3> <div class="rec-owner">juicyroots</div> <div class="rec-desc">使用 Python 和 odfdo 库在本地创建、解析和编辑 ODT(OpenDocument Text)文件,适用于用户要求创建、编辑、读取、更新、追加、检查...</div> <div class="rec-stats"> <span style="color:#f39c12;">★ 0</span> <span style="color:#5b6abf;">📥 522</span> </div> </div> </div> </div> </div> <script> document.addEventListener('DOMContentLoaded',function(){ document.querySelectorAll('.detail-tab').forEach(function(btn){ btn.addEventListener('click',function(e){ var tab = this.getAttribute('data-tab'); document.querySelectorAll('.detail-tab').forEach(function(b){b.classList.remove('active')}); document.querySelectorAll('.detail-content').forEach(function(c){c.classList.remove('active')}); this.classList.add('active'); var el = document.getElementById('tab-'+tab); if(el) el.classList.add('active'); }); }); }); </script> <div class="footer"> <p>Skill工具集 © 2026</p> </div></body> </html>