protocol-proxy 2.8.2 → 2.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/converters/openai-to-anthropic.js +4 -0
- package/lib/proxy-server.js +2 -0
- package/package.json +1 -1
- package/public/app.js +382 -0
- package/public/index.html +42 -0
- package/public/style.css +326 -0
- package/server.js +1838 -1226
|
@@ -303,6 +303,10 @@ function processLine(line, state, targetModel) {
|
|
|
303
303
|
return prefix + encodeOpenAIChunk(state.messageId, targetModel, { content: delta.text });
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
if (delta.type === 'thinking_delta' && delta.thinking) {
|
|
307
|
+
return prefix + encodeOpenAIChunk(state.messageId, targetModel, { reasoning_content: delta.thinking });
|
|
308
|
+
}
|
|
309
|
+
|
|
306
310
|
if (delta.type === 'input_json_delta' && delta.partial_json !== undefined) {
|
|
307
311
|
if (!state.sentToolInit) {
|
|
308
312
|
state.sentToolInit = true;
|
package/lib/proxy-server.js
CHANGED
|
@@ -595,6 +595,8 @@ function createProxyApp(proxyConfigOrGetter) {
|
|
|
595
595
|
const flushed = sseConverter.flush();
|
|
596
596
|
if (flushed) res.write(flushed);
|
|
597
597
|
}
|
|
598
|
+
|
|
599
|
+
res.end();
|
|
598
600
|
} catch (err) {
|
|
599
601
|
recordFailure(proxyId, candidate.providerId);
|
|
600
602
|
logger.error(`[${requestId}] Stream error:`, err.message);
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -17,6 +17,9 @@ let currentPage = 'dashboard';
|
|
|
17
17
|
let providerPoolItems = [];
|
|
18
18
|
let providerModelTags = [];
|
|
19
19
|
let providerKeys = [];
|
|
20
|
+
let assistantMessages = [];
|
|
21
|
+
let assistantProxyId = '';
|
|
22
|
+
let assistantAbortController = null;
|
|
20
23
|
|
|
21
24
|
// ---------- Theme ----------
|
|
22
25
|
const THEMES = [
|
|
@@ -80,6 +83,7 @@ function navigateTo(page) {
|
|
|
80
83
|
stats: '\u7528\u91cf\u7edf\u8ba1',
|
|
81
84
|
'request-logs': '\u8bf7\u6c42\u65e5\u5fd7',
|
|
82
85
|
'system-logs': '\u7cfb\u7edf\u65e5\u5fd7',
|
|
86
|
+
assistant: '\u667a\u63a7\u52a9\u624b',
|
|
83
87
|
settings: '\u8bbe\u7f6e',
|
|
84
88
|
};
|
|
85
89
|
document.getElementById('page-title').textContent = titles[page] || page;
|
|
@@ -91,6 +95,7 @@ function navigateTo(page) {
|
|
|
91
95
|
if (page === 'stats') loadStats();
|
|
92
96
|
if (page === 'system-logs') loadLogs();
|
|
93
97
|
if (page === 'request-logs') renderRequestLogs();
|
|
98
|
+
if (page === 'assistant') populateAssistantProxySelect();
|
|
94
99
|
}
|
|
95
100
|
|
|
96
101
|
document.querySelectorAll('.nav-item[data-page]').forEach(item => {
|
|
@@ -1573,6 +1578,21 @@ async function init() {
|
|
|
1573
1578
|
// Auto-refresh
|
|
1574
1579
|
setInterval(loadStats, 30000);
|
|
1575
1580
|
setInterval(loadKeyHealth, 5 * 60 * 1000);
|
|
1581
|
+
|
|
1582
|
+
// Assistant textarea auto-resize
|
|
1583
|
+
const assistantInput = document.getElementById('assistant-input');
|
|
1584
|
+
if (assistantInput) {
|
|
1585
|
+
assistantInput.addEventListener('input', function() {
|
|
1586
|
+
this.style.height = 'auto';
|
|
1587
|
+
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
|
|
1588
|
+
});
|
|
1589
|
+
assistantInput.addEventListener('keydown', function(e) {
|
|
1590
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
1591
|
+
e.preventDefault();
|
|
1592
|
+
sendAssistantMessage();
|
|
1593
|
+
}
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1576
1596
|
}
|
|
1577
1597
|
|
|
1578
1598
|
async function loadRequestLogHistory() {
|
|
@@ -1587,4 +1607,366 @@ async function loadRequestLogHistory() {
|
|
|
1587
1607
|
}
|
|
1588
1608
|
}
|
|
1589
1609
|
|
|
1610
|
+
// ---------- Assistant ----------
|
|
1611
|
+
|
|
1612
|
+
function populateAssistantProxySelect() {
|
|
1613
|
+
const select = document.getElementById('assistant-proxy-select');
|
|
1614
|
+
if (!select) return;
|
|
1615
|
+
const running = proxies.filter(p => p.running);
|
|
1616
|
+
const current = select.value;
|
|
1617
|
+
select.innerHTML = '<option value="">选择后端代理...</option>' +
|
|
1618
|
+
running.map(p => `<option value="${escapeHtml(p.id)}">${escapeHtml(p.name)} (:${p.port})</option>`).join('');
|
|
1619
|
+
if (current && running.find(p => p.id === current)) {
|
|
1620
|
+
select.value = current;
|
|
1621
|
+
} else {
|
|
1622
|
+
assistantProxyId = '';
|
|
1623
|
+
document.getElementById('assistant-send-btn').disabled = true;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
function buildSystemPrompt() {
|
|
1628
|
+
const now = new Date().toLocaleString('zh-CN', { hour12: false });
|
|
1629
|
+
return `你是 Protocol Proxy 的智能助手,专门帮助管理员监控和排障。当前时间:${now}
|
|
1630
|
+
|
|
1631
|
+
你有以下工具可以调用:
|
|
1632
|
+
|
|
1633
|
+
系统查询:
|
|
1634
|
+
- get_system_status: 获取系统概览(代理运行状态、供应商数量、运行时长)
|
|
1635
|
+
- get_providers / get_provider: 获取供应商列表或详情
|
|
1636
|
+
- get_proxies / get_proxy: 获取代理列表或详情
|
|
1637
|
+
- get_usage_stats: 查询用量统计(支持按时间范围、代理筛选)
|
|
1638
|
+
- get_recent_requests: 获取最近请求日志
|
|
1639
|
+
- get_system_logs: 获取系统日志
|
|
1640
|
+
- get_key_health: 获取 API Key 健康检查结果
|
|
1641
|
+
- get_settings: 获取系统设置项
|
|
1642
|
+
- get_config_history: 获取配置快照历史
|
|
1643
|
+
|
|
1644
|
+
文件与命令:
|
|
1645
|
+
- read_file: 读取任意文件内容(支持指定行范围)
|
|
1646
|
+
- write_file: 写入文件(会覆盖已有内容)
|
|
1647
|
+
- list_directory: 列出目录内容
|
|
1648
|
+
- search_files: 按 glob 模式搜索文件
|
|
1649
|
+
- execute_command: 执行 shell 命令
|
|
1650
|
+
|
|
1651
|
+
规则:
|
|
1652
|
+
- 当用户询问系统状态、代理、供应商、日志、用量等运维相关问题时,调用工具获取实时数据后再回答
|
|
1653
|
+
- 当用户需要查看或修改文件、执行命令时,使用对应的文件和命令工具
|
|
1654
|
+
- 当用户只是打招呼、闲聊、或询问与系统无关的问题时,直接回答,不要调用工具
|
|
1655
|
+
- 不要凭空猜测系统状态,需要数据时必须调用工具
|
|
1656
|
+
- 执行写操作或危险命令前,先告知用户将要做什么
|
|
1657
|
+
|
|
1658
|
+
你的职责:
|
|
1659
|
+
1. 回答关于代理配置和运行状态的问题
|
|
1660
|
+
2. 分析日志,指出异常和可能原因
|
|
1661
|
+
3. 根据数据给出优化建议(负载均衡、模型选择、故障切换策略)
|
|
1662
|
+
4. 用自然语言解释技术问题
|
|
1663
|
+
5. 如果发现问题,给出具体的修复步骤
|
|
1664
|
+
|
|
1665
|
+
请用中文回答,保持专业且易懂。`;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
async function sendAssistantMessage() {
|
|
1669
|
+
const input = document.getElementById('assistant-input');
|
|
1670
|
+
const text = input.value.trim();
|
|
1671
|
+
if (!text || !assistantProxyId) return;
|
|
1672
|
+
|
|
1673
|
+
addAssistantMessage('user', text);
|
|
1674
|
+
input.value = '';
|
|
1675
|
+
input.style.height = 'auto';
|
|
1676
|
+
|
|
1677
|
+
const systemPrompt = buildSystemPrompt();
|
|
1678
|
+
|
|
1679
|
+
// 构建消息历史,正确序列化 tool 相关字段
|
|
1680
|
+
const messages = [
|
|
1681
|
+
{ role: 'system', content: systemPrompt },
|
|
1682
|
+
...assistantMessages.filter(m => m.role !== 'thinking' && m.role !== 'tool-calls' && m.role !== 'tool-result').map(m => {
|
|
1683
|
+
if (m.role === 'assistant' && m.tool_calls) {
|
|
1684
|
+
const msg = { role: 'assistant', content: m.content || null, tool_calls: m.tool_calls };
|
|
1685
|
+
if (m.reasoning_content) msg.reasoning_content = m.reasoning_content;
|
|
1686
|
+
return msg;
|
|
1687
|
+
}
|
|
1688
|
+
if (m.role === 'assistant' && m.reasoning_content) {
|
|
1689
|
+
const msg = { role: 'assistant', content: m.content };
|
|
1690
|
+
msg.reasoning_content = m.reasoning_content;
|
|
1691
|
+
return msg;
|
|
1692
|
+
}
|
|
1693
|
+
if (m.role === 'tool') {
|
|
1694
|
+
return { role: 'tool', tool_call_id: m.tool_call_id, content: m.content };
|
|
1695
|
+
}
|
|
1696
|
+
return { role: m.role, content: m.content };
|
|
1697
|
+
})
|
|
1698
|
+
];
|
|
1699
|
+
|
|
1700
|
+
const proxy = proxies.find(p => p.id === assistantProxyId);
|
|
1701
|
+
if (!proxy) {
|
|
1702
|
+
addAssistantMessage('assistant', '所选代理不存在或已停止,请重新选择。');
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
const thinkingId = addAssistantMessage('thinking', '');
|
|
1707
|
+
assistantAbortController = new AbortController();
|
|
1708
|
+
|
|
1709
|
+
try {
|
|
1710
|
+
const res = await fetch('/api/assistant/chat', {
|
|
1711
|
+
method: 'POST',
|
|
1712
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1713
|
+
body: JSON.stringify({ proxyId: proxy.id, messages }),
|
|
1714
|
+
signal: assistantAbortController.signal,
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
if (!res.ok) {
|
|
1718
|
+
const err = await res.text().catch(() => 'Unknown error');
|
|
1719
|
+
removeAssistantMessage(thinkingId);
|
|
1720
|
+
addAssistantMessage('assistant', `请求失败: HTTP ${res.status}\n\n${err}`);
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
const reader = res.body.getReader();
|
|
1725
|
+
const decoder = new TextDecoder();
|
|
1726
|
+
let buffer = '';
|
|
1727
|
+
let fullContent = '';
|
|
1728
|
+
let currentEvent = '';
|
|
1729
|
+
let msgId = null;
|
|
1730
|
+
|
|
1731
|
+
removeAssistantMessage(thinkingId);
|
|
1732
|
+
console.log('[assistant] SSE stream started');
|
|
1733
|
+
|
|
1734
|
+
while (true) {
|
|
1735
|
+
const { done, value } = await reader.read();
|
|
1736
|
+
if (done) break;
|
|
1737
|
+
|
|
1738
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1739
|
+
const lines = buffer.split('\n');
|
|
1740
|
+
buffer = lines.pop();
|
|
1741
|
+
|
|
1742
|
+
for (const line of lines) {
|
|
1743
|
+
const trimmed = line.trim();
|
|
1744
|
+
if (!trimmed) continue;
|
|
1745
|
+
|
|
1746
|
+
if (trimmed.startsWith('event: ')) {
|
|
1747
|
+
currentEvent = trimmed.slice(7);
|
|
1748
|
+
continue;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
if (!trimmed.startsWith('data: ')) continue;
|
|
1752
|
+
let data;
|
|
1753
|
+
try { data = JSON.parse(trimmed.slice(6)); } catch { continue; }
|
|
1754
|
+
|
|
1755
|
+
console.log('[assistant] SSE event:', currentEvent, data);
|
|
1756
|
+
switch (currentEvent) {
|
|
1757
|
+
case 'content': {
|
|
1758
|
+
if (!msgId) msgId = addAssistantMessage('assistant', '');
|
|
1759
|
+
fullContent += data.delta;
|
|
1760
|
+
updateAssistantMessage(msgId, fullContent);
|
|
1761
|
+
break;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
case 'tool_calls': {
|
|
1765
|
+
const calls = data.calls || [];
|
|
1766
|
+
const toolCallsData = calls.map(tc => ({
|
|
1767
|
+
id: tc.id,
|
|
1768
|
+
type: 'function',
|
|
1769
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.arguments || {}) },
|
|
1770
|
+
}));
|
|
1771
|
+
// 存储 assistant 消息(含 tool_calls)用于对话序列化
|
|
1772
|
+
const assistantMsg = {
|
|
1773
|
+
id: 'msg-' + Date.now() + '-' + Math.random().toString(36).slice(2),
|
|
1774
|
+
role: 'assistant',
|
|
1775
|
+
content: fullContent || null,
|
|
1776
|
+
tool_calls: toolCallsData,
|
|
1777
|
+
};
|
|
1778
|
+
if (data.reasoning_content) assistantMsg.reasoning_content = data.reasoning_content;
|
|
1779
|
+
assistantMessages.push(assistantMsg);
|
|
1780
|
+
fullContent = '';
|
|
1781
|
+
// 创建显示用的 tool-calls 消息
|
|
1782
|
+
const callHtml = calls.map(tc => {
|
|
1783
|
+
const argsStr = Object.keys(tc.arguments || {}).length > 0
|
|
1784
|
+
? `<span class="tool-call-args">${escapeHtml(JSON.stringify(tc.arguments))}</span>`
|
|
1785
|
+
: '';
|
|
1786
|
+
return `<div class="tool-call-item"><span class="tool-call-name">${escapeHtml(tc.name)}</span>${argsStr}</div>`;
|
|
1787
|
+
}).join('');
|
|
1788
|
+
addAssistantMessage('tool-calls', callHtml);
|
|
1789
|
+
break;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
case 'tool_result': {
|
|
1793
|
+
const resultStr = JSON.stringify(data.result, null, 2);
|
|
1794
|
+
addAssistantMessage('tool-result', { name: data.name, result: resultStr, tool_call_id: data.tool_call_id });
|
|
1795
|
+
// 追加 role: 'tool' 消息用于对话序列化(不创建 DOM 元素)
|
|
1796
|
+
assistantMessages.push({
|
|
1797
|
+
id: 'msg-' + Date.now() + '-' + Math.random().toString(36).slice(2),
|
|
1798
|
+
role: 'tool',
|
|
1799
|
+
tool_call_id: data.tool_call_id,
|
|
1800
|
+
tool_name: data.name,
|
|
1801
|
+
content: JSON.stringify(data.result),
|
|
1802
|
+
});
|
|
1803
|
+
break;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
case 'done': {
|
|
1807
|
+
if (msgId) {
|
|
1808
|
+
const msgObj = assistantMessages.find(m => m.id === msgId);
|
|
1809
|
+
if (msgObj) {
|
|
1810
|
+
msgObj.content = fullContent;
|
|
1811
|
+
if (data.reasoning_content) msgObj.reasoning_content = data.reasoning_content;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
break;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
case 'error': {
|
|
1818
|
+
addAssistantMessage('assistant', `错误: ${data.message}`);
|
|
1819
|
+
break;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// 流结束但没有收到 done 事件时,确保最终内容被保存
|
|
1826
|
+
if (msgId && fullContent) {
|
|
1827
|
+
const msgObj = assistantMessages.find(m => m.id === msgId);
|
|
1828
|
+
if (msgObj && !msgObj.content) msgObj.content = fullContent;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
} catch (err) {
|
|
1832
|
+
removeAssistantMessage(thinkingId);
|
|
1833
|
+
if (err.name === 'AbortError') {
|
|
1834
|
+
addAssistantMessage('assistant', '已取消');
|
|
1835
|
+
} else {
|
|
1836
|
+
addAssistantMessage('assistant', `请求出错: ${err.message}`);
|
|
1837
|
+
}
|
|
1838
|
+
} finally {
|
|
1839
|
+
assistantAbortController = null;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
function addAssistantMessage(role, content) {
|
|
1844
|
+
const id = 'msg-' + Date.now() + '-' + Math.random().toString(36).slice(2);
|
|
1845
|
+
assistantMessages.push({ id, role, content });
|
|
1846
|
+
|
|
1847
|
+
const chat = document.getElementById('assistant-chat');
|
|
1848
|
+
if (!chat) return id;
|
|
1849
|
+
|
|
1850
|
+
const displayRoles = ['user', 'assistant', 'tool', 'tool-calls', 'tool-result'];
|
|
1851
|
+
if (assistantMessages.filter(m => displayRoles.includes(m.role)).length === 1 && role === 'user') {
|
|
1852
|
+
chat.innerHTML = '';
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
const div = document.createElement('div');
|
|
1856
|
+
div.id = id;
|
|
1857
|
+
div.className = `assistant-message ${role}`;
|
|
1858
|
+
|
|
1859
|
+
if (role === 'thinking') {
|
|
1860
|
+
div.innerHTML = `<div class="assistant-dot"></div><div class="assistant-dot"></div><div class="assistant-dot"></div>`;
|
|
1861
|
+
} else if (role === 'tool-calls') {
|
|
1862
|
+
// content 已经是 HTML 字符串
|
|
1863
|
+
div.innerHTML = `<div class="tool-calls-header">调用工具</div>${content}`;
|
|
1864
|
+
} else if (role === 'tool-result') {
|
|
1865
|
+
// content 是 {name, result, tool_call_id}
|
|
1866
|
+
const toolData = typeof content === 'object' ? content : { name: 'unknown', result: content };
|
|
1867
|
+
const resultId = 'result-' + id;
|
|
1868
|
+
div.innerHTML = `
|
|
1869
|
+
<div class="tool-result-header" onclick="document.getElementById('${resultId}').classList.toggle('expanded')">
|
|
1870
|
+
<span class="tool-result-name">${escapeHtml(toolData.name)}</span>
|
|
1871
|
+
<span class="tool-result-toggle">展开结果 ▾</span>
|
|
1872
|
+
</div>
|
|
1873
|
+
<div class="tool-result-body" id="${resultId}">
|
|
1874
|
+
<pre>${escapeHtml(toolData.result)}</pre>
|
|
1875
|
+
</div>`;
|
|
1876
|
+
// 保存 tool_call_id 到消息对象
|
|
1877
|
+
const msgObj = assistantMessages.find(m => m.id === id);
|
|
1878
|
+
if (msgObj) {
|
|
1879
|
+
msgObj.tool_call_id = toolData.tool_call_id;
|
|
1880
|
+
msgObj.tool_name = toolData.name;
|
|
1881
|
+
msgObj.content = toolData.result;
|
|
1882
|
+
}
|
|
1883
|
+
} else if (role === 'assistant') {
|
|
1884
|
+
div.innerHTML = formatAssistantContent(content);
|
|
1885
|
+
} else {
|
|
1886
|
+
div.textContent = content;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
chat.appendChild(div);
|
|
1890
|
+
chat.scrollTop = chat.scrollHeight;
|
|
1891
|
+
return id;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
function updateAssistantMessage(id, content) {
|
|
1895
|
+
const div = document.getElementById(id);
|
|
1896
|
+
if (div) {
|
|
1897
|
+
div.innerHTML = formatAssistantContent(content);
|
|
1898
|
+
const chat = document.getElementById('assistant-chat');
|
|
1899
|
+
if (chat) chat.scrollTop = chat.scrollHeight;
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
function removeAssistantMessage(id) {
|
|
1904
|
+
const div = document.getElementById(id);
|
|
1905
|
+
if (div) div.remove();
|
|
1906
|
+
assistantMessages = assistantMessages.filter(m => m.id !== id);
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
function formatAssistantContent(text) {
|
|
1910
|
+
if (!text) return '';
|
|
1911
|
+
let html = escapeHtml(text);
|
|
1912
|
+
// 代码块
|
|
1913
|
+
html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
|
|
1914
|
+
// 行内代码
|
|
1915
|
+
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
1916
|
+
// 段落
|
|
1917
|
+
const paragraphs = html.split(/\n{2,}/);
|
|
1918
|
+
html = paragraphs.map(p => {
|
|
1919
|
+
p = p.trim();
|
|
1920
|
+
if (!p) return '';
|
|
1921
|
+
// 如果已经是 pre,不包 p
|
|
1922
|
+
if (p.startsWith('<pre>')) return p;
|
|
1923
|
+
// 列表项
|
|
1924
|
+
if (p.startsWith('- ') || p.startsWith('* ')) {
|
|
1925
|
+
const items = p.split('\n').filter(l => l.trim().startsWith('- ') || l.trim().startsWith('* '));
|
|
1926
|
+
return '<ul>' + items.map(i => `<li>${i.trim().slice(2)}</li>`).join('') + '</ul>';
|
|
1927
|
+
}
|
|
1928
|
+
// 数字列表
|
|
1929
|
+
if (/^\d+\./.test(p)) {
|
|
1930
|
+
const items = p.split('\n').filter(l => /^\d+\./.test(l.trim()));
|
|
1931
|
+
return '<ol>' + items.map(i => `<li>${i.trim().replace(/^\d+\.\s*/, '')}</li>`).join('') + '</ol>';
|
|
1932
|
+
}
|
|
1933
|
+
return '<p>' + p.replace(/\n/g, '<br>') + '</p>';
|
|
1934
|
+
}).join('');
|
|
1935
|
+
return html;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
function clearAssistantChat() {
|
|
1939
|
+
assistantMessages = [];
|
|
1940
|
+
const chat = document.getElementById('assistant-chat');
|
|
1941
|
+
if (!chat) return;
|
|
1942
|
+
chat.innerHTML = `
|
|
1943
|
+
<div class="assistant-welcome">
|
|
1944
|
+
<div class="assistant-welcome-icon">
|
|
1945
|
+
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
|
|
1946
|
+
</div>
|
|
1947
|
+
<h3>智控助手</h3>
|
|
1948
|
+
<p>我是你的 Protocol Proxy 智能助手,可以帮你:</p>
|
|
1949
|
+
<ul>
|
|
1950
|
+
<li>查询代理和供应商运行状态</li>
|
|
1951
|
+
<li>分析日志,定位异常原因</li>
|
|
1952
|
+
<li>解读配置并给出优化建议</li>
|
|
1953
|
+
<li>自然语言排障与问答</li>
|
|
1954
|
+
</ul>
|
|
1955
|
+
<p class="assistant-hint">请先选择一个运行中的代理作为对话后端</p>
|
|
1956
|
+
</div>
|
|
1957
|
+
`;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// 监听代理选择
|
|
1961
|
+
(function() {
|
|
1962
|
+
const select = document.getElementById('assistant-proxy-select');
|
|
1963
|
+
if (select) {
|
|
1964
|
+
select.addEventListener('change', function() {
|
|
1965
|
+
assistantProxyId = this.value;
|
|
1966
|
+
const btn = document.getElementById('assistant-send-btn');
|
|
1967
|
+
if (btn) btn.disabled = !this.value;
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
})();
|
|
1971
|
+
|
|
1590
1972
|
init();
|
package/public/index.html
CHANGED
|
@@ -46,6 +46,11 @@
|
|
|
46
46
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
|
|
47
47
|
<span>系统日志</span>
|
|
48
48
|
</a>
|
|
49
|
+
<a href="#" class="nav-item" data-page="assistant">
|
|
50
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
|
|
51
|
+
<span>智控助手</span>
|
|
52
|
+
<span class="nav-badge" style="background:var(--accent);color:#fff">AI</span>
|
|
53
|
+
</a>
|
|
49
54
|
<a href="#" class="nav-item" data-page="settings">
|
|
50
55
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 5 15.34a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 20.39 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
|
|
51
56
|
<span>设置</span>
|
|
@@ -313,6 +318,43 @@
|
|
|
313
318
|
</div>
|
|
314
319
|
</div>
|
|
315
320
|
|
|
321
|
+
<!-- ==================== Assistant Page ==================== -->
|
|
322
|
+
<div class="page" id="page-assistant">
|
|
323
|
+
<div class="assistant-layout">
|
|
324
|
+
<div class="assistant-toolbar">
|
|
325
|
+
<div class="assistant-model-select">
|
|
326
|
+
<label>后端代理</label>
|
|
327
|
+
<select id="assistant-proxy-select">
|
|
328
|
+
<option value="">选择后端代理...</option>
|
|
329
|
+
</select>
|
|
330
|
+
</div>
|
|
331
|
+
<div class="toolbar-actions">
|
|
332
|
+
<button class="btn btn-sm" onclick="clearAssistantChat()">清空对话</button>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
<div class="assistant-chat" id="assistant-chat">
|
|
336
|
+
<div class="assistant-welcome">
|
|
337
|
+
<div class="assistant-welcome-icon">
|
|
338
|
+
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
|
|
339
|
+
</div>
|
|
340
|
+
<h3>智控助手</h3>
|
|
341
|
+
<p>我是你的 Protocol Proxy 智能助手,可以帮你:</p>
|
|
342
|
+
<ul>
|
|
343
|
+
<li>查询代理和供应商运行状态</li>
|
|
344
|
+
<li>分析日志,定位异常原因</li>
|
|
345
|
+
<li>解读配置并给出优化建议</li>
|
|
346
|
+
<li>自然语言排障与问答</li>
|
|
347
|
+
</ul>
|
|
348
|
+
<p class="assistant-hint">请先选择一个运行中的代理作为对话后端</p>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
<div class="assistant-input-area">
|
|
352
|
+
<textarea id="assistant-input" placeholder="输入问题,例如:帮我看看哪个供应商最慢,最近有什么异常?" rows="1"></textarea>
|
|
353
|
+
<button class="btn btn-primary" id="assistant-send-btn" onclick="sendAssistantMessage()" disabled>发送</button>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
316
358
|
<!-- ==================== Settings Page ==================== -->
|
|
317
359
|
<div class="page" id="page-settings">
|
|
318
360
|
<div class="settings-grid">
|