Communitygithub.com

xiaowu89/skill-function

AI 驅動的圖片內容審核工具 — 可偵測色情、政治及暴力內容

skill-function 是什麼?

這是一個專為 AI 代理(如 Claude Code)設計的圖片內容審核技能,基於 nx-mcp-audit MCP 服務,能自動掃描圖片中的色情、政治及暴力等違規內容。它支援常見圖片格式(PNG、JPG、WebP、BMP、TGA),並可選擇審核單張圖片或整個目錄。技能透過 `.mcp.json` 配置檔連接 MCP 端點,無需重啟 Claude Code。工作流程包含:配置檢查、sharp 圖片壓縮(統一為 500x500、JPEG quality 40 的 data URL)及 MCP 非同步審核,最後生成詳細報表(通過/違規/失敗)。注意事項包括:必須三步驟初始化(init → notified → call)、絕對不能省略 `apiKey` 及 `notifications/initialized` 步驟、檔案路徑只能使用正斜槓。

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

在你喜歡的 AI 中提問

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

說明文件

圖片內容審核

使用 nx-mcp-audit MCP 服務審核圖片中的色情、政治及暴力內容。

配置

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

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

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

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

No API Key? Contact WeChat zhjian_2026 to get one.

審核流程

步驟 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 取得,等使用者拿到後回來配置

⚠️ 配置缺失時不要安裝 sharp 或繼續後續步驟,先解決配置再往下走。

步驟 2:安裝 sharp + 執行審核(一次 Bash 呼叫,純記憶體,零檔案)

替換 PIC_DIR、MCP_URL、API_KEY 後,heredoc 直接透過 stdin 餵給 node,不寫任何檔案:

NODE_PATH=$(npm root -g) node -e "require('sharp')" 2>/dev/null || npm install -g sharp
NODE_PATH=$(npm root -g) node << 'AUDITEOF'
const fs=require('fs'),path=require('path'),sharp=require('sharp');
let PIC_DIR='<目標圖片目錄絕對路徑>';
const SINGLE_FILE='<單張圖片路徑,為空則審核整個目錄>';
const MCP_URL='<從.mcp.json讀取的url>';
const API_KEY='<從.mcp.json讀取的NX_API_KEY>';
(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 records=[],data_urls=[];let compTotal=0;console.time('壓縮');
for(let i=0;i<imgs.length;i++){const f=imgs[i],fp=path.join(PIC_DIR,f),osz=fs.statSync(fp).size;
try{const buf=await sharp(fp,{limitInputPixels:false}).resize({width:500,height:500,fit:'inside',withoutEnlargement:true}).jpeg({quality:40}).toBuffer();
const url='data:image/jpeg;base64,'+buf.toString('base64');records.push({name:f,origKb:osz,compKb:buf.length,dataUrl:url});
data_urls.push(url);compTotal+=buf.length;console.log(`  [${i+1}/${imgs.length}] ${f} ${(osz/1024).toFixed(0)}KB→${(buf.length/1024).toFixed(0)}KB`)}
catch(e){records.push({name:f,origKb:osz,compKb:0,dataUrl:null,error:e.message});console.log(`  [${i+1}/${imgs.length}] ${f} ❌ ${e.message}`)}}
console.timeEnd('壓縮');
console.log(`payload: ${(compTotal/1024).toFixed(0)}KB`);
console.time('MCP審核');
const H={'Content-Type':'application/json','Accept':'application/json, text/event-stream','Authorization':`Bearer ${API_KEY}`};
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'}}})});
const sid=r1.headers.get('Mcp-Session-Id');H['Mcp-Session-Id']=sid;console.log(`MCP: init→${sid}`);
await fetch(MCP_URL,{method:'POST',headers:H,body:JSON.stringify({jsonrpc:'2.0',method:'notifications/initialized'})});console.log('MCP: notified→202');
let items=[];
if(data_urls.length>0){const r3=await fetch(MCP_URL,{method:'POST',headers:H,body:JSON.stringify({jsonrpc:'2.0',id:'3',method:'tools/call',params:{name:'nx_img_audit',arguments:{files:data_urls,apiKey:API_KEY}}})});
const raw=await r3.json();const inner=JSON.parse(raw.result.content[0].text);items=inner.items}
console.timeEnd('MCP審核');
let itemIdx=0,pass=0,block=0,fail=0;
console.log('\n'+'='.repeat(85));console.log(`${'檔案'.padEnd(50)} ${'原始'.padStart(6)} ${'結果'.padStart(6)} ${'引擎'.padStart(6)} 說明`);console.log('-'.repeat(85));
for(const r of records){const oszS=(r.origKb/1024).toFixed(0)+'KB';
if(r.dataUrl){const item=items[itemIdx++],safe=item.safe,src=item.source||'-';const ec=item.errcode,em=item.errmsg||item.error||'';let st;
if(em==='invalid api key'||em==='未配置 API Key'){st='⚠️ 錯誤';fail++}else if(safe===true){st='✅ 通過';pass++}else if(safe===false){st='⛔ 違規';block++}else{st='❌ 失敗';fail++}
console.log(`${r.name.padEnd(50)} ${oszS.padStart(6)} ${st.padStart(6)} ${src.padStart(6)} ${em.padStart(8)}`)}
else{console.log(`${r.name.padEnd(50)} ${oszS.padStart(6)} ${'❌ 失敗'.padStart(6)} ${'—'.padStart(6)} 壓縮失敗`);fail++}}
const total=records.length;console.log(`\n📊 ${total} 張 | ✅ ${pass} 通過 | ⛔ ${block} 違規 | ⚠️ ${fail} 錯誤/失敗 | v${items[0]?.auditVersion||'?'}`);
})();
AUDITEOF

建議

  • ✅ 通過:可正常使用
  • ⛔ 違規:建議刪除或人工複核
  • ❌ 失敗:重試一次

返回欄位速查

欄位類型說明
safebooleantrue=通過,false=違規
sourcestring審核引擎(wechat / api)
errcodenumber錯誤碼,0=正常
errmsgstring錯誤資訊,"ok"=正常
messagestring審核結果描述
auditVersionstring服務版本號
summaryobject{total, pass, block, error}

常見錯誤速查

錯誤現象原因正確做法
-32602 Invalid request parameters未發送 notifications/initialized必須三步:init → notified → call
406 Not Acceptable缺少 Accept 頭同時聲明 application/json 和 text/event-stream
"請提供 urls 或 files 參數"用了不存在的 imagePath改用 files(base64 dataUrl 陣列)
"invalid api key"API Key 錯誤或過期(errcode=-1)檢查 .mcp.json 中的 Key 是否正確
"未配置 API Key"沒傳 apiKey必須傳,工具定義說可選是誤導
413 Payload Too Largepayload 超限壓縮後通常 < 200KB,不觸發;未壓縮大圖需分批
Cannot find module 'sharp'未全域安裝或缺少 NODE_PATHNODE_PATH=$(npm root -g) node ...

禁止事項

  • ❌ 不要跳過壓縮(即使圖片很小)
  • ❌ 不要使用 imagePath 參數(不存在)
  • ❌ 不要省略 apiKey 參數
  • ❌ 不要省略 notifications/initialized 步驟
  • ❌ 不要寫任何暫存檔案(heredoc 直接餵 stdin,純記憶體執行)
  • ❌ 不要把流程拆成多次 Bash 呼叫(一次 node << 'AUDITEOF' 搞定)
  • ❌ 不要使用反斜線路徑(d:\path),bash heredoc 會被轉義,一律用正斜線(d:/path)

相關技能