""" 标伙伴 · 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('', 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()