2024年7月16日 星期二

SanDisk RMA 海外送修體驗 - Dual Drive Go USB Type-C 512GB


之前買 SanDisk USB 隨身碟覺得很小,十分適合隨身攜帶。想到之前 Macbook 蓋上後無法開機而送修的經驗,隨時加密備份資料到 USB 隨身碟,變成每週想到就會做一下事,雖然 iCloud 付費甚至免費的空間也夠用,但我也曾經歷過 iCloud 同步失敗事件 Orz 最終還是自己弄一套本地備份的機制吧!

不知是不是太常備份?在 macos 系統中,如果資料搬移的過程沒有正常走完時,隨身碟很容易進入異常狀態,接著 macOS 無法辨識。這時把它插進 Windows 系統,Windows 會偵測該隨身碟狀態,如果發現異常時會詢問是否嘗試修復,滿高的機率會修復成功的。然後,這一次沒那麼好運,連 Windows 也辨識不出,直接顯示有問題 Orz 


在完全辨識不出之前,其實也有徵兆,發現幾十GB的單一大檔案資料複製失敗,接著就自己手賤拔掉它(沒有在 macOS 安心退出),然後在 Windows 11 環境上也顯示異常無法修復。就這樣開始體驗送修機制。

若隨身碟外觀一切良好,建議回顧一下當初買到時,封面還背面到底貼了哪一家代理商的貼紙,例如展碁國際等,但我的案例是已經不知道了,再加上隨身碟外觀有點損傷(可移動的蓋子壞了被我拔掉),就聯絡了 WD (2016收購SanDisk),但起源發信給 support@SanDisk.com ,並提供當初在哪個地方購買的發票證明


這時客服也會先引導你是否要先找當地代理商換貨,最終我的案例是建立 RMA,並把商品使用掛號郵寄寄到香港檢測,若情況可以換新品,接著對方也會快遞把新品寄到指定的地點。使用者承擔掛號郵寄的費用,WD承擔寄送到使用者的快遞費用。

一個隨身碟掛號郵寄到香港,約80台幣,此外,收快遞進台灣還得處理 EZWay 報關,對方會申報商品價值,如 10 美金。

掛號郵寄到對方約四天,快遞收件也差不多四天,整體服務體驗很不錯。

2024年7月15日 星期一

macOS - android fileTransfer 下載位置 AndroidFileTransfer.dmg


圖:2024-07-15 https://www.android.com/filetransfer/


以前都用 Google 找 Android fileTransfer 來下載,現在有點詭異,不知為何連到 https://www.android.com/filetransfer/ 時,首頁看不到下載位置。這大概是 2024 年春天以後發生的事。

只好亂翻一下得到 google.com 下載位置,像是用 web.archive.org 定位一下:
有興趣可以在這邊回顧,看來是 2024-05-21 起就不見了:




2024年7月2日 星期二

Linux 開發維護 - OpenSSH Server 資安漏洞補強,資安事件代號:CVE-2024-6387 @ Ubuntu 22.04

2024.07.02 一早就看到了 OpenSSH Server 漏洞,到處都在講,可參考 iThome 的文章:

iThome 2024.07.02 - OpenSSH含有可遠端攻陷伺服器的回歸漏洞

其中文章最後有提到:

OpenSSH團隊亦於本周一釋出了OpenSSH 9.8/9.8p1, 以修補CVE-2024-6387及另一個邏輯漏洞。該團隊亦說明,在實驗室環境下,要攻陷CVE-2024-6387需要不斷建立連結並持續6~8小時,直至達到伺服器極限,目前已於具備ASLR的32位元Linux系統上成功展現。

因此,除了修正外,也可以安裝一些阻擋 retry ssh 登入的行為,以前學生時代裝 denyhosts ,現在問 ChatGPT 得到 fail2ban 套件,在 Ubuntu 22.04 快速上手:

$ sudo apt-get update
$ sudo apt-get install fail2ban
$ sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
$ sudo systemctl restart fail2ban

建議還是要去看一下 /etc/fail2ban/jail.local 設定,像是 sshd retry 幾次會阻擋,以此評估是不是適合自己(常打錯密碼擋到自己也很無奈的)。裝完就可以用以下指令得知資訊:

$ sudo fail2ban-client status sshd 
Distributor ID: Ubuntu 
Description: Ubuntu 22.04.4 LTS 
Release: 22.04 
Codename: jammy 
Status for the jail: sshd 
|- Filter 
| |- Currently failed: 4 
| |- Total failed: 13 
| `- File list: /var/log/auth.log 
`- Actions 
|- Currently banned: 0 
|- Total banned: 0 
`- Banned IP list: 

不過,回到 CVE-2024-6387 資安事件,其實只要單純把 Ubuntu 系統更新,Ubuntu TLS 都給你傳便便:


Ubuntu 24.04
openssh-client - 1:9.6p1-3ubuntu13.3
openssh-server - 1:9.6p1-3ubuntu13.3

Ubuntu 23.10
openssh-client - 1:9.3p1-1ubuntu3.6
openssh-server - 1:9.3p1-1ubuntu3.6

Ubuntu 22.04
openssh-client - 1:8.9p1-3ubuntu0.10
openssh-server - 1:8.9p1-3ubuntu0.10

例如 Ubuntu 22.04 openssh 1:8.9p1-3ubuntu0.10 內容:


openssh 1:8.9p1-3ubuntu0.10 source package in Ubuntu
Changelog
openssh (1:8.9p1-3ubuntu0.10) jammy-security; urgency=medium

  * SECURITY UPDATE: remote code execution via signal handler race
    condition (LP: #2070497)
    - debian/patches/CVE-2024-6387.patch: don't log in sshsigdie() in log.c.
    - CVE-2024-6387

 -- Marc Deslauriers <email address hidden>  Wed, 26 Jun 2024 09:11:55 -0400

而自己的系統就只要查一下是否有更新至此版即可:

$ sudo apt upgrade && sudo apt upgrade && lsb_release -a && sudo apt policy openssh-server && ssh -V
...
Distributor ID: Ubuntu
Description: Ubuntu 22.04.4 LTS
Release: 22.04
Codename: jammy

openssh-server:
  Installed: 1:8.9p1-3ubuntu0.10
  Candidate: 1:8.9p1-3ubuntu0.10
  Version table:
 *** 1:8.9p1-3ubuntu0.10 500
        500 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 Packages
        500 http://security.ubuntu.com/ubuntu jammy-security/main amd64 Packages
        100 /var/lib/dpkg/status
     1:8.9p1-3 500
        500 http://archive.ubuntu.com/ubuntu jammy/main amd64 Packages

OpenSSH_8.9p1 Ubuntu-3ubuntu0.10, OpenSSL 3.0.2 15 Mar 2022

2024年6月29日 星期六

ThinkPad E15 開機無法偵測到 SSD 排除 - 2101: Detection error on Storage Device0 (M.2 Main Slot)

這台是 ThinkPad E15 2020年版,是疫情期間買給長輩用的平價筆電,在 2024.06 開機讀不到硬碟,由於已經沒有保固,再加上想要送回廠修理的方式,全台只有四個點且週末、假日不營業,實在不太方便,就乾脆自己修吧!


給原廠修的主因是想保有正版 Windows,省去煩惱一些資安問題,而自己處理則是可以加強隱私保護,確保沒人可以存取硬碟內的資訊,像是自己物理破壞掉也行。

研究了一下,據說 Windows 8 以後的筆電,會把 Windows 序號已經綁定在韌體上(BIOS?),因此筆電外殼也不會再貼序號貼紙,就這樣,只需看看能不能取得正版 Windows ISO 即可重新安裝:

首先,先研究一下如何拆筆電後蓋殼子,隨意找一下,發現有個 Youtube 影片教學快速看一下,剛好家裡也有個塑膠墊片就拿來開了!第一次沒經驗也把塑膠墊片弄得變形 XD 整體上需要一點點技巧的,可直接看官網 Support 教學影片:Lenovo Support 拆解與更換影片 > 底蓋拆卸

先買了一個 SSD 外接盒(非必要)想試試看救資料,結果失敗 Orz 看來 SSD 真的一壞了就難靠個人救了,要也得交給專業的來。回過頭來,也買一個特價中 MSI M371 1TB NVMe M.2 SSD 來使用,接著使用微軟官網下載的 ISO 檔,實際測試安裝都是失敗告收 XD 使用 Windows 10 或 11 的 x64 ISO 製成的 USB 安裝碟,永遠卡在要指定驅動程式的步驟:


原先還誤以為 SSD 沒抓到,跑去研究更新 BIOS 等事情,最終回想起 Lenovo USB Recovery Creator 工具,透過 Lenovo USB Recovery Creator 工具可以找到符合自己筆電的還原檔案(總共約11GB)並製作成還原隨身碟,必須找另一台 Windows 環境來運行該程式。整體上花 10分鐘下載資料,製作 USB 開機碟還要花個 20 分鐘。



終於,可以用 USB 還原隨身碟安裝 Windows 10 了:


以上步驟筆記:
經過了上述流程,終於筆電開起來是一個 Windows 10 Home 環境,接著就是一連串的 Windows 更新(數小時),預計最後要升級到 Windows 11 才收工,努力了 Orz 這時記得要先調整電源方案,調整成使用電源供電中不休眠。

當 Windows 10 安全更新至最新後,一直沒法收到 Windows 11 通知,只有一些 Windows 11 的簡介,像是跳轉到微軟官網看。後來透過以下方式可以更主動升級到 Windows 11:

2024年6月20日 星期四

AI開發筆記 - 使用 OpenAI API 對文字抽特徵向量 (embedding) 並使用 FacebookResearch Faiss library 作向量搜尋

之前公司已經訓練出簡單的 AI Model 做一些應用,想要使用 Retrieval Augmented Generation (RAG) 架構進行加強,現在就先想到靠 OpenAI API with text-embedding-3-small model 來對文字抽特徵向量,接著把它存起來,再透過 FacebookResearch Faiss library 建立搜尋架構(尋找相似的向量),就可以完成簡易的向量搜尋。

整體流程:
  1. 整理一份 FAQ csv 格式,並把問題的文字,使用 text-embedding-3-small 模型抽特徵向量
  2. 使用 python pandas 用 csv 結構存起來
  3. 從 csv 建構出 python pandas 結構,再轉成 Facebook Faiss 結構
  4. 把 query strings 使用 text-embedding-3-small 模型對文字抽特徵向量
  5. 再拿 query strings 特徵向量去搜尋相似向量,印出五筆資料

% python3 -m venv venv
% source venv/bin/activate
(venv) % python3 -V
Python 3.12.2
(venv) % sw_vers 
ProductName: macOS
ProductVersion: 14.5
BuildVersion: 23F79
(venv) % pip install -r requirements.txt 

輸入資料:

% cat .env 
OPENAI_API_KEY=sk-proj-XXXXXXXXXXXXXXXXXXXXXX

% tree data 
data
├── all_with_embeddings.csv
└── raw.csv

1 directory, 2 files

% head -n 1 data/raw.csv 
ID,Product,Question,Answer

% head -n 2 data/all_with_embeddings.csv 
ID,Product,Question,Answer,embedding
1,產品,如何安裝設備,1. 首先依照...,"[0.02327146753668785, -0.05237892270088196, ..., -0.001935890642926097]"

運行查詢:

% bash script_query.sh "我的設備沒有訊號,該怎樣處理"

2024年6月17日 星期一

2024生成式AI年會回放筆記 - 玉山銀行 GENIE: 金融服務建置生成式 AI 的困境與攻略


年初滑 linkedin 時就曾看到布丁提及 2024生成式AI年會 的活動,不過時間太遙遠,很難確保沒有期他活動會撞期(家庭第一)。後來還真的邊渡假邊看共筆筆記,第一眼就吸引我的是來自玉山銀行的徐銘霞的分享,自稱 GENIE 夫人,玉山銀行智能金融處主任工程師。從她的投影片中,已經可以窺探許多珍貴的研發經驗,包括從五人兼職的團隊做出POC後,跟長官要求資源(算力經費跟人力)成長到百人團隊,實在驚人,瞬間有感碰到貴人跟大公司資源的感慨,不是每個人都有這樣的緣分能被賞識且被投資,當然,不能抹滅的還是團隊的執行力。

由於網路上的討論文不算多,大多都被網紅 志祺七七 跟 薩泰爾娛樂 Sunny 給吸引走 XD 他們的分享文當然是很讚的,只是場域上跟我在意的 GAI 戰場不同,所以還是來寫一篇筆記吧!

我主要持續關注聯發科 DaVinci(達哥) 生成式AI平台,也加入 DaVinci (達哥)生成式AI平台社群 ,而玉山 GENIE 也正是同領域的!把玉山 GENIE 的分享+聯發科 達哥資訊拼湊一下,已經有非常多的乾貨可吸收。甚至財經網紅股癌在六月多集 podcast 也分享起聯發科達哥的一些細節。回過頭來,實在感恩有回放可以看,想到就覺得滿爽的,可以兩倍速以及不斷咀嚼片段,甚至講者沒有分享投影片也能從回放影片看到。

此場演講內容中,提到的玉山一開始切入點就是先做集團內的 ChatGPT 服務,做完再導入特定型應用的研發,接著提到全體公司使用率低,以及未來的規劃等。

一些足跡恰好讓我回想起近兩年公司的變化,先試著用 OpenAI API 包裝一些應用場景,如會議紀錄,可惜會議情境場域變化太多,想要靠同樣的 prompt 吃天下太難了,隨著時間,在 2024 年春天老闆搖旗吶喊,指派我幫公司導入 AI 輔助應用,當時研究幾輪後,先求個七十分,筆記了 企業導入 AI 輔助方案:廣義用法 ChatGPT / 設計行銷 Stable Diffusion 與 AI 主播 / 程式開發 Github Copilot 方案,其過程也研究了不少款對岸研發的 open source ,像是自行搭建 Private ChatGPT Web 的方式,或是 Coding 輔助抽換成各家 GAI API 方案等。

回到 GENIE 夫人 的演講內容,有一張投影片提到了整個研發時程:
  • [外部] 2022.11 ChatGPT 橫空出世
  • [內部] 2022.12 玉山行內 GAI 黑客松、LLM/RAG、PoC
  • [外部] 2023.02 Bing Chat
  • [外部] 2023.03 OpenAI GPT-4, Google Gemini 公布
  • [內部] 2023.03 玉山高階長官 GAI 科普課程、成立全行 GAI 工作小組
  • [內部] 2023.03 ~ 2023.06 玉山內部推廣 GAI WorkShop、GAI工作小組確認法規與需求
  • [內部] 2023.06 ~ 2023.09 建構通用型服務 GENIE 平台
  • [內部] 2023.09 ~ 2023.12 GENIE 試營運、開發特定型應用
  • [內部] 2024.01 GENIE 正式上線、特定型應用(RAG+GAI)上線測試
可以看到 GENIE 前期準備時程約 3個月(2023.03~2023.06),開發時程約 3個月 (2023.06~2023.09),試營運也是 3個月。其中,也看到玉山的 Best Practice 分享 - GENIE x RPA: 業務單位自行開發的負面新聞摘要日報,透過 RPA爬交易對手的相關新聞,每天利用 GENIE 判斷新聞為正面或負面,並提供摘要

我之前已接觸過一些 no code 服務略知一二,有些工程上要花大量心力研發的“模組”已經完全可以靠 LLM 頂替了。例如新聞的正面或負面的判斷上,可能一般人看到一兩句話沒有太多感觸,過去有緣在新創研發環境上,當時有個題目在做美食文章分析模組,粗略可以想成工程團隊:
  • 兩人開發 crawler 
  • 兩人開發分析模組,判斷文章是正面描述美食,還是負面描述美食
  • 兩人進行系統整合,並且建構出人機回饋評分架構,讓 n>5 位受過中文系專業訓練的成員評分模組判斷的正確性
這樣的人力配置,用 5位工程師來說嘴,一年就要燒掉 600~800萬的費用的。而現在,只要套用個 LLM 來判斷!真的很省錢。在另一場運輸領域的,也有用 OCR+LLM+RPA 加速百種紙本單據轉數位化的研發,有興趣都可以追一下。如李宏毅教授的線上課程【生成式AI導論 2024】第0講:課程說明,一開始也就舉這個為例:


回過頭來,真心感謝玉山銀行的徐銘霞的分享,如演講題目的“攻略“,想要把 AI輔助 導入公司使用真的很費心的,心有戚戚焉啊,包括並不是每個人可以從 ChatGPT 中得到成果,如同 Google Search 擺在眼前,高手跟普通人的搜尋效率還是天差地遠的。

這時就得佩服 志祺七七 推出的 simpleinfo.ai ,在業配腳本上規劃 SOP 來讓公司同仁能更快上手,這跟一些管理書籍提到高度相關,很多超強的大企業,其管理目標就是不斷將做事方式切得更細,目的是讓普羅大眾也能做好。

最後,有興趣可考慮看看 2024年會回放影片:videos.gaiconf.com/view/gaiconf1 ,感謝策展人李慕約,看完其他回放,才發現講者的議題都源自於策展人的規劃的,真收穫滿滿。

相關資訊:

2024年5月29日 星期三

2024 about time


前幾年趁 apple tv+ about time 特價台幣 90元收藏了。老實說,不曾再完整看過,夜深人靜時,還是會回想起片段的劇情,特別是人生的選擇,一旦做了選擇,正向的說,產生了新的變化,負面來說,已經無法退回到過去。

翻一下 blog ,原來七年前也曾 murmur 過,哈。

近幾年的變化,可能更加重偏心靈層面?似乎跟七年前不太一樣,唯一不變的是對時間的焦愁,只能不斷提醒自己,有些事該放下的。然後,開始重視人生資源的分配,六個罐子,雖然是被用在理財角度,其實最貴的是時間,包括自己周邊的親友和人事物。

像工作上碰到處理事情效率不佳的情況時,會退回一步想,是啊,大家都是混口飯吃的 XD 儘管資源重組(裁員)已成常態,但別把這當理由強推。許多管理書籍的重點也落在 SOP 這領域,重點是讓事情有進展,並且不依賴超強人才特質,實務上 overqualified 的人才也待不久的,卻也令人期待人才蛻變後,主管們多了這甜蜜的煩惱。

關於親友之間,反而被些大學好友不斷互相砥礪,持續增加自己的視野,但也能認清階級的差距。因資源不同而視野不同,煩惱也不同,絕對不會因此沒煩惱的。想起幾年跟總經理聊上幾句,我跟他分享同輩的在哪些美商陸商發展,他直接分享十多年前的別人挖角他的故事,翻翻手機內的聯絡人清單,某某某已經是該區域的 Top 50 富豪,當年跟著他的也 Top 100,但人生再來一次,是不是會放棄那個選擇?也著實是個有趣的議題。

回到自己的人生,越來越更重要的事需要珍惜,如何讓自己保有餘力去實踐,真的又是個 about time 的議題。每個人打從出生至今,每個階段拿到的手牌資源都不同,唯一不變的,仍是該好好的出牌。

2024年5月24日 星期五

Docker 開發筆記 - 使用 Dockerfile 透過 Ubuntu 18.04 建置 PHP7 + MySQL 5.7 + PHPMyAdmin 開發環境 @ macOS arm64

Ubuntu:18.04 + php7.2 + mysql5.7 + phpinfo();

此需求源自於舊網站的開發環境的建置,一般人不會碰到。

同事回報之前弄的 Docker 環境,因為從 Macbook Intel x86 架構,換到 Mac M2 ARM 環境後,出現問題。主因是網站使用很舊的 MySQL 5.6 環境,在 macos arm64 的 Docker 環境中,無法很便利的只用官方環境建置出來,預設只能直接升級到 MySQL 8 跑,接著還要面對 MySQL 5.6 跟 MySQL 5.7 以上的 SQL 語法變化。

在此的解法是放棄 MySQL 5.6 ,頂多弄出 MySQL 5.7 環境,並追蹤有哪些 SQL 語法需要修改,SQL語法方面,主要是面對 ONLY_FULL_GROUP_BY 模式的處理。此紀錄透過 Docker 弄個 MySQL 5.7 環境出來過度,且最終只需用 ubuntu:18.04 即可搞定大部分,以下是流水帳:

原先要用 Docker - Ubuntu 24.04 arm64 環境來使用,但追蹤後發現 Ubuntu 20.04, Ubuntu 22.04, Ubuntu 24.04 預設都只提供 MySQL 8 的環境,並且依照 MySQL APT Repository 的方式安裝 mysql-apt-config_0.8.30-1_all.deb 後,確認都沒 mysql-5.7 可選,而坊間其他教學文,主要是透過舊版 mysql-apt-config_0.8.12-1_all.deb 套件,在 Ubuntu 20.04 安裝了 mysql-5.7 ,但其過程是 mysql-apt-config 套件無法偵測出 Ubuntu 環境,透過人工選擇 Ubuntu 18.04 的環境裝到 mysql-5.7 而已,以下是個例子:

% docker run -it ubuntu:24.04 /bin/bash
root@2560222cf1ee:/# apt update && apt install -y wget lsb-release gnupg dialog
root@2560222cf1ee:/# wget https://dev.mysql.com/get/mysql-apt-config_0.8.30-1_all.deb
root@2560222cf1ee:/# dpkg -i ./mysql-apt-config_0.8.30-1_all.deb

MySQL APT Repo features MySQL Server along with a variety of MySQL components. You may select the appropriate product to choose the version that you
wish to receive.

Once you are satisfied with the configuration then select last option 'Ok' to save the configuration, then run 'apt-get update' to load package list.
Advanced users can always change the configurations later, depending on their own needs.

  1. MySQL Server & Cluster (Currently selected: mysql-8.4-lts)  3. MySQL Preview Packages (Currently selected: Disabled)
  2. MySQL Tools & Connectors (Currently selected: Enabled)      4. Ok

Which MySQL product do you wish to configure?
Which server version do you wish to receive?

來試試看裝舊版 mysql-apt-config_0.8.12-1_all.deb:

root@2560222cf1ee:/# dpkg -i ./mysql-apt-config_0.8.12-1_all.deb
root@bc33821f13de:/# apt update
W: GPG error: http://repo.mysql.com/apt/ubuntu cosmic InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY B7B3B788A8D3785C
E: The repository 'http://repo.mysql.com/apt/ubuntu cosmic InRelease' is not signed.
N: Updating from such a repository can't be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.
root@bc33821f13de:/# apt-key adv --keyserver keyserver.ubuntu.com --recv-keys B7B3B788A8D3785C
Warning: apt-key is deprecated. Manage keyring files in trusted.gpg.d instead (see apt-key(8)).
Executing: /tmp/apt-key-gpghome.eheHF2erjp/gpg.1.sh --keyserver keyserver.ubuntu.com --recv-keys B7B3B788A8D3785C
gpg: key B7B3B788A8D3785C: public key "MySQL Release Engineering <mysql-build@oss.oracle.com>" imported
gpg: Total number processed: 1
gpg:               imported: 1
root@bc33821f13de:/# apt update
W: http://repo.mysql.com/apt/ubuntu/dists/cosmic/InRelease: Key is stored in legacy trusted.gpg keyring (/etc/apt/trusted.gpg), see the DEPRECATION section in apt-key(8) for details.
N: Skipping acquire of configured file 'mysql-tools/binary-arm64/Packages' as repository 'http://repo.mysql.com/apt/ubuntu cosmic InRelease' doesn't support architecture 'arm64'
N: Skipping acquire of configured file 'mysql-5.7/binary-arm64/Packages' as repository 'http://repo.mysql.com/apt/ubuntu cosmic InRelease' doesn't support architecture 'arm64'
N: Skipping acquire of configured file 'mysql-apt-config/binary-arm64/Packages' as repository 'http://repo.mysql.com/apt/ubuntu cosmic InRelease' doesn't support architecture 'arm64'

最後就輕鬆改用 ubuntu:18.04 ,再把 phpmyadmin 擺好即可,收工。

連續動作:

# Use the official Ubuntu 18.04 as the base image
FROM ubuntu:18.04

# Set non-interactive mode for APT
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Taipei
ENV PHP_VERSION=7.2

# Update the package list and install necessary packages
RUN apt-get update && \
    apt-get install -y tzdata && \
    ln -fs /usr/share/zoneinfo/$TZ /etc/localtime && dpkg-reconfigure --frontend noninteractive tzdata && \
    apt-get install -y \
    htop \
    jq \
    vim \
    curl \
    wget \
    telnet \
    php7.2-cli \
    php7.2-curl \
    php7.2-fpm \
    php7.2-ldap \
    php7.2-mysql \
    php7.2-zip \
    phpunit \
    mysql-server-5.7 \
    ssl-cert \
    nginx \
    openssh-server && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

EXPOSE 22 80 443 8080 8443 3306

# Set up MySQL
RUN service mysql start && \
    mysql -e "CREATE USER 'developer'@'%' IDENTIFIED BY 'developer'; GRANT ALL PRIVILEGES ON *.* TO 'developer'@'%'; CREATE DATABASE developer; FLUSH PRIVILEGES;"

# Set up PHP-FPM Short Open Tag
RUN test -f /etc/php/$PHP_VERSION/fpm/php.ini && \ 
    sed -i 's/short_open_tag = Off/short_open_tag = On/' /etc/php/$PHP_VERSION/fpm/php.ini && \
    sed -i 's/max_execution_time = 30/max_execution_time = 300/' /etc/php/$PHP_VERSION/fpm/php.ini && \
    sed -i 's/upload_max_filesize = 2M/upload_max_filesize = 100M/' /etc/php/$PHP_VERSION/fpm/php.ini && \
    sed -i 's/post_max_size = 8M/post_max_size = 100M/' /etc/php/$PHP_VERSION/fpm/php.ini 

# Set up phpmyadmin
RUN cd /tmp && \
    wget https://www.phpmyadmin.net/downloads/phpMyAdmin-latest-all-languages.tar.gz && \
    tar xvzf phpMyAdmin-latest-all-languages.tar.gz && \
    mv phpMyAdmin-*-all-languages /usr/share/phpmyadmin && \
    mkdir -p /usr/share/phpmyadmin/tmp && \
    chown -R www-data:www-data /usr/share/phpmyadmin && \
    chmod 777 /usr/share/phpmyadmin/tmp

# Copy the entrypoint script
COPY nginx-php7.2-fpm-phpmyadmin.conf /etc/nginx/sites-enabled/phpmyadmin
COPY nginx-php7.2-fpm-website.conf /etc/nginx/sites-enabled/default 
COPY entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh

# Set the entrypoint
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

# Start services in the foreground
CMD ["bash"]

# Define a volume for mounting the local directory
VOLUME ["/var/www/my-website"]

運行:

% ls
Dockerfile nginx-php7.2-fpm-phpmyadmin.conf
README.md nginx-php7.2-fpm-website.conf
entrypoint.sh
% docker build -t test .
% mkdir /tmp/php
% echo "<?php phpinfo();" > /tmp/php/index.php
% cat /tmp/php/index.php 
<?php phpinfo();
% docker run -p 80:80 -p 443:443 -p 8080:8080 -p 8443:8443 -v /tmp/php:/var/www/my-website -t test
 * Starting MySQL database server mysqld                                                           No directory, logging in with HOME=/
                                                                                            [ OK ]
 * Starting OpenBSD Secure Shell server sshd                                                [ OK ] 
 * Starting nginx nginx                                                                     [ OK ] 
Fri May 24 21:42:43 CST 2024

如此,就可以用 http://localhost, https://localhost/ 看到 phpinfo() 的資料,而用 http://localhost:8080, https://localhost:8443/ 則會看到 phpmyadmin 網頁服務

要關閉時,記得找一下 container id:

% docker ps
CONTAINER ID   IMAGE     COMMAND                   CREATED              STATUS              PORTS                                                                                                        NAMES
a1631b90d924   test      "/usr/local/bin/entr…"   About a minute ago   Up About a minute   0.0.0.0:80->80/tcp, 22/tcp, 0.0.0.0:443->443/tcp, 0.0.0.0:8080->8080/tcp, 3306/tcp, 0.0.0.0:8443->8443/tcp   thirsty_booth
% docker stop a1631b90d924
a1631b90d924

 其他資訊:changyy/docker-study/ubuntu18.04-php7.2-mysql5.7-phpmyadmin

2024年4月11日 星期四

PHP 開發筆記 - 修正 osTicket v1.18.1 信件處理流程 (Mail Parser / Mail Fetch / MIMEDecode)


經歷一陣子的追蹤,發現 osTicket v1.18.1 的信件處理流程更新的不錯,但在一個關鍵的格式拼裝上出了錯誤,目前就發個 PR 讓團隊評估是否能修正了

這陣子研究 osTicket v1.18.1 時,信件處理分成兩個部分,一個是信件下載,另一個是信件解析。之前設法先把他信件架在過程中,埋了一個機制把信件存起來,接著去研究它的 MIMEDecode 流程,並猜測這邊要補強,實際上是他下載信件後,要組成 MIME 格式有問題,才使得信件解析有問題


        public function getRawEmail(int $i) {
-           return $this->getRawHeader($i) . $this->getRawContent($i);
+           return trim($this->getRawHeader($i)) . "\r\n\r\n" . $this->getRawContent($i);
        }

透過這次也研究了 osTicket v1.18 採用了 laminas-mail 來處理信件下載流程,整體上跑 api/cron.php 可以看到:

# php8.1 cron.php
@class.mail.php - Imap class: __construct
@Storage/Imap.php: __construct 
@Storage/Imap.php: selectFolder(INBOX) 
@Storage/Imap.php: countMessages()
@Storage/Imap.php: getRawHeader(1, , 0)
@Storage/Imap.php: getRawContent(1, )
@class.mail.php, Imap class: markAsSeen(1)
@Storage/Imap.php: setFlags(1, Array)
@class.mail.php, Imap class: expunge()
@Storage/Imap.php:close()

而在 class.mail.php 在 v1.16 是看不到的,可在 osTicket/osTicket/blob/1.16.x/include/class.mailfetch.php 觀看直接用 PHP imap_* 系列的函式做事,而在 v1.17 起多了 class.mail.php 檔案,在其檔案內可以看到 getRawEmail() 的實作分成 getRawHeader() + getRawContent() 

再往下追就會發現架構也變得漂亮。趁這次多了解了 osTicket ,而為了這次任務,解題思維:
  1. 設法用 Docker 建立 osTicket 測試環境
  2. 設法在 osTicket 處理信件流程中,取出一個 eml 檔案實驗
  3. 設法在 osTicket 架構下,建立匯入 eml 檔案作為重現 MIMEDecode 的測試
  4. 設法直接用 MIMEDecode 測試流程
  5. 追蹤 osTicket IMAP 信件下載流程
沒想到最後一步才發現問題的根源,若一開始先懷疑信件下載流程,應當可以秒解任務。後續再看送出的 PR 有沒緣被收了

2024年3月27日 星期三

PHP 開發筆記 - 使用 Docker 在 Ubuntu 22.04 和 PHP 8.1 架設 osTicket v1.18.1 環境,研究 Mail Parser 流程

接續上篇追蹤 osTicket 信件處理流程的筆記,這次用 Docker 包裝可運行和測試的獨立環境,主要採用 Ubuntu 22.04 與 PHP 8.1,並且規劃方式,建構出備份原始信件的架構,以及可反覆解析信件的流程。

首先是 Dockerfile ,主要設計從 GitHub.com 取出 v1.18.1.zip 並解壓縮 /var/www/osticket-v1.18.1,其餘是安裝相關套件和資料庫的帳號建立等等,最後再把 MySQL, PHP-FPM 和 nginx 運行起來,並預設把 nginx logs 寫到 stdout 觀看:

RUN wget https://github.com/osTicket/osTicket/releases/download/v1.18.1/osTicket-v1.18.1.zip -O /tmp/osticket.zip \
    && unzip /tmp/osticket.zip -d /var/www/ \
    && mv /var/www/upload /var/www/osticket-v1.18.1 \
    && cp /var/www/osticket-v1.18.1/include/ost-sampleconfig.php /var/www/osticket-v1.18.1/include/ost-config.php \
    && chown -R www-data:www-data /var/www/osticket-v1.18.1 \
    && rm /tmp/osticket.zip

...

RUN service mysql start && \
    mysql -e "CREATE USER 'developer'@'%' IDENTIFIED BY '12345678';" && \
    mysql -e "GRANT ALL PRIVILEGES ON *.* TO 'developer'@'%' WITH GRANT OPTION;" && \
    mysql -e "CREATE DATABASE osticket_dev CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" && \
    mysql -e "GRANT ALL PRIVILEGES ON osticket_dev.* TO 'developer'@'%';" && \
    mysql -e "FLUSH PRIVILEGES;" 

...

CMD service mysql start && service php8.1-fpm start && service nginx start && tail -f /var/log/nginx/access.log /var/log/nginx/error.log;

接下來更動 include/class.mailfetch.php 檔案,讓他下載信件時,可以存一份在 /tmp 方便後續使用:

@file_put_contents("/tmp/debug-mail.$i", $this->mbox->getRawEmail($i));

最後,弄一隻 api/cron-dev.php 檔案,可以指定 RawMail 的格式路徑,從指定位置讀進來解析信件,如此靠 api/cron-dev.php 就可以輕鬆不斷實驗 MIMEDecode 流程:

# php api/cron-dev.php 
[INFO] Input: /tmp/debug-mail
[INFO] Input File not found

# php api/cron-dev.php /tmp/debug-mail.1
[INFO] Input: /tmp/debug-mail.1
Ticket Object
(
    [ht] => Array
        (
            [ticket_id] => 2
            [ticket_pid] => 
...
            [lastupdate] => 2024-03-27 21:35:52
            [created] => 2024-03-27 21:35:52
            [updated] => 2024-03-27 21:35:52
            [topic] => 
            [staff] => 
            [user] => User Object
                (
                    [ht] => Array
                        (
                            [id] => 2
                            [org_id] => 0
                            [default_email_id] => 2
                            [status] => 0
                            [name] => UserName
                            [created] => 2024-03-27 21:35:52
                            [updated] => 2024-03-27 21:35:52
                            [default_email] => UserEmailModel Object
                                (
                                    [ht] => Array
                                        (
                                            [id] => 2
                                            [user_id] => 2
                                            [flags] => 0
                                            [address] => user@example.com
...

PHP 開發筆記 - 追蹤 osTicket v1.18.1 在 PHP8 環境處理信件的流程

公司用 osTicket 系統幾年了,最近把環境升級到 PHP8 和最新版 osTicket v1.18.1 後,同事回報踩到信件內容的 cid 圖片沒正常處理,就先追蹤一下信件處理流程,主要先追到 MIMEDecode 即可。

從官網文件 POP3/IMAP Settings Guide 得知,信件處理的觸發有從 crontab 和 api 的管道,目前就從 crontab 來追:

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.6 LTS
Release:        20.04
Codename:       focal

$ php -v
PHP 8.2.17 (cli) (built: Mar 16 2024 08:41:44) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.2.17, Copyright (c) Zend Technologies
    with Zend OPcache v8.2.17, Copyright (c), by Zend Technologies

$ php api/cron.php

接著開始看 api/cron.php 程式碼,大概追到 include/class.cron.php 時,就可以看到 osTicket\Mail\Fetcher::run(); 和 include/class.email.php 檔案了,從 include/class.mailfetch.php 可以看到:

```php
 78     function processMessage(int $i, array $defaults = []) {
 79         try {
 80             // Please note that the returned object could be anything from
 81             // ticket, task to thread entry or a boolean.
 82             // Don't let TicketApi call fool you!
 83             return $this->getTicketsApi()->processEmail(
 84                     $this->mbox->getRawEmail($i), $defaults);
 85         } catch (\TicketDenied $ex) {
 86             // If a ticket is denied we're going to report it as processed
 87             // so it can be moved out of the Fetch Folder or Deleted based
 88             // on the MailBox settings.
 89             return true;
 90         } catch (\EmailParseError $ex) {
 91             // Upstream we try to create a ticket on email parse error - if
 92             // it fails then that means we have invalid headers.
 93             // For Debug purposes log the parse error + headers as a warning
 94             $this->logWarning(sprintf("%s\n\n%s",
 95                         $ex->getMessage(),
 96                         $this->mbox->getRawHeader($i)));
 97         }
 98         return false;
 99     }
```

接著在 processEmail 前埋一個 @file_put_contents('/tmp/debug-mail', print_r($this->mbox->getRawEmail($i), true)); ,如此處理信件時,原始格式就會被記錄在 /tmp/debug-mail 裡,下一刻就能再重現信件格式的處理流程

小改後面:

$ sudo cp api/cron.php  api/cron-debug.php
$ tail -n 5 api/cron-debug.php 
//LocalCronApiController::call();
//
$obj = new \TicketApiController('cli');
print_r($obj->processEmail(file_get_contents('/tmp/debug-mail'), []));
?>

如此運行時就可以看資訊:

$ php api/cron-debug.php 
Ticket Object
(
    [ht] => Array
        (
            [ticket_id] => #
            [ticket_pid] => 
            [number] => #####
...

接著要來研究信件處理流程,就來到了 include/api.tickets.php 檔案:

```php
    function processEmail($data=false, array $defaults = []) {

        try {
            if (!$data)
                $data = $this->getEmailRequest();
            elseif (!is_array($data))
                $data = $this->parseEmail($data);
            print_r($data);
        } catch (Exception $ex)  {
            throw new EmailParseError($ex->getMessage());
        }
...
```

繼續追 include/class.api.php 檔案:

```php
238     function parseRequest($stream, $format, $validate=true) {
239         $parser = null;
240         switch(strtolower($format)) {
241             case 'xml':
242                 if (!function_exists('xml_parser_create'))
243                     return $this->exerr(501, __('XML extension not supported'));
244                 $parser = new ApiXmlDataParser();
245                 break;
246             case 'json':
247                 $parser = new ApiJsonDataParser();
248                 break;
249             case 'email': 
250                 $parser = new ApiEmailDataParser();
251                 break;
252             default:
253                 return $this->exerr(415, __('Unsupported data format'));
254         }
255         
256         if (!($data = $parser->parse($stream)) || !is_array($data)) {
257             $this->exerr(400, $parser->lastError());
258         }
259         
260         //Validate structure of the request.
261         if ($validate && $data)
262             $this->validate($data, $format, false);
263         
264         return $data;
265     }
266     
267     function parseEmail($content) {
268         return $this->parseRequest($content, 'email', false);
269     }
```

繼續追 include/class.mailparse.php 檔案,主要是追蹤 EmailDataParser -> Mail_Parse -> Mail_mimeDecode ,最後就是 include/pear/Mail/mimeDecode.php 的處理 MIMEDecode 的實作了。

剩下的工作就是研究它解析原始信件的流程,看看是 MIMEDecode 是否少了遞迴解,還是有什麼限制了:

2024年3月15日 星期五

企業導入 AI 輔助方案:廣義用法 ChatGPT / 設計行銷 Stable Diffusion 與 AI 主播 / 程式開發 Github Copilot

各大 AI 應用大放光彩一年多後,上個月也接到一個任務要去評估各大 AI 服務是否能大力提升公司同仁工作效率,然而最近會被找去跟人分享交流,想著想著,還是把一些公開的資料整理一下,方便以後交流丟 link 偷懶

主要將工作輔助切成四大塊:
  1. 廣義 AI 助理
  2. 設計行銷生成式 AI 需求
  3. 研發工程 Code Suggestion / Code Review
  4. 使用 OpenAI API 自行開發服務,如 客服信半自動回應系統
廣義 AI 應用,那就是 AI 助理,可直接用 ChatGPT 即可,無論是個人免費還是付費版,單純 GPT-3.5 免費版也能完成七成工作,而掏錢加碼到付費版,則可以擁有更多應用,包括可安裝 plugins ,能直接下載訊息中的 link 資訊加以分析,我想,最主要的是可以上傳檔案叫 ChatGPT 分析吧!舉凡 PDF 還是 CSV 都行,還可以叫他去下載指定 link 內容回來分析。像近期 微軟 Copilot 推得很兇,免費版也能體驗到 GPT-4 turbo 了


2024-03-15 報價 - openai.com/chatgpt/pricing

ChatGPT Team 可以月為單位測試,習慣後改成年繳省錢便宜

如果願意的話,可以試試 ChatGPT Team 版本,原先發信給 ChatGPT Sales 詢問 Enterprise 該怎樣申請,交談幾次需求後被回說用 ChatGPT Team 已經足夠!

在 ChatGPT Team 付費服務下,開帳號流程進入邀請式架構,直接輸入同事信箱即可。讓同事在創建 ChatGPT 帳號是很順暢的(不用簡訊認證),這對 ChatGPT 不支援的國家地區是有很大的幫助的。當然,有興趣的話,還是可以多試試排行榜上的幾間,如 Claude AI (Anthropic 創辦人都是 OpenAI 出身)、Mistral AI 和 Google AI (Gemini) 和 Microsoft AI (基本上用 OpenAI 也行) 等。


2024-03-15 ChatGPT - Plugin store



關於設計行銷生成式 AI 需求,首先就是建立 Stable Diffusion (Stability AI) 準沒錯,把以文字生圖的項目先準備好,接著則是 MidJourney 也要去參拜一下。然而,在行銷資訊上,如何產出多影音應當是當務之急,這邊可以參考數位時代 AI 主播,該篇文章已經提了不少重點

  1. 先找到 model 拍個正面照,接著再轉成2D形象
  2. 使用 d-id.com 產出人物嘴型會跟文字變化的效果(也能上傳錄好的聲音檔案)
  3. 接著再把製作好的影片下載會來後製,像是去除背景,整合到新的影片內容
以上是簡單的原理,後續就可以靠這招套版型,做成 AI 虛擬主播,未來可以制式化產生影片,光這項就能省上不少時間。此外,也可以留意 17Live 的 政治類AI主播「答可特」,整體上數位時代AI主播已經很清晰建置流程,要說缺點的話,就是數位時代 AI主播 的模型源自於一位真實的記者,包括記者的語言偏好年紀習性都全盤複製,在這個隱私時代裡,可能適合先退一步思考該怎樣保護隱私?或是個人肖像權版權等。

對於在雲台上架設 Stable Diffusion 的確燒錢,可以一同評估買顯卡自架,估計預算都要抓個10萬台幣以內會比較彈性。其實雲台用法應當用多少算多少,除非設計團隊可以配合到要使用時開機,不然架設雲台的開銷,要求 GPU 16G vram 時,錢真的用噴的,例如一小時 3美金。


單純用 "16G vram" 去問問 google ,可以得到目前市價販售的顯卡價格,且很有可能還用 "24G vram" 了 XD 價格飛奔上去。

對設計行銷的素材使用還不太清楚成效,很難決議是否該自建 server ,特別是 AI 產出的圖片影音還要面對商用版權問題,這時,直接用線上服務掏錢用商用版!未來有版權問題時,公司自身也可以比較輕鬆,可以直接說是 XXX 公司提供的商用版,請對方去告 (誤)

2024-03-15 Github Copilot 價錢 - github.com/features/copilot

關於研發工程的利器 Code Suggestion,研究了 Tabnine, Tabnine Pro, Gitlab Duo, Github Enterprise Cloud, Github Enterprise Server (Private Cloud) 後,最終選擇 Github Copilot 個人版,主因:
  • 若決議公司程式碼不上到 github.com,使得 Github Team 方案也變成多餘(且安全管控相對差),因此也不用考慮 Github Copilot Business 方案了
  • 實測 Tabnine Pro 用起來剛好略遜於 Github Copilot 個人版,這當然可能只是純個案,畢竟每一次詢問成果都是不一樣的,個人覺得用 OpenAI 牌的 Github Copilot 後面又有 open source 訓練而來以及微軟背書,假設稱不上市場第一名,理當也不會是最後一名的選擇,不吃虧的。

2024-03-15 Github Copilot Use GPT-3.5 Turbo Model

此外,在 Github Copilot 網頁上可以看到他是建構在 GPT-3.5 turbo ,推論不用到 GPT-4 成效也已經很不錯,以及啊,眾多 AI 服務創辦人,很多都源自於 OpenAI 這個團隊的,在這種情境下,採用 OpenAI 為主的服務,其實不太會吃虧的,算是不錯的開局。


vscode + github copilot chat 範例


vscode + copilot: 07~29 是 copilot 補的

最後,簡單提一下使用 OpenAI API 自行開發 AI 輔助應用,這段屬於高度客製化的項目,很吃研發人力且成效的優化也是要持續的,就像 Machine learning 一樣,當 AI 回饋的答案不好時,需要把結果存起來再次餵回去給 OpenAI 練出新的 Model ,往後就用指定 Model 來問問題。



練法其實還滿簡單的,如以往推薦系統建置流程差不多,反而前期的資料清理、設計回饋系統架構、如何整合在公司內部服務內透過 UX 節省時間,這些才是整個應用最吃重的工程整合。

我想,回歸到最初,其實只要啟用了廣義 AI 輔助系統即可,起手式鼓勵大家用免費版 ChatGPT 或微軟 Copilot 服務,基本上會有很顯著的改變了,當然,這類 AI 輔助系統不是萬能的,就像二十年前 Search Engine 橫空出世一樣,還是使用者問對問題才是核心,套個 Youtuber 超認真少年花 400萬 老大樓重整 (印象中標題有350變成400了? ) 的片段心得:

交給年輕的設計師還是有經驗的設計師?給予回饋一樣可以有不錯的成果,差別就差在效率(效率影響用料量、製程時間)

可能有經驗的高手透過 AI 問個 3句話就搞定,而經驗淺的新手可能 30 句都還沒問完,站在 Machine learning 的角度,新手還是可以透過 AI 回饋+自身經驗提升,最終得到想要的結果(而花的時間比較多)

這時,你覺得 AI 輔助效益差,究竟是 AI 真的很差?還是使用者對於要解的任務,其的經驗還不夠充足呢?

其他資訊(AI演進太快要加日期提醒自己新鮮程度):


2024年3月13日 星期三

Node.js 開發筆記 - 將 C structure 轉成 Javascript code 流程

在 C code 處理記憶體時,有些偷閒招數是強制型態轉換,這時要移植到 node.js 運行的 js code 有點繞,整個開發過程就不斷在 c code 輸出 structure 的資訊,跟 js code 那端交叉比對,理解後也沒什麼難,就是要計算記憶體位置

C code: 

// 宣告
typedef struct 
{
char flag[16];
unsigned int number1;
unsigned int number2;
unsigned int number3;
unsigned int number4;
} MyHeader;


// 用法:
{
    MyHeader *header;
    header = (MyHeader *)buffer;
}

接著在 js code 要對應處理稍微累了點,但也還行:

const buffer = ....;
const headerOffset = 0;
const headerData = buffer.slice(headerOffset, headerOffset + 32);

headerFlag = headerData.slice(0, 16).toString('ascii');
headerNumber1 = headerData.readUInt32LE(16);
headerNumber2 = headerData.readUInt32LE(20);
headerNumber3 = headerData.readUInt32LE(24);
headerNumber4 = headerData.readUInt32LE(28);

如此可以做簡單的 C Code 轉 node.js Javascript code。

建議移植時,要分別在 C & JS 輸出數值來比對,小步小步進行,避免程式碼很大包,最後出包了難 debug (大概稱得上 test-driven development 吧?)

2024年2月21日 星期三

Python 開發筆記 - 使用 Selenium / undetected_chromedriver / ChatGoogleGenerativeAI / gemini-pro-vision 完成自動登入網站的流程(含 retry 驗證碼架構)

有些工作任務需要去下載表單做一些自動化應用,因此有了要自動登入的需求,當然也會碰到認證碼辨識問題。此篇是延續 Python 開發筆記 - 使用 Google AI, Generative Language API, gemini-pro-vision 辨識圖片認證碼

整個處理原理:
  1. 使用 Selenium 去偵測網頁的狀態,取得登入要用的帳號, 密碼, 認證碼圖片, 認證碼數值, 登入按鈕
  2. 使用 ChatGoogleGenerativeAI/gemini-pro-vision 分析圖片內容,設法分析出認證碼數值
  3. 觸發 登入按鈕 送出表單
  4. 檢視登入流程,檢查是否有登入失敗的訊息,或是反過來思考怎樣判斷登入成功,若登入失敗重回 (1) 去取得新的認證碼圖片 
引入的函式庫:

import getpass
import os
import sys
import time
import json
import base64
import undetected_chromedriver as uc
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
 
from langchain_core.messages import HumanMessage
from langchain_google_genai import ChatGoogleGenerativeAI

先採用 undetected_chromedriver 來包裝一下取得 browser driver:

def getBrowserDriver():
    option = uc.ChromeOptions()
    option.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36')
    #option.add_argument('--window-size=%d,%d' % self.res)
    #option.add_argument('--headless')
    driver = uc.Chrome(options=option)
    return driver

辨別圖片文字靠 ChatGoogleGenerativeAI model="gemini-pro-vision":

def codeDetection(imageBase64URL: str):
    llm = ChatGoogleGenerativeAI(model="gemini-pro-vision")
    message = HumanMessage(
        content=[
            {
                "type": "text",
                "text": "Please identify the English or numbers appearing in the image. The output format is 'The answer is: XXXX'"
,
            },
            {"type": "image_url", "image_url": imageBase64URL},
        ]
    )
    result = llm.invoke([message])
    return result

處理流程:

if __name__ == '__main__':
    if "GOOGLE_API_KEY" not in os.environ:
        os.environ["GOOGLE_API_KEY"] = getpass.getpass("Provide your Google API Key: ")
    if not os.environ["GOOGLE_API_KEY"]:
        print('ERROR, no GOOGLE_API_KEY info')
        sys.exit(1)

    output = {
        'status': False,
        'time': [],
    }

    browser = getBrowserDriver()
    start_time = time.time()

    browser.get(LOGIN_URL)

    # 15s timeout
    wait = WebDriverWait(browser, 15)

    # 等待關鍵的表單資料
    conditions = [
        EC.presence_of_element_located((By.ID, "input_user")),
        EC.presence_of_element_located((By.ID, "input_password")),
        EC.presence_of_element_located((By.ID, "input_velidation_code")),
        EC.presence_of_element_located((By.ID, "velidation_code_image")),
        EC.presence_of_element_located((By.ID, "login_button")),
    ]
    if wait.until(lambda driver: all(condition(driver) for condition in conditions)):
        output['status'] = True
    output['time'].append( time.time() - start_time )

    # 取得圖片元素
    imageElement = wait.until(EC.presence_of_element_located((By.ID, "velidation_code_image")))

    # 取得圖片的 HTML code
    imageHTMLCode = imageElement.get_attribute("outerHTML")
    print("Image HTML Code:", imageHTMLCode)

    # 取得圖片的 URL
    imageSrcURL = imageElement.get_attribute("src")
    print("Image URL:", imageSrcURL)

    # 透過 JavaScript 監聽 src 屬性變化
    script = f"""
        var target = document.getElementById('velidation_code_image');
        var observer = new MutationObserver(function(mutations) {{
            mutations.forEach(function(mutation) {{
                if (mutation.attributeName === 'src') {{
                    console.log('src attribute changed:', target.getAttribute('src'));
                }}
            }});
        }});
    
        var config = {{ attributes: true }};
        observer.observe(target, config);
    """
    
    # 執行 JavaScript 代碼
    browser.execute_script(script)

    # 等待一段時間,確保有足夠的時間監聽 src 屬性的變化
    time.sleep(5)

    # 取得更新後的圖片的 URL
    updatedImageSrc = imageElement.get_attribute("src")
    print("Updated Image URL:", updatedImageSrc)

    if not updatedImageSrc:
        print('ERROR, velidation_code_image not found')
        sys.exit(1)

    result = codeDetection(updatedImageSrc)
    print(result.content)
    loginCode = ''
    for c in result.content.split(':', 2)[1]:
        if c == '' or c == ' ':
            continue
        loginCode += c

    print(f"LoginCode: {loginCode}")
    element = wait.until(EC.presence_of_element_located((By.ID, "input_user")))
    element.send_keys('YourAccountName')
    element = wait.until(EC.presence_of_element_located((By.ID, "input_password")))
    element.send_keys('YourPassword')
    element = wait.until(EC.presence_of_element_located((By.ID, "input_velidation_code")))
    element.send_keys(loginCode)
    element = wait.until(EC.presence_of_element_located((By.ID, "login_button")))

    start_time = time.time()
    element.click()

    loginDone = False
    loginRetry = 0
    while loginDone == False and loginRetry <= 3:
        try:
            wait = WebDriverWait(browser, 5)
            element = wait.until(EC.presence_of_element_located((By.ID, "WebsiteErrorMessage")))
            div_element = element.find_element(By.TAG_NAME, "div")
            span_element = div_element.find_element(By.TAG_NAME, "span")
            inner_html = span_element.get_attribute('innerHTML')
            # 驗證碼輸入錯誤
            print(f"retry: {loginRetry}, inner HTML: {inner_html}")

            # 關閉錯誤訊息
            element = wait.until(EC.presence_of_element_located((By.ID, "WebsiteErrorMessageWindow")))
            div_element = element.find_element(By.TAG_NAME, "div")
            button_element = element.find_element(By.TAG_NAME, "button")
            button_element.click()

            loginRetry += 1

            updatedImageSrc = imageElement.get_attribute("src")
            print("Updated Image URL:", updatedImageSrc)
            result = codeDetection(updatedImageSrc)
            print(result.content)
            loginCode = ''
            for c in result.content.split(':', 2)[1]:
                if c == '' or c == ' ':
                    continue
                loginCode += c

            print(f"LoginCode: {loginCode}")
            element = wait.until(EC.presence_of_element_located((By.ID, "input_user")))
            element.clear()
            element.send_keys('YourAccountName')
            time.sleep(1)
            element = wait.until(EC.presence_of_element_located((By.ID, "input_password"))) 
            element.clear()
            element.send_keys('YourPassword')
            time.sleep(1)
            element = wait.until(EC.presence_of_element_located((By.ID, "input_velidation_code")))
            element.clear()
            element.send_keys(loginCode)
            time.sleep(1)
            element = wait.until(EC.presence_of_element_located((By.ID, "login_button")))
            element.click()
        except:
            loginDone = True

    output['time'].append( time.time() - start_time )

    if loginDone:
        print("Login Successful")
    else:
        print(f"Login Failed with retry times: {loginRetry}")
 
    print(json.dumps(output, indent=4))
    while True:
        time.sleep(1)