diff --git a/all_code.txt b/all_code.txt index 400faac..07d3234 100644 --- a/all_code.txt +++ b/all_code.txt @@ -1429,7 +1429,7 @@ async def sync_cmts_leaves_async(host, username, password, leaf_paths: list, con async with asyncssh.connect(host, username=username, password=password, known_hosts=None) as conn: async with conn.create_process(term_type='xterm-256color', term_size=(200, 24), encoding='utf-8') as process: - async def read_until_quiet(timeout=1.0): + async def read_until_quiet(timeout=1.0, prompt_pattern: str = None): output = "" while True: try: @@ -1439,6 +1439,9 @@ async def sync_cmts_leaves_async(host, username, password, leaf_paths: list, con if "--More--" in chunk or "More" in chunk: process.stdin.write(" ") await process.stdin.drain() + # 🌟 精準 Prompt 偵測 + if prompt_pattern and re.search(prompt_pattern, output): + break except asyncio.TimeoutError: break return output @@ -1447,14 +1450,14 @@ async def sync_cmts_leaves_async(host, username, password, leaf_paths: list, con if cmts_version == "unknown": process.stdin.write("show version\n") await process.stdin.drain() - version_output = await read_until_quiet(timeout=1.5) + version_output = await read_until_quiet(timeout=1.5, prompt_pattern=r"(?:#|>)") match = re.search(r"(?:infra|vcmts-cd-0)\s+([\w\.\-]+)", version_output) if match: cmts_version = match.group(1) process.stdin.write("config\n") await process.stdin.drain() - await read_until_quiet(timeout=1.5) + await read_until_quiet(timeout=1.5, prompt_pattern=r"\(config\)#") for path in batch: # 🌟 優化 1:強迫讓出事件迴圈控制權,讓 FastAPI 去處理其他使用者的請求 @@ -1609,7 +1612,7 @@ async def fetch_raw_config(host: str, username: str, password: str, config_type: async with asyncssh.connect(host, username=username, password=password, known_hosts=None) as conn: async with conn.create_process(term_type='xterm-256color', term_size=(200, 24), encoding='utf-8') as process: - async def read_until_quiet(timeout=2.0): + async def read_until_quiet(timeout=2.0, prompt_pattern: str = None): output = "" while True: try: @@ -1620,25 +1623,28 @@ async def fetch_raw_config(host: str, username: str, password: str, config_type: if "--More--" in chunk or "More" in chunk: process.stdin.write(" ") await process.stdin.drain() + # 🌟 精準 Prompt 偵測 + if prompt_pattern and re.search(prompt_pattern, output): + break except asyncio.TimeoutError: break return output # 🌟 新增這行:剛連線成功後,先清空終端機的登入歡迎詞 (MOTD) 與雜訊 - await read_until_quiet(timeout=1.0) + await read_until_quiet(timeout=1.0, prompt_pattern=r"(?:#|>)") # 關鍵修改:根據 config_type 切換模式與指令,並加上 | nomore 關閉分頁 if config_type == "full": # 1. 先進入 config 模式 process.stdin.write("config\n") await process.stdin.drain() - await read_until_quiet(timeout=1.5) # 等待提示字元變成 (config)# + await read_until_quiet(timeout=1.5, prompt_pattern=r"\(config\)#") # 等待提示字元變成 (config)# # 2. 下達 full-configuration 指令 (🌟 加上 | nomore) cmd = "show full-configuration | nomore" process.stdin.write(f"{cmd}\n") await process.stdin.drain() - raw_output = await read_until_quiet(timeout=3.0) + raw_output = await read_until_quiet(timeout=3.0, prompt_pattern=r"\(config\)#") # 3. 抓完後退回上一層 (保持良好習慣) process.stdin.write("exit\n") @@ -5127,8 +5133,8 @@ export function collapseAll(elementId) { // 🌟 核心函數:生成完整設備配置樹狀圖 (Lazy 版) // ========================================== export function buildTree(node, path = '', isParentCommandGroup = false, mode = 'running') { - let html = ''; - if (!node || typeof node !== 'object') return html; + const htmlParts = []; + if (!node || typeof node !== 'object') return htmlParts.join(''); for (const [key, value] of Object.entries(node)) { const currentPath = path ? `${path}::${key}` : key; @@ -5169,8 +5175,7 @@ export function buildTree(node, path = '', isParentCommandGroup = false, mode = `; - // 🌟 關鍵修改:加入 data-* 屬性,並在 ontoggle 時呼叫 lazyLoadFolder - html += ` + htmlParts.push(`
@@ -5211,9 +5216,8 @@ export function buildTree(node, path = '', isParentCommandGroup = false, mode =
- `; + `); } else { - // 葉節點邏輯保持不變 const isVirtualIndex = /^\[\d+\]$/.test(key); const isNoCommand = (key === 'no'); const safeValue = (value === null ? '' : value).toString().replace(/"/g, """); @@ -5258,7 +5262,7 @@ export function buildTree(node, path = '', isParentCommandGroup = false, mode = `; } - html += ` + htmlParts.push(`
- `; + `); } } - return html; + return htmlParts.join(''); } // ========================================== @@ -5298,7 +5302,8 @@ export function buildTree(node, path = '', isParentCommandGroup = false, mode = export function buildRealFilterTree(data, parentPath = '', hiddenKeys = [], isParentCommandGroup = false, mode = 'running') { if (typeof data !== 'object' || data === null) return ''; - let html = `
`; + const htmlParts = []; + htmlParts.push(`
`); for (const key in data) { const value = data[key]; @@ -5348,7 +5353,7 @@ export function buildRealFilterTree(data, parentPath = '', hiddenKeys = [], isPa `; // 🌟 加入 lazyLoadFolder 觸發 - html += ` + htmlParts.push(`
@@ -5366,9 +5371,8 @@ export function buildRealFilterTree(data, parentPath = '', hiddenKeys = [], isPa
- `; + `); } else { - // 葉節點邏輯保持不變 const isVirtualIndex = /^\[\d+\]$/.test(key); let displayName = isVirtualIndex ? `項目 ${key}` : `${key}`; const safeValue = (value === null ? '' : value).toString().replace(/"/g, """); @@ -5389,7 +5393,7 @@ export function buildRealFilterTree(data, parentPath = '', hiddenKeys = [], isPa currentIcon = ``; } - html += ` + htmlParts.push(`
${displayName}${colonHtml} ${valueHtml}
- `; + `); } } - html += '
'; - return html; + htmlParts.push(''); + return htmlParts.join(''); } ================================================================================ @@ -5655,7 +5659,7 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest): async with asyncssh.connect(req.host, username=req.username, password=req.password, known_hosts=None) as conn: async with conn.create_process(term_type='xterm-256color', term_size=(200, 24), encoding='utf-8') as process: - async def read_until_quiet(timeout=1.0): + async def read_until_quiet(timeout=1.0, prompt_pattern: str = None): output = "" while True: try: @@ -5665,6 +5669,9 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest): if "--More--" in chunk or "More" in chunk: process.stdin.write(" ") await process.stdin.drain() + # 🌟 精準 Prompt 偵測 + if prompt_pattern and re.search(prompt_pattern, output): + break except asyncio.TimeoutError: break return output @@ -5672,14 +5679,14 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest): # 2. 進入設定模式 process.stdin.write("config\n") await process.stdin.drain() - await read_until_quiet(timeout=1.5) + await read_until_quiet(timeout=1.5, prompt_pattern=r"\(config\)#") yield json.dumps({"status": "progress", "message": "✅ 成功進入 Global Configuration 模式"}) + "\n" # 3. 逐行寫入並進行 Fail-safe 檢查 for cmd in commands: process.stdin.write(f"{cmd}\n") await process.stdin.drain() - out = await read_until_quiet(timeout=0.2) + out = await read_until_quiet(timeout=0.2, prompt_pattern=r"\(config.*\)#") clean_out = re.sub(r'\x1b\[[0-9;]*[mGK]', '', out).strip() @@ -5694,7 +5701,7 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest): # 2. 對設備下達 abort 指令 process.stdin.write("abort\n") await process.stdin.drain() - await read_until_quiet(timeout=1.0) # 等待設備處理 abort 並退出 config 模式 + await read_until_quiet(timeout=1.0, prompt_pattern=r"(?:#|>)") # 等待設備處理 abort 並退出 config 模式 # 3. 回報最終錯誤並關閉連線 yield json.dumps({ @@ -5702,7 +5709,6 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest): "message": f"❌ 寫入中斷且已安全撤銷!失敗指令: {cmd}", "output": clean_out }) + "\n" - # 🌟 移除手動 conn.close(),交由 async with 處理 return yield json.dumps({ @@ -5718,8 +5724,7 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest): yield json.dumps({"status": "progress", "message": "⏳ 正在寫入 Commit 保存設定..."}) + "\n" process.stdin.write("commit\n") await process.stdin.drain() - commit_out = await read_until_quiet(timeout=6.0) - clean_commit = re.sub(r'\x1b\[[0-9;]*[mGK]', '', commit_out).strip() + commit_out = await read_until_quiet(timeout=6.0, prompt_pattern=r"\(config\)#") # 5. 退出並關閉連線 process.stdin.write("exit\n") @@ -5753,6 +5758,9 @@ router = APIRouter() async def websocket_terminal(websocket: WebSocket, host: str, username: str, password: str): await websocket.accept() conn = None + process = None + ws_task = None + ssh_task = None try: # 建立連線 conn = await asyncssh.connect(host, username=username, password=password, known_hosts=None) @@ -5774,6 +5782,8 @@ async def websocket_terminal(websocket: WebSocket, host: str, username: str, pas safe_text = data_bytes.decode('utf-8', errors='replace') await websocket.send_text(safe_text) + except asyncio.CancelledError: + pass except Exception as e: print("\n❌ [WS Forward Error] 讀取設備畫面時發生錯誤:") traceback.print_exc() @@ -5791,6 +5801,9 @@ async def websocket_terminal(websocket: WebSocket, host: str, username: str, pas process.stdin.write(data_bytes) await process.stdin.drain() except WebSocketDisconnect: + # 主動拋出,讓外層捕捉以進行資源回收 + raise + except asyncio.CancelledError: pass except Exception as e: print("\n❌ [SSH Forward Error] 寫入指令到設備時發生錯誤:") @@ -5807,6 +5820,12 @@ async def websocket_terminal(websocket: WebSocket, host: str, username: str, pas for task in pending: task.cancel() + except WebSocketDisconnect: + print("[DEBUG] WebSocket 正常斷線,正在終止背景任務...") + if ws_task and not ws_task.done(): + ws_task.cancel() + if ssh_task and not ssh_task.done(): + ssh_task.cancel() except Exception as e: error_msg = f"\r\n\x1b[31mSSH Connection Error: {str(e)}\x1b[0m\r\n" try: @@ -5816,6 +5835,9 @@ async def websocket_terminal(websocket: WebSocket, host: str, username: str, pas print("\n❌ [Connection Error] 建立連線或執行過程中發生錯誤:") traceback.print_exc() finally: + # 🌟 確保 process 與 conn 絕對被關閉,防止 Memory Leak + if process: + process.close() if conn: conn.close() try: @@ -5824,6 +5846,7 @@ async def websocket_terminal(websocket: WebSocket, host: str, username: str, pas pass + ================================================================================ FILE: routers/lock.py ================================================================================ @@ -6294,7 +6317,7 @@ async def execute_cmts_config(req: ConfigRequest): async with conn.create_process() as process: # 🌟 建立安全的非同步讀取函數 (讀到安靜為止,避免卡死) - async def read_until_quiet(timeout=0.5): + async def read_until_quiet(timeout=0.5, prompt_pattern: str = None): out_data = "" while True: try: @@ -6303,6 +6326,9 @@ async def execute_cmts_config(req: ConfigRequest): if not chunk: break out_data += str(chunk) + # 🌟 精準 Prompt 偵測 + if prompt_pattern and re.search(prompt_pattern, out_data): + break except asyncio.TimeoutError: break return out_data @@ -6310,7 +6336,7 @@ async def execute_cmts_config(req: ConfigRequest): # 1. 進入設定模式 process.stdin.write("config\n") await process.stdin.drain() - await read_until_quiet(0.5) # 清空歡迎訊息 + await read_until_quiet(0.5, prompt_pattern=r"\(config\)#") # 清空歡迎訊息 # 2. 逐行送出指令並回報進度 for cmd in commands: @@ -6320,7 +6346,7 @@ async def execute_cmts_config(req: ConfigRequest): # ⏱️ 智慧等待:如果是 commit 指令,給予 5 秒讓設備存檔;其他指令等 0.5 秒 wait_time = 5.0 if cmd.strip() == "commit" else 0.5 - output = await read_until_quiet(wait_time) + output = await read_until_quiet(wait_time, prompt_pattern=r"\(config.*\)#") # 🛡️ 防呆撤銷機制:偵測到錯誤立刻 abort lower_output = output.lower() @@ -6338,7 +6364,7 @@ async def execute_cmts_config(req: ConfigRequest): # 3. 退出設定模式 process.stdin.write("exit\n") await process.stdin.drain() - final_output = await read_until_quiet(1.0) + final_output = await read_until_quiet(1.0, prompt_pattern=r"(?:#|>)") yield json.dumps({ "status": "success", diff --git a/cmts_scraper.py b/cmts_scraper.py index 2dfd694..d059997 100644 --- a/cmts_scraper.py +++ b/cmts_scraper.py @@ -163,7 +163,7 @@ async def sync_cmts_leaves_async(host, username, password, leaf_paths: list, con async with asyncssh.connect(host, username=username, password=password, known_hosts=None) as conn: async with conn.create_process(term_type='xterm-256color', term_size=(200, 24), encoding='utf-8') as process: - async def read_until_quiet(timeout=1.0): + async def read_until_quiet(timeout=1.0, prompt_pattern: str = None): output = "" while True: try: @@ -173,6 +173,9 @@ async def sync_cmts_leaves_async(host, username, password, leaf_paths: list, con if "--More--" in chunk or "More" in chunk: process.stdin.write(" ") await process.stdin.drain() + # 🌟 精準 Prompt 偵測 + if prompt_pattern and re.search(prompt_pattern, output): + break except asyncio.TimeoutError: break return output @@ -181,14 +184,14 @@ async def sync_cmts_leaves_async(host, username, password, leaf_paths: list, con if cmts_version == "unknown": process.stdin.write("show version\n") await process.stdin.drain() - version_output = await read_until_quiet(timeout=1.5) + version_output = await read_until_quiet(timeout=1.5, prompt_pattern=r"(?:#|>)") match = re.search(r"(?:infra|vcmts-cd-0)\s+([\w\.\-]+)", version_output) if match: cmts_version = match.group(1) process.stdin.write("config\n") await process.stdin.drain() - await read_until_quiet(timeout=1.5) + await read_until_quiet(timeout=1.5, prompt_pattern=r"\(config\)#") for path in batch: # 🌟 優化 1:強迫讓出事件迴圈控制權,讓 FastAPI 去處理其他使用者的請求 @@ -343,7 +346,7 @@ async def fetch_raw_config(host: str, username: str, password: str, config_type: async with asyncssh.connect(host, username=username, password=password, known_hosts=None) as conn: async with conn.create_process(term_type='xterm-256color', term_size=(200, 24), encoding='utf-8') as process: - async def read_until_quiet(timeout=2.0): + async def read_until_quiet(timeout=2.0, prompt_pattern: str = None): output = "" while True: try: @@ -354,25 +357,28 @@ async def fetch_raw_config(host: str, username: str, password: str, config_type: if "--More--" in chunk or "More" in chunk: process.stdin.write(" ") await process.stdin.drain() + # 🌟 精準 Prompt 偵測 + if prompt_pattern and re.search(prompt_pattern, output): + break except asyncio.TimeoutError: break return output # 🌟 新增這行:剛連線成功後,先清空終端機的登入歡迎詞 (MOTD) 與雜訊 - await read_until_quiet(timeout=1.0) + await read_until_quiet(timeout=1.0, prompt_pattern=r"(?:#|>)") # 關鍵修改:根據 config_type 切換模式與指令,並加上 | nomore 關閉分頁 if config_type == "full": # 1. 先進入 config 模式 process.stdin.write("config\n") await process.stdin.drain() - await read_until_quiet(timeout=1.5) # 等待提示字元變成 (config)# + await read_until_quiet(timeout=1.5, prompt_pattern=r"\(config\)#") # 等待提示字元變成 (config)# # 2. 下達 full-configuration 指令 (🌟 加上 | nomore) cmd = "show full-configuration | nomore" process.stdin.write(f"{cmd}\n") await process.stdin.drain() - raw_output = await read_until_quiet(timeout=3.0) + raw_output = await read_until_quiet(timeout=3.0, prompt_pattern=r"\(config\)#") # 3. 抓完後退回上一層 (保持良好習慣) process.stdin.write("exit\n") diff --git a/routers/backup.py b/routers/backup.py index b689b0b..a9b6ab8 100644 --- a/routers/backup.py +++ b/routers/backup.py @@ -241,7 +241,7 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest): async with asyncssh.connect(req.host, username=req.username, password=req.password, known_hosts=None) as conn: async with conn.create_process(term_type='xterm-256color', term_size=(200, 24), encoding='utf-8') as process: - async def read_until_quiet(timeout=1.0): + async def read_until_quiet(timeout=1.0, prompt_pattern: str = None): output = "" while True: try: @@ -251,6 +251,9 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest): if "--More--" in chunk or "More" in chunk: process.stdin.write(" ") await process.stdin.drain() + # 🌟 精準 Prompt 偵測 + if prompt_pattern and re.search(prompt_pattern, output): + break except asyncio.TimeoutError: break return output @@ -258,14 +261,14 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest): # 2. 進入設定模式 process.stdin.write("config\n") await process.stdin.drain() - await read_until_quiet(timeout=1.5) + await read_until_quiet(timeout=1.5, prompt_pattern=r"\(config\)#") yield json.dumps({"status": "progress", "message": "✅ 成功進入 Global Configuration 模式"}) + "\n" # 3. 逐行寫入並進行 Fail-safe 檢查 for cmd in commands: process.stdin.write(f"{cmd}\n") await process.stdin.drain() - out = await read_until_quiet(timeout=0.2) + out = await read_until_quiet(timeout=0.2, prompt_pattern=r"\(config.*\)#") clean_out = re.sub(r'\x1b\[[0-9;]*[mGK]', '', out).strip() @@ -280,7 +283,7 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest): # 2. 對設備下達 abort 指令 process.stdin.write("abort\n") await process.stdin.drain() - await read_until_quiet(timeout=1.0) # 等待設備處理 abort 並退出 config 模式 + await read_until_quiet(timeout=1.0, prompt_pattern=r"(?:#|>)") # 等待設備處理 abort 並退出 config 模式 # 3. 回報最終錯誤並關閉連線 yield json.dumps({ @@ -288,7 +291,6 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest): "message": f"❌ 寫入中斷且已安全撤銷!失敗指令: {cmd}", "output": clean_out }) + "\n" - # 🌟 移除手動 conn.close(),交由 async with 處理 return yield json.dumps({ @@ -304,8 +306,7 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest): yield json.dumps({"status": "progress", "message": "⏳ 正在寫入 Commit 保存設定..."}) + "\n" process.stdin.write("commit\n") await process.stdin.drain() - commit_out = await read_until_quiet(timeout=6.0) - clean_commit = re.sub(r'\x1b\[[0-9;]*[mGK]', '', commit_out).strip() + commit_out = await read_until_quiet(timeout=6.0, prompt_pattern=r"\(config\)#") # 5. 退出並關閉連線 process.stdin.write("exit\n") diff --git a/routers/config.py b/routers/config.py index daab193..e3f7acc 100644 --- a/routers/config.py +++ b/routers/config.py @@ -105,7 +105,7 @@ async def execute_cmts_config(req: ConfigRequest): async with conn.create_process() as process: # 🌟 建立安全的非同步讀取函數 (讀到安靜為止,避免卡死) - async def read_until_quiet(timeout=0.5): + async def read_until_quiet(timeout=0.5, prompt_pattern: str = None): out_data = "" while True: try: @@ -114,6 +114,9 @@ async def execute_cmts_config(req: ConfigRequest): if not chunk: break out_data += str(chunk) + # 🌟 精準 Prompt 偵測 + if prompt_pattern and re.search(prompt_pattern, out_data): + break except asyncio.TimeoutError: break return out_data @@ -121,7 +124,7 @@ async def execute_cmts_config(req: ConfigRequest): # 1. 進入設定模式 process.stdin.write("config\n") await process.stdin.drain() - await read_until_quiet(0.5) # 清空歡迎訊息 + await read_until_quiet(0.5, prompt_pattern=r"\(config\)#") # 清空歡迎訊息 # 2. 逐行送出指令並回報進度 for cmd in commands: @@ -131,7 +134,7 @@ async def execute_cmts_config(req: ConfigRequest): # ⏱️ 智慧等待:如果是 commit 指令,給予 5 秒讓設備存檔;其他指令等 0.5 秒 wait_time = 5.0 if cmd.strip() == "commit" else 0.5 - output = await read_until_quiet(wait_time) + output = await read_until_quiet(wait_time, prompt_pattern=r"\(config.*\)#") # 🛡️ 防呆撤銷機制:偵測到錯誤立刻 abort lower_output = output.lower() @@ -149,7 +152,7 @@ async def execute_cmts_config(req: ConfigRequest): # 3. 退出設定模式 process.stdin.write("exit\n") await process.stdin.drain() - final_output = await read_until_quiet(1.0) + final_output = await read_until_quiet(1.0, prompt_pattern=r"(?:#|>)") yield json.dumps({ "status": "success", diff --git a/routers/terminal.py b/routers/terminal.py index f25da43..1f31e6c 100644 --- a/routers/terminal.py +++ b/routers/terminal.py @@ -9,6 +9,9 @@ router = APIRouter() async def websocket_terminal(websocket: WebSocket, host: str, username: str, password: str): await websocket.accept() conn = None + process = None + ws_task = None + ssh_task = None try: # 建立連線 conn = await asyncssh.connect(host, username=username, password=password, known_hosts=None) @@ -30,6 +33,8 @@ async def websocket_terminal(websocket: WebSocket, host: str, username: str, pas safe_text = data_bytes.decode('utf-8', errors='replace') await websocket.send_text(safe_text) + except asyncio.CancelledError: + pass except Exception as e: print("\n❌ [WS Forward Error] 讀取設備畫面時發生錯誤:") traceback.print_exc() @@ -47,6 +52,9 @@ async def websocket_terminal(websocket: WebSocket, host: str, username: str, pas process.stdin.write(data_bytes) await process.stdin.drain() except WebSocketDisconnect: + # 主動拋出,讓外層捕捉以進行資源回收 + raise + except asyncio.CancelledError: pass except Exception as e: print("\n❌ [SSH Forward Error] 寫入指令到設備時發生錯誤:") @@ -63,6 +71,12 @@ async def websocket_terminal(websocket: WebSocket, host: str, username: str, pas for task in pending: task.cancel() + except WebSocketDisconnect: + print("[DEBUG] WebSocket 正常斷線,正在終止背景任務...") + if ws_task and not ws_task.done(): + ws_task.cancel() + if ssh_task and not ssh_task.done(): + ssh_task.cancel() except Exception as e: error_msg = f"\r\n\x1b[31mSSH Connection Error: {str(e)}\x1b[0m\r\n" try: @@ -72,9 +86,13 @@ async def websocket_terminal(websocket: WebSocket, host: str, username: str, pas print("\n❌ [Connection Error] 建立連線或執行過程中發生錯誤:") traceback.print_exc() finally: + # 🌟 確保 process 與 conn 絕對被關閉,防止 Memory Leak + if process: + process.close() if conn: conn.close() try: await websocket.close() except: pass + diff --git a/static/tree-ui.js b/static/tree-ui.js index 617f597..567004b 100644 --- a/static/tree-ui.js +++ b/static/tree-ui.js @@ -74,8 +74,8 @@ export function collapseAll(elementId) { // 🌟 核心函數:生成完整設備配置樹狀圖 (Lazy 版) // ========================================== export function buildTree(node, path = '', isParentCommandGroup = false, mode = 'running') { - let html = ''; - if (!node || typeof node !== 'object') return html; + const htmlParts = []; + if (!node || typeof node !== 'object') return htmlParts.join(''); for (const [key, value] of Object.entries(node)) { const currentPath = path ? `${path}::${key}` : key; @@ -116,8 +116,7 @@ export function buildTree(node, path = '', isParentCommandGroup = false, mode = `; - // 🌟 關鍵修改:加入 data-* 屬性,並在 ontoggle 時呼叫 lazyLoadFolder - html += ` + htmlParts.push(`
@@ -158,9 +157,8 @@ export function buildTree(node, path = '', isParentCommandGroup = false, mode =
- `; + `); } else { - // 葉節點邏輯保持不變 const isVirtualIndex = /^\[\d+\]$/.test(key); const isNoCommand = (key === 'no'); const safeValue = (value === null ? '' : value).toString().replace(/"/g, """); @@ -205,7 +203,7 @@ export function buildTree(node, path = '', isParentCommandGroup = false, mode = `; } - html += ` + htmlParts.push(`
- `; + `); } } - return html; + return htmlParts.join(''); } // ========================================== @@ -245,7 +243,8 @@ export function buildTree(node, path = '', isParentCommandGroup = false, mode = export function buildRealFilterTree(data, parentPath = '', hiddenKeys = [], isParentCommandGroup = false, mode = 'running') { if (typeof data !== 'object' || data === null) return ''; - let html = `
`; + const htmlParts = []; + htmlParts.push(`
`); for (const key in data) { const value = data[key]; @@ -295,7 +294,7 @@ export function buildRealFilterTree(data, parentPath = '', hiddenKeys = [], isPa `; // 🌟 加入 lazyLoadFolder 觸發 - html += ` + htmlParts.push(`
@@ -313,9 +312,8 @@ export function buildRealFilterTree(data, parentPath = '', hiddenKeys = [], isPa
- `; + `); } else { - // 葉節點邏輯保持不變 const isVirtualIndex = /^\[\d+\]$/.test(key); let displayName = isVirtualIndex ? `項目 ${key}` : `${key}`; const safeValue = (value === null ? '' : value).toString().replace(/"/g, """); @@ -336,7 +334,7 @@ export function buildRealFilterTree(data, parentPath = '', hiddenKeys = [], isPa currentIcon = ``; } - html += ` + htmlParts.push(`
${displayName}${colonHtml} ${valueHtml}
- `; + `); } } - html += '
'; - return html; + htmlParts.push(''); + return htmlParts.join(''); } \ No newline at end of file