173 lines
6.3 KiB
Python
173 lines
6.3 KiB
Python
"""
|
||
标伙伴 · 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()
|