feat: load packet logs from directory source
This commit is contained in:
@@ -3,7 +3,10 @@ package isphere
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -13,9 +16,59 @@ func LoadEncryptedPacketLogSourceFromFile(path string) (EncryptedPacketLogSource
|
||||
return EncryptedPacketLogSource{}, fmt.Errorf("packet log file path is empty")
|
||||
}
|
||||
|
||||
file, err := os.Open(trimmedPath)
|
||||
lines, err := readEncryptedPacketLogLinesFromFile(trimmedPath)
|
||||
if err != nil {
|
||||
return EncryptedPacketLogSource{}, fmt.Errorf("open packet log file %q: %w", trimmedPath, err)
|
||||
return EncryptedPacketLogSource{}, err
|
||||
}
|
||||
return EncryptedPacketLogSource{Lines: lines}, nil
|
||||
}
|
||||
|
||||
func LoadEncryptedPacketLogSourceFromDirectory(path string) (EncryptedPacketLogSource, error) {
|
||||
trimmedPath := strings.TrimSpace(path)
|
||||
if trimmedPath == "" {
|
||||
return EncryptedPacketLogSource{}, fmt.Errorf("packet log directory path is empty")
|
||||
}
|
||||
info, err := os.Stat(trimmedPath)
|
||||
if err != nil {
|
||||
return EncryptedPacketLogSource{}, fmt.Errorf("stat packet log directory %q: %w", trimmedPath, err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return EncryptedPacketLogSource{}, fmt.Errorf("packet log directory %q is not a directory", trimmedPath)
|
||||
}
|
||||
|
||||
paths := []string{}
|
||||
if err := filepath.WalkDir(trimmedPath, func(path string, entry fs.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if entry.IsDir() {
|
||||
return nil
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(entry.Name()))
|
||||
if ext == ".log" || ext == ".txt" {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return EncryptedPacketLogSource{}, fmt.Errorf("walk packet log directory %q: %w", trimmedPath, err)
|
||||
}
|
||||
sort.Strings(paths)
|
||||
|
||||
lines := []string{}
|
||||
for _, path := range paths {
|
||||
fileLines, err := readEncryptedPacketLogLinesFromFile(path)
|
||||
if err != nil {
|
||||
return EncryptedPacketLogSource{}, err
|
||||
}
|
||||
lines = append(lines, fileLines...)
|
||||
}
|
||||
return EncryptedPacketLogSource{Lines: lines}, nil
|
||||
}
|
||||
|
||||
func readEncryptedPacketLogLinesFromFile(path string) ([]string, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open packet log file %q: %w", path, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
@@ -30,7 +83,7 @@ func LoadEncryptedPacketLogSourceFromFile(path string) (EncryptedPacketLogSource
|
||||
lines = append(lines, line)
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return EncryptedPacketLogSource{}, fmt.Errorf("read packet log file %q: %w", trimmedPath, err)
|
||||
return nil, fmt.Errorf("read packet log file %q: %w", path, err)
|
||||
}
|
||||
return EncryptedPacketLogSource{Lines: lines}, nil
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package isphere
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
@@ -19,6 +20,23 @@ func TestLoadEncryptedPacketLogSourceFromFileReadsNonEmptyLines(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadEncryptedPacketLogSourceFromDirectoryReadsSortedLogFiles(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writePacketLogFileAtPathForTest(t, root+"\\b.log", "\n line-b1 \n")
|
||||
writePacketLogFileAtPathForTest(t, root+"\\a.log", "line-a1\n\nline-a2\n")
|
||||
writePacketLogFileAtPathForTest(t, root+"\\nested\\c.txt", "line-c1\n")
|
||||
writePacketLogFileAtPathForTest(t, root+"\\ignored.tmp", "ignored\n")
|
||||
|
||||
got, err := LoadEncryptedPacketLogSourceFromDirectory(root)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadEncryptedPacketLogSourceFromDirectory returned error: %v", err)
|
||||
}
|
||||
want := EncryptedPacketLogSource{Lines: []string{"line-a1", "line-a2", "line-b1", "line-c1"}}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("source = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func writePacketLogFileForTest(t *testing.T, content string) string {
|
||||
t.Helper()
|
||||
path := t.TempDir() + "\\packet.log"
|
||||
@@ -27,3 +45,13 @@ func writePacketLogFileForTest(t *testing.T, content string) string {
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func writePacketLogFileAtPathForTest(t *testing.T, path string, content string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatalf("create parent for %s: %v", path, err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
||||
t.Fatalf("write test packet log %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
ServerTitle = "iSphere AI Bridge"
|
||||
ServerVersion = "0.1.0"
|
||||
EnvPacketLogFileKey = "ISPHERE_PACKET_LOG_FILE"
|
||||
EnvPacketLogDirKey = "ISPHERE_PACKET_LOG_DIR"
|
||||
)
|
||||
|
||||
func NewServer() *mcp.Server {
|
||||
@@ -26,12 +27,22 @@ func NewServer() *mcp.Server {
|
||||
func NewServerFromEnv() (*mcp.Server, error) {
|
||||
var source tools.ReceiveMessagesSource = isphere.EncryptedPacketLogSource{}
|
||||
packetLogFile := strings.TrimSpace(os.Getenv(EnvPacketLogFileKey))
|
||||
packetLogDir := strings.TrimSpace(os.Getenv(EnvPacketLogDirKey))
|
||||
if packetLogFile != "" && packetLogDir != "" {
|
||||
return nil, fmt.Errorf("configure packet log source: set only one of %s or %s", EnvPacketLogFileKey, EnvPacketLogDirKey)
|
||||
}
|
||||
if packetLogFile != "" {
|
||||
loaded, err := isphere.LoadEncryptedPacketLogSourceFromFile(packetLogFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("configure %s: %w", EnvPacketLogFileKey, err)
|
||||
}
|
||||
source = loaded
|
||||
} else if packetLogDir != "" {
|
||||
loaded, err := isphere.LoadEncryptedPacketLogSourceFromDirectory(packetLogDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("configure %s: %w", EnvPacketLogDirKey, err)
|
||||
}
|
||||
source = loaded
|
||||
}
|
||||
return NewServerWithReceiveSource(source), nil
|
||||
}
|
||||
|
||||
@@ -127,6 +127,70 @@ func TestNewServerFromEnvUsesPacketLogFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewServerFromEnvUsesPacketLogDirectory(t *testing.T) {
|
||||
plaintext := `--------------------------------------------------------------------------------------------------------------------------------------------
|
||||
2026/7/7 15:30:07
|
||||
<message id="msg-dir-1" from="sender@imopenfire1-lanzhou/imp_pc_4.1.2.6842" to="receiver@imopenfire1-lanzhou" type="chat">
|
||||
<body>redacted from dir</body>
|
||||
<received xmlns="urn:xmpp:receipts" id="receipt-dir-1" type="1" stamp="" />
|
||||
<subject>DIR_SOURCE</subject>
|
||||
<isphere xmlns="isphere.im" type="1001" sendtime="1783423807000" version="1" />
|
||||
<readed />
|
||||
</message>`
|
||||
dirPath := writeEncryptedPacketLogDirectoryForServerTest(t, plaintext)
|
||||
t.Setenv("ISPHERE_PACKET_LOG_DIR", dirPath)
|
||||
|
||||
server, err := NewServerFromEnv()
|
||||
if err != nil {
|
||||
t.Fatalf("NewServerFromEnv returned error: %v", err)
|
||||
}
|
||||
session, cleanup := connectServerTestSession(t, server)
|
||||
defer cleanup()
|
||||
|
||||
callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
|
||||
Name: "isphere_receive_messages",
|
||||
Arguments: map[string]any{"limit": 10},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("call isphere_receive_messages: %v", err)
|
||||
}
|
||||
if callResult.IsError {
|
||||
t.Fatalf("call result is error: %+v", callResult)
|
||||
}
|
||||
|
||||
var decoded struct {
|
||||
Messages []struct {
|
||||
ID string `json:"id"`
|
||||
Text string `json:"text"`
|
||||
Subject string `json:"subject"`
|
||||
} `json:"messages"`
|
||||
}
|
||||
payload, err := json.Marshal(callResult.StructuredContent)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal structured content: %v", err)
|
||||
}
|
||||
if err := json.Unmarshal(payload, &decoded); err != nil {
|
||||
t.Fatalf("decode structured content %s: %v", payload, err)
|
||||
}
|
||||
if len(decoded.Messages) != 1 {
|
||||
t.Fatalf("message count = %d, want 1", len(decoded.Messages))
|
||||
}
|
||||
msg := decoded.Messages[0]
|
||||
if msg.ID != "msg-dir-1" || msg.Text != "redacted from dir" || msg.Subject != "DIR_SOURCE" {
|
||||
t.Fatalf("unexpected message: %+v", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func writeEncryptedPacketLogDirectoryForServerTest(t *testing.T, plaintext string) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
line := encryptPacketLogLineForServerTest(t, plaintext)
|
||||
if err := os.WriteFile(dir+"\\packet-reader-2.txt", []byte("\n"+line+"\n"), 0o600); err != nil {
|
||||
t.Fatalf("write encrypted packet log directory fixture: %v", err)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func connectServerTestSession(t *testing.T, server *mcp.Server) (*mcp.ClientSession, func()) {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
Reference in New Issue
Block a user