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:
1 parent
1597c10a3c
commit
3b7bdb7a20
10 files changed
+750
-38
No files matched your search
+20
-9
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user