- 新增图像生成接口,支持试用、积分和自定义API Key模式 - 实现生成图片结果异步上传至MinIO存储,带重试机制 - 优化积分预扣除和异常退还逻辑,保障用户积分准确 - 添加获取生成历史记录接口,支持时间范围和分页 - 提供本地字典配置接口,支持模型、比例、提示模板和尺寸 - 实现图片批量上传接口,支持S3兼容对象存储 feat(admin): 增加管理员角色管理与权限分配接口 - 实现角色列表查询、角色创建、更新及删除功能 - 增加权限列表查询接口 - 实现用户角色分配接口,便于统一管理用户权限 - 增加系统字典增删查改接口,支持分类过滤和排序 - 权限控制全面覆盖管理接口,保证安全访问 feat(auth): 完善用户登录注册及权限相关接口与页面 - 实现手机号验证码发送及校验功能,保障注册安全 - 支持手机号注册、登录及退出接口,集成日志记录 - 增加修改密码功能,验证原密码后更新 - 提供动态导航菜单接口,基于权限展示不同菜单 - 实现管理界面路由及日志、角色、字典管理页面访问权限控制 - 添加系统日志查询接口,支持关键词和等级筛选 feat(app): 初始化Flask应用并配置蓝图与数据库 - 创建应用程序工厂,加载配置,初始化数据库和Redis客户端 - 注册认证、API及管理员蓝图,整合路由 - 根路由渲染主页模板 - 应用上下文中自动创建数据库表,保证运行环境准备完毕 feat(database): 提供数据库创建与迁移支持脚本 - 新增数据库创建脚本,支持自动检测是否已存在 - 添加数据库表初始化脚本,支持创建和删除所有表 - 实现RBAC权限初始化,包含基础权限和角色创建 - 新增字段手动修复脚本,添加用户API Key和积分字段 - 强制迁移脚本支持清理连接和修复表结构,初始化默认数据及角色分配 feat(config): 新增系统配置参数 - 配置数据库、Redis、Session和MinIO相关参数 - 添加AI接口地址及试用Key配置 - 集成阿里云短信服务配置及开发模式相关参数 feat(extensions): 初始化数据库、Redis和MinIO客户端 - 创建全局SQLAlchemy数据库实例和Redis客户端 - 配置基于boto3的MinIO兼容S3客户端 chore(logs): 添加示例系统日志文件 - 记录用户请求、验证码发送成功与失败的日志信息
345 lines
9.8 KiB
JavaScript
345 lines
9.8 KiB
JavaScript
docReady(() => {
|
|
if (!EVALEX_TRUSTED) {
|
|
initPinBox();
|
|
}
|
|
// if we are in console mode, show the console.
|
|
if (CONSOLE_MODE && EVALEX) {
|
|
createInteractiveConsole();
|
|
}
|
|
|
|
const frames = document.querySelectorAll("div.traceback div.frame");
|
|
if (EVALEX) {
|
|
addConsoleIconToFrames(frames);
|
|
}
|
|
addEventListenersToElements(document.querySelectorAll("div.detail"), "click", () =>
|
|
document.querySelector("div.traceback").scrollIntoView(false)
|
|
);
|
|
addToggleFrameTraceback(frames);
|
|
addToggleTraceTypesOnClick(document.querySelectorAll("h2.traceback"));
|
|
addInfoPrompt(document.querySelectorAll("span.nojavascript"));
|
|
wrapPlainTraceback();
|
|
});
|
|
|
|
function addToggleFrameTraceback(frames) {
|
|
frames.forEach((frame) => {
|
|
frame.addEventListener("click", () => {
|
|
frame.getElementsByTagName("pre")[0].parentElement.classList.toggle("expanded");
|
|
});
|
|
})
|
|
}
|
|
|
|
|
|
function wrapPlainTraceback() {
|
|
const plainTraceback = document.querySelector("div.plain textarea");
|
|
const wrapper = document.createElement("pre");
|
|
const textNode = document.createTextNode(plainTraceback.textContent);
|
|
wrapper.appendChild(textNode);
|
|
plainTraceback.replaceWith(wrapper);
|
|
}
|
|
|
|
function makeDebugURL(args) {
|
|
const params = new URLSearchParams(args)
|
|
params.set("s", SECRET)
|
|
return `?__debugger__=yes&${params}`
|
|
}
|
|
|
|
function initPinBox() {
|
|
document.querySelector(".pin-prompt form").addEventListener(
|
|
"submit",
|
|
function (event) {
|
|
event.preventDefault();
|
|
const btn = this.btn;
|
|
btn.disabled = true;
|
|
|
|
fetch(
|
|
makeDebugURL({cmd: "pinauth", pin: this.pin.value})
|
|
)
|
|
.then((res) => res.json())
|
|
.then(({auth, exhausted}) => {
|
|
if (auth) {
|
|
EVALEX_TRUSTED = true;
|
|
fadeOut(document.getElementsByClassName("pin-prompt")[0]);
|
|
} else {
|
|
alert(
|
|
`Error: ${
|
|
exhausted
|
|
? "too many attempts. Restart server to retry."
|
|
: "incorrect pin"
|
|
}`
|
|
);
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
alert("Error: Could not verify PIN. Network error?");
|
|
console.error(err);
|
|
})
|
|
.finally(() => (btn.disabled = false));
|
|
},
|
|
false
|
|
);
|
|
}
|
|
|
|
function promptForPin() {
|
|
if (!EVALEX_TRUSTED) {
|
|
fetch(makeDebugURL({cmd: "printpin"}));
|
|
const pinPrompt = document.getElementsByClassName("pin-prompt")[0];
|
|
fadeIn(pinPrompt);
|
|
document.querySelector('.pin-prompt input[name="pin"]').focus();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function for shell initialization
|
|
*/
|
|
function openShell(consoleNode, target, frameID) {
|
|
promptForPin();
|
|
if (consoleNode) {
|
|
slideToggle(consoleNode);
|
|
return consoleNode;
|
|
}
|
|
let historyPos = 0;
|
|
const history = [""];
|
|
const consoleElement = createConsole();
|
|
const output = createConsoleOutput();
|
|
const form = createConsoleInputForm();
|
|
const command = createConsoleInput();
|
|
|
|
target.parentNode.appendChild(consoleElement);
|
|
consoleElement.append(output);
|
|
consoleElement.append(form);
|
|
form.append(command);
|
|
command.focus();
|
|
slideToggle(consoleElement);
|
|
|
|
form.addEventListener("submit", (e) => {
|
|
handleConsoleSubmit(e, command, frameID).then((consoleOutput) => {
|
|
output.append(consoleOutput);
|
|
command.focus();
|
|
consoleElement.scrollTo(0, consoleElement.scrollHeight);
|
|
const old = history.pop();
|
|
history.push(command.value);
|
|
if (typeof old !== "undefined") {
|
|
history.push(old);
|
|
}
|
|
historyPos = history.length - 1;
|
|
command.value = "";
|
|
});
|
|
});
|
|
|
|
command.addEventListener("keydown", (e) => {
|
|
if (e.key === "l" && e.ctrlKey) {
|
|
output.innerText = "--- screen cleared ---";
|
|
} else if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
|
// Handle up arrow and down arrow.
|
|
if (e.key === "ArrowUp" && historyPos > 0) {
|
|
e.preventDefault();
|
|
historyPos--;
|
|
} else if (e.key === "ArrowDown" && historyPos < history.length - 1) {
|
|
historyPos++;
|
|
}
|
|
command.value = history[historyPos];
|
|
}
|
|
return false;
|
|
});
|
|
|
|
return consoleElement;
|
|
}
|
|
|
|
function addEventListenersToElements(elements, event, listener) {
|
|
elements.forEach((el) => el.addEventListener(event, listener));
|
|
}
|
|
|
|
/**
|
|
* Add extra info
|
|
*/
|
|
function addInfoPrompt(elements) {
|
|
for (let i = 0; i < elements.length; i++) {
|
|
elements[i].innerHTML =
|
|
"<p>To switch between the interactive traceback and the plaintext " +
|
|
'one, you can click on the "Traceback" headline. From the text ' +
|
|
"traceback you can also create a paste of it. " +
|
|
(!EVALEX
|
|
? ""
|
|
: "For code execution mouse-over the frame you want to debug and " +
|
|
"click on the console icon on the right side." +
|
|
"<p>You can execute arbitrary Python code in the stack frames and " +
|
|
"there are some extra helpers available for introspection:" +
|
|
"<ul><li><code>dump()</code> shows all variables in the frame" +
|
|
"<li><code>dump(obj)</code> dumps all that's known about the object</ul>");
|
|
elements[i].classList.remove("nojavascript");
|
|
}
|
|
}
|
|
|
|
function addConsoleIconToFrames(frames) {
|
|
for (let i = 0; i < frames.length; i++) {
|
|
let consoleNode = null;
|
|
const target = frames[i];
|
|
const frameID = frames[i].id.substring(6);
|
|
|
|
for (let j = 0; j < target.getElementsByTagName("pre").length; j++) {
|
|
const img = createIconForConsole();
|
|
img.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
consoleNode = openShell(consoleNode, target, frameID);
|
|
return false;
|
|
});
|
|
target.getElementsByTagName("pre")[j].append(img);
|
|
}
|
|
}
|
|
}
|
|
|
|
function slideToggle(target) {
|
|
target.classList.toggle("active");
|
|
}
|
|
|
|
/**
|
|
* toggle traceback types on click.
|
|
*/
|
|
function addToggleTraceTypesOnClick(elements) {
|
|
for (let i = 0; i < elements.length; i++) {
|
|
elements[i].addEventListener("click", () => {
|
|
document.querySelector("div.traceback").classList.toggle("hidden");
|
|
document.querySelector("div.plain").classList.toggle("hidden");
|
|
});
|
|
elements[i].style.cursor = "pointer";
|
|
document.querySelector("div.plain").classList.toggle("hidden");
|
|
}
|
|
}
|
|
|
|
function createConsole() {
|
|
const consoleNode = document.createElement("pre");
|
|
consoleNode.classList.add("console");
|
|
consoleNode.classList.add("active");
|
|
return consoleNode;
|
|
}
|
|
|
|
function createConsoleOutput() {
|
|
const output = document.createElement("div");
|
|
output.classList.add("output");
|
|
output.innerHTML = "[console ready]";
|
|
return output;
|
|
}
|
|
|
|
function createConsoleInputForm() {
|
|
const form = document.createElement("form");
|
|
form.innerHTML = ">>> ";
|
|
return form;
|
|
}
|
|
|
|
function createConsoleInput() {
|
|
const command = document.createElement("input");
|
|
command.type = "text";
|
|
command.setAttribute("autocomplete", "off");
|
|
command.setAttribute("spellcheck", false);
|
|
command.setAttribute("autocapitalize", "off");
|
|
command.setAttribute("autocorrect", "off");
|
|
return command;
|
|
}
|
|
|
|
function createIconForConsole() {
|
|
const img = document.createElement("img");
|
|
img.setAttribute("src", makeDebugURL({cmd: "resource", f: "console.png"}));
|
|
img.setAttribute("title", "Open an interactive python shell in this frame");
|
|
return img;
|
|
}
|
|
|
|
function createExpansionButtonForConsole() {
|
|
const expansionButton = document.createElement("a");
|
|
expansionButton.setAttribute("href", "#");
|
|
expansionButton.setAttribute("class", "toggle");
|
|
expansionButton.innerHTML = " ";
|
|
return expansionButton;
|
|
}
|
|
|
|
function createInteractiveConsole() {
|
|
const target = document.querySelector("div.console div.inner");
|
|
while (target.firstChild) {
|
|
target.removeChild(target.firstChild);
|
|
}
|
|
openShell(null, target, 0);
|
|
}
|
|
|
|
function handleConsoleSubmit(e, command, frameID) {
|
|
// Prevent page from refreshing.
|
|
e.preventDefault();
|
|
|
|
return new Promise((resolve) => {
|
|
fetch(makeDebugURL({cmd: command.value, frm: frameID}))
|
|
.then((res) => {
|
|
return res.text();
|
|
})
|
|
.then((data) => {
|
|
const tmp = document.createElement("div");
|
|
tmp.innerHTML = data;
|
|
resolve(tmp);
|
|
|
|
// Handle expandable span for long list outputs.
|
|
// Example to test: list(range(13))
|
|
let wrapperAdded = false;
|
|
const wrapperSpan = document.createElement("span");
|
|
const expansionButton = createExpansionButtonForConsole();
|
|
|
|
tmp.querySelectorAll("span.extended").forEach((spanToWrap) => {
|
|
const parentDiv = spanToWrap.parentNode;
|
|
if (!wrapperAdded) {
|
|
parentDiv.insertBefore(wrapperSpan, spanToWrap);
|
|
wrapperAdded = true;
|
|
}
|
|
parentDiv.removeChild(spanToWrap);
|
|
wrapperSpan.append(spanToWrap);
|
|
spanToWrap.hidden = true;
|
|
|
|
expansionButton.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
spanToWrap.hidden = !spanToWrap.hidden;
|
|
expansionButton.classList.toggle("open");
|
|
return false;
|
|
});
|
|
});
|
|
|
|
// Add expansion button at end of wrapper.
|
|
if (wrapperAdded) {
|
|
wrapperSpan.append(expansionButton);
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.error(err);
|
|
});
|
|
return false;
|
|
});
|
|
}
|
|
|
|
function fadeOut(element) {
|
|
element.style.opacity = 1;
|
|
|
|
(function fade() {
|
|
element.style.opacity -= 0.1;
|
|
if (element.style.opacity < 0) {
|
|
element.style.display = "none";
|
|
} else {
|
|
requestAnimationFrame(fade);
|
|
}
|
|
})();
|
|
}
|
|
|
|
function fadeIn(element, display) {
|
|
element.style.opacity = 0;
|
|
element.style.display = display || "block";
|
|
|
|
(function fade() {
|
|
let val = parseFloat(element.style.opacity) + 0.1;
|
|
if (val <= 1) {
|
|
element.style.opacity = val;
|
|
requestAnimationFrame(fade);
|
|
}
|
|
})();
|
|
}
|
|
|
|
function docReady(fn) {
|
|
if (document.readyState === "complete" || document.readyState === "interactive") {
|
|
setTimeout(fn, 1);
|
|
} else {
|
|
document.addEventListener("DOMContentLoaded", fn);
|
|
}
|
|
}
|