feat: OpenClaw session import with sorted history

Backend:
- New OpenClawEndpoint with GET /api/openclaw/sessions and POST /api/openclaw/import
- Lists all OpenClaw sessions with preview messages, model, summary
- Imports user/assistant text messages (skips tool-use/tool-result)
- Cleans user messages (removes webchat prefixes, cron tags, conversation info blocks)
- Re-import support: deletes previous openclaw-import chains before importing
- New DatabaseService methods: CreateChainForImport, CreateMessageForImport, DeleteOpenClawImport
- Added SortKey to MessageRecord (computed: openclaw-import gets id-10B, else id)

History sorting:
- OpenClaw-imported messages always appear before native messages
- Sort by CASE WHEN tag='openclaw-import' THEN id-10000000000 ELSE id END
- Cursor-based paging updated from beforeId/afterId to beforeSortKey/afterSortKey
- All history queries (GetLatestActive, Before, After, CountBefore) updated

Frontend:
- Dashboard: new 'OpenClaw Import' menu item in session card dropdown
- Modal: shows OpenClaw sessions with metadata, summary, preview messages
- Imported messages display blue 'OC' badge in chat
- API functions: apiGetOpenclawSessions, apiImportOpenclawSession
- History paging updated to use sortKey cursors
This commit is contained in:
Meleki committed 2026-03-08 17:32:32 +01:00
1 parent 1597c10a3c
commit 3b7bdb7a20
10 files changed
+750 -38

No files matched your search

+20 -9
View File
@@ -34,7 +34,7 @@ async function _autoLoadImageAttachments(messageId) {
// ── History State ──
let lastKnownMessageId = 0;
let _oldestLoadedMessageId = 0;
let _oldestLoadedSortKey = 0;
let _hasMoreOlderMessages = false;
let historyPollInterval = null;
@@ -50,7 +50,7 @@ async function loadHistory() {
// Älteste geladene Message-ID merken für Nachladen
_hasMoreOlderMessages = data.hasMore;
_oldestLoadedMessageId = _findOldestMessageId(entries);
_oldestLoadedSortKey = _findOldestSortKey(entries);
if (data.hasMore) {
showOlderMessagesButton(data.remainingCount);
@@ -76,7 +76,7 @@ async function loadHistory() {
}
async function loadOlderMessages() {
if (!_hasMoreOlderMessages || !_oldestLoadedMessageId) return;
if (!_hasMoreOlderMessages || !_oldestLoadedSortKey) return;
const btn = chatEl().querySelector('.older-messages-btn');
if (btn) {
@@ -86,12 +86,12 @@ async function loadOlderMessages() {
try {
const sid = getCurrentSessionId();
const data = await apiHistory(sid, _oldestLoadedMessageId, isDeletedVisible());
const data = await apiHistory(sid, _oldestLoadedSortKey, isDeletedVisible());
const entries = data.entries || [];
_hasMoreOlderMessages = data.hasMore;
const newOldest = _findOldestMessageId(entries);
if (newOldest > 0) _oldestLoadedMessageId = newOldest;
const newOldest = _findOldestSortKey(entries);
if (newOldest) _oldestLoadedSortKey = newOldest;
// Button entfernen
removeOlderMessagesButton();
@@ -145,10 +145,10 @@ async function loadOlderMessages() {
// ── Helper Functions ──
function _findOldestMessageId(entries) {
function _findOldestSortKey(entries) {
for (const entry of entries) {
if (entry.messages && entry.messages.length > 0) {
return entry.messages[0].id;
return entry.messages[0].sortKey;
}
}
return 0;
@@ -205,6 +205,7 @@ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds, opts) {
const isReviewMsg = _isReviewMessage(msg.content);
if (msg.role === 'user' && !isReviewMsg) {
const div = _createMessageBubble({ role: 'user', timestamp: msg.createdAt, messageId: msg.id, isMarked: msg.isMarked, deletedOn: msg.deletedOn, archived });
if (msg.tag === 'openclaw-import') _addOpenClawBadge(div);
renderUserContent(div, msg.content);
_addBubbleActions(div, { timestamp: msg.createdAt, messageId: msg.id, deletedOn: msg.deletedOn, isMarked: msg.isMarked });
fragment.appendChild(div);
@@ -224,6 +225,7 @@ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds, opts) {
const div = _createMessageBubble({ role: 'assistant', timestamp: msg.createdAt, messageId: msg.id, isMarked: msg.isMarked, deletedOn: msg.deletedOn, archived });
div.innerHTML = renderMarkdown(msg.content);
parseFluentEmojis(div);
if (msg.tag === 'openclaw-import') _addOpenClawBadge(div);
_addBubbleActions(div, { timestamp: msg.createdAt, messageId: msg.id, deletedOn: msg.deletedOn, isMarked: msg.isMarked });
fragment.appendChild(div);
}
@@ -379,6 +381,14 @@ function _renderReviewGroup(tagGroup, archived) {
return bubble;
}
function _addOpenClawBadge(div) {
var badge = document.createElement('span');
badge.className = 'openclaw-badge';
badge.innerHTML = '<i class="fa fa-cloud-download"></i> OC';
badge.title = 'Importiert aus OpenClaw';
div.insertBefore(badge, div.firstChild);
}
function _isReviewMessage(content) {
return content && (content.startsWith('[Review:') || content.startsWith('[Review-Feedback') || content.startsWith('[Finalizer:') || content.startsWith('[Preparer:'));
}
@@ -429,7 +439,8 @@ function renderMessages(msgs, addToConversation) {
chatEl().appendChild(fragment);
// Track lastKnownMessageId
for (const msg of msgs) {
if (msg.id > lastKnownMessageId) lastKnownMessageId = msg.id;
const sk = msg.sortKey || msg.id;
if (sk > lastKnownMessageId) lastKnownMessageId = sk;
}
// Auto-load image attachments
for (const mid of imageMessageIds) _autoLoadImageAttachments(mid);