feat: add streaming ui configuration
This commit is contained in:
@@ -332,6 +332,30 @@
|
||||
getUsageOverview: function(workspaceId, params) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/usage' + query(params));
|
||||
},
|
||||
getProtocolCapabilities: function(workspaceId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/protocol-capabilities');
|
||||
},
|
||||
listStreamSessions: function(workspaceId, params) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/stream-sessions' + query(params));
|
||||
},
|
||||
getStreamSession: function(workspaceId, sessionId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/stream-sessions/' + encodeURIComponent(sessionId));
|
||||
},
|
||||
stopStreamSession: function(workspaceId, sessionId) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/stream-sessions/' + encodeURIComponent(sessionId) + '/stop', {});
|
||||
},
|
||||
listAsyncJobs: function(workspaceId, params) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/async-jobs' + query(params));
|
||||
},
|
||||
getAsyncJob: function(workspaceId, jobId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/async-jobs/' + encodeURIComponent(jobId));
|
||||
},
|
||||
cancelAsyncJob: function(workspaceId, jobId) {
|
||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/async-jobs/' + encodeURIComponent(jobId) + '/cancel', {});
|
||||
},
|
||||
getAsyncJobResult: function(workspaceId, jobId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/async-jobs/' + encodeURIComponent(jobId) + '/result');
|
||||
},
|
||||
getOperationUsage: function(workspaceId, operationId, params) {
|
||||
return get(
|
||||
'/workspaces/' + encodeURIComponent(workspaceId) + '/usage/operations/' + encodeURIComponent(operationId) + query(params)
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var state = {
|
||||
items: [],
|
||||
workspaceId: null,
|
||||
loading: false,
|
||||
error: '',
|
||||
openId: null,
|
||||
};
|
||||
|
||||
var list = document.getElementById('async-jobs-list');
|
||||
var summary = document.getElementById('async-jobs-summary');
|
||||
var refreshButton = document.getElementById('async-jobs-refresh');
|
||||
var statusFilter = document.getElementById('async-jobs-status');
|
||||
|
||||
function tKey(key, vars) {
|
||||
if (!window.t) return key;
|
||||
return t(key, vars);
|
||||
}
|
||||
|
||||
function currentWorkspaceId() {
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
return workspace ? workspace.id : null;
|
||||
}
|
||||
|
||||
function formatJson(value) {
|
||||
if (value === null || value === undefined) return '';
|
||||
if (typeof value === 'string') return value;
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) return '—';
|
||||
return new Date(value).toLocaleString();
|
||||
}
|
||||
|
||||
function renderEmpty(title, body) {
|
||||
list.innerHTML = '<div class="empty-state"><div class="empty-state-title">' + title + '</div><div class="empty-state-text">' + body + '</div></div>';
|
||||
}
|
||||
|
||||
function render() {
|
||||
summary.textContent = tKey('async_jobs.summary', { count: state.items.length });
|
||||
|
||||
if (state.loading && state.items.length === 0) {
|
||||
renderEmpty(tKey('async_jobs.loading.title'), tKey('async_jobs.loading.body'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.error) {
|
||||
renderEmpty(tKey('async_jobs.error.title'), state.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.items.length) {
|
||||
renderEmpty(tKey('async_jobs.empty.title'), tKey('async_jobs.empty.body'));
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = state.items.map(function(item) {
|
||||
var open = state.openId === item.id;
|
||||
var statusLabel = tKey('async_jobs.status.' + item.status);
|
||||
return [
|
||||
'<div class="resource-card" data-job-id="' + item.id + '">',
|
||||
' <div class="resource-card-header">',
|
||||
' <div>',
|
||||
' <div class="resource-card-title">' + item.id + '</div>',
|
||||
' <div class="resource-card-subtitle">' + tKey('async_jobs.operation', { operation: item.operation_id }) + '</div>',
|
||||
' <div class="resource-pill-row">',
|
||||
' <span class="resource-status-pill ' + String(item.status || '').toLowerCase() + '">' + statusLabel + '</span>',
|
||||
' </div>',
|
||||
' </div>',
|
||||
' <div class="resource-card-actions">',
|
||||
item.status === 'created' || item.status === 'running'
|
||||
? ' <button class="btn-secondary" data-action="cancel" data-job-id="' + item.id + '">' + tKey('async_jobs.cancel') + '</button>'
|
||||
: '',
|
||||
' <button class="btn-secondary" data-action="toggle" data-job-id="' + item.id + '">' + (open ? tKey('async_jobs.hide_details') : tKey('async_jobs.show_details')) + '</button>',
|
||||
' </div>',
|
||||
' </div>',
|
||||
' <div class="resource-meta-grid">',
|
||||
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('async_jobs.meta.created') + '</div><div class="resource-meta-value">' + formatDateTime(item.created_at) + '</div></div>',
|
||||
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('async_jobs.meta.updated') + '</div><div class="resource-meta-value">' + formatDateTime(item.updated_at) + '</div></div>',
|
||||
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('async_jobs.meta.finished') + '</div><div class="resource-meta-value">' + formatDateTime(item.finished_at) + '</div></div>',
|
||||
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('async_jobs.meta.expires') + '</div><div class="resource-meta-value">' + formatDateTime(item.expires_at) + '</div></div>',
|
||||
' </div>',
|
||||
open
|
||||
? ' <div class="resource-detail-block"><div class="resource-detail-title">' + tKey('async_jobs.progress') + '</div><pre class="resource-detail-pre">' + formatJson(item.progress) + '</pre></div>'
|
||||
: '',
|
||||
open
|
||||
? ' <div class="resource-detail-block"><div class="resource-detail-title">' + tKey('async_jobs.error_preview') + '</div><pre class="resource-detail-pre">' + formatJson(item.error) + '</pre></div>'
|
||||
: '',
|
||||
open && (item.status === 'completed' || item.status === 'failed' || item.status === 'cancelled' || item.status === 'expired')
|
||||
? ' <div class="resource-detail-block"><div class="resource-detail-title">' + tKey('async_jobs.result') + '</div><pre class="resource-detail-pre" data-result-for="' + item.id + '">' + tKey('async_jobs.result_loading') + '</pre></div>'
|
||||
: '',
|
||||
'</div>'
|
||||
].join('');
|
||||
}).join('');
|
||||
|
||||
list.querySelectorAll('[data-action="toggle"]').forEach(function(button) {
|
||||
button.addEventListener('click', function () {
|
||||
var jobId = this.getAttribute('data-job-id');
|
||||
state.openId = state.openId === jobId ? null : jobId;
|
||||
if (state.openId) {
|
||||
void loadDetail(jobId);
|
||||
} else {
|
||||
render();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
list.querySelectorAll('[data-action="cancel"]').forEach(function(button) {
|
||||
button.addEventListener('click', async function () {
|
||||
try {
|
||||
var jobId = this.getAttribute('data-job-id');
|
||||
await window.CrankApi.cancelAsyncJob(state.workspaceId, jobId);
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(tKey('async_jobs.toast.cancel.body'), tKey('async_jobs.toast.cancel.title'));
|
||||
}
|
||||
await load();
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(error.message || tKey('async_jobs.toast.cancel_error.body'), tKey('async_jobs.toast.cancel_error.title'));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function loadResult(jobId) {
|
||||
try {
|
||||
var result = await window.CrankApi.getAsyncJobResult(state.workspaceId, jobId);
|
||||
var node = document.querySelector('[data-result-for="' + jobId + '"]');
|
||||
if (node) node.textContent = formatJson(result);
|
||||
} catch (error) {
|
||||
var failedNode = document.querySelector('[data-result-for="' + jobId + '"]');
|
||||
if (failedNode) failedNode.textContent = error.message || tKey('async_jobs.result_error');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDetail(jobId) {
|
||||
var detail = await window.CrankApi.getAsyncJob(state.workspaceId, jobId);
|
||||
state.items = state.items.map(function(item) {
|
||||
return item.id === jobId ? detail : item;
|
||||
});
|
||||
render();
|
||||
if (detail.status === 'completed' || detail.status === 'failed' || detail.status === 'cancelled' || detail.status === 'expired') {
|
||||
await loadResult(jobId);
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
state.workspaceId = currentWorkspaceId();
|
||||
if (!state.workspaceId) {
|
||||
state.items = [];
|
||||
state.error = tKey('async_jobs.error.workspace');
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
state.loading = true;
|
||||
state.error = '';
|
||||
render();
|
||||
|
||||
try {
|
||||
var response = await window.CrankApi.listAsyncJobs(state.workspaceId, {
|
||||
status: statusFilter.value || null,
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
});
|
||||
state.items = response.items || [];
|
||||
} catch (error) {
|
||||
state.error = error.message || tKey('async_jobs.error.load');
|
||||
} finally {
|
||||
state.loading = false;
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
refreshButton.addEventListener('click', load);
|
||||
statusFilter.addEventListener('change', load);
|
||||
document.addEventListener('workspace:changed', load);
|
||||
void load();
|
||||
});
|
||||
@@ -8,6 +8,8 @@
|
||||
apiKeys: '/api-keys',
|
||||
logs: '/logs',
|
||||
usage: '/usage',
|
||||
streamSessions: '/stream-sessions',
|
||||
asyncJobs: '/async-jobs',
|
||||
settings: '/settings',
|
||||
workspaceSetup: '/workspace-setup',
|
||||
wizard: '/wizard/'
|
||||
|
||||
@@ -1569,6 +1569,240 @@ var TRANSLATIONS = {
|
||||
}
|
||||
};
|
||||
|
||||
Object.assign(TRANSLATIONS.en, {
|
||||
'wizard.streaming.title': 'Streaming execution',
|
||||
'wizard.streaming.subtitle': 'Bounded stream, session and async job settings for MCP tool families.',
|
||||
'wizard.streaming.execution_mode': 'Execution mode',
|
||||
'wizard.streaming.transport_behavior': 'Transport behavior',
|
||||
'wizard.streaming.aggregation_mode': 'Aggregation mode',
|
||||
'wizard.streaming.window_duration': 'Window duration (ms)',
|
||||
'wizard.streaming.poll_interval': 'Poll interval (ms)',
|
||||
'wizard.streaming.upstream_timeout': 'Upstream timeout (ms)',
|
||||
'wizard.streaming.idle_timeout': 'Idle timeout (ms)',
|
||||
'wizard.streaming.session_lifetime': 'Session lifetime (ms)',
|
||||
'wizard.streaming.max_items': 'Max items',
|
||||
'wizard.streaming.max_bytes': 'Max bytes',
|
||||
'wizard.streaming.max_field_length': 'Max field length',
|
||||
'wizard.streaming.sampling_rate': 'Sampling rate',
|
||||
'wizard.streaming.items_path': 'Items path',
|
||||
'wizard.streaming.summary_path': 'Summary path',
|
||||
'wizard.streaming.done_path': 'Done path',
|
||||
'wizard.streaming.cursor_path': 'Cursor path',
|
||||
'wizard.streaming.status_path': 'Status path',
|
||||
'wizard.streaming.redacted_paths': 'Redacted paths',
|
||||
'wizard.streaming.truncate_item_fields': 'Truncate item fields',
|
||||
'wizard.streaming.drop_duplicates': 'Drop duplicates',
|
||||
'wizard.streaming.tool_family': 'Tool family',
|
||||
'wizard.streaming.start_tool_name': 'Start tool name',
|
||||
'wizard.streaming.poll_tool_name': 'Poll tool name',
|
||||
'wizard.streaming.stop_tool_name': 'Stop tool name',
|
||||
'wizard.streaming.status_tool_name': 'Status tool name',
|
||||
'wizard.streaming.result_tool_name': 'Result tool name',
|
||||
'wizard.streaming.cancel_tool_name': 'Cancel tool name',
|
||||
'wizard.streaming.mode.unary': 'Unary',
|
||||
'wizard.streaming.mode.window': 'Window',
|
||||
'wizard.streaming.mode.session': 'Session',
|
||||
'wizard.streaming.mode.async_job': 'Async job',
|
||||
'wizard.streaming.transport.request_response': 'Request / response',
|
||||
'wizard.streaming.transport.server_stream': 'Server stream',
|
||||
'wizard.streaming.transport.stateful_session': 'Stateful session',
|
||||
'wizard.streaming.transport.deferred_result': 'Deferred result',
|
||||
'wizard.streaming.aggregation.raw_items': 'Raw items',
|
||||
'wizard.streaming.aggregation.summary_only': 'Summary only',
|
||||
'wizard.streaming.aggregation.summary_plus_samples': 'Summary + samples',
|
||||
'wizard.streaming.aggregation.stats': 'Stats',
|
||||
'wizard.streaming.aggregation.latest_state': 'Latest state',
|
||||
'wizard.streaming.help.unary': 'Unary keeps the existing request-response model. Other modes create bounded stream-aware MCP tools.',
|
||||
'wizard.streaming.help.window': 'Window mode collects a bounded slice of data and returns a compact result.',
|
||||
'wizard.streaming.help.session': 'Session mode publishes start/poll/stop tool families backed by persisted stream sessions.',
|
||||
'wizard.streaming.help.async_job': 'Async job mode publishes start/status/result/cancel tools and stores runtime job state.',
|
||||
|
||||
'stream_sessions.title': 'Stream sessions',
|
||||
'stream_sessions.subtitle': 'Inspect stateful streaming sessions created by generated MCP tool families.',
|
||||
'stream_sessions.refresh': 'Refresh',
|
||||
'stream_sessions.summary': '{count} sessions',
|
||||
'stream_sessions.filter.all_statuses': 'All statuses',
|
||||
'stream_sessions.filter.all_modes': 'All modes',
|
||||
'stream_sessions.loading.title': 'Loading stream sessions…',
|
||||
'stream_sessions.loading.body': 'Fetching persisted session state for the current workspace.',
|
||||
'stream_sessions.error.title': 'Unable to load stream sessions',
|
||||
'stream_sessions.error.workspace': 'Workspace is not selected',
|
||||
'stream_sessions.error.load': 'Failed to load stream sessions',
|
||||
'stream_sessions.empty.title': 'No stream sessions found',
|
||||
'stream_sessions.empty.body': 'Session-mode MCP tools will create entries here after start/poll/stop flows.',
|
||||
'stream_sessions.operation': 'Operation: {operation}',
|
||||
'stream_sessions.meta.created': 'Created',
|
||||
'stream_sessions.meta.last_poll': 'Last poll',
|
||||
'stream_sessions.meta.expires': 'Expires',
|
||||
'stream_sessions.meta.agent': 'Agent',
|
||||
'stream_sessions.cursor': 'Cursor',
|
||||
'stream_sessions.stop': 'Stop session',
|
||||
'stream_sessions.show_details': 'Show details',
|
||||
'stream_sessions.hide_details': 'Hide details',
|
||||
'stream_sessions.toast.stop.title': 'Session stopped',
|
||||
'stream_sessions.toast.stop.body': 'The stream session was stopped.',
|
||||
'stream_sessions.toast.stop_error.title': 'Stop failed',
|
||||
'stream_sessions.toast.stop_error.body': 'Failed to stop stream session',
|
||||
'stream_sessions.status.created': 'Created',
|
||||
'stream_sessions.status.running': 'Running',
|
||||
'stream_sessions.status.failed': 'Failed',
|
||||
'stream_sessions.status.stopped': 'Stopped',
|
||||
'stream_sessions.status.expired': 'Expired',
|
||||
|
||||
'async_jobs.title': 'Async jobs',
|
||||
'async_jobs.subtitle': 'Inspect deferred jobs started by streaming MCP tool families.',
|
||||
'async_jobs.refresh': 'Refresh',
|
||||
'async_jobs.summary': '{count} jobs',
|
||||
'async_jobs.filter.all_statuses': 'All statuses',
|
||||
'async_jobs.loading.title': 'Loading async jobs…',
|
||||
'async_jobs.loading.body': 'Fetching persisted job state for the current workspace.',
|
||||
'async_jobs.error.title': 'Unable to load async jobs',
|
||||
'async_jobs.error.workspace': 'Workspace is not selected',
|
||||
'async_jobs.error.load': 'Failed to load async jobs',
|
||||
'async_jobs.empty.title': 'No async jobs found',
|
||||
'async_jobs.empty.body': 'Async job MCP tools will create entries here after start/status/result flows.',
|
||||
'async_jobs.operation': 'Operation: {operation}',
|
||||
'async_jobs.meta.created': 'Created',
|
||||
'async_jobs.meta.updated': 'Updated',
|
||||
'async_jobs.meta.finished': 'Finished',
|
||||
'async_jobs.meta.expires': 'Expires',
|
||||
'async_jobs.progress': 'Progress payload',
|
||||
'async_jobs.error_preview': 'Error preview',
|
||||
'async_jobs.result': 'Result payload',
|
||||
'async_jobs.result_loading': 'Loading result…',
|
||||
'async_jobs.result_error': 'Failed to load result',
|
||||
'async_jobs.cancel': 'Cancel job',
|
||||
'async_jobs.show_details': 'Show details',
|
||||
'async_jobs.hide_details': 'Hide details',
|
||||
'async_jobs.toast.cancel.title': 'Job cancelled',
|
||||
'async_jobs.toast.cancel.body': 'The async job was cancelled.',
|
||||
'async_jobs.toast.cancel_error.title': 'Cancel failed',
|
||||
'async_jobs.toast.cancel_error.body': 'Failed to cancel async job',
|
||||
'async_jobs.status.created': 'Created',
|
||||
'async_jobs.status.running': 'Running',
|
||||
'async_jobs.status.completed': 'Completed',
|
||||
'async_jobs.status.failed': 'Failed',
|
||||
'async_jobs.status.cancelled': 'Cancelled',
|
||||
'async_jobs.status.expired': 'Expired',
|
||||
});
|
||||
|
||||
Object.assign(TRANSLATIONS.ru, {
|
||||
'wizard.streaming.title': 'Потоковое выполнение',
|
||||
'wizard.streaming.subtitle': 'Ограниченные настройки стрима, сессий и async job для семейств MCP-инструментов.',
|
||||
'wizard.streaming.execution_mode': 'Режим выполнения',
|
||||
'wizard.streaming.transport_behavior': 'Поведение транспорта',
|
||||
'wizard.streaming.aggregation_mode': 'Режим агрегации',
|
||||
'wizard.streaming.window_duration': 'Длительность окна (мс)',
|
||||
'wizard.streaming.poll_interval': 'Интервал poll (мс)',
|
||||
'wizard.streaming.upstream_timeout': 'Таймаут upstream (мс)',
|
||||
'wizard.streaming.idle_timeout': 'Таймаут простоя (мс)',
|
||||
'wizard.streaming.session_lifetime': 'Время жизни сессии (мс)',
|
||||
'wizard.streaming.max_items': 'Максимум элементов',
|
||||
'wizard.streaming.max_bytes': 'Максимум байт',
|
||||
'wizard.streaming.max_field_length': 'Максимальная длина поля',
|
||||
'wizard.streaming.sampling_rate': 'Частота выборки',
|
||||
'wizard.streaming.items_path': 'Путь к items',
|
||||
'wizard.streaming.summary_path': 'Путь к summary',
|
||||
'wizard.streaming.done_path': 'Путь к done',
|
||||
'wizard.streaming.cursor_path': 'Путь к cursor',
|
||||
'wizard.streaming.status_path': 'Путь к status',
|
||||
'wizard.streaming.redacted_paths': 'Скрываемые пути',
|
||||
'wizard.streaming.truncate_item_fields': 'Обрезать поля элементов',
|
||||
'wizard.streaming.drop_duplicates': 'Удалять дубликаты',
|
||||
'wizard.streaming.tool_family': 'Семейство инструментов',
|
||||
'wizard.streaming.start_tool_name': 'Имя start-инструмента',
|
||||
'wizard.streaming.poll_tool_name': 'Имя poll-инструмента',
|
||||
'wizard.streaming.stop_tool_name': 'Имя stop-инструмента',
|
||||
'wizard.streaming.status_tool_name': 'Имя status-инструмента',
|
||||
'wizard.streaming.result_tool_name': 'Имя result-инструмента',
|
||||
'wizard.streaming.cancel_tool_name': 'Имя cancel-инструмента',
|
||||
'wizard.streaming.mode.unary': 'Unary',
|
||||
'wizard.streaming.mode.window': 'Window',
|
||||
'wizard.streaming.mode.session': 'Session',
|
||||
'wizard.streaming.mode.async_job': 'Async job',
|
||||
'wizard.streaming.transport.request_response': 'Запрос / ответ',
|
||||
'wizard.streaming.transport.server_stream': 'Серверный стрим',
|
||||
'wizard.streaming.transport.stateful_session': 'Состояние сессии',
|
||||
'wizard.streaming.transport.deferred_result': 'Отложенный результат',
|
||||
'wizard.streaming.aggregation.raw_items': 'Сырые элементы',
|
||||
'wizard.streaming.aggregation.summary_only': 'Только summary',
|
||||
'wizard.streaming.aggregation.summary_plus_samples': 'Summary + примеры',
|
||||
'wizard.streaming.aggregation.stats': 'Статистика',
|
||||
'wizard.streaming.aggregation.latest_state': 'Последнее состояние',
|
||||
'wizard.streaming.help.unary': 'Unary сохраняет текущую модель запрос-ответ. Остальные режимы создают ограниченные потоковые MCP-инструменты.',
|
||||
'wizard.streaming.help.window': 'Режим window собирает ограниченный срез данных и возвращает компактный результат.',
|
||||
'wizard.streaming.help.session': 'Режим session публикует семейство start/poll/stop и опирается на сохраненные stream sessions.',
|
||||
'wizard.streaming.help.async_job': 'Режим async job публикует start/status/result/cancel и хранит состояние фоновой задачи.',
|
||||
|
||||
'stream_sessions.title': 'Потоковые сессии',
|
||||
'stream_sessions.subtitle': 'Просмотр stateful streaming-сессий, созданных сгенерированными семействами MCP-инструментов.',
|
||||
'stream_sessions.refresh': 'Обновить',
|
||||
'stream_sessions.summary': '{count} сессий',
|
||||
'stream_sessions.filter.all_statuses': 'Все статусы',
|
||||
'stream_sessions.filter.all_modes': 'Все режимы',
|
||||
'stream_sessions.loading.title': 'Загрузка потоковых сессий…',
|
||||
'stream_sessions.loading.body': 'Получаем сохраненное состояние сессий для текущего workspace.',
|
||||
'stream_sessions.error.title': 'Не удалось загрузить потоковые сессии',
|
||||
'stream_sessions.error.workspace': 'Workspace не выбран',
|
||||
'stream_sessions.error.load': 'Не удалось загрузить потоковые сессии',
|
||||
'stream_sessions.empty.title': 'Потоковых сессий пока нет',
|
||||
'stream_sessions.empty.body': 'Сессии появятся здесь после вызовов start/poll/stop для session-mode инструментов.',
|
||||
'stream_sessions.operation': 'Операция: {operation}',
|
||||
'stream_sessions.meta.created': 'Создана',
|
||||
'stream_sessions.meta.last_poll': 'Последний poll',
|
||||
'stream_sessions.meta.expires': 'Истекает',
|
||||
'stream_sessions.meta.agent': 'Агент',
|
||||
'stream_sessions.cursor': 'Cursor',
|
||||
'stream_sessions.stop': 'Остановить сессию',
|
||||
'stream_sessions.show_details': 'Показать детали',
|
||||
'stream_sessions.hide_details': 'Скрыть детали',
|
||||
'stream_sessions.toast.stop.title': 'Сессия остановлена',
|
||||
'stream_sessions.toast.stop.body': 'Потоковая сессия остановлена.',
|
||||
'stream_sessions.toast.stop_error.title': 'Не удалось остановить',
|
||||
'stream_sessions.toast.stop_error.body': 'Не удалось остановить потоковую сессию',
|
||||
'stream_sessions.status.created': 'Создана',
|
||||
'stream_sessions.status.running': 'Выполняется',
|
||||
'stream_sessions.status.failed': 'Ошибка',
|
||||
'stream_sessions.status.stopped': 'Остановлена',
|
||||
'stream_sessions.status.expired': 'Истекла',
|
||||
|
||||
'async_jobs.title': 'Асинхронные задачи',
|
||||
'async_jobs.subtitle': 'Просмотр отложенных задач, запущенных потоковыми семействами MCP-инструментов.',
|
||||
'async_jobs.refresh': 'Обновить',
|
||||
'async_jobs.summary': '{count} задач',
|
||||
'async_jobs.filter.all_statuses': 'Все статусы',
|
||||
'async_jobs.loading.title': 'Загрузка async job…',
|
||||
'async_jobs.loading.body': 'Получаем сохраненное состояние задач для текущего workspace.',
|
||||
'async_jobs.error.title': 'Не удалось загрузить async job',
|
||||
'async_jobs.error.workspace': 'Workspace не выбран',
|
||||
'async_jobs.error.load': 'Не удалось загрузить async job',
|
||||
'async_jobs.empty.title': 'Асинхронных задач пока нет',
|
||||
'async_jobs.empty.body': 'Задачи появятся здесь после вызовов start/status/result для async job инструментов.',
|
||||
'async_jobs.operation': 'Операция: {operation}',
|
||||
'async_jobs.meta.created': 'Создана',
|
||||
'async_jobs.meta.updated': 'Обновлена',
|
||||
'async_jobs.meta.finished': 'Завершена',
|
||||
'async_jobs.meta.expires': 'Истекает',
|
||||
'async_jobs.progress': 'Payload прогресса',
|
||||
'async_jobs.error_preview': 'Предпросмотр ошибки',
|
||||
'async_jobs.result': 'Payload результата',
|
||||
'async_jobs.result_loading': 'Загрузка результата…',
|
||||
'async_jobs.result_error': 'Не удалось загрузить результат',
|
||||
'async_jobs.cancel': 'Отменить задачу',
|
||||
'async_jobs.show_details': 'Показать детали',
|
||||
'async_jobs.hide_details': 'Скрыть детали',
|
||||
'async_jobs.toast.cancel.title': 'Задача отменена',
|
||||
'async_jobs.toast.cancel.body': 'Асинхронная задача отменена.',
|
||||
'async_jobs.toast.cancel_error.title': 'Не удалось отменить',
|
||||
'async_jobs.toast.cancel_error.body': 'Не удалось отменить async job',
|
||||
'async_jobs.status.created': 'Создана',
|
||||
'async_jobs.status.running': 'Выполняется',
|
||||
'async_jobs.status.completed': 'Завершена',
|
||||
'async_jobs.status.failed': 'Ошибка',
|
||||
'async_jobs.status.cancelled': 'Отменена',
|
||||
'async_jobs.status.expired': 'Истекла',
|
||||
});
|
||||
|
||||
function t(key) {
|
||||
var lang = localStorage.getItem('crank_lang') || 'en';
|
||||
var tr = TRANSLATIONS[lang] || TRANSLATIONS.en;
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var state = {
|
||||
items: [],
|
||||
workspaceId: null,
|
||||
loading: false,
|
||||
error: '',
|
||||
openId: null,
|
||||
};
|
||||
|
||||
var list = document.getElementById('stream-sessions-list');
|
||||
var summary = document.getElementById('stream-sessions-summary');
|
||||
var refreshButton = document.getElementById('stream-sessions-refresh');
|
||||
var statusFilter = document.getElementById('stream-sessions-status');
|
||||
var modeFilter = document.getElementById('stream-sessions-mode');
|
||||
|
||||
function tKey(key, vars) {
|
||||
if (!window.t) return key;
|
||||
return t(key, vars);
|
||||
}
|
||||
|
||||
function currentWorkspaceId() {
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
return workspace ? workspace.id : null;
|
||||
}
|
||||
|
||||
function formatJson(value) {
|
||||
if (value === null || value === undefined) return '';
|
||||
if (typeof value === 'string') return value;
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) return '—';
|
||||
return new Date(value).toLocaleString();
|
||||
}
|
||||
|
||||
function renderEmpty(title, body) {
|
||||
list.innerHTML = '<div class="empty-state"><div class="empty-state-title">' + title + '</div><div class="empty-state-text">' + body + '</div></div>';
|
||||
}
|
||||
|
||||
function render() {
|
||||
summary.textContent = tKey('stream_sessions.summary', { count: state.items.length });
|
||||
|
||||
if (state.loading && state.items.length === 0) {
|
||||
renderEmpty(tKey('stream_sessions.loading.title'), tKey('stream_sessions.loading.body'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.error) {
|
||||
renderEmpty(tKey('stream_sessions.error.title'), state.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.items.length) {
|
||||
renderEmpty(tKey('stream_sessions.empty.title'), tKey('stream_sessions.empty.body'));
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = state.items.map(function(item) {
|
||||
var open = state.openId === item.id;
|
||||
var statusLabel = tKey('stream_sessions.status.' + item.status);
|
||||
return [
|
||||
'<div class="resource-card" data-session-id="' + item.id + '">',
|
||||
' <div class="resource-card-header">',
|
||||
' <div>',
|
||||
' <div class="resource-card-title">' + item.id + '</div>',
|
||||
' <div class="resource-card-subtitle">' + tKey('stream_sessions.operation', { operation: item.operation_id }) + '</div>',
|
||||
' <div class="resource-pill-row">',
|
||||
' <span class="resource-status-pill ' + String(item.status || '').toLowerCase() + '">' + statusLabel + '</span>',
|
||||
' <span class="resource-status-pill">' + item.mode + '</span>',
|
||||
' <span class="resource-status-pill">' + item.protocol + '</span>',
|
||||
' </div>',
|
||||
' </div>',
|
||||
' <div class="resource-card-actions">',
|
||||
item.status === 'created' || item.status === 'running'
|
||||
? ' <button class="btn-secondary" data-action="stop" data-session-id="' + item.id + '">' + tKey('stream_sessions.stop') + '</button>'
|
||||
: '',
|
||||
' <button class="btn-secondary" data-action="toggle" data-session-id="' + item.id + '">' + (open ? tKey('stream_sessions.hide_details') : tKey('stream_sessions.show_details')) + '</button>',
|
||||
' </div>',
|
||||
' </div>',
|
||||
' <div class="resource-meta-grid">',
|
||||
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('stream_sessions.meta.created') + '</div><div class="resource-meta-value">' + formatDateTime(item.created_at) + '</div></div>',
|
||||
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('stream_sessions.meta.last_poll') + '</div><div class="resource-meta-value">' + formatDateTime(item.last_poll_at) + '</div></div>',
|
||||
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('stream_sessions.meta.expires') + '</div><div class="resource-meta-value">' + formatDateTime(item.expires_at) + '</div></div>',
|
||||
' <div class="resource-meta-item"><div class="resource-meta-label">' + tKey('stream_sessions.meta.agent') + '</div><div class="resource-meta-value">' + (item.agent_id || '—') + '</div></div>',
|
||||
' </div>',
|
||||
open
|
||||
? ' <div class="resource-detail-block"><div class="resource-detail-title">' + tKey('stream_sessions.cursor') + '</div><pre class="resource-detail-pre">' + formatJson(item.cursor) + '</pre></div>'
|
||||
: '',
|
||||
'</div>'
|
||||
].join('');
|
||||
}).join('');
|
||||
|
||||
list.querySelectorAll('[data-action="toggle"]').forEach(function(button) {
|
||||
button.addEventListener('click', function () {
|
||||
var sessionId = this.getAttribute('data-session-id');
|
||||
state.openId = state.openId === sessionId ? null : sessionId;
|
||||
if (state.openId) {
|
||||
void loadDetail(sessionId);
|
||||
} else {
|
||||
render();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
list.querySelectorAll('[data-action="stop"]').forEach(function(button) {
|
||||
button.addEventListener('click', async function () {
|
||||
try {
|
||||
var sessionId = this.getAttribute('data-session-id');
|
||||
await window.CrankApi.stopStreamSession(state.workspaceId, sessionId);
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.success(tKey('stream_sessions.toast.stop.body'), tKey('stream_sessions.toast.stop.title'));
|
||||
}
|
||||
await load();
|
||||
} catch (error) {
|
||||
if (window.CrankUi) {
|
||||
window.CrankUi.error(error.message || tKey('stream_sessions.toast.stop_error.body'), tKey('stream_sessions.toast.stop_error.title'));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function loadDetail(sessionId) {
|
||||
var detail = await window.CrankApi.getStreamSession(state.workspaceId, sessionId);
|
||||
state.items = state.items.map(function(item) {
|
||||
return item.id === sessionId ? detail : item;
|
||||
});
|
||||
render();
|
||||
}
|
||||
|
||||
async function load() {
|
||||
state.workspaceId = currentWorkspaceId();
|
||||
if (!state.workspaceId) {
|
||||
state.items = [];
|
||||
state.error = tKey('stream_sessions.error.workspace');
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
state.loading = true;
|
||||
state.error = '';
|
||||
render();
|
||||
|
||||
try {
|
||||
var response = await window.CrankApi.listStreamSessions(state.workspaceId, {
|
||||
status: statusFilter.value || null,
|
||||
mode: modeFilter.value || null,
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
});
|
||||
state.items = response.items || [];
|
||||
} catch (error) {
|
||||
state.error = error.message || tKey('stream_sessions.error.load');
|
||||
} finally {
|
||||
state.loading = false;
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
refreshButton.addEventListener('click', load);
|
||||
statusFilter.addEventListener('change', load);
|
||||
modeFilter.addEventListener('change', load);
|
||||
document.addEventListener('workspace:changed', load);
|
||||
void load();
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
(function() {
|
||||
function currentWorkspaceId() {
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
return workspace ? workspace.id : null;
|
||||
}
|
||||
|
||||
async function startWindowTest(operationId, payload) {
|
||||
if (!window.CrankApi || !operationId) {
|
||||
throw new Error('Window test requires an operation id.');
|
||||
}
|
||||
return window.CrankApi.runOperationTest(currentWorkspaceId(), operationId, payload);
|
||||
}
|
||||
|
||||
async function startSessionTest(workspaceId, operationId, payload) {
|
||||
return window.CrankApi.runOperationTest(workspaceId || currentWorkspaceId(), operationId, payload);
|
||||
}
|
||||
|
||||
async function pollSessionTest(workspaceId, sessionId) {
|
||||
return window.CrankApi.getStreamSession(workspaceId || currentWorkspaceId(), sessionId);
|
||||
}
|
||||
|
||||
async function stopSessionTest(workspaceId, sessionId) {
|
||||
return window.CrankApi.stopStreamSession(workspaceId || currentWorkspaceId(), sessionId);
|
||||
}
|
||||
|
||||
async function startAsyncJobTest(workspaceId, operationId, payload) {
|
||||
return window.CrankApi.runOperationTest(workspaceId || currentWorkspaceId(), operationId, payload);
|
||||
}
|
||||
|
||||
async function refreshAsyncJobStatus(workspaceId, jobId) {
|
||||
return window.CrankApi.getAsyncJob(workspaceId || currentWorkspaceId(), jobId);
|
||||
}
|
||||
|
||||
async function loadAsyncJobResult(workspaceId, jobId) {
|
||||
return window.CrankApi.getAsyncJobResult(workspaceId || currentWorkspaceId(), jobId);
|
||||
}
|
||||
|
||||
window.CrankStreamTestRun = {
|
||||
startWindowTest: startWindowTest,
|
||||
startSessionTest: startSessionTest,
|
||||
pollSessionTest: pollSessionTest,
|
||||
stopSessionTest: stopSessionTest,
|
||||
startAsyncJobTest: startAsyncJobTest,
|
||||
refreshAsyncJobStatus: refreshAsyncJobStatus,
|
||||
loadAsyncJobResult: loadAsyncJobResult,
|
||||
};
|
||||
}());
|
||||
@@ -0,0 +1,127 @@
|
||||
(function() {
|
||||
function text(id) {
|
||||
var element = document.getElementById(id);
|
||||
return element ? String(element.value || '').trim() : '';
|
||||
}
|
||||
|
||||
function number(id) {
|
||||
var value = text(id);
|
||||
if (!value) return null;
|
||||
var parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function lines(id) {
|
||||
var value = text(id);
|
||||
return value
|
||||
? value.split('\n').map(function(item) { return item.trim(); }).filter(Boolean)
|
||||
: [];
|
||||
}
|
||||
|
||||
function checked(id) {
|
||||
var element = document.getElementById(id);
|
||||
return !!(element && element.checked);
|
||||
}
|
||||
|
||||
function selectedMode() {
|
||||
return text('streaming-mode') || 'unary';
|
||||
}
|
||||
|
||||
function serializeStreamingConfig() {
|
||||
if (selectedMode() === 'unary') {
|
||||
return null;
|
||||
}
|
||||
|
||||
var config = {
|
||||
mode: selectedMode(),
|
||||
transport_behavior: text('streaming-transport-behavior') || 'request_response',
|
||||
aggregation_mode: text('streaming-aggregation-mode') || 'summary_plus_samples',
|
||||
window_duration_ms: number('streaming-window-duration-ms'),
|
||||
poll_interval_ms: number('streaming-poll-interval-ms'),
|
||||
upstream_timeout_ms: number('streaming-upstream-timeout-ms'),
|
||||
idle_timeout_ms: number('streaming-idle-timeout-ms'),
|
||||
max_session_lifetime_ms: number('streaming-session-lifetime-ms'),
|
||||
max_items: number('streaming-max-items'),
|
||||
max_bytes: number('streaming-max-bytes'),
|
||||
max_field_length: number('streaming-max-field-length'),
|
||||
sampling_rate: number('streaming-sampling-rate'),
|
||||
items_path: text('streaming-items-path') || null,
|
||||
summary_path: text('streaming-summary-path') || null,
|
||||
done_path: text('streaming-done-path') || null,
|
||||
cursor_path: text('streaming-cursor-path') || null,
|
||||
status_path: text('streaming-status-path') || null,
|
||||
redacted_paths: lines('streaming-redacted-paths'),
|
||||
truncate_item_fields: checked('streaming-truncate-item-fields'),
|
||||
drop_duplicates: checked('streaming-drop-duplicates'),
|
||||
tool_family: {},
|
||||
};
|
||||
|
||||
if (config.mode === 'session') {
|
||||
config.tool_family.start_tool_name = text('streaming-start-tool-name') || null;
|
||||
config.tool_family.poll_tool_name = text('streaming-poll-tool-name') || null;
|
||||
config.tool_family.stop_tool_name = text('streaming-stop-tool-name') || null;
|
||||
} else if (config.mode === 'async_job') {
|
||||
config.tool_family.start_tool_name = text('streaming-start-tool-name') || null;
|
||||
config.tool_family.status_tool_name = text('streaming-status-tool-name') || null;
|
||||
config.tool_family.result_tool_name = text('streaming-result-tool-name') || null;
|
||||
config.tool_family.cancel_tool_name = text('streaming-cancel-tool-name') || null;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
function validateStreamingConfig(capabilities) {
|
||||
var config = serializeStreamingConfig();
|
||||
if (!config) {
|
||||
return { valid: true, errors: [] };
|
||||
}
|
||||
|
||||
var errors = [];
|
||||
var capability = (capabilities || []).find(function(item) {
|
||||
return item.protocol === window.wizardProtocol;
|
||||
});
|
||||
|
||||
if (capability && Array.isArray(capability.supports_execution_modes)) {
|
||||
var supported = capability.supports_execution_modes.map(String);
|
||||
if (supported.indexOf(config.mode) === -1) {
|
||||
errors.push('Selected execution mode is not supported by the current protocol.');
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.max_items || config.max_items <= 0) {
|
||||
errors.push('Max items must be a positive integer.');
|
||||
}
|
||||
|
||||
if (!config.max_bytes || config.max_bytes <= 0) {
|
||||
errors.push('Max bytes must be a positive integer.');
|
||||
}
|
||||
|
||||
if (config.mode === 'window' && (!config.window_duration_ms || config.window_duration_ms <= 0)) {
|
||||
errors.push('Window duration must be set for window mode.');
|
||||
}
|
||||
|
||||
if (config.mode === 'session' && (!config.poll_interval_ms || config.poll_interval_ms <= 0)) {
|
||||
errors.push('Poll interval must be set for session mode.');
|
||||
}
|
||||
|
||||
if (config.mode === 'async_job' && (!config.max_session_lifetime_ms || config.max_session_lifetime_ms <= 0)) {
|
||||
errors.push('Session lifetime must be set for async job mode.');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors: errors,
|
||||
config: config,
|
||||
};
|
||||
}
|
||||
|
||||
window.CrankStreamingForm = {
|
||||
serializeStreamingConfig: serializeStreamingConfig,
|
||||
deserializeStreamingConfig: function(streaming) {
|
||||
if (typeof window.applyStreamingConfig === 'function') {
|
||||
window.applyStreamingConfig(streaming);
|
||||
}
|
||||
},
|
||||
validateStreamingConfig: validateStreamingConfig,
|
||||
};
|
||||
}());
|
||||
@@ -10,6 +10,7 @@ var wizardProtoUpload = null;
|
||||
var wizardDescriptorSetUpload = null;
|
||||
var wizardTestResponsePreview = null;
|
||||
var grpcDescriptorServices = [];
|
||||
var wizardProtocolCapabilities = null;
|
||||
function tKey(key) {
|
||||
return typeof t === 'function' ? t(key) : key;
|
||||
}
|
||||
@@ -17,6 +18,48 @@ function tfKey(key, vars) {
|
||||
return typeof tf === 'function' ? tf(key, vars) : key;
|
||||
}
|
||||
|
||||
function defaultProtocolCapabilities() {
|
||||
return {
|
||||
rest: {
|
||||
supports_execution_modes: ['unary', 'window', 'session', 'async_job'],
|
||||
supports_transport_behaviors: ['request_response', 'server_stream', 'deferred_result'],
|
||||
},
|
||||
graphql: {
|
||||
supports_execution_modes: ['unary'],
|
||||
supports_transport_behaviors: ['request_response'],
|
||||
},
|
||||
grpc: {
|
||||
supports_execution_modes: ['unary', 'window', 'session', 'async_job'],
|
||||
supports_transport_behaviors: ['request_response', 'server_stream'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function currentProtocolCapabilities() {
|
||||
var capabilities = wizardProtocolCapabilities || defaultProtocolCapabilities();
|
||||
return capabilities[wizardProtocol] || capabilities.rest;
|
||||
}
|
||||
|
||||
async function loadProtocolCapabilities() {
|
||||
if (!wizardWorkspaceId || !window.CrankApi || typeof window.CrankApi.getProtocolCapabilities !== 'function') {
|
||||
wizardProtocolCapabilities = defaultProtocolCapabilities();
|
||||
return wizardProtocolCapabilities;
|
||||
}
|
||||
|
||||
try {
|
||||
var response = await window.CrankApi.getProtocolCapabilities(wizardWorkspaceId);
|
||||
var mapped = {};
|
||||
(response.items || []).forEach(function(item) {
|
||||
mapped[item.protocol] = item;
|
||||
});
|
||||
wizardProtocolCapabilities = mapped;
|
||||
} catch (_error) {
|
||||
wizardProtocolCapabilities = defaultProtocolCapabilities();
|
||||
}
|
||||
|
||||
return wizardProtocolCapabilities;
|
||||
}
|
||||
|
||||
/* ── Dynamic step loading ── */
|
||||
function _stepFile(n) {
|
||||
return (n === 3) ? 'step3-' + wizardProtocol + '.html' : 'step' + n + '.html';
|
||||
@@ -222,10 +265,12 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
|
||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||
wizardWorkspaceId = workspace ? workspace.id : null;
|
||||
await loadProtocolCapabilities();
|
||||
|
||||
await loadWizardPanels([1, 2, 3, 4, 5]);
|
||||
bindProtocolCards();
|
||||
bindWizardLiveActions();
|
||||
bindStreamingConfigControls();
|
||||
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
if (params.get('mode') === 'edit' && params.get('operationId')) {
|
||||
@@ -1092,6 +1137,12 @@ function buildMappingSet(rawValue, mode) {
|
||||
}
|
||||
|
||||
function parseExecutionConfig(text) {
|
||||
if (window.CrankStreamingForm && typeof window.CrankStreamingForm.validateStreamingConfig === 'function') {
|
||||
var validation = window.CrankStreamingForm.validateStreamingConfig([currentProtocolCapabilities()]);
|
||||
if (!validation.valid) {
|
||||
throw new Error(validation.errors[0]);
|
||||
}
|
||||
}
|
||||
var value = parseStructuredText(text);
|
||||
var retry = value.retry || value.retry_policy || null;
|
||||
var headers = value.headers && typeof value.headers === 'object' ? value.headers : {};
|
||||
@@ -1113,6 +1164,8 @@ function parseExecutionConfig(text) {
|
||||
config.protocol_options = { grpc: { use_tls: useTls } };
|
||||
}
|
||||
|
||||
config.streaming = collectStreamingConfig();
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -1236,6 +1289,264 @@ function parseHeaderMap(text) {
|
||||
return value && typeof value === 'object' ? value : {};
|
||||
}
|
||||
|
||||
function numericValueOrNull(id) {
|
||||
var value = textValue(id);
|
||||
if (!value) return null;
|
||||
var parsed = Number(value);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
||||
}
|
||||
|
||||
function stringValueOrNull(id) {
|
||||
var value = textValue(id);
|
||||
return value ? value : null;
|
||||
}
|
||||
|
||||
function textareaLines(id) {
|
||||
var value = textValue(id);
|
||||
if (!value) return [];
|
||||
return value
|
||||
.split(/\r?\n/)
|
||||
.map(function(line) { return line.trim(); })
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function selectedStreamingMode() {
|
||||
var element = document.getElementById('streaming-mode');
|
||||
return element ? element.value : 'unary';
|
||||
}
|
||||
|
||||
function transportOptionsForMode(mode) {
|
||||
var capabilities = currentProtocolCapabilities();
|
||||
var behaviors = (capabilities.supports_transport_behaviors || []).slice();
|
||||
if (mode === 'unary') return behaviors.filter(function(value) { return value === 'request_response'; });
|
||||
if (mode === 'window') return behaviors.filter(function(value) {
|
||||
return value === 'request_response' || value === 'server_stream';
|
||||
});
|
||||
if (mode === 'session') return behaviors.filter(function(value) {
|
||||
return value === 'server_stream' || value === 'stateful_session';
|
||||
});
|
||||
if (mode === 'async_job') return behaviors.filter(function(value) {
|
||||
return value === 'deferred_result';
|
||||
});
|
||||
return behaviors;
|
||||
}
|
||||
|
||||
function modeOptionsForProtocol() {
|
||||
var capabilities = currentProtocolCapabilities();
|
||||
return (capabilities.supports_execution_modes || ['unary']).filter(function(mode) {
|
||||
return transportOptionsForMode(mode).length > 0 || mode === 'unary';
|
||||
});
|
||||
}
|
||||
|
||||
function updateStreamingTransportOptions(preferredValue) {
|
||||
var element = document.getElementById('streaming-transport-behavior');
|
||||
if (!element) return;
|
||||
var options = transportOptionsForMode(selectedStreamingMode());
|
||||
element.innerHTML = '';
|
||||
options.forEach(function(value) {
|
||||
var option = document.createElement('option');
|
||||
option.value = value;
|
||||
option.textContent = tKey('wizard.streaming.transport.' + value);
|
||||
element.appendChild(option);
|
||||
});
|
||||
if (preferredValue && options.indexOf(preferredValue) >= 0) {
|
||||
element.value = preferredValue;
|
||||
} else if (options.length) {
|
||||
element.value = options[0];
|
||||
}
|
||||
}
|
||||
|
||||
function updateStreamingModeOptions(preferredValue) {
|
||||
var element = document.getElementById('streaming-mode');
|
||||
if (!element) return;
|
||||
var options = modeOptionsForProtocol();
|
||||
element.innerHTML = '';
|
||||
options.forEach(function(value) {
|
||||
var option = document.createElement('option');
|
||||
option.value = value;
|
||||
option.textContent = tKey('wizard.streaming.mode.' + value);
|
||||
element.appendChild(option);
|
||||
});
|
||||
if (preferredValue && options.indexOf(preferredValue) >= 0) {
|
||||
element.value = preferredValue;
|
||||
} else {
|
||||
element.value = options.indexOf('unary') >= 0 ? 'unary' : (options[0] || 'unary');
|
||||
}
|
||||
updateStreamingTransportOptions();
|
||||
}
|
||||
|
||||
function ensureStreamingToolFamilyDefaults(mode) {
|
||||
var base = textValue('tool-name') || 'stream_tool';
|
||||
if (mode === 'session') {
|
||||
if (!textValue('streaming-start-tool-name')) setValue('streaming-start-tool-name', base + '_start');
|
||||
if (!textValue('streaming-poll-tool-name')) setValue('streaming-poll-tool-name', base + '_poll');
|
||||
if (!textValue('streaming-stop-tool-name')) setValue('streaming-stop-tool-name', base + '_stop');
|
||||
}
|
||||
if (mode === 'async_job') {
|
||||
if (!textValue('streaming-start-tool-name')) setValue('streaming-start-tool-name', base + '_start');
|
||||
if (!textValue('streaming-status-tool-name')) setValue('streaming-status-tool-name', base + '_status');
|
||||
if (!textValue('streaming-result-tool-name')) setValue('streaming-result-tool-name', base + '_result');
|
||||
if (!textValue('streaming-cancel-tool-name')) setValue('streaming-cancel-tool-name', base + '_cancel');
|
||||
}
|
||||
}
|
||||
|
||||
function updateStreamingConfigVisibility() {
|
||||
var mode = selectedStreamingMode();
|
||||
var card = document.getElementById('wizard-streaming-config-card');
|
||||
var help = document.getElementById('streaming-mode-help');
|
||||
var toolFamily = document.getElementById('streaming-tool-family-block');
|
||||
var asyncRow = document.getElementById('streaming-async-tool-family-row');
|
||||
if (!card) return;
|
||||
|
||||
var isUnary = mode === 'unary';
|
||||
var isWindow = mode === 'window';
|
||||
var isSession = mode === 'session';
|
||||
var isAsyncJob = mode === 'async_job';
|
||||
|
||||
[
|
||||
'streaming-window-duration-ms',
|
||||
'streaming-poll-interval-ms',
|
||||
'streaming-upstream-timeout-ms',
|
||||
'streaming-idle-timeout-ms',
|
||||
'streaming-session-lifetime-ms',
|
||||
'streaming-max-items',
|
||||
'streaming-max-bytes',
|
||||
'streaming-max-field-length',
|
||||
'streaming-sampling-rate',
|
||||
'streaming-items-path',
|
||||
'streaming-summary-path',
|
||||
'streaming-done-path',
|
||||
'streaming-cursor-path',
|
||||
'streaming-status-path',
|
||||
'streaming-redacted-paths',
|
||||
'streaming-truncate-item-fields',
|
||||
'streaming-drop-duplicates'
|
||||
].forEach(function(id) {
|
||||
var node = document.getElementById(id);
|
||||
if (!node) return;
|
||||
var group = node.closest('.form-group') || node.closest('.checkbox-pill');
|
||||
if (group) {
|
||||
group.style.display = isUnary ? 'none' : '';
|
||||
}
|
||||
});
|
||||
|
||||
if (help) {
|
||||
help.textContent = isUnary
|
||||
? tKey('wizard.streaming.help.unary')
|
||||
: (isSession
|
||||
? tKey('wizard.streaming.help.session')
|
||||
: (isAsyncJob
|
||||
? tKey('wizard.streaming.help.async_job')
|
||||
: tKey('wizard.streaming.help.window')));
|
||||
}
|
||||
|
||||
if (toolFamily) toolFamily.style.display = (isSession || isAsyncJob) ? '' : 'none';
|
||||
if (asyncRow) asyncRow.style.display = isAsyncJob ? '' : 'none';
|
||||
|
||||
ensureStreamingToolFamilyDefaults(mode);
|
||||
}
|
||||
|
||||
function bindStreamingConfigControls() {
|
||||
updateStreamingModeOptions();
|
||||
updateStreamingConfigVisibility();
|
||||
|
||||
var mode = document.getElementById('streaming-mode');
|
||||
if (mode) {
|
||||
mode.addEventListener('change', function() {
|
||||
updateStreamingTransportOptions();
|
||||
updateStreamingConfigVisibility();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function collectStreamingConfig() {
|
||||
var mode = selectedStreamingMode();
|
||||
if (mode === 'unary') return null;
|
||||
|
||||
var config = {
|
||||
mode: mode,
|
||||
transport_behavior: stringValueOrNull('streaming-transport-behavior') || 'request_response',
|
||||
window_duration_ms: numericValueOrNull('streaming-window-duration-ms'),
|
||||
poll_interval_ms: numericValueOrNull('streaming-poll-interval-ms'),
|
||||
upstream_timeout_ms: numericValueOrNull('streaming-upstream-timeout-ms'),
|
||||
idle_timeout_ms: numericValueOrNull('streaming-idle-timeout-ms'),
|
||||
max_session_lifetime_ms: numericValueOrNull('streaming-session-lifetime-ms'),
|
||||
max_items: numericValueOrNull('streaming-max-items'),
|
||||
max_bytes: numericValueOrNull('streaming-max-bytes'),
|
||||
aggregation_mode: stringValueOrNull('streaming-aggregation-mode') || 'summary_plus_samples',
|
||||
summary_path: stringValueOrNull('streaming-summary-path'),
|
||||
items_path: stringValueOrNull('streaming-items-path'),
|
||||
cursor_path: stringValueOrNull('streaming-cursor-path'),
|
||||
status_path: stringValueOrNull('streaming-status-path'),
|
||||
done_path: stringValueOrNull('streaming-done-path'),
|
||||
redacted_paths: textareaLines('streaming-redacted-paths'),
|
||||
truncate_item_fields: !!document.getElementById('streaming-truncate-item-fields').checked,
|
||||
max_field_length: numericValueOrNull('streaming-max-field-length'),
|
||||
drop_duplicates: !!document.getElementById('streaming-drop-duplicates').checked,
|
||||
sampling_rate: (function() {
|
||||
var value = textValue('streaming-sampling-rate');
|
||||
if (!value) return null;
|
||||
var parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}()),
|
||||
tool_family: {},
|
||||
};
|
||||
|
||||
if (mode === 'session') {
|
||||
config.tool_family.start_tool_name = stringValueOrNull('streaming-start-tool-name');
|
||||
config.tool_family.poll_tool_name = stringValueOrNull('streaming-poll-tool-name');
|
||||
config.tool_family.stop_tool_name = stringValueOrNull('streaming-stop-tool-name');
|
||||
} else if (mode === 'async_job') {
|
||||
config.tool_family.start_tool_name = stringValueOrNull('streaming-start-tool-name');
|
||||
config.tool_family.status_tool_name = stringValueOrNull('streaming-status-tool-name');
|
||||
config.tool_family.result_tool_name = stringValueOrNull('streaming-result-tool-name');
|
||||
config.tool_family.cancel_tool_name = stringValueOrNull('streaming-cancel-tool-name');
|
||||
}
|
||||
|
||||
Object.keys(config).forEach(function(key) {
|
||||
if (config[key] === null) delete config[key];
|
||||
});
|
||||
if (!config.redacted_paths.length) delete config.redacted_paths;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
function applyStreamingConfig(streaming) {
|
||||
updateStreamingModeOptions(streaming && streaming.mode ? streaming.mode : 'unary');
|
||||
if (!streaming) {
|
||||
updateStreamingConfigVisibility();
|
||||
return;
|
||||
}
|
||||
|
||||
setValue('streaming-window-duration-ms', streaming.window_duration_ms || '');
|
||||
setValue('streaming-poll-interval-ms', streaming.poll_interval_ms || '');
|
||||
setValue('streaming-upstream-timeout-ms', streaming.upstream_timeout_ms || '');
|
||||
setValue('streaming-idle-timeout-ms', streaming.idle_timeout_ms || '');
|
||||
setValue('streaming-session-lifetime-ms', streaming.max_session_lifetime_ms || '');
|
||||
setValue('streaming-max-items', streaming.max_items || '');
|
||||
setValue('streaming-max-bytes', streaming.max_bytes || '');
|
||||
setValue('streaming-max-field-length', streaming.max_field_length || '');
|
||||
setValue('streaming-sampling-rate', streaming.sampling_rate || '');
|
||||
setValue('streaming-items-path', streaming.items_path || '');
|
||||
setValue('streaming-summary-path', streaming.summary_path || '');
|
||||
setValue('streaming-done-path', streaming.done_path || '');
|
||||
setValue('streaming-cursor-path', streaming.cursor_path || '');
|
||||
setValue('streaming-status-path', streaming.status_path || '');
|
||||
setValue('streaming-redacted-paths', (streaming.redacted_paths || []).join('\n'));
|
||||
document.getElementById('streaming-truncate-item-fields').checked = !!streaming.truncate_item_fields;
|
||||
document.getElementById('streaming-drop-duplicates').checked = !!streaming.drop_duplicates;
|
||||
updateStreamingTransportOptions(streaming.transport_behavior);
|
||||
setValue('streaming-start-tool-name', streaming.tool_family && streaming.tool_family.start_tool_name ? streaming.tool_family.start_tool_name : '');
|
||||
setValue('streaming-poll-tool-name', streaming.tool_family && streaming.tool_family.poll_tool_name ? streaming.tool_family.poll_tool_name : '');
|
||||
setValue('streaming-stop-tool-name', streaming.tool_family && streaming.tool_family.stop_tool_name ? streaming.tool_family.stop_tool_name : '');
|
||||
setValue('streaming-status-tool-name', streaming.tool_family && streaming.tool_family.status_tool_name ? streaming.tool_family.status_tool_name : '');
|
||||
setValue('streaming-result-tool-name', streaming.tool_family && streaming.tool_family.result_tool_name ? streaming.tool_family.result_tool_name : '');
|
||||
setValue('streaming-cancel-tool-name', streaming.tool_family && streaming.tool_family.cancel_tool_name ? streaming.tool_family.cancel_tool_name : '');
|
||||
var aggregation = document.getElementById('streaming-aggregation-mode');
|
||||
if (aggregation && streaming.aggregation_mode) aggregation.value = streaming.aggregation_mode;
|
||||
updateStreamingConfigVisibility();
|
||||
}
|
||||
|
||||
function buildToolDescription() {
|
||||
return {
|
||||
title: textValue('tool-title') || textValue('tool-display-name') || textValue('tool-name'),
|
||||
@@ -1439,6 +1750,7 @@ function prefillWizardFromEdit(detail, versionDocument) {
|
||||
setValue('tool-input-mapping', mappingSetToEditorValue(versionDocument.input_mapping, 'input', detail.protocol));
|
||||
setValue('tool-output-mapping', mappingSetToEditorValue(versionDocument.output_mapping, 'output', detail.protocol));
|
||||
setValue('tool-exec-config', executionConfigToEditorValue(versionDocument.execution_config));
|
||||
applyStreamingConfig(versionDocument.execution_config ? versionDocument.execution_config.streaming : null);
|
||||
|
||||
var toolNameInput = document.getElementById('tool-name');
|
||||
if (toolNameInput) toolNameInput.disabled = true;
|
||||
@@ -1525,6 +1837,8 @@ async function runWizardLiveAction(button, busyLabel, handler) {
|
||||
function updateWizardProtocolVisibility() {
|
||||
var grpcTools = document.getElementById('wizard-grpc-live-tools');
|
||||
if (grpcTools) grpcTools.style.display = wizardProtocol === 'grpc' ? '' : 'none';
|
||||
updateStreamingModeOptions();
|
||||
updateStreamingConfigVisibility();
|
||||
}
|
||||
|
||||
function showWizardLiveStatus(title, text, isError) {
|
||||
|
||||
Reference in New Issue
Block a user