fix: 清洗标签并收敛入库标签噪音

This commit is contained in:
2026-02-21 12:07:02 +08:00
parent 220e9f8b71
commit c2eabfea97
19 changed files with 4399 additions and 145 deletions
+615
View File
@@ -0,0 +1,615 @@
{
"totalTags": 859,
"keepCount": 100,
"dropCount": 759,
"uncoveredProjects": [],
"forcedAdded": [
{
"id": "cmkqnj1pt0001l504vmrywqeg",
"name": "需要配置",
"projects": 163
}
],
"keepTop100": [
{
"id": "cmk9fpg4h0000kt048l3a2nqu",
"name": "Python",
"nameEn": "Python",
"projects": 268
},
{
"id": "cmkqimx520005jx04vyegybp9",
"name": "AI开发工具",
"nameEn": "AI Development Tool",
"projects": 213
},
{
"id": "cmkqnj1pt0001l504vmrywqeg",
"name": "需要配置",
"nameEn": "Requires Configuration",
"projects": 163
},
{
"id": "cmkjjb37v00064j12b84dsrwg",
"name": "TypeScript",
"nameEn": "TypeScript",
"projects": 118
},
{
"id": "cmks0yz0h002xl1040w65w47d",
"name": "AI代理",
"nameEn": "AI Agents",
"projects": 52
},
{
"id": "cmkql448t0002l504m7bkovze",
"name": "自动化",
"nameEn": "Automation",
"projects": 46
},
{
"id": "cmkqjvuo5000ejr04y9ksio48",
"name": "Docker",
"nameEn": "Docker",
"projects": 43
},
{
"id": "cmkjjb37v00074j122wpemyas",
"name": "Node.js",
"nameEn": "Node.js",
"projects": 40
},
{
"id": "cmkoutfvz0005jr04knetubim",
"name": "JavaScript",
"nameEn": "JavaScript",
"projects": 34
},
{
"id": "cmkqklwxm0018jr04zyzlrmlv",
"name": "React",
"nameEn": "React",
"projects": 34
},
{
"id": "cmkqjgpzq0002l104qknt9pho",
"name": "开源",
"nameEn": "Open Source",
"projects": 33
},
{
"id": "cmkqnvcbg0001jo04us31pcaj",
"name": "低学习成本",
"nameEn": "Low Learning Curve",
"projects": 31
},
{
"id": "cmkqk1dbi000ljr045f2rm18m",
"name": "PyTorch",
"nameEn": "PyTorch",
"projects": 30
},
{
"id": "cmkrv2jpg0000l204swrx5xh4",
"name": "LangChain",
"nameEn": "LangChain",
"projects": 29
},
{
"id": "cmkqlcukz000dl5044c6z8542",
"name": "多智能体系统",
"nameEn": "Multi-Agent System",
"projects": 28
},
{
"id": "cmkqk1dc5000ojr04jq0exq1z",
"name": "大语言模型",
"nameEn": null,
"projects": 25
},
{
"id": "cmkqimwxl0000jx04l8q0rdzo",
"name": "LLM",
"nameEn": "LLM",
"projects": 22
},
{
"id": "cmks0rbk1002jl104uc7yg2xv",
"name": "Go",
"nameEn": "Go",
"projects": 20
},
{
"id": "cmkqm1c6j0003jl04clkn9qyb",
"name": "企业级",
"nameEn": "Enterprise",
"projects": 20
},
{
"id": "cmkqnn8hj0006l504j2uedblz",
"name": "需要基础",
"nameEn": "Requires Basics",
"projects": 20
},
{
"id": "cmks0yz1e002yl104nw4apfcq",
"name": "Library",
"nameEn": "Library",
"projects": 18
},
{
"id": "cmkrx8euz0007ky04hrgq5ryg",
"name": "MCP",
"nameEn": "Model Context Protocol",
"projects": 18
},
{
"id": "cmks16lb0003ml104acf4zi7e",
"name": "Markdown",
"nameEn": "Markdown",
"projects": 17
},
{
"id": "cmkqklwr20016jr04opezrtpo",
"name": "Next.js",
"nameEn": "Next.js",
"projects": 17
},
{
"id": "cmks003y10015l104xruieqo9",
"name": "RAG",
"nameEn": "RAG",
"projects": 17
},
{
"id": "cmkqm1bzv0000jl04c3ks3w7w",
"name": "Java",
"nameEn": "Java",
"projects": 16
},
{
"id": "cmks0c9ll0022l10436svssah",
"name": "MIT许可",
"nameEn": "MIT License",
"projects": 16
},
{
"id": "cmkqnn8gl0005l5042jdeuuur",
"name": "LangGraph",
"nameEn": "LangGraph",
"projects": 15
},
{
"id": "cmkrx664z0000ky0481j124cq",
"name": "FastAPI",
"nameEn": "FastAPI",
"projects": 14
},
{
"id": "cmkqjvuir0008jr048591dzwp",
"name": "Rust",
"nameEn": "Rust",
"projects": 14
},
{
"id": "cmkrzlq020001l104sosii49r",
"name": "复杂部署",
"nameEn": "Complex Deployment",
"projects": 14
},
{
"id": "cmkjjb38l000c4j12oucpveph",
"name": "工作流自动化",
"nameEn": "Workflow Automation",
"projects": 14
},
{
"id": "cmks3iu8m006wl104ndk8no10",
"name": "C#",
"nameEn": "C#",
"projects": 11
},
{
"id": "cmkqnn8mm0007l5045q6z4zb0",
"name": "OpenAI",
"nameEn": "OpenAI",
"projects": 11
},
{
"id": "cmkrzvdfk000tl104jsq77zig",
"name": "OpenAI API",
"nameEn": "OpenAI API",
"projects": 11
},
{
"id": "cmks3d1wy006ll104gltxc3et",
"name": "SDK",
"nameEn": "SDK",
"projects": 11
},
{
"id": "cmks1aoxo003xl104gap1drce",
"name": "强化学习",
"nameEn": "Reinforcement Learning",
"projects": 11
},
{
"id": "cmks1aoyn003yl104r4zspprz",
"name": "机器学习",
"nameEn": "Machine Learning",
"projects": 11
},
{
"id": "cmksczul60015jv04ft4n1cb2",
"name": "GitHub",
"nameEn": "GitHub",
"projects": 10
},
{
"id": "cmks0m380002gl104o6f9cjtj",
"name": "PostgreSQL",
"nameEn": "PostgreSQL",
"projects": 10
},
{
"id": "cmkrx8f0j000aky046uomv3rs",
"name": "多模态",
"nameEn": "Multimodal",
"projects": 10
},
{
"id": "cmkqjs7e50004jr042mrpi3ac",
"name": "桌面应用",
"nameEn": null,
"projects": 10
},
{
"id": "cmkqk4i0d000vjr04ai18b8h3",
"name": "知识管理",
"nameEn": null,
"projects": 10
},
{
"id": "cmkqk4i0g000wjr04t82lzvaq",
"name": "向量数据库",
"nameEn": null,
"projects": 9
},
{
"id": "cmkrzlq0q0003l10453qhsniu",
"name": "Transformers",
"nameEn": "Transformers",
"projects": 8
},
{
"id": "cmks1ubzb004pl104mvs08pmu",
"name": "vLLM",
"nameEn": "vLLM",
"projects": 8
},
{
"id": "cmks0eqvq0028l10426ogrvkb",
"name": "云端服务",
"nameEn": "Cloud Service",
"projects": 8
},
{
"id": "cmks13z53003el104re4468xt",
"name": "代码生成",
"nameEn": "Code Generation",
"projects": 8
},
{
"id": "cmks05ci1001gl104lus7os2q",
"name": "企业级AI应用",
"nameEn": "Enterprise AI",
"projects": 8
},
{
"id": "cmks1s7p2004il104kpwprttq",
"name": "智能体框架",
"nameEn": "Agent Framework",
"projects": 8
},
{
"id": "cmkqjs7dw0003jr04uy2bguck",
"name": "跨平台",
"nameEn": null,
"projects": 8
},
{
"id": "cmkqklwxr0019jr048wp0hds8",
"name": "AI 助手",
"nameEn": "AI Assistant",
"projects": 7
},
{
"id": "cmks5ey0j001el204vjk69nwg",
"name": "AI安全",
"nameEn": "AI Security",
"projects": 7
},
{
"id": "cmkqjs77s0000jr041cypbjkn",
"name": "Electron",
"nameEn": "Electron",
"projects": 7
},
{
"id": "cmkry59tc0001jy04kr21i343",
"name": "Playwright",
"nameEn": "Playwright",
"projects": 7
},
{
"id": "cmk9fpg5f0003kt04fcgbvj7y",
"name": "多智能体",
"nameEn": "Multi-Agent",
"projects": 7
},
{
"id": "cmkrzxk4i0011l1045cubzwwr",
"name": "深度学习",
"nameEn": "Deep Learning",
"projects": 7
},
{
"id": "cmks13z46003dl1047ba0entr",
"name": "AI代理框架",
"nameEn": "AI Agent Framework",
"projects": 6
},
{
"id": "cmkry59u70003jy049eoxzkc7",
"name": "AI研究",
"nameEn": "AI Research",
"projects": 6
},
{
"id": "cmks2a6r1005il104ormxbqud",
"name": "Web应用",
"nameEn": "Web Application",
"projects": 6
},
{
"id": "cmkshrboj0000la04h6bmugvj",
"name": "学术研究",
"nameEn": "Academic Research",
"projects": 6
},
{
"id": "cmks112wx0039l104zltcl2hp",
"name": "工作流编排",
"nameEn": "Workflow Orchestration",
"projects": 6
},
{
"id": "cmkrzvdfe000sl104q0gge67f",
"name": "教程",
"nameEn": "Tutorial",
"projects": 6
},
{
"id": "cmks3w0g0000bl904u4lmk5d5",
"name": "浏览器自动化",
"nameEn": "Browser Automation",
"projects": 6
},
{
"id": "cmkt8m55q000nla04kgfld9x3",
"name": "知识图谱",
"nameEn": "Knowledge Graph",
"projects": 6
},
{
"id": "cmkqklwxj0017jr04czubkbbi",
"name": "知识库",
"nameEn": "Knowledge Base",
"projects": 6
},
{
"id": "cmkshubus0004la04zjuh5nn5",
"name": "自主智能体",
"nameEn": "Autonomous Agents",
"projects": 6
},
{
"id": "cmkqk4i0d000ujr045d2njmc7",
"name": "隐私保护",
"nameEn": null,
"projects": 6
},
{
"id": "cmks02o6m001al104gi1jztpn",
"name": "Apache-2.0",
"nameEn": "Apache-2.0",
"projects": 5
},
{
"id": "cmks4roac000el204paypouzl",
"name": "CLI",
"nameEn": "CLI",
"projects": 5
},
{
"id": "cmkt7yhut0000la040ij92vgk",
"name": "MCP协议",
"nameEn": "MCP Protocol",
"projects": 5
},
{
"id": "cmks5rr2e0020l204sn9ydvka",
"name": "Nest.js",
"nameEn": "Nest.js",
"projects": 5
},
{
"id": "cmkjjb37z00084j121651lnjl",
"name": "Vue.js",
"nameEn": "Vue.js",
"projects": 5
},
{
"id": "cmksg1wc90061jv04114izr2d",
"name": "Web自动化",
"nameEn": "Web Automation",
"projects": 5
},
{
"id": "cmkuulu5d000jjc04u7hx67sz",
"name": "分布式系统",
"nameEn": "Distributed Systems",
"projects": 5
},
{
"id": "cmkqimx3n0001jx043mvrmarx",
"name": "可视化",
"nameEn": "Visualization",
"projects": 5
},
{
"id": "cmksf0ik1004ijv047hiusscw",
"name": "多模态AI",
"nameEn": "Multimodal AI",
"projects": 5
},
{
"id": "cmktoexz8001wld046f13barg",
"name": "多语言支持",
"nameEn": "Multi-language Support",
"projects": 5
},
{
"id": "cmks2gqie005ol104v4d6nvck",
"name": "数据分析",
"nameEn": "Data Analytics",
"projects": 5
},
{
"id": "cmks0equs0026l104sl4td34d",
"name": "聊天机器人",
"nameEn": "Chatbot",
"projects": 5
},
{
"id": "cmkjjb38j000b4j125a3fg282",
"name": "自托管",
"nameEn": "Self-hosted",
"projects": 5
},
{
"id": "cmktkj5to001hl704wtmqjft4",
"name": "自然语言处理",
"nameEn": "Natural Language Processing",
"projects": 5
},
{
"id": "cmkrzxk4a0010l104u7irj9je",
"name": "计算机视觉",
"nameEn": "Computer Vision",
"projects": 5
},
{
"id": "cmkto2g8o0019ld04b61pgj4y",
"name": "AI研究资源",
"nameEn": "AI Research Resource",
"projects": 4
},
{
"id": "cmktk5hid000sl7049a2bo5al",
"name": "Benchmark",
"nameEn": "Benchmark",
"projects": 4
},
{
"id": "cmkv6j7uq000gk4043x1gfnp1",
"name": "Bun",
"nameEn": "Bun",
"projects": 4
},
{
"id": "cmkti8i470000kz04bpsajkzk",
"name": "ChromaDB",
"nameEn": "ChromaDB",
"projects": 4
},
{
"id": "cmks47ct90013l904x03rupwb",
"name": "Flask",
"nameEn": "Flask",
"projects": 4
},
{
"id": "cmkql9bho0007l50406oflxr0",
"name": "Gradio",
"nameEn": "Gradio",
"projects": 4
},
{
"id": "cmks112qf0034l104gkihwp10",
"name": "JavaScript/TypeScript",
"nameEn": "JavaScript/TypeScript",
"projects": 4
},
{
"id": "cmks1s7ut004jl104gvlybgfs",
"name": "LiteLLM",
"nameEn": "LiteLLM",
"projects": 4
},
{
"id": "cmks4pb54000bl204erpb9bbp",
"name": "LlamaIndex",
"nameEn": "LlamaIndex",
"projects": 4
},
{
"id": "cmksfnrz1005qjv044pnraikr",
"name": "LLM应用",
"nameEn": "LLM Applications",
"projects": 4
},
{
"id": "cmkqimx480003jx04bcqob7pb",
"name": "NLP",
"nameEn": "NLP",
"projects": 4
},
{
"id": "cmkqm1c690001jl04ge5elv46",
"name": "Spring Boot",
"nameEn": "Spring Boot",
"projects": 4
},
{
"id": "cmks2a6l3005gl1040u4s0k4s",
"name": "Streamlit",
"nameEn": "Streamlit",
"projects": 4
},
{
"id": "cmkth0k4x0026ju04t0euxxwk",
"name": "VS Code扩展",
"nameEn": "VS Code Extension",
"projects": 4
},
{
"id": "cmkscxy490012jv04rf4k4h4h",
"name": "学术资源",
"nameEn": "Academic Resource",
"projects": 4
},
{
"id": "cmkscgc0z0007jv04nwmzbn8c",
"name": "工具调用",
"nameEn": "Tool Calling",
"projects": 4
},
{
"id": "cmkuxtw0m002ojm04pl9qazxs",
"name": "用户通知",
"nameEn": "User Notification",
"projects": 1
}
]
}
@@ -0,0 +1,102 @@
[
"cmk9fpg4h0000kt048l3a2nqu",
"cmkjjb37v00064j12b84dsrwg",
"cmkqimx520005jx04vyegybp9",
"cmks16lb0003ml104acf4zi7e",
"cmks0yz0h002xl1040w65w47d",
"cmkqjgpzq0002l104qknt9pho",
"cmkql448t0002l504m7bkovze",
"cmkqm1bzv0000jl04c3ks3w7w",
"cmkoutfvz0005jr04knetubim",
"cmks0rbk1002jl104uc7yg2xv",
"cmks3iu8m006wl104ndk8no10",
"cmkqk1dc5000ojr04jq0exq1z",
"cmkqimwxl0000jx04l8q0rdzo",
"cmks0yz1e002yl104nw4apfcq",
"cmkqjvuo5000ejr04y9ksio48",
"cmkjjb37v00074j122wpemyas",
"cmkqnvcbg0001jo04us31pcaj",
"cmkrzlq020001l104sosii49r",
"cmks1aoyn003yl104r4zspprz",
"cmks5ey0j001el204vjk69nwg",
"cmkshrboj0000la04h6bmugvj",
"cmkscgc0z0007jv04nwmzbn8c",
"cmkscxy490012jv04rf4k4h4h",
"cmkuxtw0m002ojm04pl9qazxs",
"cmkqklwxm0018jr04zyzlrmlv",
"cmkqk1dbi000ljr045f2rm18m",
"cmkrv2jpg0000l204swrx5xh4",
"cmkqlcukz000dl5044c6z8542",
"cmkqm1c6j0003jl04clkn9qyb",
"cmkqnn8hj0006l504j2uedblz",
"cmkrx8euz0007ky04hrgq5ryg",
"cmkqklwr20016jr04opezrtpo",
"cmks003y10015l104xruieqo9",
"cmks0c9ll0022l10436svssah",
"cmkqnn8gl0005l5042jdeuuur",
"cmkrx664z0000ky0481j124cq",
"cmkqjvuir0008jr048591dzwp",
"cmkjjb38l000c4j12oucpveph",
"cmkqnn8mm0007l5045q6z4zb0",
"cmkrzvdfk000tl104jsq77zig",
"cmks3d1wy006ll104gltxc3et",
"cmks1aoxo003xl104gap1drce",
"cmksczul60015jv04ft4n1cb2",
"cmks0m380002gl104o6f9cjtj",
"cmkrx8f0j000aky046uomv3rs",
"cmkqjs7e50004jr042mrpi3ac",
"cmkqk4i0d000vjr04ai18b8h3",
"cmkqk4i0g000wjr04t82lzvaq",
"cmkrzlq0q0003l10453qhsniu",
"cmks1ubzb004pl104mvs08pmu",
"cmks0eqvq0028l10426ogrvkb",
"cmks13z53003el104re4468xt",
"cmks05ci1001gl104lus7os2q",
"cmks1s7p2004il104kpwprttq",
"cmkqjs7dw0003jr04uy2bguck",
"cmkqklwxr0019jr048wp0hds8",
"cmkqjs77s0000jr041cypbjkn",
"cmkry59tc0001jy04kr21i343",
"cmk9fpg5f0003kt04fcgbvj7y",
"cmkrzxk4i0011l1045cubzwwr",
"cmks13z46003dl1047ba0entr",
"cmkry59u70003jy049eoxzkc7",
"cmks2a6r1005il104ormxbqud",
"cmks112wx0039l104zltcl2hp",
"cmkrzvdfe000sl104q0gge67f",
"cmks3w0g0000bl904u4lmk5d5",
"cmkt8m55q000nla04kgfld9x3",
"cmkqklwxj0017jr04czubkbbi",
"cmkshubus0004la04zjuh5nn5",
"cmkqk4i0d000ujr045d2njmc7",
"cmks02o6m001al104gi1jztpn",
"cmks4roac000el204paypouzl",
"cmkt7yhut0000la040ij92vgk",
"cmks5rr2e0020l204sn9ydvka",
"cmkjjb37z00084j121651lnjl",
"cmksg1wc90061jv04114izr2d",
"cmkuulu5d000jjc04u7hx67sz",
"cmkqimx3n0001jx043mvrmarx",
"cmksf0ik1004ijv047hiusscw",
"cmktoexz8001wld046f13barg",
"cmks2gqie005ol104v4d6nvck",
"cmks0equs0026l104sl4td34d",
"cmkjjb38j000b4j125a3fg282",
"cmktkj5to001hl704wtmqjft4",
"cmkrzxk4a0010l104u7irj9je",
"cmkto2g8o0019ld04b61pgj4y",
"cmktk5hid000sl7049a2bo5al",
"cmkv6j7uq000gk4043x1gfnp1",
"cmkti8i470000kz04bpsajkzk",
"cmks47ct90013l904x03rupwb",
"cmkql9bho0007l50406oflxr0",
"cmks112qf0034l104gkihwp10",
"cmks1s7ut004jl104gvlybgfs",
"cmks4pb54000bl204erpb9bbp",
"cmksfnrz1005qjv044pnraikr",
"cmkqimx480003jx04bcqob7pb",
"cmkqm1c690001jl04ge5elv46",
"cmks2a6l3005gl1040u4s0k4s",
"cmkth0k4x0026ju04t0euxxwk",
"cmkqnj1pt0001l504vmrywqeg"
]
+102
View File
@@ -0,0 +1,102 @@
[
"cmk9fpg4h0000kt048l3a2nqu",
"cmkqimx520005jx04vyegybp9",
"cmkjjb37v00064j12b84dsrwg",
"cmks0yz0h002xl1040w65w47d",
"cmkql448t0002l504m7bkovze",
"cmkqjvuo5000ejr04y9ksio48",
"cmkjjb37v00074j122wpemyas",
"cmkoutfvz0005jr04knetubim",
"cmkqklwxm0018jr04zyzlrmlv",
"cmkqjgpzq0002l104qknt9pho",
"cmkqnvcbg0001jo04us31pcaj",
"cmkqk1dbi000ljr045f2rm18m",
"cmkrv2jpg0000l204swrx5xh4",
"cmkqlcukz000dl5044c6z8542",
"cmkqk1dc5000ojr04jq0exq1z",
"cmkqimwxl0000jx04l8q0rdzo",
"cmks0rbk1002jl104uc7yg2xv",
"cmkqm1c6j0003jl04clkn9qyb",
"cmkqnn8hj0006l504j2uedblz",
"cmks0yz1e002yl104nw4apfcq",
"cmkrx8euz0007ky04hrgq5ryg",
"cmks16lb0003ml104acf4zi7e",
"cmkqklwr20016jr04opezrtpo",
"cmks003y10015l104xruieqo9",
"cmkqm1bzv0000jl04c3ks3w7w",
"cmks0c9ll0022l10436svssah",
"cmkqnn8gl0005l5042jdeuuur",
"cmkrzlq020001l104sosii49r",
"cmkrx664z0000ky0481j124cq",
"cmkqjvuir0008jr048591dzwp",
"cmkjjb38l000c4j12oucpveph",
"cmks3iu8m006wl104ndk8no10",
"cmks1aoyn003yl104r4zspprz",
"cmkqnn8mm0007l5045q6z4zb0",
"cmkrzvdfk000tl104jsq77zig",
"cmks3d1wy006ll104gltxc3et",
"cmks1aoxo003xl104gap1drce",
"cmksczul60015jv04ft4n1cb2",
"cmks0m380002gl104o6f9cjtj",
"cmkrx8f0j000aky046uomv3rs",
"cmkqjs7e50004jr042mrpi3ac",
"cmkqk4i0d000vjr04ai18b8h3",
"cmkqk4i0g000wjr04t82lzvaq",
"cmkrzlq0q0003l10453qhsniu",
"cmks1ubzb004pl104mvs08pmu",
"cmks0eqvq0028l10426ogrvkb",
"cmks13z53003el104re4468xt",
"cmks05ci1001gl104lus7os2q",
"cmks1s7p2004il104kpwprttq",
"cmkqjs7dw0003jr04uy2bguck",
"cmks5ey0j001el204vjk69nwg",
"cmkqklwxr0019jr048wp0hds8",
"cmkqjs77s0000jr041cypbjkn",
"cmkry59tc0001jy04kr21i343",
"cmk9fpg5f0003kt04fcgbvj7y",
"cmkrzxk4i0011l1045cubzwwr",
"cmkshrboj0000la04h6bmugvj",
"cmks13z46003dl1047ba0entr",
"cmkry59u70003jy049eoxzkc7",
"cmks2a6r1005il104ormxbqud",
"cmks112wx0039l104zltcl2hp",
"cmkrzvdfe000sl104q0gge67f",
"cmks3w0g0000bl904u4lmk5d5",
"cmkt8m55q000nla04kgfld9x3",
"cmkqklwxj0017jr04czubkbbi",
"cmkshubus0004la04zjuh5nn5",
"cmkqk4i0d000ujr045d2njmc7",
"cmks02o6m001al104gi1jztpn",
"cmks4roac000el204paypouzl",
"cmkt7yhut0000la040ij92vgk",
"cmks5rr2e0020l204sn9ydvka",
"cmkjjb37z00084j121651lnjl",
"cmksg1wc90061jv04114izr2d",
"cmkuulu5d000jjc04u7hx67sz",
"cmkqimx3n0001jx043mvrmarx",
"cmksf0ik1004ijv047hiusscw",
"cmktoexz8001wld046f13barg",
"cmks2gqie005ol104v4d6nvck",
"cmks0equs0026l104sl4td34d",
"cmkjjb38j000b4j125a3fg282",
"cmktkj5to001hl704wtmqjft4",
"cmkrzxk4a0010l104u7irj9je",
"cmkscgc0z0007jv04nwmzbn8c",
"cmkscxy490012jv04rf4k4h4h",
"cmkto2g8o0019ld04b61pgj4y",
"cmktk5hid000sl7049a2bo5al",
"cmkv6j7uq000gk4043x1gfnp1",
"cmkti8i470000kz04bpsajkzk",
"cmks47ct90013l904x03rupwb",
"cmkql9bho0007l50406oflxr0",
"cmks112qf0034l104gkihwp10",
"cmks1s7ut004jl104gvlybgfs",
"cmks4pb54000bl204erpb9bbp",
"cmksfnrz1005qjv044pnraikr",
"cmkqimx480003jx04bcqob7pb",
"cmkqm1c690001jl04ge5elv46",
"cmks2a6l3005gl1040u4s0k4s",
"cmkth0k4x0026ju04t0euxxwk",
"cmkuuqo3g000njc04l72uwte3",
"cmkuxtw0m002ojm04pl9qazxs"
]
+784
View File
@@ -0,0 +1,784 @@
{
"totalTags": 859,
"candidatesAfterGenericFilter": 831,
"selectedCount": 100,
"selectedProjectsCoverage": {
"coveredProjects": 453,
"totalProjects": 454,
"coverageRatio": 0.9978,
"uncoveredProjects": 1
},
"dropCount": 759,
"topSelected": [
{
"id": "cmk9fpg4h0000kt048l3a2nqu",
"name": "Python",
"nameEn": "Python",
"projects": 268
},
{
"id": "cmkqimx520005jx04vyegybp9",
"name": "AI开发工具",
"nameEn": "AI Development Tool",
"projects": 213
},
{
"id": "cmkjjb37v00064j12b84dsrwg",
"name": "TypeScript",
"nameEn": "TypeScript",
"projects": 118
},
{
"id": "cmks0yz0h002xl1040w65w47d",
"name": "AI代理",
"nameEn": "AI Agents",
"projects": 52
},
{
"id": "cmkql448t0002l504m7bkovze",
"name": "自动化",
"nameEn": "Automation",
"projects": 46
},
{
"id": "cmkqjvuo5000ejr04y9ksio48",
"name": "Docker",
"nameEn": "Docker",
"projects": 43
},
{
"id": "cmkjjb37v00074j122wpemyas",
"name": "Node.js",
"nameEn": "Node.js",
"projects": 40
},
{
"id": "cmkoutfvz0005jr04knetubim",
"name": "JavaScript",
"nameEn": "JavaScript",
"projects": 34
},
{
"id": "cmkqklwxm0018jr04zyzlrmlv",
"name": "React",
"nameEn": "React",
"projects": 34
},
{
"id": "cmkqjgpzq0002l104qknt9pho",
"name": "开源",
"nameEn": "Open Source",
"projects": 33
},
{
"id": "cmkqnvcbg0001jo04us31pcaj",
"name": "低学习成本",
"nameEn": "Low Learning Curve",
"projects": 31
},
{
"id": "cmkqk1dbi000ljr045f2rm18m",
"name": "PyTorch",
"nameEn": "PyTorch",
"projects": 30
},
{
"id": "cmkrv2jpg0000l204swrx5xh4",
"name": "LangChain",
"nameEn": "LangChain",
"projects": 29
},
{
"id": "cmkqlcukz000dl5044c6z8542",
"name": "多智能体系统",
"nameEn": "Multi-Agent System",
"projects": 28
},
{
"id": "cmkqk1dc5000ojr04jq0exq1z",
"name": "大语言模型",
"nameEn": null,
"projects": 25
},
{
"id": "cmkqimwxl0000jx04l8q0rdzo",
"name": "LLM",
"nameEn": "LLM",
"projects": 22
},
{
"id": "cmks0rbk1002jl104uc7yg2xv",
"name": "Go",
"nameEn": "Go",
"projects": 20
},
{
"id": "cmkqm1c6j0003jl04clkn9qyb",
"name": "企业级",
"nameEn": "Enterprise",
"projects": 20
},
{
"id": "cmkqnn8hj0006l504j2uedblz",
"name": "需要基础",
"nameEn": "Requires Basics",
"projects": 20
},
{
"id": "cmks0yz1e002yl104nw4apfcq",
"name": "Library",
"nameEn": "Library",
"projects": 18
},
{
"id": "cmkrx8euz0007ky04hrgq5ryg",
"name": "MCP",
"nameEn": "Model Context Protocol",
"projects": 18
},
{
"id": "cmks16lb0003ml104acf4zi7e",
"name": "Markdown",
"nameEn": "Markdown",
"projects": 17
},
{
"id": "cmkqklwr20016jr04opezrtpo",
"name": "Next.js",
"nameEn": "Next.js",
"projects": 17
},
{
"id": "cmks003y10015l104xruieqo9",
"name": "RAG",
"nameEn": "RAG",
"projects": 17
},
{
"id": "cmkqm1bzv0000jl04c3ks3w7w",
"name": "Java",
"nameEn": "Java",
"projects": 16
},
{
"id": "cmks0c9ll0022l10436svssah",
"name": "MIT许可",
"nameEn": "MIT License",
"projects": 16
},
{
"id": "cmkqnn8gl0005l5042jdeuuur",
"name": "LangGraph",
"nameEn": "LangGraph",
"projects": 15
},
{
"id": "cmkrzlq020001l104sosii49r",
"name": "复杂部署",
"nameEn": "Complex Deployment",
"projects": 14
},
{
"id": "cmkrx664z0000ky0481j124cq",
"name": "FastAPI",
"nameEn": "FastAPI",
"projects": 14
},
{
"id": "cmkqjvuir0008jr048591dzwp",
"name": "Rust",
"nameEn": "Rust",
"projects": 14
},
{
"id": "cmkjjb38l000c4j12oucpveph",
"name": "工作流自动化",
"nameEn": "Workflow Automation",
"projects": 14
},
{
"id": "cmks3iu8m006wl104ndk8no10",
"name": "C#",
"nameEn": "C#",
"projects": 11
},
{
"id": "cmks1aoyn003yl104r4zspprz",
"name": "机器学习",
"nameEn": "Machine Learning",
"projects": 11
},
{
"id": "cmkqnn8mm0007l5045q6z4zb0",
"name": "OpenAI",
"nameEn": "OpenAI",
"projects": 11
},
{
"id": "cmkrzvdfk000tl104jsq77zig",
"name": "OpenAI API",
"nameEn": "OpenAI API",
"projects": 11
},
{
"id": "cmks3d1wy006ll104gltxc3et",
"name": "SDK",
"nameEn": "SDK",
"projects": 11
},
{
"id": "cmks1aoxo003xl104gap1drce",
"name": "强化学习",
"nameEn": "Reinforcement Learning",
"projects": 11
},
{
"id": "cmksczul60015jv04ft4n1cb2",
"name": "GitHub",
"nameEn": "GitHub",
"projects": 10
},
{
"id": "cmks0m380002gl104o6f9cjtj",
"name": "PostgreSQL",
"nameEn": "PostgreSQL",
"projects": 10
},
{
"id": "cmkrx8f0j000aky046uomv3rs",
"name": "多模态",
"nameEn": "Multimodal",
"projects": 10
},
{
"id": "cmkqjs7e50004jr042mrpi3ac",
"name": "桌面应用",
"nameEn": null,
"projects": 10
},
{
"id": "cmkqk4i0d000vjr04ai18b8h3",
"name": "知识管理",
"nameEn": null,
"projects": 10
},
{
"id": "cmkqk4i0g000wjr04t82lzvaq",
"name": "向量数据库",
"nameEn": null,
"projects": 9
},
{
"id": "cmkrzlq0q0003l10453qhsniu",
"name": "Transformers",
"nameEn": "Transformers",
"projects": 8
},
{
"id": "cmks1ubzb004pl104mvs08pmu",
"name": "vLLM",
"nameEn": "vLLM",
"projects": 8
},
{
"id": "cmks0eqvq0028l10426ogrvkb",
"name": "云端服务",
"nameEn": "Cloud Service",
"projects": 8
},
{
"id": "cmks13z53003el104re4468xt",
"name": "代码生成",
"nameEn": "Code Generation",
"projects": 8
},
{
"id": "cmks05ci1001gl104lus7os2q",
"name": "企业级AI应用",
"nameEn": "Enterprise AI",
"projects": 8
},
{
"id": "cmks1s7p2004il104kpwprttq",
"name": "智能体框架",
"nameEn": "Agent Framework",
"projects": 8
},
{
"id": "cmkqjs7dw0003jr04uy2bguck",
"name": "跨平台",
"nameEn": null,
"projects": 8
},
{
"id": "cmks5ey0j001el204vjk69nwg",
"name": "AI安全",
"nameEn": "AI Security",
"projects": 7
},
{
"id": "cmkqklwxr0019jr048wp0hds8",
"name": "AI 助手",
"nameEn": "AI Assistant",
"projects": 7
},
{
"id": "cmkqjs77s0000jr041cypbjkn",
"name": "Electron",
"nameEn": "Electron",
"projects": 7
},
{
"id": "cmkry59tc0001jy04kr21i343",
"name": "Playwright",
"nameEn": "Playwright",
"projects": 7
},
{
"id": "cmk9fpg5f0003kt04fcgbvj7y",
"name": "多智能体",
"nameEn": "Multi-Agent",
"projects": 7
},
{
"id": "cmkrzxk4i0011l1045cubzwwr",
"name": "深度学习",
"nameEn": "Deep Learning",
"projects": 7
},
{
"id": "cmkshrboj0000la04h6bmugvj",
"name": "学术研究",
"nameEn": "Academic Research",
"projects": 6
},
{
"id": "cmks13z46003dl1047ba0entr",
"name": "AI代理框架",
"nameEn": "AI Agent Framework",
"projects": 6
},
{
"id": "cmkry59u70003jy049eoxzkc7",
"name": "AI研究",
"nameEn": "AI Research",
"projects": 6
},
{
"id": "cmks2a6r1005il104ormxbqud",
"name": "Web应用",
"nameEn": "Web Application",
"projects": 6
},
{
"id": "cmks112wx0039l104zltcl2hp",
"name": "工作流编排",
"nameEn": "Workflow Orchestration",
"projects": 6
},
{
"id": "cmkrzvdfe000sl104q0gge67f",
"name": "教程",
"nameEn": "Tutorial",
"projects": 6
},
{
"id": "cmks3w0g0000bl904u4lmk5d5",
"name": "浏览器自动化",
"nameEn": "Browser Automation",
"projects": 6
},
{
"id": "cmkt8m55q000nla04kgfld9x3",
"name": "知识图谱",
"nameEn": "Knowledge Graph",
"projects": 6
},
{
"id": "cmkqklwxj0017jr04czubkbbi",
"name": "知识库",
"nameEn": "Knowledge Base",
"projects": 6
},
{
"id": "cmkshubus0004la04zjuh5nn5",
"name": "自主智能体",
"nameEn": "Autonomous Agents",
"projects": 6
},
{
"id": "cmkqk4i0d000ujr045d2njmc7",
"name": "隐私保护",
"nameEn": null,
"projects": 6
},
{
"id": "cmks02o6m001al104gi1jztpn",
"name": "Apache-2.0",
"nameEn": "Apache-2.0",
"projects": 5
},
{
"id": "cmks4roac000el204paypouzl",
"name": "CLI",
"nameEn": "CLI",
"projects": 5
},
{
"id": "cmkt7yhut0000la040ij92vgk",
"name": "MCP协议",
"nameEn": "MCP Protocol",
"projects": 5
},
{
"id": "cmks5rr2e0020l204sn9ydvka",
"name": "Nest.js",
"nameEn": "Nest.js",
"projects": 5
},
{
"id": "cmkjjb37z00084j121651lnjl",
"name": "Vue.js",
"nameEn": "Vue.js",
"projects": 5
},
{
"id": "cmksg1wc90061jv04114izr2d",
"name": "Web自动化",
"nameEn": "Web Automation",
"projects": 5
},
{
"id": "cmkuulu5d000jjc04u7hx67sz",
"name": "分布式系统",
"nameEn": "Distributed Systems",
"projects": 5
},
{
"id": "cmkqimx3n0001jx043mvrmarx",
"name": "可视化",
"nameEn": "Visualization",
"projects": 5
},
{
"id": "cmksf0ik1004ijv047hiusscw",
"name": "多模态AI",
"nameEn": "Multimodal AI",
"projects": 5
},
{
"id": "cmktoexz8001wld046f13barg",
"name": "多语言支持",
"nameEn": "Multi-language Support",
"projects": 5
},
{
"id": "cmks2gqie005ol104v4d6nvck",
"name": "数据分析",
"nameEn": "Data Analytics",
"projects": 5
},
{
"id": "cmks0equs0026l104sl4td34d",
"name": "聊天机器人",
"nameEn": "Chatbot",
"projects": 5
},
{
"id": "cmkjjb38j000b4j125a3fg282",
"name": "自托管",
"nameEn": "Self-hosted",
"projects": 5
},
{
"id": "cmktkj5to001hl704wtmqjft4",
"name": "自然语言处理",
"nameEn": "Natural Language Processing",
"projects": 5
},
{
"id": "cmkrzxk4a0010l104u7irj9je",
"name": "计算机视觉",
"nameEn": "Computer Vision",
"projects": 5
},
{
"id": "cmkscgc0z0007jv04nwmzbn8c",
"name": "工具调用",
"nameEn": "Tool Calling",
"projects": 4
},
{
"id": "cmkscxy490012jv04rf4k4h4h",
"name": "学术资源",
"nameEn": "Academic Resource",
"projects": 4
},
{
"id": "cmkto2g8o0019ld04b61pgj4y",
"name": "AI研究资源",
"nameEn": "AI Research Resource",
"projects": 4
},
{
"id": "cmktk5hid000sl7049a2bo5al",
"name": "Benchmark",
"nameEn": "Benchmark",
"projects": 4
},
{
"id": "cmkv6j7uq000gk4043x1gfnp1",
"name": "Bun",
"nameEn": "Bun",
"projects": 4
},
{
"id": "cmkti8i470000kz04bpsajkzk",
"name": "ChromaDB",
"nameEn": "ChromaDB",
"projects": 4
},
{
"id": "cmks47ct90013l904x03rupwb",
"name": "Flask",
"nameEn": "Flask",
"projects": 4
},
{
"id": "cmkql9bho0007l50406oflxr0",
"name": "Gradio",
"nameEn": "Gradio",
"projects": 4
},
{
"id": "cmks112qf0034l104gkihwp10",
"name": "JavaScript/TypeScript",
"nameEn": "JavaScript/TypeScript",
"projects": 4
},
{
"id": "cmks1s7ut004jl104gvlybgfs",
"name": "LiteLLM",
"nameEn": "LiteLLM",
"projects": 4
},
{
"id": "cmks4pb54000bl204erpb9bbp",
"name": "LlamaIndex",
"nameEn": "LlamaIndex",
"projects": 4
},
{
"id": "cmksfnrz1005qjv044pnraikr",
"name": "LLM应用",
"nameEn": "LLM Applications",
"projects": 4
},
{
"id": "cmkqimx480003jx04bcqob7pb",
"name": "NLP",
"nameEn": "NLP",
"projects": 4
},
{
"id": "cmkqm1c690001jl04ge5elv46",
"name": "Spring Boot",
"nameEn": "Spring Boot",
"projects": 4
},
{
"id": "cmks2a6l3005gl1040u4s0k4s",
"name": "Streamlit",
"nameEn": "Streamlit",
"projects": 4
},
{
"id": "cmkth0k4x0026ju04t0euxxwk",
"name": "VS Code扩展",
"nameEn": "VS Code Extension",
"projects": 4
},
{
"id": "cmkuuqo3g000njc04l72uwte3",
"name": "文件系统操作",
"nameEn": "File System Operations",
"projects": 2
},
{
"id": "cmkuxtw0m002ojm04pl9qazxs",
"name": "用户通知",
"nameEn": "User Notification",
"projects": 1
}
],
"genericRemovedTop": [
{
"id": "cmkoutfta0001jr04qlwpr3xh",
"name": "活跃维护",
"nameEn": "Active Maintenance",
"projects": 317
},
{
"id": "cmkqk1dbs000njr0418v38jh4",
"name": "可商用",
"nameEn": null,
"projects": 171
},
{
"id": "cmkqnj1pt0001l504vmrywqeg",
"name": "需要配置",
"nameEn": "Requires Configuration",
"projects": 163
},
{
"id": "cmkqk1div000qjr04ckie91k5",
"name": "AI",
"nameEn": "AI",
"projects": 115
},
{
"id": "cmkoutfub0004jr049qn5fxc0",
"name": "易上手",
"nameEn": "Easy to Start",
"projects": 107
},
{
"id": "cmkoutftj0002jr04xqiuu0cb",
"name": "框架",
"nameEn": "Framework",
"projects": 90
},
{
"id": "cmkqk1d5o000kjr049wqf7i5o",
"name": "开发工具",
"nameEn": null,
"projects": 88
},
{
"id": "cmkqnvc540000jo04qrexxuhd",
"name": "开箱即用",
"nameEn": "Ready to Use",
"projects": 72
},
{
"id": "cmkqkapuh0010jr043s59my3y",
"name": "文档完善",
"nameEn": "Well Documented",
"projects": 69
},
{
"id": "cmkqk4i0g000xjr04sn06e7wt",
"name": "本地部署",
"nameEn": null,
"projects": 38
},
{
"id": "cmkoutfu10003jr04dw11uzvu",
"name": "有示例",
"nameEn": "Has Examples",
"projects": 36
},
{
"id": "cmkql442c0000l504qbomadup",
"name": "需要Docker",
"nameEn": "Requires Docker",
"projects": 36
},
{
"id": "cmkryxmob0003l80414uo5zqz",
"name": "CLI工具",
"nameEn": "CLI Tool",
"projects": 33
},
{
"id": "cmks1fjqc0042l104cqi0dh9v",
"name": "工具",
"nameEn": "Tool",
"projects": 33
},
{
"id": "cmkqkaq140011jr04n5aqr763",
"name": "平台",
"nameEn": "Platform",
"projects": 29
},
{
"id": "cmkrzlq0e0002l104jfnarvv8",
"name": "智能体",
"nameEn": "Agent",
"projects": 16
},
{
"id": "cmk9fpg560001kt04ab5369hg",
"name": "开发框架",
"nameEn": "Development Framework",
"projects": 10
},
{
"id": "cmk9fpg580002kt04sslxarl5",
"name": "人工智能",
"nameEn": "Artificial Intelligence",
"projects": 7
},
{
"id": "cmkrv2jvc0001l204703u5836",
"name": "开发者工具",
"nameEn": "Developer Tool",
"projects": 4
},
{
"id": "cmksdlj9o0029jv04x9tdyc11",
"name": "科研工具",
"nameEn": "Research Tool",
"projects": 4
},
{
"id": "cmks4tb3k000jl204z6oxfuyh",
"name": "终端工具",
"nameEn": "Terminal Tools",
"projects": 4
},
{
"id": "cmkqjgpzv0003l104asyyc6dg",
"name": "低代码平台",
"nameEn": "Low-Code Platform",
"projects": 3
},
{
"id": "cmkrxh1kc000hky04rpxvrj1w",
"name": "数据库工具",
"nameEn": "Database Tools",
"projects": 3
},
{
"id": "cmks05cng001il1049zztjibt",
"name": "Agent平台",
"nameEn": "Agent Platform",
"projects": 3
},
{
"id": "cmkjjb38300094j12lum5vwjj",
"name": "集成平台",
"nameEn": "Integration Platform",
"projects": 2
},
{
"id": "cmkt8ex6g000ala0488t486bj",
"name": "平台/框架",
"nameEn": "Platform/Framework",
"projects": 1
},
{
"id": "cmktpw9pe003tld04fjjgo4zt",
"name": "系统监控",
"nameEn": "System Monitoring",
"projects": 1
},
{
"id": "cmkuxtvzn002mjm04wdcx9wwi",
"name": "系统通知",
"nameEn": "System Notification",
"projects": 1
}
]
}
@@ -0,0 +1,305 @@
{
"totalTags": 859,
"totalProjects": 454,
"activeProjects": 444,
"distributionAtLeast": {
"1": 859,
"2": 283,
"3": 169,
"4": 130,
"5": 100,
"6": 85,
"8": 67,
"10": 59,
"15": 43,
"20": 34,
"30": 26,
"50": 13
},
"top100CoverageProjects": 453,
"projectsWithoutTop100Tag": 1,
"top20": [
{
"name": "活跃维护",
"nameEn": "Active Maintenance",
"projects": 317
},
{
"name": "Python",
"nameEn": "Python",
"projects": 268
},
{
"name": "AI开发工具",
"nameEn": "AI Development Tool",
"projects": 213
},
{
"name": "可商用",
"nameEn": null,
"projects": 171
},
{
"name": "需要配置",
"nameEn": "Requires Configuration",
"projects": 163
},
{
"name": "TypeScript",
"nameEn": "TypeScript",
"projects": 118
},
{
"name": "AI",
"nameEn": "AI",
"projects": 115
},
{
"name": "易上手",
"nameEn": "Easy to Start",
"projects": 107
},
{
"name": "框架",
"nameEn": "Framework",
"projects": 90
},
{
"name": "开发工具",
"nameEn": null,
"projects": 88
},
{
"name": "开箱即用",
"nameEn": "Ready to Use",
"projects": 72
},
{
"name": "文档完善",
"nameEn": "Well Documented",
"projects": 69
},
{
"name": "AI代理",
"nameEn": "AI Agents",
"projects": 52
},
{
"name": "自动化",
"nameEn": "Automation",
"projects": 46
},
{
"name": "Docker",
"nameEn": "Docker",
"projects": 43
},
{
"name": "Node.js",
"nameEn": "Node.js",
"projects": 40
},
{
"name": "本地部署",
"nameEn": null,
"projects": 38
},
{
"name": "有示例",
"nameEn": "Has Examples",
"projects": 36
},
{
"name": "需要Docker",
"nameEn": "Requires Docker",
"projects": 36
},
{
"name": "JavaScript",
"nameEn": "JavaScript",
"projects": 34
}
],
"genericTagsTop30": [
{
"id": "cmkqimx520005jx04vyegybp9",
"name": "AI开发工具",
"nameEn": "AI Development Tool",
"projects": 213
},
{
"id": "cmkqk1div000qjr04ckie91k5",
"name": "AI",
"nameEn": "AI",
"projects": 115
},
{
"id": "cmkoutftj0002jr04xqiuu0cb",
"name": "框架",
"nameEn": "Framework",
"projects": 90
},
{
"id": "cmkqk1d5o000kjr049wqf7i5o",
"name": "开发工具",
"nameEn": null,
"projects": 88
},
{
"id": "cmks0yz0h002xl1040w65w47d",
"name": "AI代理",
"nameEn": "AI Agents",
"projects": 52
},
{
"id": "cmkryxmob0003l80414uo5zqz",
"name": "CLI工具",
"nameEn": "CLI Tool",
"projects": 33
},
{
"id": "cmks1fjqc0042l104cqi0dh9v",
"name": "工具",
"nameEn": "Tool",
"projects": 33
},
{
"id": "cmkqkaq140011jr04n5aqr763",
"name": "平台",
"nameEn": "Platform",
"projects": 29
},
{
"id": "cmkqlcukz000dl5044c6z8542",
"name": "多智能体系统",
"nameEn": "Multi-Agent System",
"projects": 28
},
{
"id": "cmkrzlq0e0002l104jfnarvv8",
"name": "智能体",
"nameEn": "Agent",
"projects": 16
},
{
"id": "cmk9fpg560001kt04ab5369hg",
"name": "开发框架",
"nameEn": "Development Framework",
"projects": 10
},
{
"id": "cmks05ci1001gl104lus7os2q",
"name": "企业级AI应用",
"nameEn": "Enterprise AI",
"projects": 8
},
{
"id": "cmks1s7p2004il104kpwprttq",
"name": "智能体框架",
"nameEn": "Agent Framework",
"projects": 8
},
{
"id": "cmk9fpg580002kt04sslxarl5",
"name": "人工智能",
"nameEn": "Artificial Intelligence",
"projects": 7
},
{
"id": "cmk9fpg5f0003kt04fcgbvj7y",
"name": "多智能体",
"nameEn": "Multi-Agent",
"projects": 7
},
{
"id": "cmkqklwxr0019jr048wp0hds8",
"name": "AI 助手",
"nameEn": "AI Assistant",
"projects": 7
},
{
"id": "cmkshubus0004la04zjuh5nn5",
"name": "自主智能体",
"nameEn": "Autonomous Agents",
"projects": 6
},
{
"id": "cmks13z46003dl1047ba0entr",
"name": "AI代理框架",
"nameEn": "AI Agent Framework",
"projects": 6
},
{
"id": "cmksf0ik1004ijv047hiusscw",
"name": "多模态AI",
"nameEn": "Multimodal AI",
"projects": 5
},
{
"id": "cmkrv2jvc0001l204703u5836",
"name": "开发者工具",
"nameEn": "Developer Tool",
"projects": 4
},
{
"id": "cmksdlj9o0029jv04x9tdyc11",
"name": "科研工具",
"nameEn": "Research Tool",
"projects": 4
},
{
"id": "cmksf2ncg004pjv0446rsr0e6",
"name": "生成式AI",
"nameEn": "Generative AI",
"projects": 4
},
{
"id": "cmks4tb3k000jl204z6oxfuyh",
"name": "终端工具",
"nameEn": "Terminal Tools",
"projects": 4
},
{
"id": "cmkqjgpzv0003l104asyyc6dg",
"name": "低代码平台",
"nameEn": "Low-Code Platform",
"projects": 3
},
{
"id": "cmkqlcuqx000fl504ywms66me",
"name": "AI开发框架",
"nameEn": "AI Development Framework",
"projects": 3
},
{
"id": "cmkujqy2l0007jx04w5zxt8be",
"name": "游戏AI",
"nameEn": "Game AI",
"projects": 3
},
{
"id": "cmkrxh1kc000hky04rpxvrj1w",
"name": "数据库工具",
"nameEn": "Database Tools",
"projects": 3
},
{
"id": "cmks05cng001il1049zztjibt",
"name": "Agent平台",
"nameEn": "Agent Platform",
"projects": 3
},
{
"id": "cmks3y6z0000hl9048h3bv62n",
"name": "记忆系统",
"nameEn": "Memory System",
"projects": 3
},
{
"id": "cmkjjb38300094j12lum5vwjj",
"name": "集成平台",
"nameEn": "Integration Platform",
"projects": 2
}
]
}
@@ -0,0 +1,163 @@
{
"totalTags": 100,
"totalProjectTags": 1888,
"zeroTags": 0,
"aiTagExists": false,
"aiTag": null,
"projectCoverage": {
"covered": 454,
"total": 454
},
"top30": [
{
"name": "Python",
"nameEn": "Python",
"projects": 268
},
{
"name": "AI开发工具",
"nameEn": "AI Development Tool",
"projects": 213
},
{
"name": "需要配置",
"nameEn": "Requires Configuration",
"projects": 163
},
{
"name": "TypeScript",
"nameEn": "TypeScript",
"projects": 118
},
{
"name": "AI代理",
"nameEn": "AI Agents",
"projects": 52
},
{
"name": "自动化",
"nameEn": "Automation",
"projects": 46
},
{
"name": "Docker",
"nameEn": "Docker",
"projects": 43
},
{
"name": "Node.js",
"nameEn": "Node.js",
"projects": 40
},
{
"name": "JavaScript",
"nameEn": "JavaScript",
"projects": 34
},
{
"name": "React",
"nameEn": "React",
"projects": 34
},
{
"name": "开源",
"nameEn": "Open Source",
"projects": 33
},
{
"name": "低学习成本",
"nameEn": "Low Learning Curve",
"projects": 31
},
{
"name": "PyTorch",
"nameEn": "PyTorch",
"projects": 30
},
{
"name": "LangChain",
"nameEn": "LangChain",
"projects": 29
},
{
"name": "多智能体系统",
"nameEn": "Multi-Agent System",
"projects": 28
},
{
"name": "大语言模型",
"nameEn": null,
"projects": 25
},
{
"name": "LLM",
"nameEn": "LLM",
"projects": 22
},
{
"name": "Go",
"nameEn": "Go",
"projects": 20
},
{
"name": "企业级",
"nameEn": "Enterprise",
"projects": 20
},
{
"name": "需要基础",
"nameEn": "Requires Basics",
"projects": 20
},
{
"name": "Library",
"nameEn": "Library",
"projects": 18
},
{
"name": "MCP",
"nameEn": "Model Context Protocol",
"projects": 18
},
{
"name": "Markdown",
"nameEn": "Markdown",
"projects": 17
},
{
"name": "Next.js",
"nameEn": "Next.js",
"projects": 17
},
{
"name": "RAG",
"nameEn": "RAG",
"projects": 17
},
{
"name": "Java",
"nameEn": "Java",
"projects": 16
},
{
"name": "MIT许可",
"nameEn": "MIT License",
"projects": 16
},
{
"name": "LangGraph",
"nameEn": "LangGraph",
"projects": 15
},
{
"name": "FastAPI",
"nameEn": "FastAPI",
"projects": 14
},
{
"name": "Rust",
"nameEn": "Rust",
"projects": 14
}
]
}
@@ -0,0 +1,44 @@
{
"generatedAt": "2026-02-21T03:47:31.204Z",
"mergeGroupCount": 6,
"mergePairCount": 6,
"mergePairs": [
{
"sourceId": "cmkoutg0o0006jr04goozjhft",
"targetId": "cmkqimx520005jx04vyegybp9",
"key": "ai development tool"
},
{
"sourceId": "cmkjjb387000a4j12pdf123zn",
"targetId": "cmks0yz0h002xl1040w65w47d",
"key": "ai agent"
},
{
"sourceId": "cmkun0ao70048jx04atm2lwwq",
"targetId": "cmkryxmob0003l80414uo5zqz",
"key": "cli tool"
},
{
"sourceId": "cmkrzqdlu000fl104qmy9gu2u",
"targetId": "cmkqlcukz000dl5044c6z8542",
"key": "multi agent system"
},
{
"sourceId": "cmktk3biu000nl704iiohmv4i",
"targetId": "cmksdlj9o0029jv04x9tdyc11",
"key": "research tool"
},
{
"sourceId": "cmktpotkp003eld04mv09ckde",
"targetId": "cmkshubus0004la04zjuh5nn5",
"key": "autonomous agent"
}
],
"before": {
"totalTags": 955,
"totalProjectTags": 4131,
"zeroTags": 90,
"missingNameEnAscii": null
},
"sqlPath": ".sisyphus/sql/tag-cleanup-2026-02-21.sql"
}
@@ -0,0 +1,42 @@
{
"after": {
"totalTags": 859,
"totalProjectTags": 4131,
"zeroTags": 0,
"missingNameEn": 17,
"missingNameEnAscii": 0
},
"deletedSourceTagsStillExists": [],
"mergedTargetProjectCounts": [
{
"id": "cmkshubus0004la04zjuh5nn5",
"name": "自主智能体",
"projects": 6
},
{
"id": "cmkryxmob0003l80414uo5zqz",
"name": "CLI工具",
"projects": 33
},
{
"id": "cmkqlcukz000dl5044c6z8542",
"name": "多智能体系统",
"projects": 28
},
{
"id": "cmkqimx520005jx04vyegybp9",
"name": "AI开发工具",
"projects": 213
},
{
"id": "cmks0yz0h002xl1040w65w47d",
"name": "AI代理",
"projects": 52
},
{
"id": "cmksdlj9o0029jv04x9tdyc11",
"name": "科研工具",
"projects": 4
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,132 @@
{
"executedAt": "2026-02-21T04:05:08.555Z",
"totalTags": 83,
"totalProjectTags": 1547,
"zeroTags": 0,
"totalProjects": 454,
"projectsWithTags": 454,
"projectsWithoutTags": 0,
"aiTagExists": false,
"removedNoise": [
"开源",
"学术资源",
"复杂部署",
"用户通知",
"需要配置",
"低学习成本",
"云端服务",
"MIT许可",
"Apache-2.0",
"教程",
"企业级",
"需要基础",
"多语言支持",
"AI研究资源",
"跨平台",
"自托管",
"学术研究"
],
"removedNoiseStillExists": [],
"topTags": [
{
"name": "Python",
"nameEn": "Python",
"projectCount": 268
},
{
"name": "AI开发工具",
"nameEn": "AI Development Tool",
"projectCount": 215
},
{
"name": "TypeScript",
"nameEn": "TypeScript",
"projectCount": 118
},
{
"name": "AI代理",
"nameEn": "AI Agents",
"projectCount": 54
},
{
"name": "自动化",
"nameEn": "Automation",
"projectCount": 47
},
{
"name": "Docker",
"nameEn": "Docker",
"projectCount": 43
},
{
"name": "Node.js",
"nameEn": "Node.js",
"projectCount": 40
},
{
"name": "JavaScript",
"nameEn": "JavaScript",
"projectCount": 34
},
{
"name": "React",
"nameEn": "React",
"projectCount": 34
},
{
"name": "PyTorch",
"nameEn": "PyTorch",
"projectCount": 30
},
{
"name": "LangChain",
"nameEn": "LangChain",
"projectCount": 29
},
{
"name": "多智能体系统",
"nameEn": "Multi-Agent System",
"projectCount": 28
},
{
"name": "大语言模型",
"nameEn": null,
"projectCount": 25
},
{
"name": "LLM",
"nameEn": "LLM",
"projectCount": 22
},
{
"name": "Go",
"nameEn": "Go",
"projectCount": 20
},
{
"name": "Library",
"nameEn": "Library",
"projectCount": 18
},
{
"name": "MCP",
"nameEn": "Model Context Protocol",
"projectCount": 18
},
{
"name": "RAG",
"nameEn": "RAG",
"projectCount": 17
},
{
"name": "Next.js",
"nameEn": "Next.js",
"projectCount": 17
},
{
"name": "Markdown",
"nameEn": "Markdown",
"projectCount": 17
}
]
}
@@ -0,0 +1,127 @@
{
"executedAt": "2026-02-21T04:03:10.320Z",
"removedNames": [
"自托管",
"跨平台",
"企业级",
"需要基础",
"AI研究资源",
"多语言支持",
"教程",
"Apache-2.0",
"学术研究",
"MIT许可",
"云端服务",
"低学习成本"
],
"removedStillExists": [],
"totalTags": 88,
"totalProjectTags": 1754,
"zeroTags": 0,
"totalProjects": 454,
"projectsWithTags": 454,
"projectsWithoutTags": 0,
"aiTagExists": false,
"topTags": [
{
"name": "Python",
"nameEn": "Python",
"projectCount": 268
},
{
"name": "AI开发工具",
"nameEn": "AI Development Tool",
"projectCount": 213
},
{
"name": "需要配置",
"nameEn": "Requires Configuration",
"projectCount": 163
},
{
"name": "TypeScript",
"nameEn": "TypeScript",
"projectCount": 118
},
{
"name": "AI代理",
"nameEn": "AI Agents",
"projectCount": 52
},
{
"name": "自动化",
"nameEn": "Automation",
"projectCount": 46
},
{
"name": "Docker",
"nameEn": "Docker",
"projectCount": 43
},
{
"name": "Node.js",
"nameEn": "Node.js",
"projectCount": 40
},
{
"name": "JavaScript",
"nameEn": "JavaScript",
"projectCount": 34
},
{
"name": "React",
"nameEn": "React",
"projectCount": 34
},
{
"name": "开源",
"nameEn": "Open Source",
"projectCount": 33
},
{
"name": "PyTorch",
"nameEn": "PyTorch",
"projectCount": 30
},
{
"name": "LangChain",
"nameEn": "LangChain",
"projectCount": 29
},
{
"name": "多智能体系统",
"nameEn": "Multi-Agent System",
"projectCount": 28
},
{
"name": "大语言模型",
"nameEn": null,
"projectCount": 25
},
{
"name": "LLM",
"nameEn": "LLM",
"projectCount": 22
},
{
"name": "Go",
"nameEn": "Go",
"projectCount": 20
},
{
"name": "MCP",
"nameEn": "Model Context Protocol",
"projectCount": 18
},
{
"name": "Library",
"nameEn": "Library",
"projectCount": 18
},
{
"name": "RAG",
"nameEn": "RAG",
"projectCount": 17
}
]
}
+211
View File
@@ -0,0 +1,211 @@
BEGIN;
DELETE FROM "project_tags"
WHERE "tagId" NOT IN (
'cmk9fpg4h0000kt048l3a2nqu',
'cmkjjb37v00064j12b84dsrwg',
'cmkqimx520005jx04vyegybp9',
'cmks16lb0003ml104acf4zi7e',
'cmks0yz0h002xl1040w65w47d',
'cmkqjgpzq0002l104qknt9pho',
'cmkql448t0002l504m7bkovze',
'cmkqm1bzv0000jl04c3ks3w7w',
'cmkoutfvz0005jr04knetubim',
'cmks0rbk1002jl104uc7yg2xv',
'cmks3iu8m006wl104ndk8no10',
'cmkqk1dc5000ojr04jq0exq1z',
'cmkqimwxl0000jx04l8q0rdzo',
'cmks0yz1e002yl104nw4apfcq',
'cmkqjvuo5000ejr04y9ksio48',
'cmkjjb37v00074j122wpemyas',
'cmkqnvcbg0001jo04us31pcaj',
'cmkrzlq020001l104sosii49r',
'cmks1aoyn003yl104r4zspprz',
'cmks5ey0j001el204vjk69nwg',
'cmkshrboj0000la04h6bmugvj',
'cmkscgc0z0007jv04nwmzbn8c',
'cmkscxy490012jv04rf4k4h4h',
'cmkuxtw0m002ojm04pl9qazxs',
'cmkqklwxm0018jr04zyzlrmlv',
'cmkqk1dbi000ljr045f2rm18m',
'cmkrv2jpg0000l204swrx5xh4',
'cmkqlcukz000dl5044c6z8542',
'cmkqm1c6j0003jl04clkn9qyb',
'cmkqnn8hj0006l504j2uedblz',
'cmkrx8euz0007ky04hrgq5ryg',
'cmkqklwr20016jr04opezrtpo',
'cmks003y10015l104xruieqo9',
'cmks0c9ll0022l10436svssah',
'cmkqnn8gl0005l5042jdeuuur',
'cmkrx664z0000ky0481j124cq',
'cmkqjvuir0008jr048591dzwp',
'cmkjjb38l000c4j12oucpveph',
'cmkqnn8mm0007l5045q6z4zb0',
'cmkrzvdfk000tl104jsq77zig',
'cmks3d1wy006ll104gltxc3et',
'cmks1aoxo003xl104gap1drce',
'cmksczul60015jv04ft4n1cb2',
'cmks0m380002gl104o6f9cjtj',
'cmkrx8f0j000aky046uomv3rs',
'cmkqjs7e50004jr042mrpi3ac',
'cmkqk4i0d000vjr04ai18b8h3',
'cmkqk4i0g000wjr04t82lzvaq',
'cmkrzlq0q0003l10453qhsniu',
'cmks1ubzb004pl104mvs08pmu',
'cmks0eqvq0028l10426ogrvkb',
'cmks13z53003el104re4468xt',
'cmks05ci1001gl104lus7os2q',
'cmks1s7p2004il104kpwprttq',
'cmkqjs7dw0003jr04uy2bguck',
'cmkqklwxr0019jr048wp0hds8',
'cmkqjs77s0000jr041cypbjkn',
'cmkry59tc0001jy04kr21i343',
'cmk9fpg5f0003kt04fcgbvj7y',
'cmkrzxk4i0011l1045cubzwwr',
'cmks13z46003dl1047ba0entr',
'cmkry59u70003jy049eoxzkc7',
'cmks2a6r1005il104ormxbqud',
'cmks112wx0039l104zltcl2hp',
'cmkrzvdfe000sl104q0gge67f',
'cmks3w0g0000bl904u4lmk5d5',
'cmkt8m55q000nla04kgfld9x3',
'cmkqklwxj0017jr04czubkbbi',
'cmkshubus0004la04zjuh5nn5',
'cmkqk4i0d000ujr045d2njmc7',
'cmks02o6m001al104gi1jztpn',
'cmks4roac000el204paypouzl',
'cmkt7yhut0000la040ij92vgk',
'cmks5rr2e0020l204sn9ydvka',
'cmkjjb37z00084j121651lnjl',
'cmksg1wc90061jv04114izr2d',
'cmkuulu5d000jjc04u7hx67sz',
'cmkqimx3n0001jx043mvrmarx',
'cmksf0ik1004ijv047hiusscw',
'cmktoexz8001wld046f13barg',
'cmks2gqie005ol104v4d6nvck',
'cmks0equs0026l104sl4td34d',
'cmkjjb38j000b4j125a3fg282',
'cmktkj5to001hl704wtmqjft4',
'cmkrzxk4a0010l104u7irj9je',
'cmkto2g8o0019ld04b61pgj4y',
'cmktk5hid000sl7049a2bo5al',
'cmkv6j7uq000gk4043x1gfnp1',
'cmkti8i470000kz04bpsajkzk',
'cmks47ct90013l904x03rupwb',
'cmkql9bho0007l50406oflxr0',
'cmks112qf0034l104gkihwp10',
'cmks1s7ut004jl104gvlybgfs',
'cmks4pb54000bl204erpb9bbp',
'cmksfnrz1005qjv044pnraikr',
'cmkqimx480003jx04bcqob7pb',
'cmkqm1c690001jl04ge5elv46',
'cmks2a6l3005gl1040u4s0k4s',
'cmkth0k4x0026ju04t0euxxwk',
'cmkqnj1pt0001l504vmrywqeg'
);
DELETE FROM "tags"
WHERE "id" NOT IN (
'cmk9fpg4h0000kt048l3a2nqu',
'cmkjjb37v00064j12b84dsrwg',
'cmkqimx520005jx04vyegybp9',
'cmks16lb0003ml104acf4zi7e',
'cmks0yz0h002xl1040w65w47d',
'cmkqjgpzq0002l104qknt9pho',
'cmkql448t0002l504m7bkovze',
'cmkqm1bzv0000jl04c3ks3w7w',
'cmkoutfvz0005jr04knetubim',
'cmks0rbk1002jl104uc7yg2xv',
'cmks3iu8m006wl104ndk8no10',
'cmkqk1dc5000ojr04jq0exq1z',
'cmkqimwxl0000jx04l8q0rdzo',
'cmks0yz1e002yl104nw4apfcq',
'cmkqjvuo5000ejr04y9ksio48',
'cmkjjb37v00074j122wpemyas',
'cmkqnvcbg0001jo04us31pcaj',
'cmkrzlq020001l104sosii49r',
'cmks1aoyn003yl104r4zspprz',
'cmks5ey0j001el204vjk69nwg',
'cmkshrboj0000la04h6bmugvj',
'cmkscgc0z0007jv04nwmzbn8c',
'cmkscxy490012jv04rf4k4h4h',
'cmkuxtw0m002ojm04pl9qazxs',
'cmkqklwxm0018jr04zyzlrmlv',
'cmkqk1dbi000ljr045f2rm18m',
'cmkrv2jpg0000l204swrx5xh4',
'cmkqlcukz000dl5044c6z8542',
'cmkqm1c6j0003jl04clkn9qyb',
'cmkqnn8hj0006l504j2uedblz',
'cmkrx8euz0007ky04hrgq5ryg',
'cmkqklwr20016jr04opezrtpo',
'cmks003y10015l104xruieqo9',
'cmks0c9ll0022l10436svssah',
'cmkqnn8gl0005l5042jdeuuur',
'cmkrx664z0000ky0481j124cq',
'cmkqjvuir0008jr048591dzwp',
'cmkjjb38l000c4j12oucpveph',
'cmkqnn8mm0007l5045q6z4zb0',
'cmkrzvdfk000tl104jsq77zig',
'cmks3d1wy006ll104gltxc3et',
'cmks1aoxo003xl104gap1drce',
'cmksczul60015jv04ft4n1cb2',
'cmks0m380002gl104o6f9cjtj',
'cmkrx8f0j000aky046uomv3rs',
'cmkqjs7e50004jr042mrpi3ac',
'cmkqk4i0d000vjr04ai18b8h3',
'cmkqk4i0g000wjr04t82lzvaq',
'cmkrzlq0q0003l10453qhsniu',
'cmks1ubzb004pl104mvs08pmu',
'cmks0eqvq0028l10426ogrvkb',
'cmks13z53003el104re4468xt',
'cmks05ci1001gl104lus7os2q',
'cmks1s7p2004il104kpwprttq',
'cmkqjs7dw0003jr04uy2bguck',
'cmkqklwxr0019jr048wp0hds8',
'cmkqjs77s0000jr041cypbjkn',
'cmkry59tc0001jy04kr21i343',
'cmk9fpg5f0003kt04fcgbvj7y',
'cmkrzxk4i0011l1045cubzwwr',
'cmks13z46003dl1047ba0entr',
'cmkry59u70003jy049eoxzkc7',
'cmks2a6r1005il104ormxbqud',
'cmks112wx0039l104zltcl2hp',
'cmkrzvdfe000sl104q0gge67f',
'cmks3w0g0000bl904u4lmk5d5',
'cmkt8m55q000nla04kgfld9x3',
'cmkqklwxj0017jr04czubkbbi',
'cmkshubus0004la04zjuh5nn5',
'cmkqk4i0d000ujr045d2njmc7',
'cmks02o6m001al104gi1jztpn',
'cmks4roac000el204paypouzl',
'cmkt7yhut0000la040ij92vgk',
'cmks5rr2e0020l204sn9ydvka',
'cmkjjb37z00084j121651lnjl',
'cmksg1wc90061jv04114izr2d',
'cmkuulu5d000jjc04u7hx67sz',
'cmkqimx3n0001jx043mvrmarx',
'cmksf0ik1004ijv047hiusscw',
'cmktoexz8001wld046f13barg',
'cmks2gqie005ol104v4d6nvck',
'cmks0equs0026l104sl4td34d',
'cmkjjb38j000b4j125a3fg282',
'cmktkj5to001hl704wtmqjft4',
'cmkrzxk4a0010l104u7irj9je',
'cmkto2g8o0019ld04b61pgj4y',
'cmktk5hid000sl7049a2bo5al',
'cmkv6j7uq000gk4043x1gfnp1',
'cmkti8i470000kz04bpsajkzk',
'cmks47ct90013l904x03rupwb',
'cmkql9bho0007l50406oflxr0',
'cmks112qf0034l104gkihwp10',
'cmks1s7ut004jl104gvlybgfs',
'cmks4pb54000bl204erpb9bbp',
'cmksfnrz1005qjv044pnraikr',
'cmkqimx480003jx04bcqob7pb',
'cmkqm1c690001jl04ge5elv46',
'cmks2a6l3005gl1040u4s0k4s',
'cmkth0k4x0026ju04t0euxxwk',
'cmkqnj1pt0001l504vmrywqeg'
);
COMMIT;
+46
View File
@@ -0,0 +1,46 @@
BEGIN;
-- Step 1: Merge duplicate tags by high-confidence canonicalized nameEn groups
WITH merge_map(source_id, target_id) AS (
VALUES
('cmkoutg0o0006jr04goozjhft', 'cmkqimx520005jx04vyegybp9'),
('cmkjjb387000a4j12pdf123zn', 'cmks0yz0h002xl1040w65w47d'),
('cmkun0ao70048jx04atm2lwwq', 'cmkryxmob0003l80414uo5zqz'),
('cmkrzqdlu000fl104qmy9gu2u', 'cmkqlcukz000dl5044c6z8542'),
('cmktk3biu000nl704iiohmv4i', 'cmksdlj9o0029jv04x9tdyc11'),
('cmktpotkp003eld04mv09ckde', 'cmkshubus0004la04zjuh5nn5')
)
INSERT INTO "project_tags" ("projectId", "tagId")
SELECT pt."projectId", mm.target_id
FROM "project_tags" pt
JOIN merge_map mm ON mm.source_id = pt."tagId"
ON CONFLICT ("projectId", "tagId") DO NOTHING;
WITH merge_map(source_id, target_id) AS (
VALUES
('cmkoutg0o0006jr04goozjhft', 'cmkqimx520005jx04vyegybp9'),
('cmkjjb387000a4j12pdf123zn', 'cmks0yz0h002xl1040w65w47d'),
('cmkun0ao70048jx04atm2lwwq', 'cmkryxmob0003l80414uo5zqz'),
('cmkrzqdlu000fl104qmy9gu2u', 'cmkqlcukz000dl5044c6z8542'),
('cmktk3biu000nl704iiohmv4i', 'cmksdlj9o0029jv04x9tdyc11'),
('cmktpotkp003eld04mv09ckde', 'cmkshubus0004la04zjuh5nn5')
)
DELETE FROM "tags" t
USING merge_map mm
WHERE t."id" = mm.source_id;
-- Step 2: Fill missing English names for ASCII-only tags
UPDATE "tags"
SET "nameEn" = "name"
WHERE ("nameEn" IS NULL OR "nameEn" = '')
AND "name" ~ '^[[:ascii:]]+$';
-- Step 3: Delete tags no longer referenced by any project
DELETE FROM "tags" t
WHERE NOT EXISTS (
SELECT 1
FROM "project_tags" pt
WHERE pt."tagId" = t."id"
);
COMMIT;
@@ -0,0 +1,76 @@
BEGIN;
WITH noise AS (
SELECT id
FROM tags
WHERE name IN (
'开源',
'学术资源',
'复杂部署',
'用户通知',
'需要配置'
)
),
projects_without_meaningful_tags AS (
SELECT p.id, p.name
FROM projects p
LEFT JOIN project_tags pt ON pt."projectId" = p.id
LEFT JOIN noise n ON n.id = pt."tagId"
GROUP BY p.id, p.name
HAVING COUNT(pt."tagId") > 0 AND COUNT(pt."tagId") = COUNT(n.id)
),
fallback_tags AS (
SELECT
(SELECT id FROM tags WHERE name = 'AI研究' LIMIT 1) AS research_id,
(SELECT id FROM tags WHERE name = 'AI 助手' LIMIT 1) AS assistant_id,
(SELECT id FROM tags WHERE name = 'AI代理' LIMIT 1) AS agent_id,
(SELECT id FROM tags WHERE name = '自动化' LIMIT 1) AS automation_id,
(SELECT id FROM tags WHERE name = 'AI开发工具' LIMIT 1) AS tool_id
),
assignments AS (
SELECT
p.id AS project_id,
CASE
WHEN p.name ~* '(论文|研究|paper|survey)' THEN (SELECT research_id FROM fallback_tags)
WHEN p.name ~* '(助手|assistant)' THEN (SELECT assistant_id FROM fallback_tags)
WHEN p.name ~* '(框架|agent|代理|智能体)' THEN (SELECT agent_id FROM fallback_tags)
WHEN p.name ~* '(通知|alert|quota|配额)' THEN (SELECT automation_id FROM fallback_tags)
ELSE (SELECT tool_id FROM fallback_tags)
END AS tag_id
FROM projects_without_meaningful_tags p
)
INSERT INTO project_tags ("projectId", "tagId")
SELECT project_id, tag_id
FROM assignments
WHERE tag_id IS NOT NULL
ON CONFLICT ("projectId", "tagId") DO NOTHING;
WITH noise AS (
SELECT id
FROM tags
WHERE name IN (
'开源',
'学术资源',
'复杂部署',
'用户通知',
'需要配置'
)
)
DELETE FROM project_tags
WHERE "tagId" IN (SELECT id FROM noise);
WITH noise AS (
SELECT id
FROM tags
WHERE name IN (
'开源',
'学术资源',
'复杂部署',
'用户通知',
'需要配置'
)
)
DELETE FROM tags
WHERE id IN (SELECT id FROM noise);
COMMIT;
@@ -0,0 +1,43 @@
BEGIN;
WITH target_tags AS (
SELECT id FROM tags
WHERE name IN (
'自托管',
'跨平台',
'企业级',
'需要基础',
'AI研究资源',
'多语言支持',
'教程',
'Apache-2.0',
'学术研究',
'MIT许可',
'云端服务',
'低学习成本'
)
)
DELETE FROM project_tags
WHERE "tagId" IN (SELECT id FROM target_tags);
WITH target_tags AS (
SELECT id FROM tags
WHERE name IN (
'自托管',
'跨平台',
'企业级',
'需要基础',
'AI研究资源',
'多语言支持',
'教程',
'Apache-2.0',
'学术研究',
'MIT许可',
'云端服务',
'低学习成本'
)
)
DELETE FROM tags
WHERE id IN (SELECT id FROM target_tags);
COMMIT;
+25 -24
View File
@@ -69,48 +69,49 @@
### 功能 ### 功能
每日自动拉取标签,利用 LLM 生成 `merges + updates` 计划,并调用维护 API 执行标签合并 每日自动拉取标签,按分桶分块交给 AI Agent 处理,并通过结构化输出解析器强制 JSON 格式,最终调用维护 API 执行合并与 `nameEn` 补全
### 执行流程 ### 执行流程
1. **Schedule Trigger**: 每日 UTC 03:00 触发 1. **Schedule Trigger**: 每日 UTC 03:00 触发
2. **Fetch Tags**: 调用 `GET /api/tags` 获取标签和项目计数 2. **Fetch Tags**: 调用 `GET /api/tags` 获取标签和项目计数
3. **Preprocess Tags**: 预处理排序(按 projectCount 3. **Prepare Chunk Batches**: 预处理并按 bucket + chunk 拆分(默认每块 80
4. **AI Analyze Tags**: 生成严格 JSON 计划(`merges``updates` 4. **Tag Merge Agent + Structured Output Parser**: Agent 生成计划,Parser 强制输出 schema
5. **Validate JSON**: 校验输出结构和自合并风险 5. **Validate Plan**: 校验覆盖率(`reviewedTagIds` 必须覆盖整块)、ID 合法性、自合并
6. **Execute Maintenance**: 调用 `POST /api/tags/maintenance` 执行 6. **Retry Loop**: 校验失败会自动重试(默认 2 次,可配置)
7. **Summarize Results**: 输出日报(merged/deleted/updated 7. **Execute Maintenance**: 仅对校验通过的块调用 `POST /api/tags/maintenance`
8. **Summarize Results**: 汇总 executed/noop/failed_validation/failed_execute
### API 端点 ### API 端点
- `GET {API_URL}/api/tags` - 拉取标签列表(含 `_count.projects` - `GET {API_URL}/api/tags` - 拉取标签列表(含 `_count.projects`
- `POST {API_URL}/api/tags/maintenance` - 执行批量 updates/merges - `POST {API_URL}/api/tags/maintenance` - 执行批量 updates/merges
### 调试 ### 环境变量
检查每个节点的输出,确保数据格式正确 在 n8n 进程环境中配置
- **Calculate Quarter**: 应输出 `quarter`, `startDate`, `endDate` | 变量名 | 说明 | 默认值 |
- **Extract Keywords**: 应输出 `word`, `trendScore`, `quarter` | --- | --- | --- |
- **Match Visual Rules**: 应添加 `visualConfig` 字段 | `SITE_BASE_URL` | 站点地址 | 无 |
- **Send to API**: 应返回 `success: true` | `WEBHOOK_API_KEY` | 维护 API 鉴权密钥 | 无 |
| `TAG_JANITOR_CHUNK_SIZE` | 每块标签数量 | `80` |
| `TAG_JANITOR_MAX_RETRIES` | 校验失败自动重试次数 | `2` |
### 注意事项 ### 注意事项
1. **Google Trends API 限制** 1. **不要全量一次喂模型**
- Google Trends 有请求频率限制 - 900+ 标签必须分块,否则大概率返回不完整。
- 建议在非高峰期执行工作流
2. **数据质量** 2. **结构化输出是硬约束**
- 当前工作流中的 `description``detailPoints` 是占位符 - `Structured Output Parser` 保证格式。
- 需要添加 AI 节点(如 OpenAI)来生成真实内容 - `Validate Plan` 仍会做二次校验,防止“格式正确但内容不完整”。
3. **时区处理** 3. **失败块不会误执行**
- 确保时区设置正确,以便准确计算季度末日期 - 校验不通过的块不会调用维护 API,会在 summary 中归类为失败并给出原因。
### 扩展建议 ### 扩展建议
- **AI 内容生成**:在 "Match Visual Rules" 后添加 OpenAI 节点,生成描述和要点 - `failed_*` 摘要接入 Slack/Telegram 告警
- **数据验证**:添加验证节点检查数据完整性 - `failed_validation` 块做二次人工审核队列
- **错误处理**:添加错误分支,记录失败的关键词 - 在预处理阶段先做字符串近似聚类,进一步减少 LLM token 消耗
- **通知**:工作流完成后发送通知(如 Slack、Email
+272 -50
View File
@@ -30,65 +30,112 @@
}, },
{ {
"parameters": { "parameters": {
"jsCode": "const response = $input.first().json;\nif (!response.success) {\n throw new Error('Failed to fetch tags: ' + JSON.stringify(response));\n}\n\nconst tags = response.tags;\nconst formatted = tags.map(t => ({\n id: t.id,\n name: t.name,\n nameEn: t.nameEn || '',\n projectCount: t._count?.projects || 0\n}));\n\n// Sort by project count descending\nformatted.sort((a, b) => b.projectCount - a.projectCount);\n\nreturn [{ json: { tags: formatted, total: formatted.length } }];" "jsCode": "const response = $input.first().json;\nif (!response.success) {\n throw new Error('Failed to fetch tags: ' + JSON.stringify(response));\n}\n\nconst rawTags = Array.isArray(response.tags) ? response.tags : [];\nconst chunkSize = Math.max(20, Math.min(120, Number.parseInt($env.TAG_JANITOR_CHUNK_SIZE || '80', 10) || 80));\n\nconst normalizedTags = rawTags\n .map((tag) => ({\n id: tag.id,\n name: String(tag.name || '').trim(),\n nameEn: String(tag.nameEn || '').trim(),\n projectCount: Number(tag?._count?.projects || 0),\n }))\n .filter((tag) => tag.id && tag.name);\n\nnormalizedTags.sort((a, b) => b.projectCount - a.projectCount || a.name.localeCompare(b.name));\n\nif (normalizedTags.length === 0) {\n return [{\n json: {\n chunkId: 'empty-0',\n bucketKey: 'empty',\n tags: [],\n totalInChunk: 0,\n totalTags: 0,\n retryCount: 0,\n retryFeedback: '',\n status: 'noop',\n shouldExecute: false\n }\n }];\n}\n\nconst bucketMap = new Map();\nfor (const tag of normalizedTags) {\n const seed = (tag.nameEn || tag.name).toLowerCase().trim();\n const normalized = seed.replace(/[^a-z0-9\\u4e00-\\u9fa5]+/g, '');\n\n let bucketKey = 'misc';\n if (normalized.length > 0) {\n const first = normalized[0];\n if (/[a-z0-9]/.test(first)) {\n bucketKey = 'en-' + first;\n } else if (/[\\u4e00-\\u9fa5]/.test(first)) {\n bucketKey = 'zh-' + first;\n }\n }\n\n const list = bucketMap.get(bucketKey) || [];\n list.push(tag);\n bucketMap.set(bucketKey, list);\n}\n\nconst chunks = [];\nfor (const [bucketKey, tags] of bucketMap.entries()) {\n tags.sort((a, b) => b.projectCount - a.projectCount || a.name.localeCompare(b.name));\n for (let i = 0; i < tags.length; i += chunkSize) {\n chunks.push({\n bucketKey,\n tags: tags.slice(i, i + chunkSize)\n });\n }\n}\n\nchunks.sort((a, b) => b.tags.length - a.tags.length);\n\nreturn chunks.map((chunk, idx) => ({\n json: {\n chunkId: chunk.bucketKey + '-' + (idx + 1),\n bucketKey: chunk.bucketKey,\n tags: chunk.tags,\n totalInChunk: chunk.tags.length,\n totalTags: normalizedTags.length,\n retryCount: 0,\n retryFeedback: ''\n }\n}));"
}, },
"id": "preprocess", "id": "prepare-chunks",
"name": "Preprocess Tags", "name": "Prepare Chunk Batches",
"type": "n8n-nodes-base.code", "type": "n8n-nodes-base.code",
"typeVersion": 2, "typeVersion": 2,
"position": [440, 0] "position": [460, 0]
}, },
{ {
"parameters": { "parameters": {
"model": "gpt-4o", "text": "=Chunk {{$json.chunkId}} from {{$json.bucketKey}}.\\nThis chunk contains {{$json.totalInChunk}} tags out of {{$json.totalTags}} total tags.\\nRetry count: {{$json.retryCount || 0}}.\\nRetry feedback: {{$json.retryFeedback || 'none'}}.\\n\\nOutput MUST be JSON and MUST follow the connected structured parser schema.\\nOnly use tag IDs from this chunk.\\n\\nChunk tags JSON:\\n{{JSON.stringify($json.tags)}}",
"messages": { "options": {
"values": [ "systemMessage": "You are a strict tag-governance agent.\\n\\nFor the current chunk only:\\n1) Find semantic duplicates and output merges\\n2) Fill missing or low-quality English names and output updates\\n3) Return reviewedTagIds that fully cover every tag ID in this chunk\\n4) Put uncertain tags into unresolvedTagIds\\n\\nHard rules:\\n- Never reference tag IDs outside the current chunk\\n- Prefer existing target ID with higher projectCount\\n- Do not self-merge\\n- Return no markdown, no prose, JSON only"
{ },
"role": "system", "promptType": "define",
"content": "You are a tag management expert. Analyze tags and identify semantic duplicates for merging and missing English names for completion. Output STRICT JSON only." "hasOutputParser": true
}, },
{ "id": "tag-merge-agent",
"role": "user", "name": "Tag Merge Agent",
"content": "Analyze these tags and identify:\n1. Semantic duplicates to merge (e.g., \"机器学习\" and \"ML\" → keep higher projectCount)\n2. Tags missing nameEn that need English names\n\nTags ({{$json.total}} total):\n{{JSON.stringify($json.tags)}}\n\nOutput STRICT JSON (no markdown):\n{\n \"merges\": [\n {\n \"target\": { \"name\": \"保留的标签\", \"nameEn\": \"Canonical Name\" },\n \"sourceTagIds\": [\"id1\", \"id2\"]\n }\n ],\n \"updates\": [\n { \"tagId\": \"id\", \"nameEn\": \"English Name\" }\n ]\n}\n\nRules:\n- Keep tag with higher projectCount as merge target\n- target can use { \"id\": \"existing\" } to keep existing tag\n- Only include tags needing action (empty arrays if none)\n- nameEn must be proper English, not pinyin" "type": "@n8n/n8n-nodes-langchain.agent",
} "typeVersion": 1.8,
] "position": [700, 0]
},
{
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-mini",
"cachedResultName": "gpt-4.1-mini"
}, },
"options": { "options": {
"temperature": 0.1 "temperature": 0.1
} }
}, },
"id": "ai-analyze", "id": "openai-model",
"name": "AI Analyze Tags", "name": "OpenAI Chat Model",
"type": "@n8n/n8n-nodes-langchain.openAi", "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.8, "typeVersion": 1.2,
"position": [660, 0] "position": [700, 220]
}, },
{ {
"parameters": { "parameters": {
"jsCode": "const response = $input.first().json;\nlet plan;\n\ntry {\n const content = response.message?.content || response.text || response;\n plan = typeof content === 'string' ? JSON.parse(content) : content;\n} catch (e) {\n throw new Error('Failed to parse AI response: ' + e.message);\n}\n\nif (!Array.isArray(plan.merges)) plan.merges = [];\nif (!Array.isArray(plan.updates)) plan.updates = [];\n\nfor (const merge of plan.merges) {\n if (merge.target.id && merge.sourceTagIds.includes(merge.target.id)) {\n throw new Error('Self-merge detected: ' + merge.target.id);\n }\n}\n\nif (plan.merges.length === 0 && plan.updates.length === 0) {\n return [{ json: { skipped: true, reason: 'No changes needed' } }];\n}\n\nreturn [{ json: plan }];" "schemaType": "manual",
"inputSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"reviewedTagIds\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"unresolvedTagIds\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"merges\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"target\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"id\": { \"type\": \"string\" }\n },\n \"required\": [\"id\"]\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"name\": { \"type\": \"string\" },\n \"nameEn\": { \"type\": \"string\" }\n },\n \"required\": [\"name\", \"nameEn\"]\n }\n ]\n },\n \"sourceTagIds\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"required\": [\"target\", \"sourceTagIds\"]\n }\n },\n \"updates\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"tagId\": { \"type\": \"string\" },\n \"nameEn\": { \"type\": \"string\" }\n },\n \"required\": [\"tagId\", \"nameEn\"]\n }\n }\n },\n \"required\": [\"reviewedTagIds\", \"unresolvedTagIds\", \"merges\", \"updates\"]\n}",
"autoFix": true
}, },
"id": "validate-json", "id": "structured-parser",
"name": "Validate JSON", "name": "Structured Output Parser",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"typeVersion": 1.2,
"position": [960, 220]
},
{
"parameters": {
"jsCode": "const maxRetries = Math.max(0, Number.parseInt($env.TAG_JANITOR_MAX_RETRIES || '2', 10) || 2);\n\nfunction normalizeIds(input) {\n if (!Array.isArray(input)) return [];\n return [...new Set(input.map((id) => String(id || '').trim()).filter(Boolean))];\n}\n\nfunction safeArray(input) {\n return Array.isArray(input) ? input : [];\n}\n\nreturn $input.all().map((item) => {\n const row = item.json || {};\n const parsed = row.output && typeof row.output === 'object' ? row.output : {};\n\n const tags = safeArray(row.tags);\n const chunkTagIds = normalizeIds(tags.map((tag) => tag.id));\n const chunkTagIdSet = new Set(chunkTagIds);\n\n const reviewedTagIds = normalizeIds(parsed.reviewedTagIds);\n const unresolvedTagIds = normalizeIds(parsed.unresolvedTagIds);\n\n const updates = safeArray(parsed.updates).flatMap((update) => {\n if (!update || typeof update !== 'object') return [];\n const tagId = String(update.tagId || '').trim();\n const nameEn = String(update.nameEn || '').trim();\n if (!tagId || !nameEn) return [];\n return [{ tagId, nameEn }];\n });\n\n const merges = safeArray(parsed.merges).flatMap((merge) => {\n if (!merge || typeof merge !== 'object') return [];\n const sourceTagIds = normalizeIds(merge.sourceTagIds);\n if (sourceTagIds.length === 0) return [];\n\n const target = merge.target;\n if (!target || typeof target !== 'object') return [];\n\n if (typeof target.id === 'string' && target.id.trim()) {\n return [{\n target: { id: target.id.trim() },\n sourceTagIds\n }];\n }\n\n const name = String(target.name || '').trim();\n const nameEn = String(target.nameEn || '').trim();\n if (!name || !nameEn) return [];\n\n return [{\n target: { name, nameEn },\n sourceTagIds\n }];\n });\n\n const errors = [];\n\n const reviewedSet = new Set(reviewedTagIds);\n const missingReviewed = chunkTagIds.filter((id) => !reviewedSet.has(id));\n if (missingReviewed.length > 0) {\n errors.push(`Missing reviewedTagIds coverage: ${missingReviewed.join(', ')}`);\n }\n\n for (const update of updates) {\n if (!chunkTagIdSet.has(update.tagId)) {\n errors.push(`Update references unknown tagId in chunk: ${update.tagId}`);\n }\n }\n\n for (const merge of merges) {\n for (const sourceTagId of merge.sourceTagIds) {\n if (!chunkTagIdSet.has(sourceTagId)) {\n errors.push(`Merge sourceTagId not in chunk: ${sourceTagId}`);\n }\n }\n\n if ('id' in merge.target) {\n if (!chunkTagIdSet.has(merge.target.id)) {\n errors.push(`Merge target.id not in chunk: ${merge.target.id}`);\n }\n if (merge.sourceTagIds.includes(merge.target.id)) {\n errors.push(`Self merge detected: ${merge.target.id}`);\n }\n }\n }\n\n const retryCount = Number(row.retryCount || 0);\n const hasValidationErrors = errors.length > 0;\n const needsRetry = hasValidationErrors && retryCount < maxRetries;\n const shouldExecute = !hasValidationErrors && (updates.length > 0 || merges.length > 0);\n\n let status = 'noop';\n if (needsRetry) status = 'retry';\n else if (hasValidationErrors) status = 'failed_validation';\n else if (shouldExecute) status = 'ready';\n\n return {\n json: {\n ...row,\n validated: {\n reviewedTagIds,\n unresolvedTagIds,\n updates,\n merges\n },\n validationErrors: errors,\n needsRetry,\n shouldExecute,\n status\n }\n };\n});"
},
"id": "validate-plan",
"name": "Validate Plan",
"type": "n8n-nodes-base.code", "type": "n8n-nodes-base.code",
"typeVersion": 2, "typeVersion": 2,
"position": [880, 0] "position": [960, 0]
}, },
{ {
"parameters": { "parameters": {
"conditions": { "conditions": {
"boolean": [ "boolean": [
{ {
"value1": "={{$json.skipped}}", "value1": "={{$json.needsRetry}}",
"value2": true "value2": true
} }
] ]
} }
}, },
"id": "check-skip", "id": "retry-needed",
"name": "Check Skip", "name": "Retry Needed?",
"type": "n8n-nodes-base.if", "type": "n8n-nodes-base.if",
"typeVersion": 2, "typeVersion": 2,
"position": [1100, 0] "position": [1180, 0]
},
{
"parameters": {
"jsCode": "return $input.all().map((item) => {\n const row = item.json || {};\n const nextRetryCount = Number(row.retryCount || 0) + 1;\n const retryFeedback = (row.validationErrors || []).join(' | ');\n\n return {\n json: {\n ...row,\n retryCount: nextRetryCount,\n retryFeedback\n }\n };\n});"
},
"id": "increment-retry",
"name": "Increment Retry",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [1400, -120]
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{$json.shouldExecute}}",
"value2": true
}
]
}
},
"id": "execute-needed",
"name": "Execute Needed?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [1400, 120]
}, },
{ {
"parameters": { "parameters": {
@@ -96,50 +143,225 @@
"url": "={{$env.SITE_BASE_URL}}/api/tags/maintenance", "url": "={{$env.SITE_BASE_URL}}/api/tags/maintenance",
"sendBody": true, "sendBody": true,
"specifyBody": "json", "specifyBody": "json",
"jsonBody": "={\n \"apiKey\": \"{{$env.WEBHOOK_API_KEY}}\",\n \"updates\": {{JSON.stringify($json.updates)}},\n \"merges\": {{JSON.stringify($json.merges)}}\n}", "jsonBody": "={\n \"apiKey\": \"{{$env.WEBHOOK_API_KEY}}\",\n \"updates\": {{JSON.stringify($json.validated.updates || [])}},\n \"merges\": {{JSON.stringify($json.validated.merges || [])}}\n}",
"options": {} "options": {}
}, },
"id": "execute-maintenance", "id": "execute-maintenance",
"name": "Execute Maintenance", "name": "Execute Maintenance",
"type": "n8n-nodes-base.httpRequest", "type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2, "typeVersion": 4.2,
"position": [1320, -100] "position": [1620, 20],
"continueOnFail": true
}, },
{ {
"parameters": { "parameters": {
"jsCode": "const maintenanceResult = $('Execute Maintenance').first()?.json || {};\nconst skipped = $('Check Skip').first()?.json?.skipped;\n\nif (skipped) {\n return [{ json: {\n status: 'skipped',\n message: 'No changes needed',\n timestamp: new Date().toISOString()\n }}];\n}\n\nconst result = maintenanceResult.result || {};\n\nreturn [{ json: {\n status: maintenanceResult.success ? 'success' : 'failed',\n updatedCount: result.updatedCount || 0,\n mergedCount: result.mergedCount || 0,\n deletedTagCount: result.deletedTagCount || 0,\n timestamp: new Date().toISOString(),\n error: maintenanceResult.error || null\n}}];" "jsCode": "return $input.all().map((item) => {\n const row = item.json || {};\n return {\n json: {\n status: row.status || 'noop',\n chunkId: row.chunkId || null,\n retryCount: Number(row.retryCount || 0),\n validationErrors: row.validationErrors || [],\n updatedCount: 0,\n mergedCount: 0,\n deletedTagCount: 0\n }\n };\n});"
}, },
"id": "summarize", "id": "build-skip-result",
"name": "Build Skipped Result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [1620, 220]
},
{
"parameters": {
"jsCode": "return $input.all().map((item) => {\n const row = item.json || {};\n\n if (row.success === true) {\n return {\n json: {\n status: 'executed',\n chunkId: null,\n retryCount: null,\n validationErrors: [],\n updatedCount: Number(row?.result?.updatedCount || 0),\n mergedCount: Number(row?.result?.mergedCount || 0),\n deletedTagCount: Number(row?.result?.deletedTagCount || 0)\n }\n };\n }\n\n const message = row?.error?.message || row?.message || row?.error || 'Unknown execution error';\n\n return {\n json: {\n status: 'failed_execute',\n chunkId: null,\n retryCount: null,\n validationErrors: [String(message)],\n updatedCount: 0,\n mergedCount: 0,\n deletedTagCount: 0\n }\n };\n});"
},
"id": "normalize-execution",
"name": "Normalize Execution Result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [1840, 20]
},
{
"parameters": {
"mode": "append"
},
"id": "merge-results",
"name": "Merge Results",
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"position": [2060, 120]
},
{
"parameters": {
"jsCode": "const rows = $input.all().map((item) => item.json || {});\n\nconst summary = {\n status: 'success',\n chunkRuns: rows.length,\n executedRuns: 0,\n noopRuns: 0,\n retryRuns: 0,\n failedValidationRuns: 0,\n failedExecuteRuns: 0,\n updatedCount: 0,\n mergedCount: 0,\n deletedTagCount: 0,\n failures: [],\n timestamp: new Date().toISOString()\n};\n\nfor (const row of rows) {\n if (row.status === 'executed') {\n summary.executedRuns += 1;\n summary.updatedCount += Number(row.updatedCount || 0);\n summary.mergedCount += Number(row.mergedCount || 0);\n summary.deletedTagCount += Number(row.deletedTagCount || 0);\n continue;\n }\n\n if (row.status === 'noop') {\n summary.noopRuns += 1;\n continue;\n }\n\n if (row.status === 'retry') {\n summary.retryRuns += 1;\n continue;\n }\n\n if (row.status === 'failed_validation') {\n summary.failedValidationRuns += 1;\n summary.failures.push({\n type: 'failed_validation',\n chunkId: row.chunkId || null,\n reasons: row.validationErrors || []\n });\n continue;\n }\n\n if (row.status === 'failed_execute') {\n summary.failedExecuteRuns += 1;\n summary.failures.push({\n type: 'failed_execute',\n chunkId: row.chunkId || null,\n reasons: row.validationErrors || []\n });\n }\n}\n\nif (summary.failedValidationRuns > 0 || summary.failedExecuteRuns > 0) {\n summary.status = 'partial_failed';\n}\n\nsummary.failures = summary.failures.slice(0, 20);\n\nreturn [{ json: summary }];"
},
"id": "summarize-results",
"name": "Summarize Results", "name": "Summarize Results",
"type": "n8n-nodes-base.code", "type": "n8n-nodes-base.code",
"typeVersion": 2, "typeVersion": 2,
"position": [1540, 0] "position": [2280, 120]
} }
], ],
"connections": { "connections": {
"Daily 3AM UTC": { "Daily 3AM UTC": {
"main": [[{ "node": "Fetch Tags", "type": "main", "index": 0 }]] "main": [
[
{
"node": "Fetch Tags",
"type": "main",
"index": 0
}
]
]
}, },
"Fetch Tags": { "Fetch Tags": {
"main": [[{ "node": "Preprocess Tags", "type": "main", "index": 0 }]]
},
"Preprocess Tags": {
"main": [[{ "node": "AI Analyze Tags", "type": "main", "index": 0 }]]
},
"AI Analyze Tags": {
"main": [[{ "node": "Validate JSON", "type": "main", "index": 0 }]]
},
"Validate JSON": {
"main": [[{ "node": "Check Skip", "type": "main", "index": 0 }]]
},
"Check Skip": {
"main": [ "main": [
[{ "node": "Execute Maintenance", "type": "main", "index": 0 }], [
[{ "node": "Summarize Results", "type": "main", "index": 0 }] {
"node": "Prepare Chunk Batches",
"type": "main",
"index": 0
}
]
]
},
"Prepare Chunk Batches": {
"main": [
[
{
"node": "Tag Merge Agent",
"type": "main",
"index": 0
}
]
]
},
"OpenAI Chat Model": {
"ai_languageModel": [
[
{
"node": "Tag Merge Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Structured Output Parser": {
"ai_outputParser": [
[
{
"node": "Tag Merge Agent",
"type": "ai_outputParser",
"index": 0
}
]
]
},
"Tag Merge Agent": {
"main": [
[
{
"node": "Validate Plan",
"type": "main",
"index": 0
}
]
]
},
"Validate Plan": {
"main": [
[
{
"node": "Retry Needed?",
"type": "main",
"index": 0
}
]
]
},
"Retry Needed?": {
"main": [
[
{
"node": "Increment Retry",
"type": "main",
"index": 0
}
],
[
{
"node": "Execute Needed?",
"type": "main",
"index": 0
}
]
]
},
"Increment Retry": {
"main": [
[
{
"node": "Tag Merge Agent",
"type": "main",
"index": 0
}
]
]
},
"Execute Needed?": {
"main": [
[
{
"node": "Execute Maintenance",
"type": "main",
"index": 0
}
],
[
{
"node": "Build Skipped Result",
"type": "main",
"index": 0
}
]
] ]
}, },
"Execute Maintenance": { "Execute Maintenance": {
"main": [[{ "node": "Summarize Results", "type": "main", "index": 0 }]] "main": [
[
{
"node": "Normalize Execution Result",
"type": "main",
"index": 0
}
]
]
},
"Normalize Execution Result": {
"main": [
[
{
"node": "Merge Results",
"type": "main",
"index": 0
}
]
]
},
"Build Skipped Result": {
"main": [
[
{
"node": "Merge Results",
"type": "main",
"index": 1
}
]
]
},
"Merge Results": {
"main": [
[
{
"node": "Summarize Results",
"type": "main",
"index": 0
}
]
]
} }
}, },
"settings": { "settings": {
+305 -30
View File
@@ -1,6 +1,166 @@
import { prisma } from '@/lib/prisma' import { prisma } from '@/lib/prisma'
import type { ProjectInput } from '@/lib/validations' import type { ProjectInput } from '@/lib/validations'
import { generateSlug } from '@/lib/slug' import { generateSlug } from '@/lib/slug'
import type { Prisma } from '@prisma/client'
const PLURAL_NORMALIZATION_MAP = new Map([
['agents', 'agent'],
['assistants', 'assistant'],
['tools', 'tool'],
['frameworks', 'framework'],
['models', 'model'],
['servers', 'server'],
['clients', 'client'],
['workflows', 'workflow'],
['plugins', 'plugin'],
['libraries', 'library'],
['datasets', 'dataset'],
['platforms', 'platform'],
['systems', 'system'],
['repositories', 'repository'],
['engines', 'engine'],
])
const NOISE_TAG_KEYS = new Set([
'ai',
'artificial intelligence',
'open source',
'requires configuration',
'requires basics',
'low learning curve',
'enterprise',
'complex deployment',
'cloud service',
'cross platform',
'tutorial',
'academic research',
'academic resource',
'ai research resource',
'multi language support',
'self hosted',
'user notification',
'mit license',
'apache 2 0',
'开源',
'需要配置',
'需要基础',
'低学习成本',
'企业级',
'复杂部署',
'云端服务',
'跨平台',
'教程',
'学术研究',
'学术资源',
'多语言支持',
'自托管',
'用户通知',
'许可',
])
const TAG_FALLBACK = {
name: 'AI开发工具',
nameEn: 'AI Development Tool',
} as const
function normalizeWhitespace(value: string): string {
return value.replace(/\s+/g, ' ').trim()
}
function isAsciiText(value: string): boolean {
return /^[\x00-\x7f]+$/.test(value)
}
function canonicalizeTagKey(value: string): string {
const normalized = normalizeWhitespace(value)
.normalize('NFKC')
.toLowerCase()
.replace(/[+/_&|-]+/g, ' ')
.replace(/[^a-z0-9\u4e00-\u9fa5\s]/g, ' ')
.replace(/\s+/g, ' ')
.trim()
if (!normalized) {
return ''
}
return normalized
.split(' ')
.map((word) => PLURAL_NORMALIZATION_MAP.get(word) || word)
.join(' ')
.trim()
}
function normalizeIncomingTag(tag: ProjectInput['tags'][number]) {
const normalizedName = normalizeWhitespace(tag.name)
const normalizedNameEn = normalizeWhitespace(tag.nameEn || '')
const resolvedNameEn = normalizedNameEn || (isAsciiText(normalizedName) ? normalizedName : '')
const canonicalNameKey = canonicalizeTagKey(normalizedName)
const canonicalNameEnKey = canonicalizeTagKey(resolvedNameEn)
return {
name: normalizedName,
nameEn: resolvedNameEn || null,
canonicalNameKey,
canonicalNameEnKey,
slug: generateSlug(normalizedName, resolvedNameEn || null),
}
}
function isMeaningfulTag(tag: ReturnType<typeof normalizeIncomingTag>): boolean {
if (!tag.name) {
return false
}
if (NOISE_TAG_KEYS.has(tag.canonicalNameKey)) {
return false
}
if (tag.canonicalNameEnKey && NOISE_TAG_KEYS.has(tag.canonicalNameEnKey)) {
return false
}
return true
}
type TagWithProjectCount = Prisma.TagGetPayload<{
include: {
_count: { select: { projects: true } }
}
}>
function chooseBestTag(candidates: TagWithProjectCount[]): TagWithProjectCount {
return [...candidates].sort((a, b) => {
const projectDiff = b._count.projects - a._count.projects
if (projectDiff !== 0) {
return projectDiff
}
return a.createdAt.getTime() - b.createdAt.getTime()
})[0]!
}
async function getFallbackTag(): Promise<TagWithProjectCount> {
const fallbackSlug = generateSlug(TAG_FALLBACK.name, TAG_FALLBACK.nameEn)
const fallbackTag = await prisma.tag.upsert({
where: { slug: fallbackSlug },
update: {
name: TAG_FALLBACK.name,
nameEn: TAG_FALLBACK.nameEn,
},
create: {
name: TAG_FALLBACK.name,
nameEn: TAG_FALLBACK.nameEn,
slug: fallbackSlug,
},
include: {
_count: {
select: { projects: true },
},
},
})
return fallbackTag
}
/** /**
* 多级去重策略:查找已存在的项目 * 多级去重策略:查找已存在的项目
@@ -90,44 +250,159 @@ export async function findExistingProject(projectData: ProjectInput) {
* @returns 标签连接对象数组 * @returns 标签连接对象数组
*/ */
export async function upsertTags(tags: ProjectInput['tags']) { export async function upsertTags(tags: ProjectInput['tags']) {
// 优化:批量查询所有已存在的标签,避免 N+1 问题 const meaningfulTags = tags
const allTagNames = tags.map((t) => t.name) .map((tag) => normalizeIncomingTag(tag))
.filter((tag) => isMeaningfulTag(tag))
const normalizedIncomingTags = Array.from(
new Map(
meaningfulTags.map((normalized) => {
return [
normalized.canonicalNameEnKey || normalized.canonicalNameKey || normalized.name,
normalized,
] as const
})
).values()
)
if (normalizedIncomingTags.length === 0) {
return [await getFallbackTag()]
}
const existingTags = await prisma.tag.findMany({ const existingTags = await prisma.tag.findMany({
where: { name: { in: allTagNames } }, include: {
_count: {
select: { projects: true },
},
},
}) })
const existingTagNames = new Set(existingTags.map((t) => t.name))
// Upsert tags - 优化后只查询不存在的标签 const tagByExactName = new Map<string, TagWithProjectCount[]>()
return await Promise.all( const tagBySlug = new Map<string, TagWithProjectCount>()
tags.map(async (tag) => { const tagByCanonicalName = new Map<string, TagWithProjectCount[]>()
const tagSlug = generateSlug(tag.name, tag.nameEn) const tagByCanonicalNameEn = new Map<string, TagWithProjectCount[]>()
// 首先从批量查询结果中查找 for (const tag of existingTags) {
if (existingTagNames.has(tag.name)) { const exactNameKey = normalizeWhitespace(tag.name)
return existingTags.find((t) => t.name === tag.name)! const existingCanonicalName = canonicalizeTagKey(tag.name)
const existingCanonicalNameEn = canonicalizeTagKey(tag.nameEn || '')
if (!tagByExactName.has(exactNameKey)) {
tagByExactName.set(exactNameKey, [])
}
tagByExactName.get(exactNameKey)!.push(tag)
if (existingCanonicalName) {
if (!tagByCanonicalName.has(existingCanonicalName)) {
tagByCanonicalName.set(existingCanonicalName, [])
} }
tagByCanonicalName.get(existingCanonicalName)!.push(tag)
}
// 只有标签不存在时才尝试 upsert if (existingCanonicalNameEn) {
try { if (!tagByCanonicalNameEn.has(existingCanonicalNameEn)) {
return await prisma.tag.upsert({ tagByCanonicalNameEn.set(existingCanonicalNameEn, [])
where: { slug: tagSlug }, }
update: {}, tagByCanonicalNameEn.get(existingCanonicalNameEn)!.push(tag)
create: { }
name: tag.name,
nameEn: tag.nameEn || null, tagBySlug.set(tag.slug, tag)
slug: tagSlug, }
},
const resolvedTags: TagWithProjectCount[] = []
for (const incomingTag of normalizedIncomingTags) {
const exactNameCandidates = tagByExactName.get(incomingTag.name) || []
let matchedTag = exactNameCandidates.length > 0 ? chooseBestTag(exactNameCandidates) : null
if (!matchedTag && incomingTag.canonicalNameKey) {
const canonicalNameCandidates = tagByCanonicalName.get(incomingTag.canonicalNameKey) || []
if (canonicalNameCandidates.length > 0) {
matchedTag = chooseBestTag(canonicalNameCandidates)
}
}
if (!matchedTag && incomingTag.canonicalNameEnKey) {
const canonicalNameEnCandidates = tagByCanonicalNameEn.get(incomingTag.canonicalNameEnKey) || []
if (canonicalNameEnCandidates.length > 0) {
matchedTag = chooseBestTag(canonicalNameEnCandidates)
}
}
if (!matchedTag) {
matchedTag = tagBySlug.get(incomingTag.slug) || null
}
if (matchedTag) {
if (incomingTag.nameEn && !matchedTag.nameEn) {
await prisma.tag.update({
where: { id: matchedTag.id },
data: { nameEn: incomingTag.nameEn },
}) })
} catch (error) { matchedTag = {
// 如果 slug 冲突,查找并使用已存在的标签 ...matchedTag,
const existingBySlug = await prisma.tag.findUnique({ nameEn: incomingTag.nameEn,
where: { slug: tagSlug },
})
if (existingBySlug) {
return existingBySlug
} }
throw error
} }
})
resolvedTags.push(matchedTag)
continue
}
try {
const createdTag = await prisma.tag.create({
data: {
name: incomingTag.name,
nameEn: incomingTag.nameEn,
slug: incomingTag.slug,
},
include: {
_count: {
select: { projects: true },
},
},
})
resolvedTags.push(createdTag)
if (!tagByExactName.has(createdTag.name)) {
tagByExactName.set(createdTag.name, [])
}
tagByExactName.get(createdTag.name)!.push(createdTag)
tagBySlug.set(createdTag.slug, createdTag)
if (createdTag.nameEn) {
const canonicalNameEn = canonicalizeTagKey(createdTag.nameEn)
if (canonicalNameEn) {
if (!tagByCanonicalNameEn.has(canonicalNameEn)) {
tagByCanonicalNameEn.set(canonicalNameEn, [])
}
tagByCanonicalNameEn.get(canonicalNameEn)!.push(createdTag)
}
}
} catch {
const fallbackTag = await prisma.tag.findFirst({
where: {
OR: [
{ name: incomingTag.name },
{ slug: incomingTag.slug },
],
},
include: {
_count: {
select: { projects: true },
},
},
})
if (fallbackTag) {
resolvedTags.push(fallbackTag)
continue
}
throw new Error(`Failed to resolve tag: ${incomingTag.name}`)
}
}
return Array.from(
new Map(resolvedTags.map((tag) => [tag.id, tag])).values()
) )
} }
+3 -41
View File
@@ -8,7 +8,7 @@ import {
type WebhookPayload, type WebhookPayload,
type ProjectInput, type ProjectInput,
} from '@/lib/validations' } from '@/lib/validations'
import { findExistingProject } from '../../discovery/lib/discovery-service' import { findExistingProject, upsertTags } from '../../discovery/lib/discovery-service'
import { generateSlug } from '@/lib/slug' import { generateSlug } from '@/lib/slug'
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
@@ -90,46 +90,8 @@ export async function POST(request: NextRequest) {
// 多级去重:查找已存在的项目 // 多级去重:查找已存在的项目
const existingProject = await findExistingProject(validProject) const existingProject = await findExistingProject(validProject)
// 优化:批量查询所有已存在的标签,避免 N+1 问题 // 标签规范化与自动复用(防止同义标签持续膨胀)
const allTagNames = validProject.tags.map((t) => t.name) const tagConnections = await upsertTags(validProject.tags)
const existingTags = await prisma.tag.findMany({
where: { name: { in: allTagNames } },
})
const existingTagNames = new Set(existingTags.map((t) => t.name))
// Upsert tags - 优化后只查询不存在的标签
const tagConnections = await Promise.all(
validProject.tags.map(async (tag) => {
const tagSlug = generateSlug(tag.name, tag.nameEn)
// 首先从批量查询结果中查找
if (existingTagNames.has(tag.name)) {
return existingTags.find((t) => t.name === tag.name)!
}
// 只有标签不存在时才尝试 upsert
try {
return await prisma.tag.upsert({
where: { slug: tagSlug },
update: {},
create: {
name: tag.name,
nameEn: tag.nameEn || null,
slug: tagSlug,
},
})
} catch (error) {
// 如果 slug 冲突,查找并使用已存在的标签
const existingBySlug = await prisma.tag.findUnique({
where: { slug: tagSlug },
})
if (existingBySlug) {
return existingBySlug
}
throw error
}
})
)
// Generate slug for project // Generate slug for project
const slug = generateSlug(validProject.name, validProject.nameEn) const slug = generateSlug(validProject.name, validProject.nameEn)