Add files via upload
This commit is contained in:
26
Dockerfile
Normal file
26
Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
FROM python:3.10-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
libpq-dev \
|
||||
gcc \
|
||||
python3-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 复制依赖文件
|
||||
COPY requirements.txt .
|
||||
|
||||
# 安装Python依赖
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 复制应用代码
|
||||
COPY . .
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8000
|
||||
|
||||
# 启动命令
|
||||
CMD ["python", "main.py"]
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 yumo666
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
116
README.md
Normal file
116
README.md
Normal file
@@ -0,0 +1,116 @@
|
||||
<p align="center">
|
||||
<img src="image/CloudFuse.svg" alt="CloudFuse Logo" width="120"/>
|
||||
</p>
|
||||
|
||||
# CloudFuse - 云函数管理系统
|
||||
|
||||
CloudFuse 是一个基于 FastAPI 的现代化云函数管理平台,支持函数的动态上传、管理、调用、依赖自动安装、调用统计、实时日志监控等功能,适合自建 Serverless/FAAS 场景。
|
||||
|
||||
---
|
||||
|
||||
## 主要特性
|
||||
|
||||
- 动态上传/创建/编辑/删除函数(支持 Web 界面和 API)
|
||||
- 自动依赖管理:函数依赖自动检测与安装
|
||||
- 函数调用统计:支持天/小时/总量统计
|
||||
- 实时日志面板:只展示 logs/app.log,自动轮转,最多1000行
|
||||
- 安全的沙箱式函数执行环境
|
||||
- 支持多种文件管理操作(上传、下载、重命名、删除、目录树等)
|
||||
- API 文档自动生成(/docs)
|
||||
- 支持 Docker 部署
|
||||
- 热重载开发体验
|
||||
|
||||
---
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
.
|
||||
├── admin/ # 后台管理前端模板与静态资源
|
||||
│ ├── static/
|
||||
│ └── templates/
|
||||
├── apps/ # 所有云函数目录
|
||||
│ └── <function_name>/
|
||||
│ ├── function.py
|
||||
│ ├── config.json
|
||||
│ ├── intro.md
|
||||
│ ├── requirements.txt
|
||||
│ └── ...
|
||||
├── logs/ # 日志目录,仅有 app.log
|
||||
│ └── app.log
|
||||
├── main.py # FastAPI 启动入口
|
||||
├── config.py # 配置文件(含日志、路径、API等)
|
||||
├── requirements.txt # 依赖
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
1. **安装依赖**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. **启动服务**
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
3. **访问管理后台**
|
||||
- Web 管理页面:http://localhost:8000/admin/
|
||||
- API 文档:http://localhost:8000/docs
|
||||
|
||||
---
|
||||
|
||||
## 函数管理
|
||||
|
||||
- 上传/创建函数:支持通过 Web 页面上传 zip/文件夹,或在线新建函数
|
||||
- 函数目录要求:每个函数一个独立目录,需包含 function.py、config.json、intro.md
|
||||
- 依赖自动安装:每次上传/保存函数时自动检测 requirements.txt 并安装新依赖
|
||||
- 函数调用:通过 Web 或 API 直接调用,支持参数自动识别
|
||||
|
||||
---
|
||||
|
||||
## 日志系统
|
||||
|
||||
- 所有日志仅保存在 `logs/app.log`
|
||||
- 自动轮转:只保留最新 1000 行,超出部分自动丢弃
|
||||
- Web 日志面板:支持实时查看 logs/app.log 内容
|
||||
|
||||
---
|
||||
|
||||
## 文件管理
|
||||
|
||||
- 支持 apps 目录下的文件/文件夹上传、下载、重命名、删除、目录树浏览等操作
|
||||
|
||||
---
|
||||
|
||||
## 统计与监控
|
||||
|
||||
- 支持函数调用量的天/小时/总量统计
|
||||
- 监控面板实时展示调用趋势、错误日志等
|
||||
|
||||
---
|
||||
|
||||
## Docker 部署
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
- 依赖安装失败:请检查 requirements.txt 是否正确,或手动 pip install
|
||||
- 函数调用报错:请检查函数实现、参数类型、返回值格式
|
||||
- 日志不显示:请确认 logs/app.log 是否有写入权限
|
||||
|
||||
---
|
||||
|
||||
## 贡献与许可
|
||||
|
||||
- 欢迎提交 PR 或 Issue
|
||||
- 本项目采用 MIT 许可证
|
||||
BIN
__pycache__/config.cpython-311.pyc
Normal file
BIN
__pycache__/config.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/main.cpython-311.pyc
Normal file
BIN
__pycache__/main.cpython-311.pyc
Normal file
Binary file not shown.
BIN
admin/__pycache__/admin.cpython-311.pyc
Normal file
BIN
admin/__pycache__/admin.cpython-311.pyc
Normal file
Binary file not shown.
1076
admin/admin.py
Normal file
1076
admin/admin.py
Normal file
File diff suppressed because it is too large
Load Diff
174
admin/static/js/apidebug.js
Normal file
174
admin/static/js/apidebug.js
Normal file
@@ -0,0 +1,174 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
let allApiConfigs = [];
|
||||
let filteredApiConfigs = [];
|
||||
let currentApi = null;
|
||||
|
||||
async function loadApiConfigs() {
|
||||
const res = await fetch('/functions');
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
if (!data.functions) return;
|
||||
allApiConfigs = [];
|
||||
for (const fn of data.functions) {
|
||||
if (!fn.name) continue;
|
||||
try {
|
||||
const res2 = await fetch(`/admin/get_function_file_content/${fn.name}?file_path=config.json`);
|
||||
if (!res2.ok) continue;
|
||||
const conf = await res2.json();
|
||||
if (conf && conf.content) {
|
||||
const cfg = JSON.parse(conf.content);
|
||||
cfg._funcname = fn.name;
|
||||
allApiConfigs.push(cfg);
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
filteredApiConfigs = allApiConfigs;
|
||||
renderApiList();
|
||||
if (filteredApiConfigs.length > 0) showApiDetail(filteredApiConfigs[0]);
|
||||
}
|
||||
function renderApiList() {
|
||||
const list = document.getElementById('api-list');
|
||||
list.innerHTML = '';
|
||||
filteredApiConfigs.forEach((api, idx) => {
|
||||
const mainTitle = api.name && api.name.trim() ? api.name : api._funcname;
|
||||
const subTitle = api.url || '';
|
||||
const item = document.createElement('div');
|
||||
item.className = 'apilist-item' + (currentApi && currentApi._funcname === api._funcname ? ' active' : '');
|
||||
item.style = `padding:12px 14px;margin:6px 0;border-radius:8px;cursor:pointer;background:${currentApi && currentApi._funcname === api._funcname ? '#e3f0ff':'#fff'};border:1px solid #e3eaf2;font-size:15px;transition:background 0.2s;`;
|
||||
item.innerHTML = `<span style=\"color:#2196F3;font-weight:bold;\">${mainTitle}</span><br><span style=\"font-size:13px;color:#888\">${subTitle}</span>`;
|
||||
item.onclick = () => { currentApi = api; renderApiList(); showApiDetail(api); };
|
||||
list.appendChild(item);
|
||||
});
|
||||
}
|
||||
document.getElementById('api-search').oninput = function() {
|
||||
const kw = this.value.trim().toLowerCase();
|
||||
filteredApiConfigs = allApiConfigs.filter(api => api.name.toLowerCase().includes(kw) || (api.url||'').toLowerCase().includes(kw));
|
||||
renderApiList();
|
||||
if (filteredApiConfigs.length > 0) showApiDetail(filteredApiConfigs[0]);
|
||||
else document.getElementById('api-detail-content').innerHTML = '';
|
||||
};
|
||||
function showApiDetail(api) {
|
||||
let html = '';
|
||||
const mainTitle = api._display_name && api._display_name.trim() ? api._display_name : api._funcname;
|
||||
const fullUrl = `http://${window.location.host}${api.url||''}`;
|
||||
html += `<div style=\"background:#fff;border-radius:16px;box-shadow:0 2px 12px rgba(33,150,243,0.06);padding:32px 36px 24px 36px;\">
|
||||
<div style=\"display:flex;align-items:center;gap:16px;margin-bottom:8px;\">
|
||||
<div style=\"font-size:22px;color:#2196F3;font-weight:bold;\">${api.name}</div>
|
||||
<span class=\"route-chip\" onclick=\"copyRouteLink(this, '${fullUrl}')\">${api.url||''}</span>
|
||||
</div>
|
||||
<div style=\"margin:0 0 18px 0;\">
|
||||
<span class=\"call-url-chip\" onclick=\"copyRouteLink(this, '${fullUrl}')\">${fullUrl}</span>
|
||||
</div>
|
||||
<div style=\"margin:18px 0 0 0;font-size:15px;color:#555;\">${api._display_name||''}</div>
|
||||
<div style=\"margin:24px 0 0 0;\">
|
||||
<div style=\"font-size:16px;font-weight:bold;color:#2196F3;margin-bottom:8px;\">请求体结构</div>
|
||||
<div>${renderParamStruct(api.parameters||[])}</div>
|
||||
</div>
|
||||
<div style=\"margin:24px 0 0 0;\">
|
||||
<div style=\"font-size:16px;font-weight:bold;color:#2196F3;margin-bottom:8px;\">接口调试</div>
|
||||
<form id=\"apidebug-form\" onsubmit=\"sendApiDebug(event)\">
|
||||
${renderParamInputs(api.parameters||[])}
|
||||
<button type=\"submit\" style=\"margin-top:12px;background:#2196F3;color:#fff;border:none;border-radius:8px;padding:10px 28px;font-size:16px;cursor:pointer;\">发送请求</button>
|
||||
</form>
|
||||
</div>
|
||||
<div id=\"api-body-panel\" style=\"margin:24px 0 0 0;\">
|
||||
<div style=\"font-size:16px;font-weight:bold;color:#2196F3;margin-bottom:8px;display:flex;align-items:center;gap:16px;\">
|
||||
<span>请求体(JSON)</span>
|
||||
<button id=\"toggle-body-btn\" style=\"background:#e3f0ff;color:#2196F3;border:none;border-radius:6px;padding:4px 10px;cursor:pointer;font-size:14px;\" onclick=\"toggleApiBody()\">展开</button>
|
||||
<button id=\"copy-body-btn\" style=\"background:#e3f0ff;color:#2196F3;border:none;border-radius:6px;padding:4px 10px;cursor:pointer;font-size:14px;display:none;\" onclick=\"copyApiBodyJson()\">复制</button>
|
||||
</div>
|
||||
<pre id=\"api-body-json\" style=\"background:#f7faff;border-radius:8px;padding:18px 14px;font-size:15px;min-height:48px;max-height:320px;overflow:auto;display:none;\"></pre>
|
||||
</div>
|
||||
<div style=\"margin:24px 0 0 0;\">
|
||||
<div style=\"font-size:16px;font-weight:bold;color:#2196F3;margin-bottom:8px;\">响应</div>
|
||||
<pre id=\"apidebug-response\" style=\"background:#f7faff;border-radius:8px;padding:18px 14px;font-size:15px;min-height:48px;\"></pre>
|
||||
</div>
|
||||
</div>`;
|
||||
document.getElementById('api-detail-content').innerHTML = html;
|
||||
updateApiBodyJson(api);
|
||||
const form = document.getElementById('apidebug-form');
|
||||
if (form) {
|
||||
form.oninput = () => updateApiBodyJson(api);
|
||||
}
|
||||
}
|
||||
function updateApiBodyJson(api) {
|
||||
const form = document.getElementById('apidebug-form');
|
||||
const params = {};
|
||||
if (form && api.parameters) {
|
||||
for (const p of api.parameters) {
|
||||
const el = form.querySelector(`[name='${p.name}']`);
|
||||
params[p.name] = el ? el.value : (p.default || (p.type === 'number' ? 0 : ''));
|
||||
}
|
||||
}
|
||||
const jsonStr = JSON.stringify(params, null, 2);
|
||||
const pre = document.getElementById('api-body-json');
|
||||
if (pre) pre.textContent = jsonStr;
|
||||
}
|
||||
function toggleApiBody() {
|
||||
const pre = document.getElementById('api-body-json');
|
||||
const btn = document.getElementById('toggle-body-btn');
|
||||
const copyBtn = document.getElementById('copy-body-btn');
|
||||
if (pre.style.display === 'none') {
|
||||
pre.style.display = 'block';
|
||||
btn.textContent = '收起';
|
||||
copyBtn.style.display = '';
|
||||
} else {
|
||||
pre.style.display = 'none';
|
||||
btn.textContent = '展开';
|
||||
copyBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
function copyApiBodyJson() {
|
||||
const pre = document.getElementById('api-body-json');
|
||||
if (pre) {
|
||||
navigator.clipboard.writeText(pre.textContent);
|
||||
const btn = document.getElementById('copy-body-btn');
|
||||
btn.textContent = '已复制!';
|
||||
setTimeout(() => { btn.textContent = '复制'; }, 1000);
|
||||
}
|
||||
}
|
||||
function renderParamStruct(params) {
|
||||
if (!params.length) return '<span style="color:#aaa;">无参数</span>';
|
||||
return params.map(p => `<div style="margin-bottom:8px;"><span style="background:#e3f0ff;color:#2196F3;border-radius:6px;padding:2px 10px;font-size:14px;margin-right:8px;">${p.name}</span><span style="color:#888;">${p.type}</span> <span style="color:#bbb;">${p.required?'必填':'可选'}</span> <span style="color:#aaa;">${p.description||''}</span></div>`).join('');
|
||||
}
|
||||
function renderParamInputs(params) {
|
||||
if (!params.length) return '<div style="color:#aaa;">无参数</div>';
|
||||
return params.map(p => `<div style="margin-bottom:12px;"><label style="font-size:15px;color:#555;margin-bottom:4px;display:block;">${p.name} <span style="color:#888;font-size:13px;">(${p.type}${p.required?',必填':',可选'})</span></label><input name="${p.name}" type="text" placeholder="${p.description||''}" value="${p.default||''}" style="width:100%;padding:8px 12px;border-radius:8px;border:1px solid #e3eaf2;background:#f7faff;font-size:15px;"></div>`).join('');
|
||||
}
|
||||
window.sendApiDebug = async function(e) {
|
||||
e.preventDefault();
|
||||
if (!currentApi) return;
|
||||
const form = document.getElementById('apidebug-form');
|
||||
const params = {};
|
||||
for (const el of form.elements) {
|
||||
if (el.name) params[el.name] = el.value;
|
||||
}
|
||||
let url = currentApi.url;
|
||||
let method = (currentApi.method||'GET').toUpperCase();
|
||||
let options = { method, headers: { 'Content-Type': 'application/json' } };
|
||||
if (method === 'GET') {
|
||||
const usp = new URLSearchParams(params);
|
||||
url += '?' + usp.toString();
|
||||
} else {
|
||||
options.body = JSON.stringify(params);
|
||||
}
|
||||
try {
|
||||
const res = await fetch(url, options);
|
||||
const data = await res.json();
|
||||
document.getElementById('apidebug-response').textContent = JSON.stringify(data, null, 2);
|
||||
} catch(e) {
|
||||
document.getElementById('apidebug-response').textContent = '请求失败:'+e.message;
|
||||
}
|
||||
}
|
||||
window.copyRouteLink = function(el, url) {
|
||||
navigator.clipboard.writeText(url);
|
||||
const old = el.textContent;
|
||||
el.textContent = '已复制!';
|
||||
el.style.background = '#b2e0ff';
|
||||
setTimeout(() => {
|
||||
el.textContent = old;
|
||||
el.style.background = '';
|
||||
}, 1000);
|
||||
}
|
||||
loadApiConfigs();
|
||||
});
|
||||
516
admin/static/js/file.js
Normal file
516
admin/static/js/file.js
Normal file
@@ -0,0 +1,516 @@
|
||||
// 文件管理页主要 JS 逻辑
|
||||
// 这里只做基础结构,后续可根据需要细化
|
||||
// ...(可根据 index.html 相关 JS 拆分填充)
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// ========== 文件管理页新版 ========== //
|
||||
const fileTableContainer = document.getElementById('file-table-container');
|
||||
const filePathNav = document.getElementById('file-path-nav');
|
||||
const fileBtnBack = document.getElementById('file-btn-back');
|
||||
const fileBtnAdd = document.getElementById('file-btn-add');
|
||||
const fileBtnRefresh = document.getElementById('file-btn-refresh');
|
||||
const fileBtnUpload = document.getElementById('file-btn-upload');
|
||||
const fileFolderUploadInput = document.getElementById('file-folder-upload');
|
||||
|
||||
let fileCurrentPath = '';
|
||||
let fileTableData = [];
|
||||
let fileSelected = new Set();
|
||||
|
||||
// File Edit Dialog elements
|
||||
const editDialog = document.getElementById('edit-dialog');
|
||||
const editDialogMask = document.getElementById('file-dialog-mask'); // Re-using existing mask
|
||||
const currentFileTitle = document.getElementById('current-file-title');
|
||||
const fileEditor = document.getElementById('file-editor');
|
||||
const editSaveBtn = document.getElementById('edit-save-btn');
|
||||
const editCancelBtn = document.getElementById('edit-cancel-btn');
|
||||
|
||||
let currentEditingFilePath = null; // To store the full path of the file being edited
|
||||
|
||||
function renderFilePathNav() {
|
||||
const segs = fileCurrentPath ? fileCurrentPath.split('/') : [];
|
||||
let html = `<span class='file-path-seg${segs.length === 0 ? ' active' : ''}' onclick="fileJumpTo('')">apps</span>`;
|
||||
let path = '';
|
||||
for (let i = 0; i < segs.length; ++i) {
|
||||
path += (path ? '/' : '') + segs[i];
|
||||
const isLast = i === segs.length - 1;
|
||||
html += `<span class='file-path-arrow'>></span><span class='file-path-seg${isLast ? ' active' : ''}' ${isLast ? '' : `onclick=\"fileJumpTo('${path}')\"`}>${segs[i]}</span>`;
|
||||
}
|
||||
filePathNav.innerHTML = html;
|
||||
}
|
||||
|
||||
async function loadFileTable(path = '') {
|
||||
fileCurrentPath = path;
|
||||
fileSelected.clear();
|
||||
renderFilePathNav();
|
||||
fileTableContainer.innerHTML = '<div style="color:#888;padding:18px;">加载中...</div>';
|
||||
try {
|
||||
const res = await fetch('/admin/list_dir?path=' + encodeURIComponent(path));
|
||||
if (!res.ok) throw new Error('接口错误');
|
||||
fileTableData = await res.json();
|
||||
renderFileTable();
|
||||
} catch(e) {
|
||||
fileTableContainer.innerHTML = '<div style="color:red;padding:18px;">加载失败</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderFileTable() {
|
||||
let html = `<table style="width:100%;background:#fff;border-radius:8px;box-shadow:0 2px 8px #f0f0f0;font-size:15px;overflow:hidden;">
|
||||
<thead><tr style="background:#f7faff;">
|
||||
<th style="width:36px;"><input type='checkbox' id='file-check-all'></th>
|
||||
<th style="text-align:left;">名称</th>
|
||||
<th style="width:80px;">类型</th>
|
||||
<th style="width:100px;">大小</th>
|
||||
<th style="width:180px;">修改时间</th>
|
||||
<th style="width:220px;">操作</th>
|
||||
</tr></thead><tbody>`;
|
||||
for (const item of fileTableData) {
|
||||
const isFolder = item.type === 'folder';
|
||||
const icon = isFolder ? '📁' : '📄';
|
||||
const ext = item.name.split('.').pop().toLowerCase();
|
||||
// Only allow editing for specific file extensions
|
||||
const canEdit = !isFolder && ['py','json','txt','md','yaml','yml'].includes(ext);
|
||||
const rowId = 'row-' + item.name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
html += `<tr class='file-row' id='${rowId}'>
|
||||
<td><input type='checkbox' class='file-check' data-name='${item.name}'></td>
|
||||
<td class='file-name-cell' data-name='${item.name}' data-type='${item.type}' data-can-edit='${canEdit}'>
|
||||
<button class='file-main-btn' onclick="event.stopPropagation();${isFolder ? `fileJumpTo('${fileCurrentPath ? fileCurrentPath + '/' : ''}${item.name}')` : (canEdit ? `fileEdit('${item.name}')` : `fileDownload('${item.name}')`)}">
|
||||
<span style='font-size:18px;'>${icon}</span> <span>${item.name}</span>
|
||||
</button>
|
||||
</td>
|
||||
<td>${isFolder ? '目录' : '文件'}</td>
|
||||
<td>${isFolder ? '-' : (item.size == null ? '-' : formatSize(item.size))}</td>
|
||||
<td>${item.mtime || '-'}</td>
|
||||
<td>
|
||||
<button class='file-op-btn' title='下载' onclick="fileDownload('${item.name}')">⬇️</button>
|
||||
<button class='file-op-btn' title='删除' onclick="fileDelete('${item.name}')">🗑️</button>
|
||||
<button class='file-op-btn' title='重命名' onclick="fileRename('${item.name}')">✏️</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
fileTableContainer.innerHTML = html;
|
||||
document.getElementById('file-check-all').onclick = function() {
|
||||
document.querySelectorAll('.file-check').forEach(cb => { cb.checked = this.checked; });
|
||||
};
|
||||
// 行选中交互
|
||||
document.querySelectorAll('.file-row').forEach(row => {
|
||||
row.onclick = function(e) {
|
||||
// 如果点击的是按钮或checkbox,不处理
|
||||
if (e.target.closest('.file-op-btn') || e.target.closest('.file-main-btn') || e.target.classList.contains('file-check')) return;
|
||||
row.classList.toggle('selected');
|
||||
const name = row.querySelector('.file-name-cell').getAttribute('data-name');
|
||||
const cb = row.querySelector('.file-check');
|
||||
cb.checked = row.classList.contains('selected');
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function formatSize(size) {
|
||||
if (size < 1024) return size + ' B';
|
||||
if (size < 1024*1024) return (size/1024).toFixed(1) + ' KB';
|
||||
if (size < 1024*1024*1024) return (size/1024/1024).toFixed(1) + ' MB';
|
||||
return (size/1024/1024/1024).toFixed(1) + ' GB';
|
||||
}
|
||||
|
||||
window.fileJumpTo = function(path) { loadFileTable(path); };
|
||||
|
||||
// New function to handle file editing
|
||||
window.fileEdit = async function(name) {
|
||||
const filePath = fileCurrentPath ? fileCurrentPath + '/' + name : name;
|
||||
currentEditingFilePath = filePath;
|
||||
currentFileTitle.textContent = `编辑文件: ${filePath}`;
|
||||
fileEditor.value = '加载中...';
|
||||
editDialogMask.style.display = 'flex';
|
||||
editDialog.style.display = 'flex';
|
||||
|
||||
try {
|
||||
const res = await fetch('/admin/get_file_content?path=' + encodeURIComponent(filePath));
|
||||
if (!res.ok) throw new Error(`Error fetching file: ${res.status} ${res.statusText}`);
|
||||
const data = await res.json();
|
||||
if (data.content !== undefined) {
|
||||
fileEditor.value = data.content;
|
||||
} else {
|
||||
fileEditor.value = '无法获取文件内容。';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load file content:', error);
|
||||
fileEditor.value = '加载文件内容失败: ' + error.message;
|
||||
}
|
||||
};
|
||||
|
||||
window.hideEditDialog = function() {
|
||||
editDialogMask.style.display = 'none';
|
||||
editDialog.style.display = 'none';
|
||||
currentEditingFilePath = null;
|
||||
fileEditor.value = '';
|
||||
};
|
||||
|
||||
// Add event listeners for save and cancel buttons in edit dialog
|
||||
editCancelBtn.onclick = window.hideEditDialog;
|
||||
editSaveBtn.onclick = async () => {
|
||||
if (!currentEditingFilePath) return;
|
||||
const content = fileEditor.value;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('path', currentEditingFilePath);
|
||||
formData.append('content', content);
|
||||
|
||||
try {
|
||||
// Temporarily disable save button and show saving status
|
||||
editSaveBtn.textContent = '保存中...';
|
||||
editSaveBtn.disabled = true;
|
||||
|
||||
const res = await fetch('/admin/save_file', {
|
||||
method: 'POST',
|
||||
body: formData // Use FormData for file content
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
showSuccessNotification('保存成功');
|
||||
window.hideEditDialog();
|
||||
// Optionally refresh the file list to update modified time
|
||||
loadFileTable(fileCurrentPath);
|
||||
} else {
|
||||
const errorText = await res.text();
|
||||
alert('保存失败: ' + res.status + ' ' + res.statusText + ', Details: ' + errorText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save file:', error);
|
||||
alert('保存文件失败: ' + error.message);
|
||||
} finally {
|
||||
// Re-enable save button and restore text
|
||||
editSaveBtn.textContent = '保存';
|
||||
editSaveBtn.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Function to show a custom success notification
|
||||
function showSuccessNotification(message) {
|
||||
const notification = document.createElement('div');
|
||||
notification.style.position = 'fixed';
|
||||
notification.style.top = '20px';
|
||||
notification.style.left = '50%';
|
||||
notification.style.transform = 'translateX(-50%)';
|
||||
notification.style.background = '#fff';
|
||||
notification.style.color = '#333';
|
||||
notification.style.padding = '12px 24px';
|
||||
notification.style.borderRadius = '24px';
|
||||
notification.style.boxShadow = '0 4px 12px rgba(0,0,0,0.1)';
|
||||
notification.style.zIndex = '3000';
|
||||
notification.style.display = 'flex';
|
||||
notification.style.alignItems = 'center';
|
||||
notification.style.gap = '10px';
|
||||
notification.style.fontSize = '17px';
|
||||
notification.style.opacity = '0';
|
||||
notification.style.transition = 'opacity 0.3s ease-in-out';
|
||||
|
||||
notification.innerHTML = `
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10" fill="#4CAF50"/>
|
||||
<path d="M8 12.3L11 15.3L16 9.3" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="checkmark"/>
|
||||
</svg>
|
||||
<span>${message}</span>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Fade in
|
||||
setTimeout(() => {
|
||||
notification.style.opacity = '1';
|
||||
}, 10);
|
||||
|
||||
// Fade out and remove after 3 seconds
|
||||
setTimeout(() => {
|
||||
notification.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
window.fileDownload = function(name) {
|
||||
const path = fileCurrentPath ? fileCurrentPath + '/' + name : name;
|
||||
window.open('/admin/download_file?path=' + encodeURIComponent(path), '_blank');
|
||||
};
|
||||
window.fileDelete = function(name) {
|
||||
// 这里只做简单实现,实际可弹窗确认
|
||||
if (!confirm('确定要删除 ' + name + ' 吗?')) return;
|
||||
const path = fileCurrentPath ? fileCurrentPath + '/' + name : name;
|
||||
fetch('/admin/delete_file?path=' + encodeURIComponent(path), {method:'DELETE'}).then(res => {
|
||||
if(res.ok) loadFileTable(fileCurrentPath);
|
||||
else alert('删除失败');
|
||||
});
|
||||
};
|
||||
window.fileRename = function(name) {
|
||||
const newName = prompt('输入新名称', name);
|
||||
if (!newName || newName === name) return;
|
||||
const path = fileCurrentPath ? fileCurrentPath + '/' + name : name;
|
||||
const form = new FormData();
|
||||
form.append('path', path);
|
||||
form.append('new_name', newName);
|
||||
fetch('/admin/rename_file', {method:'POST', body: form}).then(res => {
|
||||
if(res.ok) {
|
||||
showSuccessNotification('重命名成功');
|
||||
loadFileTable(fileCurrentPath);
|
||||
}
|
||||
else alert('重命名失败');
|
||||
});
|
||||
};
|
||||
|
||||
fileBtnBack.onclick = () => {
|
||||
if (!fileCurrentPath) return;
|
||||
const segs = fileCurrentPath.split('/');
|
||||
segs.pop();
|
||||
loadFileTable(segs.join('/'));
|
||||
};
|
||||
fileBtnRefresh.onclick = () => loadFileTable(fileCurrentPath);
|
||||
fileBtnAdd.onclick = () => {
|
||||
let types = fileCurrentPath === ''
|
||||
? [
|
||||
{type: 'project', label: '项目'},
|
||||
{type: 'folder', label: '文件夹'},
|
||||
{type: 'file', label: '文件'}
|
||||
]
|
||||
: [
|
||||
{type: 'folder', label: '文件夹'},
|
||||
{type: 'file', label: '文件'}
|
||||
];
|
||||
let typeBtns = types.map((t, i) =>
|
||||
`<button type='button' class='file-create-type-btn${i===0?' selected':''}' data-type='${t.type}'>${t.label}</button>`
|
||||
).join('');
|
||||
showFileDialog(`
|
||||
<div class='file-create-dialog-title'>新建</div>
|
||||
<div class='file-create-type-group'>${typeBtns}</div>
|
||||
<div class='file-create-input-wrap'>
|
||||
<input id='create-name' class='file-create-input' placeholder='名称' autocomplete='off'>
|
||||
</div>
|
||||
<div class='file-create-btn-row'>
|
||||
<button class='file-create-btn cancel' type='button'>取消</button>
|
||||
<button class='file-create-btn confirm' type='button'>创建</button>
|
||||
</div>
|
||||
`);
|
||||
setTimeout(() => {
|
||||
const btns = document.querySelectorAll('.file-create-type-btn');
|
||||
btns.forEach(btn => {
|
||||
btn.onclick = function() {
|
||||
btns.forEach(b => b.classList.remove('selected'));
|
||||
this.classList.add('selected');
|
||||
};
|
||||
});
|
||||
document.querySelector('.file-create-btn.cancel').onclick = hideFileDialog;
|
||||
document.querySelector('.file-create-btn.confirm').onclick = async () => {
|
||||
const nameInput = document.getElementById('create-name');
|
||||
const name = nameInput ? nameInput.value.trim() : '';
|
||||
const selectedBtn = document.querySelector('.file-create-type-btn.selected');
|
||||
const type = selectedBtn ? selectedBtn.getAttribute('data-type') : types[0].type;
|
||||
const typeText = types.find(t => t.type === type)?.label || '';
|
||||
if (!name) {
|
||||
nameInput.focus();
|
||||
nameInput.placeholder = '名称不能为空';
|
||||
return;
|
||||
}
|
||||
if (type === 'project') {
|
||||
showProjectDetailDialog(name);
|
||||
return;
|
||||
}
|
||||
const data = { path: fileCurrentPath, name, type };
|
||||
const res = await fetch('/admin/create_file_or_folder', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (res.ok) {
|
||||
const result = await res.json();
|
||||
if (result.status === 'success') {
|
||||
showSuccessNotification(`${typeText} "${name}" 创建成功`);
|
||||
loadFileTable(fileCurrentPath);
|
||||
hideFileDialog();
|
||||
} else {
|
||||
alert(`${typeText} "${name}" 创建失败: ` + (result.detail || JSON.stringify(result)));
|
||||
}
|
||||
} else {
|
||||
const errorText = await res.text();
|
||||
alert(`${typeText} "${name}" 创建失败: ` + res.status + ' ' + res.statusText + ', Details: ' + errorText);
|
||||
}
|
||||
};
|
||||
}, 10);
|
||||
};
|
||||
|
||||
// ========== 迁移 showFileDialog、hideFileDialog、showProjectDetailDialog ==========
|
||||
const fileDialogMask = document.getElementById('file-dialog-mask');
|
||||
const fileDialog = document.getElementById('file-dialog');
|
||||
function showFileDialog(html, onok, oncancel) {
|
||||
fileDialog.innerHTML = html;
|
||||
fileDialogMask.style.display = 'flex';
|
||||
fileDialog.style.display = 'block';
|
||||
const okBtn = fileDialog.querySelector('.file-dialog-ok');
|
||||
const cancelBtn = fileDialog.querySelector('.file-dialog-cancel');
|
||||
if (okBtn) okBtn.onclick = () => { if(onok) onok(); hideFileDialog(); };
|
||||
if (cancelBtn) cancelBtn.onclick = hideFileDialog;
|
||||
fileDialogMask.onclick = function(e) {
|
||||
if (e.target === fileDialogMask) {
|
||||
hideFileDialog();
|
||||
}
|
||||
};
|
||||
}
|
||||
function hideFileDialog() {
|
||||
fileDialogMask.style.display = 'none';
|
||||
fileDialog.style.display = 'none';
|
||||
}
|
||||
function showProjectDetailDialog(projectName) {
|
||||
let inputParams = [];
|
||||
let outputParams = [];
|
||||
function syncParams() {
|
||||
document.querySelectorAll('#input-param-list > div').forEach((div, i) => {
|
||||
const [nameInput, typeSelect] = div.querySelectorAll('input,select');
|
||||
if (inputParams[i]) {
|
||||
inputParams[i].name = nameInput.value;
|
||||
inputParams[i].type = typeSelect.value;
|
||||
}
|
||||
});
|
||||
document.querySelectorAll('#output-param-list > div').forEach((div, i) => {
|
||||
const [nameInput, typeSelect] = div.querySelectorAll('input,select');
|
||||
if (outputParams[i]) {
|
||||
outputParams[i].name = nameInput.value;
|
||||
outputParams[i].type = typeSelect.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
function render() {
|
||||
const descInput = document.getElementById('project-desc');
|
||||
const savedDesc = descInput ? descInput.value : '';
|
||||
let html = `
|
||||
<div class='file-create-dialog-title'>新建项目:${projectName}</div>
|
||||
<div style='margin-bottom:18px;'>
|
||||
<label style='font-weight:bold;font-size:20px;'>函数描述:</label><br>
|
||||
<input id='project-desc' class='file-create-input' placeholder='输入函数描述' style='margin-top:10px;margin-bottom:0;' value='${savedDesc}'>
|
||||
</div>
|
||||
<div style='margin-bottom:18px;'>
|
||||
<label style='font-weight:bold;font-size:20px;'>输入参数:</label><br>
|
||||
<div id='input-param-list'>
|
||||
${inputParams.map((p, i) => `
|
||||
<div style='display:flex;align-items:center;gap:10px;margin-bottom:10px;'>
|
||||
<input class='file-create-input' style='flex:2;padding:10px 16px;font-size:16px;' placeholder='参数名称' value='${p.name||''}'>
|
||||
<select class='file-create-input' style='flex:1;padding:10px 16px;font-size:16px;background:#fff;'>
|
||||
<option value='str' ${p.type === 'str' ? 'selected' : ''}>字符串 (str)</option>
|
||||
<option value='int' ${p.type === 'int' ? 'selected' : ''}>整数 (int)</option>
|
||||
<option value='float' ${p.type === 'float' ? 'selected' : ''}>浮点数 (float)</option>
|
||||
<option value='bool' ${p.type === 'bool' ? 'selected' : ''}>布尔值 (bool)</option>
|
||||
<option value='list' ${p.type === 'list' ? 'selected' : ''}>列表 (list)</option>
|
||||
<option value='dict' ${p.type === 'dict' ? 'selected' : ''}>字典 (dict)</option>
|
||||
</select>
|
||||
<button class='file-create-btn cancel param-del-btn' style='padding:6px 16px;font-size:15px;' data-idx='${i}'>删除</button>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<button class='file-create-btn confirm' id='add-input-param' style='width:100%;margin-top:8px;'>添加输入参数</button>
|
||||
</div>
|
||||
<div style='margin-bottom:18px;'>
|
||||
<label style='font-weight:bold;font-size:20px;'>返回参数:</label><br>
|
||||
<div id='output-param-list'>
|
||||
${outputParams.map((p, i) => `
|
||||
<div style='display:flex;align-items:center;gap:10px;margin-bottom:10px;'>
|
||||
<input class='file-create-input' style='flex:2;padding:10px 16px;font-size:16px;' placeholder='参数名称' value='${p.name||''}'>
|
||||
<select class='file-create-input' style='flex:1;padding:10px 16px;font-size:16px;background:#fff;'>
|
||||
<option value='str' ${p.type === 'str' ? 'selected' : ''}>字符串 (str)</option>
|
||||
<option value='int' ${p.type === 'int' ? 'selected' : ''}>整数 (int)</option>
|
||||
<option value='float' ${p.type === 'float' ? 'selected' : ''}>浮点数 (float)</option>
|
||||
<option value='bool' ${p.type === 'bool' ? 'selected' : ''}>布尔值 (bool)</option>
|
||||
<option value='list' ${p.type === 'list' ? 'selected' : ''}>列表 (list)</option>
|
||||
<option value='dict' ${p.type === 'dict' ? 'selected' : ''}>字典 (dict)</option>
|
||||
</select>
|
||||
<button class='file-create-btn cancel param-del-btn' style='padding:6px 16px;font-size:15px;' data-idx='${i}'>删除</button>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<button class='file-create-btn confirm' id='add-output-param' style='width:100%;margin-top:8px;'>添加返回参数</button>
|
||||
</div>
|
||||
<div class='file-create-btn-row'>
|
||||
<button class='file-create-btn cancel' type='button'>取消</button>
|
||||
<button class='file-create-btn confirm' id='project-create-btn' type='button'>创建</button>
|
||||
</div>
|
||||
`;
|
||||
showFileDialog(html);
|
||||
fileDialog.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
if (e.target.classList.contains('param-del-btn')) {
|
||||
syncParams();
|
||||
const idx = parseInt(e.target.getAttribute('data-idx'));
|
||||
const isInput = e.target.closest('#input-param-list') !== null;
|
||||
if (isInput) inputParams.splice(idx, 1);
|
||||
else outputParams.splice(idx, 1);
|
||||
render();
|
||||
}
|
||||
if (e.target.id === 'add-input-param') {
|
||||
syncParams();
|
||||
inputParams.push({name:'',type:'str'});
|
||||
render();
|
||||
}
|
||||
if (e.target.id === 'add-output-param') {
|
||||
syncParams();
|
||||
outputParams.push({name:'',type:'str'});
|
||||
render();
|
||||
}
|
||||
};
|
||||
document.querySelector('.file-create-btn.cancel').onclick = hideFileDialog;
|
||||
document.getElementById('project-create-btn').onclick = async () => {
|
||||
syncParams();
|
||||
const desc = document.getElementById('project-desc').value.trim();
|
||||
const inputList = inputParams.filter(p => p.name);
|
||||
const outputList = outputParams.filter(p => p.name);
|
||||
function genPyType(t) {
|
||||
return {str:'str',int:'int',float:'float',bool:'bool',list:'list',dict:'dict'}[t]||'str';
|
||||
}
|
||||
function genDefaultValue(t) {
|
||||
return {str: "''", int: '0', float: '0.0', bool: 'False', list: '[]', dict: '{}'}[t]||"''";
|
||||
}
|
||||
const paramStr = inputList.map(p => `${p.name}: ${genPyType(p.type)}`).join(', ');
|
||||
const docParams = inputList.map(p => ` ${p.name}: ${p.type}`).join('\n');
|
||||
const docReturns = outputList.map(p => ` ${p.name}: ${p.type}`).join('\n');
|
||||
const funcCode = `def ${projectName}(${paramStr}):\n """\n ${desc}\n 参数:\n${docParams}\n 返回:\n${docReturns}\n """\n # TODO: 实现你的业务逻辑\n return {}`;
|
||||
const config = {
|
||||
url: `/function/${projectName}`,
|
||||
method: 'GET',
|
||||
name: desc,
|
||||
parameters: inputList.map(p => ({
|
||||
name: p.name,
|
||||
type: p.type,
|
||||
required: true,
|
||||
description: '',
|
||||
default: ''
|
||||
}))
|
||||
};
|
||||
const data = {
|
||||
function_name: projectName,
|
||||
files: {
|
||||
function: funcCode,
|
||||
config: JSON.stringify(config, null, 2),
|
||||
intro: desc
|
||||
}
|
||||
};
|
||||
const res = await fetch('/admin/create_function', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (res.ok) {
|
||||
const result = await res.json();
|
||||
if (result.status === 'success') {
|
||||
showSuccessNotification(`项目 "${projectName}" 创建成功`);
|
||||
loadFileTable(fileCurrentPath);
|
||||
hideFileDialog();
|
||||
} else {
|
||||
alert(`项目 "${projectName}" 创建失败: ` + (result.detail || JSON.stringify(result)));
|
||||
}
|
||||
} else {
|
||||
const errorText = await res.text();
|
||||
alert(`项目 "${projectName}" 创建失败: ` + res.status + ' ' + res.statusText + ', Details: ' + errorText);
|
||||
}
|
||||
};
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
// 页面加载时自动加载根目录
|
||||
loadFileTable('');
|
||||
});
|
||||
28
admin/static/js/log.js
Normal file
28
admin/static/js/log.js
Normal file
@@ -0,0 +1,28 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
let logWs = null;
|
||||
let logBuffer = '';
|
||||
function startLogStream() {
|
||||
if (logWs && logWs.readyState === WebSocket.OPEN) return;
|
||||
if (logWs) logWs.close();
|
||||
logBuffer = '';
|
||||
logWs = new WebSocket((location.protocol==='https:'?'wss://':'ws://')+location.host+'/admin/logs/stream');
|
||||
logWs.onopen = function() {
|
||||
logBuffer = '连接成功,等待输出...\n';
|
||||
renderLogStream();
|
||||
};
|
||||
logWs.onmessage = function(e) {
|
||||
logBuffer += e.data;
|
||||
renderLogStream();
|
||||
};
|
||||
logWs.onclose = function(){};
|
||||
logWs.onerror = function(error){};
|
||||
}
|
||||
function renderLogStream() {
|
||||
const preLog = document.getElementById('log-cards');
|
||||
if (preLog) {
|
||||
preLog.textContent = logBuffer;
|
||||
preLog.scrollTop = preLog.scrollHeight;
|
||||
}
|
||||
}
|
||||
startLogStream();
|
||||
});
|
||||
24
admin/static/js/sysinfo.js
Normal file
24
admin/static/js/sysinfo.js
Normal file
@@ -0,0 +1,24 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
async function fetchStats() {
|
||||
try {
|
||||
const res1 = await fetch('/functions');
|
||||
let totalFunctions = '--';
|
||||
if (res1.ok) {
|
||||
const data = await res1.json();
|
||||
totalFunctions = data.functions ? data.functions.length : '--';
|
||||
}
|
||||
document.getElementById('total-functions').textContent = totalFunctions;
|
||||
const res2 = await fetch('/admin/call_stats_data');
|
||||
let totalCalls = '--';
|
||||
if (res2.ok) {
|
||||
const data = await res2.json();
|
||||
totalCalls = data.total !== undefined ? data.total : '--';
|
||||
}
|
||||
document.getElementById('total-calls').textContent = totalCalls;
|
||||
} catch(e) {
|
||||
document.getElementById('total-functions').textContent = '--';
|
||||
document.getElementById('total-calls').textContent = '--';
|
||||
}
|
||||
}
|
||||
fetchStats();
|
||||
});
|
||||
1
admin/static/logo.svg
Normal file
1
admin/static/logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 2.4 MiB |
1182
admin/static/style.css
Normal file
1182
admin/static/style.css
Normal file
File diff suppressed because it is too large
Load Diff
22
admin/templates/apidebug.html
Normal file
22
admin/templates/apidebug.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="page-top-bar">
|
||||
<span class="page-title">接口调试</span>
|
||||
</div>
|
||||
<div style="display:flex;height:calc(100vh - 60px - 20px);background:#f7faff;">
|
||||
<!-- 左侧API列表 -->
|
||||
<div id="apilist-panel" style="width:270px;background:#f3f7fd;border-right:1px solid #e3eaf2;display:flex;flex-direction:column;">
|
||||
<div style="padding:18px 18px 8px 18px;">
|
||||
<input id="api-search" type="text" placeholder="搜索 API" style="width:100%;padding:8px 12px;border-radius:8px;border:1px solid #e3eaf2;background:#fff;outline:none;font-size:15px;">
|
||||
</div>
|
||||
<div id="api-list" style="flex:1;overflow-y:auto;padding:0 8px 8px 8px;"></div>
|
||||
</div>
|
||||
<!-- 右侧详情区 -->
|
||||
<div id="apidetail-panel" style="flex:1;display:flex;flex-direction:column;align-items:stretch;overflow-y:auto;padding:0 0 0 0;">
|
||||
<div id="api-detail-content" style="margin:32px auto 0 auto;width:90%;max-width:900px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block body_end %}
|
||||
<script src="/static/js/apidebug.js"></script>
|
||||
{% endblock %}
|
||||
31
admin/templates/base.html
Normal file
31
admin/templates/base.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>CloudFuse 云函数管理系统</title>
|
||||
<meta charset="utf-8">
|
||||
<link rel="icon" type="image/svg+xml" href="/static/logo.svg">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="main-layout">
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-logo">
|
||||
<img src="/static/logo.svg" alt="logo" style="width:40px;height:40px;">
|
||||
</div>
|
||||
<div class="sidebar-title">CloudFuse</div>
|
||||
<div class="sidebar-menu">
|
||||
<a class="sidebar-menu-item {% if page=='sysinfo' %}active{% endif %}" href="/admin/sysinfo">基础信息</a>
|
||||
<a class="sidebar-menu-item {% if page=='apidebug' %}active{% endif %}" href="/admin/apidebug">接口调试</a>
|
||||
<a class="sidebar-menu-item {% if page=='log' %}active{% endif %}" href="/admin/log">日志信息</a>
|
||||
<a class="sidebar-menu-item {% if page=='file' %}active{% endif %}" href="/admin/file">文件管理</a>
|
||||
</div>
|
||||
<div class="sidebar-footer">© 2024 YourName</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% block body_end %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
41
admin/templates/file.html
Normal file
41
admin/templates/file.html
Normal file
@@ -0,0 +1,41 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="page-top-bar">
|
||||
<span class="page-title">文件管理</span>
|
||||
</div>
|
||||
<div style="padding:0 36px;">
|
||||
<div id="file-toolbar" style="display:flex;align-items:center;gap:10px;margin:18px 0 10px 0;">
|
||||
<button id="file-btn-back" class="file-toolbar-btn" title="返回上一级"><span>⬅️</span></button>
|
||||
<button id="file-btn-add" class="file-toolbar-btn" title="新建文件夹/文件"><span>➕</span></button>
|
||||
<button id="file-btn-refresh" class="file-toolbar-btn" title="刷新"><span>🔄</span></button>
|
||||
<button id="file-btn-upload" class="file-toolbar-btn" title="上传"><span>⏫</span></button>
|
||||
<div id="file-path-nav" class="file-path-box"></div>
|
||||
</div>
|
||||
<div id="file-table-container"></div>
|
||||
</div>
|
||||
<input type="file" id="file-folder-upload" webkitdirectory directory multiple style="display:none">
|
||||
<div id="file-dialog-mask" style="display:none;position:fixed;left:0;top:0;width:100vw;height:100vh;background:rgba(0,0,0,0.18);z-index:2000;align-items:center;justify-content:center;"></div>
|
||||
<div id="file-dialog" style="display:none;position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);background:#fff;border-radius:12px;box-shadow:0 4px 32px rgba(0,0,0,0.13);z-index:2001;min-width:320px;max-width:90vw;padding:32px 28px 24px 28px;"></div>
|
||||
<div id="edit-dialog" class="edit-dialog" style="display: none;">
|
||||
<div class="edit-dialog-content">
|
||||
<button class="edit-dialog-close" onclick="hideEditDialog()">×</button>
|
||||
<div class="file-list-panel" style="display: none;">
|
||||
<div class="file-list-title">文件列表</div>
|
||||
<div class="file-list-content"></div>
|
||||
</div>
|
||||
<div class="file-content-panel">
|
||||
<div id="current-file-title" style="font-weight: bold; font-size: 16px; margin-bottom: 10px;"></div>
|
||||
<div id="file-content-area" style="flex: 1; display: flex; flex-direction: column; min-height: 0;">
|
||||
<textarea id="file-editor" class="file-editor" placeholder="加载中..."></textarea>
|
||||
</div>
|
||||
<div class="dialog-buttons">
|
||||
<button id="edit-save-btn" style="background:#2196F3;color:#fff;border:none;border-radius:6px;padding:8px 22px;font-size:15px;">保存</button>
|
||||
<button id="edit-cancel-btn" style="background:#e3f0ff;color:#2196F3;border:none;border-radius:6px;padding:8px 22px;font-size:15px;">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block body_end %}
|
||||
<script src="/static/js/file.js"></script>
|
||||
{% endblock %}
|
||||
14
admin/templates/log.html
Normal file
14
admin/templates/log.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="page-top-bar">
|
||||
<span class="page-title">日志信息</span>
|
||||
</div>
|
||||
<div style="padding:0 36px;max-width:1400px;margin:auto;">
|
||||
<div id="log-realtime-panel">
|
||||
<pre id="log-cards" class="log-content-pre"></pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block body_end %}
|
||||
<script src="/static/js/log.js"></script>
|
||||
{% endblock %}
|
||||
33
admin/templates/sysinfo.html
Normal file
33
admin/templates/sysinfo.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="page-top-bar">
|
||||
<span class="page-title">基础信息</span>
|
||||
</div>
|
||||
<div style="padding:0 36px;">
|
||||
<div class="sysinfo-top">
|
||||
<div class="sysinfo-card">
|
||||
<div class="sysinfo-logo">
|
||||
<img src="/static/logo.svg" alt="logo" style="width:48px;height:48px;">
|
||||
</div>
|
||||
<div class="sysinfo-meta">
|
||||
<div class="sysinfo-title">CloudFuse 云函数管理系统</div>
|
||||
<div class="sysinfo-author">作者:YourName</div>
|
||||
<div class="sysinfo-version">版本:v1.0.0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-cards">
|
||||
<div class="stats-card">
|
||||
<div class="stats-card-title">总函数个数</div>
|
||||
<div class="stats-card-value" id="total-functions">--</div>
|
||||
</div>
|
||||
<div class="stats-card">
|
||||
<div class="stats-card-title">总调用次数</div>
|
||||
<div class="stats-card-value" id="total-calls">--</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block body_end %}
|
||||
<script src="/static/js/sysinfo.js"></script>
|
||||
{% endblock %}
|
||||
142
apps/README.md
Normal file
142
apps/README.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# 云函数开发与接入指南
|
||||
|
||||
本说明文档介绍如何为 CloudFuse 云函数管理系统添加和开发新函数。
|
||||
|
||||
---
|
||||
|
||||
## 1. 函数目录结构
|
||||
|
||||
每个函数需独立一个目录,目录名即为函数名,结构如下:
|
||||
|
||||
```
|
||||
function_name/
|
||||
├── function.py # 函数实现(必需,需有同名函数)
|
||||
├── config.json # 函数API配置(必需)
|
||||
├── intro.md # 简要介绍(必需)
|
||||
├── requirements.txt # 依赖(可选,自动安装)
|
||||
├── readme.md # 详细文档(可选)
|
||||
└── xlsx_files/ # Excel文件目录(可选)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 必需文件说明
|
||||
|
||||
### function.py
|
||||
- 必须包含一个与目录同名的函数。
|
||||
- 支持参数类型注解和默认值。
|
||||
|
||||
示例:
|
||||
```python
|
||||
def example_function(param1: str, param2: int = 0):
|
||||
"""示例函数"""
|
||||
return {"result": f"处理 {param1} 和 {param2}"}
|
||||
```
|
||||
|
||||
### config.json
|
||||
- 定义API接口信息、参数类型、描述等。
|
||||
|
||||
示例:
|
||||
```json
|
||||
{
|
||||
"url": "/function/example_function",
|
||||
"method": "GET",
|
||||
"name": "示例函数的简要描述",
|
||||
"parameters": [
|
||||
{"name": "param1", "type": "string", "required": true, "description": "第一个参数", "default": ""},
|
||||
{"name": "param2", "type": "number", "required": false, "description": "第二个参数", "default": "0"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### intro.md
|
||||
- 简要介绍函数用途和特点。
|
||||
|
||||
---
|
||||
|
||||
## 3. 可选文件说明
|
||||
|
||||
- **requirements.txt**:列出依赖包,系统自动检测并安装。
|
||||
- **readme.md**:详细文档,建议包含用法、参数、返回值、示例等。
|
||||
- **xlsx_files/**:如需处理Excel文件可放于此。
|
||||
|
||||
---
|
||||
|
||||
## 4. 支持的参数类型
|
||||
- string
|
||||
- number
|
||||
- boolean
|
||||
- array
|
||||
- object
|
||||
|
||||
---
|
||||
|
||||
## 5. 示例
|
||||
|
||||
### 简单回显函数
|
||||
```python
|
||||
def echo_message(message: str = "Hello World"):
|
||||
return {"message": message}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "/function/echo_message",
|
||||
"method": "GET",
|
||||
"name": "函数中文名",
|
||||
"parameters": [
|
||||
{"name": "message", "type": "string", "required": true, "description": "要回显的消息内容", "default": "Hello World"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Excel处理函数
|
||||
```python
|
||||
import pandas as pd
|
||||
import random
|
||||
import os
|
||||
|
||||
def get_random_xlsx_line(filename: str):
|
||||
file_path = os.path.join('xlsx_files', filename)
|
||||
if not os.path.exists(file_path):
|
||||
return {"error": "文件不存在"}
|
||||
df = pd.read_excel(file_path)
|
||||
random_row = df.iloc[random.randint(0, len(df)-1)]
|
||||
return random_row.to_dict()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 开发与调试建议
|
||||
|
||||
- 本地先测试函数逻辑,确保可用。
|
||||
- 检查必需文件和参数类型。
|
||||
- 返回值必须可JSON序列化,建议用dict。
|
||||
- 依赖请写入 requirements.txt,系统自动安装。
|
||||
- 错误处理要明确,建议 try-except。
|
||||
- 上传/保存后可在Web界面直接测试。
|
||||
|
||||
---
|
||||
|
||||
## 7. 上传与管理
|
||||
|
||||
- 推荐通过 Web 管理界面上传/新建函数,系统自动校验、安装依赖、刷新路由。
|
||||
- 也可手动将函数目录放入 apps/,重启服务或点击刷新。
|
||||
|
||||
---
|
||||
|
||||
## 8. 常见问题
|
||||
|
||||
- **依赖未安装**:请检查 requirements.txt 格式和包名。
|
||||
- **参数报错**:请检查 config.json 参数类型和函数签名。
|
||||
- **函数不可用**:请确保函数名与目录名一致,且有 function.py。
|
||||
- **返回值异常**:请确保返回值可被 JSON 序列化。
|
||||
|
||||
---
|
||||
|
||||
## 9. 最佳实践
|
||||
|
||||
- 遵循 PEP8 规范,适当注释
|
||||
- 合理设计参数和默认值
|
||||
- 明确错误处理和返回格式
|
||||
- 文档齐全,便于协作和维护
|
||||
6
apps/calculate/README.md
Normal file
6
apps/calculate/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
GET /function/calculate
|
||||
参数:
|
||||
- num1: 第一个数字(必需,默认:0)
|
||||
- num2: 第二个数字(必需,默认:0)
|
||||
- operation: 运算符(必需,可选值:add/subtract/multiply/divide,默认:add)
|
||||
返回:计算结果和运算说明
|
||||
BIN
apps/calculate/__pycache__/function.cpython-311.pyc
Normal file
BIN
apps/calculate/__pycache__/function.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/calculate/__pycache__/function.cpython-313.pyc
Normal file
BIN
apps/calculate/__pycache__/function.cpython-313.pyc
Normal file
Binary file not shown.
BIN
apps/calculate/__pycache__/function.cpython-39.pyc
Normal file
BIN
apps/calculate/__pycache__/function.cpython-39.pyc
Normal file
Binary file not shown.
28
apps/calculate/config.json
Normal file
28
apps/calculate/config.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"url": "/function/calculate",
|
||||
"method": "GET",
|
||||
"name": "计算器函数",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "num1",
|
||||
"type": "number",
|
||||
"required": true,
|
||||
"description": "第一个数字",
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "num2",
|
||||
"type": "number",
|
||||
"required": true,
|
||||
"description": "第二个数字",
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "operation",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "运算符(add/subtract/multiply/divide)",
|
||||
"default": "add"
|
||||
}
|
||||
]
|
||||
}
|
||||
32
apps/calculate/function.py
Normal file
32
apps/calculate/function.py
Normal file
@@ -0,0 +1,32 @@
|
||||
def calculate(num1: float, num2: float, operation: str = "add"):
|
||||
"""
|
||||
多参数示例函数,执行基本的数学运算
|
||||
|
||||
Args:
|
||||
num1 (float): 第一个数字
|
||||
num2 (float): 第二个数字
|
||||
operation (str): 运算符(add/subtract/multiply/divide)
|
||||
|
||||
Returns:
|
||||
dict: 包含计算结果和运算说明
|
||||
"""
|
||||
operations = {
|
||||
"add": (lambda x, y: x + y, "加"),
|
||||
"subtract": (lambda x, y: x - y, "减"),
|
||||
"multiply": (lambda x, y: x * y, "乘"),
|
||||
"divide": (lambda x, y: x / y if y != 0 else "除数不能为0", "除")
|
||||
}
|
||||
|
||||
try:
|
||||
if operation not in operations:
|
||||
return {"error": "不支持的运算符"}
|
||||
|
||||
func, op_name = operations[operation]
|
||||
result = func(float(num1), float(num2))
|
||||
|
||||
return {
|
||||
"result": result,
|
||||
"description": f"{num1} {op_name} {num2} = {result}"
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
6
apps/calculate/intro.md
Normal file
6
apps/calculate/intro.md
Normal file
@@ -0,0 +1,6 @@
|
||||
GET /function/calculate
|
||||
参数:
|
||||
- num1: 第一个数字(必需,默认:0)
|
||||
- num2: 第二个数字(必需,默认:0)
|
||||
- operation: 运算符(必需,可选值:add/subtract/multiply/divide,默认:add)
|
||||
返回:计算结果和运算说明
|
||||
1
apps/calculate/requirements.txt
Normal file
1
apps/calculate/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
# 仅使用Python标准库
|
||||
4
apps/echo_message/README.md
Normal file
4
apps/echo_message/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
GET /function/echo_message
|
||||
参数:
|
||||
- message: 要回显的消息(必需,默认:Hello!)
|
||||
返回:原始消息和时间戳
|
||||
BIN
apps/echo_message/__pycache__/function.cpython-311.pyc
Normal file
BIN
apps/echo_message/__pycache__/function.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/echo_message/__pycache__/function.cpython-313.pyc
Normal file
BIN
apps/echo_message/__pycache__/function.cpython-313.pyc
Normal file
Binary file not shown.
BIN
apps/echo_message/__pycache__/function.cpython-39.pyc
Normal file
BIN
apps/echo_message/__pycache__/function.cpython-39.pyc
Normal file
Binary file not shown.
14
apps/echo_message/config.json
Normal file
14
apps/echo_message/config.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"url": "/function/echo_message",
|
||||
"method": "GET",
|
||||
"name": "一个参数的示例函数,回显消息的示例函数",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "message",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "要回显的消息内容",
|
||||
"default": "Hello Word!"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
apps/echo_message/function.py
Normal file
15
apps/echo_message/function.py
Normal file
@@ -0,0 +1,15 @@
|
||||
def echo_message(message: str):
|
||||
"""
|
||||
单参数示例函数,回显输入的消息
|
||||
|
||||
Args:
|
||||
message (str): 要回显的消息
|
||||
|
||||
Returns:
|
||||
dict: 包含原始消息和时间戳
|
||||
"""
|
||||
from datetime import datetime
|
||||
return {
|
||||
"original_message": message,
|
||||
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
4
apps/echo_message/intro.md
Normal file
4
apps/echo_message/intro.md
Normal file
@@ -0,0 +1,4 @@
|
||||
GET /function/echo_message
|
||||
参数:
|
||||
- message: 要回显的消息(必需,默认:Hello!)
|
||||
返回:原始消息和时间戳
|
||||
1
apps/echo_message/requirements.txt
Normal file
1
apps/echo_message/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
# 仅使用Python标准库
|
||||
BIN
apps/example_function/__pycache__/function.cpython-311.pyc
Normal file
BIN
apps/example_function/__pycache__/function.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/example_function/__pycache__/function.cpython-313.pyc
Normal file
BIN
apps/example_function/__pycache__/function.cpython-313.pyc
Normal file
Binary file not shown.
BIN
apps/example_function/__pycache__/function.cpython-39.pyc
Normal file
BIN
apps/example_function/__pycache__/function.cpython-39.pyc
Normal file
Binary file not shown.
6
apps/example_function/config.json
Normal file
6
apps/example_function/config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"url": "/function/example_function",
|
||||
"method": "GET",
|
||||
"name": "无参数示例函数 - 返回Hello World",
|
||||
"parameters": []
|
||||
}
|
||||
5
apps/example_function/function.py
Normal file
5
apps/example_function/function.py
Normal file
@@ -0,0 +1,5 @@
|
||||
def example_function():
|
||||
"""
|
||||
示例函数,返回Hello World
|
||||
"""
|
||||
return {"message": "Hello World"}
|
||||
3
apps/example_function/intro.md
Normal file
3
apps/example_function/intro.md
Normal file
@@ -0,0 +1,3 @@
|
||||
GET /function/example_function
|
||||
返回:Hello World 示例响应
|
||||
参数:无
|
||||
8
apps/example_function/readme.md
Normal file
8
apps/example_function/readme.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# 示例函数说明
|
||||
|
||||
## 接口说明
|
||||
请求方式:GET
|
||||
接口地址:/function/example_function
|
||||
|
||||
## 返回值
|
||||
- 返回一个包含消息的JSON对象:{"message": "这是一个示例函数"}
|
||||
2
apps/functions/README.md
Normal file
2
apps/functions/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
GET /functions
|
||||
返回:系统中所有可用函数的列表及其配置信息
|
||||
BIN
apps/functions/__pycache__/function.cpython-311.pyc
Normal file
BIN
apps/functions/__pycache__/function.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/functions/__pycache__/function.cpython-313.pyc
Normal file
BIN
apps/functions/__pycache__/function.cpython-313.pyc
Normal file
Binary file not shown.
BIN
apps/functions/__pycache__/function.cpython-39.pyc
Normal file
BIN
apps/functions/__pycache__/function.cpython-39.pyc
Normal file
Binary file not shown.
6
apps/functions/config.json
Normal file
6
apps/functions/config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"url": "/functions",
|
||||
"method": "GET",
|
||||
"name": "获取所有可用函数列表",
|
||||
"parameters": []
|
||||
}
|
||||
30
apps/functions/function.py
Normal file
30
apps/functions/function.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import os
|
||||
import json
|
||||
from config import PATHS
|
||||
from fastapi import HTTPException
|
||||
|
||||
def functions():
|
||||
"""
|
||||
获取所有可用函数列表
|
||||
"""
|
||||
try:
|
||||
functions = []
|
||||
apps_dir = PATHS["APPS_DIR"]
|
||||
|
||||
for item in os.listdir(apps_dir):
|
||||
if os.path.isdir(os.path.join(apps_dir, item)) and not item.startswith('__'):
|
||||
config_path = os.path.join(apps_dir, item, 'config.json')
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
functions.append({
|
||||
"name": item,
|
||||
"url": config["url"],
|
||||
"method": config["method"],
|
||||
"display_name": config.get("name", item),
|
||||
"parameters": config["parameters"]
|
||||
})
|
||||
|
||||
return {"functions": functions}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
2
apps/functions/intro.md
Normal file
2
apps/functions/intro.md
Normal file
@@ -0,0 +1,2 @@
|
||||
GET /functions
|
||||
返回:系统中所有可用函数的列表及其配置信息
|
||||
BIN
apps/get_random_xlsx_line/__pycache__/function.cpython-311.pyc
Normal file
BIN
apps/get_random_xlsx_line/__pycache__/function.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/get_random_xlsx_line/__pycache__/function.cpython-313.pyc
Normal file
BIN
apps/get_random_xlsx_line/__pycache__/function.cpython-313.pyc
Normal file
Binary file not shown.
BIN
apps/get_random_xlsx_line/__pycache__/function.cpython-39.pyc
Normal file
BIN
apps/get_random_xlsx_line/__pycache__/function.cpython-39.pyc
Normal file
Binary file not shown.
14
apps/get_random_xlsx_line/config.json
Normal file
14
apps/get_random_xlsx_line/config.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"url": "/function/get_random_xlsx_line",
|
||||
"method": "GET",
|
||||
"name": "从Excel文件中随机获取一行数据",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "filename",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Excel文件名称(需位于xlsx_files目录下)",
|
||||
"default": "中国近现代史单选.xlsx"
|
||||
}
|
||||
]
|
||||
}
|
||||
33
apps/get_random_xlsx_line/function.py
Normal file
33
apps/get_random_xlsx_line/function.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import pandas as pd
|
||||
import random
|
||||
import os
|
||||
|
||||
def get_random_xlsx_line(filename: str):
|
||||
"""
|
||||
从Excel文件中随机获取一行数据
|
||||
|
||||
Args:
|
||||
filename (str): Excel文件名
|
||||
|
||||
Returns:
|
||||
dict: 包含随机行数据的字典
|
||||
"""
|
||||
try:
|
||||
file_path = os.path.join("apps", "get_random_xlsx_line", "xlsx_files", filename)
|
||||
df = pd.read_excel(file_path)
|
||||
|
||||
if df.empty:
|
||||
return {"error": "Excel文件为空"}
|
||||
|
||||
# 获取随机行
|
||||
random_row = df.iloc[random.randint(0, len(df)-1)]
|
||||
|
||||
# 只返回题目内容(第一列)
|
||||
if len(df.columns) > 0:
|
||||
question = random_row[df.columns[0]]
|
||||
return {"data": question}
|
||||
else:
|
||||
return {"error": "Excel文件格式不正确"}
|
||||
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
3
apps/get_random_xlsx_line/intro.md
Normal file
3
apps/get_random_xlsx_line/intro.md
Normal file
@@ -0,0 +1,3 @@
|
||||
GET /function/get_random_xlsx_line
|
||||
参数:filename=中国近现代史单选.xlsx
|
||||
返回:随机一行内容(单列返回字符串,多列返回数组)
|
||||
11
apps/get_random_xlsx_line/readme.md
Normal file
11
apps/get_random_xlsx_line/readme.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# 随机返回Excel文件中的一行数据
|
||||
|
||||
## 接口说明
|
||||
请求方式:GET
|
||||
接口地址:/function/get_random_xlsx_line
|
||||
参数:
|
||||
- filename: Excel文件名称(必须位于xlsx_files目录下)
|
||||
|
||||
## 返回值
|
||||
- 单列Excel:返回随机一行的字符串
|
||||
- 多列Excel:返回随机一行的所有列值(数组格式)
|
||||
2
apps/get_random_xlsx_line/requirements.txt
Normal file
2
apps/get_random_xlsx_line/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
pandas>=1.3.0
|
||||
openpyxl>=3.0.0
|
||||
BIN
apps/get_random_xlsx_line/xlsx_files/中国近现代史单选.xlsx
Normal file
BIN
apps/get_random_xlsx_line/xlsx_files/中国近现代史单选.xlsx
Normal file
Binary file not shown.
3
apps/hello_world/README.md
Normal file
3
apps/hello_world/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
GET /function/hello_world
|
||||
参数:无
|
||||
返回:Hello World消息
|
||||
BIN
apps/hello_world/__pycache__/function.cpython-311.pyc
Normal file
BIN
apps/hello_world/__pycache__/function.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/hello_world/__pycache__/function.cpython-313.pyc
Normal file
BIN
apps/hello_world/__pycache__/function.cpython-313.pyc
Normal file
Binary file not shown.
BIN
apps/hello_world/__pycache__/function.cpython-39.pyc
Normal file
BIN
apps/hello_world/__pycache__/function.cpython-39.pyc
Normal file
Binary file not shown.
6
apps/hello_world/config.json
Normal file
6
apps/hello_world/config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"url": "/function/hello_world",
|
||||
"method": "GET",
|
||||
"name": "简单的Hello World示例函数",
|
||||
"parameters": []
|
||||
}
|
||||
8
apps/hello_world/function.py
Normal file
8
apps/hello_world/function.py
Normal file
@@ -0,0 +1,8 @@
|
||||
def hello_world():
|
||||
"""
|
||||
无参数的示例函数,返回Hello World
|
||||
"""
|
||||
return {
|
||||
"message": "Hello World!",
|
||||
"status": "success"
|
||||
}
|
||||
3
apps/hello_world/intro.md
Normal file
3
apps/hello_world/intro.md
Normal file
@@ -0,0 +1,3 @@
|
||||
GET /function/hello_world
|
||||
参数:无
|
||||
返回:Hello World消息
|
||||
1
apps/hello_world/requirements.txt
Normal file
1
apps/hello_world/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
# 无需额外依赖
|
||||
6
call_stats.json
Normal file
6
call_stats.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"total": 0,
|
||||
"functions": {},
|
||||
"history_day": {},
|
||||
"history_hour": {}
|
||||
}
|
||||
87
config.py
Normal file
87
config.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import os
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
# 基础配置
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
LOGS_DIR = os.path.join(BASE_DIR, 'logs')
|
||||
os.makedirs(LOGS_DIR, exist_ok=True)
|
||||
|
||||
# 自定义日志轮转Handler(不再生成日期log,只保留最新1000行)
|
||||
class RotatingLineFileHandler(logging.FileHandler):
|
||||
def __init__(self, filename, max_lines=1000, encoding=None):
|
||||
super().__init__(filename, encoding=encoding)
|
||||
self.filename = filename
|
||||
self.max_lines = max_lines
|
||||
|
||||
def emit(self, record):
|
||||
super().emit(record)
|
||||
self.rotate_if_needed()
|
||||
|
||||
def rotate_if_needed(self):
|
||||
try:
|
||||
with open(self.filename, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
if len(lines) > self.max_lines:
|
||||
# 只保留最新max_lines行,丢弃旧的
|
||||
with open(self.filename, 'w', encoding='utf-8') as f:
|
||||
f.writelines(lines[-self.max_lines:])
|
||||
except Exception as e:
|
||||
print(f'Log rotation error: {e}')
|
||||
|
||||
# API配置
|
||||
API_CONFIG = {
|
||||
"HOST": "127.0.0.1",
|
||||
"PORT": 8000,
|
||||
"DEBUG": True
|
||||
}
|
||||
|
||||
# 路径配置
|
||||
PATHS = {
|
||||
"BASE_DIR": BASE_DIR,
|
||||
"APPS_DIR": os.path.join(BASE_DIR, "apps"),
|
||||
"ROUTES_FILE": os.path.join(BASE_DIR, "routes.txt"),
|
||||
"STATIC_DIR": os.path.join(BASE_DIR, "admin", "static"),
|
||||
"TEMPLATES_DIR": os.path.join(BASE_DIR, "admin", "templates"),
|
||||
"LOGS_DIR": LOGS_DIR
|
||||
}
|
||||
|
||||
# 日志配置
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"default": {
|
||||
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "default",
|
||||
},
|
||||
"file": {
|
||||
"()": RotatingLineFileHandler,
|
||||
"filename": os.path.join(LOGS_DIR, "app.log"),
|
||||
"max_lines": 1000,
|
||||
"formatter": "default",
|
||||
"encoding": "utf-8"
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"watchfiles": {
|
||||
"handlers": [],
|
||||
"propagate": False,
|
||||
"level": "WARNING"
|
||||
},
|
||||
"uvicorn.error": {
|
||||
"handlers": [],
|
||||
"propagate": False,
|
||||
"level": "WARNING"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"handlers": ["console", "file"],
|
||||
"level": "INFO",
|
||||
}
|
||||
}
|
||||
62
hot_reload.py
Normal file
62
hot_reload.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import time
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
|
||||
class AppReloader(FileSystemEventHandler):
|
||||
def __init__(self):
|
||||
self.process = None
|
||||
self.start_app()
|
||||
|
||||
def start_app(self):
|
||||
# 终止现有进程
|
||||
if self.process:
|
||||
self.process.terminate()
|
||||
self.process.wait()
|
||||
|
||||
# 启动新进程
|
||||
print("\n=== 正在启动服务器 ===")
|
||||
self.process = subprocess.Popen([sys.executable, "main.py"])
|
||||
print("=== 服务器已启动 ===\n")
|
||||
|
||||
def on_modified(self, event):
|
||||
if event.src_path.endswith('.py') or event.src_path.endswith('.md'):
|
||||
print(f"\n检测到文件变化: {event.src_path}")
|
||||
self.start_app()
|
||||
|
||||
def on_created(self, event):
|
||||
if event.src_path.endswith('.py') or event.src_path.endswith('.md'):
|
||||
print(f"\n检测到新文件: {event.src_path}")
|
||||
self.start_app()
|
||||
|
||||
def main():
|
||||
# 创建观察者
|
||||
observer = Observer()
|
||||
event_handler = AppReloader()
|
||||
|
||||
# 监视apps目录
|
||||
observer.schedule(event_handler, 'apps', recursive=True)
|
||||
# 监视main.py
|
||||
observer.schedule(event_handler, '.', recursive=False)
|
||||
|
||||
observer.start()
|
||||
print("=== 热重载监视器已启动 ===")
|
||||
print("监视目录: ./apps")
|
||||
print("监视文件: ./main.py")
|
||||
print("按Ctrl+C退出\n")
|
||||
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
observer.stop()
|
||||
if event_handler.process:
|
||||
event_handler.process.terminate()
|
||||
print("\n=== 服务已停止 ===")
|
||||
|
||||
observer.join()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1
image/CloudFuse.svg
Normal file
1
image/CloudFuse.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 2.4 MiB |
1007
logs/app.log
Normal file
1007
logs/app.log
Normal file
File diff suppressed because it is too large
Load Diff
258
main.py
Normal file
258
main.py
Normal file
@@ -0,0 +1,258 @@
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime
|
||||
import re # Import re module for regex
|
||||
# ANSI escape code regex
|
||||
ANSI_ESCAPE = re.compile(r'\x1B(?:[@-Z\\-_]|\\[0-?][ -/][@-~])')
|
||||
|
||||
# Tee class to duplicate stdout/stderr
|
||||
class Tee(object):
|
||||
def __init__(self, *files):
|
||||
self.files = files
|
||||
# Store original stdout/stderr to forward isatty() calls
|
||||
self.original_stdout = sys.__stdout__ # Use sys.__stdout__ for the actual original stream
|
||||
self.original_stderr = sys.__stderr__ # Use sys.__stderr__ for the actual original stream
|
||||
|
||||
def filter_ansi(self, text):
|
||||
# More robust ANSI escape code removal
|
||||
# Source: https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python
|
||||
ansi_escape = re.compile(r'(\x1b[^m]*m)')
|
||||
return ansi_escape.sub('', text)
|
||||
|
||||
def write(self, obj):
|
||||
for f in self.files:
|
||||
# If writing to the log file, filter ANSI codes
|
||||
if f is log_file: # Directly check if it's the log file object
|
||||
content_to_write = self.filter_ansi(obj)
|
||||
# Ensure newlines for file logging consistency
|
||||
if not content_to_write.endswith('\n'):
|
||||
content_to_write += '\n'
|
||||
else:
|
||||
content_to_write = obj
|
||||
f.write(content_to_write)
|
||||
f.flush()
|
||||
def flush(self):
|
||||
for f in self.files:
|
||||
f.flush()
|
||||
def isatty(self):
|
||||
# Forward the isatty() call to the original stdout/stderr
|
||||
# Check if original_stdout is a file-like object before calling isatty
|
||||
if hasattr(self.original_stdout, 'isatty'):
|
||||
return self.original_stdout.isatty()
|
||||
return False # Default to False if not a proper terminal
|
||||
# 日志目录和文件
|
||||
log_dir = os.path.join(os.path.dirname(__file__), "logs")
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
log_path = os.path.join(log_dir, "app.log")
|
||||
# 日志归档函数
|
||||
import threading, time
|
||||
|
||||
def rotate_log_daily():
|
||||
while True:
|
||||
now = datetime.now()
|
||||
next_day = datetime(now.year, now.month, now.day) # 今天0点
|
||||
next_day = next_day.replace(day=now.day+1) if now.hour >= 0 else next_day
|
||||
seconds = (next_day - now).total_seconds()
|
||||
time.sleep(seconds)
|
||||
# 归档
|
||||
archive_name = os.path.join(log_dir, now.strftime("%Y-%m-%d.log"))
|
||||
if os.path.exists(log_path):
|
||||
with open(log_path, "rb") as src, open(archive_name, "ab") as dst:
|
||||
dst.write(src.read())
|
||||
open(log_path, "w").close() # 清空
|
||||
threading.Thread(target=rotate_log_daily, daemon=True).start()
|
||||
# Redirect stdout and stderr to both the log file and original stdout
|
||||
log_file = open(log_path, "a", encoding="utf-8", buffering=1)
|
||||
original_stdout = sys.stdout
|
||||
sys.stdout = Tee(original_stdout, log_file)
|
||||
sys.stderr = Tee(sys.stderr, log_file)
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import JSONResponse
|
||||
import time
|
||||
from utils.logger import logger
|
||||
from config import API_CONFIG, PATHS
|
||||
from admin.admin import router as admin_router, FunctionManager
|
||||
import importlib
|
||||
from typing import List, Dict
|
||||
import json
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
# 定义生命周期管理器
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# 启动时执行
|
||||
try:
|
||||
logger.info("Checking dependencies on startup...")
|
||||
new_deps = await FunctionManager.check_and_install_dependencies()
|
||||
if new_deps:
|
||||
logger.info(f"Installed new dependencies on startup: {new_deps}")
|
||||
else:
|
||||
logger.info("No new dependencies needed")
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking dependencies on startup: {e}")
|
||||
raise e
|
||||
|
||||
yield # 这里是应用运行的地方
|
||||
|
||||
# 关闭时执行(如果需要)
|
||||
# 清理代码可以放在这里
|
||||
|
||||
# 使用生命周期管理器创建应用
|
||||
app = FastAPI(title="API Service", version="1.0.0", lifespan=lifespan)
|
||||
|
||||
# CORS中间件
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 添加可信主机中间件
|
||||
app.add_middleware(
|
||||
TrustedHostMiddleware,
|
||||
allowed_hosts=["*"] # 在生产环境中应该限制允许的主机
|
||||
)
|
||||
|
||||
# 请求计时中间件
|
||||
@app.middleware("http")
|
||||
async def add_process_time_header(request: Request, call_next):
|
||||
start_time = time.time()
|
||||
response = await call_next(request)
|
||||
process_time = time.time() - start_time
|
||||
response.headers["X-Process-Time"] = str(process_time)
|
||||
return response
|
||||
|
||||
# 全局错误处理
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
logger.error(f"Global error: {exc}", exc_info=True)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "Internal server error", "message": str(exc)}
|
||||
)
|
||||
|
||||
# 静态文件
|
||||
app.mount("/static", StaticFiles(directory=PATHS["STATIC_DIR"]), name="static")
|
||||
|
||||
# 路由
|
||||
app.include_router(admin_router, prefix="/admin")
|
||||
|
||||
# 获取函数列表的路由
|
||||
@app.get("/functions")
|
||||
async def get_functions():
|
||||
try:
|
||||
module = importlib.import_module("apps.functions.function")
|
||||
return module.functions()
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting functions list: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# 动态函数调用
|
||||
@app.get("/function/{function_name}")
|
||||
async def call_function(function_name: str, request: Request):
|
||||
try:
|
||||
module = importlib.import_module(f"apps.{function_name}.function")
|
||||
func = getattr(module, function_name)
|
||||
logger.info(f"Calling function: {function_name}")
|
||||
|
||||
# 获取所有查询参数
|
||||
query_params = dict(request.query_params)
|
||||
|
||||
# 获取函数参数信息
|
||||
import inspect
|
||||
sig = inspect.signature(func)
|
||||
|
||||
# 准备函数参数
|
||||
kwargs = {}
|
||||
for param_name, param in sig.parameters.items():
|
||||
if param_name in query_params:
|
||||
# 根据参数类型转换值
|
||||
param_type = param.annotation if param.annotation != inspect.Parameter.empty else str
|
||||
try:
|
||||
if param_type == float:
|
||||
kwargs[param_name] = float(query_params[param_name])
|
||||
elif param_type == int:
|
||||
kwargs[param_name] = int(query_params[param_name])
|
||||
else:
|
||||
kwargs[param_name] = query_params[param_name]
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400,
|
||||
detail=f"Invalid value for parameter {param_name}: {str(e)}")
|
||||
elif param.default == inspect.Parameter.empty:
|
||||
# 如果参数没有默认值且未提供,则报错
|
||||
raise HTTPException(status_code=400,
|
||||
detail=f"Missing required parameter: {param_name}")
|
||||
|
||||
# 调用函数
|
||||
result = func(**kwargs)
|
||||
|
||||
# 统计调用次数(支持天/小时)
|
||||
try:
|
||||
now = datetime.now()
|
||||
day_str = now.strftime('%Y-%m-%d')
|
||||
hour_str = now.strftime('%Y-%m-%d-%H')
|
||||
stats_file = os.path.join(PATHS["BASE_DIR"], "call_stats.json")
|
||||
# 兼容旧数据结构
|
||||
if os.path.exists(stats_file):
|
||||
with open(stats_file, "r", encoding="utf-8") as f:
|
||||
stats = json.load(f)
|
||||
if "history_day" not in stats or not isinstance(stats["history_day"], dict):
|
||||
stats["history_day"] = {}
|
||||
if "history_hour" not in stats or not isinstance(stats["history_hour"], dict):
|
||||
stats["history_hour"] = {}
|
||||
else:
|
||||
stats = {"total": 0, "functions": {}, "history_day": {}, "history_hour": {}}
|
||||
# 总数
|
||||
stats["total"] = stats.get("total", 0) + 1
|
||||
stats["functions"][function_name] = stats["functions"].get(function_name, 0) + 1
|
||||
# 按天
|
||||
if day_str not in stats["history_day"] or not isinstance(stats["history_day"][day_str], dict):
|
||||
stats["history_day"][day_str] = {"total": 0}
|
||||
stats["history_day"][day_str]["total"] = stats["history_day"][day_str].get("total", 0) + 1
|
||||
stats["history_day"][day_str][function_name] = stats["history_day"][day_str].get(function_name, 0) + 1
|
||||
# 按小时
|
||||
if hour_str not in stats["history_hour"] or not isinstance(stats["history_hour"][hour_str], dict):
|
||||
stats["history_hour"][hour_str] = {"total": 0}
|
||||
stats["history_hour"][hour_str]["total"] = stats["history_hour"][hour_str].get("total", 0) + 1
|
||||
stats["history_hour"][hour_str][function_name] = stats["history_hour"][hour_str].get(function_name, 0) + 1
|
||||
with open(stats_file, "w", encoding="utf-8") as f:
|
||||
json.dump(stats, f, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating call stats: {e}")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error calling function {function_name}: {e}")
|
||||
raise HTTPException(status_code=404, detail=f"Function error: {str(e)}")
|
||||
|
||||
# 添加错误处理中间件
|
||||
@app.middleware("http")
|
||||
async def error_handling_middleware(request: Request, call_next):
|
||||
try:
|
||||
response = await call_next(request)
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"Unhandled error: {e}", exc_info=True)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "Internal server error", "message": str(e)}
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host="0.0.0.0", # 修改为监听所有网络接口
|
||||
port=API_CONFIG["PORT"],
|
||||
reload=API_CONFIG["DEBUG"]
|
||||
)
|
||||
43
requirements.txt
Normal file
43
requirements.txt
Normal file
@@ -0,0 +1,43 @@
|
||||
# Web框架和核心依赖
|
||||
fastapi>=0.68.0,<1.0.0
|
||||
uvicorn>=0.15.0,<1.0.0
|
||||
python-multipart>=0.0.5 # 用于文件上传
|
||||
aiofiles>=0.7.0 # 异步文件操作
|
||||
requests>=2.26.0,<3.0.0 # HTTP请求
|
||||
pydantic>=1.8.2,<2.0.0 # FastAPI数据验证
|
||||
|
||||
# 数据处理
|
||||
numpy>=1.21.0,<2.0.0 # 数值计算
|
||||
pandas>=1.3.0,<2.0.0 # 数据处理和Excel文件支持
|
||||
openpyxl>=3.0.7 # Excel文件支持
|
||||
xlrd>=2.0.1 # Excel文件读取(旧格式)
|
||||
|
||||
# 文件处理
|
||||
python-docx>=0.8.11 # 生成Word文档
|
||||
reportlab>=3.6.12 # 生成PDF文件
|
||||
pdf2image>=1.16.0 # 将PDF转换为图片
|
||||
markdown2>=2.4.4 # 将Markdown转换为HTML
|
||||
imgkit>=1.2.2 # 将HTML转换为图片(需要安装wkhtmltopdf)
|
||||
|
||||
# 工具和辅助库
|
||||
python-dateutil>=2.8.2 # 日期处理
|
||||
pytz>=2021.3 # 时区支持
|
||||
|
||||
# 测试和开发工具
|
||||
pytest>=6.2.5,<7.0.0 # 测试框架
|
||||
pluggy>=1.0.0 # pytest插件管理
|
||||
iniconfig>=1.1.1 # pytest配置
|
||||
watchdog>=2.1.6 # 文件监控
|
||||
colorama>=0.4.6 # 终端颜色支持
|
||||
certifi>=2024.8.30 # SSL证书
|
||||
charset-normalizer>=2.0.0,<3.0.0 # 字符集支持
|
||||
h11>=0.12.0 # HTTP/1.1协议支持
|
||||
httpcore>=0.13.7 # HTTP核心库
|
||||
rfc3986>=1.5.0 # URL解析
|
||||
six>=1.16.0 # Python 2和3兼容
|
||||
sniffio>=1.3.1 # 异步IO支持
|
||||
atomicwrites>=1.4.1 # 原子写入
|
||||
attrs>=21.2.0 # 属性管理
|
||||
setuptools>=57.5.0 # 包管理
|
||||
toml>=0.10.2 # TOML文件支持
|
||||
typing_extensions>=3.10.0 # 类型扩展
|
||||
9
routes.txt
Normal file
9
routes.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
/function/calculate
|
||||
/function/echo_message
|
||||
/function/example_function
|
||||
/function/functions
|
||||
/function/hello_world
|
||||
/functions
|
||||
/function/get_random_xlsx_line
|
||||
/function/static
|
||||
/function/cs
|
||||
BIN
utils/__pycache__/logger.cpython-311.pyc
Normal file
BIN
utils/__pycache__/logger.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/logger.cpython-313.pyc
Normal file
BIN
utils/__pycache__/logger.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/logger.cpython-39.pyc
Normal file
BIN
utils/__pycache__/logger.cpython-39.pyc
Normal file
Binary file not shown.
6
utils/logger.py
Normal file
6
utils/logger.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import logging.config
|
||||
from config import LOGGING
|
||||
|
||||
# 配置日志
|
||||
logging.config.dictConfig(LOGGING)
|
||||
logger = logging.getLogger(__name__)
|
||||
Reference in New Issue
Block a user