Communitygithub.com

xiaowu89/skill-compress

伺服器端圖片壓縮技能,支援 CDN 輸出,可批次處理與單檔案模式

skill-compress 是什麼?

這是一個遠端圖片壓縮的 AI Agent 技能(SKILL),專為 Claude Code 等開發者工具設計。它能透過 nx-mcp-server 將圖片以 Base64 編碼傳送至遠端壓縮服務,獲得壓縮後的 CDN 網址。支援批次壓縮整個目錄或單一檔案,且內建 5MB 大小限制與並發池控制(預設同時處理 5 張),壓縮品質可設定(預設 90)。此技能完全在記憶體中運作,不寫入任何暫存檔案,由單一 heredoc 節點腳本完成初始化、通知、工具呼叫三步驟。返回字段包含原始大小、壓縮後大小、壓縮率及 CDN 網址。若使用者需要,可以透過 curl 下載結果,無需依賴本機 sharp 等套件。支援錯誤代碼速查,如遺漏 apiKey、API Key 無效、未發送 notifications/initialized、缺少 Accept 標頭等常見問題。

相容平台✓Claude Code~Codex CLI✓Cursor
npx skills add xiaowu89/skill-compress

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

圖片壓縮

透過 nx-mcp-server 遠端壓縮服務對圖片進行智慧壓縮。

配置

在專案根目錄建立 .mcp.json:

{
  "mcpServers": {
    "nx-mcp-compress": {
      "type": "url",
      "url": "https://mcp.api-inference.modelscope.net/4378d43d3e7d4c/mcp",
      "env": {
        "NX_API_KEY": "你的 API Key"
      }
    }
  }
}

查找順序:專案根目錄 → 使用者家目錄。提取 url → MCP_URL,env.NX_API_KEY → API_KEY。

Skill 直連 MCP 端點,無需重啟 Claude Code。

沒有 API Key? 聯絡微信 zhjian_2026 取得。

壓縮流程

步驟 1:檢查配置

cat .mcp.json 2>/dev/null || cat ~/.mcp.json 2>/dev/null
  • 找到 → 記錄 url 和 NX_API_KEY,繼續步驟 2
  • 找不到 → 詢問使用者是否已有 API Key:
    • 有 Key:幫使用者建立 ~/.mcp.json,全域和專案安裝都通用
    • 沒有 Key:告知聯絡微信 zhjian_2026 取得

⚠️ 配置缺失時不要繼續後續步驟。

步驟 2:執行壓縮(一次 Bash 呼叫,純記憶體,零檔案)

替換 PIC_DIR、MCP_URL、API_KEY、QUALITY 後,heredoc 直接透過 stdin 餵給 node:

NODE_PATH=$(npm root -g) node << 'COMPRESSEOF'
const fs=require('fs'),path=require('path');
let PIC_DIR='<目標圖片目錄絕對路徑>';
const SINGLE_FILE='<單張圖片路徑,為空則壓縮整個目錄>';
const MCP_URL='<從.mcp.json讀取的url>';
const API_KEY='<從.mcp.json讀取的NX_API_KEY>';
const QUALITY=90;
(async()=>{
const exts=['.png','.jpg','.jpeg','.webp','.bmp','.tga'];
let imgs;
if(SINGLE_FILE){imgs=[path.basename(SINGLE_FILE)];PIC_DIR=path.dirname(SINGLE_FILE)}
else{imgs=fs.readdirSync(PIC_DIR).filter(f=>exts.includes(path.extname(f).toLowerCase())).sort()}
const origTotal=imgs.reduce((s,f)=>s+fs.statSync(path.join(PIC_DIR,f)).size,0);
console.log(`共 ${imgs.length} 張,總 ${(origTotal/1024).toFixed(0)}KB`);
const maxSize=5*1024*1024;
const records=[];let skip=0;
for(let i=0;i<imgs.length;i++){const f=imgs[i],fp=path.join(PIC_DIR,f),osz=fs.statSync(fp).size;
if(osz>maxSize){records.push({name:f,origKb:osz,compKb:0,compUrl:null,ratio:null,error:'超過5MB限制'});skip++;console.log(`  [${i+1}/${imgs.length}] ${f} ${(osz/1024).toFixed(0)}KB ⚠️ 超過5MB跳過`);continue}
const buf=fs.readFileSync(fp);const ext=path.extname(f).toLowerCase().slice(1);
const mime=ext==='png'?'image/png':ext==='webp'?'image/webp':'image/jpeg';
records.push({name:f,origKb:osz,compKb:0,compUrl:null,ratio:null,dataUrl:'data:'+mime+';base64,'+buf.toString('base64')});}
console.log(`可壓縮: ${records.length-skip} 張, 跳過: ${skip} 張`);
console.time('壓縮');
const H={'Content-Type':'application/json','Accept':'application/json, text/event-stream'};
const r1=await fetch(MCP_URL,{method:'POST',headers:H,body:JSON.stringify({jsonrpc:'2.0',id:'1',method:'initialize',params:{protocolVersion:'2025-06-18',capabilities:{},clientInfo:{name:'cc',version:'1'}}})});
H['Mcp-Session-Id']=r1.headers.get('Mcp-Session-Id');console.log('MCP: init');
await fetch(MCP_URL,{method:'POST',headers:H,body:JSON.stringify({jsonrpc:'2.0',method:'notifications/initialized'})});console.log('MCP: notified');
// 並發池:始終保持 LIMIT 個請求在飛,完成一個補一個
	const LIMIT=5;
	let totalSaved=0,pass=0,fail=0,done=0;
	const pending=records.filter(r=>!r.error);
	
	async function compressOne(r,idx){
	  try{
	    const r3=await fetch(MCP_URL,{method:'POST',headers:H,body:JSON.stringify({jsonrpc:'2.0',id:'3',method:'tools/call',params:{name:'nx_compress',arguments:{files:[r.dataUrl],quality:QUALITY,apiKey:API_KEY}}})});
	    const raw=await r3.json();
	    if(!raw.result){r.error='MCP: '+(raw.error?.message||'unknown');fail++}
	    else{
	      const inner=JSON.parse(raw.result.content[0].text);
	      if(inner.error){r.error=inner.code+': '+inner.error;fail++}
	      else{const it=inner.items[0];
	        if(it.error){r.error=it.error;fail++}
	        else{r.compKb=it.compressedSize/1024;r.ratio=it.ratio;r.compUrl=it.compressedUrl;totalSaved+=it.originalSize-it.compressedSize;pass++;console.log(`  [${idx+1}/${imgs.length}] ${r.name} ${(r.origKb/1024).toFixed(0)}KB→${(r.compKb).toFixed(0)}KB (${r.ratio})`)}
	      }
	    }
	  }catch(e){r.error=e.message;fail++}
	  done++;
	}
	
	// 滑動視窗:始終保持 LIMIT 個並發
	let i=0;
	const running=new Set();
	while(i<pending.length||running.size>0){
	  while(running.size<LIMIT&&i<pending.length){
	    const p=compressOne(pending[i],i).then(()=>running.delete(p));
	    running.add(p);i++;
	  }
	  if(running.size>0) await Promise.race(running);
	}console.timeEnd('壓縮');
console.log('\n'+'='.repeat(100));
console.log(`${'檔案'.padEnd(40)} ${'原始'.padStart(8)} ${'壓縮後'.padStart(8)} ${'壓縮率'.padStart(8)} ${'結果'.padStart(6)}`);
console.log('-'.repeat(100));
for(const r of records){const oszS=(r.origKb/1024).toFixed(1)+'KB';
if(r.compUrl){const cszS=(r.compKb).toFixed(1)+'KB';console.log(`${r.name.padEnd(40)} ${oszS.padStart(8)} ${cszS.padStart(8)} ${(r.ratio||'?').padStart(8)} ${'✅'.padStart(6)} ${r.compUrl}`)}
else{console.log(`${r.name.padEnd(40)} ${oszS.padStart(8)} ${'—'.padStart(8)} ${'—'.padStart(8)} ${'❌'.padStart(6)}`)}}
console.log(`\n📊 ${records.length} 張 | ✅ ${pass} 成功 | ❌ ${fail} 失敗 | 共節省 ${(totalSaved/1024).toFixed(0)}KB`);

})();
COMPRESSEOF

壓縮參數:quality 預設 90(1-100),無需本機 sharp,直傳服務端壓縮。

步驟 3:下載到本地(如使用者需要)

不要重新執行壓縮!壓縮結果中的 CDN URL 已在控制台輸出,直接用 curl 下載:

curl -o <輸出目錄>/<檔案名稱> "<CDN地址>"
  • ✅ 成功:壓縮後的 CDN URL 可直接使用
  • ⚠️ 超 5MB:檔案超過服務端限制,需手動處理
  • ❌ 失敗:重試一次

返回欄位速查

欄位類型說明
originalSizenumber原始大小(bytes)
compressedSizenumber壓縮後大小(bytes)
ratiostring壓縮率,如 "71.5%"
compressedUrlstringCDN 地址

常見錯誤速查

錯誤現象原因正確做法
MISSING_API_KEY沒傳 apiKey必須傳
API_AUTH_FAILEDAPI Key 無效檢查 .mcp.json 中的 Key
-32602 Invalid request parameters未發送 notifications/initialized必須三步:init → notified → call
406 Not Acceptable缺少 Accept 頭同時宣告 application/json 和 text/event-stream
檔案超過 5MB 被跳過服務端限制提示使用者手動處理

禁止事項

  • ❌ 不要本地壓縮(服務端智慧壓縮,本地 sharp 多餘)
  • ❌ 不要傳 output 參數(不存在)
  • ❌ 不要省略 apiKey 參數
  • ❌ 不要省略 notifications/initialized 步驟
  • ❌ 不要寫任何暫存檔案(heredoc 直接 stdin,純記憶體執行)
  • ❌ 不要將流程拆成多次 Bash 呼叫(一次 node << 'COMPRESSEOF' 搞定)
  • ❌ 不要使用反斜線路徑,一律用正斜線(d:/path)

相關技能