jb/script_manager.py
2025-06-05 16:04:09 +08:00

419 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import sqlite3
import os
import datetime
import json
import importlib.util
from typing import List, Dict
import pyperclip
class ScriptManager:
def __init__(self, db_path="scripts.db", scripts_dir="scripts"):
self.db_path = db_path
self.scripts_dir = scripts_dir
self._init_db()
self._init_scripts_dir()
self.tag_library = {
"文本": ["string", "text", "format", "generate"],
"转换": ["convert", "transform", "format"],
"处理": ["process", "handle", "manipulate"],
"生成": ["create", "generate", "make"],
"解析": ["parse", "extract", "analyze"]
}
def _init_db(self):
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS scripts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
path TEXT NOT NULL,
ui_path TEXT NOT NULL,
created_at TEXT NOT NULL,
tags TEXT,
abbreviation TEXT,
usage_count INTEGER DEFAULT 0
)
""")
conn.commit()
def _init_scripts_dir(self):
os.makedirs(self.scripts_dir, exist_ok=True)
def _suggest_tags(self, script_name: str) -> List[str]:
name = script_name.lower()
suggested_tags = []
for category, keywords in self.tag_library.items():
if any(keyword in name for keyword in keywords):
suggested_tags.append(category)
return suggested_tags
def add_script(self, script_name: str, script_content: str = None, script_path: str = None,
ui_content: str = None) -> Dict:
if not script_path:
script_name_clean = script_name.replace(" ", "_").replace("/", "_").replace("\\", "_")
script_path = os.path.join(self.scripts_dir, f"{script_name_clean}.py")
ui_path = os.path.join(self.scripts_dir, f"{script_name_clean}.ui.py")
if not script_content:
script_content = '''def process(input_strings: list) -> list:
"""处理输入字符串并返回结果"""
return [s.upper() for s in input_strings]
'''
if not ui_content:
ui_content = '''import tkinter as tk
from tkinter import ttk
import pyperclip
def create_ui(parent, run_callback):
"""创建前端界面"""
frame = ttk.Frame(parent)
frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
frame.columnconfigure(1, weight=1)
ttk.Label(frame, text="输入字符串 1").grid(row=0, column=0, sticky=tk.W, pady=2)
input_entry = ttk.Entry(frame)
input_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=2)
ttk.Label(frame, text="输出结果 1").grid(row=1, column=0, sticky=tk.W, pady=2)
output_entry = ttk.Entry(frame, state="readonly")
output_entry.grid(row=1, column=1, sticky=(tk.W, tk.E), pady=2)
def copy_output(event):
output = output_entry.get()
if output:
pyperclip.copy(output)
tk.messagebox.showinfo("提示", "输出已复制到剪贴板")
output_entry.bind("<Button-1>", copy_output)
def run():
input_text = input_entry.get().strip()
if not input_text:
tk.messagebox.showerror("错误", "请输入字符串")
return
result = run_callback([input_text])
output_entry.configure(state="normal")
output_entry.delete(0, tk.END)
output_entry.insert(0, result[0] if result else "")
output_entry.configure(state="readonly")
ttk.Button(frame, text="运行脚本", command=run).grid(row=2, column=0, columnspan=2, pady=5)
return frame
'''
with open(script_path, 'w', encoding='utf-8') as f:
f.write(script_content)
with open(ui_path, 'w', encoding='utf-8') as f:
f.write(ui_content)
abbr = ''.join(word[0].upper() for word in script_name.split('_') if word)
suggested_tags = self._suggest_tags(script_name)
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute("""
INSERT INTO scripts (name, path, ui_path, created_at, tags, abbreviation, usage_count)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (
script_name,
script_path,
ui_path,
datetime.datetime.now().isoformat(),
json.dumps(suggested_tags),
abbr,
0
))
conn.commit()
return {
"id": cursor.lastrowid,
"name": script_name,
"path": script_path,
"ui_path": ui_path,
"suggested_tags": suggested_tags,
"abbreviation": abbr
}
def list_scripts(self, tag: str = None, name: str = None) -> List[Dict]:
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
query = "SELECT * FROM scripts"
params = []
conditions = []
if tag:
conditions.append("tags LIKE ?")
params.append(f'%{tag}%')
if name:
conditions.append("name LIKE ?")
params.append(f'%{name}%')
if conditions:
query += " WHERE " + " AND ".join(conditions)
cursor.execute(query, params)
scripts = []
for row in cursor.fetchall():
try:
tags = json.loads(row[5]) if row[5] and row[5].strip() else []
except json.JSONDecodeError:
tags = [] # Fallback to empty list if JSON is invalid
scripts.append({
"id": row[0],
"name": row[1],
"path": row[2],
"created_at": row[3],
"tags": tags,
"abbreviation": row[5],
"usage_count": row[6],
"ui_path":row[7]
})
return scripts
def run_script(self, script_id: int, input_strings: List[str]) -> List[str]:
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute("SELECT path FROM scripts WHERE id = ?", (script_id,))
result = cursor.fetchone()
if not result:
raise ValueError("脚本未找到")
script_path = result[0]
cursor.execute("UPDATE scripts SET usage_count = usage_count + 1 WHERE id = ?", (script_id,))
conn.commit()
spec = importlib.util.spec_from_file_location("script_module", script_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module.process(input_strings)
def load_ui_module(self, ui_path: str):
"""加载前端界面模块"""
if not ui_path or not os.path.isfile(ui_path):
raise ValueError(f"前端界面文件不存在或路径无效:{ui_path}")
try:
spec = importlib.util.spec_from_file_location("ui_module", ui_path)
if spec is None:
raise ValueError(f"无法加载前端界面模块:{ui_path} (模块规格无效)")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
except Exception as e:
raise ValueError(f"加载前端界面失败:{str(e)}")
class ScriptManagerGUI:
def __init__(self, root):
self.root = root
self.root.title("脚本管理器")
self.manager = ScriptManager()
self.selected_script_id = None
self.current_ui_frame = None
self.create_widgets()
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
def create_widgets(self):
# 使用 Notebook 创建选项卡
self.notebook = ttk.Notebook(self.root)
self.notebook.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=10, pady=5)
self.notebook.columnconfigure(0, weight=1)
self.notebook.rowconfigure(0, weight=1)
# 添加脚本选项卡
add_frame = ttk.Frame(self.notebook, padding="10")
self.notebook.add(add_frame, text="添加脚本")
add_frame.columnconfigure(1, weight=1)
add_frame.rowconfigure(3, weight=1)
# 脚本名称
ttk.Label(add_frame, text="脚本名称:").grid(row=0, column=0, sticky=tk.W, pady=5)
self.script_name_var = tk.StringVar()
ttk.Entry(add_frame, textvariable=self.script_name_var).grid(row=0, column=1, sticky=(tk.W, tk.E), pady=5)
# 脚本路径
ttk.Label(add_frame, text="脚本路径(可选):").grid(row=1, column=0, sticky=tk.W, pady=5)
self.script_path_var = tk.StringVar()
ttk.Entry(add_frame, textvariable=self.script_path_var).grid(row=1, column=1, sticky=(tk.W, tk.E), pady=5)
ttk.Button(add_frame, text="选择文件", command=self.browse_file).grid(row=1, column=2, padx=5)
# 标签
ttk.Label(add_frame, text="标签(逗号分隔,可选):").grid(row=2, column=0, sticky=tk.W, pady=5)
self.tags_var = tk.StringVar()
ttk.Entry(add_frame, textvariable=self.tags_var).grid(row=2, column=1, sticky=(tk.W, tk.E), pady=5)
# 脚本内容
ttk.Label(add_frame, text="脚本内容(可选):").grid(row=3, column=0, sticky=tk.W, pady=5)
self.script_content_text = tk.Text(add_frame, height=5)
self.script_content_text.grid(row=3, column=1, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5)
# 前端界面内容
ttk.Label(add_frame, text="前端界面内容(可选):").grid(row=4, column=0, sticky=tk.W, pady=5)
self.ui_content_text = tk.Text(add_frame, height=5)
self.ui_content_text.grid(row=4, column=1, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5)
ttk.Button(add_frame, text="添加脚本", command=self.add_script).grid(row=5, column=0, columnspan=3, pady=5)
# 索引运行脚本选项卡
run_frame = ttk.Frame(self.notebook, padding="10")
self.notebook.add(run_frame, text="索引运行脚本")
run_frame.columnconfigure(0, weight=1)
run_frame.columnconfigure(2, weight=1)
run_frame.rowconfigure(0, weight=1)
# 左侧:搜索和脚本列表
left_frame = ttk.Frame(run_frame)
left_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 5))
left_frame.columnconfigure(0, weight=1)
left_frame.rowconfigure(1, weight=1)
ttk.Label(left_frame, text="搜索(名称或标签):").grid(row=0, column=0, sticky=tk.W, pady=5)
self.search_var = tk.StringVar()
ttk.Entry(left_frame, textvariable=self.search_var).grid(row=0, column=1, sticky=(tk.W, tk.E), pady=5)
ttk.Button(left_frame, text="搜索", command=self.search_scripts).grid(row=0, column=2, padx=5)
self.tree = ttk.Treeview(left_frame, columns=("ID", "名称", "标签", "缩写", "使用次数", "创建时间"),
show="headings")
# Set column headings
self.tree.heading("ID", text="ID")
self.tree.heading("名称", text="名称")
self.tree.heading("标签", text="标签")
self.tree.heading("缩写", text="缩写")
self.tree.heading("使用次数", text="使用次数")
self.tree.heading("创建时间", text="创建时间")
# Set column widths (in pixels)
self.tree.column("ID", width=25, minwidth=25, stretch=False) # Narrow for ID
self.tree.column("名称", width=150, minwidth=100, stretch=True) # Wider for name
self.tree.column("标签", width=100, minwidth=80, stretch=True) # Medium for tags
self.tree.column("缩写", width=60, minwidth=50, stretch=False) # Narrow for abbreviation
self.tree.column("使用次数", width=80, minwidth=60, stretch=False) # Narrow for usage count
self.tree.column("创建时间", width=120, minwidth=100, stretch=True) # Medium for date
# Place the Treeview in the grid
self.tree.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S))
self.tree.bind("<<TreeviewSelect>>", self.on_script_select)
# 分割线
ttk.Separator(run_frame, orient=tk.VERTICAL).grid(row=0, column=1, sticky=(tk.N, tk.S), padx=5)
# 右侧:动态前端界面
self.io_frame = ttk.Frame(run_frame)
self.io_frame.grid(row=0, column=2, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(5, 0))
self.io_frame.columnconfigure(0, weight=1)
self.io_frame.rowconfigure(0, weight=1)
# 加载脚本列表
self.refresh_script_list()
def browse_file(self):
file_path = filedialog.askopenfilename(filetypes=[("Python files", "*.py")])
if file_path:
self.script_path_var.set(file_path)
def refresh_script_list(self, search_term: str = None):
for item in self.tree.get_children():
self.tree.delete(item)
scripts = self.manager.list_scripts(tag=search_term, name=search_term)
for script in scripts:
self.tree.insert("", tk.END, values=(
script["id"],
script["name"],
", ".join(script["tags"]),
script["abbreviation"],
script["usage_count"],
script["created_at"]
))
def add_script(self):
script_name = self.script_name_var.get().strip()
script_path = self.script_path_var.get().strip()
script_content = self.script_content_text.get("1.0", tk.END).strip()
ui_content = self.ui_content_text.get("1.0", tk.END).strip()
tags = [t.strip() for t in self.tags_var.get().split(",") if t.strip()]
if not script_name and not script_path:
messagebox.showerror("错误", "脚本名称和脚本路径不能同时为空")
return
if not script_path and not script_content:
messagebox.showerror("错误", "脚本路径和脚本内容不能同时为空")
return
if not script_name and script_path:
script_name = os.path.splitext(os.path.basename(script_path))[0]
try:
result = self.manager.add_script(script_name, script_content or None, script_path or None,
ui_content or None)
if tags:
self.manager.update_tags(result["id"], tags)
messagebox.showinfo("成功", f"脚本 '{script_name}' 已添加!\n建议标签:{', '.join(result['suggested_tags'])}")
self.script_name_var.set("")
self.script_path_var.set("")
self.script_content_text.delete("1.0", tk.END)
self.ui_content_text.delete("1.0", tk.END)
self.tags_var.set(", ".join(result["suggested_tags"]))
self.refresh_script_list()
except Exception as e:
messagebox.showerror("错误", f"添加脚本失败:{str(e)}")
def on_script_select(self, event):
selection = self.tree.selection()
if selection:
item = self.tree.item(selection[0])
self.selected_script_id = int(item["values"][0])
script = next(s for s in self.manager.list_scripts() if s["id"] == self.selected_script_id)
self.refresh_io_frame(script["ui_path"], script["path"])
def refresh_io_frame(self, ui_path: str = None, script_path: str = None):
# 清空现有界面
if self.current_ui_frame:
self.current_ui_frame.destroy()
if not ui_path or not script_path:
self.current_ui_frame = ttk.Label(
self.io_frame,
text="请选择一个脚本",
wraplength=int(self.io_frame.winfo_width() * 0.9) or 300
)
self.current_ui_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
return
try:
# 加载前端界面模块
ui_module = self.manager.load_ui_module(ui_path)
# 创建运行回调
def run_callback(input_strings):
return self.manager.run_script(self.selected_script_id, input_strings)
# 调用前端界面的 create_ui 函数
self.current_ui_frame = ui_module.create_ui(self.io_frame, run_callback)
self.current_ui_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
except Exception as e:
frame_width = self.io_frame.winfo_width() or 300
self.current_ui_frame = ttk.Label(
self.io_frame,
text=f"加载前端界面失败:{str(e)}",
wraplength=int(frame_width * 0.9),
padding=(5, 5),
foreground="red"
)
self.current_ui_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
def search_scripts(self):
search_term = self.search_var.get().strip()
self.refresh_script_list(search_term)
if __name__ == "__main__":
root = tk.Tk()
root.geometry("800x600")
app = ScriptManagerGUI(root)
root.mainloop()