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 $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 吧?)