harnessed_reasoning/wwwroot/reasoning_console.ui
yumoqing c7b14f17dd fix: update WebSocket connection logic for better session handling
- Modified connect() function to pass session as URL parameter
- Fallback to cookie-based auth if session not available
- Improves compatibility with httponly cookie settings
2026-05-17 00:07:04 +08:00

162 lines
11 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"widgettype": "VBox",
"options": {
"width": "100%",
"height": "100%",
"spacing": 12
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"width": "100%",
"padding": "16px 20px",
"spacing": 12
},
"subwidgets": [
{
"widgettype": "Title2",
"options": {
"text": "AI 推理控制台",
"halign": "left"
}
},
{
"widgettype": "Text",
"id": "ws_status",
"options": {
"text": "未连接",
"halign": "right"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"width": "calc(100% - 40px)",
"margin": "0 20px",
"padding": "16px",
"spacing": 12
},
"subwidgets": [
{
"widgettype": "Form",
"id": "input_form",
"options": {
"layout": "vertical",
"fields": [
{
"name": "user_input",
"label": "输入请求",
"uitype": "text",
"height": "120px",
"placeholder": "输入你的请求AI 将自动推理、规划并执行..."
}
]
}
},
{
"widgettype": "HBox",
"options": {
"width": "100%",
"spacing": 10
},
"subwidgets": [
{
"widgettype": "Button",
"id": "start_btn",
"options": {
"label": "开始推理",
"variant": "primary",
"bgcolor": "#2196F3"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "script",
"script": "startReasoning()"
}
]
},
{
"widgettype": "Button",
"id": "clear_btn",
"options": {
"label": "清空日志",
"variant": "secondary",
"bgcolor": "#607D8B"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "script",
"script": "clearSteps()"
}
]
}
]
}
]
},
{
"widgettype": "VBox",
"id": "status_bar",
"options": {
"width": "calc(100% - 40px)",
"margin": "0 20px",
"padding": "12px 16px",
"spacing": 4
},
"subwidgets": [
{
"widgettype": "Text",
"id": "status_text",
"options": {
"text": "等待连接...",
"halign": "left"
}
},
{
"widgettype": "Text",
"id": "current_step",
"options": {
"text": "",
"halign": "left"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"width": "100%",
"height": "400px",
"style": "overflow-y: auto;"
},
"subwidgets": [
{
"widgettype": "VBox",
"id": "steps_container",
"options": {
"width": "calc(100% - 40px)",
"margin": "0 20px",
"spacing": 8,
"padding": "8px 0"
},
"subwidgets": []
}
]
},
{
"widgettype": "Html",
"id": "ws_logic",
"options": {
"html": "<script>\n(function() {\n var userId = '{{ get_user() }}';\n var wsPath = '{{ entire_url('/wss/harnessed_reasoning/reasoning_console.wss') }}';\n var ws = null;\n var connected = false;\n var stepCount = 0;\n\n function updateStatus(state, text) {\n var statusWidget = bricks.app.find_widget_by_id('status_text');\n var badgeWidget = bricks.app.find_widget_by_id('ws_status');\n if (statusWidget) statusWidget.set_text(text);\n if (badgeWidget) {\n var labels = {connected: '已连接', connecting: '连接中', disconnected: '未连接'};\n badgeWidget.set_text(labels[state] || text);\n }\n }\n\n function handleMessage(msg) {\n if (msg.type === 'connected') {\n updateStatus('connected', '已连接');\n return;\n }\n if (msg.type === 'pong') return;\n if (msg.type === 'error') {\n addStep('error', msg.data ? msg.data.message : msg.message || '错误', msg.data || {});\n return;\n }\n var event = msg.event || msg.type;\n var data = msg.data || {};\n var message = data.message || event;\n addStep(event, message, data);\n }\n\n function addStep(event, message, data) {\n var container = bricks.app.find_widget_by_id('steps_container');\n if (!container) return;\n stepCount++;\n\n var typeClass = 'info', typeLabel = event;\n if (event.indexOf('context') >= 0) { typeClass = 'context'; typeLabel = '上下文'; }\n else if (event.indexOf('plan') >= 0 || event.indexOf('thinking') >= 0) { typeClass = 'plan'; typeLabel = '思考'; }\n else if (event.indexOf('tool_call') >= 0) { typeClass = 'tool'; typeLabel = data.success === false ? '工具失败' : '工具调用'; }\n else if (event.indexOf('step_') >= 0) { typeClass = 'execute'; typeLabel = '执行步骤'; }\n else if (event.indexOf('complete') >= 0) { typeClass = 'result'; typeLabel = '完成'; }\n else if (event.indexOf('error') >= 0) { typeClass = 'error'; typeLabel = '错误'; }\n else if (event.indexOf('safety') >= 0) { typeClass = 'plan'; typeLabel = '安全检查'; }\n\n var now = new Date();\n var timeStr = now.getHours().toString().padStart(2,'0') + ':' + now.getMinutes().toString().padStart(2,'0') + ':' + now.getSeconds().toString().padStart(2,'0');\n\n var detailsHtml = '';\n if (data.request) detailsHtml += '<div style=\"margin-top:6px;padding:6px 10px;background:#0f1923;border-radius:3px;font-size:12px;color:#78909c;word-break:break-all;\">请求: ' + escapeHtml(data.request) + '</div>';\n if (data.analysis) detailsHtml += '<div style=\"margin-top:6px;padding:6px 10px;background:#0f1923;border-radius:3px;font-size:12px;color:#78909c;word-break:break-all;\">分析: ' + escapeHtml(data.analysis) + '</div>';\n if (data.tool) detailsHtml += '<div style=\"margin-top:6px;padding:6px 10px;background:#0f1923;border-radius:3px;font-size:12px;color:#78909c;word-break:break-all;\">工具: ' + data.tool + (data.parameters ? ' 参数: ' + JSON.stringify(data.parameters) : '') + '</div>';\n if (data.result) detailsHtml += '<div style=\"margin-top:6px;padding:6px 10px;background:#0f1923;border-radius:3px;font-size:12px;color:#78909c;word-break:break-all;max-height:100px;overflow-y:auto;\">结果: ' + escapeHtml(String(data.result).substring(0, 500)) + '</div>';\n\n var borderColor = typeClass === 'result' ? '#4caf50' : typeClass === 'error' ? '#f44336' : typeClass === 'tool' ? '#ce93d8' : typeClass === 'execute' ? '#ffcc80' : typeClass === 'plan' ? '#90caf9' : '#2196F3';\n var bgColor = typeClass === 'result' ? '#4caf50' : typeClass === 'error' ? '#f44336' : typeClass === 'tool' ? '#7b1fa2' : typeClass === 'execute' ? '#e65100' : typeClass === 'plan' ? '#0d47a1' : '#1565c0';\n var labelBg = typeClass === 'context' ? '#1b5e20' : typeClass === 'plan' ? '#0d47a1' : typeClass === 'tool' ? '#4a148c' : typeClass === 'execute' ? '#e65100' : typeClass === 'result' ? '#1b5e20' : typeClass === 'error' ? '#b71c1c' : '#1565c0';\n var labelColor = typeClass === 'context' ? '#a5d6a7' : typeClass === 'plan' ? '#90caf9' : typeClass === 'tool' ? '#ce93d8' : typeClass === 'execute' ? '#ffcc80' : typeClass === 'result' ? '#a5d6a7' : typeClass === 'error' ? '#ef9a9a' : '#bbdefb';\n\n var stepHtml = '<div style=\"position:relative;margin-bottom:10px;padding-left:20px;\">' +\n '<div style=\"position:absolute;left:4px;top:10px;width:10px;height:10px;border-radius:50%;border:2px solid ' + borderColor + ';background:' + bgColor + '\"></div>' +\n '<div style=\"background:#1a2733;border:1px solid #2d4a5e;border-radius:6px;padding:10px 14px;\">' +\n '<div style=\"display:flex;align-items:center;justify-content:space-between;margin-bottom:4px;\">' +\n '<span style=\"font-size:10px;padding:2px 6px;border-radius:3px;font-weight:600;text-transform:uppercase;background:' + labelBg + ';color:' + labelColor + '\">' + typeLabel + '</span>' +\n '<span style=\"font-size:10px;color:#546e7a;\">' + timeStr + '</span></div>' +\n '<div style=\"font-size:13px;color:#b0bec5;line-height:1.5;\">' + escapeHtml(message) + '</div>' +\n detailsHtml + '</div></div>';\n\n container.el.insertAdjacentHTML('beforeend', stepHtml);\n var currentWidget = bricks.app.find_widget_by_id('current_step');\n if (currentWidget) currentWidget.set_text(message);\n container.el.scrollTop = container.el.scrollHeight;\n }\n\n function escapeHtml(text) {\n var div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n }\n\n window.startReasoning = function() {\n if (!connected) {\n alert('WebSocket 未连接');\n return;\n }\n var form = bricks.app.find_widget_by_id('input_form');\n if (!form) return;\n var inputVal = form.get_value('user_input');\n if (!inputVal || !inputVal.trim()) return;\n \n if (ws && ws.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({cmd: 'start_reasoning', request: inputVal.trim(), user_id: userId}));\n }\n };\n\n window.clearSteps = function() {\n var container = bricks.app.find_widget_by_id('steps_container');\n if (container) container.el.innerHTML = '';\n stepCount = 0;\n var currentWidget = bricks.app.find_widget_by_id('current_step');\n if (currentWidget) currentWidget.set_text('');\n };\n\n function connect() {\n if (ws && ws.readyState === WebSocket.OPEN) return;\n\n var url = wsPath;\n if (url.indexOf('http') !== 0) {\n var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n url = protocol + '//' + window.location.host + url;\n }\n\n // 修复使用URL参数传递session而不是作为WebSocket协议\n // 浏览器会自动携带当前域的cookie\n var session = '';\n try {\n if (window.bricks && window.bricks.app && window.bricks.app.get_session) {\n session = window.bricks.app.get_session();\n }\n } catch(e) { }\n\n try {\n // 如果session存在将其作为URL参数传递\n var wsUrl = session ? url + '?session=' + encodeURIComponent(session) : url;\n ws = new WebSocket(wsUrl);\n\n ws.onopen = function() {\n console.log('WS Open');\n connected = true;\n updateStatus('connected', '已连接');\n ws.send(JSON.stringify({cmd: 'connect', user_id: userId}));\n };\n\n ws.onmessage = function(e) {\n try {\n var msg = JSON.parse(e.data);\n handleMessage(msg);\n } catch(err) { console.error('Parse error', err); }\n };\n\n ws.onerror = function(e) {\n console.error('WS Error', e);\n connected = false;\n updateStatus('disconnected', '连接错误');\n };\n\n ws.onclose = function() {\n console.log('WS Close');\n connected = false;\n updateStatus('disconnected', '已断开');\n setTimeout(connect, 3000);\n };\n } catch(e) {\n console.error('Connect failed', e);\n }\n }\n\n setTimeout(connect, 500);\n})();\n</script>"
}
}
]
}