Spaces:
Running
Running
Force push code via HfApi (Git fallback)
Browse files- app.py +117 -5
- scripts/force_push_code.py +45 -0
app.py
CHANGED
|
@@ -359,6 +359,43 @@ class ShowcaseHandler(BaseHTTPRequestHandler):
|
|
| 359 |
|
| 360 |
def do_GET(self) -> None:
|
| 361 |
_, path = self._parse()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
|
| 363 |
if path.startswith("/static/"):
|
| 364 |
file_path = Path(".") / path.lstrip("/")
|
|
@@ -707,7 +744,32 @@ class ShowcaseHandler(BaseHTTPRequestHandler):
|
|
| 707 |
<html lang="zh-CN">
|
| 708 |
<head>
|
| 709 |
<meta charset="utf-8" />
|
| 710 |
-
<title>项目
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 711 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 712 |
<style>
|
| 713 |
:root { --bg: #f3f4f6; --panel: #ffffff; --card: #ffffff; --muted: #6b7280; --fg: #111827; --accent: #2563eb; --border: rgba(148,163,184,0.35); }
|
|
@@ -719,8 +781,10 @@ class ShowcaseHandler(BaseHTTPRequestHandler):
|
|
| 719 |
.title { font-weight:700; font-size:20px; color:#0f172a; }
|
| 720 |
.sub { font-size:12px; color: var(--muted); }
|
| 721 |
.actions { display:flex; align-items:center; gap:8px; }
|
| 722 |
-
.btn { border:1px solid var(--border); border-radius:999px; padding:8px 18px; font-weight:600; cursor:pointer; color:var(--fg); background:#ffffff; box-shadow: 0 4px 12px rgba(0,0,0,0.05); transition: all .2s ease; }
|
| 723 |
.btn:hover { border-color:var(--accent); color:var(--accent); box-shadow: 0 8px 16px rgba(37,99,235,0.15); transform: translateY(-1px); }
|
|
|
|
|
|
|
| 724 |
.layout { flex: 1; display:flex; gap:16px; margin-top:16px; overflow: hidden; }
|
| 725 |
aside.panel { width: 280px; display: flex; flex-direction: column; overflow-y: auto; flex-shrink: 0; }
|
| 726 |
main.panel { flex: 1; overflow-y: auto; display: flex; flex-direction: column; }
|
|
@@ -794,7 +858,10 @@ class ShowcaseHandler(BaseHTTPRequestHandler):
|
|
| 794 |
<div class="shell">
|
| 795 |
<div class="topbar">
|
| 796 |
<div class="brand"><div class="title">项目集/工具站</div><div class="sub">项目分类与搜索</div></div>
|
| 797 |
-
<div class="actions">
|
|
|
|
|
|
|
|
|
|
| 798 |
</div>
|
| 799 |
<div class="layout">
|
| 800 |
<aside class="panel">
|
|
@@ -866,7 +933,7 @@ class ShowcaseHandler(BaseHTTPRequestHandler):
|
|
| 866 |
</div>
|
| 867 |
<script>
|
| 868 |
const TYPES = {types_json};
|
| 869 |
-
const state = { items: [], counts: {}, total: 0, q: "", type: "", admin: false, authenticated: false, newProjectTypes: [] };
|
| 870 |
|
| 871 |
function pickGradient(project) {
|
| 872 |
if (state.type && (project.ptypes || []).includes(state.type)) {
|
|
@@ -1009,10 +1076,51 @@ class ShowcaseHandler(BaseHTTPRequestHandler):
|
|
| 1009 |
renderFilters();
|
| 1010 |
const grid = document.getElementById("grid"); grid.innerHTML = "";
|
| 1011 |
for (const p of state.items) grid.appendChild(card(p));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1012 |
}
|
| 1013 |
function openModal(project) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1014 |
document.getElementById("modal").classList.add("open");
|
| 1015 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1016 |
let src = "";
|
| 1017 |
if (project.embed_url || project.hf_space_url) {
|
| 1018 |
const base = project.embed_url || project.hf_space_url;
|
|
@@ -1041,6 +1149,10 @@ class ShowcaseHandler(BaseHTTPRequestHandler):
|
|
| 1041 |
});
|
| 1042 |
}
|
| 1043 |
function closeModal() {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1044 |
document.getElementById("modal").classList.remove("open");
|
| 1045 |
document.getElementById("iframe").src = "";
|
| 1046 |
}
|
|
|
|
| 359 |
|
| 360 |
def do_GET(self) -> None:
|
| 361 |
_, path = self._parse()
|
| 362 |
+
|
| 363 |
+
if path == "/robots.txt":
|
| 364 |
+
content = "User-agent: *\nAllow: /\nSitemap: https://huggingface.co/spaces/duqing2026/project-show/sitemap.xml"
|
| 365 |
+
self.send_response(200)
|
| 366 |
+
self.send_header("Content-Type", "text/plain")
|
| 367 |
+
self.send_header("Content-Length", str(len(content)))
|
| 368 |
+
self.end_headers()
|
| 369 |
+
self.wfile.write(content.encode("utf-8"))
|
| 370 |
+
return
|
| 371 |
+
|
| 372 |
+
if path == "/sitemap.xml":
|
| 373 |
+
base_url = "https://huggingface.co/spaces/duqing2026/project-show"
|
| 374 |
+
xml = '<?xml version="1.0" encoding="UTF-8"?>\n'
|
| 375 |
+
xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
|
| 376 |
+
xml += f' <url>\n <loc>{base_url}</loc>\n <changefreq>daily</changefreq>\n <priority>1.0</priority>\n </url>\n'
|
| 377 |
+
|
| 378 |
+
# Add projects to sitemap
|
| 379 |
+
try:
|
| 380 |
+
for p in store.projects:
|
| 381 |
+
pid = p.get("id")
|
| 382 |
+
if pid is not None:
|
| 383 |
+
updated = p.get("updated_at", "")
|
| 384 |
+
if updated and len(updated) >= 10: updated = updated[:10]
|
| 385 |
+
else: updated = datetime.datetime.utcnow().strftime("%Y-%m-%d")
|
| 386 |
+
|
| 387 |
+
# XML escape for loc if needed, but IDs are usually ints. safe.
|
| 388 |
+
xml += f' <url>\n <loc>{base_url}?project_id={pid}</loc>\n <changefreq>weekly</changefreq>\n <priority>0.8</priority>\n <lastmod>{updated}</lastmod>\n </url>\n'
|
| 389 |
+
except Exception as e:
|
| 390 |
+
print(f"Sitemap gen error: {e}")
|
| 391 |
+
|
| 392 |
+
xml += '</urlset>'
|
| 393 |
+
self.send_response(200)
|
| 394 |
+
self.send_header("Content-Type", "application/xml")
|
| 395 |
+
self.send_header("Content-Length", str(len(xml)))
|
| 396 |
+
self.end_headers()
|
| 397 |
+
self.wfile.write(xml.encode("utf-8"))
|
| 398 |
+
return
|
| 399 |
|
| 400 |
if path.startswith("/static/"):
|
| 401 |
file_path = Path(".") / path.lstrip("/")
|
|
|
|
| 744 |
<html lang="zh-CN">
|
| 745 |
<head>
|
| 746 |
<meta charset="utf-8" />
|
| 747 |
+
<title>渡青的项目展示 - 全栈开发与AI实战 | Duqing's Showcase</title>
|
| 748 |
+
<meta name="description" content="探索渡青的个人项目集,涵盖RAG系统、电商应用、游戏开发、小程序及AI模型实战。分享编程心得与技术创新。" />
|
| 749 |
+
<meta name="keywords" content="渡青, 个人主页, 项目展示, 全栈开发, Python, RAG, AI, 编程博客, 语雀" />
|
| 750 |
+
<meta name="author" content="渡青" />
|
| 751 |
+
<meta property="og:type" content="website" />
|
| 752 |
+
<meta property="og:title" content="渡青的项目展示 - 全栈开发与AI实战" />
|
| 753 |
+
<meta property="og:description" content="探索渡青的个人项目集,涵盖RAG系统、电商应用、游戏开发、小程序及AI模型实战。" />
|
| 754 |
+
<meta property="twitter:card" content="summary" />
|
| 755 |
+
<meta property="twitter:title" content="渡青的项目展示" />
|
| 756 |
+
<meta property="twitter:description" content="探索渡青的个人项目集,涵盖RAG系统、电商应用、游戏开发、小程序及AI模型实战。" />
|
| 757 |
+
<script type="application/ld+json">
|
| 758 |
+
{
|
| 759 |
+
"@context": "https://schema.org",
|
| 760 |
+
"@type": "ProfilePage",
|
| 761 |
+
"mainEntity": {
|
| 762 |
+
"@type": "Person",
|
| 763 |
+
"name": "渡青",
|
| 764 |
+
"alternateName": "Duqing",
|
| 765 |
+
"description": "全栈开发者,专注于 Python, RAG, AI Agent 及 Web 应用开发。",
|
| 766 |
+
"url": "https://huggingface.co/spaces/duqing2026/project-show",
|
| 767 |
+
"sameAs": [
|
| 768 |
+
"https://www.yuque.com/lianmt"
|
| 769 |
+
]
|
| 770 |
+
}
|
| 771 |
+
}
|
| 772 |
+
</script>
|
| 773 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 774 |
<style>
|
| 775 |
:root { --bg: #f3f4f6; --panel: #ffffff; --card: #ffffff; --muted: #6b7280; --fg: #111827; --accent: #2563eb; --border: rgba(148,163,184,0.35); }
|
|
|
|
| 781 |
.title { font-weight:700; font-size:20px; color:#0f172a; }
|
| 782 |
.sub { font-size:12px; color: var(--muted); }
|
| 783 |
.actions { display:flex; align-items:center; gap:8px; }
|
| 784 |
+
.btn { border:1px solid var(--border); border-radius:999px; padding:8px 18px; font-size:14px; font-weight:600; cursor:pointer; color:var(--fg); background:#ffffff; box-shadow: 0 4px 12px rgba(0,0,0,0.05); transition: all .2s ease; }
|
| 785 |
.btn:hover { border-color:var(--accent); color:var(--accent); box-shadow: 0 8px 16px rgba(37,99,235,0.15); transform: translateY(-1px); }
|
| 786 |
+
.btn-outline-primary { border-color: var(--accent); color: var(--accent); background: rgba(37,99,235,0.04); }
|
| 787 |
+
.btn-outline-primary:hover { background: var(--accent); color: #ffffff; box-shadow: 0 8px 16px rgba(37,99,235,0.25); }
|
| 788 |
.layout { flex: 1; display:flex; gap:16px; margin-top:16px; overflow: hidden; }
|
| 789 |
aside.panel { width: 280px; display: flex; flex-direction: column; overflow-y: auto; flex-shrink: 0; }
|
| 790 |
main.panel { flex: 1; overflow-y: auto; display: flex; flex-direction: column; }
|
|
|
|
| 858 |
<div class="shell">
|
| 859 |
<div class="topbar">
|
| 860 |
<div class="brand"><div class="title">项目集/工具站</div><div class="sub">项目分类与搜索</div></div>
|
| 861 |
+
<div class="actions">
|
| 862 |
+
<a href="https://www.yuque.com/lianmt" target="_blank" class="btn btn-outline-primary" style="text-decoration:none;">我的博客</a>
|
| 863 |
+
<button class="btn" id="admin-toggle">管理</button>
|
| 864 |
+
</div>
|
| 865 |
</div>
|
| 866 |
<div class="layout">
|
| 867 |
<aside class="panel">
|
|
|
|
| 933 |
</div>
|
| 934 |
<script>
|
| 935 |
const TYPES = {types_json};
|
| 936 |
+
const state = { items: [], counts: {}, total: 0, q: "", type: "", admin: false, authenticated: false, newProjectTypes: [], initialLoad: true };
|
| 937 |
|
| 938 |
function pickGradient(project) {
|
| 939 |
if (state.type && (project.ptypes || []).includes(state.type)) {
|
|
|
|
| 1076 |
renderFilters();
|
| 1077 |
const grid = document.getElementById("grid"); grid.innerHTML = "";
|
| 1078 |
for (const p of state.items) grid.appendChild(card(p));
|
| 1079 |
+
|
| 1080 |
+
if (state.initialLoad) {
|
| 1081 |
+
state.initialLoad = false;
|
| 1082 |
+
const params = new URLSearchParams(window.location.search);
|
| 1083 |
+
const pid = params.get("project_id");
|
| 1084 |
+
if (pid) {
|
| 1085 |
+
const p = state.items.find(x => x.id == pid);
|
| 1086 |
+
if (p) openModal(p);
|
| 1087 |
+
}
|
| 1088 |
+
}
|
| 1089 |
}
|
| 1090 |
function openModal(project) {
|
| 1091 |
+
const url = new URL(window.location);
|
| 1092 |
+
url.searchParams.set("project_id", project.id);
|
| 1093 |
+
window.history.pushState({}, "", url);
|
| 1094 |
+
|
| 1095 |
document.getElementById("modal").classList.add("open");
|
| 1096 |
+
|
| 1097 |
+
// Update header with copy link btn
|
| 1098 |
+
const head = document.querySelector(".modal-head");
|
| 1099 |
+
head.innerHTML = "";
|
| 1100 |
+
|
| 1101 |
+
const title = document.createElement("div"); title.className = "modal-title"; title.textContent = project.name || "项目演示";
|
| 1102 |
+
const actions = document.createElement("div"); actions.style.display = "flex"; actions.style.gap = "8px";
|
| 1103 |
+
|
| 1104 |
+
const copyBtn = document.createElement("button"); copyBtn.className = "btn-sm"; copyBtn.textContent = "复制链接";
|
| 1105 |
+
copyBtn.onclick = () => {
|
| 1106 |
+
const link = window.location.href;
|
| 1107 |
+
navigator.clipboard.writeText(link).then(() => {
|
| 1108 |
+
const orig = copyBtn.textContent;
|
| 1109 |
+
copyBtn.textContent = "已复制";
|
| 1110 |
+
setTimeout(() => copyBtn.textContent = orig, 2000);
|
| 1111 |
+
});
|
| 1112 |
+
};
|
| 1113 |
+
|
| 1114 |
+
const closeBtn = document.createElement("button"); closeBtn.className = "btn-sm"; closeBtn.textContent = "关闭";
|
| 1115 |
+
closeBtn.id = "close-modal"; // Restore ID for event listener binding if needed, but we bind click below
|
| 1116 |
+
closeBtn.onclick = closeModal;
|
| 1117 |
+
|
| 1118 |
+
actions.appendChild(copyBtn);
|
| 1119 |
+
actions.appendChild(closeBtn);
|
| 1120 |
+
|
| 1121 |
+
head.appendChild(title);
|
| 1122 |
+
head.appendChild(actions);
|
| 1123 |
+
|
| 1124 |
let src = "";
|
| 1125 |
if (project.embed_url || project.hf_space_url) {
|
| 1126 |
const base = project.embed_url || project.hf_space_url;
|
|
|
|
| 1149 |
});
|
| 1150 |
}
|
| 1151 |
function closeModal() {
|
| 1152 |
+
const url = new URL(window.location);
|
| 1153 |
+
url.searchParams.delete("project_id");
|
| 1154 |
+
window.history.pushState({}, "", url);
|
| 1155 |
+
|
| 1156 |
document.getElementById("modal").classList.remove("open");
|
| 1157 |
document.getElementById("iframe").src = "";
|
| 1158 |
}
|
scripts/force_push_code.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
from huggingface_hub import HfApi
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
|
| 6 |
+
# Load env
|
| 7 |
+
load_dotenv(override=True)
|
| 8 |
+
|
| 9 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 10 |
+
REPO_ID = "duqing2026/project-show"
|
| 11 |
+
|
| 12 |
+
if not HF_TOKEN:
|
| 13 |
+
print("Error: HF_TOKEN not found.")
|
| 14 |
+
sys.exit(1)
|
| 15 |
+
|
| 16 |
+
api = HfApi(token=HF_TOKEN)
|
| 17 |
+
|
| 18 |
+
print(f"Force pushing code to Space: {REPO_ID}...")
|
| 19 |
+
|
| 20 |
+
# Allow patterns: upload everything except exclusions
|
| 21 |
+
# Better to explicitly exclude
|
| 22 |
+
ignore_patterns = [
|
| 23 |
+
".git*",
|
| 24 |
+
".env*",
|
| 25 |
+
"venv/*",
|
| 26 |
+
"hf_project_showcase_data/*",
|
| 27 |
+
"__pycache__/*",
|
| 28 |
+
"*.pyc",
|
| 29 |
+
".DS_Store",
|
| 30 |
+
"*.log"
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
try:
|
| 34 |
+
api.upload_folder(
|
| 35 |
+
folder_path=".",
|
| 36 |
+
repo_id=REPO_ID,
|
| 37 |
+
repo_type="space",
|
| 38 |
+
path_in_repo=".",
|
| 39 |
+
commit_message="Force push code via HfApi (Git fallback)",
|
| 40 |
+
ignore_patterns=ignore_patterns
|
| 41 |
+
)
|
| 42 |
+
print("Code pushed successfully via HfApi.")
|
| 43 |
+
except Exception as e:
|
| 44 |
+
print(f"Failed to push code: {e}")
|
| 45 |
+
sys.exit(1)
|