mozi-bot 1.0.0
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/LICENSE +190 -0
- package/README.md +467 -0
- package/dist/agents/agent.d.ts +118 -0
- package/dist/agents/agent.d.ts.map +1 -0
- package/dist/agents/agent.js +686 -0
- package/dist/agents/agent.js.map +1 -0
- package/dist/agents/compaction.d.ts +78 -0
- package/dist/agents/compaction.d.ts.map +1 -0
- package/dist/agents/compaction.js +350 -0
- package/dist/agents/compaction.js.map +1 -0
- package/dist/agents/failover-error.d.ts +42 -0
- package/dist/agents/failover-error.d.ts.map +1 -0
- package/dist/agents/failover-error.js +171 -0
- package/dist/agents/failover-error.js.map +1 -0
- package/dist/agents/index.d.ts +10 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +10 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/model-fallback.d.ts +54 -0
- package/dist/agents/model-fallback.d.ts.map +1 -0
- package/dist/agents/model-fallback.js +227 -0
- package/dist/agents/model-fallback.js.map +1 -0
- package/dist/agents/session-store.d.ts +53 -0
- package/dist/agents/session-store.d.ts.map +1 -0
- package/dist/agents/session-store.js +217 -0
- package/dist/agents/session-store.js.map +1 -0
- package/dist/agents/system-prompt.d.ts +29 -0
- package/dist/agents/system-prompt.d.ts.map +1 -0
- package/dist/agents/system-prompt.js +299 -0
- package/dist/agents/system-prompt.js.map +1 -0
- package/dist/channels/common/base.d.ts +40 -0
- package/dist/channels/common/base.d.ts.map +1 -0
- package/dist/channels/common/base.js +23 -0
- package/dist/channels/common/base.js.map +1 -0
- package/dist/channels/common/index.d.ts +21 -0
- package/dist/channels/common/index.d.ts.map +1 -0
- package/dist/channels/common/index.js +66 -0
- package/dist/channels/common/index.js.map +1 -0
- package/dist/channels/dingtalk/api.d.ts +30 -0
- package/dist/channels/dingtalk/api.d.ts.map +1 -0
- package/dist/channels/dingtalk/api.js +149 -0
- package/dist/channels/dingtalk/api.js.map +1 -0
- package/dist/channels/dingtalk/events.d.ts +80 -0
- package/dist/channels/dingtalk/events.d.ts.map +1 -0
- package/dist/channels/dingtalk/events.js +92 -0
- package/dist/channels/dingtalk/events.js.map +1 -0
- package/dist/channels/dingtalk/index.d.ts +51 -0
- package/dist/channels/dingtalk/index.d.ts.map +1 -0
- package/dist/channels/dingtalk/index.js +231 -0
- package/dist/channels/dingtalk/index.js.map +1 -0
- package/dist/channels/dingtalk/stream.d.ts +31 -0
- package/dist/channels/dingtalk/stream.d.ts.map +1 -0
- package/dist/channels/dingtalk/stream.js +116 -0
- package/dist/channels/dingtalk/stream.js.map +1 -0
- package/dist/channels/feishu/api.d.ts +33 -0
- package/dist/channels/feishu/api.d.ts.map +1 -0
- package/dist/channels/feishu/api.js +120 -0
- package/dist/channels/feishu/api.js.map +1 -0
- package/dist/channels/feishu/events.d.ts +87 -0
- package/dist/channels/feishu/events.d.ts.map +1 -0
- package/dist/channels/feishu/events.js +163 -0
- package/dist/channels/feishu/events.js.map +1 -0
- package/dist/channels/feishu/index.d.ts +46 -0
- package/dist/channels/feishu/index.d.ts.map +1 -0
- package/dist/channels/feishu/index.js +207 -0
- package/dist/channels/feishu/index.js.map +1 -0
- package/dist/channels/feishu/websocket.d.ts +32 -0
- package/dist/channels/feishu/websocket.d.ts.map +1 -0
- package/dist/channels/feishu/websocket.js +121 -0
- package/dist/channels/feishu/websocket.js.map +1 -0
- package/dist/channels/index.d.ts +9 -0
- package/dist/channels/index.d.ts.map +1 -0
- package/dist/channels/index.js +9 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +468 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/commands/index.d.ts +54 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +171 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/config/index.d.ts +201 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +261 -0
- package/dist/config/index.js.map +1 -0
- package/dist/gateway/index.d.ts +5 -0
- package/dist/gateway/index.d.ts.map +1 -0
- package/dist/gateway/index.js +5 -0
- package/dist/gateway/index.js.map +1 -0
- package/dist/gateway/server.d.ts +42 -0
- package/dist/gateway/server.d.ts.map +1 -0
- package/dist/gateway/server.js +235 -0
- package/dist/gateway/server.js.map +1 -0
- package/dist/hooks/index.d.ts +168 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +148 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/memory/index.d.ts +94 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +282 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/plugins/index.d.ts +57 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +103 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/providers/anthropic-compatible.d.ts +48 -0
- package/dist/providers/anthropic-compatible.d.ts.map +1 -0
- package/dist/providers/anthropic-compatible.js +380 -0
- package/dist/providers/anthropic-compatible.js.map +1 -0
- package/dist/providers/base.d.ts +26 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +58 -0
- package/dist/providers/base.js.map +1 -0
- package/dist/providers/custom-openai.d.ts +66 -0
- package/dist/providers/custom-openai.d.ts.map +1 -0
- package/dist/providers/custom-openai.js +255 -0
- package/dist/providers/custom-openai.js.map +1 -0
- package/dist/providers/dashscope.d.ts +15 -0
- package/dist/providers/dashscope.d.ts.map +1 -0
- package/dist/providers/dashscope.js +237 -0
- package/dist/providers/dashscope.js.map +1 -0
- package/dist/providers/deepseek.d.ts +10 -0
- package/dist/providers/deepseek.d.ts.map +1 -0
- package/dist/providers/deepseek.js +57 -0
- package/dist/providers/deepseek.js.map +1 -0
- package/dist/providers/index.d.ts +33 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +142 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/kimi.d.ts +10 -0
- package/dist/providers/kimi.d.ts.map +1 -0
- package/dist/providers/kimi.js +83 -0
- package/dist/providers/kimi.js.map +1 -0
- package/dist/providers/minimax.d.ts +20 -0
- package/dist/providers/minimax.d.ts.map +1 -0
- package/dist/providers/minimax.js +252 -0
- package/dist/providers/minimax.js.map +1 -0
- package/dist/providers/modelscope.d.ts +15 -0
- package/dist/providers/modelscope.d.ts.map +1 -0
- package/dist/providers/modelscope.js +237 -0
- package/dist/providers/modelscope.js.map +1 -0
- package/dist/providers/openai-compatible.d.ts +57 -0
- package/dist/providers/openai-compatible.d.ts.map +1 -0
- package/dist/providers/openai-compatible.js +193 -0
- package/dist/providers/openai-compatible.js.map +1 -0
- package/dist/providers/stepfun.d.ts +10 -0
- package/dist/providers/stepfun.d.ts.map +1 -0
- package/dist/providers/stepfun.js +125 -0
- package/dist/providers/stepfun.js.map +1 -0
- package/dist/providers/zhipu.d.ts +15 -0
- package/dist/providers/zhipu.d.ts.map +1 -0
- package/dist/providers/zhipu.js +247 -0
- package/dist/providers/zhipu.js.map +1 -0
- package/dist/sessions/index.d.ts +6 -0
- package/dist/sessions/index.d.ts.map +1 -0
- package/dist/sessions/index.js +6 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/sessions/store.d.ts +45 -0
- package/dist/sessions/store.d.ts.map +1 -0
- package/dist/sessions/store.js +268 -0
- package/dist/sessions/store.js.map +1 -0
- package/dist/sessions/types.d.ts +110 -0
- package/dist/sessions/types.d.ts.map +1 -0
- package/dist/sessions/types.js +6 -0
- package/dist/sessions/types.js.map +1 -0
- package/dist/tools/builtin/apply-patch.d.ts +8 -0
- package/dist/tools/builtin/apply-patch.d.ts.map +1 -0
- package/dist/tools/builtin/apply-patch.js +272 -0
- package/dist/tools/builtin/apply-patch.js.map +1 -0
- package/dist/tools/builtin/bash.d.ts +25 -0
- package/dist/tools/builtin/bash.d.ts.map +1 -0
- package/dist/tools/builtin/bash.js +212 -0
- package/dist/tools/builtin/bash.js.map +1 -0
- package/dist/tools/builtin/browser.d.ts +12 -0
- package/dist/tools/builtin/browser.d.ts.map +1 -0
- package/dist/tools/builtin/browser.js +693 -0
- package/dist/tools/builtin/browser.js.map +1 -0
- package/dist/tools/builtin/filesystem.d.ts +29 -0
- package/dist/tools/builtin/filesystem.d.ts.map +1 -0
- package/dist/tools/builtin/filesystem.js +484 -0
- package/dist/tools/builtin/filesystem.js.map +1 -0
- package/dist/tools/builtin/image.d.ts +13 -0
- package/dist/tools/builtin/image.d.ts.map +1 -0
- package/dist/tools/builtin/image.js +126 -0
- package/dist/tools/builtin/image.js.map +1 -0
- package/dist/tools/builtin/index.d.ts +30 -0
- package/dist/tools/builtin/index.d.ts.map +1 -0
- package/dist/tools/builtin/index.js +55 -0
- package/dist/tools/builtin/index.js.map +1 -0
- package/dist/tools/builtin/process-registry.d.ts +71 -0
- package/dist/tools/builtin/process-registry.d.ts.map +1 -0
- package/dist/tools/builtin/process-registry.js +172 -0
- package/dist/tools/builtin/process-registry.js.map +1 -0
- package/dist/tools/builtin/process-tool.d.ts +8 -0
- package/dist/tools/builtin/process-tool.d.ts.map +1 -0
- package/dist/tools/builtin/process-tool.js +279 -0
- package/dist/tools/builtin/process-tool.js.map +1 -0
- package/dist/tools/builtin/subagent.d.ts +33 -0
- package/dist/tools/builtin/subagent.d.ts.map +1 -0
- package/dist/tools/builtin/subagent.js +185 -0
- package/dist/tools/builtin/subagent.js.map +1 -0
- package/dist/tools/builtin/system.d.ts +11 -0
- package/dist/tools/builtin/system.d.ts.map +1 -0
- package/dist/tools/builtin/system.js +144 -0
- package/dist/tools/builtin/system.js.map +1 -0
- package/dist/tools/builtin/web.d.ts +9 -0
- package/dist/tools/builtin/web.d.ts.map +1 -0
- package/dist/tools/builtin/web.js +87 -0
- package/dist/tools/builtin/web.js.map +1 -0
- package/dist/tools/common.d.ts +45 -0
- package/dist/tools/common.d.ts.map +1 -0
- package/dist/tools/common.js +129 -0
- package/dist/tools/common.js.map +1 -0
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/registry.d.ts +33 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +174 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/types.d.ts +58 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +11 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/types/index.d.ts +280 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +34 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/index.d.ts +35 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +114 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +24 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +94 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/web/index.d.ts +7 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +7 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/static.d.ts +12 -0
- package/dist/web/static.d.ts.map +1 -0
- package/dist/web/static.js +1260 -0
- package/dist/web/static.js.map +1 -0
- package/dist/web/types.d.ts +95 -0
- package/dist/web/types.d.ts.map +1 -0
- package/dist/web/types.js +5 -0
- package/dist/web/types.js.map +1 -0
- package/dist/web/websocket.d.ts +58 -0
- package/dist/web/websocket.d.ts.map +1 -0
- package/dist/web/websocket.js +371 -0
- package/dist/web/websocket.js.map +1 -0
- package/package.json +89 -0
|
@@ -0,0 +1,1260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 静态文件服务和 Control UI
|
|
3
|
+
*/
|
|
4
|
+
import { getChildLogger } from "../utils/logger.js";
|
|
5
|
+
const logger = getChildLogger("static");
|
|
6
|
+
/** MIME 类型映射 */
|
|
7
|
+
const MIME_TYPES = {
|
|
8
|
+
".html": "text/html; charset=utf-8",
|
|
9
|
+
".css": "text/css; charset=utf-8",
|
|
10
|
+
".js": "application/javascript; charset=utf-8",
|
|
11
|
+
".json": "application/json; charset=utf-8",
|
|
12
|
+
".png": "image/png",
|
|
13
|
+
".jpg": "image/jpeg",
|
|
14
|
+
".jpeg": "image/jpeg",
|
|
15
|
+
".gif": "image/gif",
|
|
16
|
+
".svg": "image/svg+xml",
|
|
17
|
+
".ico": "image/x-icon",
|
|
18
|
+
".woff": "font/woff",
|
|
19
|
+
".woff2": "font/woff2",
|
|
20
|
+
".ttf": "font/ttf",
|
|
21
|
+
};
|
|
22
|
+
/** 获取内嵌的 HTML 页面 */
|
|
23
|
+
function getEmbeddedHtml(config) {
|
|
24
|
+
const assistantName = "墨子";
|
|
25
|
+
const defaultModel = config.agent.defaultModel;
|
|
26
|
+
const defaultProvider = config.agent.defaultProvider;
|
|
27
|
+
return `<!DOCTYPE html>
|
|
28
|
+
<html lang="zh-CN">
|
|
29
|
+
<head>
|
|
30
|
+
<meta charset="UTF-8">
|
|
31
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
32
|
+
<title>${assistantName} - AI 助手</title>
|
|
33
|
+
<style>
|
|
34
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
35
|
+
:root {
|
|
36
|
+
--primary: #4f46e5;
|
|
37
|
+
--primary-hover: #4338ca;
|
|
38
|
+
--bg: #f9fafb;
|
|
39
|
+
--bg-card: #ffffff;
|
|
40
|
+
--text: #111827;
|
|
41
|
+
--text-secondary: #6b7280;
|
|
42
|
+
--border: #e5e7eb;
|
|
43
|
+
--user-bg: #4f46e5;
|
|
44
|
+
--assistant-bg: #f3f4f6;
|
|
45
|
+
--sidebar-width: 280px;
|
|
46
|
+
}
|
|
47
|
+
body {
|
|
48
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
49
|
+
background: var(--bg);
|
|
50
|
+
color: var(--text);
|
|
51
|
+
height: 100vh;
|
|
52
|
+
display: flex;
|
|
53
|
+
}
|
|
54
|
+
/* 侧边栏 */
|
|
55
|
+
.sidebar {
|
|
56
|
+
width: var(--sidebar-width);
|
|
57
|
+
background: var(--bg-card);
|
|
58
|
+
border-right: 1px solid var(--border);
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-direction: column;
|
|
61
|
+
flex-shrink: 0;
|
|
62
|
+
}
|
|
63
|
+
.sidebar-header {
|
|
64
|
+
padding: 1rem;
|
|
65
|
+
border-bottom: 1px solid var(--border);
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
gap: 0.75rem;
|
|
69
|
+
}
|
|
70
|
+
.sidebar-logo { font-size: 1.5rem; }
|
|
71
|
+
.sidebar-title { font-weight: 600; font-size: 1.125rem; }
|
|
72
|
+
.new-chat-btn {
|
|
73
|
+
margin: 1rem;
|
|
74
|
+
padding: 0.75rem 1rem;
|
|
75
|
+
background: var(--primary);
|
|
76
|
+
color: white;
|
|
77
|
+
border: none;
|
|
78
|
+
border-radius: 0.5rem;
|
|
79
|
+
font-size: 0.875rem;
|
|
80
|
+
font-weight: 500;
|
|
81
|
+
cursor: pointer;
|
|
82
|
+
display: flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
justify-content: center;
|
|
85
|
+
gap: 0.5rem;
|
|
86
|
+
transition: background 0.2s;
|
|
87
|
+
}
|
|
88
|
+
.new-chat-btn:hover { background: var(--primary-hover); }
|
|
89
|
+
.session-list {
|
|
90
|
+
flex: 1;
|
|
91
|
+
overflow-y: auto;
|
|
92
|
+
padding: 0.5rem;
|
|
93
|
+
}
|
|
94
|
+
.session-item {
|
|
95
|
+
padding: 0.75rem 1rem;
|
|
96
|
+
border-radius: 0.5rem;
|
|
97
|
+
cursor: pointer;
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
gap: 0.75rem;
|
|
101
|
+
margin-bottom: 0.25rem;
|
|
102
|
+
transition: background 0.15s;
|
|
103
|
+
}
|
|
104
|
+
.session-item:hover { background: var(--bg); }
|
|
105
|
+
.session-item.active { background: #eef2ff; }
|
|
106
|
+
.session-icon { font-size: 1rem; opacity: 0.7; }
|
|
107
|
+
.session-info { flex: 1; min-width: 0; }
|
|
108
|
+
.session-title {
|
|
109
|
+
font-size: 0.875rem;
|
|
110
|
+
font-weight: 500;
|
|
111
|
+
white-space: nowrap;
|
|
112
|
+
overflow: hidden;
|
|
113
|
+
text-overflow: ellipsis;
|
|
114
|
+
}
|
|
115
|
+
.session-meta {
|
|
116
|
+
font-size: 0.75rem;
|
|
117
|
+
color: var(--text-secondary);
|
|
118
|
+
display: flex;
|
|
119
|
+
gap: 0.5rem;
|
|
120
|
+
}
|
|
121
|
+
.session-delete {
|
|
122
|
+
opacity: 0;
|
|
123
|
+
padding: 0.25rem;
|
|
124
|
+
border: none;
|
|
125
|
+
background: none;
|
|
126
|
+
cursor: pointer;
|
|
127
|
+
font-size: 0.875rem;
|
|
128
|
+
color: var(--text-secondary);
|
|
129
|
+
border-radius: 0.25rem;
|
|
130
|
+
}
|
|
131
|
+
.session-item:hover .session-delete { opacity: 1; }
|
|
132
|
+
.session-delete:hover { background: #fee2e2; color: #dc2626; }
|
|
133
|
+
.sidebar-footer {
|
|
134
|
+
padding: 0.75rem 1rem;
|
|
135
|
+
border-top: 1px solid var(--border);
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
justify-content: space-between;
|
|
139
|
+
font-size: 0.75rem;
|
|
140
|
+
color: var(--text-secondary);
|
|
141
|
+
}
|
|
142
|
+
/* 主内容区 */
|
|
143
|
+
.main-container {
|
|
144
|
+
flex: 1;
|
|
145
|
+
display: flex;
|
|
146
|
+
flex-direction: column;
|
|
147
|
+
min-width: 0;
|
|
148
|
+
}
|
|
149
|
+
.header {
|
|
150
|
+
background: var(--bg-card);
|
|
151
|
+
border-bottom: 1px solid var(--border);
|
|
152
|
+
padding: 1rem 1.5rem;
|
|
153
|
+
display: flex;
|
|
154
|
+
align-items: center;
|
|
155
|
+
justify-content: space-between;
|
|
156
|
+
}
|
|
157
|
+
.header-left { display: flex; align-items: center; gap: 0.75rem; }
|
|
158
|
+
.menu-btn {
|
|
159
|
+
display: none;
|
|
160
|
+
padding: 0.5rem;
|
|
161
|
+
border: none;
|
|
162
|
+
background: none;
|
|
163
|
+
cursor: pointer;
|
|
164
|
+
font-size: 1.25rem;
|
|
165
|
+
}
|
|
166
|
+
.title { font-size: 1.25rem; font-weight: 600; }
|
|
167
|
+
.subtitle { font-size: 0.75rem; color: var(--text-secondary); }
|
|
168
|
+
.status { display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; color: var(--text-secondary); }
|
|
169
|
+
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: #10b981; }
|
|
170
|
+
.status-dot.disconnected { background: #ef4444; }
|
|
171
|
+
.main { flex: 1; display: flex; flex-direction: column; max-width: 900px; width: 100%; margin: 0 auto; padding: 1rem; overflow: hidden; }
|
|
172
|
+
.messages { flex: 1; overflow-y: auto; padding: 1rem 0; display: flex; flex-direction: column; gap: 1rem; }
|
|
173
|
+
.message { display: flex; gap: 0.75rem; max-width: 85%; }
|
|
174
|
+
.message.user { align-self: flex-end; flex-direction: row-reverse; }
|
|
175
|
+
.message-avatar { width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 1rem; flex-shrink: 0; }
|
|
176
|
+
.message.user .message-avatar { background: var(--user-bg); color: white; }
|
|
177
|
+
.message.assistant .message-avatar { background: var(--assistant-bg); }
|
|
178
|
+
.message-content { padding: 0.75rem 1rem; border-radius: 1rem; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
|
|
179
|
+
.message.user .message-content { background: var(--user-bg); color: white; border-bottom-right-radius: 0.25rem; }
|
|
180
|
+
.message.assistant .message-content { background: var(--assistant-bg); border-bottom-left-radius: 0.25rem; }
|
|
181
|
+
.message-content code { background: rgba(0, 0, 0, 0.1); padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-family: "SF Mono", Monaco, monospace; font-size: 0.875em; }
|
|
182
|
+
.message.user .message-content code { background: rgba(255, 255, 255, 0.2); }
|
|
183
|
+
.message-content pre { background: rgba(0, 0, 0, 0.05); padding: 0.75rem; border-radius: 0.5rem; overflow-x: auto; margin: 0.5rem 0; }
|
|
184
|
+
.message.user .message-content pre { background: rgba(255, 255, 255, 0.1); }
|
|
185
|
+
.typing { display: flex; gap: 0.25rem; padding: 0.5rem; }
|
|
186
|
+
.typing span { width: 8px; height: 8px; background: var(--text-secondary); border-radius: 50%; animation: typing 1.4s infinite; }
|
|
187
|
+
.typing span:nth-child(2) { animation-delay: 0.2s; }
|
|
188
|
+
.typing span:nth-child(3) { animation-delay: 0.4s; }
|
|
189
|
+
@keyframes typing { 0%, 60%, 100% { transform: translateY(0); } 30% { transform: translateY(-4px); } }
|
|
190
|
+
.input-area { background: var(--bg-card); border: 1px solid var(--border); border-radius: 1rem; padding: 0.75rem; display: flex; gap: 0.75rem; align-items: flex-end; }
|
|
191
|
+
.input-area textarea { flex: 1; border: none; outline: none; resize: none; font-size: 1rem; line-height: 1.5; max-height: 150px; font-family: inherit; background: transparent; }
|
|
192
|
+
.input-area button { background: var(--primary); color: white; border: none; border-radius: 0.5rem; padding: 0.5rem 1rem; font-size: 0.875rem; font-weight: 500; cursor: pointer; transition: background 0.2s; display: flex; align-items: center; gap: 0.375rem; }
|
|
193
|
+
.input-area button:hover { background: var(--primary-hover); }
|
|
194
|
+
.input-area button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
195
|
+
.btn-icon { background: transparent !important; color: var(--text-secondary) !important; padding: 0.5rem !important; }
|
|
196
|
+
.btn-icon:hover { color: var(--text) !important; background: var(--bg) !important; }
|
|
197
|
+
.welcome { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: 1rem; color: var(--text-secondary); }
|
|
198
|
+
.welcome-icon { font-size: 4rem; }
|
|
199
|
+
.welcome h2 { color: var(--text); font-size: 1.5rem; }
|
|
200
|
+
.welcome p { max-width: 400px; }
|
|
201
|
+
.features { display: flex; gap: 1rem; margin-top: 1rem; flex-wrap: wrap; justify-content: center; }
|
|
202
|
+
.feature { background: var(--bg-card); border: 1px solid var(--border); border-radius: 0.75rem; padding: 1rem; width: 140px; text-align: center; }
|
|
203
|
+
.feature-icon { font-size: 1.5rem; margin-bottom: 0.5rem; }
|
|
204
|
+
.feature-text { font-size: 0.875rem; color: var(--text); }
|
|
205
|
+
/* Markdown styles */
|
|
206
|
+
.message-content.markdown { white-space: normal; }
|
|
207
|
+
.message-content.markdown h1, .message-content.markdown h2, .message-content.markdown h3, .message-content.markdown h4 { margin: 0.75em 0 0.5em 0; font-weight: 600; line-height: 1.3; }
|
|
208
|
+
.message-content.markdown h1 { font-size: 1.4em; }
|
|
209
|
+
.message-content.markdown h2 { font-size: 1.25em; }
|
|
210
|
+
.message-content.markdown h3 { font-size: 1.1em; }
|
|
211
|
+
.message-content.markdown p { margin: 0.5em 0; }
|
|
212
|
+
.message-content.markdown ul, .message-content.markdown ol { margin: 0.5em 0; padding-left: 1.5em; }
|
|
213
|
+
.message-content.markdown li { margin: 0.25em 0; }
|
|
214
|
+
.message-content.markdown pre { background: #1e1e1e; color: #d4d4d4; padding: 1em; border-radius: 0.5em; overflow-x: auto; margin: 0.75em 0; font-family: "SF Mono", Monaco, Consolas, monospace; font-size: 0.9em; line-height: 1.4; }
|
|
215
|
+
.message-content.markdown pre code { background: none; padding: 0; color: inherit; font-size: inherit; }
|
|
216
|
+
.message-content.markdown code { background: rgba(0, 0, 0, 0.08); padding: 0.15em 0.4em; border-radius: 0.25em; font-family: "SF Mono", Monaco, Consolas, monospace; font-size: 0.9em; }
|
|
217
|
+
.message.user .message-content.markdown code { background: rgba(255, 255, 255, 0.15); }
|
|
218
|
+
.message-content.markdown table { border-collapse: collapse; margin: 0.75em 0; width: 100%; font-size: 0.9em; }
|
|
219
|
+
.message-content.markdown th, .message-content.markdown td { border: 1px solid var(--border); padding: 0.5em 0.75em; text-align: left; }
|
|
220
|
+
.message-content.markdown th { background: rgba(0, 0, 0, 0.04); font-weight: 600; }
|
|
221
|
+
.message-content.markdown blockquote { border-left: 3px solid var(--primary); margin: 0.75em 0; padding: 0.5em 1em; background: rgba(0, 0, 0, 0.03); }
|
|
222
|
+
.message-content.markdown hr { border: none; border-top: 1px solid var(--border); margin: 1em 0; }
|
|
223
|
+
.message-content.markdown a { color: var(--primary); text-decoration: none; }
|
|
224
|
+
.message-content.markdown a:hover { text-decoration: underline; }
|
|
225
|
+
.message-content.markdown strong { font-weight: 600; }
|
|
226
|
+
.message-content.markdown em { font-style: italic; }
|
|
227
|
+
/* 响应式 */
|
|
228
|
+
@media (max-width: 768px) {
|
|
229
|
+
.sidebar { position: fixed; left: -100%; top: 0; bottom: 0; z-index: 100; transition: left 0.3s; }
|
|
230
|
+
.sidebar.open { left: 0; }
|
|
231
|
+
.sidebar-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.3); z-index: 99; }
|
|
232
|
+
.sidebar.open + .sidebar-overlay { display: block; }
|
|
233
|
+
.menu-btn { display: block; }
|
|
234
|
+
.message { max-width: 95%; }
|
|
235
|
+
}
|
|
236
|
+
.empty-sessions { padding: 2rem 1rem; text-align: center; color: var(--text-secondary); font-size: 0.875rem; }
|
|
237
|
+
</style>
|
|
238
|
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
239
|
+
</head>
|
|
240
|
+
<body>
|
|
241
|
+
<aside class="sidebar" id="sidebar">
|
|
242
|
+
<div class="sidebar-header">
|
|
243
|
+
<span class="sidebar-logo">🐼</span>
|
|
244
|
+
<span class="sidebar-title">${assistantName}</span>
|
|
245
|
+
</div>
|
|
246
|
+
<button class="new-chat-btn" id="newChatBtn">➕ 新建对话</button>
|
|
247
|
+
<div class="session-list" id="sessionList">
|
|
248
|
+
<div class="empty-sessions">暂无历史会话</div>
|
|
249
|
+
</div>
|
|
250
|
+
<div class="sidebar-footer">
|
|
251
|
+
<span id="sessionCount">0 个会话</span>
|
|
252
|
+
<a href="/control" style="color: var(--primary); text-decoration: none;">控制台</a>
|
|
253
|
+
</div>
|
|
254
|
+
</aside>
|
|
255
|
+
<div class="sidebar-overlay" id="sidebarOverlay"></div>
|
|
256
|
+
|
|
257
|
+
<div class="main-container">
|
|
258
|
+
<header class="header">
|
|
259
|
+
<div class="header-left">
|
|
260
|
+
<button class="menu-btn" id="menuBtn">☰</button>
|
|
261
|
+
<div>
|
|
262
|
+
<div class="title">${assistantName}</div>
|
|
263
|
+
<div class="subtitle">${defaultProvider} / ${defaultModel}</div>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
<div class="status">
|
|
267
|
+
<span class="status-dot" id="statusDot"></span>
|
|
268
|
+
<span id="statusText">连接中...</span>
|
|
269
|
+
</div>
|
|
270
|
+
</header>
|
|
271
|
+
|
|
272
|
+
<main class="main">
|
|
273
|
+
<div class="messages" id="messages">
|
|
274
|
+
<div class="welcome" id="welcome">
|
|
275
|
+
<div class="welcome-icon">🐼</div>
|
|
276
|
+
<h2>欢迎使用 ${assistantName}</h2>
|
|
277
|
+
<p>我是一个支持国产模型的智能助手,可以帮助你回答问题、编写代码、分析数据等。</p>
|
|
278
|
+
<div class="features">
|
|
279
|
+
<div class="feature"><div class="feature-icon">💬</div><div class="feature-text">智能对话</div></div>
|
|
280
|
+
<div class="feature"><div class="feature-icon">💻</div><div class="feature-text">代码助手</div></div>
|
|
281
|
+
<div class="feature"><div class="feature-icon">📊</div><div class="feature-text">数据分析</div></div>
|
|
282
|
+
<div class="feature"><div class="feature-icon">🔧</div><div class="feature-text">工具调用</div></div>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
<div class="input-area">
|
|
287
|
+
<textarea id="input" placeholder="输入消息... (Enter 发送, Shift+Enter 换行)" rows="1"></textarea>
|
|
288
|
+
<button class="btn-icon" id="clearBtn" title="清除对话">🗑️</button>
|
|
289
|
+
<button id="sendBtn"><span>发送</span><span>↵</span></button>
|
|
290
|
+
</div>
|
|
291
|
+
</main>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<script>
|
|
295
|
+
let ws = null;
|
|
296
|
+
let reconnectTimer = null;
|
|
297
|
+
let pendingRequests = new Map();
|
|
298
|
+
let requestId = 0;
|
|
299
|
+
let isStreaming = false;
|
|
300
|
+
let currentStreamContent = '';
|
|
301
|
+
let currentSessionKey = null;
|
|
302
|
+
let sessionRestored = false;
|
|
303
|
+
let allSessions = [];
|
|
304
|
+
|
|
305
|
+
const STORAGE_KEY = 'mozi_session_key';
|
|
306
|
+
|
|
307
|
+
const sidebar = document.getElementById('sidebar');
|
|
308
|
+
const sidebarOverlay = document.getElementById('sidebarOverlay');
|
|
309
|
+
const menuBtn = document.getElementById('menuBtn');
|
|
310
|
+
const sessionList = document.getElementById('sessionList');
|
|
311
|
+
const sessionCount = document.getElementById('sessionCount');
|
|
312
|
+
const newChatBtn = document.getElementById('newChatBtn');
|
|
313
|
+
const messagesEl = document.getElementById('messages');
|
|
314
|
+
const welcomeEl = document.getElementById('welcome');
|
|
315
|
+
const inputEl = document.getElementById('input');
|
|
316
|
+
const sendBtn = document.getElementById('sendBtn');
|
|
317
|
+
const clearBtn = document.getElementById('clearBtn');
|
|
318
|
+
const statusDot = document.getElementById('statusDot');
|
|
319
|
+
const statusText = document.getElementById('statusText');
|
|
320
|
+
|
|
321
|
+
function getSavedSessionKey() { return localStorage.getItem(STORAGE_KEY); }
|
|
322
|
+
function saveSessionKey(sessionKey) { localStorage.setItem(STORAGE_KEY, sessionKey); currentSessionKey = sessionKey; }
|
|
323
|
+
|
|
324
|
+
function toggleSidebar() {
|
|
325
|
+
sidebar.classList.toggle('open');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
menuBtn.addEventListener('click', toggleSidebar);
|
|
329
|
+
sidebarOverlay.addEventListener('click', toggleSidebar);
|
|
330
|
+
|
|
331
|
+
function connect() {
|
|
332
|
+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
333
|
+
ws = new WebSocket(protocol + '//' + location.host + '/ws');
|
|
334
|
+
|
|
335
|
+
ws.onopen = () => {
|
|
336
|
+
statusDot.classList.remove('disconnected');
|
|
337
|
+
statusText.textContent = '已连接';
|
|
338
|
+
sessionRestored = false;
|
|
339
|
+
// 注意:不在此处加载数据,等待服务器发送 connected 事件后再操作
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
ws.onclose = () => {
|
|
343
|
+
statusDot.classList.add('disconnected');
|
|
344
|
+
statusText.textContent = '已断开';
|
|
345
|
+
reconnectTimer = setTimeout(connect, 3000);
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
ws.onerror = (err) => { console.error('WebSocket error:', err); };
|
|
349
|
+
ws.onmessage = (event) => {
|
|
350
|
+
try {
|
|
351
|
+
const frame = JSON.parse(event.data);
|
|
352
|
+
handleFrame(frame);
|
|
353
|
+
} catch (e) { console.error('Failed to parse message:', e); }
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function restoreSession(sessionKey) {
|
|
358
|
+
try {
|
|
359
|
+
const result = await request('sessions.restore', { sessionKey });
|
|
360
|
+
sessionRestored = true;
|
|
361
|
+
if (result && result.sessionKey) {
|
|
362
|
+
saveSessionKey(result.sessionKey);
|
|
363
|
+
}
|
|
364
|
+
if (result && result.messages && result.messages.length > 0) {
|
|
365
|
+
loadHistoryMessages(result.messages);
|
|
366
|
+
}
|
|
367
|
+
updateSessionListActive();
|
|
368
|
+
} catch (e) {
|
|
369
|
+
console.log('No previous session found, starting fresh');
|
|
370
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
371
|
+
sessionRestored = true;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function loadSessionList() {
|
|
376
|
+
try {
|
|
377
|
+
const result = await request('sessions.list', { limit: 50 });
|
|
378
|
+
allSessions = result.sessions || [];
|
|
379
|
+
renderSessionList();
|
|
380
|
+
} catch (e) { console.error('Failed to load sessions:', e); }
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function renderSessionList() {
|
|
384
|
+
// 过滤掉没有消息的空会话
|
|
385
|
+
const sessionsWithMessages = allSessions.filter(s => (s.messageCount || 0) > 0);
|
|
386
|
+
|
|
387
|
+
if (sessionsWithMessages.length === 0) {
|
|
388
|
+
sessionList.innerHTML = '<div class="empty-sessions">暂无历史会话</div>';
|
|
389
|
+
sessionCount.textContent = '0 个会话';
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
sessionCount.textContent = sessionsWithMessages.length + ' 个会话';
|
|
394
|
+
const currentKey = getSavedSessionKey();
|
|
395
|
+
|
|
396
|
+
sessionList.innerHTML = sessionsWithMessages.map(s => {
|
|
397
|
+
const isActive = s.sessionKey === currentKey;
|
|
398
|
+
const title = s.label || s.sessionKey.replace(/^webchat:/, '').slice(0, 12) + '...';
|
|
399
|
+
const time = new Date(s.updatedAt).toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
|
|
400
|
+
const msgCount = s.messageCount || 0;
|
|
401
|
+
return \`
|
|
402
|
+
<div class="session-item \${isActive ? 'active' : ''}" data-key="\${s.sessionKey}">
|
|
403
|
+
<span class="session-icon">💬</span>
|
|
404
|
+
<div class="session-info">
|
|
405
|
+
<div class="session-title">\${escapeHtml(title)}</div>
|
|
406
|
+
<div class="session-meta"><span>\${msgCount} 条消息</span><span>\${time}</span></div>
|
|
407
|
+
</div>
|
|
408
|
+
<button class="session-delete" data-key="\${s.sessionKey}" title="删除">🗑️</button>
|
|
409
|
+
</div>
|
|
410
|
+
\`;
|
|
411
|
+
}).join('');
|
|
412
|
+
|
|
413
|
+
sessionList.querySelectorAll('.session-item').forEach(el => {
|
|
414
|
+
el.addEventListener('click', (e) => {
|
|
415
|
+
if (e.target.classList.contains('session-delete')) return;
|
|
416
|
+
switchToSession(el.dataset.key);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
sessionList.querySelectorAll('.session-delete').forEach(el => {
|
|
421
|
+
el.addEventListener('click', (e) => {
|
|
422
|
+
e.stopPropagation();
|
|
423
|
+
deleteSession(el.dataset.key);
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function updateSessionListActive() {
|
|
429
|
+
const currentKey = getSavedSessionKey();
|
|
430
|
+
sessionList.querySelectorAll('.session-item').forEach(el => {
|
|
431
|
+
el.classList.toggle('active', el.dataset.key === currentKey);
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function switchToSession(sessionKey) {
|
|
436
|
+
if (isStreaming) return;
|
|
437
|
+
clearMessagesUI();
|
|
438
|
+
saveSessionKey(sessionKey);
|
|
439
|
+
try {
|
|
440
|
+
const result = await request('sessions.restore', { sessionKey });
|
|
441
|
+
if (result && result.messages && result.messages.length > 0) {
|
|
442
|
+
loadHistoryMessages(result.messages);
|
|
443
|
+
}
|
|
444
|
+
updateSessionListActive();
|
|
445
|
+
if (window.innerWidth <= 768) toggleSidebar();
|
|
446
|
+
} catch (e) {
|
|
447
|
+
console.error('Failed to switch session:', e);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function deleteSession(sessionKey) {
|
|
452
|
+
if (!confirm('确定要删除这个会话吗?')) return;
|
|
453
|
+
try {
|
|
454
|
+
await request('sessions.delete', { sessionKey });
|
|
455
|
+
if (getSavedSessionKey() === sessionKey) {
|
|
456
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
457
|
+
clearMessagesUI();
|
|
458
|
+
showWelcome();
|
|
459
|
+
}
|
|
460
|
+
loadSessionList();
|
|
461
|
+
} catch (e) { console.error('Failed to delete session:', e); }
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function createNewChat() {
|
|
465
|
+
if (isStreaming) return;
|
|
466
|
+
// 通知服务器创建新会话,清除 Agent 上下文
|
|
467
|
+
try {
|
|
468
|
+
const result = await request('chat.clear');
|
|
469
|
+
// 不保存返回的 sessionKey 到 localStorage
|
|
470
|
+
// 这样 UI 会显示欢迎页面,而非立即绑定新会话
|
|
471
|
+
} catch (e) {
|
|
472
|
+
// 如果当前没有 session(服务器端),忽略错误
|
|
473
|
+
console.log('Create new chat:', e.message);
|
|
474
|
+
}
|
|
475
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
476
|
+
clearMessagesUI();
|
|
477
|
+
showWelcome();
|
|
478
|
+
currentSessionKey = null;
|
|
479
|
+
loadSessionList();
|
|
480
|
+
if (window.innerWidth <= 768) toggleSidebar();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
newChatBtn.addEventListener('click', createNewChat);
|
|
484
|
+
|
|
485
|
+
function clearMessagesUI() {
|
|
486
|
+
const msgs = messagesEl.querySelectorAll('.message');
|
|
487
|
+
msgs.forEach(m => m.remove());
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function showWelcome() {
|
|
491
|
+
if (welcomeEl) welcomeEl.style.display = 'flex';
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function loadHistoryMessages(messages) {
|
|
495
|
+
if (!messages || messages.length === 0) return;
|
|
496
|
+
if (welcomeEl) welcomeEl.style.display = 'none';
|
|
497
|
+
for (const msg of messages) {
|
|
498
|
+
if (msg.role === 'user' || msg.role === 'assistant') {
|
|
499
|
+
const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
|
|
500
|
+
addMessage(msg.role, content, false);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function handleFrame(frame) {
|
|
506
|
+
if (frame.type === 'res') {
|
|
507
|
+
const pending = pendingRequests.get(frame.id);
|
|
508
|
+
if (pending) {
|
|
509
|
+
pendingRequests.delete(frame.id);
|
|
510
|
+
if (frame.ok) pending.resolve(frame.payload);
|
|
511
|
+
else pending.reject(new Error(frame.error?.message || 'Unknown error'));
|
|
512
|
+
}
|
|
513
|
+
} else if (frame.type === 'event') {
|
|
514
|
+
handleEvent(frame.event, frame.payload);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function handleEvent(event, payload) {
|
|
519
|
+
if (event === 'connected') {
|
|
520
|
+
console.log('Connected, clientId:', payload.clientId);
|
|
521
|
+
// 服务器准备好了,现在加载数据
|
|
522
|
+
const savedSessionKey = getSavedSessionKey();
|
|
523
|
+
if (savedSessionKey) {
|
|
524
|
+
restoreSession(savedSessionKey);
|
|
525
|
+
} else {
|
|
526
|
+
// 没有保存的 session,等第一次发消息时服务器会自动创建
|
|
527
|
+
sessionRestored = true;
|
|
528
|
+
}
|
|
529
|
+
loadSessionList();
|
|
530
|
+
} else if (event === 'chat.delta') {
|
|
531
|
+
if (!isStreaming) {
|
|
532
|
+
isStreaming = true;
|
|
533
|
+
currentStreamContent = '';
|
|
534
|
+
addMessage('assistant', '', true);
|
|
535
|
+
}
|
|
536
|
+
if (payload.delta) {
|
|
537
|
+
currentStreamContent += payload.delta;
|
|
538
|
+
updateStreamingMessage(currentStreamContent);
|
|
539
|
+
}
|
|
540
|
+
if (payload.done) {
|
|
541
|
+
isStreaming = false;
|
|
542
|
+
finalizeStreamingMessage();
|
|
543
|
+
loadSessionList();
|
|
544
|
+
}
|
|
545
|
+
} else if (event === 'chat.error') {
|
|
546
|
+
isStreaming = false;
|
|
547
|
+
addMessage('assistant', '❌ 错误: ' + payload.error);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function request(method, params) {
|
|
552
|
+
return new Promise((resolve, reject) => {
|
|
553
|
+
const id = String(++requestId);
|
|
554
|
+
pendingRequests.set(id, { resolve, reject });
|
|
555
|
+
ws.send(JSON.stringify({ type: 'req', id, method, params }));
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function renderContent(content, isAssistant = false) {
|
|
560
|
+
if (isAssistant && typeof marked !== 'undefined') {
|
|
561
|
+
marked.setOptions({ breaks: true, gfm: true });
|
|
562
|
+
return marked.parse(content);
|
|
563
|
+
}
|
|
564
|
+
return escapeHtml(content);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function addMessage(role, content, streaming = false) {
|
|
568
|
+
if (welcomeEl) welcomeEl.style.display = 'none';
|
|
569
|
+
const msgEl = document.createElement('div');
|
|
570
|
+
msgEl.className = 'message ' + role;
|
|
571
|
+
if (streaming) msgEl.id = 'streaming-message';
|
|
572
|
+
const avatar = role === 'user' ? '👤' : '🐼';
|
|
573
|
+
const isAssistant = role === 'assistant';
|
|
574
|
+
const contentClass = isAssistant ? 'message-content markdown' : 'message-content';
|
|
575
|
+
msgEl.innerHTML = \`<div class="message-avatar">\${avatar}</div><div class="\${contentClass}">\${streaming ? '<div class="typing"><span></span><span></span><span></span></div>' : renderContent(content, isAssistant)}</div>\`;
|
|
576
|
+
messagesEl.appendChild(msgEl);
|
|
577
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function updateStreamingMessage(content) {
|
|
581
|
+
const msgEl = document.getElementById('streaming-message');
|
|
582
|
+
if (msgEl) {
|
|
583
|
+
const contentEl = msgEl.querySelector('.message-content');
|
|
584
|
+
contentEl.innerHTML = renderContent(content, true);
|
|
585
|
+
contentEl.classList.add('markdown');
|
|
586
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function finalizeStreamingMessage() {
|
|
591
|
+
const msgEl = document.getElementById('streaming-message');
|
|
592
|
+
if (msgEl) msgEl.removeAttribute('id');
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function escapeHtml(text) {
|
|
596
|
+
const div = document.createElement('div');
|
|
597
|
+
div.textContent = text;
|
|
598
|
+
return div.innerHTML;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function sendMessage() {
|
|
602
|
+
const message = inputEl.value.trim();
|
|
603
|
+
if (!message || isStreaming) return;
|
|
604
|
+
inputEl.value = '';
|
|
605
|
+
inputEl.style.height = 'auto';
|
|
606
|
+
addMessage('user', message);
|
|
607
|
+
try {
|
|
608
|
+
await request('chat.send', { message });
|
|
609
|
+
} catch (e) {
|
|
610
|
+
addMessage('assistant', '❌ 发送失败: ' + e.message);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async function clearChat() {
|
|
615
|
+
if (isStreaming) return;
|
|
616
|
+
try {
|
|
617
|
+
const result = await request('chat.clear');
|
|
618
|
+
if (result && result.sessionKey) saveSessionKey(result.sessionKey);
|
|
619
|
+
clearMessagesUI();
|
|
620
|
+
showWelcome();
|
|
621
|
+
loadSessionList();
|
|
622
|
+
} catch (e) { console.error('Failed to clear:', e); }
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function autoResize() {
|
|
626
|
+
inputEl.style.height = 'auto';
|
|
627
|
+
inputEl.style.height = Math.min(inputEl.scrollHeight, 150) + 'px';
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
sendBtn.addEventListener('click', sendMessage);
|
|
631
|
+
clearBtn.addEventListener('click', clearChat);
|
|
632
|
+
inputEl.addEventListener('input', autoResize);
|
|
633
|
+
inputEl.addEventListener('keydown', (e) => {
|
|
634
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
connect();
|
|
638
|
+
</script>
|
|
639
|
+
</body>
|
|
640
|
+
</html>`;
|
|
641
|
+
}
|
|
642
|
+
/** 获取 Control UI 页面 */
|
|
643
|
+
function getControlHtml(config) {
|
|
644
|
+
const assistantName = "墨子";
|
|
645
|
+
const defaultModel = config.agent.defaultModel;
|
|
646
|
+
const defaultProvider = config.agent.defaultProvider;
|
|
647
|
+
return `<!DOCTYPE html>
|
|
648
|
+
<html lang="zh-CN">
|
|
649
|
+
<head>
|
|
650
|
+
<meta charset="UTF-8">
|
|
651
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
652
|
+
<title>${assistantName} - 控制台</title>
|
|
653
|
+
<style>
|
|
654
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
655
|
+
:root {
|
|
656
|
+
--primary: #4f46e5;
|
|
657
|
+
--primary-hover: #4338ca;
|
|
658
|
+
--bg: #f1f5f9;
|
|
659
|
+
--bg-card: #ffffff;
|
|
660
|
+
--text: #1e293b;
|
|
661
|
+
--text-secondary: #64748b;
|
|
662
|
+
--border: #e2e8f0;
|
|
663
|
+
--success: #22c55e;
|
|
664
|
+
--warning: #f59e0b;
|
|
665
|
+
--error: #ef4444;
|
|
666
|
+
}
|
|
667
|
+
body {
|
|
668
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
669
|
+
background: var(--bg);
|
|
670
|
+
color: var(--text);
|
|
671
|
+
min-height: 100vh;
|
|
672
|
+
}
|
|
673
|
+
.layout {
|
|
674
|
+
display: flex;
|
|
675
|
+
min-height: 100vh;
|
|
676
|
+
}
|
|
677
|
+
/* 侧边栏 */
|
|
678
|
+
.sidebar {
|
|
679
|
+
width: 240px;
|
|
680
|
+
background: var(--bg-card);
|
|
681
|
+
border-right: 1px solid var(--border);
|
|
682
|
+
padding: 1.5rem 0;
|
|
683
|
+
display: flex;
|
|
684
|
+
flex-direction: column;
|
|
685
|
+
}
|
|
686
|
+
.sidebar-header {
|
|
687
|
+
padding: 0 1.5rem 1.5rem;
|
|
688
|
+
border-bottom: 1px solid var(--border);
|
|
689
|
+
margin-bottom: 1rem;
|
|
690
|
+
}
|
|
691
|
+
.sidebar-logo {
|
|
692
|
+
display: flex;
|
|
693
|
+
align-items: center;
|
|
694
|
+
gap: 0.75rem;
|
|
695
|
+
}
|
|
696
|
+
.sidebar-logo span:first-child { font-size: 1.75rem; }
|
|
697
|
+
.sidebar-logo span:last-child { font-size: 1.25rem; font-weight: 600; }
|
|
698
|
+
.nav-section {
|
|
699
|
+
padding: 0.5rem 1rem;
|
|
700
|
+
font-size: 0.75rem;
|
|
701
|
+
font-weight: 600;
|
|
702
|
+
color: var(--text-secondary);
|
|
703
|
+
text-transform: uppercase;
|
|
704
|
+
letter-spacing: 0.05em;
|
|
705
|
+
}
|
|
706
|
+
.nav-item {
|
|
707
|
+
display: flex;
|
|
708
|
+
align-items: center;
|
|
709
|
+
gap: 0.75rem;
|
|
710
|
+
padding: 0.75rem 1.5rem;
|
|
711
|
+
color: var(--text-secondary);
|
|
712
|
+
text-decoration: none;
|
|
713
|
+
cursor: pointer;
|
|
714
|
+
transition: all 0.15s;
|
|
715
|
+
}
|
|
716
|
+
.nav-item:hover { background: var(--bg); color: var(--text); }
|
|
717
|
+
.nav-item.active { background: #eef2ff; color: var(--primary); font-weight: 500; }
|
|
718
|
+
.nav-item-icon { font-size: 1.125rem; }
|
|
719
|
+
/* 主内容 */
|
|
720
|
+
.main-content {
|
|
721
|
+
flex: 1;
|
|
722
|
+
padding: 2rem;
|
|
723
|
+
overflow-y: auto;
|
|
724
|
+
}
|
|
725
|
+
.page-header {
|
|
726
|
+
margin-bottom: 2rem;
|
|
727
|
+
}
|
|
728
|
+
.page-title {
|
|
729
|
+
font-size: 1.5rem;
|
|
730
|
+
font-weight: 600;
|
|
731
|
+
margin-bottom: 0.5rem;
|
|
732
|
+
}
|
|
733
|
+
.page-desc {
|
|
734
|
+
color: var(--text-secondary);
|
|
735
|
+
}
|
|
736
|
+
/* 卡片 */
|
|
737
|
+
.cards {
|
|
738
|
+
display: grid;
|
|
739
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
740
|
+
gap: 1.5rem;
|
|
741
|
+
margin-bottom: 2rem;
|
|
742
|
+
}
|
|
743
|
+
.card {
|
|
744
|
+
background: var(--bg-card);
|
|
745
|
+
border-radius: 0.75rem;
|
|
746
|
+
padding: 1.5rem;
|
|
747
|
+
border: 1px solid var(--border);
|
|
748
|
+
}
|
|
749
|
+
.card-header {
|
|
750
|
+
display: flex;
|
|
751
|
+
align-items: center;
|
|
752
|
+
justify-content: space-between;
|
|
753
|
+
margin-bottom: 1rem;
|
|
754
|
+
}
|
|
755
|
+
.card-title {
|
|
756
|
+
font-size: 0.875rem;
|
|
757
|
+
font-weight: 500;
|
|
758
|
+
color: var(--text-secondary);
|
|
759
|
+
}
|
|
760
|
+
.card-icon {
|
|
761
|
+
font-size: 1.5rem;
|
|
762
|
+
}
|
|
763
|
+
.card-value {
|
|
764
|
+
font-size: 2rem;
|
|
765
|
+
font-weight: 600;
|
|
766
|
+
margin-bottom: 0.25rem;
|
|
767
|
+
}
|
|
768
|
+
.card-label {
|
|
769
|
+
font-size: 0.875rem;
|
|
770
|
+
color: var(--text-secondary);
|
|
771
|
+
}
|
|
772
|
+
/* 状态指示器 */
|
|
773
|
+
.status-badge {
|
|
774
|
+
display: inline-flex;
|
|
775
|
+
align-items: center;
|
|
776
|
+
gap: 0.375rem;
|
|
777
|
+
padding: 0.25rem 0.75rem;
|
|
778
|
+
border-radius: 9999px;
|
|
779
|
+
font-size: 0.75rem;
|
|
780
|
+
font-weight: 500;
|
|
781
|
+
}
|
|
782
|
+
.status-badge.online { background: #dcfce7; color: #166534; }
|
|
783
|
+
.status-badge.offline { background: #fee2e2; color: #991b1b; }
|
|
784
|
+
.status-dot {
|
|
785
|
+
width: 6px;
|
|
786
|
+
height: 6px;
|
|
787
|
+
border-radius: 50%;
|
|
788
|
+
background: currentColor;
|
|
789
|
+
}
|
|
790
|
+
/* 表格 */
|
|
791
|
+
.table-container {
|
|
792
|
+
background: var(--bg-card);
|
|
793
|
+
border-radius: 0.75rem;
|
|
794
|
+
border: 1px solid var(--border);
|
|
795
|
+
overflow: hidden;
|
|
796
|
+
}
|
|
797
|
+
.table-header {
|
|
798
|
+
padding: 1rem 1.5rem;
|
|
799
|
+
border-bottom: 1px solid var(--border);
|
|
800
|
+
display: flex;
|
|
801
|
+
align-items: center;
|
|
802
|
+
justify-content: space-between;
|
|
803
|
+
}
|
|
804
|
+
.table-title {
|
|
805
|
+
font-weight: 600;
|
|
806
|
+
}
|
|
807
|
+
table {
|
|
808
|
+
width: 100%;
|
|
809
|
+
border-collapse: collapse;
|
|
810
|
+
}
|
|
811
|
+
th, td {
|
|
812
|
+
padding: 0.875rem 1.5rem;
|
|
813
|
+
text-align: left;
|
|
814
|
+
border-bottom: 1px solid var(--border);
|
|
815
|
+
}
|
|
816
|
+
th {
|
|
817
|
+
background: var(--bg);
|
|
818
|
+
font-size: 0.75rem;
|
|
819
|
+
font-weight: 600;
|
|
820
|
+
color: var(--text-secondary);
|
|
821
|
+
text-transform: uppercase;
|
|
822
|
+
letter-spacing: 0.05em;
|
|
823
|
+
}
|
|
824
|
+
tr:last-child td { border-bottom: none; }
|
|
825
|
+
tr:hover td { background: var(--bg); }
|
|
826
|
+
/* 按钮 */
|
|
827
|
+
.btn {
|
|
828
|
+
display: inline-flex;
|
|
829
|
+
align-items: center;
|
|
830
|
+
gap: 0.5rem;
|
|
831
|
+
padding: 0.5rem 1rem;
|
|
832
|
+
border-radius: 0.5rem;
|
|
833
|
+
font-size: 0.875rem;
|
|
834
|
+
font-weight: 500;
|
|
835
|
+
cursor: pointer;
|
|
836
|
+
border: none;
|
|
837
|
+
transition: all 0.15s;
|
|
838
|
+
}
|
|
839
|
+
.btn-primary { background: var(--primary); color: white; }
|
|
840
|
+
.btn-primary:hover { background: var(--primary-hover); }
|
|
841
|
+
.btn-secondary { background: var(--bg); color: var(--text); border: 1px solid var(--border); }
|
|
842
|
+
.btn-secondary:hover { background: var(--border); }
|
|
843
|
+
.btn-danger { background: var(--error); color: white; }
|
|
844
|
+
.btn-danger:hover { opacity: 0.9; }
|
|
845
|
+
/* 隐藏视图 */
|
|
846
|
+
.view { display: none; }
|
|
847
|
+
.view.active { display: block; }
|
|
848
|
+
/* 空状态 */
|
|
849
|
+
.empty-state {
|
|
850
|
+
text-align: center;
|
|
851
|
+
padding: 3rem;
|
|
852
|
+
color: var(--text-secondary);
|
|
853
|
+
}
|
|
854
|
+
.empty-state-icon { font-size: 3rem; margin-bottom: 1rem; }
|
|
855
|
+
/* 模型卡片 */
|
|
856
|
+
.model-card {
|
|
857
|
+
background: var(--bg-card);
|
|
858
|
+
border: 1px solid var(--border);
|
|
859
|
+
border-radius: 0.75rem;
|
|
860
|
+
padding: 1rem 1.25rem;
|
|
861
|
+
}
|
|
862
|
+
.model-name { font-weight: 600; margin-bottom: 0.25rem; }
|
|
863
|
+
.model-id { font-size: 0.75rem; color: var(--text-secondary); font-family: monospace; }
|
|
864
|
+
.model-tags { display: flex; gap: 0.5rem; margin-top: 0.75rem; }
|
|
865
|
+
.model-tag {
|
|
866
|
+
padding: 0.125rem 0.5rem;
|
|
867
|
+
border-radius: 0.25rem;
|
|
868
|
+
font-size: 0.75rem;
|
|
869
|
+
background: var(--bg);
|
|
870
|
+
color: var(--text-secondary);
|
|
871
|
+
}
|
|
872
|
+
.model-tag.vision { background: #dbeafe; color: #1e40af; }
|
|
873
|
+
.model-tag.reasoning { background: #fef3c7; color: #92400e; }
|
|
874
|
+
/* 日志 */
|
|
875
|
+
.log-container {
|
|
876
|
+
background: #1e293b;
|
|
877
|
+
border-radius: 0.75rem;
|
|
878
|
+
padding: 1rem;
|
|
879
|
+
max-height: 400px;
|
|
880
|
+
overflow-y: auto;
|
|
881
|
+
font-family: "SF Mono", Monaco, monospace;
|
|
882
|
+
font-size: 0.8125rem;
|
|
883
|
+
line-height: 1.6;
|
|
884
|
+
}
|
|
885
|
+
.log-entry { color: #e2e8f0; }
|
|
886
|
+
.log-entry.info { color: #38bdf8; }
|
|
887
|
+
.log-entry.warn { color: #fbbf24; }
|
|
888
|
+
.log-entry.error { color: #f87171; }
|
|
889
|
+
.log-entry .time { color: #64748b; }
|
|
890
|
+
</style>
|
|
891
|
+
</head>
|
|
892
|
+
<body>
|
|
893
|
+
<div class="layout">
|
|
894
|
+
<aside class="sidebar">
|
|
895
|
+
<div class="sidebar-header">
|
|
896
|
+
<div class="sidebar-logo">
|
|
897
|
+
<span>🐼</span>
|
|
898
|
+
<span>${assistantName}</span>
|
|
899
|
+
</div>
|
|
900
|
+
</div>
|
|
901
|
+
<div class="nav-section">监控</div>
|
|
902
|
+
<div class="nav-item active" data-view="overview">
|
|
903
|
+
<span class="nav-item-icon">📊</span>
|
|
904
|
+
<span>概览</span>
|
|
905
|
+
</div>
|
|
906
|
+
<div class="nav-item" data-view="sessions">
|
|
907
|
+
<span class="nav-item-icon">💬</span>
|
|
908
|
+
<span>会话</span>
|
|
909
|
+
</div>
|
|
910
|
+
<div class="nav-section">配置</div>
|
|
911
|
+
<div class="nav-item" data-view="providers">
|
|
912
|
+
<span class="nav-item-icon">🤖</span>
|
|
913
|
+
<span>模型提供商</span>
|
|
914
|
+
</div>
|
|
915
|
+
<div class="nav-item" data-view="channels">
|
|
916
|
+
<span class="nav-item-icon">📱</span>
|
|
917
|
+
<span>通讯通道</span>
|
|
918
|
+
</div>
|
|
919
|
+
<div class="nav-section">工具</div>
|
|
920
|
+
<div class="nav-item" data-view="logs">
|
|
921
|
+
<span class="nav-item-icon">📋</span>
|
|
922
|
+
<span>日志</span>
|
|
923
|
+
</div>
|
|
924
|
+
<div style="flex:1"></div>
|
|
925
|
+
<a href="/" class="nav-item">
|
|
926
|
+
<span class="nav-item-icon">💬</span>
|
|
927
|
+
<span>返回聊天</span>
|
|
928
|
+
</a>
|
|
929
|
+
</aside>
|
|
930
|
+
|
|
931
|
+
<main class="main-content">
|
|
932
|
+
<!-- 概览视图 -->
|
|
933
|
+
<div class="view active" id="view-overview">
|
|
934
|
+
<div class="page-header">
|
|
935
|
+
<h1 class="page-title">系统概览</h1>
|
|
936
|
+
<p class="page-desc">查看系统运行状态和关键指标</p>
|
|
937
|
+
</div>
|
|
938
|
+
<div class="cards">
|
|
939
|
+
<div class="card">
|
|
940
|
+
<div class="card-header">
|
|
941
|
+
<span class="card-title">连接状态</span>
|
|
942
|
+
<span class="card-icon">🔌</span>
|
|
943
|
+
</div>
|
|
944
|
+
<div id="connection-status">
|
|
945
|
+
<span class="status-badge offline"><span class="status-dot"></span>连接中</span>
|
|
946
|
+
</div>
|
|
947
|
+
</div>
|
|
948
|
+
<div class="card">
|
|
949
|
+
<div class="card-header">
|
|
950
|
+
<span class="card-title">运行时间</span>
|
|
951
|
+
<span class="card-icon">⏱️</span>
|
|
952
|
+
</div>
|
|
953
|
+
<div class="card-value" id="uptime">--</div>
|
|
954
|
+
<div class="card-label">自服务启动</div>
|
|
955
|
+
</div>
|
|
956
|
+
<div class="card">
|
|
957
|
+
<div class="card-header">
|
|
958
|
+
<span class="card-title">活跃会话</span>
|
|
959
|
+
<span class="card-icon">👥</span>
|
|
960
|
+
</div>
|
|
961
|
+
<div class="card-value" id="session-count">0</div>
|
|
962
|
+
<div class="card-label">当前连接数</div>
|
|
963
|
+
</div>
|
|
964
|
+
<div class="card">
|
|
965
|
+
<div class="card-header">
|
|
966
|
+
<span class="card-title">默认模型</span>
|
|
967
|
+
<span class="card-icon">🧠</span>
|
|
968
|
+
</div>
|
|
969
|
+
<div class="card-value" style="font-size:1rem;word-break:break-all">${defaultModel}</div>
|
|
970
|
+
<div class="card-label">${defaultProvider}</div>
|
|
971
|
+
</div>
|
|
972
|
+
</div>
|
|
973
|
+
<div class="table-container">
|
|
974
|
+
<div class="table-header">
|
|
975
|
+
<span class="table-title">系统信息</span>
|
|
976
|
+
<button class="btn btn-secondary" onclick="refreshStatus()">刷新</button>
|
|
977
|
+
</div>
|
|
978
|
+
<table>
|
|
979
|
+
<tbody id="system-info">
|
|
980
|
+
<tr><td>版本</td><td id="version">--</td></tr>
|
|
981
|
+
<tr><td>模型提供商</td><td id="provider-count">--</td></tr>
|
|
982
|
+
<tr><td>通讯通道</td><td id="channel-count">--</td></tr>
|
|
983
|
+
</tbody>
|
|
984
|
+
</table>
|
|
985
|
+
</div>
|
|
986
|
+
</div>
|
|
987
|
+
|
|
988
|
+
<!-- 会话视图 -->
|
|
989
|
+
<div class="view" id="view-sessions">
|
|
990
|
+
<div class="page-header">
|
|
991
|
+
<h1 class="page-title">会话管理</h1>
|
|
992
|
+
<p class="page-desc">查看和管理当前活跃的聊天会话</p>
|
|
993
|
+
</div>
|
|
994
|
+
<div class="table-container">
|
|
995
|
+
<div class="table-header">
|
|
996
|
+
<span class="table-title">活跃会话</span>
|
|
997
|
+
<button class="btn btn-secondary" onclick="refreshSessions()">刷新</button>
|
|
998
|
+
</div>
|
|
999
|
+
<table>
|
|
1000
|
+
<thead>
|
|
1001
|
+
<tr>
|
|
1002
|
+
<th>会话 ID</th>
|
|
1003
|
+
<th>通道</th>
|
|
1004
|
+
<th>消息数</th>
|
|
1005
|
+
<th>最后活跃</th>
|
|
1006
|
+
<th>操作</th>
|
|
1007
|
+
</tr>
|
|
1008
|
+
</thead>
|
|
1009
|
+
<tbody id="sessions-list">
|
|
1010
|
+
<tr><td colspan="5" class="empty-state">暂无活跃会话</td></tr>
|
|
1011
|
+
</tbody>
|
|
1012
|
+
</table>
|
|
1013
|
+
</div>
|
|
1014
|
+
</div>
|
|
1015
|
+
|
|
1016
|
+
<!-- 模型提供商视图 -->
|
|
1017
|
+
<div class="view" id="view-providers">
|
|
1018
|
+
<div class="page-header">
|
|
1019
|
+
<h1 class="page-title">模型提供商</h1>
|
|
1020
|
+
<p class="page-desc">查看已配置的 AI 模型提供商和可用模型</p>
|
|
1021
|
+
</div>
|
|
1022
|
+
<div class="cards" id="providers-list">
|
|
1023
|
+
<div class="empty-state">
|
|
1024
|
+
<div class="empty-state-icon">🤖</div>
|
|
1025
|
+
<p>加载中...</p>
|
|
1026
|
+
</div>
|
|
1027
|
+
</div>
|
|
1028
|
+
</div>
|
|
1029
|
+
|
|
1030
|
+
<!-- 通讯通道视图 -->
|
|
1031
|
+
<div class="view" id="view-channels">
|
|
1032
|
+
<div class="page-header">
|
|
1033
|
+
<h1 class="page-title">通讯通道</h1>
|
|
1034
|
+
<p class="page-desc">查看已配置的通讯平台连接状态</p>
|
|
1035
|
+
</div>
|
|
1036
|
+
<div class="table-container">
|
|
1037
|
+
<table>
|
|
1038
|
+
<thead>
|
|
1039
|
+
<tr>
|
|
1040
|
+
<th>通道</th>
|
|
1041
|
+
<th>状态</th>
|
|
1042
|
+
<th>类型</th>
|
|
1043
|
+
</tr>
|
|
1044
|
+
</thead>
|
|
1045
|
+
<tbody id="channels-list">
|
|
1046
|
+
<tr><td colspan="3" class="empty-state">加载中...</td></tr>
|
|
1047
|
+
</tbody>
|
|
1048
|
+
</table>
|
|
1049
|
+
</div>
|
|
1050
|
+
</div>
|
|
1051
|
+
|
|
1052
|
+
<!-- 日志视图 -->
|
|
1053
|
+
<div class="view" id="view-logs">
|
|
1054
|
+
<div class="page-header">
|
|
1055
|
+
<h1 class="page-title">系统日志</h1>
|
|
1056
|
+
<p class="page-desc">实时查看系统运行日志</p>
|
|
1057
|
+
</div>
|
|
1058
|
+
<div class="log-container" id="log-container">
|
|
1059
|
+
<div class="log-entry info"><span class="time">[--:--:--]</span> 等待连接...</div>
|
|
1060
|
+
</div>
|
|
1061
|
+
</div>
|
|
1062
|
+
</main>
|
|
1063
|
+
</div>
|
|
1064
|
+
|
|
1065
|
+
<script>
|
|
1066
|
+
let ws = null;
|
|
1067
|
+
let pendingRequests = new Map();
|
|
1068
|
+
let requestId = 0;
|
|
1069
|
+
let systemStatus = null;
|
|
1070
|
+
|
|
1071
|
+
// 导航
|
|
1072
|
+
document.querySelectorAll('.nav-item[data-view]').forEach(item => {
|
|
1073
|
+
item.addEventListener('click', () => {
|
|
1074
|
+
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
1075
|
+
item.classList.add('active');
|
|
1076
|
+
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
|
|
1077
|
+
document.getElementById('view-' + item.dataset.view).classList.add('active');
|
|
1078
|
+
});
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
// WebSocket 连接
|
|
1082
|
+
function connect() {
|
|
1083
|
+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
1084
|
+
ws = new WebSocket(protocol + '//' + location.host + '/ws');
|
|
1085
|
+
|
|
1086
|
+
ws.onopen = () => {
|
|
1087
|
+
document.getElementById('connection-status').innerHTML =
|
|
1088
|
+
'<span class="status-badge online"><span class="status-dot"></span>已连接</span>';
|
|
1089
|
+
addLog('info', '已连接到服务器');
|
|
1090
|
+
refreshStatus();
|
|
1091
|
+
};
|
|
1092
|
+
|
|
1093
|
+
ws.onclose = () => {
|
|
1094
|
+
document.getElementById('connection-status').innerHTML =
|
|
1095
|
+
'<span class="status-badge offline"><span class="status-dot"></span>已断开</span>';
|
|
1096
|
+
addLog('warn', '连接已断开,正在重连...');
|
|
1097
|
+
setTimeout(connect, 3000);
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
ws.onmessage = (event) => {
|
|
1101
|
+
try {
|
|
1102
|
+
const frame = JSON.parse(event.data);
|
|
1103
|
+
if (frame.type === 'res') {
|
|
1104
|
+
const pending = pendingRequests.get(frame.id);
|
|
1105
|
+
if (pending) {
|
|
1106
|
+
pendingRequests.delete(frame.id);
|
|
1107
|
+
if (frame.ok) pending.resolve(frame.payload);
|
|
1108
|
+
else pending.reject(new Error(frame.error?.message || 'Unknown error'));
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
} catch (e) {
|
|
1112
|
+
console.error('Parse error:', e);
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function request(method, params) {
|
|
1118
|
+
return new Promise((resolve, reject) => {
|
|
1119
|
+
const id = String(++requestId);
|
|
1120
|
+
pendingRequests.set(id, { resolve, reject });
|
|
1121
|
+
ws.send(JSON.stringify({ type: 'req', id, method, params }));
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
async function refreshStatus() {
|
|
1126
|
+
try {
|
|
1127
|
+
systemStatus = await request('status.get');
|
|
1128
|
+
updateOverview(systemStatus);
|
|
1129
|
+
updateProviders(systemStatus);
|
|
1130
|
+
updateChannels(systemStatus);
|
|
1131
|
+
addLog('info', '状态已刷新');
|
|
1132
|
+
} catch (e) {
|
|
1133
|
+
addLog('error', '获取状态失败: ' + e.message);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function updateOverview(status) {
|
|
1138
|
+
document.getElementById('version').textContent = status.version || '--';
|
|
1139
|
+
document.getElementById('session-count').textContent = status.sessions || 0;
|
|
1140
|
+
document.getElementById('provider-count').textContent = (status.providers || []).length + ' 个';
|
|
1141
|
+
document.getElementById('channel-count').textContent = (status.channels || []).length + ' 个';
|
|
1142
|
+
|
|
1143
|
+
const uptime = status.uptime || 0;
|
|
1144
|
+
const hours = Math.floor(uptime / 3600000);
|
|
1145
|
+
const mins = Math.floor((uptime % 3600000) / 60000);
|
|
1146
|
+
document.getElementById('uptime').textContent = hours + 'h ' + mins + 'm';
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function updateProviders(status) {
|
|
1150
|
+
const providers = status.providers || [];
|
|
1151
|
+
const container = document.getElementById('providers-list');
|
|
1152
|
+
|
|
1153
|
+
if (providers.length === 0) {
|
|
1154
|
+
container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🤖</div><p>暂无已配置的提供商</p></div>';
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
container.innerHTML = providers.map(p => \`
|
|
1159
|
+
<div class="card">
|
|
1160
|
+
<div class="card-header">
|
|
1161
|
+
<span class="card-title">\${p.name || p.id}</span>
|
|
1162
|
+
<span class="status-badge \${p.available ? 'online' : 'offline'}">
|
|
1163
|
+
<span class="status-dot"></span>\${p.available ? '可用' : '不可用'}
|
|
1164
|
+
</span>
|
|
1165
|
+
</div>
|
|
1166
|
+
<div class="card-label">ID: \${p.id}</div>
|
|
1167
|
+
</div>
|
|
1168
|
+
\`).join('');
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function updateChannels(status) {
|
|
1172
|
+
const channels = status.channels || [];
|
|
1173
|
+
const tbody = document.getElementById('channels-list');
|
|
1174
|
+
|
|
1175
|
+
// 添加 WebChat
|
|
1176
|
+
const allChannels = [
|
|
1177
|
+
{ id: 'webchat', name: 'WebChat', connected: true },
|
|
1178
|
+
...channels
|
|
1179
|
+
];
|
|
1180
|
+
|
|
1181
|
+
tbody.innerHTML = allChannels.map(c => \`
|
|
1182
|
+
<tr>
|
|
1183
|
+
<td>\${c.name || c.id}</td>
|
|
1184
|
+
<td>
|
|
1185
|
+
<span class="status-badge \${c.connected ? 'online' : 'offline'}">
|
|
1186
|
+
<span class="status-dot"></span>\${c.connected ? '已连接' : '未连接'}
|
|
1187
|
+
</span>
|
|
1188
|
+
</td>
|
|
1189
|
+
<td>\${c.id}</td>
|
|
1190
|
+
</tr>
|
|
1191
|
+
\`).join('');
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function refreshSessions() {
|
|
1195
|
+
// 会话数据通过 status 获取
|
|
1196
|
+
addLog('info', '会话列表已刷新');
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function addLog(level, message) {
|
|
1200
|
+
const container = document.getElementById('log-container');
|
|
1201
|
+
const time = new Date().toLocaleTimeString();
|
|
1202
|
+
const entry = document.createElement('div');
|
|
1203
|
+
entry.className = 'log-entry ' + level;
|
|
1204
|
+
entry.innerHTML = '<span class="time">[' + time + ']</span> ' + message;
|
|
1205
|
+
container.appendChild(entry);
|
|
1206
|
+
container.scrollTop = container.scrollHeight;
|
|
1207
|
+
|
|
1208
|
+
// 限制日志数量
|
|
1209
|
+
while (container.children.length > 100) {
|
|
1210
|
+
container.removeChild(container.firstChild);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// 启动
|
|
1215
|
+
connect();
|
|
1216
|
+
</script>
|
|
1217
|
+
</body>
|
|
1218
|
+
</html>`;
|
|
1219
|
+
}
|
|
1220
|
+
/** 处理静态文件请求 */
|
|
1221
|
+
export function handleStaticRequest(req, res, options) {
|
|
1222
|
+
const url = req.url || "/";
|
|
1223
|
+
const pathname = url.split("?")[0] || "/";
|
|
1224
|
+
// WebSocket 路径跳过
|
|
1225
|
+
if (pathname === "/ws") {
|
|
1226
|
+
return false;
|
|
1227
|
+
}
|
|
1228
|
+
// API 路径跳过
|
|
1229
|
+
if (pathname.startsWith("/api/") || pathname.startsWith("/webhook/") || pathname.startsWith("/feishu/") || pathname.startsWith("/dingtalk/")) {
|
|
1230
|
+
return false;
|
|
1231
|
+
}
|
|
1232
|
+
// 健康检查跳过
|
|
1233
|
+
if (pathname === "/health") {
|
|
1234
|
+
return false;
|
|
1235
|
+
}
|
|
1236
|
+
// Control UI
|
|
1237
|
+
if (pathname === "/control" || pathname === "/control/") {
|
|
1238
|
+
const html = getControlHtml(options.config);
|
|
1239
|
+
res.writeHead(200, {
|
|
1240
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1241
|
+
"Content-Length": Buffer.byteLength(html),
|
|
1242
|
+
});
|
|
1243
|
+
res.end(html);
|
|
1244
|
+
return true;
|
|
1245
|
+
}
|
|
1246
|
+
// 根路径或 index.html - 返回 WebChat HTML
|
|
1247
|
+
if (pathname === "/" || pathname === "/index.html") {
|
|
1248
|
+
const html = getEmbeddedHtml(options.config);
|
|
1249
|
+
res.writeHead(200, {
|
|
1250
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1251
|
+
"Content-Length": Buffer.byteLength(html),
|
|
1252
|
+
});
|
|
1253
|
+
res.end(html);
|
|
1254
|
+
return true;
|
|
1255
|
+
}
|
|
1256
|
+
// 其他静态文件 - 暂不支持外部文件
|
|
1257
|
+
// 可以后续添加从 public 目录读取文件的功能
|
|
1258
|
+
return false;
|
|
1259
|
+
}
|
|
1260
|
+
//# sourceMappingURL=static.js.map
|