feat: add user input history MCP and rebuild release

This commit is contained in:
shiyue
2026-08-18 09:10:25 +08:00
parent 6ed8c7d30b
commit 389690af64
11 changed files with 957 additions and 0 deletions

View File

@@ -5475,6 +5475,18 @@ function clampMcpLimit(value) {
return Math.max(1, Math.min(100, parsed));
}
function clampUserInputsLimit(value) {
const parsed = Number.parseInt(String(value || ''), 10);
if (!Number.isFinite(parsed)) return 15;
return Math.max(1, Math.min(50, parsed));
}
function clampUserInputsMaxChars(value) {
const parsed = Number.parseInt(String(value || ''), 10);
if (!Number.isFinite(parsed)) return 1000;
return Math.max(1, Math.min(1000, parsed));
}
function listConversationSummaries(args = {}, sourceSessionId = '') {
reconcilePendingCrossConversationReplies();
const scope = args.scope === 'children' ? 'children' : 'all';
@@ -5532,6 +5544,90 @@ function listConversationSummaries(args = {}, sourceSessionId = '') {
};
}
function listUserInputHistory(args = {}, sourceSessionId = '') {
const hasExplicitConversationId = Object.prototype.hasOwnProperty.call(args, 'conversationId');
let conversationId;
if (hasExplicitConversationId) {
const rawConversationId = String(args.conversationId || '').trim();
const sanitizedConversationId = sanitizeId(rawConversationId);
if (!sanitizedConversationId || sanitizedConversationId !== rawConversationId) {
return mcpToolError('invalid_conversation_id', 'conversationId 无效。', {
conversationId: rawConversationId || null,
});
}
conversationId = sanitizedConversationId;
} else {
conversationId = sanitizeId(sourceSessionId || '');
if (!conversationId) {
return mcpToolError('missing_source_conversation', '缺少来源对话 ID。');
}
}
const session = loadSession(conversationId);
if (!session) {
return mcpToolError('conversation_not_found', '目标对话不存在。', {
conversationId,
});
}
const requestedLimit = clampUserInputsLimit(args.limit);
const maxChars = clampUserInputsMaxChars(args.maxChars);
const userMessages = (Array.isArray(session.messages) ? session.messages : [])
.filter((message) => message?.role === 'user' && typeof message.content === 'string' && message.content.trim())
.map((message) => ({
messageId: message.id || message.messageId || null,
createdAt: message.createdAt || message.timestamp || null,
content: message.content,
}));
const candidates = userMessages.slice(-requestedLimit);
const items = [];
let remainingChars = maxChars;
let totalChars = 0;
let budgetLimited = false;
for (let index = candidates.length - 1; index >= 0; index -= 1) {
if (remainingChars <= 0) {
budgetLimited = true;
break;
}
const candidate = candidates[index];
const codePoints = Array.from(candidate.content);
const originalChars = codePoints.length;
const truncated = originalChars > remainingChars;
const content = truncated ? codePoints.slice(0, remainingChars).join('') : candidate.content;
const contentChars = truncated ? remainingChars : originalChars;
items.push({
messageId: candidate.messageId,
createdAt: candidate.createdAt,
content,
contentChars,
originalChars,
truncated,
});
totalChars += contentChars;
remainingChars -= contentChars;
if (truncated) {
budgetLimited = true;
break;
}
}
items.reverse();
return {
ok: true,
conversationId,
requestedLimit,
maxChars,
totalChars,
returnedCount: items.length,
hasMore: userMessages.length > candidates.length || budgetLimited || items.length < candidates.length,
items,
};
}
function normalizeMcpPromptText(value, maxChars) {
if (value === null || value === undefined) return '';
return truncateTextValue(String(value).trim(), maxChars, '...');
@@ -6296,6 +6392,8 @@ function callInternalMcpTool(tool, args, sourceSessionId, sourceHopCount) {
return createCcwebDisplayImage(args, sourceSessionId);
case 'ccweb_list_conversations':
return listConversationSummaries(args, sanitizeId(sourceSessionId || ''));
case 'ccweb_list_user_inputs':
return listUserInputHistory(args, sourceSessionId);
case 'ccweb_set_title':
return setCurrentConversationTitle(args, sourceSessionId);
case 'ccweb_create_conversation':