Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions addon/appModules/_chatParser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import re

_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+?)已收回訊息$')
Comment on lines +3 to +5
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 正規表示式假設欄位以空白分隔,但 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.

Fix in Claude Code Fix in Codex

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 _RECALL_RE 在訊息名稱與「已收回訊息」之間缺少空白

_MSG_RE 使用空白分隔三個欄位(時間、名稱、內容),格式為 HH:MM NAME content。但 _RECALL_RE 在名稱與「已收回訊息」之間沒有空白,格式為 HH:MM NAME已收回訊息

若 LINE 匯出格式為 HH:MM NAME 已收回訊息(名稱後有空白),_RECALL_RE 無法匹配,此行會進入 _MSG_RE 匹配,將「已收回訊息」視為普通訊息內容,失去語意標記。

建議修正為:

Suggested change
_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.

Fix in Claude Code Fix in Codex



def parseChatFile(filePath):
"""Parse a LINE chat export text file.

Returns a list of dicts with keys: name, content, time.
Display format: name content time
Continuation lines (Shift+Enter multi-line messages) are appended to the
previous message's content.
"""
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 (Shift+Enter multi-line message)
if messages:
messages[-1]['content'] += '\n' + line
return messages
144 changes: 144 additions & 0 deletions addon/appModules/_messageReader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import wx
import gui
from logHandler import log


class MessageReaderDialog(wx.Dialog):
"""A dialog for reading LINE chat messages with up/down arrow navigation.

Each message is displayed as: name content time
Up arrow moves to the previous message, down arrow moves to the next.
"""

def __init__(self, messages, title=None, cleanupPath=None):
if title is None:
title = _("訊息閱讀器")
super().__init__(
gui.mainFrame,
title=title,
style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER,
)
self._messages = messages
self._pos = 0 if messages else -1
self._cleanupPath = cleanupPath

panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)

self._totalLabel = wx.StaticText(panel, label="")
sizer.Add(self._totalLabel, 0, wx.ALL, 5)

self._textCtrl = wx.TextCtrl(
panel,
style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH2,
size=(500, 100),
)
sizer.Add(self._textCtrl, 1, wx.EXPAND | wx.ALL, 5)

closeBtn = wx.Button(panel, wx.ID_CLOSE, _("關閉(&C)"))
sizer.Add(closeBtn, 0, wx.ALIGN_CENTER | wx.ALL, 5)

panel.SetSizer(sizer)
sizer.Fit(self)

self._textCtrl.Bind(wx.EVT_KEY_DOWN, self._onKeyDown)
closeBtn.Bind(wx.EVT_BUTTON, self._onClose)
self.Bind(wx.EVT_CLOSE, self._onClose)
self.Bind(wx.EVT_CHAR_HOOK, self._onCharHook)

self._updateDisplay()
self._textCtrl.SetFocus()

def _formatMessage(self, msg):
return f"{msg['name']} {msg['content']} {msg['time']}"

def _updateDisplay(self):
if not self._messages or self._pos < 0:
self._textCtrl.SetValue(_("沒有訊息"))
self._totalLabel.SetLabel("")
return
msg = self._messages[self._pos]
text = self._formatMessage(msg)
self._textCtrl.SetValue(text)
self._totalLabel.SetLabel(
f"{self._pos + 1} / {len(self._messages)}"
)
self._speakMessage(text)

def _speakMessage(self, text):
try:
import speech
speech.cancelSpeech()
speech.speakMessage(text)
except Exception:
pass

def _onKeyDown(self, evt):
keyCode = evt.GetKeyCode()
if keyCode == wx.WXK_UP:
self._movePrevious()
elif keyCode == wx.WXK_DOWN:
self._moveNext()
else:
evt.Skip()

def _onCharHook(self, evt):
keyCode = evt.GetKeyCode()
if keyCode == wx.WXK_ESCAPE:
self.Close()
return
evt.Skip()

def _movePrevious(self):
if not self._messages:
return
if self._pos > 0:
self._pos -= 1
self._updateDisplay()
else:
self._speakMessage(_("已經是第一則訊息"))

def _moveNext(self):
if not self._messages:
return
if self._pos < len(self._messages) - 1:
self._pos += 1
self._updateDisplay()
else:
self._speakMessage(_("已經是最後一則訊息"))

def _onClose(self, evt):
global _readerDlg
_readerDlg = None # Allow future invocations to create a new instance
# Clean up temp file if specified
if self._cleanupPath:
try:
import os
if os.path.isfile(self._cleanupPath):
os.remove(self._cleanupPath)
log.debug(f"Deleted temp chat export: {self._cleanupPath}")
except Exception as e:
log.warning(f"Failed to delete temp chat export: {e}")
self.Destroy()


_readerDlg = None # module-level singleton sentinel


def openMessageReader(messages, title=None, cleanupPath=None):
"""Open the message reader dialog on the main GUI thread.

Args:
messages: List of parsed message dicts
title: Dialog window title
cleanupPath: Optional file path to delete when dialog closes
"""
def _show():
global _readerDlg
if _readerDlg and _readerDlg.IsShown():
_readerDlg.Raise()
return
_readerDlg = MessageReaderDialog(messages, title=title, cleanupPath=cleanupPath)
_readerDlg.Show()
_readerDlg.Raise()
wx.CallAfter(_show)
Loading
Loading