Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
custom-skills
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
eazy-template
custom-skills
Commits
489af456
Commit
489af456
authored
Mar 21, 2026
by
yuxiaodi
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
init
parent
b1c588a4
Changes
2
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
286 additions
and
19 deletions
+286
-19
README.md
README.md
+1
-1
app.py
app.py
+285
-18
No files found.
README.md
View file @
489af456
...
...
@@ -2,7 +2,7 @@
这是一个基于 Python
`FastAPI`
的演示项目,提供“智能体技能开发模板”的最小服务能力:
1.
用
`GET /`
返回
开发引导文案(包含可下载地址
)
1.
用
`GET /`
返回
前端页面(开发引导说明、默认下载区、列出
`skills/`
下各
`.skill`
的独立下载与复制链接
)
2.
用
`GET /download`
下载打包好的
`.skill`
压缩包
## 目录结构
...
...
app.py
View file @
489af456
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"
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment