gitinstall 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gitinstall/__init__.py +61 -0
- gitinstall/_sdk.py +541 -0
- gitinstall/academic.py +831 -0
- gitinstall/admin.html +327 -0
- gitinstall/auto_update.py +384 -0
- gitinstall/autopilot.py +349 -0
- gitinstall/badge.py +476 -0
- gitinstall/checkpoint.py +330 -0
- gitinstall/cicd.py +499 -0
- gitinstall/clawhub.html +718 -0
- gitinstall/config_schema.py +353 -0
- gitinstall/db.py +984 -0
- gitinstall/db_backend.py +445 -0
- gitinstall/dep_chain.py +337 -0
- gitinstall/dependency_audit.py +1153 -0
- gitinstall/detector.py +542 -0
- gitinstall/doctor.py +493 -0
- gitinstall/education.py +869 -0
- gitinstall/enterprise.py +802 -0
- gitinstall/error_fixer.py +953 -0
- gitinstall/event_bus.py +251 -0
- gitinstall/executor.py +577 -0
- gitinstall/feature_flags.py +138 -0
- gitinstall/fetcher.py +921 -0
- gitinstall/huggingface.py +922 -0
- gitinstall/hw_detect.py +988 -0
- gitinstall/i18n.py +664 -0
- gitinstall/installer_registry.py +362 -0
- gitinstall/knowledge_base.py +379 -0
- gitinstall/license_check.py +605 -0
- gitinstall/llm.py +569 -0
- gitinstall/log.py +236 -0
- gitinstall/main.py +1408 -0
- gitinstall/mcp_agent.py +841 -0
- gitinstall/mcp_server.py +386 -0
- gitinstall/monorepo.py +810 -0
- gitinstall/multi_source.py +425 -0
- gitinstall/onboard.py +276 -0
- gitinstall/planner.py +222 -0
- gitinstall/planner_helpers.py +323 -0
- gitinstall/planner_known_projects.py +1010 -0
- gitinstall/planner_templates.py +996 -0
- gitinstall/remote_gpu.py +633 -0
- gitinstall/resilience.py +608 -0
- gitinstall/run_tests.py +572 -0
- gitinstall/skills.py +476 -0
- gitinstall/tool_schemas.py +324 -0
- gitinstall/trending.py +279 -0
- gitinstall/uninstaller.py +415 -0
- gitinstall/validate_top100.py +607 -0
- gitinstall/watchdog.py +180 -0
- gitinstall/web.py +1277 -0
- gitinstall/web_ui.html +2277 -0
- gitinstall-1.1.0.dist-info/METADATA +275 -0
- gitinstall-1.1.0.dist-info/RECORD +59 -0
- gitinstall-1.1.0.dist-info/WHEEL +5 -0
- gitinstall-1.1.0.dist-info/entry_points.txt +3 -0
- gitinstall-1.1.0.dist-info/licenses/LICENSE +21 -0
- gitinstall-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""
|
|
2
|
+
gitinstall Tool Schemas — 让任意 AI 模型调用安装引擎
|
|
3
|
+
====================================================
|
|
4
|
+
|
|
5
|
+
提供多种格式的工具定义,使任何支持 Function Calling / Tool Use 的模型
|
|
6
|
+
(无论是 OpenAI、自训练模型、垂直模型、还是本地 Ollama 模型)
|
|
7
|
+
都能调用 gitinstall 的能力。
|
|
8
|
+
|
|
9
|
+
用法::
|
|
10
|
+
|
|
11
|
+
# 获取 OpenAI 格式 (也兼容 Ollama / vLLM / LM Studio / 任意 OpenAI 兼容 API)
|
|
12
|
+
from gitinstall.tool_schemas import openai_tools
|
|
13
|
+
|
|
14
|
+
# 获取原始 JSON Schema 格式 (框架无关)
|
|
15
|
+
from gitinstall.tool_schemas import json_schemas
|
|
16
|
+
|
|
17
|
+
# 获取全部工具名列表
|
|
18
|
+
from gitinstall.tool_schemas import tool_names
|
|
19
|
+
|
|
20
|
+
# 执行工具调用(通用分发器)
|
|
21
|
+
from gitinstall.tool_schemas import call_tool
|
|
22
|
+
result = call_tool("detect", {})
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import sys
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
_THIS_DIR = Path(__file__).parent
|
|
32
|
+
if str(_THIS_DIR) not in sys.path:
|
|
33
|
+
sys.path.insert(0, str(_THIS_DIR))
|
|
34
|
+
|
|
35
|
+
# ── 原始工具定义(格式无关的真相源)──────────────
|
|
36
|
+
|
|
37
|
+
_TOOLS = [
|
|
38
|
+
{
|
|
39
|
+
"name": "detect",
|
|
40
|
+
"description": (
|
|
41
|
+
"Detect the current system environment. Returns OS type, CPU architecture, "
|
|
42
|
+
"GPU (CUDA/ROCm/MPS/CPU), installed runtimes (Python, Node.js, Docker, Rust, Go, FFmpeg), "
|
|
43
|
+
"package managers (pip, conda, brew, apt, npm, cargo), disk space, and network connectivity."
|
|
44
|
+
),
|
|
45
|
+
"parameters": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"properties": {},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "plan",
|
|
52
|
+
"description": (
|
|
53
|
+
"Generate a step-by-step installation plan for a GitHub project. "
|
|
54
|
+
"Analyzes the project's tech stack, dependencies, and the current environment "
|
|
55
|
+
"to produce an optimal sequence of shell commands. Returns steps with commands, "
|
|
56
|
+
"launch command, confidence level (high/medium/low), and strategy used. "
|
|
57
|
+
"The plan is NOT executed — use the 'install' tool to execute it."
|
|
58
|
+
),
|
|
59
|
+
"parameters": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"properties": {
|
|
62
|
+
"project": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"description": "GitHub project identifier: 'owner/repo' or full URL",
|
|
65
|
+
},
|
|
66
|
+
"llm": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"description": "LLM provider for analysis. Auto-selects if omitted.",
|
|
69
|
+
"enum": ["anthropic", "openai", "openrouter", "gemini", "groq",
|
|
70
|
+
"deepseek", "lmstudio", "ollama", "none"],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
"required": ["project"],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "install",
|
|
78
|
+
"description": (
|
|
79
|
+
"Execute the installation of a GitHub project on this system. "
|
|
80
|
+
"Runs shell commands with safety filtering (blocks dangerous commands), "
|
|
81
|
+
"automatic error recovery (25+ patterns), and multi-strategy fallback. "
|
|
82
|
+
"Returns success status, install directory, and launch command."
|
|
83
|
+
),
|
|
84
|
+
"parameters": {
|
|
85
|
+
"type": "object",
|
|
86
|
+
"properties": {
|
|
87
|
+
"project": {
|
|
88
|
+
"type": "string",
|
|
89
|
+
"description": "GitHub project: 'owner/repo' or full URL",
|
|
90
|
+
},
|
|
91
|
+
"install_dir": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"description": "Target directory for installation.",
|
|
94
|
+
},
|
|
95
|
+
"dry_run": {
|
|
96
|
+
"type": "boolean",
|
|
97
|
+
"description": "If true, show plan only without executing.",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
"required": ["project"],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"name": "diagnose",
|
|
105
|
+
"description": (
|
|
106
|
+
"Diagnose a software installation error and suggest fixes. "
|
|
107
|
+
"Covers 25+ error patterns: dependency conflicts, permission issues, "
|
|
108
|
+
"missing tools, version mismatches, PEP 668, CUDA/GPU issues, and more."
|
|
109
|
+
),
|
|
110
|
+
"parameters": {
|
|
111
|
+
"type": "object",
|
|
112
|
+
"properties": {
|
|
113
|
+
"stderr": {
|
|
114
|
+
"type": "string",
|
|
115
|
+
"description": "Error output text to diagnose",
|
|
116
|
+
},
|
|
117
|
+
"command": {
|
|
118
|
+
"type": "string",
|
|
119
|
+
"description": "The command that produced the error.",
|
|
120
|
+
},
|
|
121
|
+
"stdout": {
|
|
122
|
+
"type": "string",
|
|
123
|
+
"description": "Standard output text.",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
"required": ["stderr"],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"name": "fetch",
|
|
131
|
+
"description": (
|
|
132
|
+
"Fetch metadata about a GitHub project: name, description, stars, "
|
|
133
|
+
"language, license, project type, clone URL, dependency files, README preview."
|
|
134
|
+
),
|
|
135
|
+
"parameters": {
|
|
136
|
+
"type": "object",
|
|
137
|
+
"properties": {
|
|
138
|
+
"project": {
|
|
139
|
+
"type": "string",
|
|
140
|
+
"description": "GitHub project: 'owner/repo' or full URL",
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
"required": ["project"],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"name": "doctor",
|
|
148
|
+
"description": (
|
|
149
|
+
"Run a comprehensive system diagnostic. Checks tool installations, "
|
|
150
|
+
"runtime versions, configuration issues, and system health."
|
|
151
|
+
),
|
|
152
|
+
"parameters": {
|
|
153
|
+
"type": "object",
|
|
154
|
+
"properties": {},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"name": "audit",
|
|
159
|
+
"description": (
|
|
160
|
+
"Audit a GitHub project's dependencies for security vulnerabilities (CVEs)."
|
|
161
|
+
),
|
|
162
|
+
"parameters": {
|
|
163
|
+
"type": "object",
|
|
164
|
+
"properties": {
|
|
165
|
+
"project": {
|
|
166
|
+
"type": "string",
|
|
167
|
+
"description": "GitHub project: 'owner/repo' or full URL",
|
|
168
|
+
},
|
|
169
|
+
"online": {
|
|
170
|
+
"type": "boolean",
|
|
171
|
+
"description": "Query online CVE databases for thorough results.",
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
"required": ["project"],
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
# ── 工具名列表 ──────────────────────────────
|
|
180
|
+
|
|
181
|
+
tool_names: list[str] = [t["name"] for t in _TOOLS]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ── OpenAI Function Calling 格式 ────────────
|
|
185
|
+
# 兼容: OpenAI / Ollama / vLLM / LM Studio / Azure OpenAI / Groq /
|
|
186
|
+
# DeepSeek / Together AI / Fireworks / Mistral / 任何 OpenAI 兼容 API
|
|
187
|
+
|
|
188
|
+
openai_tools: list[dict] = [
|
|
189
|
+
{
|
|
190
|
+
"type": "function",
|
|
191
|
+
"function": {
|
|
192
|
+
"name": t["name"],
|
|
193
|
+
"description": t["description"],
|
|
194
|
+
"parameters": t["parameters"],
|
|
195
|
+
},
|
|
196
|
+
}
|
|
197
|
+
for t in _TOOLS
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ── Anthropic Tool Use 格式 ─────────────────
|
|
202
|
+
# 兼容: Claude API / AWS Bedrock Claude
|
|
203
|
+
|
|
204
|
+
anthropic_tools: list[dict] = [
|
|
205
|
+
{
|
|
206
|
+
"name": t["name"],
|
|
207
|
+
"description": t["description"],
|
|
208
|
+
"input_schema": t["parameters"],
|
|
209
|
+
}
|
|
210
|
+
for t in _TOOLS
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ── Google Gemini 格式 ──────────────────────
|
|
215
|
+
# 兼容: Gemini API / Vertex AI
|
|
216
|
+
|
|
217
|
+
gemini_tools: list[dict] = [
|
|
218
|
+
{
|
|
219
|
+
"function_declarations": [
|
|
220
|
+
{
|
|
221
|
+
"name": t["name"],
|
|
222
|
+
"description": t["description"],
|
|
223
|
+
"parameters": t["parameters"],
|
|
224
|
+
}
|
|
225
|
+
for t in _TOOLS
|
|
226
|
+
]
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# ── 纯 JSON Schema 格式(框架无关)────────────
|
|
232
|
+
# 任何框架/模型都可以消费
|
|
233
|
+
|
|
234
|
+
json_schemas: list[dict] = [
|
|
235
|
+
{
|
|
236
|
+
"name": t["name"],
|
|
237
|
+
"description": t["description"],
|
|
238
|
+
"parameters": t["parameters"],
|
|
239
|
+
}
|
|
240
|
+
for t in _TOOLS
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ── 通用工具调用分发器 ──────────────────────
|
|
245
|
+
|
|
246
|
+
def call_tool(name: str, arguments: dict) -> dict | None:
|
|
247
|
+
"""
|
|
248
|
+
执行 gitinstall 工具调用。
|
|
249
|
+
|
|
250
|
+
所有 AI 框架集成都可以用这一个函数来分发工具调用:
|
|
251
|
+
|
|
252
|
+
result = call_tool("detect", {})
|
|
253
|
+
result = call_tool("plan", {"project": "comfyanonymous/ComfyUI"})
|
|
254
|
+
result = call_tool("diagnose", {"stderr": "error: ..."})
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
name: 工具名 (detect/plan/install/diagnose/fetch/doctor/audit)
|
|
258
|
+
arguments: 工具参数 dict
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
工具执行结果 dict,或 None(diagnose 无匹配时)
|
|
262
|
+
"""
|
|
263
|
+
from _sdk import detect, plan, install, diagnose, fetch, doctor, audit
|
|
264
|
+
|
|
265
|
+
if name == "detect":
|
|
266
|
+
return detect()
|
|
267
|
+
|
|
268
|
+
if name == "plan":
|
|
269
|
+
return plan(
|
|
270
|
+
arguments["project"],
|
|
271
|
+
llm=arguments.get("llm"),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if name == "install":
|
|
275
|
+
return install(
|
|
276
|
+
arguments["project"],
|
|
277
|
+
install_dir=arguments.get("install_dir"),
|
|
278
|
+
dry_run=arguments.get("dry_run", False),
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
if name == "diagnose":
|
|
282
|
+
return diagnose(
|
|
283
|
+
arguments["stderr"],
|
|
284
|
+
command=arguments.get("command", ""),
|
|
285
|
+
stdout=arguments.get("stdout", ""),
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
if name == "fetch":
|
|
289
|
+
return fetch(arguments["project"])
|
|
290
|
+
|
|
291
|
+
if name == "doctor":
|
|
292
|
+
return doctor()
|
|
293
|
+
|
|
294
|
+
if name == "audit":
|
|
295
|
+
return audit(
|
|
296
|
+
arguments["project"],
|
|
297
|
+
online=arguments.get("online", False),
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
raise ValueError(f"Unknown tool: {name}. Available: {tool_names}")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# ── 输出为 JSON ──────────────────────────────
|
|
304
|
+
|
|
305
|
+
def to_json(format: str = "openai") -> str:
|
|
306
|
+
"""
|
|
307
|
+
输出指定格式的工具定义 JSON。
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
format: "openai" | "anthropic" | "gemini" | "json_schema"
|
|
311
|
+
"""
|
|
312
|
+
schemas = {
|
|
313
|
+
"openai": openai_tools,
|
|
314
|
+
"anthropic": anthropic_tools,
|
|
315
|
+
"gemini": gemini_tools,
|
|
316
|
+
"json_schema": json_schemas,
|
|
317
|
+
}
|
|
318
|
+
data = schemas.get(format, openai_tools)
|
|
319
|
+
return json.dumps(data, indent=2, ensure_ascii=False)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
if __name__ == "__main__":
|
|
323
|
+
fmt = sys.argv[1] if len(sys.argv) > 1 else "openai"
|
|
324
|
+
print(to_json(fmt))
|
gitinstall/trending.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
trending.py - 动态 GitHub 热门项目爬取与缓存
|
|
3
|
+
=============================================
|
|
4
|
+
|
|
5
|
+
设计思路:
|
|
6
|
+
1. 从 GitHub Search API 获取各分类 Top 项目
|
|
7
|
+
2. 本地文件缓存,默认 6 小时刷新一次
|
|
8
|
+
3. 首次启动时先用静态 fallback,后台异步刷新
|
|
9
|
+
4. 支持手动强制刷新
|
|
10
|
+
|
|
11
|
+
缓存策略(解决排名动态变化问题):
|
|
12
|
+
- 文件缓存: ~/.cache/gitinstall/trending.json(TTL 6h)
|
|
13
|
+
- 内存缓存: 进程内 dict(避免重复读磁盘)
|
|
14
|
+
- 增量合并: 新排名数据与旧数据合并,避免项目突然消失
|
|
15
|
+
- 稳定性窗口: 用加权积分(当前排名 + 历史出现频次)平滑排名波动
|
|
16
|
+
|
|
17
|
+
只使用 Python 标准库,无需第三方依赖。
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import time
|
|
25
|
+
import urllib.error
|
|
26
|
+
import urllib.parse
|
|
27
|
+
import urllib.request
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from threading import Thread, Lock
|
|
30
|
+
|
|
31
|
+
# ─── 配置 ─────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
_CACHE_DIR = Path.home() / ".cache" / "gitinstall"
|
|
34
|
+
_CACHE_FILE = _CACHE_DIR / "trending.json"
|
|
35
|
+
_CACHE_TTL = int(os.getenv("GITINSTALL_TRENDING_TTL", str(6 * 3600))) # 默认 6 小时
|
|
36
|
+
_MEM_LOCK = Lock()
|
|
37
|
+
_mem_cache: dict | None = None
|
|
38
|
+
_mem_ts: float = 0.0
|
|
39
|
+
|
|
40
|
+
# 分类搜索查询(覆盖 AI / Web / 工具 / IoT 四大分类)
|
|
41
|
+
_SEARCH_QUERIES = [
|
|
42
|
+
# (query, tag, limit)
|
|
43
|
+
("topic:machine-learning stars:>5000", "AI", 25),
|
|
44
|
+
("topic:llm stars:>3000", "AI", 25),
|
|
45
|
+
("topic:web-framework stars:>5000", "Web", 25),
|
|
46
|
+
("topic:developer-tools stars:>3000", "工具", 25),
|
|
47
|
+
("topic:home-automation stars:>1000", "IoT", 15),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
# 语言 → 图标(emoji)
|
|
51
|
+
_LANG_ICONS = {
|
|
52
|
+
"python": "🐍", "javascript": "🟨", "typescript": "🔷", "go": "🟦",
|
|
53
|
+
"rust": "🦀", "java": "☕", "kotlin": "🟣", "swift": "🍎",
|
|
54
|
+
"c++": "⚙️", "c": "⚙️", "ruby": "💎", "php": "🐘",
|
|
55
|
+
"dart": "🎯", "shell": "🐚", "lua": "🌙", "haskell": "λ",
|
|
56
|
+
"c#": "🟩", "scala": "🔴", "elixir": "💧", "zig": "⚡",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# 标签 → 默认图标
|
|
60
|
+
_TAG_ICONS = {"AI": "🤖", "Web": "🌐", "工具": "🔧", "IoT": "🏠"}
|
|
61
|
+
|
|
62
|
+
# ─── 静态 Fallback(首次启动时无缓存可用) ─────────
|
|
63
|
+
|
|
64
|
+
_STATIC_TRENDING = [
|
|
65
|
+
{"repo": "open-webui/open-webui", "name": "Open WebUI", "icon": "🐳",
|
|
66
|
+
"desc": "ChatGPT 风格的本地 AI 聊天界面", "stars": "80k+", "lang": "Python", "tag": "AI"},
|
|
67
|
+
{"repo": "comfyanonymous/ComfyUI", "name": "ComfyUI", "icon": "🎨",
|
|
68
|
+
"desc": "强大的 Stable Diffusion 节点式工作流", "stars": "75k+", "lang": "Python", "tag": "AI"},
|
|
69
|
+
{"repo": "ollama/ollama", "name": "Ollama", "icon": "🤖",
|
|
70
|
+
"desc": "一键运行 LLaMA/Mistral 等大模型", "stars": "130k+", "lang": "Go", "tag": "AI"},
|
|
71
|
+
{"repo": "yt-dlp/yt-dlp", "name": "yt-dlp", "icon": "📺",
|
|
72
|
+
"desc": "最强视频下载工具,支持数千个站点", "stars": "100k+", "lang": "Python", "tag": "工具"},
|
|
73
|
+
{"repo": "fastapi/fastapi", "name": "FastAPI", "icon": "⚡",
|
|
74
|
+
"desc": "高性能 Python Web 框架", "stars": "82k+", "lang": "Python", "tag": "Web"},
|
|
75
|
+
{"repo": "home-assistant/core", "name": "Home Assistant", "icon": "🏠",
|
|
76
|
+
"desc": "开源智能家居自动化平台", "stars": "78k+", "lang": "Python", "tag": "IoT"},
|
|
77
|
+
{"repo": "AUTOMATIC1111/stable-diffusion-webui", "name": "SD WebUI", "icon": "🖼️",
|
|
78
|
+
"desc": "Stable Diffusion 最流行的 Web 界面", "stars": "148k+", "lang": "Python", "tag": "AI"},
|
|
79
|
+
{"repo": "langgenius/dify", "name": "Dify", "icon": "🧠",
|
|
80
|
+
"desc": "LLM 应用开发平台,可视化编排", "stars": "90k+", "lang": "Python", "tag": "AI"},
|
|
81
|
+
{"repo": "pallets/flask", "name": "Flask", "icon": "🌶️",
|
|
82
|
+
"desc": "轻量级 Python Web 微框架", "stars": "69k+", "lang": "Python", "tag": "Web"},
|
|
83
|
+
{"repo": "excalidraw/excalidraw", "name": "Excalidraw", "icon": "✏️",
|
|
84
|
+
"desc": "手绘风格的在线白板协作工具", "stars": "95k+", "lang": "TypeScript", "tag": "工具"},
|
|
85
|
+
{"repo": "rustdesk/rustdesk", "name": "RustDesk", "icon": "🖥️",
|
|
86
|
+
"desc": "开源远程桌面软件,TeamViewer 替代", "stars": "82k+", "lang": "Rust", "tag": "工具"},
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _fmt_stars(n: int) -> str:
|
|
91
|
+
"""格式化 star 数: 1234 → '1.2k', 123456 → '123k'"""
|
|
92
|
+
if n >= 1000:
|
|
93
|
+
return f"{n/1000:.0f}k+" if n >= 10000 else f"{n/1000:.1f}k+"
|
|
94
|
+
return str(n)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _github_search(query: str, per_page: int = 30) -> list[dict]:
|
|
98
|
+
"""调用 GitHub Search API 获取项目列表"""
|
|
99
|
+
url = "https://api.github.com/search/repositories?" + urllib.parse.urlencode({
|
|
100
|
+
"q": query,
|
|
101
|
+
"sort": "stars",
|
|
102
|
+
"order": "desc",
|
|
103
|
+
"per_page": str(min(per_page, 100)),
|
|
104
|
+
})
|
|
105
|
+
headers = {
|
|
106
|
+
"Accept": "application/vnd.github.v3+json",
|
|
107
|
+
"User-Agent": "gitinstall/1.0",
|
|
108
|
+
}
|
|
109
|
+
token = os.getenv("GITHUB_TOKEN") or os.getenv("GH_TOKEN")
|
|
110
|
+
if token:
|
|
111
|
+
headers["Authorization"] = f"token {token}"
|
|
112
|
+
|
|
113
|
+
req = urllib.request.Request(url, headers=headers)
|
|
114
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
115
|
+
return json.loads(resp.read()).get("items", [])
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _item_to_project(item: dict, tag: str) -> dict:
|
|
119
|
+
"""将 GitHub API 返回的 item 转换为前端格式"""
|
|
120
|
+
lang = (item.get("language") or "").strip()
|
|
121
|
+
icon = _LANG_ICONS.get(lang.lower(), _TAG_ICONS.get(tag, "📦"))
|
|
122
|
+
return {
|
|
123
|
+
"repo": item["full_name"],
|
|
124
|
+
"name": item["name"],
|
|
125
|
+
"icon": icon,
|
|
126
|
+
"desc": (item.get("description") or "")[:100],
|
|
127
|
+
"stars": _fmt_stars(item.get("stargazers_count", 0)),
|
|
128
|
+
"lang": lang,
|
|
129
|
+
"tag": tag,
|
|
130
|
+
"_stars_num": item.get("stargazers_count", 0),
|
|
131
|
+
"_fetched_at": time.time(),
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _fetch_all() -> list[dict]:
|
|
136
|
+
"""从 GitHub 爬取所有分类的热门项目"""
|
|
137
|
+
all_projects = []
|
|
138
|
+
seen_repos = set()
|
|
139
|
+
|
|
140
|
+
for query, tag, limit in _SEARCH_QUERIES:
|
|
141
|
+
try:
|
|
142
|
+
items = _github_search(query, per_page=limit)
|
|
143
|
+
for item in items:
|
|
144
|
+
repo = item["full_name"].lower()
|
|
145
|
+
if repo in seen_repos:
|
|
146
|
+
continue
|
|
147
|
+
seen_repos.add(repo)
|
|
148
|
+
all_projects.append(_item_to_project(item, tag))
|
|
149
|
+
time.sleep(6.5) # GitHub Search API: max 10 req/min unauthenticated
|
|
150
|
+
except Exception:
|
|
151
|
+
continue # 某个分类失败不影响其他分类
|
|
152
|
+
|
|
153
|
+
# 按 star 数降序
|
|
154
|
+
all_projects.sort(key=lambda x: x.get("_stars_num", 0), reverse=True)
|
|
155
|
+
return all_projects[:100] # 保留 Top 100
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _merge_with_old(new_projects: list[dict], old_projects: list[dict]) -> list[dict]:
|
|
159
|
+
"""
|
|
160
|
+
增量合并策略(解决排名动态波动问题):
|
|
161
|
+
- 新旧数据以 repo 为 key 合并
|
|
162
|
+
- 新数据中出现的项目:更新 stars/desc 等字段
|
|
163
|
+
- 旧数据中未在新数据出现但仍有高 star 的:保留(标记为 _stale),避免突然消失
|
|
164
|
+
- 超过 3 次刷新都未出现的 stale 项目:移除
|
|
165
|
+
"""
|
|
166
|
+
old_map = {p["repo"].lower(): p for p in old_projects}
|
|
167
|
+
new_map = {p["repo"].lower(): p for p in new_projects}
|
|
168
|
+
merged = {}
|
|
169
|
+
|
|
170
|
+
# 1. 所有新数据直接加入
|
|
171
|
+
for key, proj in new_map.items():
|
|
172
|
+
proj["_stale_count"] = 0
|
|
173
|
+
merged[key] = proj
|
|
174
|
+
|
|
175
|
+
# 2. 旧数据中不在新数据的:标记 stale,保留 3 轮
|
|
176
|
+
for key, proj in old_map.items():
|
|
177
|
+
if key not in merged:
|
|
178
|
+
stale = proj.get("_stale_count", 0) + 1
|
|
179
|
+
if stale <= 3:
|
|
180
|
+
proj["_stale_count"] = stale
|
|
181
|
+
merged[key] = proj
|
|
182
|
+
|
|
183
|
+
# 按 star 数降序,截取 Top 100
|
|
184
|
+
result = sorted(merged.values(), key=lambda x: x.get("_stars_num", 0), reverse=True)
|
|
185
|
+
return result[:100]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _read_cache() -> dict | None:
|
|
189
|
+
"""读取磁盘缓存"""
|
|
190
|
+
try:
|
|
191
|
+
if not _CACHE_FILE.exists():
|
|
192
|
+
return None
|
|
193
|
+
data = json.loads(_CACHE_FILE.read_text("utf-8"))
|
|
194
|
+
return data
|
|
195
|
+
except Exception:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _write_cache(projects: list[dict]):
|
|
200
|
+
"""写入磁盘缓存"""
|
|
201
|
+
try:
|
|
202
|
+
_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
203
|
+
data = {
|
|
204
|
+
"version": 1,
|
|
205
|
+
"updated_at": time.time(),
|
|
206
|
+
"projects": projects,
|
|
207
|
+
}
|
|
208
|
+
_CACHE_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=1), "utf-8")
|
|
209
|
+
except Exception:
|
|
210
|
+
pass
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _refresh_worker():
|
|
214
|
+
"""后台刷新线程"""
|
|
215
|
+
global _mem_cache, _mem_ts
|
|
216
|
+
try:
|
|
217
|
+
new_projects = _fetch_all()
|
|
218
|
+
if not new_projects:
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
# 与旧数据合并
|
|
222
|
+
old_data = _read_cache()
|
|
223
|
+
old_projects = old_data.get("projects", []) if old_data else []
|
|
224
|
+
merged = _merge_with_old(new_projects, old_projects)
|
|
225
|
+
|
|
226
|
+
_write_cache(merged)
|
|
227
|
+
with _MEM_LOCK:
|
|
228
|
+
_mem_cache = merged
|
|
229
|
+
_mem_ts = time.time()
|
|
230
|
+
except Exception:
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def get_trending(force_refresh: bool = False) -> list[dict]:
|
|
235
|
+
"""
|
|
236
|
+
获取热门项目列表(对外唯一接口)。
|
|
237
|
+
|
|
238
|
+
策略:
|
|
239
|
+
1. 内存缓存有效 → 直接返回
|
|
240
|
+
2. 磁盘缓存有效 → 加载到内存并返回
|
|
241
|
+
3. 缓存过期 → 返回旧数据 + 后台异步刷新
|
|
242
|
+
4. 无任何缓存 → 返回静态 fallback + 后台异步爬取
|
|
243
|
+
"""
|
|
244
|
+
global _mem_cache, _mem_ts
|
|
245
|
+
|
|
246
|
+
now = time.time()
|
|
247
|
+
|
|
248
|
+
# 1. 内存缓存有效且不强制刷新
|
|
249
|
+
with _MEM_LOCK:
|
|
250
|
+
if _mem_cache and (now - _mem_ts < _CACHE_TTL) and not force_refresh:
|
|
251
|
+
return _clean_for_frontend(_mem_cache)
|
|
252
|
+
|
|
253
|
+
# 2. 磁盘缓存
|
|
254
|
+
disk = _read_cache()
|
|
255
|
+
if disk:
|
|
256
|
+
projects = disk.get("projects", [])
|
|
257
|
+
updated_at = disk.get("updated_at", 0)
|
|
258
|
+
with _MEM_LOCK:
|
|
259
|
+
_mem_cache = projects
|
|
260
|
+
_mem_ts = updated_at
|
|
261
|
+
|
|
262
|
+
if (now - updated_at < _CACHE_TTL) and not force_refresh:
|
|
263
|
+
return _clean_for_frontend(projects)
|
|
264
|
+
else:
|
|
265
|
+
# 缓存过期:返回旧数据,后台刷新
|
|
266
|
+
Thread(target=_refresh_worker, daemon=True).start()
|
|
267
|
+
return _clean_for_frontend(projects)
|
|
268
|
+
|
|
269
|
+
# 3. 完全无缓存:返回静态 fallback,后台爬取
|
|
270
|
+
Thread(target=_refresh_worker, daemon=True).start()
|
|
271
|
+
return _STATIC_TRENDING[:]
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _clean_for_frontend(projects: list[dict]) -> list[dict]:
|
|
275
|
+
"""移除内部字段,只返回前端需要的数据"""
|
|
276
|
+
return [
|
|
277
|
+
{k: v for k, v in p.items() if not k.startswith("_")}
|
|
278
|
+
for p in projects
|
|
279
|
+
]
|