feat: establish NekoNest Cloud control and relay
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
package tenantstore
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/klarkxy/nekonest/relaycore/protocol"
|
||||
)
|
||||
|
||||
// SaveSealedMessage persists an opaque sealed session_message envelope.
|
||||
// No application plaintext is written; ciphertext lives in metadata_json.
|
||||
func (db *DB) SaveSealedMessage(deviceID, sessionID string, msg *protocol.NekoMessage) error {
|
||||
if msg == nil || msg.SealedPayload == nil {
|
||||
return nil
|
||||
}
|
||||
id := msg.ClientMsgID
|
||||
if id == "" {
|
||||
id = fmt.Sprintf("sealed_%d_%d", msg.Timestamp, msg.SealedPayload.Sequence)
|
||||
}
|
||||
meta, _ := marshalJSON(map[string]any{
|
||||
"sealed": true,
|
||||
"sealed_payload": msg.SealedPayload,
|
||||
"protocol_version": msg.ProtocolVersion,
|
||||
"transport_mode": msg.TransportMode,
|
||||
})
|
||||
_, err := db.conn.Exec(`
|
||||
INSERT INTO session_messages (id, device_id, session_id, role, content, type, timestamp, metadata_json)
|
||||
VALUES (?, ?, ?, 'assistant', '', 'sealed', ?, ?)
|
||||
ON CONFLICT(id, device_id, session_id) DO UPDATE SET
|
||||
timestamp = excluded.timestamp,
|
||||
metadata_json = excluded.metadata_json`,
|
||||
id, deviceID, sessionID, msg.Timestamp, string(meta),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// SaveMessage stores a session message in the database.
|
||||
// Same id is upserted so streaming patches update content in place.
|
||||
func (db *DB) SaveMessage(deviceID, sessionID string, msg *protocol.SessionMessage) error {
|
||||
_, err := db.conn.Exec(`
|
||||
INSERT INTO session_messages (id, device_id, session_id, role, content, type, timestamp, metadata_json)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id, device_id, session_id) DO UPDATE SET
|
||||
content = excluded.content,
|
||||
type = excluded.type,
|
||||
timestamp = excluded.timestamp,
|
||||
metadata_json = excluded.metadata_json,
|
||||
role = excluded.role`,
|
||||
msg.ID, deviceID, sessionID, msg.Role, msg.Content, msg.Type, msg.Timestamp,
|
||||
metadataToJSON(msg.Metadata),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetMessages retrieves messages for a session, ordered by timestamp.
|
||||
// limit=0 means no limit.
|
||||
func (db *DB) GetMessages(deviceID, sessionID string, limit int) ([]*protocol.SessionMessage, error) {
|
||||
query := `SELECT id, role, content, type, timestamp, metadata_json
|
||||
FROM session_messages
|
||||
WHERE device_id = ? AND session_id = ?
|
||||
ORDER BY timestamp ASC`
|
||||
|
||||
if limit > 0 {
|
||||
query = `SELECT id, role, content, type, timestamp, metadata_json
|
||||
FROM session_messages
|
||||
WHERE device_id = ? AND session_id = ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ?`
|
||||
}
|
||||
|
||||
var rows *sql.Rows
|
||||
var err error
|
||||
|
||||
if limit > 0 {
|
||||
rows, err = db.conn.Query(query, deviceID, sessionID, limit)
|
||||
} else {
|
||||
rows, err = db.conn.Query(query, deviceID, sessionID)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var messages []*protocol.SessionMessage
|
||||
for rows.Next() {
|
||||
msg := &protocol.SessionMessage{}
|
||||
var metadataJSON sql.NullString
|
||||
if err := rows.Scan(&msg.ID, &msg.Role, &msg.Content, &msg.Type, &msg.Timestamp, &metadataJSON); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if metadataJSON.Valid {
|
||||
msg.Metadata = jsonToMetadata(metadataJSON.String)
|
||||
}
|
||||
messages = append(messages, msg)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If we used LIMIT, reverse to get chronological order
|
||||
if limit > 0 {
|
||||
for i, j := 0, len(messages)-1; i < j; i, j = i+1, j-1 {
|
||||
messages[i], messages[j] = messages[j], messages[i]
|
||||
}
|
||||
}
|
||||
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
// GetMessageCount returns the number of messages for a session.
|
||||
func (db *DB) GetMessageCount(deviceID, sessionID string) (int, error) {
|
||||
var count int
|
||||
err := db.conn.QueryRow(
|
||||
`SELECT COUNT(*) FROM session_messages WHERE device_id = ? AND session_id = ?`,
|
||||
deviceID, sessionID,
|
||||
).Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
// DeleteOldMessages removes messages older than the given timestamp.
|
||||
func (db *DB) DeleteOldMessages(before time.Time) (int64, error) {
|
||||
result, err := db.conn.Exec(
|
||||
`DELETE FROM session_messages WHERE timestamp < ?`,
|
||||
before.Unix(),
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected()
|
||||
}
|
||||
|
||||
// DeleteSessionMessages removes all messages for a session.
|
||||
func (db *DB) DeleteSessionMessages(deviceID, sessionID string) error {
|
||||
_, err := db.conn.Exec(
|
||||
`DELETE FROM session_messages WHERE device_id = ? AND session_id = ?`,
|
||||
deviceID, sessionID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// ListSessionsWithMessages returns session IDs that have stored messages for a device.
|
||||
func (db *DB) ListSessionsWithMessages(deviceID string) ([]string, error) {
|
||||
rows, err := db.conn.Query(
|
||||
`SELECT session_id FROM session_messages WHERE device_id = ? GROUP BY session_id ORDER BY MAX(timestamp) DESC`,
|
||||
deviceID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var sessionIDs []string
|
||||
for rows.Next() {
|
||||
var sid string
|
||||
if err := rows.Scan(&sid); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sessionIDs = append(sessionIDs, sid)
|
||||
}
|
||||
return sessionIDs, nil
|
||||
}
|
||||
|
||||
// metadataToJSON converts metadata map to a JSON string for storage.
|
||||
func metadataToJSON(m map[string]any) string {
|
||||
if m == nil {
|
||||
return "{}"
|
||||
}
|
||||
// Simple JSON serialization for metadata
|
||||
data, err := marshalJSON(m)
|
||||
if err != nil {
|
||||
return "{}"
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// jsonToMetadata parses a JSON string back to metadata map.
|
||||
func jsonToMetadata(s string) map[string]any {
|
||||
if s == "" || s == "{}" {
|
||||
return nil
|
||||
}
|
||||
var m map[string]any
|
||||
if err := unmarshalJSON([]byte(s), &m); err != nil {
|
||||
return nil
|
||||
}
|
||||
return m
|
||||
}
|
||||
Reference in New Issue
Block a user