| import streamlit as st |
| import time |
| import math |
| import uuid |
| import datetime |
| import os |
| import re |
|
|
| |
| from config import CUSTOM_CSS, PROVIDER_MAP, SIZE_PRESETS, QUALITY_PRESETS, ITEMS_PER_PAGE, LOCAL_CACHE_DIR, CONTAINER_DISK_QUOTA_GB, THUMBNAIL_DIR |
| from utils import get_disk_info, PSUTIL_AVAILABLE |
| from services import TokenManager, AuthManager, WebDAVManager |
| from logic import CoreGenerator, StorageManager, AutoTaskManager |
| from utils import PromptLoader, LocalUpscaler, SocialPosterGenerator |
|
|
| if PSUTIL_AVAILABLE: |
| import psutil |
|
|
| |
| @st.cache_resource(show_spinner=False) |
| def get_managers(): |
| return { |
| "pl": PromptLoader(), |
| "tm": TokenManager(), |
| "cg": CoreGenerator(), |
| "sm": StorageManager(), |
| "wm": WebDAVManager(), |
| "atm": AutoTaskManager(), |
| "up": LocalUpscaler(), |
| "pg": SocialPosterGenerator(), |
| "am": AuthManager() |
| } |
|
|
| |
| class ContainerMonitor: |
| @staticmethod |
| def _read_file(path): |
| """安全读取文件内容,失败返回 None""" |
| if not os.path.exists(path): return None |
| try: |
| with open(path, 'r') as f: return f.read().strip() |
| except: return None |
|
|
| @staticmethod |
| def get_memory_stats(): |
| """获取容器内存使用情况 (适配 v1/v2)""" |
| usage = 0 |
| limit = 0 |
| |
| |
| if os.path.exists('/sys/fs/cgroup/memory.current'): |
| usage = int(ContainerMonitor._read_file('/sys/fs/cgroup/memory.current') or 0) |
| limit_str = ContainerMonitor._read_file('/sys/fs/cgroup/memory.max') |
| if limit_str == "max": limit = 10**15 |
| else: limit = int(limit_str or 0) |
| |
| |
| elif os.path.exists('/sys/fs/cgroup/memory/memory.usage_in_bytes'): |
| usage = int(ContainerMonitor._read_file('/sys/fs/cgroup/memory/memory.usage_in_bytes') or 0) |
| limit = int(ContainerMonitor._read_file('/sys/fs/cgroup/memory/memory.limit_in_bytes') or 0) |
| |
| |
| if limit == 0 or limit > 10**14: |
| if PSUTIL_AVAILABLE: limit = psutil.virtual_memory().total |
| else: limit = 16 * 1024**3 |
| |
| return usage, limit |
|
|
| @staticmethod |
| def get_cpu_counter(): |
| """获取 CPU 累计使用时间 (纳秒)""" |
| |
| if os.path.exists('/sys/fs/cgroup/cpu.stat'): |
| content = ContainerMonitor._read_file('/sys/fs/cgroup/cpu.stat') |
| if content: |
| |
| match = re.search(r'usage_usec\s+(\d+)', content) |
| if match: return int(match.group(1)) * 1000 |
| |
| |
| path_v1 = '/sys/fs/cgroup/cpu,cpuacct/cpuacct.usage' |
| if os.path.exists(path_v1): |
| return int(ContainerMonitor._read_file(path_v1) or 0) |
| |
| |
| if PSUTIL_AVAILABLE: return int(time.time() * 1e9) |
| return 0 |
|
|
| @staticmethod |
| def get_cpu_quota(): |
| """获取容器 CPU 配额 (核数)""" |
| |
| if os.path.exists('/sys/fs/cgroup/cpu.max'): |
| content = ContainerMonitor._read_file('/sys/fs/cgroup/cpu.max') |
| if content: |
| parts = content.split() |
| if parts[0] == 'max': return os.cpu_count() or 1 |
| try: return int(parts[0]) / int(parts[1]) |
| except: pass |
| |
| |
| path_quota = '/sys/fs/cgroup/cpu/cpu.cfs_quota_us' |
| path_period = '/sys/fs/cgroup/cpu/cpu.cfs_period_us' |
| if os.path.exists(path_quota): |
| quota = int(ContainerMonitor._read_file(path_quota) or -1) |
| period = int(ContainerMonitor._read_file(path_period) or 100000) |
| if quota == -1: return os.cpu_count() or 1 |
| return quota / period |
| |
| return os.cpu_count() or 1 |
|
|
| |
|
|
| @st.dialog("作品详情", width="large") |
| def view_image_dialog(image_bytes, record): |
| if image_bytes: |
| st.image(image_bytes, width="stretch") |
| st.markdown("### 📝 提示词信息") |
| st.info(record.get('prompt_zh', '无中文描述')) |
| with st.expander("查看英文原文 (Prompt)"): |
| st.code(record.get('prompt_en', 'N/A')) |
| |
| st.markdown("### ℹ️ 元数据") |
| c1, c2, c3 = st.columns(3) |
| c1.markdown(f"<p style='font-size:14px; color:gray; margin-bottom:0;'>时间</p><p style='font-size:16px; font-weight:bold;'>{record['timestamp'].split(' ')[0]}</p>", unsafe_allow_html=True) |
| c2.markdown(f"<p style='font-size:14px; color:gray; margin-bottom:0;'>模型</p><p style='font-size:16px; font-weight:bold;'>{record.get('model', 'Unknown')}</p>", unsafe_allow_html=True) |
| c3.markdown(f"<p style='font-size:14px; color:gray; margin-bottom:0;'>风格</p><p style='font-size:16px; font-weight:bold;'>{record.get('style', 'None')}</p>", unsafe_allow_html=True) |
|
|
| @st.dialog("编辑 WebDAV") |
| def edit_webdav_dialog(conf): |
| mgrs = get_managers() |
| wm = mgrs["wm"] |
| opts = conf["options"] |
| with st.form("edit"): |
| n = st.text_input("备注名称", value=conf["name"]) |
| h = st.text_input("服务器地址 (URL)", value=opts["webdav_hostname"]) |
| u = st.text_input("账号", value=opts["webdav_login"]) |
| p = st.text_input("密码", value=opts["webdav_password"], type="password") |
| r = st.text_input("根路径 (可选)", value=opts.get("webdav_root", "/")) |
| is_enabled = st.checkbox("✅ 启用此节点", value=conf.get("enabled", True)) |
| |
| c1, c2 = st.columns(2) |
| if c1.form_submit_button("测试连接", width="stretch"): |
| ok, msg = wm.test_connection(h, u, p, r) |
| if ok: st.success(f"✅ {msg}") |
| else: st.error(msg) |
| |
| if c2.form_submit_button("保存修改", type="primary", width="stretch"): |
| ok, msg = wm.test_connection(h, u, p, r) |
| if ok: |
| wm.update_connection(conf["id"], n, h, u, p, r, is_enabled) |
| st.success("✅ 更新成功") |
| time.sleep(1) |
| st.rerun() |
| else: |
| st.error(f"连接测试失败: {msg}") |
|
|
| def render_pagination(total_pages, key_prefix): |
| c_space_l, c_first, c_prev, c_info, c_next, c_last, c_input, c_jump, c_space_r = st.columns([3, 0.7, 0.7, 1.2, 0.7, 0.7, 1.0, 0.7, 3]) |
| current = st.session_state.gallery_page |
| |
| with c_first: |
| if st.button("首页", key=f"{key_prefix}_first", disabled=current==1, width="stretch"): |
| st.session_state.gallery_page = 1 |
| st.rerun() |
| with c_prev: |
| if st.button("上一页", key=f"{key_prefix}_prev", disabled=current==1, width="stretch"): |
| st.session_state.gallery_page = max(1, current - 1) |
| st.rerun() |
| with c_info: |
| st.markdown(f"<div class='pagination-info'>{current} / {total_pages}</div>", unsafe_allow_html=True) |
| with c_next: |
| if st.button("下一页", key=f"{key_prefix}_next", disabled=current==total_pages, width="stretch"): |
| st.session_state.gallery_page = min(total_pages, current + 1) |
| st.rerun() |
| with c_last: |
| if st.button("尾页", key=f"{key_prefix}_last", disabled=current==total_pages, width="stretch"): |
| st.session_state.gallery_page = total_pages |
| st.rerun() |
| with c_input: |
| jump_val = st.number_input("页码", min_value=1, max_value=total_pages, value=current, key=f"{key_prefix}_input", label_visibility="collapsed") |
| with c_jump: |
| if st.button("跳转", key=f"{key_prefix}_jump", width="stretch"): |
| if jump_val != current: |
| st.session_state.gallery_page = jump_val |
| st.rerun() |
|
|
| |
|
|
| @st.fragment(run_every=2) |
| def render_header(): |
| """ |
| [V30.19] 容器原生资源监控版 |
| 使用 cgroup 文件系统读取真实的容器资源使用率 |
| """ |
| mgrs = get_managers() |
| sm, tm = mgrs["sm"], mgrs["tm"] |
| atm = mgrs["atm"] |
| |
| current_mtime = sm.get_file_mtime() |
| last_known_mtime = st.session_state.get("last_mtime", 0.0) |
| |
| if current_mtime > last_known_mtime: |
| sm.refresh_cache() |
| st.session_state.last_mtime = current_mtime |
| st.rerun() |
| |
| hist_len = len(sm.cache) |
| active_tasks = len(atm.active_tasks) |
| |
| |
| now = time.time() |
| |
| |
| mem_used, mem_limit = ContainerMonitor.get_memory_stats() |
| mem_percent = (mem_used / mem_limit) * 100 if mem_limit > 0 else 0 |
| mem_text = f"RAM: {mem_used/1024**3:.1f}G / {mem_limit/1024**3:.1f}G ({mem_percent:.0f}%)" |
| |
| |
| |
| cpu_usage_ns = ContainerMonitor.get_cpu_counter() |
| cpu_cores = ContainerMonitor.get_cpu_quota() |
| |
| if "mon_last_time" not in st.session_state: |
| st.session_state.mon_last_time = now |
| st.session_state.mon_last_cpu = cpu_usage_ns |
| st.session_state.mon_cpu_text = "CPU: Calc..." |
| st.session_state.mon_cpu_val = 0.0 |
| |
| time_delta = now - st.session_state.mon_last_time |
| |
| |
| if time_delta > 1.0: |
| cpu_delta = cpu_usage_ns - st.session_state.mon_last_cpu |
| |
| |
| cpu_p = (cpu_delta / (time_delta * 1e9 * cpu_cores)) * 100 |
| |
| st.session_state.mon_cpu_text = f"CPU: {cpu_p:.1f}% ({cpu_cores:.1f}C)" |
| st.session_state.mon_cpu_val = min(cpu_p / 100, 1.0) |
| |
| |
| st.session_state.mon_last_time = now |
| st.session_state.mon_last_cpu = cpu_usage_ns |
|
|
| |
| used_gb, free_gb_phys, total_phys, percent_phys = get_disk_info(LOCAL_CACHE_DIR) |
| quota_gb = CONTAINER_DISK_QUOTA_GB |
| disk_percent = min((used_gb / quota_gb) * 100, 100.0) |
| disk_text = f"Disk: {used_gb:.1f}G / {quota_gb}G ({disk_percent:.0f}%)" |
|
|
| |
| c_left, c_right = st.columns([6, 1]) |
| with c_left: |
| st.markdown(f""" |
| <div class="header-wrapper"> |
| <span class="header-title">🎨 AI 艺术 | {st.session_state.username}</span> |
| <span class="header-badge">🖼️ {hist_len}</span> |
| <span class="header-badge {'active' if active_tasks > 0 else ''}">{'🟢 自动运行中' if active_tasks > 0 else '⚪ 自动任务待机'}</span> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| |
| col_cpu, col_mem, col_disk = st.columns(3) |
| |
| with col_cpu: |
| st.progress(st.session_state.mon_cpu_val, text=st.session_state.mon_cpu_text) |
| |
| with col_mem: |
| st.progress(min(mem_percent/100, 1.0), text=mem_text) |
| |
| with col_disk: |
| st.progress(min(disk_percent/100, 1.0), text=disk_text) |
|
|
| with c_right: |
| if st.button("🚪 退出", width="stretch"): |
| st.session_state.logged_in=False |
| st.rerun() |
|
|
| @st.fragment |
| def render_automation_dashboard(): |
| """自动化任务面板""" |
| mgrs = get_managers() |
| sm, tm, wm, atm, cg = mgrs["sm"], mgrs["tm"], mgrs["wm"], mgrs["atm"], mgrs["cg"] |
| |
| col_auto_l, col_auto_r = st.columns([1, 2], gap="medium") |
| |
| with col_auto_l: |
| st.subheader("⚙️ 任务控制台") |
| task_options = ["➕ 新建任务"] + [t["name"] for t in st.session_state.saved_tasks] |
| |
| current_index = st.session_state.selected_task_index |
| if current_index >= len(task_options): |
| current_index = 0 |
| st.session_state.selected_task_index = 0 |
| |
| selected_task_name = st.selectbox("选择任务方案", task_options, index=current_index) |
| |
| current_task_data = {} |
| if selected_task_name != "➕ 新建任务": |
| found = [t for t in st.session_state.saved_tasks if t["name"] == selected_task_name] |
| if found: current_task_data = found[0] |
| |
| try: |
| idx = task_options.index(selected_task_name) |
| if st.session_state.selected_task_index != idx: |
| st.session_state.selected_task_index = idx |
| st.rerun() |
| except: pass |
|
|
| |
| task_name_input = st.text_input("任务名称", value=current_task_data.get("name", "我的新任务")) |
| seeds_input = st.text_area("灵感种子池 (一行一个)", value=current_task_data.get("seeds", "Cyberpunk\nWasteland\nFuture City"), height=100) |
| |
| c_f1, c_f2 = st.columns(2) |
| try: default_idx = list(PROVIDER_MAP.keys()).index(current_task_data.get("provider", "Pollinations AI (推荐)")) |
| except: default_idx = 0 |
| |
| iv = c_f1.number_input("周期(s)", min_value=60, value=max(60, current_task_data.get("interval", 3600))) |
| pv = c_f1.selectbox("供应商", list(PROVIDER_MAP.keys()), index=default_idx) |
| |
| try: default_qual_idx = list(QUALITY_PRESETS.keys()).index(current_task_data.get("quality", "⚡ 标准 (Standard)")) |
| except: default_qual_idx = 0 |
| qual = c_f2.selectbox("画质等级", list(QUALITY_PRESETS.keys()), index=default_qual_idx) |
| |
| c_b1, c_b2 = st.columns([1, 1]) |
| if c_b1.button("💾 保存/更新", type="primary", width="stretch"): |
| if not task_name_input: |
| st.error("名称不能为空") |
| else: |
| new_task = { |
| "id": current_task_data.get("id", str(uuid.uuid4())), |
| "name": task_name_input, |
| "seeds": seeds_input, |
| "interval": iv, |
| "provider": pv, |
| "quality": qual |
| } |
| new_list = [t for t in st.session_state.saved_tasks if t["id"] != new_task["id"]] |
| new_list.append(new_task) |
| st.session_state.saved_tasks = new_list |
| sm.save_tasks(new_list) |
| st.success("已保存") |
| time.sleep(1) |
| st.rerun() |
| |
| if c_b2.button("🗑️ 删除", type="secondary", width="stretch"): |
| if selected_task_name == "➕ 新建任务": st.error("无法删除") |
| else: |
| new_list = [t for t in st.session_state.saved_tasks if t["id"] != current_task_data["id"]] |
| st.session_state.saved_tasks = new_list |
| st.session_state.selected_task_index = 0 |
| sm.save_tasks(new_list) |
| st.rerun() |
|
|
| st.divider() |
| |
| current_task_id = current_task_data.get("id") |
| if selected_task_name == "➕ 新建任务": |
| if st.button("🚀 启动此临时任务 (Run Once)", type="primary", width="stretch"): |
| config = {"seeds": [s.strip() for s in seeds_input.split('\n') if s.strip()], "interval": iv, "provider": pv, "quality": qual} |
| temp_id = str(uuid.uuid4()) |
| ok, msg = atm.start(config, temp_id, task_name_input, sm, tm, wm, cg) |
| if ok: st.success("已启动"); time.sleep(1); st.rerun() |
| else: st.error(msg) |
| else: |
| is_running_this = current_task_id in atm.active_tasks |
| if is_running_this: |
| if st.button("🛑 停止此任务", type="primary", width="stretch"): |
| atm.stop(current_task_id) |
| st.rerun() |
| else: |
| is_full = len(atm.active_tasks) >= atm.MAX_CONCURRENT_TASKS |
| btn_label = "⚠️ 任务槽已满" if is_full else f"🚀 启动任务: {task_name_input}" |
| if st.button(btn_label, type="primary", disabled=is_full, width="stretch"): |
| config = {"seeds": [s.strip() for s in seeds_input.split('\n') if s.strip()], "interval": iv, "provider": pv, "quality": qual} |
| ok, msg = atm.start(config, current_task_id, task_name_input, sm, tm, wm, cg) |
| if ok: st.success("已启动"); time.sleep(1); st.rerun() |
| else: st.error(msg) |
|
|
| with col_auto_r: |
| st.subheader("📊 运行监控中心") |
| c_m1, c_m2 = st.columns(2) |
| active_items = list(atm.active_tasks.items()) |
| |
| with c_m1: |
| with st.container(border=True): |
| if len(active_items) > 0: |
| tid, tdata = active_items[0] |
| st.markdown(f"**🟢 {tdata['name']}**") |
| st.caption(f"Status: {tdata['status']}") |
| if st.button("🛑 停止", key=f"stop_{tid}", width="stretch"): |
| atm.stop(tid) |
| st.rerun() |
| else: |
| st.markdown(f"**⚪ 通道 1 空闲**") |
| st.markdown("<br>", unsafe_allow_html=True) |
| st.markdown("<br>", unsafe_allow_html=True) |
| |
| with c_m2: |
| with st.container(border=True): |
| if len(active_items) > 1: |
| tid, tdata = active_items[1] |
| st.markdown(f"**🟢 {tdata['name']}**") |
| st.caption(f"Status: {tdata['status']}") |
| if st.button("🛑 停止", key=f"stop_{tid}", width="stretch"): |
| atm.stop(tid) |
| st.rerun() |
| else: |
| st.markdown(f"**⚪ 通道 2 空闲**") |
| st.markdown("<br>", unsafe_allow_html=True) |
| st.markdown("<br>", unsafe_allow_html=True) |
|
|
| st.divider() |
| st.markdown("### 📜 实时运行日志") |
| log_container = st.container(height=500, border=True) |
| if not atm.logs: |
| log_container.info("暂无日志记录...") |
| else: |
| for log in atm.logs: |
| color = "green" if log["level"]=="success" else "red" if log["level"]=="error" else "orange" if log["level"]=="warning" else "blue" |
| log_container.markdown(f":{color}[{log['ts']}] {log['msg']}") |
|
|
| def login_page(): |
| st.markdown("<br><br><br>", unsafe_allow_html=True) |
| st.markdown("<h1 style='text-align: center;'>🔐 AI Studio</h1>", unsafe_allow_html=True) |
| c1, c2, c3 = st.columns([1, 1.2, 1]) |
| with c2: |
| with st.form("login"): |
| u = st.text_input("用户") |
| p = st.text_input("密码", type="password") |
| if st.form_submit_button("登录", type="primary", width="stretch"): |
| am = get_managers()["am"] |
| ok, must = am.login(u, p) |
| if ok: |
| st.session_state.logged_in = True |
| st.session_state.username = u |
| st.session_state.must_change_pw = must |
| st.rerun() |
| else: st.error("错误") |
|
|
| def change_pw_page(): |
| st.title("首次登录需修改密码") |
| with st.form("force_pw"): |
| p1 = st.text_input("新密码", type="password") |
| p2 = st.text_input("确认", type="password") |
| if st.form_submit_button("修改"): |
| if p1==p2: |
| get_managers()["am"].update_password(st.session_state.username, p1) |
| st.session_state.must_change_pw = False |
| st.rerun() |
| else: st.error("不一致") |