Conversation
- NVDA+Windows+J 自動點擊更多選項 → 儲存聊天 → 解析聊天記錄 → 開啟訊息閱讀器 - 重用現有 _activateMoreOptionsMenu + ChatMoreOptions 虛擬視窗,確保點擊位置正確 - 新增 _chatParser.py:解析 LINE 聊天匯出格式(時間、姓名、內容,支援收回訊息) - 新增 _messageReader.py:wx 對話框,上/下方向鍵逐則閱讀,格式為「姓名 內容 時間」 - 自動操作存檔對話框:以 EnumChildWindows 遞迴找到 Edit 控制項並設定暫存路徑 - 以 BM_CLICK 點擊存檔按鈕,更可靠地觸發儲存動作 - 在 NVDA 工具 > LINE Desktop 選單新增「訊息閱讀器」項目 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| _DATE_RE = re.compile(r'^\d{4}\.\d{2}\.\d{2} .+$') | ||
| _MSG_RE = re.compile(r'^(\d{2}:\d{2}) (\S+?) (.+)$') | ||
| _RECALL_RE = re.compile(r'^(\d{2}:\d{2}) (\S+?)已收回訊息$') |
There was a problem hiding this comment.
正規表示式假設欄位以空白分隔,但 LINE 匯出可能使用 Tab
LINE 桌面版的聊天記錄匯出格式通常以 Tab(\t) 分隔時間、名稱與訊息內容,例如:
14:30 王小明 你好
14:31 李美華 已收回訊息
目前 _MSG_RE 與 _RECALL_RE 皆以空白(' ')作為欄位分隔符號。若匯出格式確為 Tab 分隔,這兩個 regex 將無法正確解析任何訊息,導致 parseChatFile 永遠回傳空列表,使用者只會看到「聊天紀錄中沒有訊息」。
建議確認實際匯出格式後,視需要將空白改為 \t:
_MSG_RE = re.compile(r'^(\d{2}:\d{2})\t(\S.+?)\t(.+)$')
_RECALL_RE = re.compile(r'^(\d{2}:\d{2})\t(\S.+?)\t已收回訊息$')另外,若使用空白分隔,含有空白的使用者名稱(如 John Smith)只會將第一個詞解析為名稱,其餘詞彙會被歸入訊息內容,導致顯示錯誤。
Prompt To Fix With AI
This is a comment left during a code review.
Path: addon/appModules/_chatParser.py
Line: 3-5
Comment:
**正規表示式假設欄位以空白分隔,但 LINE 匯出可能使用 Tab**
LINE 桌面版的聊天記錄匯出格式通常以 **Tab(`\t`)** 分隔時間、名稱與訊息內容,例如:
```
14:30 王小明 你好
14:31 李美華 已收回訊息
```
目前 `_MSG_RE` 與 `_RECALL_RE` 皆以空白(`' '`)作為欄位分隔符號。若匯出格式確為 Tab 分隔,這兩個 regex 將無法正確解析任何訊息,導致 `parseChatFile` 永遠回傳空列表,使用者只會看到「聊天紀錄中沒有訊息」。
建議確認實際匯出格式後,視需要將空白改為 `\t`:
```python
_MSG_RE = re.compile(r'^(\d{2}:\d{2})\t(\S.+?)\t(.+)$')
_RECALL_RE = re.compile(r'^(\d{2}:\d{2})\t(\S.+?)\t已收回訊息$')
```
另外,若使用空白分隔,含有空白的使用者名稱(如 `John Smith`)只會將第一個詞解析為名稱,其餘詞彙會被歸入訊息內容,導致顯示錯誤。
How can I resolve this? If you propose a fix, please make it concise.|
@greptileai 目前匯出訊息實際格視為 11:29 可揚🇹🇼 有 |
|
確認格式是空白分隔(不是 Tab),現有的 不過多行訊息問題仍需處理:如果使用者以 Shift+Enter 換行,後續行會被丟棄。建議在 def parseChatFile(filePath):
messages = []
with open(filePath, 'r', encoding='utf-8') as f:
for line in f:
line = line.rstrip('\r\n')
if not line:
continue
if _DATE_RE.match(line):
continue
m = _RECALL_RE.match(line)
if m:
messages.append({
'time': m.group(1),
'name': m.group(2),
'content': '已收回訊息',
})
continue
m = _MSG_RE.match(line)
if m:
messages.append({
'time': m.group(1),
'name': m.group(2),
'content': m.group(3),
})
continue
# Continuation line: append to last message
if messages:
messages[-1]['content'] += '\n' + line
return messagesTip: You can customize Greptile's behavior for this repo with |
接續行(不符合日期/訊息格式的行)累積至上一則訊息的 content。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Tip: Greploops — Automatically fix all review issues by running Use the Greptile plugin for Claude Code to query reviews, search comments, and manage custom context directly from your terminal. |
將聊天匯出暫存檔從插件目錄內 (addons/lineDesktop/temp/) 改為 系統暫存目錄 (%TEMP%/lineDesktop_chat_export.txt), 防止 NVDA 更新插件時因資料夾被鎖定而發生 PermissionError (WinError 5)。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@greptileai 這樣可以嗎? |
|
還不行,兩個關鍵問題都未修正: 1. 多重對話框問題 ( 2. 誤觸其他對話框 ( 建議修正: 修正 1 - 防止多重對話框: # _messageReader.py 開頭新增
_readerDialog = None
def openMessageReader(messages, title="訊息閱讀器"):
def _show():
global _readerDialog
if _readerDialog is not None:
try:
_readerDialog.Raise()
_readerDialog.SetFocus()
return
except Exception:
_readerDialog = None
_readerDialog = MessageReaderDialog(messages, title=title)
_readerDialog.Show()
_readerDialog.Raise()
wx.CallAfter(_show)並在 def _onClose(self, evt):
global _readerDialog
_readerDialog = None
self.Destroy()修正 2 - 驗證對話框標題: if dialogHwnd:
# 驗證是否為覆寫確認框
textBuf = ctypes.create_unicode_buffer(512)
ctypes.windll.user32.GetWindowTextW(dialogHwnd, textBuf, 512)
dialogText = textBuf.value
OVERWRITE_KEYWORDS = ("覆寫", "取代", "已存在", "overwrite", "replace", "already exists")
if not any(kw.lower() in dialogText.lower() for kw in OVERWRITE_KEYWORDS):
# 不是覆寫確認框,直接開啟檔案
core.callLater(300, self._messageReaderOpenFile)
return
# 是覆寫確認框,送 Enter
# ... 現有的 SendMessageW 程式碼修正後再測試即可合併。 |
新增 cleanupPath 參數到訊息閱讀器,當對話框關閉時 自動刪除 %TEMP% 中的聊天匯出暫存檔,避免累積臨時檔案。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
在設定存檔路徑前先刪除已存在的 lineDesktop_chat_export.txt, 讓 LINE 的另存新檔對話框不會出現「是否取代」確認視窗。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@greptileai 這樣可以嗎? |
|
對,這三個建議都可以解決對應的問題:
這些都是安全且直接的修正。 |
- script_openMessageReader 加入 _messageReaderPending 檢查,防止重複觸發時覆寫共享狀態 - 訊息起始位置改為第一則(pos=0),符合閱讀直覺 - 移除 _onKeyDown 中重複的 Escape 處理,統一交由 _onCharHook 負責 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@greptileai 這樣可以嗎? |
|
差不多了!兩個問題已完全解決,一個還需小改: ✓ Issue 2 (訊息閱讀器起始位置): 已修正為從第一則訊息開始 ( ✓ Issue 3 (Escape 鍵重複處理): 已從
if getattr(self, '_messageReaderPending', False):
ui.message(_("訊息閱讀器正在執行中,請稍候"))
return加上這行提示,使用者體驗會更好。 |
改為朗讀「訊息閱讀器正在執行中,請稍候」而非靜默返回。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
加入模組層級的 _readerDlg sentinel,若視窗已開啟則直接 Raise 聚焦而非再建立新的 MessageReaderDialog 實例。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@greptileai 這樣可以嗎? |
|
單例防護(Issue 2)已正確實作 ✓ 你加入的程式碼: _readerDlg = None # module-level singleton sentinel
def openMessageReader(...):
def _show():
global _readerDlg
if _readerDlg and _readerDlg.IsShown():
_readerDlg.Raise()
return
_readerDlg = MessageReaderDialog(...)這個實作是正確的!現在重複按 但是 Issue 1(翻譯字串)仍未修正: 以下字串仍然是硬編碼中文,沒有用
需要全部改成: _("已經是第一則訊息")
_("已經是最後一則訊息")
_("沒有訊息")
title=_("訊息閱讀器")
_("關閉(&C)")修正這些後就完全符合建議了。 |
所有 UI 字串(視窗標題、按鈕、提示訊息)改用 _() 包裹, 支援 NVDA 多語系翻譯機制。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@greptileai 這樣可以嗎? |
|
兩個問題都還沒修正 (both issues are still not fixed): 問題 1 (_messageReader.py:108-118): 問題 2 (line.py:5885-5892):
需要修改:
_readerDlg = None # Allow future invocations to create a new instance
|
- _onClose 關閉前將 _readerDlg 重設為 None,避免持有已銷毀物件的 wrapper - 移除 _handleChatMoreOptionsAction 中過早清除 _messageReaderPending 的程式碼 - 在 _messageReaderOpenFile 的所有出口(return 及 except)加上 _messageReaderPending = False Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
移除錯誤的 `from languageHandler import gettext as _`, _() 已由 line.py 呼叫 addonHandler.initTranslation() 注入為 builtin, 子模組直接使用即可。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@greptileai 這樣可以嗎? |
|
抱歉,三個問題都還沒有修正: 1. # 現在(錯誤):
_RECALL_RE = re.compile(r'^(\d{2}:\d{2}) (\S+?)已收回訊息$')
# 應該是:
_RECALL_RE = re.compile(r'^(\d{2}:\d{2}) (\S+?) 已收回訊息$')
# ↑ 這裡需要空白2. 第一處(約 5966 行): else:
self._messageReaderPending = False # ← 加這行
ui.message(_("未偵測到儲存對話框"))第二處(約 5989 行): if not editHwnd:
self._messageReaderPending = False # ← 加這行
ui.message(_("無法操作儲存對話框"))3. # 約 6012 行,改成:
WNDENUMPROC = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM) |
self._messageReaderPending = False 被意外寫成空格縮排, 導致 Python 解析失敗,改回正確的 tab 縮排。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@greptileai 這樣可以嗎? |
|
你嘗試修復了 Issue 3,但縮排有問題 — 第 5993 行用了 4 個空格,而後面兩行用了 tab。應該統一使用 tab: 另外,Issue 1 和 Issue 2 仍未修復:
|
|
|
||
| _DATE_RE = re.compile(r'^\d{4}\.\d{2}\.\d{2} .+$') | ||
| _MSG_RE = re.compile(r'^(\d{2}:\d{2}) (\S+?) (.+)$') | ||
| _RECALL_RE = re.compile(r'^(\d{2}:\d{2}) (\S+?)已收回訊息$') |
There was a problem hiding this comment.
_RECALL_RE 在訊息名稱與「已收回訊息」之間缺少空白
_MSG_RE 使用空白分隔三個欄位(時間、名稱、內容),格式為 HH:MM NAME content。但 _RECALL_RE 在名稱與「已收回訊息」之間沒有空白,格式為 HH:MM NAME已收回訊息。
若 LINE 匯出格式為 HH:MM NAME 已收回訊息(名稱後有空白),_RECALL_RE 無法匹配,此行會進入 _MSG_RE 匹配,將「已收回訊息」視為普通訊息內容,失去語意標記。
建議修正為:
| _RECALL_RE = re.compile(r'^(\d{2}:\d{2}) (\S+?)已收回訊息$') | |
| _RECALL_RE = re.compile(r'^(\d{2}:\d{2}) (\S+?) 已收回訊息$') |
Prompt To Fix With AI
This is a comment left during a code review.
Path: addon/appModules/_chatParser.py
Line: 5
Comment:
**`_RECALL_RE` 在訊息名稱與「已收回訊息」之間缺少空白**
`_MSG_RE` 使用空白分隔三個欄位(時間、名稱、內容),格式為 `HH:MM NAME content`。但 `_RECALL_RE` 在名稱與「已收回訊息」之間沒有空白,格式為 `HH:MM NAME已收回訊息`。
若 LINE 匯出格式為 `HH:MM NAME 已收回訊息`(名稱後有空白),`_RECALL_RE` 無法匹配,此行會進入 `_MSG_RE` 匹配,將「已收回訊息」視為普通訊息內容,失去語意標記。
建議修正為:
```suggestion
_RECALL_RE = re.compile(r'^(\d{2}:\d{2}) (\S+?) 已收回訊息$')
```
How can I resolve this? If you propose a fix, please make it concise.Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
|
@greptileai 匯出的檔案姓名和已收回訊息之間沒有空格 |
|
問題出在 對照一般訊息的格式 _MSG_RE = re.compile(r'^(\d{2}:\d{2}) (\S+?) (.+)$')收回訊息也應該在姓名後有空格: |
|
@greptileai@greptileai 現在要怎麼修正 |
|
刪除 |
Summary
_activateMoreOptionsMenu+ChatMoreOptions虛擬視窗邏輯,確保點擊位置與 NVDA+Windows+O 完全相同New Files
addon/appModules/_chatParser.py— 解析 LINE 聊天匯出.txt格式(支援一般訊息、收回訊息,跳過日期行)addon/appModules/_messageReader.py— wx 訊息閱讀器對話框Changes
addon/appModules/line.py— 新增script_openMessageReader、_messageReaderAutoClickSaveChat、_messageReaderHandleSaveDialog、_findSaveDialogEdit(EnumChildWindows遞迴搜尋)、_messageReaderPressSave(BM_CLICK點擊存檔按鈕)、_messageReaderHandleOverwrite、_messageReaderOpenFile;修改_handleChatMoreOptionsAction以支援訊息閱讀器流程addon/globalPlugins/lineDesktopHelper.py— 新增NVDA+Windows+J快捷鍵、工具選單「訊息閱讀器」項目Test plan
🤖 Generated with Claude Code
Greptile Summary
此 PR 新增了「訊息閱讀器」功能(
NVDA+Windows+J),讓使用者可自動儲存 LINE 聊天紀錄並透過鍵盤逐則瀏覽與朗讀訊息。主要新增_chatParser.py(聊天匯出解析)、_messageReader.py(wx 對話框),並在line.py加入自動化點擊流程、在lineDesktopHelper.py加入快捷鍵與選單項目。RuntimeError、暫存檔案清理、_messageReaderPending旗標提前清除等_messageReaderHandleSaveDialog函式中存在兩個import tempfile語句,第一個(函式頂部)是冗餘的重複匯入_messageReaderHandleOverwrite重試次數耗盡且仍有對話框時,_messageReaderPending旗標不會被清除,且不呼叫_messageReaderOpenFile(前次審查已指出,此次仍未修正)Confidence Score: 4/5
此 PR 大幅改善,前次審查的主要問題多已解決,整體可安全合併
前次審查的關鍵問題(多行訊息、單例防護、RuntimeError、暫存檔清理、旗標提前清除、並發保護)均已妥善處理。剩餘的
_messageReaderHandleOverwrite重試耗盡後不清除_messageReaderPending及不通知使用者的問題是邊緣案例(需要連續 6 次重試都失敗),對主要功能路徑影響有限。冗餘import tempfile僅為風格問題,不影響執行。重點關注
addon/appModules/line.py的_messageReaderHandleOverwrite函式(重試耗盡後的錯誤處理路徑)Vulnerabilities
lineDesktop_chat_export.txt)現已在對話框關閉時透過cleanupPath機制刪除,已解決前次的持久化隱私問題_messageReaderHandleOverwrite向 LINE 處理序對話框送出 Enter 鍵,若重試期間 LINE 恰好彈出其他#32770對話框(如錯誤訊息),仍可能被誤關閉(前次審查已指出)Important Files Changed
_RECALL_RE分隔符問題待確認實際匯出格式_messageReaderHandleSaveDialog有冗餘import tempfile;重試耗盡後_messageReaderPending未清除的問題仍存在NVDA+Windows+J快捷鍵與選單項目,結構與現有程式碼一致Sequence Diagram
sequenceDiagram participant User as 使用者 participant GP as GlobalPlugin participant AM as AppModule participant LINE as LINE 應用程式 participant Parser as _chatParser.py participant Reader as _messageReader.py User->>GP: NVDA+Windows+J GP->>AM: script_openMessageReader(gesture) AM->>AM: 設定 _messageReaderPending = True AM->>LINE: _clickMoreOptionsButton() AM->>AM: callLater → _activateMoreOptionsMenu() AM->>AM: callLater → _messageReaderAutoClickSaveChat() AM->>LINE: 自動點擊「儲存聊天」 LINE->>AM: _handleChatMoreOptionsAction AM->>AM: callLater → _messageReaderHandleSaveDialog() AM->>LINE: EnumWindows 找儲存對話框 AM->>LINE: WM_SETTEXT 設定檔名路徑 AM->>LINE: 點擊存檔按鈕 (BM_CLICK) LINE-->>AM: 儲存完成 AM->>AM: callLater → _messageReaderHandleOverwrite() AM->>AM: callLater → _messageReaderOpenFile() AM->>Parser: parseChatFile(savePath) Parser-->>AM: messages[] AM->>Reader: openMessageReader(messages, cleanupPath) Reader-->>User: 顯示訊息閱讀器對話框 User->>Reader: 方向鍵瀏覽訊息 Reader->>Reader: speech.speakMessage(text) User->>Reader: 關閉 Reader->>Reader: os.remove(cleanupPath)Prompt To Fix All With AI
Reviews (11): Last reviewed commit: "Update addon/appModules/line.py" | Re-trigger Greptile