'use strict'; // 共用時間與日誌工具:所有訊息輸出統一為 [yyyy/MM/dd HH:mm:ss][階段][等級]: 訊息(Asia/Taipei)。 /** * 將指定時間轉為台北時區(Asia/Taipei)的顯示字串,格式固定為 yyyy/MM/dd HH:mm:ss(24 小時制)。 * 利用 sv-SE 語系的 toLocaleString 產生 yyyy-MM-dd HH:mm:ss 後再把「-」換成「/」, * 輸出不受執行環境(CI runner/主機)系統時區影響。 * * @param {Date} [date=new Date()] 要格式化的時間;省略時使用現在時間。 * 須為有效的 Date 物件;傳入 Invalid Date 會得到 "Invalid Date" 字串(不丟例外), * 傳入非 Date 型別屬誤用,可能丟出 TypeError。 * @returns {string} 台北時區的時間字串,格式 yyyy/MM/dd HH:mm:ss(例如 "2026/07/17 14:30:05")。 * @remarks * 使用情境:log() 每次輸出日誌時呼叫本函式產生時間戳前綴; * taipeiFromIso() 也在解析 ISO 字串成功後委派給本函式做最終格式化。 * 前置條件:無(純函式、無副作用);需要固定顯示格式的時間字串時皆可直接呼叫。 * 注意:格式與 JSC 規範「更新時間一律 Asia/Taipei、yyyy/MM/dd HH:mm:ss」一致,勿自行改動分隔符號。 */ function taipeiNow(date = new Date()) { return date .toLocaleString('sv-SE', { timeZone: 'Asia/Taipei', hour12: false }) .replace(/-/g, '/'); } /** * 產生檔名用的台北時區時間戳,格式固定為 yyyy-MM-dd-HH:mm:ss(24 小時制), * 即把 sv-SE 格式(yyyy-MM-dd HH:mm:ss)中的空白換成「-」,避免檔名含空白。 * 主要供 AI review findings 輸出檔的檔名命名使用。 * * @param {Date} [date=new Date()] 要格式化的時間;省略時使用現在時間。 * 須為有效的 Date 物件;傳入 Invalid Date 會得到 "Invalid-Date" 字串(不丟例外), * 傳入非 Date 型別屬誤用,可能丟出 TypeError。 * @returns {string} 檔名用時間戳字串,格式 yyyy-MM-dd-HH:mm:ss(例如 "2026-07-17-14:30:05")。 * @remarks * 使用情境:產生 findings 檔案(如 .gitea/ai-review 下的輸出檔)時呼叫, * 讓檔名帶有可排序的建立時間。前置條件:無(純函式、無副作用)。 * 注意:輸出仍含「:」字元,在 Linux 檔名合法,但不可移植到 Windows 檔案系統; * 若未來需跨平台檔名,需另行替換「:」。 */ function taipeiFileStamp(date = new Date()) { return date .toLocaleString('sv-SE', { timeZone: 'Asia/Taipei', hour12: false }) .replace(' ', '-'); } /** * 將 ISO 8601 時間字串轉為台北時區(Asia/Taipei)的顯示字串(yyyy/MM/dd HH:mm:ss); * 輸入為空或無法解析時回傳佔位符「—」,不丟例外,適合直接嵌入報表或留言等顯示用文字。 * * @param {string | null | undefined} iso ISO 8601 時間字串(例如 "2026-07-17T06:30:05Z")。 * 可為 null/undefined/空字串,皆視為無資料而回傳「—」。 * 實作上接受任何 Date 建構子可解析的輸入,但非 ISO 格式的解析結果依 JS 引擎而異,建議一律傳 ISO 字串。 * @returns {string} 台北時區時間字串(yyyy/MM/dd HH:mm:ss),或無法解析時的佔位符 "—"。 * @remarks * 使用情境:顯示外部系統(如 Gitea API、AI 服務回應)帶回的 UTC/ISO 時間欄位時呼叫, * 統一轉成台北時區給人閱讀;來源欄位可能缺值,故以「—」佔位而非丟例外。 * 前置條件:無;結果僅供顯示,不應再拿去做時間運算(需運算請直接使用原始 ISO 值)。 */ function taipeiFromIso(iso) { if (!iso) return '—'; const date = new Date(iso); return Number.isNaN(date.getTime()) ? '—' : taipeiNow(date); } /** * 以專案統一格式輸出一行日誌到標準輸出:[yyyy/MM/dd HH:mm:ss][階段][等級]: 訊息, * 時間戳固定為台北時區(Asia/Taipei)24 小時制;stage 為空時整個 [階段] 區塊省略。 * * @param {string | null | undefined} stage 階段名稱(例如 "步驟1"、"收尾"); * 傳空字串/null/undefined 時省略 [階段] 區塊。 * @param {string} level 日誌等級,約定限 "INF"、"WRN"、"ERR"、"TRC"、"DBG" 五種; * 程式碼未驗證,傳入其他字串會原樣輸出,遵守約定由呼叫端負責。 * @param {string} message 日誌訊息內容;非字串會被隱式轉字串(物件會變成 "[object Object]"), * 請由呼叫端先自行序列化。 * @returns {void} 無回傳值;副作用為寫一行到 stdout。 * @remarks * 使用情境:action 執行過程中的所有訊息輸出都應改呼叫本函式而非直接 console.log, * 讓 CI(Gitea Actions)log 具備一致的時間戳與等級標記、一行一則。 * 注意:所有等級(含 ERR)都輸出到 stdout 而非 stderr;此格式對應 JSC 的 * spec-time-log 輸出規範,勿自行變更括號與冒號排版。 */ function log(stage, level, message) { const stagePart = stage ? `[${stage}]` : ''; console.log(`[${taipeiNow()}]${stagePart}[${level}]: ${message}`); } module.exports = { taipeiNow, taipeiFileStamp, taipeiFromIso, log };