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
+64
-15
@@ -796,6 +796,55 @@ public class DatabaseService
|
||||
return messageId;
|
||||
}
|
||||
|
||||
// ── OpenClaw Import ──
|
||||
|
||||
public async Task CreateChainForImport(string chainId, string sessionId, string agentId, string model, int inputTokens, int outputTokens, int cacheCreationTokens, int cacheReadTokens, string createdAt)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
await conn.ExecuteAsync(@"
|
||||
INSERT INTO chains (chain_id, session_id, agent_id, model, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, iterations, stop_reason, duration_ms, plan_mode, created_at)
|
||||
VALUES (@chainId, @sessionId, @agentId, @model, @inputTokens, @outputTokens, @cacheCreationTokens, @cacheReadTokens, 0, 'end_turn', 0, 0, @createdAt)
|
||||
", new { chainId, sessionId, agentId, model, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, createdAt });
|
||||
}
|
||||
|
||||
public async Task CreateMessageForImport(string chainId, string role, string type, string content, string createdAt, string tag)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
await conn.ExecuteAsync(@"
|
||||
INSERT INTO messages (chain_id, role, type, content, is_error, iteration, attachment_count, tag, created_at)
|
||||
VALUES (@chainId, @role, @type, @content, 0, 0, 0, @tag, @createdAt)
|
||||
", new { chainId, role, type, content, tag, createdAt });
|
||||
}
|
||||
|
||||
public async Task DeleteOpenClawImport(string sessionId)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
using var tx = conn.BeginTransaction();
|
||||
|
||||
// Get all chain_ids that belong to this session and have imported messages
|
||||
var importedChainIds = (await conn.QueryAsync<string>(@"
|
||||
SELECT DISTINCT c.chain_id
|
||||
FROM chains c
|
||||
INNER JOIN messages m ON m.chain_id = c.chain_id
|
||||
WHERE c.session_id = @sessionId AND m.tag = 'openclaw-import'
|
||||
", new { sessionId }, tx)).ToList();
|
||||
|
||||
if (importedChainIds.Count > 0)
|
||||
{
|
||||
// Delete messages for these chains
|
||||
await conn.ExecuteAsync(
|
||||
"DELETE FROM messages WHERE chain_id IN @chainIds",
|
||||
new { chainIds = importedChainIds }, tx);
|
||||
|
||||
// Delete the chains themselves
|
||||
await conn.ExecuteAsync(
|
||||
"DELETE FROM chains WHERE chain_id IN @chainIds",
|
||||
new { chainIds = importedChainIds }, tx);
|
||||
}
|
||||
|
||||
tx.Commit();
|
||||
}
|
||||
|
||||
// ── Attachments ──
|
||||
|
||||
public async Task<IEnumerable<AttachmentRecord>> GetAttachments(long messageId)
|
||||
@@ -839,48 +888,48 @@ public class DatabaseService
|
||||
using var conn = CreateConnection();
|
||||
var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL";
|
||||
var messages = await conn.QueryAsync<MessageRecord>($@"
|
||||
SELECT m.* FROM messages m
|
||||
SELECT m.*, (CASE WHEN m.tag = 'openclaw-import' THEN m.id - 10000000000 ELSE m.id END) AS sort_key FROM messages m
|
||||
JOIN chains c ON m.chain_id = c.chain_id
|
||||
WHERE c.archive_id IS NULL AND c.session_id = @sessionId {deletedFilter}
|
||||
ORDER BY m.id DESC LIMIT @limit",
|
||||
ORDER BY sort_key DESC LIMIT @limit",
|
||||
new { sessionId, limit });
|
||||
return messages.Reverse().ToList();
|
||||
}
|
||||
|
||||
public async Task<int> GetActiveMessageCountBeforeId(string sessionId, long beforeId, bool includeDeleted = false)
|
||||
public async Task<int> GetActiveMessageCountBeforeSortKey(string sessionId, long beforeSortKey, bool includeDeleted = false)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL";
|
||||
return await conn.ExecuteScalarAsync<int>($@"
|
||||
SELECT COUNT(*) FROM messages m
|
||||
JOIN chains c ON m.chain_id = c.chain_id
|
||||
WHERE m.id < @beforeId AND c.archive_id IS NULL AND c.session_id = @sessionId AND m.type = 'text' {deletedFilter}",
|
||||
new { sessionId, beforeId });
|
||||
WHERE (CASE WHEN m.tag = 'openclaw-import' THEN m.id - 10000000000 ELSE m.id END) < @beforeSortKey AND c.archive_id IS NULL AND c.session_id = @sessionId AND m.type = 'text' {deletedFilter}",
|
||||
new { sessionId, beforeSortKey });
|
||||
}
|
||||
|
||||
public async Task<List<MessageRecord>> GetActiveMessagesBeforeId(string sessionId, long beforeId, int limit, bool includeDeleted = false)
|
||||
public async Task<List<MessageRecord>> GetActiveMessagesBeforeSortKey(string sessionId, long beforeSortKey, int limit, bool includeDeleted = false)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL";
|
||||
var messages = await conn.QueryAsync<MessageRecord>($@"
|
||||
SELECT m.* FROM messages m
|
||||
SELECT m.*, (CASE WHEN m.tag = 'openclaw-import' THEN m.id - 10000000000 ELSE m.id END) AS sort_key FROM messages m
|
||||
JOIN chains c ON m.chain_id = c.chain_id
|
||||
WHERE m.id < @beforeId AND c.archive_id IS NULL AND c.session_id = @sessionId {deletedFilter}
|
||||
ORDER BY m.id DESC LIMIT @limit",
|
||||
new { sessionId, beforeId, limit });
|
||||
WHERE (CASE WHEN m.tag = 'openclaw-import' THEN m.id - 10000000000 ELSE m.id END) < @beforeSortKey AND c.archive_id IS NULL AND c.session_id = @sessionId {deletedFilter}
|
||||
ORDER BY sort_key DESC LIMIT @limit",
|
||||
new { sessionId, beforeSortKey, limit });
|
||||
return messages.Reverse().ToList();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<MessageRecord>> GetActiveMessagesAfterId(string sessionId, long afterId, int limit = 200, bool includeDeleted = false)
|
||||
public async Task<IEnumerable<MessageRecord>> GetActiveMessagesAfterSortKey(string sessionId, long afterSortKey, int limit = 200, bool includeDeleted = false)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL";
|
||||
return await conn.QueryAsync<MessageRecord>($@"
|
||||
SELECT m.* FROM messages m
|
||||
SELECT m.*, (CASE WHEN m.tag = 'openclaw-import' THEN m.id - 10000000000 ELSE m.id END) AS sort_key FROM messages m
|
||||
JOIN chains c ON m.chain_id = c.chain_id
|
||||
WHERE m.id > @afterId AND c.archive_id IS NULL AND c.session_id = @sessionId {deletedFilter}
|
||||
ORDER BY m.id LIMIT @limit",
|
||||
new { sessionId, afterId, limit });
|
||||
WHERE (CASE WHEN m.tag = 'openclaw-import' THEN m.id - 10000000000 ELSE m.id END) > @afterSortKey AND c.archive_id IS NULL AND c.session_id = @sessionId {deletedFilter}
|
||||
ORDER BY sort_key LIMIT @limit",
|
||||
new { sessionId, afterSortKey, limit });
|
||||
}
|
||||
|
||||
// ── Message Marking ──
|
||||
|
||||
Reference in new issue
Block a user