tech-bid-manage/launcher.py
2026-04-20 16:21:06 +08:00

173 lines
6.3 KiB
Python
Raw Permalink 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.

"""
标伙伴 · AI标书助手 — 桌面启动器
运行此文件 (或打包后的 bid_partner.exe) 即可自动启动本地服务并打开浏览器。
"""
import os
import sys
import socket
import threading
import time
import webbrowser
import urllib.request
import logging
# ── 找可用端口 ──────────────────────────────────────────────────────────────
def _find_free_port(start: int = 5000, attempts: int = 20) -> int:
for port in range(start, start + attempts):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.bind(('127.0.0.1', port))
return port
except OSError:
continue
return start # 最坏情况:直接用 5000让 Flask 报错
PORT = _find_free_port()
# ── 日志 ────────────────────────────────────────────────────────────────────
def _setup_logging():
if getattr(sys, 'frozen', False):
log_dir = os.path.dirname(sys.executable)
else:
log_dir = os.path.dirname(os.path.abspath(__file__))
log_path = os.path.join(log_dir, 'bid_partner.log')
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
handlers=[logging.FileHandler(log_path, encoding='utf-8', mode='a')],
)
# ── 启动 Flask 服务 ─────────────────────────────────────────────────────────
def _start_server():
try:
import app as flask_app
flask_app.init_db()
flask_app.app.run(
host='127.0.0.1',
port=PORT,
debug=False,
threaded=True,
use_reloader=False,
)
except Exception as e:
logging.getLogger('launcher').error(f'服务启动失败: {e}', exc_info=True)
# ── 等待服务就绪 ─────────────────────────────────────────────────────────────
def _wait_for_server(timeout: int = 60) -> bool:
url = f'http://127.0.0.1:{PORT}'
deadline = time.time() + timeout
while time.time() < deadline:
try:
urllib.request.urlopen(url, timeout=1)
return True
except Exception:
time.sleep(0.4)
return False
# ── 主界面 (tkinter) ─────────────────────────────────────────────────────────
def _run_gui():
import tkinter as tk
from tkinter import ttk, font as tkfont
URL = f'http://127.0.0.1:{PORT}'
root = tk.Tk()
root.title('标伙伴 · AI标书助手')
root.geometry('400x220')
root.resizable(False, False)
root.configure(bg='#f5f5f5')
# ── 标题 ──
title_font = tkfont.Font(family='微软雅黑', size=14, weight='bold')
tk.Label(root, text='标伙伴 · AI 标书助手', font=title_font,
bg='#f5f5f5', fg='#1a1a2e').pack(pady=(22, 4))
# ── 状态行 ──
status_var = tk.StringVar(value='正在启动服务,请稍候…')
status_lbl = tk.Label(root, textvariable=status_var,
font=('微软雅黑', 10), bg='#f5f5f5', fg='#555')
status_lbl.pack(pady=4)
# ── URL 链接 ──
url_lbl = tk.Label(root, text='', font=('Consolas', 10),
bg='#f5f5f5', fg='#1a73e8', cursor='hand2')
url_lbl.pack(pady=2)
url_lbl.bind('<Button-1>', lambda _: webbrowser.open(URL))
# ── 按钮区 ──
btn_frame = tk.Frame(root, bg='#f5f5f5')
btn_frame.pack(pady=18)
open_btn = ttk.Button(btn_frame, text='打开浏览器',
command=lambda: webbrowser.open(URL),
state='disabled', width=14)
open_btn.pack(side='left', padx=8)
quit_btn = ttk.Button(btn_frame, text='退出程序',
command=root.destroy, width=10)
quit_btn.pack(side='left', padx=8)
# ── 版本信息 ──
tk.Label(root, text='单机版 · 本地运行 · 数据不上传',
font=('微软雅黑', 8), bg='#f5f5f5', fg='#aaa').pack(pady=(0, 10))
# ── 后台轮询,服务就绪后更新 UI ──
def _on_ready():
status_var.set('服务已就绪 ✓')
status_lbl.config(fg='#2e7d32')
url_lbl.config(text=URL)
open_btn.config(state='normal')
webbrowser.open(URL)
def _on_timeout():
status_var.set('启动超时,请查看 bid_partner.log')
status_lbl.config(fg='#c62828')
def _check():
if _wait_for_server():
root.after(0, _on_ready)
else:
root.after(0, _on_timeout)
threading.Thread(target=_check, daemon=True).start()
root.mainloop()
# ── 无图形模式(仅控制台) ────────────────────────────────────────────────────
def _run_headless():
print(f'[标伙伴] Starting server on port {PORT} ...')
if _wait_for_server():
print(f'[标伙伴] Ready → http://127.0.0.1:{PORT}')
webbrowser.open(f'http://127.0.0.1:{PORT}')
# 阻塞,直到用户 Ctrl+C
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print('[标伙伴] Shutting down.')
else:
print('[标伙伴] Server did not start within 60 s. Check bid_partner.log.')
# ── 入口 ─────────────────────────────────────────────────────────────────────
def main():
_setup_logging()
server_thread = threading.Thread(target=_start_server, daemon=True)
server_thread.start()
try:
_run_gui()
except Exception:
_run_headless()
if __name__ == '__main__':
main()