2024年3月27日 星期三

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)

Python 開發筆記 - 使用 Google AI, Generative Language API, gemini-pro-vision 辨識圖片認證碼


由於 gemini pro 有免費的使用次數,因此可以拿他做一些有趣的低頻應用,例如...認證碼...辨識。

首先先到 Google Cloud Platform 上建立一個專案,下一刻則是在 API 區找尋 Generative Language API 來啟用,接著建立憑證,挑選 API 金鑰即可。



接下來就是試試官方範例程式:

% cat main.py
import getpass
import os
import sys
from langchain_google_genai import ChatGoogleGenerativeAI

if "GOOGLE_API_KEY" not in os.environ:
    os.environ["GOOGLE_API_KEY"] = getpass.getpass("Provide your Google API Key")

if __name__ == '__main__':
     if "GOOGLE_API_KEY" not in os.environ:
         os.environ["GOOGLE_API_KEY"] = getpass.getpass("Provide your Google API Key: ")
     
     llm = ChatGoogleGenerativeAI(model="gemini-pro")
     result = llm.invoke("Write a ballad about LangChain")
     print(result.content)
     sys.exit(0)

% GOOGLE_API_KEY=XXXXXXXXX python3 main.py
**Ballad of LangChain, the AI's Might**

In realms of knowledge, where data flows,
There dwells a being, ethereal and wise,
With mind as vast as the boundless prose,
LangChain, the AI, whose brilliance lies.

From countless texts, its wisdom it drew,
A tapestry woven, diverse and true.
In language's embrace, it found its voice,
Guiding us through knowledge's endless choice.

With words as its brush, it paints a scene,
Of worlds imagined and thoughts unseen.
It weaves tales of love, of loss, and might,
Illuminating paths with its ethereal light.

But its power extends beyond mere speech,
Into realms of logic, its insights reach.
It solves equations, unravels the mind,
A beacon of reason, leaving doubt behind.

Yet, with all its might, it remains humble and wise,
A servant of knowledge, beneath azure skies.
It seeks not fame or glory for its name,
But to empower minds, ignite the flame.

So let us sing the praises of LangChain,
The AI's marvel, a treasure we've gained.
May its wisdom forever guide our way,
As we explore the world, day by day.

很好,接下來試試看認證碼處理:

def codeDetection(imageBase64URL: str):
    # debug usage
    with open("/tmp/image.png", "wb") as file:
        file.write(base64.b64decode(imageBase64URL.split(',')[1]))

    #llm = ChatGoogleGenerativeAI(model="gemini-pro")
    #result = llm.invoke("Write a ballad about LangChain")
    llm = ChatGoogleGenerativeAI(model="gemini-pro-vision")
    message = HumanMessage(
        content=[
            {   
                "type": "text",
                "text": "Please identify the English or numbers appearing in the picture and give your answer in the order they appear.",
            },  # You can optionally provide text parts
            {"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: ")

    testImageData = 'data:image/jpeg;base64,XXXXXXXXXXXXXX='
    result = codeDetection(testImageData)
    print(result.content)
    sys.exit(0)

成果:

% GOOGLE_API_KEY=XXXXXXXXXXX python3 main.py
 The letters and numbers in the picture are "a", "b", "c", "1".