Commit 489af456 authored by yuxiaodi's avatar yuxiaodi

init

parent b1c588a4
......@@ -2,7 +2,7 @@
这是一个基于 Python `FastAPI` 的演示项目,提供“智能体技能开发模板”的最小服务能力:
1.`GET /` 返回开发引导文案(包含可下载地址
1.`GET /` 返回前端页面(开发引导说明、默认下载区、列出 `skills/` 下各 `.skill` 的独立下载与复制链接
2.`GET /download` 下载打包好的 `.skill` 压缩包
## 目录结构
......
from fastapi import FastAPI, HTTPException, Query, Request
from fastapi.responses import FileResponse
from pathlib import Path
import html
import os
import re
from typing import Optional
from pathlib import Path
from typing import List, Optional
from urllib.parse import quote
from fastapi import FastAPI, HTTPException, Query, Request
from fastapi.responses import FileResponse, HTMLResponse
import uvicorn
app = FastAPI()
......@@ -12,17 +15,6 @@ BASE_DIR = Path(__file__).resolve().parent
SKILLS_DIR = BASE_DIR / "skills"
def _build_welcome_message(download_url: str) -> str:
return (
"欢迎来到 Eazybot 自定义技能开发模板。"
"您可以在IDE的对话框中描述你想开发的技能内容,让Eazy Develop帮您开发。"
"当然您也可以自己在 IDE 中的 `skills/` 目录下开发您的自定义技能并在开发完成后,使用 `make skill` 命令打包。"
"部署后,你可以访问: "
f"{download_url} (该地址在开发过程中为临时链接,部署后会展示稳定链接)"
"下载打包好的 `.skill` 压缩包;也可以直接把该链接交给 Eazybot,让其自行安装技能。"
)
def _build_public_base_url(request: Request) -> str:
host = request.headers.get("host", "").strip()
if host:
......@@ -30,10 +22,285 @@ def _build_public_base_url(request: Request) -> str:
return "https://localhost"
@app.get("/")
def _list_skill_packages() -> List[dict]:
"""列出 skills 目录下所有 .skill 包(文件名不含路径)。"""
if not SKILLS_DIR.is_dir():
return []
files = sorted(SKILLS_DIR.glob("*.skill"), key=lambda p: p.name.lower())
return [{"filename": p.name, "stem": p.stem} for p in files if p.is_file()]
def _render_home_html(request: Request) -> str:
base = _build_public_base_url(request)
default_download = f"{base}/download"
skills = _list_skill_packages()
rows_html = ""
for item in skills:
fname = item["filename"]
stem = item["stem"]
# 下载接口接受 stem 或带 .skill 的文件名
dl_url = f"{base}/download?skill={quote(stem, safe='')}"
safe_fname = html.escape(fname)
safe_url = html.escape(dl_url, quote=True)
rows_html += f"""
<tr class="skill-row">
<td class="skill-name"><code>{safe_fname}</code></td>
<td class="skill-actions">
<a class="btn btn-primary" href="{safe_url}" download>下载</a>
<button type="button" class="btn btn-secondary copy-btn" data-url="{safe_url}">复制链接</button>
</td>
</tr>"""
if not rows_html:
rows_html = """
<tr>
<td colspan="2" class="empty-hint">
暂无已打包的 <code>.skill</code> 文件。请在 <code>skills/</code> 下开发技能并执行打包后刷新本页。
</td>
</tr>"""
safe_default = html.escape(default_download, quote=True)
return f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Eazybot 自定义技能模板</title>
<style>
:root {{
--bg: #0f1419;
--surface: #1a2332;
--border: #2d3a4d;
--text: #e7ecf3;
--muted: #8b9cb3;
--accent: #3d8bfd;
--accent-hover: #5ba3ff;
--success: #3ecf8e;
}}
* {{ box-sizing: border-box; }}
body {{
margin: 0;
min-height: 100vh;
font-family: "SF Pro Text", "Segoe UI", system-ui, sans-serif;
background: radial-gradient(ellipse 120% 80% at 50% -20%, #1e3a5f 0%, var(--bg) 55%);
color: var(--text);
line-height: 1.6;
}}
.wrap {{
max-width: 720px;
margin: 0 auto;
padding: 2.5rem 1.25rem 3rem;
}}
.card {{
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
padding: 1.75rem 1.5rem;
box-shadow: 0 12px 40px rgba(0,0,0,.35);
}}
h1 {{
font-size: 1.5rem;
font-weight: 600;
margin: 0 0 0.5rem;
letter-spacing: -0.02em;
}}
.lead {{
color: var(--muted);
font-size: 0.95rem;
margin-bottom: 1.25rem;
}}
.intro p {{
margin: 0 0 0.85rem;
font-size: 0.95rem;
}}
.intro code {{
background: rgba(0,0,0,.25);
padding: 0.12em 0.35em;
border-radius: 6px;
font-size: 0.88em;
}}
.default-dl {{
margin: 1.25rem 0 1.5rem;
padding: 1rem;
background: rgba(0,0,0,.2);
border-radius: 12px;
border: 1px solid var(--border);
}}
.default-dl label {{
display: block;
font-size: 0.8rem;
color: var(--muted);
margin-bottom: 0.35rem;
}}
.url-row {{
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}}
.url-row input {{
flex: 1;
min-width: 200px;
padding: 0.5rem 0.65rem;
border-radius: 8px;
border: 1px solid var(--border);
background: var(--bg);
color: var(--text);
font-size: 0.85rem;
}}
.btn {{
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.45rem 0.9rem;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
border: none;
text-decoration: none;
transition: background .15s, transform .1s;
}}
.btn:active {{ transform: scale(0.98); }}
.btn-primary {{
background: var(--accent);
color: #fff;
}}
.btn-primary:hover {{ background: var(--accent-hover); }}
.btn-secondary {{
background: transparent;
color: var(--text);
border: 1px solid var(--border);
}}
.btn-secondary:hover {{ background: rgba(255,255,255,.06); }}
h2 {{
font-size: 1.05rem;
font-weight: 600;
margin: 0 0 0.75rem;
color: var(--muted);
}}
table {{
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}}
th, td {{
text-align: left;
padding: 0.65rem 0.5rem;
border-bottom: 1px solid var(--border);
}}
th {{
color: var(--muted);
font-weight: 500;
font-size: 0.8rem;
}}
.skill-name code {{
font-size: 0.88rem;
color: var(--success);
}}
.skill-actions {{
white-space: nowrap;
text-align: right;
}}
.skill-actions .btn {{ margin-left: 0.35rem; }}
.empty-hint {{
color: var(--muted);
font-size: 0.9rem;
padding: 1rem 0 !important;
}}
.toast {{
position: fixed;
bottom: 1.25rem;
left: 50%;
transform: translateX(-50%) translateY(100px);
opacity: 0;
background: var(--surface);
border: 1px solid var(--border);
color: var(--text);
padding: 0.6rem 1rem;
border-radius: 10px;
font-size: 0.85rem;
pointer-events: none;
transition: opacity .2s, transform .2s;
z-index: 100;
}}
.toast.show {{
opacity: 1;
transform: translateX(-50%) translateY(0);
}}
</style>
</head>
<body>
<div class="wrap">
<div class="card">
<h1>Eazybot 自定义技能开发模板</h1>
<p class="lead">在 IDE 中开发技能、打包后直接复制链接交给 Eazybot 安装或自行下载保存</p>
<div class="intro">
<p>您可以在 IDE 对话框中描述想开发的技能,由 <strong>Eazy Develop</strong> 协助完成;也可自行在 <code>skills/</code> 目录下开发,并使用 <code>make skill</code> 打包。</p>
<p>部署后可通过下方链接下载 <code>.skill</code> 压缩包,或将链接提供给 <strong>Eazybot</strong> 自动安装。(开发环境中链接可能为临时地址,部署后为稳定链接。)</p>
</div>
<div class="default-dl">
<label>默认下载(第一个 .skill)</label>
<div class="url-row">
<input type="text" readonly value="{safe_default}" id="default-url" />
<a class="btn btn-primary" href="{safe_default}" download>下载</a>
<button type="button" class="btn btn-secondary copy-btn" data-url="{safe_default}">复制链接</button>
</div>
</div>
<h2>已打包的技能</h2>
<table>
<thead>
<tr>
<th>文件名</th>
<th style="text-align:right">操作</th>
</tr>
</thead>
<tbody>
{rows_html}
</tbody>
</table>
</div>
</div>
<div class="toast" id="toast" role="status">已复制到剪贴板</div>
<script>
(function () {{
var toast = document.getElementById("toast");
function showToast() {{
toast.classList.add("show");
clearTimeout(showToast._t);
showToast._t = setTimeout(function () {{ toast.classList.remove("show"); }}, 2000);
}}
document.querySelectorAll(".copy-btn").forEach(function (btn) {{
btn.addEventListener("click", function () {{
var url = btn.getAttribute("data-url");
if (navigator.clipboard && navigator.clipboard.writeText) {{
navigator.clipboard.writeText(url).then(showToast).catch(function () {{
fallbackCopy(url);
}});
}} else {{
fallbackCopy(url);
}}
}});
}});
function fallbackCopy(text) {{
var input = document.createElement("input");
input.value = text;
document.body.appendChild(input);
input.select();
try {{ document.execCommand("copy"); showToast(); }} catch (e) {{}}
document.body.removeChild(input);
}}
}})();
</script>
</body>
</html>"""
@app.get("/", response_class=HTMLResponse)
def read_root(request: Request):
download_url = f"{_build_public_base_url(request)}/download"
return _build_welcome_message(download_url)
return HTMLResponse(content=_render_home_html(request))
@app.get("/download")
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment