v2: project cleanup, Desktop paths fixed, README updated, serve daemon added, mohe-xxm protocol documented

This commit is contained in:
hmo
2026-05-20 03:51:59 +08:00
parent 9e4c50a8a7
commit 3425ded733
10 changed files with 1090 additions and 163 deletions
+118
View File
@@ -0,0 +1,118 @@
# Mohe & XiaoXiaoMo Chat Viewer
# Lists [mohe]/[xxm] messages from an OpenCode session.
#
# Usage:
# .\moho_chat.ps1 <session_id> [minutes]
#
# Examples:
# .\moho_chat.ps1 ses_1d95d15c4ffehQaZ6hrbIbak5k
# .\moho_chat.ps1 ses_1d95d15c4ffehQaZ6hrbIbak5k 30
param(
[Parameter(Mandatory=$true)][string]$SessionId,
[int]$Minutes = 0
)
$ErrorActionPreference = 'Stop'
Write-Host "Exporting session..." -ForegroundColor DarkGray
$tmpFile = [System.IO.Path]::GetTempFileName() + ".json"
$env:CI = 'true'
opencode.cmd export $SessionId 2>$null | Set-Content -Path $tmpFile -NoNewline -Encoding UTF8
if (-not (Test-Path $tmpFile) -or (Get-Item $tmpFile).Length -eq 0) {
Write-Error "Export failed"
exit 1
}
$size = (Get-Item $tmpFile).Length
Write-Host "Read ($($size / 1MB -as [int]) MB)" -ForegroundColor DarkGray
$raw = Get-Content $tmpFile -Raw -Encoding UTF8
Remove-Item $tmpFile -Force
# Check if it's actually UTF-16LE in disguise (opencode export uses UTF-16LE)
if ($raw.Length -eq 0 -or $raw[0] -ne '{') {
# Re-read as UTF-16
$bytes = [System.IO.File]::ReadAllBytes($tmpFile)
Remove-Item $tmpFile -Force
$raw = [System.Text.Encoding]::Unicode.GetString($bytes)
}
$brace = $raw.IndexOf('{')
if ($brace -gt 0) { $raw = $raw.Substring($brace) }
# Use regex to find "text": "[mohe]..." patterns directly (no JSON parsing)
$pattern = '"text":\s*"\[(mohe|xxm)\][^"]*"'
$cutoff = if ($Minutes -gt 0) { (Get-Date).ToUniversalTime().AddMinutes(-$Minutes) } else { $null }
$results = @()
$matchPos = 0
while ($true) {
$m = [regex]::Match($raw, $pattern, $matchPos)
if (-not $m.Success) { break }
$matchPos = $m.Index + 1
$tag = $m.Groups[1].Value
$content = $m.Value
# Extract the full text content (everything between "text": " and the closing ")
$start = $m.Index + $m.Value.IndexOf('"', $m.Value.IndexOf(':')+1) + 1
$fullText = ''
$escape = $false
for ($i = $start; $i -lt $raw.Length; $i++) {
$c = $raw[$i]
if ($escape) { $fullText += $c; $escape = $false }
elseif ($c -eq '\') { $fullText += $c; $escape = $true }
elseif ($c -eq '"') { break }
else { $fullText += $c }
}
$fullText = $fullText.Trim()
if (-not ($fullText.StartsWith('[mohe]') -or $fullText.StartsWith('[xxm]'))) { continue }
$sender = if ($fullText.StartsWith('[mohe]')) { '[mohe] Mohe' } else { '[xxm] XiaoXiaoMo' }
$display = $fullText -replace '^\[\w+\]\s*', ''
# Get timestamp by looking backward
$before = $raw.Substring([Math]::Max(0, $m.Index - 3000), [Math]::Min(3000, $m.Index))
$tsMatch = [regex]::Match($before, '"timestamp":\s*"([^"]+)"')
$tsStr = if ($tsMatch.Success) { $tsMatch.Groups[1].Value } else { '' }
if (-not $tsStr) {
$crMatch = [regex]::Match($before, '"created":\s*(\d+)')
if ($crMatch.Success) {
$tsStr = ([DateTimeOffset]::FromUnixTimeMilliseconds([long]$crMatch.Groups[1].Value).UtcDateTime).ToString('o')
}
}
$ts = $null
if ($tsStr) {
try { $ts = [DateTime]::Parse($tsStr, $null, [System.Globalization.DateTimeStyles]::AssumeUniversal) } catch {}
}
if ($cutoff -and $ts -and $ts -lt $cutoff) { continue }
$timeLocal = if ($ts) { $ts.ToLocalTime().ToString('HH:mm:ss') } else { '??' }
$results += [PSCustomObject]@{ Time=$timeLocal; Sort=if($ts){$ts.Ticks}else{0}; Sender=$sender; Message=$display }
}
$results = $results | Sort-Object Sort
if ($results.Count -eq 0) {
Write-Host "No [mohe]/[xxm] messages found" -ForegroundColor Yellow
if ($Minutes -gt 0) { Write-Host "(last $Minutes minutes)" }
exit
}
Write-Host "`n$($results.Count) message(s)" -ForegroundColor Cyan
if ($Minutes -gt 0) { Write-Host "(last $Minutes min)" }
Write-Host ("=" * 60)
foreach ($r in $results) {
Write-Host ("[{0}] {1}: {2}" -f $r.Time, $r.Sender, $r.Message)
}