Files
agent-park/docs/n8n-tag-janitor.md
T
mzaxd fc46804405 feat: 新增 Tag Janitor API 用于 n8n 自动化标签合并
- 新增 GET /api/tags 接口返回标签列表及项目计数
- 新增 POST /api/tags/maintenance 接口支持批量 nameEn 补全和标签合并
- 合并逻辑包含 ProjectTag 去重处理(避免复合主键冲突)
- 成功后触发 ISR revalidatePath 刷新项目列表页
- 新增 Zod 校验 schemas(TagMaintenanceRequestSchema 等)
- 包含 n8n workflow JSON 模板及文档
2026-02-01 22:51:45 +08:00

3.6 KiB

n8n Tag Janitor Workflow

Overview

Daily automated workflow to clean up duplicate/similar tags using AI semantic analysis.

Workflow Structure

[Cron] → [HTTP GET /api/tags] → [Code: Preprocess] → [AI: Generate Plan] → [Code: Validate JSON] → [HTTP POST /api/tags/maintenance] → [Notification]

Node Configuration

1. Schedule Trigger (Cron)

  • Trigger: Daily at 03:00 UTC
  • Timezone: UTC

2. HTTP Request - Fetch Tags

  • Method: GET
  • URL: {{$env.SITE_BASE_URL}}/api/tags
  • Response: JSON with .tags array

3. Code Node - Preprocess

Purpose: Format tags for LLM input, handle chunking if >200 tags

const tags = $input.first().json.tags;
const formatted = tags.map((t) => ({
  id: t.id,
  name: t.name,
  nameEn: t.nameEn || "",
  projectCount: t._count.projects,
}));
// Sort by projectCount desc for prioritization
formatted.sort((a, b) => b.projectCount - a.projectCount);
return [{ json: { tags: formatted, total: formatted.length } }];

4. AI Node - Generate Merge Plan

Model: GPT-4o / Claude 3.5 Sonnet Temperature: 0.1 (deterministic)

Prompt Template:

You are a tag management expert. Analyze these tags and identify:
1. Semantic duplicates that should be merged (e.g., "机器学习" and "ML" → keep "机器学习" with nameEn "Machine Learning")
2. Tags missing English names that need nameEn补全

Tags (JSON):
{{$json.tags}}

Output STRICT JSON (no markdown, no explanation):
{
  "merges": [
    {
      "target": { "name": "保留的标签名", "nameEn": "Canonical English Name" },
      "sourceTagIds": ["id1", "id2"]
    }
  ],
  "updates": [
    { "tagId": "id", "nameEn": "English Name" }
  ]
}

Rules:
- Keep the tag with higher projectCount as target
- For merges, target can be { "id": "existing_id" } if keeping existing tag, or { "name": "...", "nameEn": "..." } to create new
- Only include tags that NEED action (empty arrays if nothing to do)
- nameEn should be proper English, not pinyin
- Common tech terms: 机器学习=Machine Learning, 深度学习=Deep Learning, 自然语言处理=NLP

5. Code Node - Validate & Parse

const response = $input.first().json;
let plan;
try {
  plan = typeof response === "string" ? JSON.parse(response) : response;
} catch (e) {
  throw new Error("Invalid JSON from AI: " + e.message);
}

// Validate structure
if (!Array.isArray(plan.merges)) plan.merges = [];
if (!Array.isArray(plan.updates)) plan.updates = [];

// Self-merge check: target.id cannot be in sourceTagIds
for (const merge of plan.merges) {
  if (merge.target.id && merge.sourceTagIds.includes(merge.target.id)) {
    throw new Error("Self-merge detected: " + merge.target.id);
  }
}

return [{ json: plan }];

6. HTTP Request - Execute Maintenance

  • Method: POST
  • URL: {{$env.SITE_BASE_URL}}/api/tags/maintenance
  • Body:
{
  "apiKey": "{{$env.WEBHOOK_API_KEY}}",
  "updates": {{$json.updates}},
  "merges": {{$json.merges}}
}

7. Notification (Slack/Email/Webhook)

Send summary:

  • Tags merged: X
  • Tags deleted: Y
  • Names updated: Z
  • Errors: [list]

Environment Variables Required

Chunking Strategy (for >200 tags)

  1. Split tags into batches of 100
  2. Process each batch sequentially
  3. Aggregate results before final notification

Error Handling

  • Retry HTTP requests 3x with exponential backoff
  • On AI parse failure: skip and alert
  • On maintenance failure: log error, continue with notification

Testing

  1. Dry run: Comment out HTTP POST node, check AI output only
  2. Real run: Enable all nodes, monitor /api/tags count before/after