@hina114514/chaite 1.9.9 → 2.0.1

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.
@@ -3,310 +3,265 @@
3
3
  <head>
4
4
  <meta charset="UTF-8"/>
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1"/>
6
- <title>Chaite</title>
6
+ <title>Chaite · 光影解码</title>
7
7
  <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
8
8
  <style>
9
9
  *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
10
10
 
11
11
  :root{
12
- --c-bg:#090c15;
13
- --c-surface:rgba(18,22,40,0.7);
14
- --c-surface2:rgba(26,30,52,0.8);
15
- --c-surface3:rgba(38,42,66,0.6);
16
- --c-border:rgba(255,255,255,0.06);
17
- --c-border2:rgba(255,255,255,0.1);
18
- --c-text:#e4e7f0;
19
- --c-dim:#7e83a0;
20
- --c-faint:#52577a;
21
- --c-accent:#5b8def;
22
- --c-accent2:rgba(91,141,239,0.15);
23
- --c-green:#4ade80;
24
- --c-green2:rgba(74,222,128,0.12);
25
- --c-red:#f87171;
26
- --c-red2:rgba(248,113,113,0.12);
27
- --c-yellow:#facc15;
28
- --c-purple:#a78bfa;
29
- --r:12px;
30
- --r2:20px;
31
- --ease: cubic-bezier(.16,1,.3,1);
32
- --ease2: cubic-bezier(.7,0,.3,1);
12
+ --c-bg:#0d0b14;
13
+ --c-surface:rgba(24,18,36,0.78);
14
+ --c-surface2:rgba(32,24,48,0.88);
15
+ --c-surface3:rgba(44,36,62,0.68);
16
+ --c-border:rgba(255,183,197,0.08);
17
+ --c-border2:rgba(255,183,197,0.15);
18
+ --c-text:#eeeaf4;
19
+ --c-dim:#a89cb8;
20
+ --c-faint:#6e6480;
21
+ --c-accent:#ff9aa2;
22
+ --c-accent2:rgba(255,154,162,0.14);
23
+ --c-accent-glow:rgba(255,154,162,0.25);
24
+ --c-pink:#ffb7c5;
25
+ --c-pink2:rgba(255,183,197,0.1);
26
+ --c-green:#8fd19e;
27
+ --c-green2:rgba(143,209,158,0.12);
28
+ --c-red:#ff8589;
29
+ --c-red2:rgba(255,133,137,0.12);
30
+ --c-yellow:#ffe0a0;
31
+ --c-purple:#c9b1ff;
32
+ --r:10px;
33
+ --r2:16px;
34
+ --ease:cubic-bezier(.16,1,.3,1);
33
35
  }
34
36
 
35
37
  body{
36
38
  background:var(--c-bg);
37
39
  color:var(--c-text);
38
- font-family: 'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
39
- font-size:14px;
40
- line-height:1.5;
41
- min-height:100vh;
42
- overflow-x:hidden;
40
+ font-family:'PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif;
41
+ font-size:14px;line-height:1.6;min-height:100vh;overflow-x:hidden;
42
+ -webkit-font-smoothing:antialiased;
43
43
  }
44
44
  body::before{
45
- content:'';position:fixed;inset:0;z-index:-1;
45
+ content:'';position:fixed;inset:0;z-index:-1;pointer-events:none;
46
46
  background:
47
- radial-gradient(ellipse 80% 60% at 0% 0%,rgba(91,141,239,.06),transparent),
48
- radial-gradient(ellipse 60% 50% at 100% 100%,rgba(91,141,239,.04),transparent);
49
- pointer-events:none;
47
+ radial-gradient(ellipse 70% 50% at 0% 0%,rgba(255,154,162,.1),transparent 70%),
48
+ radial-gradient(ellipse 50% 40% at 100% 20%,rgba(255,183,197,.06),transparent 60%),
49
+ radial-gradient(ellipse 50% 50% at 50% 90%,rgba(201,177,255,.05),transparent 60%);
50
50
  }
51
51
 
52
+ /* Sakura petals (CSS only) */
53
+ @keyframes fall{0%{transform:translateY(-10vh) rotate(0deg);opacity:1}100%{transform:translateY(110vh) rotate(720deg);opacity:0}}
54
+ @keyframes sway{0%,100%{margin-left:0}50%{margin-left:30px}}
55
+ .petal{position:fixed;top:-20px;z-index:-1;pointer-events:none;font-size:16px;opacity:.3;animation:fall linear infinite,sway 4s ease-in-out infinite}
56
+ .petal:nth-child(1){left:5%;animation-duration:12s,4s;animation-delay:0s;font-size:14px}
57
+ .petal:nth-child(2){left:15%;animation-duration:10s,3.5s;animation-delay:2s;font-size:18px}
58
+ .petal:nth-child(3){left:25%;animation-duration:14s,4.5s;animation-delay:4s;font-size:12px}
59
+ .petal:nth-child(4){left:40%;animation-duration:11s,3.8s;animation-delay:1s;font-size:20px}
60
+ .petal:nth-child(5){left:55%;animation-duration:13s,4.2s;animation-delay:3s;font-size:15px}
61
+ .petal:nth-child(6){left:65%;animation-duration:9s,3.2s;animation-delay:5s;font-size:17px}
62
+ .petal:nth-child(7){left:75%;animation-duration:15s,4.8s;animation-delay:0s;font-size:13px}
63
+ .petal:nth-child(8){left:85%;animation-duration:10.5s,3.6s;animation-delay:3.5s;font-size:16px}
64
+ .petal:nth-child(9){left:92%;animation-duration:12.5s,4s;animation-delay:1.5s;font-size:14px}
65
+ .petal:nth-child(10){left:50%;animation-duration:11.5s,3.9s;animation-delay:6s;font-size:19px}
66
+
52
67
  #app{min-height:100vh}
53
68
 
54
69
  /* Login */
55
- .login-wrap{
56
- display:flex;align-items:center;justify-content:center;min-height:100vh;
57
- padding:20px;
58
- }
70
+ .login-wrap{display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px}
59
71
  .login-card{
60
- width:100%;max-width:400px;
61
- background:var(--c-surface);
62
- border:1px solid var(--c-border);
63
- border-radius:var(--r2);
64
- padding:48px 36px;
65
- backdrop-filter:blur(20px);
66
- -webkit-backdrop-filter:blur(20px);
67
- text-align:center;
68
- animation: fadeUp .6s var(--ease) both;
72
+ width:100%;max-width:420px;background:var(--c-surface);border:1px solid var(--c-border);
73
+ border-radius:20px;padding:52px 40px;
74
+ backdrop-filter:blur(24px) saturate(1.4);
75
+ box-shadow:0 24px 80px rgba(0,0,0,.5),inset 0 1px 0 rgba(255,183,197,.06);
76
+ text-align:center;animation:fadeUp .5s var(--ease) both
69
77
  }
70
78
  .login-card h1{
71
- font-size:28px;font-weight:700;letter-spacing:-.5px;margin-bottom:4px;
72
- background:linear-gradient(135deg,#5b8def,#8b5cf6);-webkit-background-clip:text;-webkit-text-fill-color:transparent;
79
+ font-size:32px;font-weight:700;letter-spacing:-.5px;margin-bottom:4px;
80
+ background:linear-gradient(135deg,#ffb7c5,#ff9aa2,#ffb7b2,#c9b1ff);
81
+ -webkit-background-clip:text;-webkit-text-fill-color:transparent;background-size:200% auto;
82
+ animation:logoShine 3s linear infinite
73
83
  }
74
- .login-card .sub{color:var(--c-dim);font-size:13px;margin-bottom:28px}
75
- .login-card input{
76
- width:100%;padding:11px 14px;border-radius:10px;border:1px solid var(--c-border);
77
- background:rgba(255,255,255,.03);color:var(--c-text);font-size:14px;outline:none;
78
- transition:border-color .2s;
79
- }
80
- .login-card input:focus{border-color:var(--c-accent)}
81
- .login-card .err{color:var(--c-red);font-size:13px;margin:8px 0;min-height:20px}
82
- .login-card .hint{font-size:11px;color:var(--c-faint);margin-top:14px}
83
- .login-card .hint code{background:var(--c-surface2);padding:2px 6px;border-radius:4px;font-size:11px}
84
+ @keyframes logoShine{0%{background-position:0% center}100%{background-position:200% center}}
85
+ .login-card .sub{color:var(--c-dim);font-size:13px;margin-bottom:32px;letter-spacing:.2px}
86
+ .login-card input{width:100%;padding:12px 16px;border-radius:10px;border:1px solid var(--c-border);background:rgba(255,255,255,.03);color:var(--c-text);font-size:14px;outline:none;transition:all .2s}
87
+ .login-card input:focus{border-color:var(--c-accent);box-shadow:0 0 0 3px var(--c-accent-glow)}
88
+ .login-card .err{color:var(--c-red);font-size:13px;margin:10px 0;min-height:20px}
89
+ .login-card .hint{font-size:11px;color:var(--c-faint);margin-top:16px}
90
+ .login-card .hint code{background:var(--c-surface2);padding:2px 8px;border-radius:4px;font-size:11px;color:var(--c-pink)}
84
91
 
85
92
  /* Buttons */
86
- .btn{
87
- display:inline-flex;align-items:center;justify-content:center;gap:6px;
88
- padding:7px 14px;border-radius:8px;font-size:12.5px;font-weight:500;
89
- border:1px solid var(--c-border);color:var(--c-text);
90
- background:var(--c-surface2);cursor:pointer;
91
- transition:all .2s var(--ease);
92
- white-space:nowrap;
93
- }
93
+ .btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:8px 16px;border-radius:10px;font-size:13px;font-weight:500;border:1px solid var(--c-border);color:var(--c-text);background:var(--c-surface2);cursor:pointer;transition:all .2s var(--ease);white-space:nowrap;letter-spacing:.2px}
94
94
  .btn:hover{background:var(--c-surface3);border-color:var(--c-border2);transform:translateY(-1px)}
95
- .btn:active{transform:translateY(0) scale(.98)}
96
- .btn:disabled{opacity:.4;pointer-events:none}
97
- .btn.pri{
98
- background:var(--c-accent);border-color:var(--c-accent);color:#fff;
99
- box-shadow:0 2px 12px rgba(91,141,239,.25);
100
- }
101
- .btn.pri:hover{box-shadow:0 4px 20px rgba(91,141,239,.4);background:#6b9df5}
102
- .btn.danger{color:var(--c-red);border-color:rgba(248,113,113,.25)}
95
+ .btn:active{transform:translateY(0) scale(.97)}
96
+ .btn:disabled{opacity:.35;pointer-events:none}
97
+ .btn.pri{background:linear-gradient(135deg,#ff9aa2,#ff8589);border-color:transparent;color:#fff;box-shadow:0 2px 14px var(--c-accent-glow)}
98
+ .btn.pri:hover{box-shadow:0 4px 24px rgba(255,154,162,.4);background:linear-gradient(135deg,#ffb7c5,#ff9aa2)}
99
+ .btn.danger{color:var(--c-red);border-color:rgba(255,133,137,.2)}
103
100
  .btn.danger:hover{background:var(--c-red2)}
104
- .btn.sm{padding:4px 10px;font-size:11px;border-radius:6px}
101
+ .btn.sm{padding:5px 12px;font-size:11.5px;border-radius:7px}
105
102
  .btn.ghost{background:transparent;border-color:transparent;color:var(--c-dim)}
106
- .btn.ghost:hover{color:var(--c-text);background:rgba(255,255,255,.04)}
103
+ .btn.ghost:hover{color:var(--c-text);background:rgba(255,255,255,.03)}
107
104
  .btn .spin{display:inline-block;width:14px;height:14px;border:2px solid rgba(255,255,255,.2);border-top-color:#fff;border-radius:50%;animation:spin .6s linear infinite}
105
+ .btn .icon-s{margin-right:2px}
108
106
 
109
107
  /* Layout */
110
108
  .layout{display:flex;min-height:100vh}
111
- .sidebar{
112
- width:220px;min-width:220px;
113
- background:var(--c-surface);border-right:1px solid var(--c-border);
114
- display:flex;flex-direction:column;
115
- backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
116
- transition:transform .35s var(--ease);
117
- z-index:50;
118
- }
119
- .sidebar-logo{padding:24px 20px 20px}
109
+ .sidebar{width:230px;min-width:230px;background:rgba(16,12,24,0.82);border-right:1px solid var(--c-border);display:flex;flex-direction:column;backdrop-filter:blur(20px);box-shadow:2px 0 40px rgba(0,0,0,.35);transition:transform .35s var(--ease);z-index:50}
110
+ .sidebar-logo{padding:26px 20px 18px;border-bottom:1px solid var(--c-border)}
120
111
  .sidebar-logo h2{
121
112
  font-size:22px;font-weight:700;letter-spacing:-.5px;
122
- background:linear-gradient(135deg,#5b8def,#a78bfa);-webkit-background-clip:text;-webkit-text-fill-color:transparent;
113
+ background:linear-gradient(135deg,#ffb7c5,#ff9aa2,#ffb7b2);-webkit-background-clip:text;-webkit-text-fill-color:transparent
123
114
  }
124
115
  .sidebar-logo .ver{font-size:11px;color:var(--c-faint);margin-top:2px}
125
-
126
- .sidebar-nav{flex:1;padding:8px 12px}
127
- .nav-item{
128
- display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:8px;
129
- font-size:13px;color:var(--c-dim);cursor:pointer;
130
- transition:all .15s var(--ease);margin-bottom:2px;
131
- position:relative;
132
- }
116
+ .sidebar-nav{flex:1;padding:10px 10px}
117
+ .nav-item{display:flex;align-items:center;gap:10px;padding:10px 14px;border-radius:10px;font-size:13.5px;color:var(--c-dim);cursor:pointer;transition:all .15s var(--ease);margin-bottom:2px;position:relative}
133
118
  .nav-item:hover{color:var(--c-text);background:var(--c-surface2)}
134
- .nav-item.active{color:var(--c-accent);background:var(--c-accent2)}
135
- .nav-item .icon{width:18px;text-align:center;font-size:15px;line-height:1}
136
- .nav-item .badge-mini{
137
- margin-left:auto;font-size:10px;padding:1px 6px;border-radius:8px;
138
- background:var(--c-accent2);color:var(--c-accent);
139
- }
140
-
119
+ .nav-item.active{color:var(--c-accent);background:var(--c-accent2);font-weight:500}
120
+ .nav-item.active::after{content:'';position:absolute;left:-10px;top:50%;transform:translateY(-50%);width:3px;height:16px;border-radius:2px;background:var(--c-accent)}
121
+ .nav-item .icon{width:18px;text-align:center;font-size:15px;line-height:1;opacity:.8}
122
+ .nav-item.active .icon{opacity:1}
123
+ .nav-item .badge-mini{margin-left:auto;font-size:10px;padding:1px 7px;border-radius:8px;background:var(--c-accent2);color:var(--c-accent);font-weight:500}
141
124
  .sidebar-footer{padding:16px 20px;border-top:1px solid var(--c-border);font-size:11px;color:var(--c-faint)}
142
- .sidebar-footer .dot{display:inline-block;width:6px;height:6px;border-radius:50%;margin-right:6px}
143
- .sidebar-footer .dot.ok{background:var(--c-green);box-shadow:0 0 6px var(--c-green)}
125
+ .sidebar-footer .dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:8px}
126
+ .sidebar-footer .dot.ok{background:var(--c-green);box-shadow:0 0 8px var(--c-green);animation:pulse-dot 2s infinite}
144
127
 
145
128
  .main{flex:1;overflow-x:hidden;display:flex;flex-direction:column}
146
- .topbar{
147
- padding:16px 28px;display:flex;align-items:center;justify-content:space-between;
148
- border-bottom:1px solid var(--c-border);gap:12px;
149
- backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
150
- position:sticky;top:0;z-index:40;background:rgba(9,12,21,.8);
151
- }
152
- .topbar .t{font-size:15px;font-weight:600;color:var(--c-text)}
129
+ .topbar{padding:16px 28px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--c-border);gap:12px;backdrop-filter:blur(20px);position:sticky;top:0;z-index:40;background:rgba(13,11,20,.85)}
130
+ .topbar .t{font-size:15px;font-weight:600;color:var(--c-text);letter-spacing:-.3px;display:flex;align-items:center;gap:8px}
131
+ .topbar .t::before{content:'🌸';font-size:13px}
153
132
  .topbar .meta{display:flex;align-items:center;gap:10px}
154
133
  .topbar .refresh-text{font-size:11px;color:var(--c-faint)}
155
-
156
- .content{padding:24px 28px 40px;flex:1}
134
+ .content{padding:28px 28px 48px;flex:1}
157
135
 
158
136
  /* Toast */
159
- .toast{
160
- position:fixed;top:20px;right:20px;z-index:200;
161
- padding:10px 18px;border-radius:10px;font-size:13px;
162
- animation:slideRight .35s var(--ease) both;
163
- backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
164
- }
165
- .toast.ok{background:rgba(74,222,128,.9);color:#000}
166
- .toast.err{background:rgba(248,113,113,.9);color:#fff}
137
+ .toast{position:fixed;top:24px;right:24px;z-index:200;padding:11px 20px;border-radius:10px;font-size:13px;font-weight:500;animation:slideRight .35s var(--ease) both;backdrop-filter:blur(16px);box-shadow:0 8px 32px rgba(0,0,0,.4)}
138
+ .toast.ok{background:rgba(143,209,158,.92);color:#0a2816}
139
+ .toast.err{background:rgba(255,133,137,.92);color:#fff}
167
140
 
168
- /* Mobile menu toggle */
169
- .menu-toggle{
170
- display:none;width:36px;height:36px;align-items:center;justify-content:center;
171
- border:none;background:none;color:var(--c-text);cursor:pointer;font-size:20px;
172
- }
173
- .sidebar-overlay{display:none;position:fixed;inset:0;z-index:45;background:rgba(0,0,0,.5)}
141
+ .menu-toggle{display:none;width:36px;height:36px;align-items:center;justify-content:center;border:none;background:none;color:var(--c-text);cursor:pointer;font-size:20px}
142
+ .sidebar-overlay{display:none;position:fixed;inset:0;z-index:45;background:rgba(0,0,0,.5);backdrop-filter:blur(4px)}
174
143
 
175
144
  /* Cards */
176
- .card{
177
- background:var(--c-surface);border:1px solid var(--c-border);
178
- border-radius:var(--r2);padding:22px;margin-bottom:16px;
179
- backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
180
- animation: fadeUp .45s var(--ease) both;
181
- }
145
+ .card{background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r2);padding:24px;margin-bottom:18px;backdrop-filter:blur(20px);box-shadow:0 4px 24px rgba(0,0,0,.25),inset 0 1px 0 rgba(255,183,197,.04);animation:fadeUp .45s var(--ease) both;transition:border-color .3s}
146
+ .card:hover{border-color:var(--c-border2)}
182
147
  .card:nth-child(1){animation-delay:0s}
183
148
  .card:nth-child(2){animation-delay:.06s}
184
149
  .card:nth-child(3){animation-delay:.12s}
185
150
  .card:nth-child(4){animation-delay:.18s}
186
151
  .card:nth-child(5){animation-delay:.24s}
187
- .card-hd{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}
188
- .card-hd h3{font-size:14.5px;font-weight:600;display:flex;align-items:center;gap:8px}
152
+ .card-hd{display:flex;align-items:center;justify-content:space-between;margin-bottom:18px}
153
+ .card-hd h3{font-size:15px;font-weight:600;display:flex;align-items:center;gap:8px;letter-spacing:-.2px}
189
154
  .card-hd h3 .dot{width:7px;height:7px;border-radius:50%}
190
155
 
191
- /* Stat pills */
192
- .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-bottom:20px}
193
- .stat{
194
- background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r2);
195
- padding:18px 20px;text-align:center;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
196
- transition:transform .2s var(--ease),border-color .2s var(--ease);
197
- animation: fadeUp .4s var(--ease) both;
198
- }
199
- .stat:hover{transform:translateY(-3px);border-color:var(--c-accent)}
156
+ /* Stats */
157
+ .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:14px;margin-bottom:22px}
158
+ .stat{background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r2);padding:20px 22px;text-align:center;backdrop-filter:blur(20px);box-shadow:0 2px 16px rgba(0,0,0,.2),inset 0 1px 0 rgba(255,183,197,.03);transition:all .25s var(--ease);cursor:default;animation:fadeUp .4s var(--ease) both}
159
+ .stat:hover{transform:translateY(-3px);border-color:var(--c-accent);box-shadow:0 8px 28px rgba(0,0,0,.35),inset 0 1px 0 rgba(255,183,197,.08)}
200
160
  .stat:nth-child(1){animation-delay:0s}
201
161
  .stat:nth-child(2){animation-delay:.05s}
202
162
  .stat:nth-child(3){animation-delay:.1s}
203
163
  .stat:nth-child(4){animation-delay:.15s}
204
164
  .stat:nth-child(5){animation-delay:.2s}
205
165
  .stat:nth-child(6){animation-delay:.25s}
206
- .stat .num{font-size:26px;font-weight:700;color:var(--c-accent)}
207
- .stat .lbl{font-size:11px;color:var(--c-dim);margin-top:4px;text-transform:uppercase;letter-spacing:.5px}
166
+ .stat .num{font-size:28px;font-weight:700;color:var(--c-accent);letter-spacing:-1px}
167
+ .stat .lbl{font-size:11px;color:var(--c-dim);margin-top:5px;text-transform:uppercase;letter-spacing:.8px;font-weight:500}
208
168
 
209
169
  /* Table */
210
- .tbl-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch}
170
+ .tbl-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;border-radius:8px}
211
171
  .tbl{width:100%;border-collapse:collapse;font-size:13px}
212
- .tbl th{text-align:left;padding:10px 12px;color:var(--c-faint);font-weight:500;border-bottom:1px solid var(--c-border);font-size:11px;text-transform:uppercase;letter-spacing:.5px}
213
- .tbl td{padding:10px 12px;border-bottom:1px solid var(--c-border)}
172
+ .tbl th{text-align:left;padding:10px 14px;color:var(--c-faint);font-weight:500;border-bottom:1px solid var(--c-border);font-size:11px;text-transform:uppercase;letter-spacing:.6px}
173
+ .tbl td{padding:10px 14px;border-bottom:1px solid var(--c-border)}
214
174
  .tbl tbody tr{transition:background .15s}
215
- .tbl tbody tr:hover{background:var(--c-surface2)}
175
+ .tbl tbody tr:hover{background:rgba(255,183,197,.03)}
216
176
 
217
177
  /* Pills */
218
- .pill{display:inline-block;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:500}
178
+ .pill{display:inline-block;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:500;letter-spacing:.2px}
219
179
  .pill.on{background:var(--c-green2);color:var(--c-green)}
220
180
  .pill.off{background:var(--c-red2);color:var(--c-red)}
221
- .pill.model{background:var(--c-accent2);color:var(--c-accent);margin:2px 3px 2px 0}
222
- .pill.tag{background:rgba(167,139,250,.12);color:var(--c-purple);margin:2px 3px 2px 0}
223
- .pill.warn{background:rgba(250,204,21,.12);color:var(--c-yellow)}
224
-
225
- /* Config accordion */
226
- .cfg-sec{margin-bottom:10px;border:1px solid var(--c-border);border-radius:var(--r);overflow:hidden;transition:border-color .2s}
227
- .cfg-sec:hover{border-color:var(--c-border2)}
228
- .cfg-head{
229
- display:flex;align-items:center;justify-content:space-between;padding:12px 16px;
230
- background:var(--c-surface2);cursor:pointer;font-size:13px;font-weight:600;
231
- transition:background .15s;user-select:none;
232
- }
233
- .cfg-head:hover{background:var(--c-surface3)}
234
- .cfg-head .arr{transition:transform .25s var(--ease);font-size:10px;color:var(--c-faint)}
235
- .cfg-head .arr.open{transform:rotate(90deg)}
236
- .cfg-body{
237
- padding:14px 16px;display:grid;grid-template-columns:repeat(2,1fr);gap:8px;
238
- animation: slideDown .25s var(--ease) both;
239
- }
240
- .cfg-fld{padding:6px 10px;background:rgba(255,255,255,.02);border-radius:6px}
241
- .cfg-fld .k{font-size:10px;color:var(--c-faint);display:block;margin-bottom:3px;letter-spacing:.3px}
242
- .cfg-fld .v{word-break:break-all;font-size:12.5px}
243
- .cfg-fld .v code{color:var(--c-green)}
244
- .cfg-fld .v .num{color:var(--c-accent)}
245
- .cfg-json-btn{margin-top:16px}
181
+ .pill.model{background:var(--c-accent2);color:var(--c-pink);margin:2px 3px 2px 0}
182
+ .pill.tag{background:rgba(201,177,255,.12);color:var(--c-purple);margin:2px 3px 2px 0}
183
+
184
+ /* Config grid cards */
185
+ .cfg-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:14px;margin-top:16px}
186
+ .cfg-card{background:rgba(255,183,197,.02);border:1px solid var(--c-border);border-radius:14px;overflow:hidden;transition:all .25s var(--ease)}
187
+ .cfg-card:hover{border-color:var(--c-border2);background:rgba(255,183,197,.04)}
188
+ .cfg-card-head{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;background:rgba(255,183,197,.03);cursor:pointer;font-size:14px;font-weight:600;user-select:none;gap:8px;border-bottom:1px solid transparent;transition:.2s}
189
+ .cfg-card-head:hover{background:rgba(255,183,197,.06)}
190
+ .cfg-card.expanded .cfg-card-head{border-bottom-color:var(--c-border)}
191
+ .cfg-card-head .arr{font-size:10px;color:var(--c-faint);transition:transform .25s var(--ease);flex-shrink:0}
192
+ .cfg-card-head .arr.open{transform:rotate(90deg)}
193
+ .cfg-card-body{display:none;padding:4px 18px 16px}
194
+ .cfg-card-body.show{display:block;animation:slideDown .2s var(--ease) both}
195
+ .cfg-field{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;margin-bottom:3px;border-radius:8px;background:rgba(255,255,255,.012);gap:8px;transition:all .15s var(--ease)}
196
+ .cfg-field:hover{background:rgba(255,183,197,.04)}
197
+ .cfg-field .k{font-size:11px;color:var(--c-faint);font-weight:500;letter-spacing:.3px;flex-shrink:0;min-width:60px}
198
+ .cfg-field .v{font-size:12.5px;text-align:right;word-break:break-all;min-width:0;flex:1}
199
+ .cfg-field .v code{color:var(--c-pink);font-family:'SF Mono',Fira Code,monospace;font-size:11.5px}
200
+ .cfg-field .v .num{color:var(--c-accent)}
201
+ .cfg-field .edit-btn{flex-shrink:0;width:26px;height:26px;display:flex;align-items:center;justify-content:center;border:1px solid transparent;border-radius:6px;background:transparent;color:var(--c-faint);cursor:pointer;font-size:12px;transition:all .15s;opacity:0}
202
+ .cfg-field:hover .edit-btn{opacity:1;border-color:var(--c-border)}
203
+ .cfg-field .edit-btn:hover{color:var(--c-accent);border-color:var(--c-accent);background:var(--c-accent2)}
204
+
205
+ .cfg-field.editing{background:rgba(255,154,162,.06);border:1px solid rgba(255,154,162,.18);border-radius:8px;flex-wrap:wrap;padding:10px 12px}
206
+ .cfg-field.editing .v{display:none}
207
+ .cfg-field.editing .edit-btn{display:none}
208
+ .cfg-field .edit-row{display:none;width:100%;gap:6px}
209
+ .cfg-field.editing .edit-row{display:flex;align-items:center}
210
+ .cfg-field .edit-row input,.cfg-field .edit-row select{flex:1;padding:7px 10px;background:rgba(0,0,0,.25);border:1px solid var(--c-border);border-radius:6px;color:var(--c-text);font-size:12.5px;font-family:inherit;outline:none;transition:border-color .15s}
211
+ .cfg-field .edit-row input:focus,.cfg-field .edit-row select:focus{border-color:var(--c-accent)}
212
+ .cfg-field .edit-row .btn-xs{padding:5px 10px;font-size:11px;border-radius:6px;border:1px solid var(--c-border);background:var(--c-surface2);color:var(--c-text);cursor:pointer;transition:all .15s;white-space:nowrap}
213
+ .cfg-field .edit-row .btn-xs.save{background:linear-gradient(135deg,#ff9aa2,#ff8589);border-color:transparent;color:#fff}
214
+ .cfg-field .edit-row .btn-xs:hover{opacity:.85}
215
+ .cfg-actions{display:flex;gap:10px;margin-top:22px;padding-top:18px;border-top:1px solid var(--c-border)}
246
216
 
247
217
  /* Modal */
248
- .modal-mask{
249
- position:fixed;inset:0;z-index:100;display:flex;align-items:center;justify-content:center;
250
- background:rgba(0,0,0,.55);padding:20px;
251
- animation: fadeIn .2s both;
252
- backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);
253
- }
254
- .modal{
255
- background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r2);
256
- padding:28px;width:100%;max-width:520px;max-height:85vh;overflow-y:auto;
257
- backdrop-filter:blur(24px);-webkit-backdrop-filter:blur(24px);
258
- animation: scaleIn .3s var(--ease) both;
259
- }
260
- .modal h3{font-size:17px;font-weight:600;margin-bottom:20px}
218
+ .modal-mask{position:fixed;inset:0;z-index:100;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.55);padding:24px;animation:fadeIn .2s both;backdrop-filter:blur(6px)}
219
+ .modal{background:var(--c-surface);border:1px solid var(--c-border);border-radius:18px;padding:32px;width:100%;max-width:560px;max-height:85vh;overflow-y:auto;backdrop-filter:blur(28px);box-shadow:0 24px 80px rgba(0,0,0,.5);animation:scaleIn .3s var(--ease) both}
220
+ .modal h3{font-size:18px;font-weight:600;margin-bottom:24px;letter-spacing:-.3px}
261
221
  .f-row{margin-bottom:14px}
262
- .f-row label{display:block;font-size:11px;color:var(--c-dim);margin-bottom:5px;letter-spacing:.3px}
263
- .f-row input,.f-row select,.f-row textarea{
264
- width:100%;padding:9px 12px;background:rgba(255,255,255,.03);border:1px solid var(--c-border);
265
- border-radius:8px;color:var(--c-text);font-size:13px;font-family:inherit;
266
- transition:border-color .2s;
267
- }
268
- .f-row input:focus,.f-row select:focus,.f-row textarea:focus{outline:none;border-color:var(--c-accent)}
269
- .f-row textarea{min-height:80px;resize:vertical}
222
+ .f-row label{display:block;font-size:11px;color:var(--c-dim);margin-bottom:6px;letter-spacing:.5px;font-weight:500}
223
+ .f-row input,.f-row select,.f-row textarea{width:100%;padding:10px 13px;background:rgba(255,255,255,.03);border:1px solid var(--c-border);border-radius:8px;color:var(--c-text);font-size:13px;font-family:inherit;transition:all .2s;resize:vertical}
224
+ .f-row input:focus,.f-row select:focus,.f-row textarea:focus{outline:none;border-color:var(--c-accent);box-shadow:0 0 0 3px var(--c-accent-glow)}
225
+ .f-row textarea{min-height:80px}
270
226
  .f-row textarea.mono{font-family:'SF Mono',Fira Code,monospace;font-size:12px}
271
- .modal-btns{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}
272
-
273
- /* Page transition */
274
- .page-enter{animation: fadeUp .35s var(--ease) both}
275
- @keyframes fadeUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
227
+ .modal-btns{display:flex;justify-content:flex-end;gap:8px;margin-top:24px}
228
+
229
+ /* Sub-section style */
230
+ .sub-section{margin-top:16px;padding:16px;background:rgba(255,183,197,.03);border:1px solid var(--c-border);border-radius:12px}
231
+ .sub-section h5{font-size:13px;font-weight:600;margin-bottom:10px;display:flex;align-items:center;gap:6px}
232
+ .sub-section h5.gemini{color:var(--c-pink)}
233
+ .sub-section h5.gemini::before{content:'✦'}
234
+ .sub-section h5.tool{color:var(--c-yellow)}
235
+ .sub-section h5.tool::before{content:'◇'}
236
+
237
+ /* Animations */
238
+ .page-enter{animation:fadeUp .35s var(--ease) both}
239
+ @keyframes fadeUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
276
240
  @keyframes fadeIn{from{opacity:0}to{opacity:1}}
277
241
  @keyframes scaleIn{from{opacity:0;transform:scale(.96) translateY(8px)}to{opacity:1;transform:none}}
278
- @keyframes slideRight{from{opacity:0;transform:translateX(30px)}to{opacity:1;transform:none}}
279
- @keyframes slideDown{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}
242
+ @keyframes slideRight{from{opacity:0;transform:translateX(16px)}to{opacity:1;transform:none}}
243
+ @keyframes slideDown{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
280
244
  @keyframes spin{to{transform:rotate(360deg)}}
281
- @keyframes shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}
282
-
283
- .skeleton{
284
- background:linear-gradient(90deg,var(--c-surface2) 25%,var(--c-surface3) 50%,var(--c-surface2) 75%);
285
- background-size:200% 100%;animation:shimmer 1.5s infinite;border-radius:6px;
286
- }
287
- .skel-row{height:14px;margin-bottom:8px;width:70%}
288
- .skel-row.short{width:40%}
245
+ @keyframes pulse-dot{0%,100%{opacity:1}50%{opacity:.35}}
289
246
 
290
247
  .mono{font-family:'SF Mono',Fira Code,monospace}
291
- code{font-family:'SF Mono',Fira Code,monospace;font-size:12px}
248
+ code{font-family:'SF Mono',Fira Code,monospace;font-size:12px;color:var(--c-pink)}
292
249
 
293
250
  /* Responsive */
294
251
  @media(max-width:768px){
295
- .sidebar{
296
- position:fixed;left:0;top:0;bottom:0;transform:translateX(-100%);
297
- z-index:50;width:260px;
298
- }
252
+ .sidebar{position:fixed;left:0;top:0;bottom:0;transform:translateX(-100%);z-index:50;width:260px}
299
253
  .sidebar.open{transform:translateX(0)}
300
254
  .sidebar-overlay.show{display:block}
301
255
  .menu-toggle{display:flex}
302
256
  .topbar{padding:12px 16px}
303
- .content{padding:16px}
304
- .stats{grid-template-columns:repeat(2,1fr);gap:8px}
305
- .stat{padding:14px 12px}
306
- .stat .num{font-size:22px}
307
- .card{padding:16px}
308
- .cfg-body{grid-template-columns:1fr}
309
- .modal{max-width:95%;padding:20px}
257
+ .content{padding:16px 16px 32px}
258
+ .stats{grid-template-columns:repeat(2,1fr);gap:10px}
259
+ .stat{padding:16px 14px}
260
+ .stat .num{font-size:24px}
261
+ .card{padding:18px}
262
+ .cfg-grid{grid-template-columns:1fr}
263
+ .modal{max-width:95%;padding:24px}
264
+ .petal{display:none}
310
265
  }
311
266
  @media(max-width:480px){
312
267
  .stats{grid-template-columns:1fr 1fr}
@@ -316,6 +271,12 @@ code{font-family:'SF Mono',Fira Code,monospace;font-size:12px}
316
271
  </style>
317
272
  </head>
318
273
  <body>
274
+ <!-- Sakura petals -->
275
+ <div class="petal">🌸</div><div class="petal">🌸</div><div class="petal">🌸</div>
276
+ <div class="petal">🌸</div><div class="petal">🌸</div><div class="petal">🌸</div>
277
+ <div class="petal">🌸</div><div class="petal">🌸</div><div class="petal">🌸</div>
278
+ <div class="petal">🌸</div>
279
+
319
280
  <div id="app"></div>
320
281
  <script>
321
282
  const{createApp,ref,reactive,computed,onMounted,onUnmounted}=Vue
@@ -330,21 +291,18 @@ const API=(path,opt={})=>{
330
291
 
331
292
  createApp({
332
293
  setup(){
333
- // Auth
334
294
  const jwt=ref(localStorage.getItem('chaite_jwt')||'')
335
295
  const loggedIn=ref(!!jwt.value)
336
296
  const loginInput=ref('')
337
297
  const loginLoading=ref(false)
338
298
  const loginError=ref('')
339
299
 
340
- // UI state
341
300
  const page=ref('dashboard')
342
301
  const sidebarOpen=ref(false)
343
302
  const loading=ref(true)
344
303
  const refreshTime=ref('')
345
304
  let timer=null
346
305
 
347
- // Data
348
306
  const health=ref(null)
349
307
  const stats=ref(null)
350
308
  const channels=ref([])
@@ -357,22 +315,19 @@ createApp({
357
315
  const conversations=ref([])
358
316
  const convDetail=ref(null)
359
317
 
360
- // Modal
361
318
  const modal=ref(null)
362
319
  const editing=ref(null)
363
320
  const form=ref({})
364
321
 
365
- // Config edit
366
322
  const editingConfig=ref(false)
367
323
  const configForm=ref('{}')
324
+ const editingCfgField=ref(null)
368
325
 
369
- // Toast
370
326
  const toastMsg=ref('')
371
327
  const toastType=ref('ok')
372
328
  let toastTimer=null
373
329
  function toast(msg,type='ok'){toastMsg.value=msg;toastType.value=type;clearTimeout(toastTimer);toastTimer=setTimeout(()=>toastMsg.value='',3000)}
374
330
 
375
- // Login
376
331
  async function doLogin(){
377
332
  loginError.value='';loginLoading.value=true
378
333
  try{
@@ -385,13 +340,12 @@ createApp({
385
340
  jwt.value=d.data.token;localStorage.setItem('chaite_jwt',d.data.token);loggedIn.value=true
386
341
  url.searchParams.delete('token');history.replaceState(null,'',url.pathname+url.search)
387
342
  refreshAll()
388
- }else{loginError.value=d.message||'Login failed'}
389
- }catch(e){loginError.value='Network error'}
343
+ }else{loginError.value=d.message||'登录失败'}
344
+ }catch(e){loginError.value='网络错误'}
390
345
  finally{loginLoading.value=false}
391
346
  }
392
347
  function logout(){jwt.value='';localStorage.removeItem('chaite_jwt');loggedIn.value=false;clearInterval(timer)}
393
348
 
394
- // Data fetch
395
349
  async function refreshAll(){
396
350
  loading.value=true
397
351
  try{
@@ -407,74 +361,51 @@ createApp({
407
361
  API('/api/config').catch(()=>({})),
408
362
  API('/api/state/conversations/list').catch(()=>({})),
409
363
  ])
410
- health.value=h.data||null
411
- stats.value=s.data||null
412
- channels.value=c.data||[]
413
- tools.value=t.data||[]
414
- presets.value=p.data||[]
415
- processors.value=pr.data||[]
416
- triggers.value=tr.data||[]
417
- toolGroups.value=tg.data||[]
418
- config.value=cfg.data||{}
419
- conversations.value=cv.data||[]
420
- refreshTime.value=new Date().toLocaleTimeString()
364
+ health.value=h.data||null;stats.value=s.data||null;channels.value=c.data||[]
365
+ tools.value=t.data||[];presets.value=p.data||[];processors.value=pr.data||[]
366
+ triggers.value=tr.data||[];toolGroups.value=tg.data||[];config.value=cfg.data||{}
367
+ conversations.value=cv.data||[];refreshTime.value=new Date().toLocaleTimeString()
421
368
  }catch(e){console.error(e)}
422
369
  finally{loading.value=false}
423
370
  }
424
371
 
425
372
  function navigate(p){page.value=p;sidebarOpen.value=false}
426
-
427
- // Helpers
428
- function uptimeStr(s){
429
- if(!s)return '-'
430
- const h=Math.floor(s/3600),m=Math.floor(s%3600/60),sec=s%60
431
- return h>0?`${h}h ${m}m ${sec}s`:`${m}m ${sec}s`
432
- }
373
+ function uptimeStr(s){if(!s)return'-';const h=Math.floor(s/3600),m=Math.floor(s%3600/60),sec=s%60;return h>0?`${h}h ${m}m ${sec}s`:`${m}m ${sec}s`}
433
374
  function fmtNum(n){if(n>=1e6)return(n/1e6).toFixed(1)+'M';if(n>=1e3)return(n/1e3).toFixed(1)+'K';return String(n||0)}
434
375
  function fLen(s,n){return s&&s.length>n?s.slice(0,n)+'...':s||''}
435
376
 
436
- // Channel test
437
377
  async function testChannel(id){
438
378
  const d=await API('/api/system/test-channel',{method:'POST',body:JSON.stringify({channelId:id})})
439
- if(d.data?.status==='ok')toast(''+d.data.channelName+' — '+d.data.latency+'ms')
440
- else toast(''+(d.data?.error||d.message),'err')
379
+ if(d.data?.status==='ok')toast(d.data.channelName+' — '+d.data.latency+'ms')
380
+ else toast(d.data?.error||d.message,'err')
441
381
  }
442
382
 
443
- // CRUD
444
383
  function openModal(type,item=null){
445
384
  modal.value=type;editing.value=item
446
385
  if(item){
447
386
  form.value=JSON.parse(JSON.stringify(item))
448
- // 为 channel 初始化 JSON 字段
449
387
  if(type==='channel'){
450
- form.value._models = (form.value.models||[]).join(', ')
451
- form.value._responseModalities = JSON.stringify(form.value.sendMessageOption?.responseModalities||[])
452
- form.value._safetySettings = JSON.stringify(form.value.sendMessageOption?.safetySettings||[],null,2)
453
- // 确保 sendMessageOption 存在
454
- if(!form.value.sendMessageOption){
455
- form.value.sendMessageOption = {
456
- model:'',
457
- temperature:0.8,
458
- maxToken:4096,
459
- systemOverride:'',
460
- enableReasoning:false,
461
- reasoningEffort:'high',
462
- reasoningBudgetTokens:0,
463
- responseModalities:[],
464
- safetySettings:[],
465
- toolCallLimit:{maxConsecutiveCalls:8,maxConsecutiveIdenticalCalls:2}
466
- }
467
- }
388
+ form.value._models=(form.value.models||[]).join(', ')
389
+ if(!form.value.sendMessageOption)form.value.sendMessageOption={}
390
+ const smo=form.value.sendMessageOption
391
+ smo.model=smo.model||''
392
+ smo.temperature=smo.temperature||0.8
393
+ smo.maxToken=smo.maxToken||4096
394
+ smo.systemOverride=smo.systemOverride||''
395
+ if(smo.enableReasoning===undefined)smo.enableReasoning=false
396
+ smo.reasoningEffort=smo.reasoningEffort||'high'
397
+ if(smo.toolCallLimit===undefined)smo.toolCallLimit={maxConsecutiveCalls:8,maxConsecutiveIdenticalCalls:2}
398
+ if(!smo.responseModalities)smo.responseModalities=[]
399
+ if(!smo.safetySettings)smo.safetySettings=[]
400
+ form.value._responseModalities=JSON.stringify(smo.responseModalities)
401
+ form.value._safetySettings=JSON.stringify(smo.safetySettings,null,2)
468
402
  }
469
- }
470
- else{
403
+ }else{
471
404
  form.value={
472
405
  channel:{name:'',adapterType:'openai',type:'openai',models:[''],options:{baseUrl:'',apiKey:''},status:'enabled',weight:1,priority:0,sendMessageOption:{model:'',temperature:0.8,maxToken:4096,systemOverride:'',enableReasoning:false,reasoningEffort:'high',reasoningBudgetTokens:0,responseModalities:[],safetySettings:[],toolCallLimit:{maxConsecutiveCalls:8,maxConsecutiveIdenticalCalls:2}}},
473
- preset:{name:'',prefix:'',sendMessageOption:{model:'',temperature:0.8,maxToken:4096,systemOverride:'',enableReasoning:false,reasoningEffort:'high',disableHistoryRead:false,disableHistorySave:false}},
474
- tool:{name:'',description:'',code:''},
475
- processor:{name:'',type:'pre',description:'',code:''},
476
- trigger:{name:'',description:'',code:''},
477
- toolGroup:{name:'',description:'',toolIds:[],status:'enabled',isDefault:false},
406
+ preset:{name:'',prefix:'',sendMessageOption:{model:'',temperature:0.8,maxToken:4096,systemOverride:'',enableReasoning:false,reasoningEffort:'high',disableHistoryRead:false,disableHistorySave:false},groupContext:undefined,disableSystemInstructions:false},
407
+ tool:{name:'',description:'',code:''},processor:{name:'',type:'pre',description:'',code:''},
408
+ trigger:{name:'',description:'',code:''},toolGroup:{name:'',description:'',toolIds:[],status:'enabled',isDefault:false},
478
409
  }[type]||{}
479
410
  }
480
411
  }
@@ -483,93 +414,147 @@ createApp({
483
414
  async function saveItem(){
484
415
  const apiMap={channel:'/api/channels',preset:'/api/preset',tool:'/api/tools',processor:'/api/processors',trigger:'/api/triggers',toolGroup:'/api/toolGroups'}
485
416
  try{
486
- const type=modal.value
487
- const id=editing.value?.id
488
- const url=apiMap[type]+(id?'/'+id:'')
489
-
490
- // 处理 channel 的 JSON 字段
417
+ const type=modal.value,id=editing.value?.id,url=apiMap[type]+(id?'/'+id:'')
418
+ const fd=JSON.parse(JSON.stringify(form.value))
491
419
  if(type==='channel'){
492
- const formData = JSON.parse(JSON.stringify(form.value))
493
-
494
- // 处理 models 字段
495
- if(formData._models){
496
- formData.models = formData._models.split(',').map(m=>m.trim()).filter(m=>m)
497
- delete formData._models
498
- }
499
-
500
- // 处理 responseModalities
501
- if(formData._responseModalities){
502
- try{
503
- formData.sendMessageOption.responseModalities = JSON.parse(formData._responseModalities)
504
- }catch(e){
505
- // 如果解析失败,保持原样
506
- }
507
- delete formData._responseModalities
508
- }
509
-
510
- // 处理 safetySettings
511
- if(formData._safetySettings){
512
- try{
513
- formData.sendMessageOption.safetySettings = JSON.parse(formData._safetySettings)
514
- }catch(e){
515
- // 如果解析失败,保持原样
516
- }
517
- delete formData._safetySettings
518
- }
519
-
520
- form.value = formData
420
+ if(fd._models){fd.models=fd._models.split(',').map(m=>m.trim()).filter(m=>m);delete fd._models}
421
+ if(fd._responseModalities){try{fd.sendMessageOption.responseModalities=JSON.parse(fd._responseModalities)}catch(e){};delete fd._responseModalities}
422
+ if(fd._safetySettings){try{fd.sendMessageOption.safetySettings=JSON.parse(fd._safetySettings)}catch(e){};delete fd._safetySettings}
423
+ }
424
+ if(type==='toolGroup'&&fd._toolIds){
425
+ fd.toolIds=fd._toolIds.split(',').map(m=>m.trim()).filter(m=>m)
426
+ delete fd._toolIds
521
427
  }
522
-
523
- const d=await API(url,{method:id?'PUT':'POST',body:JSON.stringify(form.value)})
524
- if(d.data!==undefined){toast('Saved');closeModal();refreshAll()}
525
- else toast(d.message||'Error','err')
428
+ const d=await API(url,{method:id?'PUT':'POST',body:JSON.stringify(fd)})
429
+ if(d.data!==undefined){toast('已保存 ✨');closeModal();refreshAll()}
430
+ else toast(d.message||'错误','err')
526
431
  }catch(e){toast(e.message,'err')}
527
432
  }
528
433
 
529
434
  async function deleteItem(type,id){
530
- if(!confirm('Delete?'))return
435
+ if(!confirm('确认删除?'))return
531
436
  const apiMap={channel:'/api/channels',preset:'/api/preset',tool:'/api/tools',processor:'/api/processors',trigger:'/api/triggers',toolGroup:'/api/toolGroups'}
532
- try{await API(apiMap[type]+'/'+id,{method:'DELETE'});toast('Deleted');refreshAll()}
437
+ try{await API(apiMap[type]+'/'+id,{method:'DELETE'});toast('已删除');refreshAll()}
533
438
  catch(e){toast(e.message,'err')}
534
439
  }
535
440
 
536
- // Config
441
+ // ===== Config System =====
537
442
  const expandedCfg=ref({})
538
- const SKIP_CONFIG_KEYS=new Set(['version','_saveOrigin','authKey','cloudBaseUrl','cloudApiKey'])
443
+ const SKIP_KEYS=new Set(['version','_saveOrigin','authKey','cloudBaseUrl','cloudApiKey'])
444
+
445
+ const CFG_LABELS={
446
+ // 配置分组标签
447
+ basic:'基础配置',bym:'伪人模式',llm:'模型与对话',management:'管理控制',
448
+ chaite:'Chaite 面板',memory:'记忆系统',update:'更新相关',
449
+ // 基础配置
450
+ toggleMode:'触发方式',togglePrefix:'触发前缀',debug:'调试模式',commandPrefix:'命令前缀',
451
+ // 伪人模式
452
+ enable:'启用',hit:'必触发词',probability:'概率',defaultPreset:'伪人默认预设',presetPrefix:'预设前缀',
453
+ presetMap:'预设映射',maxTokens:'最大Token',temperature:'温度',sendReasoning:'发送思考',
454
+ // 模型与对话
455
+ defaultModel:'默认对话模型',embeddingModel:'嵌入模型',dimensions:'向量维度',
456
+ defaultChatPresetId:'默认对话预设',enableCustomPreset:'允许切换预设',
457
+ customPresetUserWhiteList:'预设切换用户白名单',customPresetUserBlackList:'预设切换用户黑名单',
458
+ promptBlockWords:'输入屏蔽词',responseBlockWords:'回复屏蔽词',
459
+ blockStrategy:'屏蔽策略',blockWordMask:'屏蔽替换字符',
460
+ enableGroupContext:'群组上下文',groupContextLength:'上下文长度',
461
+ groupContextTemplatePrefix:'上下文模板前缀',groupContextTemplateMessage:'上下文模板消息',
462
+ groupContextTemplateSuffix:'上下文模板后缀',
463
+ // 管理控制
464
+ blackGroups:'群黑名单',whiteGroups:'群白名单',blackUsers:'用户黑名单',
465
+ whiteUsers:'用户白名单',defaultRateLimit:'默认速率限制',
466
+ // Chaite 面板
467
+ dataDir:'数据目录',processorsDirPath:'处理器目录',triggersDir:'触发器目录',
468
+ toolsDirPath:'工具目录',host:'监听地址',port:'监听端口',storage:'存储实现',
469
+ accessToken:'访问令牌',tokenExpiresIn:'令牌有效期(秒)',
470
+ // 记忆系统
471
+ database:'数据库路径',vectorDimensions:'向量维度',
472
+ group:'群组记忆',user:'用户记忆',extensions:'扩展模块',
473
+ // 群组记忆
474
+ enabledGroups:'启用群组',extractionModel:'提取模型',extractionPresetId:'提取预设',
475
+ minMessageCount:'最小消息数',maxMessageWindow:'最大消息窗',
476
+ retrievalMode:'检索模式',hybridPrefer:'混合检索偏好',
477
+ historyPollInterval:'历史轮询间隔',historyBatchSize:'历史批次大小',
478
+ promptHeader:'提示词头部',promptItemTemplate:'提示词条目模板',promptFooter:'提示词尾部',
479
+ extractionSystemPrompt:'提取系统提示',extractionUserPrompt:'提取用户提示',
480
+ vectorMaxDistance:'向量最大距离',textMaxBm25Score:'文本BM25最大分数',
481
+ maxFactsPerInjection:'最大注入事实数',minImportanceForInjection:'最小注入重要度',
482
+ // 用户记忆
483
+ whitelist:'白名单',blacklist:'黑名单',
484
+ maxItemsPerInjection:'最大注入条目',maxRelevantItemsPerQuery:'最大检索条目',
485
+ // 扩展
486
+ simple:'简易分词',libraryPath:'库路径',dictPath:'词典路径',useJieba:'使用结巴分词',
487
+ }
488
+ function cfgLabel(k){return CFG_LABELS[k]||k.charAt(0).toUpperCase()+k.slice(1)}
539
489
 
540
490
  const cfgSections=computed(()=>{
541
491
  const secs=[]
542
492
  const order=['basic','bym','llm','management','chaite','memory','update']
543
493
  for(const key of order){
544
494
  const v=config.value[key]
545
- if(v!==undefined) secs.push({key,label:key.charAt(0).toUpperCase()+key.slice(1),fields:typeof v==='object'?v:{_value:v},isObj:typeof v==='object'&&!Array.isArray(v)})
495
+ if(v!==undefined) secs.push({key,label:cfgLabel(key),fields:v,isObj:typeof v==='object'&&!Array.isArray(v)})
546
496
  }
547
- // Remaining keys not in order
548
497
  for(const[k,v]of Object.entries(config.value)){
549
- if(order.includes(k)||SKIP_CONFIG_KEYS.has(k))continue
550
- secs.push({key:k,label:k,fields:typeof v==='object'?v:{_value:v},isObj:typeof v==='object'&&!Array.isArray(v)})
498
+ if(order.includes(k)||SKIP_KEYS.has(k))continue
499
+ secs.push({key:k,label:cfgLabel(k),fields:v,isObj:typeof v==='object'&&!Array.isArray(v)})
551
500
  }
552
501
  return secs
553
502
  })
554
503
  function toggleCfg(k){expandedCfg.value[k]=!expandedCfg.value[k]}
504
+
505
+ function startEditField(secKey,fieldKey,currentVal){
506
+ const isArr=Array.isArray(currentVal)
507
+ const raw=isArr?JSON.stringify(currentVal):(typeof currentVal==='string'?currentVal:JSON.stringify(currentVal))
508
+ editingCfgField.value={sec:secKey,key:fieldKey,val:raw,type:typeof currentVal==='boolean'?'bool':typeof currentVal==='number'?'num':isArr?'arr':'str'}
509
+ }
510
+ function cancelEditField(){editingCfgField.value=null}
511
+ // 深层设置对象值,支持 'a.b.c' 路径
512
+ function deepSet(obj,path,val){
513
+ const keys=path.split('.')
514
+ let cur=obj
515
+ for(let i=0;i<keys.length-1;i++){
516
+ if(!cur[keys[i]])cur[keys[i]]={}
517
+ cur=cur[keys[i]]
518
+ }
519
+ cur[keys[keys.length-1]]=val
520
+ }
521
+ async function saveEditField(secKey,fieldKey){
522
+ if(!editingCfgField.value)return
523
+ let newVal=editingCfgField.value.val
524
+ const type=editingCfgField.value.type
525
+ if(type==='bool')newVal=newVal==='true'||newVal===true
526
+ else if(type==='num')newVal=Number(newVal)
527
+ else if(type==='arr'){try{newVal=JSON.parse(newVal)}catch(e){toast('数组格式错误,请使用 JSON 数组格式','err');return}}
528
+ try{
529
+ const cfgCopy=JSON.parse(JSON.stringify(config.value))
530
+ if(fieldKey.includes('.')){
531
+ deepSet(cfgCopy[secKey],fieldKey,newVal)
532
+ }else{
533
+ cfgCopy[secKey][fieldKey]=newVal
534
+ }
535
+ const d=await API('/api/config',{method:'POST',body:JSON.stringify(cfgCopy)})
536
+ if(d.data!==undefined){toast('配置已保存 ✨');cancelEditField();refreshAll()}
537
+ else toast(d.message||'错误','err')
538
+ }catch(e){toast('保存失败: '+e.message,'err')}
539
+ }
540
+
555
541
  function editConfig(){configForm.value=JSON.stringify(config.value,null,2);editingConfig.value=true}
556
542
  async function saveConfig(){
557
543
  try{
558
544
  const obj=JSON.parse(configForm.value)
559
545
  const d=await API('/api/config',{method:'POST',body:JSON.stringify(obj)})
560
- if(d.data!==undefined){toast('Config saved');editingConfig.value=false;refreshAll()}
561
- else toast(d.message||'Error','err')
562
- }catch(e){toast('Invalid JSON: '+e.message,'err')}
546
+ if(d.data!==undefined){toast('配置已保存 ');editingConfig.value=false;refreshAll()}
547
+ else toast(d.message||'错误','err')
548
+ }catch(e){toast('无效 JSON: '+e.message,'err')}
563
549
  }
564
550
 
565
- // Conversations
566
551
  async function viewConv(uid){
567
552
  try{const d=await API('/api/state/'+encodeURIComponent(uid));convDetail.value=d.data||null}
568
553
  catch(e){toast(e.message,'err')}
569
554
  }
570
555
  async function clearConv(uid){
571
- if(!confirm('Clear state for '+uid+'?'))return
572
- try{await API('/api/state/'+encodeURIComponent(uid),{method:'DELETE'});toast('Cleared');refreshAll()}
556
+ if(!confirm('清除用户 '+uid+' 的状态?'))return
557
+ try{await API('/api/state/'+encodeURIComponent(uid),{method:'DELETE'});toast('已清除');refreshAll()}
573
558
  catch(e){toast(e.message,'err')}
574
559
  }
575
560
 
@@ -583,44 +568,52 @@ createApp({
583
568
  })
584
569
  onUnmounted(()=>clearInterval(timer))
585
570
 
571
+ // Helper: flatten object fields for display, handling nested objects
572
+ function flattenFields(obj,prefix=''){
573
+ if(!obj||typeof obj!=='object')return{[prefix]:obj}
574
+ if(Array.isArray(obj))return{[prefix]:obj}
575
+ const result={}
576
+ for(const[k,v]of Object.entries(obj)){
577
+ if(v&&typeof v==='object'&&!Array.isArray(v)){
578
+ const nested=flattenFields(v,k+'.')
579
+ Object.assign(result,nested)
580
+ }else{result[prefix+k]=v}
581
+ }
582
+ return result
583
+ }
584
+
586
585
  return{
587
586
  jwt,loggedIn,loginInput,loginLoading,loginError,doLogin,logout,
588
587
  page,sidebarOpen,navigate,loading,refreshTime,isHealthy,
589
588
  health,stats,channels,tools,presets,processors,triggers,toolGroups,config,conversations,convDetail,
590
- channelsOk,
591
- modal,editing,form,openModal,closeModal,saveItem,deleteItem,testChannel,
592
- expandedCfg,cfgSections,toggleCfg,editConfig,editingConfig,configForm,saveConfig,SKIP_CONFIG_KEYS,
593
- viewConv,clearConv,
594
- toastMsg,toastType,uptimeStr,fmtNum,fLen,refreshAll,
589
+ channelsOk,modal,editing,form,openModal,closeModal,saveItem,deleteItem,testChannel,
590
+ expandedCfg,cfgSections,toggleCfg,editConfig,editingConfig,configForm,saveConfig,
591
+ editingCfgField,startEditField,cancelEditField,saveEditField,cfgLabel,flattenFields,
592
+ viewConv,clearConv,toastMsg,toastType,uptimeStr,fmtNum,fLen,refreshAll,
595
593
  }
596
594
  },
597
595
  template:`
598
- <!-- Toast -->
599
596
  <div v-if="toastMsg" :class="['toast',toastType]">{{toastMsg}}</div>
600
597
 
601
598
  <!-- Login -->
602
599
  <div v-if="!loggedIn" class="login-wrap">
603
600
  <div class="login-card">
604
601
  <h1>Chaite</h1>
605
- <p class="sub">Management Console</p>
602
+ <p class="sub">✦ 管理控制台 ✦</p>
606
603
  <div v-if="!loginLoading">
607
- <input v-model="loginInput" type="password" placeholder="Access token..."
604
+ <input v-model="loginInput" type="password" placeholder="输入访问令牌..."
608
605
  @keyup.enter="doLogin" autofocus/>
609
606
  <div class="err">{{loginError}}</div>
610
- <button class="btn pri" @click="doLogin" style="width:100%;padding:10px;margin-top:4px">Sign In</button>
611
- <p class="hint">Or visit with <code>?token=xxx</code> in URL</p>
607
+ <button class="btn pri" @click="doLogin" style="width:100%;padding:10px;margin-top:4px">登录</button>
608
+ <p class="hint">或在 URL 中携带 <code>?token=xxx</code> 访问</p>
612
609
  </div>
613
610
  <div v-else style="padding:20px"><div class="spin" style="display:inline-block"></div></div>
614
611
  </div>
615
612
  </div>
616
613
 
617
- <!-- Main Layout -->
614
+ <!-- Main -->
618
615
  <div v-else class="layout">
619
-
620
- <!-- Sidebar overlay (mobile) -->
621
616
  <div :class="['sidebar-overlay',sidebarOpen&&'show']" @click="sidebarOpen=false"></div>
622
-
623
- <!-- Sidebar -->
624
617
  <aside :class="['sidebar',sidebarOpen&&'open']">
625
618
  <div class="sidebar-logo">
626
619
  <h2>Chaite</h2>
@@ -628,60 +621,57 @@ createApp({
628
621
  </div>
629
622
  <nav class="sidebar-nav">
630
623
  <div :class="['nav-item',page==='dashboard'&&'active']" @click="navigate('dashboard')">
631
- <span class="icon">◈</span>Overview
632
- <span class="badge-mini" v-if="health">{{health.channels?.total||0}}ch</span>
624
+ <span class="icon">◈</span>概览
625
+ <span class="badge-mini" v-if="health">{{health.channels?.total||0}}渠道</span>
633
626
  </div>
634
627
  <div :class="['nav-item',page==='channels'&&'active']" @click="navigate('channels')">
635
- <span class="icon">▤</span>Channels
628
+ <span class="icon">▤</span>渠道
636
629
  <span class="badge-mini" v-if="channelsOk">{{channelsOk}}</span>
637
630
  </div>
638
631
  <div :class="['nav-item',page==='tools'&&'active']" @click="navigate('tools')">
639
- <span class="icon">⚙</span>Tools
632
+ <span class="icon">⚙</span>工具
640
633
  <span class="badge-mini" v-if="tools.length">{{tools.length}}</span>
641
634
  </div>
642
635
  <div :class="['nav-item',page==='presets'&&'active']" @click="navigate('presets')">
643
- <span class="icon">✎</span>Presets
636
+ <span class="icon">✎</span>预设
644
637
  </div>
645
638
  <div :class="['nav-item',page==='processors'&&'active']" @click="navigate('processors')">
646
- <span class="icon">⇄</span>Processors
639
+ <span class="icon">⇄</span>处理器
647
640
  </div>
648
641
  <div :class="['nav-item',page==='triggers'&&'active']" @click="navigate('triggers')">
649
- <span class="icon">⚡</span>Triggers
642
+ <span class="icon">⚡</span>触发器
650
643
  </div>
651
644
  <div :class="['nav-item',page==='groups'&&'active']" @click="navigate('groups')">
652
- <span class="icon">☰</span>Groups
645
+ <span class="icon">☰</span>工具组
653
646
  </div>
654
647
  <div style="margin:8px 0;border-top:1px solid var(--c-border)"></div>
655
648
  <div :class="['nav-item',page==='conversations'&&'active']" @click="navigate('conversations')">
656
- <span class="icon">◎</span>Sessions
649
+ <span class="icon">◎</span>会话
657
650
  <span class="badge-mini" v-if="conversations.length">{{conversations.length}}</span>
658
651
  </div>
659
652
  <div :class="['nav-item',page==='config'&&'active']" @click="navigate('config')">
660
- <span class="icon">⚙</span>Config
653
+ <span class="icon">⚙</span>配置
661
654
  </div>
662
655
  </nav>
663
656
  <div class="sidebar-footer">
664
657
  <span :class="['dot',isHealthy?'ok':'']"></span>
665
- {{isHealthy?'Online':'Offline'}}
666
- <br><span style="font-size:10px">port {{health?.system?.platform||'-'}}</span>
658
+ {{isHealthy?'在线':'离线'}}
659
+ <br><span style="font-size:10px">端口 {{health?.system?.platform||'-'}}</span>
667
660
  </div>
668
661
  </aside>
669
662
 
670
- <!-- Main -->
671
663
  <div class="main">
672
- <!-- Top bar -->
673
664
  <header class="topbar">
674
665
  <div style="display:flex;align-items:center;gap:12px">
675
666
  <button class="menu-toggle" @click="sidebarOpen=!sidebarOpen">☰</button>
676
667
  <span class="t">
677
- {{page==='dashboard'?'Overview':page==='conversations'?'Sessions':page==='groups'?'Groups':page.charAt(0).toUpperCase()+page.slice(1)}}
668
+ {{page==='dashboard'?'概览':page==='conversations'?'会话':page==='groups'?'工具组':page.charAt(0).toUpperCase()+page.slice(1)}}
678
669
  </span>
679
670
  </div>
680
671
  <div class="meta">
681
672
  <span class="refresh-text" v-if="refreshTime">↻ {{refreshTime}}</span>
682
673
  <button class="btn ghost" @click="refreshAll" :disabled="loading">
683
- <span v-if="loading" class="spin"></span>
684
- <span v-else>↻</span>
674
+ <span v-if="loading" class="spin"></span><span v-else>↻</span>
685
675
  </button>
686
676
  <button class="btn ghost" @click="logout">⏻</button>
687
677
  </div>
@@ -689,411 +679,287 @@ createApp({
689
679
 
690
680
  <div class="content" :key="page">
691
681
 
692
- <!-- ========== DASHBOARD ========== -->
682
+ <!-- ===== DASHBOARD ===== -->
693
683
  <div v-if="page==='dashboard'" class="page-enter">
694
684
  <div class="stats" v-if="health">
695
- <div class="stat"><div class="num">{{uptimeStr(health.uptime)}}</div><div class="lbl">Uptime</div></div>
696
- <div class="stat"><div class="num">{{health.channels?.total||0}}</div><div class="lbl">Channels</div></div>
697
- <div class="stat"><div class="num">{{health.models?.count||0}}</div><div class="lbl">Models</div></div>
698
- <div class="stat"><div class="num">{{health.tools?.count||0}}</div><div class="lbl">Tools</div></div>
699
- <div class="stat"><div class="num">{{health.system?.processMemory||0}}M</div><div class="lbl">Memory</div></div>
700
- <div class="stat"><div class="num">{{fmtNum(stats?.summary?.totalCalls)}}</div><div class="lbl">Total Calls</div></div>
685
+ <div class="stat"><div class="num">{{uptimeStr(health.uptime)}}</div><div class="lbl">运行时长</div></div>
686
+ <div class="stat"><div class="num">{{health.channels?.total||0}}</div><div class="lbl">渠道</div></div>
687
+ <div class="stat"><div class="num">{{health.models?.count||0}}</div><div class="lbl">模型</div></div>
688
+ <div class="stat"><div class="num">{{health.tools?.count||0}}</div><div class="lbl">工具</div></div>
689
+ <div class="stat"><div class="num">{{health.system?.processMemory||0}}M</div><div class="lbl">内存</div></div>
690
+ <div class="stat"><div class="num">{{fmtNum(stats?.summary?.totalCalls)}}</div><div class="lbl">总调用</div></div>
701
691
  </div>
702
692
 
703
- <div v-if="!loading&&!health" class="card" style="text-align:center;color:var(--c-dim);padding:48px">No data is the server running?</div>
693
+ <div v-if="!loading&&!health" class="card" style="text-align:center;color:var(--c-dim);padding:48px">无数据服务器是否运行?</div>
704
694
 
705
695
  <div class="card" v-if="health">
706
- <div class="card-hd"><h3><span class="dot" style="background:var(--c-green)"></span>System</h3></div>
696
+ <div class="card-hd"><h3><span class="dot" style="background:var(--c-green)"></span>系统信息</h3></div>
707
697
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;font-size:13px">
708
- <div><span style="color:var(--c-dim)">Version</span><br>{{health.version}}</div>
698
+ <div><span style="color:var(--c-dim)">版本</span><br>{{health.version}}</div>
709
699
  <div><span style="color:var(--c-dim)">Node</span><br>{{health.system?.nodeVersion}}</div>
710
- <div><span style="color:var(--c-dim)">Platform</span><br>{{health.system?.platform}} {{health.system?.arch}}</div>
711
- <div><span style="color:var(--c-dim)">CPUs</span><br>{{health.system?.cpus}}</div>
712
- <div><span style="color:var(--c-dim)">Heap</span><br>{{health.system?.heapUsed}}M / {{health.system?.processMemory}}M</div>
713
- <div><span style="color:var(--c-dim)">Sys Mem</span><br>{{health.system?.freeMemory}}M free</div>
700
+ <div><span style="color:var(--c-dim)">平台</span><br>{{health.system?.platform}} {{health.system?.arch}}</div>
701
+ <div><span style="color:var(--c-dim)">CPU</span><br>{{health.system?.cpus}}</div>
702
+ <div><span style="color:var(--c-dim)">堆内存</span><br>{{health.system?.heapUsed}}M / {{health.system?.processMemory}}M</div>
703
+ <div><span style="color:var(--c-dim)">系统内存</span><br>{{health.system?.freeMemory}}M 可用</div>
714
704
  </div>
715
705
  </div>
716
706
 
717
707
  <div class="card" v-if="health?.models?.list?.length">
718
- <div class="card-hd"><h3>Models</h3></div>
708
+ <div class="card-hd"><h3>模型列表</h3></div>
719
709
  <div><span v-for="m in health.models.list" class="pill model">{{m}}</span></div>
720
710
  </div>
721
711
 
722
712
  <div class="card" v-if="stats?.perModel&&Object.keys(stats.perModel).length">
723
- <div class="card-hd"><h3>Usage by Model</h3></div>
713
+ <div class="card-hd"><h3>模型使用统计</h3></div>
724
714
  <div class="tbl-wrap"><table class="tbl">
725
- <thead><tr><th>Model</th><th>Calls</th><th>Tokens</th></tr></thead>
726
- <tbody>
727
- <tr v-for="(v,k) in stats.perModel" :key="k">
728
- <td><span class="pill model">{{k}}</span></td>
729
- <td>{{fmtNum(v.calls)}}</td><td>{{fmtNum(v.tokens)}}</td>
730
- </tr>
731
- </tbody>
715
+ <thead><tr><th>模型</th><th>调用次数</th><th>Token</th></tr></thead>
716
+ <tbody><tr v-for="(v,k) in stats.perModel" :key="k"><td><span class="pill model">{{k}}</span></td><td>{{fmtNum(v.calls)}}</td><td>{{fmtNum(v.tokens)}}</td></tr></tbody>
732
717
  </table></div>
733
718
  </div>
734
719
 
735
720
  <div class="card" v-if="stats?.perChannel&&Object.keys(stats.perChannel).length">
736
- <div class="card-hd"><h3>Usage by Channel</h3></div>
721
+ <div class="card-hd"><h3>渠道使用统计</h3></div>
737
722
  <div class="tbl-wrap"><table class="tbl">
738
- <thead><tr><th>Channel</th><th>Calls</th><th>Tokens</th><th>Models</th></tr></thead>
739
- <tbody>
740
- <tr v-for="(v,k) in stats.perChannel" :key="k">
741
- <td>{{k}}</td><td>{{fmtNum(v.calls)}}</td><td>{{fmtNum(v.tokens)}}</td>
742
- <td><span v-for="m in (v.models||[])" class="pill model">{{m}}</span></td>
743
- </tr>
744
- </tbody>
723
+ <thead><tr><th>渠道</th><th>调用次数</th><th>Token</th><th>模型</th></tr></thead>
724
+ <tbody><tr v-for="(v,k) in stats.perChannel" :key="k"><td>{{k}}</td><td>{{fmtNum(v.calls)}}</td><td>{{fmtNum(v.tokens)}}</td><td><span v-for="m in (v.models||[])" class="pill model">{{m}}</span></td></tr></tbody>
745
725
  </table></div>
746
726
  </div>
747
727
 
748
728
  <div class="card" v-if="conversations.length">
749
- <div class="card-hd"><h3>{{conversations.length}} Active Sessions</h3></div>
729
+ <div class="card-hd"><h3>{{conversations.length}} 个活跃会话</h3></div>
750
730
  <span v-for="c in conversations" class="pill tag">{{c.userId}}</span>
751
731
  </div>
752
732
  </div>
753
733
 
754
- <!-- ========== CHANNELS ========== -->
734
+ <!-- ===== CHANNELS ===== -->
755
735
  <div v-if="page==='channels'" class="page-enter">
756
736
  <div class="card">
757
- <div class="card-hd">
758
- <h3>Channels</h3>
759
- <button class="btn pri sm" @click="openModal('channel')">+ New</button>
760
- </div>
737
+ <div class="card-hd"><h3>渠道</h3><button class="btn pri sm" @click="openModal('channel')">+ 新建</button></div>
761
738
  <div class="tbl-wrap"><table class="tbl">
762
- <thead><tr><th>Status</th><th>Name</th><th>Adapter</th><th>Models</th><th>Priority</th><th>Calls</th><th></th></tr></thead>
739
+ <thead><tr><th>状态</th><th>名称</th><th>适配器</th><th>模型</th><th>优先级</th><th>调用次数</th><th></th></tr></thead>
763
740
  <tbody>
764
741
  <tr v-for="ch in channels" :key="ch.id">
765
- <td><span :class="['pill',ch.status==='enabled'?'on':'off']">{{ch.status}}</span></td>
742
+ <td><span :class="['pill',ch.status==='enabled'?'on':'off']">{{ch.status==='enabled'?'启用':'禁用'}}</span></td>
766
743
  <td>{{ch.name}}</td><td>{{ch.adapterType}}</td>
767
744
  <td><span v-for="m in (ch.models||[])" class="pill model">{{m}}</span></td>
768
745
  <td>{{ch.priority}}</td><td>{{fmtNum(ch.statistics?.callTimes)}}</td>
769
- <td>
770
- <button class="btn sm" @click="testChannel(ch.id)">Test</button>
771
- <button class="btn sm" @click="openModal('channel',ch)">Edit</button>
772
- <button class="btn sm danger" @click="deleteItem('channel',ch.id)">Del</button>
773
- </td>
746
+ <td><button class="btn sm" @click="testChannel(ch.id)">测试</button><button class="btn sm" @click="openModal('channel',ch)">编辑</button><button class="btn sm danger" @click="deleteItem('channel',ch.id)">删除</button></td>
774
747
  </tr>
775
- <tr v-if="!channels.length"><td colspan="7" style="color:var(--c-dim);text-align:center;padding:32px">No channels</td></tr>
748
+ <tr v-if="!channels.length"><td colspan="7" style="color:var(--c-dim);text-align:center;padding:32px">暂无渠道</td></tr>
776
749
  </tbody>
777
750
  </table></div>
778
751
  </div>
779
752
  </div>
780
753
 
781
- <!-- ========== TOOLS ========== -->
754
+ <!-- ===== TOOLS ===== -->
782
755
  <div v-if="page==='tools'" class="page-enter">
783
756
  <div class="card">
784
- <div class="card-hd">
785
- <h3>Tools</h3>
786
- <button class="btn pri sm" @click="openModal('tool')">+ New</button>
787
- </div>
757
+ <div class="card-hd"><h3>工具</h3><button class="btn pri sm" @click="openModal('tool')">+ 新建</button></div>
788
758
  <div class="tbl-wrap"><table class="tbl">
789
- <thead><tr><th>Name</th><th>Description</th><th></th></tr></thead>
790
- <tbody>
791
- <tr v-for="t in tools" :key="t.id">
792
- <td>{{t.name||t.id}}</td>
793
- <td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{t.description}}</td>
794
- <td>
795
- <button class="btn sm" @click="openModal('tool',t)">Edit</button>
796
- <button class="btn sm danger" @click="deleteItem('tool',t.id)">Del</button>
797
- </td>
798
- </tr>
799
- <tr v-if="!tools.length"><td colspan="3" style="color:var(--c-dim);text-align:center;padding:32px">No tools</td></tr>
800
- </tbody>
759
+ <thead><tr><th>名称</th><th>描述</th><th></th></tr></thead>
760
+ <tbody><tr v-for="t in tools" :key="t.id"><td>{{t.name||t.id}}</td><td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{t.description}}</td><td><button class="btn sm" @click="openModal('tool',t)">编辑</button><button class="btn sm danger" @click="deleteItem('tool',t.id)">删除</button></td></tr>
761
+ <tr v-if="!tools.length"><td colspan="3" style="color:var(--c-dim);text-align:center;padding:32px">暂无工具</td></tr></tbody>
801
762
  </table></div>
802
763
  </div>
803
764
  </div>
804
765
 
805
- <!-- ========== PRESETS ========== -->
766
+ <!-- ===== PRESETS ===== -->
806
767
  <div v-if="page==='presets'" class="page-enter">
807
768
  <div class="card">
808
- <div class="card-hd">
809
- <h3>Presets</h3>
810
- <button class="btn pri sm" @click="openModal('preset')">+ New</button>
811
- </div>
769
+ <div class="card-hd"><h3>预设</h3><button class="btn pri sm" @click="openModal('preset')">+ 新建</button></div>
812
770
  <div class="tbl-wrap"><table class="tbl">
813
- <thead><tr><th>Name</th><th>Prefix</th><th>Model</th><th>Temp</th><th></th></tr></thead>
814
- <tbody>
815
- <tr v-for="p in presets" :key="p.id">
816
- <td>{{p.name}}</td>
817
- <td><code>{{p.prefix}}</code></td>
818
- <td><span class="pill model">{{p.sendMessageOption?.model||'-'}}</span></td>
819
- <td>{{p.sendMessageOption?.temperature}}</td>
820
- <td>
821
- <button class="btn sm" @click="openModal('preset',p)">Edit</button>
822
- <button class="btn sm danger" @click="deleteItem('preset',p.id)">Del</button>
823
- </td>
824
- </tr>
825
- <tr v-if="!presets.length"><td colspan="5" style="color:var(--c-dim);text-align:center;padding:32px">No presets</td></tr>
826
- </tbody>
771
+ <thead><tr><th>名称</th><th>预设 ID</th><th>模型</th><th>温度</th><th>前缀</th><th>群聊</th><th></th></tr></thead>
772
+ <tbody><tr v-for="p in presets" :key="p.id">
773
+ <td>{{p.name}}</td><td><code>{{p.id}}</code></td>
774
+ <td><span class="pill model">{{p.sendMessageOption?.model||'-'}}</span></td><td>{{p.sendMessageOption?.temperature}}</td>
775
+ <td><code v-if="p.prefix" style="font-size:11px" :title="p.prefix">{{fLen(p.prefix,12)}}</code><span v-else style="color:var(--c-faint)">-</span></td>
776
+ <td><span class="pill" :class="p.groupContext==='disabled'?'off':(p.groupContext?'on':'')">{{p.groupContext==='disabled'?'禁用':p.groupContext==='enabled'?'启用':p.groupContext==='use_system'?'系统':'默认'}}</span></td>
777
+ <td><button class="btn sm" @click="openModal('preset',p)">编辑</button><button class="btn sm danger" @click="deleteItem('preset',p.id)">删除</button></td>
778
+ </tr>
779
+ <tr v-if="!presets.length"><td colspan="5" style="color:var(--c-dim);text-align:center;padding:32px">暂无预设</td></tr></tbody>
827
780
  </table></div>
828
781
  </div>
829
782
  </div>
830
783
 
831
- <!-- ========== PROCESSORS ========== -->
784
+ <!-- ===== PROCESSORS ===== -->
832
785
  <div v-if="page==='processors'" class="page-enter">
833
786
  <div class="card">
834
- <div class="card-hd">
835
- <h3>Processors</h3>
836
- <button class="btn pri sm" @click="openModal('processor')">+ New</button>
837
- </div>
787
+ <div class="card-hd"><h3>处理器</h3><button class="btn pri sm" @click="openModal('processor')">+ 新建</button></div>
838
788
  <div class="tbl-wrap"><table class="tbl">
839
- <thead><tr><th>Name</th><th>Type</th><th>Description</th><th></th></tr></thead>
840
- <tbody>
841
- <tr v-for="p in processors" :key="p.id">
842
- <td>{{p.name}}</td>
843
- <td><span class="pill" :style="{background:p.type==='pre'?'rgba(167,139,250,.12)':'rgba(250,204,21,.12)',color:p.type==='pre'?'var(--c-purple)':'var(--c-yellow)'}">{{p.type}}</span></td>
844
- <td>{{p.description}}</td>
845
- <td>
846
- <button class="btn sm" @click="openModal('processor',p)">Edit</button>
847
- <button class="btn sm danger" @click="deleteItem('processor',p.id)">Del</button>
848
- </td>
849
- </tr>
850
- <tr v-if="!processors.length"><td colspan="4" style="color:var(--c-dim);text-align:center;padding:32px">No processors</td></tr>
851
- </tbody>
789
+ <thead><tr><th>名称</th><th>类型</th><th>描述</th><th></th></tr></thead>
790
+ <tbody><tr v-for="p in processors" :key="p.id"><td>{{p.name}}</td><td><span class="pill" :style="{background:p.type==='pre'?'rgba(201,177,255,.12)':'rgba(255,224,160,.12)',color:p.type==='pre'?'var(--c-purple)':'var(--c-yellow)'}">{{p.type==='pre'?'前置':'后置'}}</span></td><td>{{p.description}}</td><td><button class="btn sm" @click="openModal('processor',p)">编辑</button><button class="btn sm danger" @click="deleteItem('processor',p.id)">删除</button></td></tr>
791
+ <tr v-if="!processors.length"><td colspan="4" style="color:var(--c-dim);text-align:center;padding:32px">暂无处理器</td></tr></tbody>
852
792
  </table></div>
853
793
  </div>
854
794
  </div>
855
795
 
856
- <!-- ========== TRIGGERS ========== -->
796
+ <!-- ===== TRIGGERS ===== -->
857
797
  <div v-if="page==='triggers'" class="page-enter">
858
798
  <div class="card">
859
- <div class="card-hd">
860
- <h3>Triggers</h3>
861
- <button class="btn pri sm" @click="openModal('trigger')">+ New</button>
862
- </div>
799
+ <div class="card-hd"><h3>触发器</h3><button class="btn pri sm" @click="openModal('trigger')">+ 新建</button></div>
863
800
  <div class="tbl-wrap"><table class="tbl">
864
- <thead><tr><th>Name</th><th>Description</th><th></th></tr></thead>
865
- <tbody>
866
- <tr v-for="t in triggers" :key="t.id">
867
- <td>{{t.name||t.id}}</td><td>{{t.description}}</td>
868
- <td>
869
- <button class="btn sm" @click="openModal('trigger',t)">Edit</button>
870
- <button class="btn sm danger" @click="deleteItem('trigger',t.id)">Del</button>
871
- </td>
872
- </tr>
873
- <tr v-if="!triggers.length"><td colspan="3" style="color:var(--c-dim);text-align:center;padding:32px">No triggers</td></tr>
874
- </tbody>
801
+ <thead><tr><th>名称</th><th>描述</th><th></th></tr></thead>
802
+ <tbody><tr v-for="t in triggers" :key="t.id"><td>{{t.name||t.id}}</td><td>{{t.description}}</td><td><button class="btn sm" @click="openModal('trigger',t)">编辑</button><button class="btn sm danger" @click="deleteItem('trigger',t.id)">删除</button></td></tr>
803
+ <tr v-if="!triggers.length"><td colspan="3" style="color:var(--c-dim);text-align:center;padding:32px">暂无触发器</td></tr></tbody>
875
804
  </table></div>
876
805
  </div>
877
806
  </div>
878
807
 
879
- <!-- ========== GROUPS ========== -->
808
+ <!-- ===== GROUPS ===== -->
880
809
  <div v-if="page==='groups'" class="page-enter">
881
810
  <div class="card">
882
- <div class="card-hd">
883
- <h3>Tool Groups</h3>
884
- <button class="btn pri sm" @click="openModal('toolGroup')">+ New</button>
885
- </div>
811
+ <div class="card-hd"><h3>工具组</h3><button class="btn pri sm" @click="openModal('toolGroup')">+ 新建</button></div>
886
812
  <div class="tbl-wrap"><table class="tbl">
887
- <thead><tr><th>Name</th><th>Description</th><th>Default</th><th></th></tr></thead>
888
- <tbody>
889
- <tr v-for="g in toolGroups" :key="g.id">
890
- <td>{{g.name}}</td><td>{{g.description}}</td>
891
- <td><span v-if="g.isDefault" class="pill on">Default</span></td>
892
- <td>
893
- <button class="btn sm" @click="openModal('toolGroup',g)">Edit</button>
894
- <button class="btn sm danger" @click="deleteItem('toolGroup',g.id)">Del</button>
895
- </td>
896
- </tr>
897
- <tr v-if="!toolGroups.length"><td colspan="4" style="color:var(--c-dim);text-align:center;padding:32px">No groups</td></tr>
898
- </tbody>
813
+ <thead><tr><th>名称</th><th>描述</th><th>默认</th><th></th></tr></thead>
814
+ <tbody><tr v-for="g in toolGroups" :key="g.id"><td>{{g.name}}</td><td>{{g.description}}</td><td><span v-if="g.isDefault" class="pill on">默认</span></td><td><button class="btn sm" @click="openModal('toolGroup',g)">编辑</button><button class="btn sm danger" @click="deleteItem('toolGroup',g.id)">删除</button></td></tr>
815
+ <tr v-if="!toolGroups.length"><td colspan="4" style="color:var(--c-dim);text-align:center;padding:32px">暂无工具组</td></tr></tbody>
899
816
  </table></div>
900
817
  </div>
901
818
  </div>
902
819
 
903
- <!-- ========== SESSIONS ========== -->
820
+ <!-- ===== SESSIONS ===== -->
904
821
  <div v-if="page==='conversations'" class="page-enter">
905
822
  <div class="card">
906
- <div class="card-hd"><h3>Active Sessions</h3></div>
823
+ <div class="card-hd"><h3>活跃会话</h3></div>
907
824
  <div class="tbl-wrap"><table class="tbl">
908
- <thead><tr><th>User ID</th><th>Conversation</th><th>Model</th><th></th></tr></thead>
909
- <tbody>
910
- <tr v-for="c in conversations" :key="c.userId">
911
- <td>{{c.userId}}</td>
912
- <td><code style="font-size:11px">{{c.conversationId||'-'}}</code></td>
913
- <td><span v-if="c.currentModel" class="pill model">{{c.currentModel}}</span><span v-else style="color:var(--c-dim)">-</span></td>
914
- <td>
915
- <button class="btn sm" @click="viewConv(c.userId)">View</button>
916
- <button class="btn sm danger" @click="clearConv(c.userId)">Clear</button>
917
- </td>
918
- </tr>
919
- <tr v-if="!conversations.length"><td colspan="4" style="color:var(--c-dim);text-align:center;padding:32px">No active sessions</td></tr>
920
- </tbody>
825
+ <thead><tr><th>用户 ID</th><th>对话 ID</th><th>模型</th><th></th></tr></thead>
826
+ <tbody><tr v-for="c in conversations" :key="c.userId"><td>{{c.userId}}</td><td><code style="font-size:11px">{{c.conversationId||'-'}}</code></td><td><span v-if="c.currentModel" class="pill model">{{c.currentModel}}</span><span v-else style="color:var(--c-dim)">-</span></td><td><button class="btn sm" @click="viewConv(c.userId)">查看</button><button class="btn sm danger" @click="clearConv(c.userId)">清除</button></td></tr>
827
+ <tr v-if="!conversations.length"><td colspan="4" style="color:var(--c-dim);text-align:center;padding:32px">暂无活跃会话</td></tr></tbody>
921
828
  </table></div>
922
829
  </div>
923
830
  </div>
924
831
 
925
- <!-- ========== CONFIG ========== -->
832
+ <!-- ===== CONFIG (redesigned) ===== -->
926
833
  <div v-if="page==='config'" class="page-enter">
927
834
  <div class="card">
928
- <div class="card-hd"><h3>Plugin Configuration</h3></div>
929
- <div v-if="cfgSections.length">
930
- <div v-for="sec in cfgSections" :key="sec.key" class="cfg-sec">
931
- <div class="cfg-head" @click="toggleCfg(sec.key)">
932
- <span>{{sec.label}} <span style="font-weight:400;color:var(--c-faint);font-size:11px">· {{Object.keys(sec.fields||{}).length}} items</span></span>
933
- <span :class="['arr',expandedCfg[sec.key]&&'open']">▶</span>
835
+ <div class="card-hd">
836
+ <h3>插件配置</h3>
837
+ <button class="btn sm" @click="editConfig">编辑完整 JSON</button>
838
+ </div>
839
+ <div v-if="cfgSections.length" class="cfg-grid">
840
+ <div v-for="sec in cfgSections" :key="sec.key"
841
+ :class="['cfg-card',expandedCfg[sec.key]&&'expanded']">
842
+ <div class="cfg-card-head" @click="toggleCfg(sec.key)">
843
+ <span>{{sec.label}}</span>
844
+ <span style="display:flex;align-items:center;gap:8px">
845
+ <span style="font-weight:400;color:var(--c-faint);font-size:11px">{{Object.keys(sec.fields||{}).length}} 项</span>
846
+ <span :class="['arr',expandedCfg[sec.key]&&'open']">▶</span>
847
+ </span>
934
848
  </div>
935
- <div v-if="expandedCfg[sec.key]" class="cfg-body">
936
- <div v-for="(fv,fk) in sec.fields" :key="fk" class="cfg-fld">
937
- <span class="k">{{fk}}</span>
938
- <span class="v">
939
- <template v-if="typeof fv==='boolean'"><span :class="['pill',fv?'on':'off']">{{fv}}</span></template>
940
- <template v-else-if="typeof fv==='number'"><span class="num">{{fv}}</span></template>
941
- <template v-else-if="typeof fv==='string'&&fv.length<=80"><code>{{fv||'""'}}</code></template>
942
- <template v-else-if="typeof fv==='string'"><code :title="fv">{{fLen(fv,60)}}</code></template>
943
- <template v-else><span style="color:var(--c-faint);font-size:11px">obj</span></template>
944
- </span>
945
- </div>
849
+ <div :class="['cfg-card-body',expandedCfg[sec.key]&&'show']">
850
+ <template v-if="sec.isObj">
851
+ <div v-for="(fv,fk) in flattenFields(sec.fields)" :key="fk"
852
+ :class="['cfg-field',editingCfgField&&editingCfgField.sec===sec.key&&editingCfgField.key===fk?'editing':'']">
853
+ <span class="k">{{cfgLabel(fk)}}</span>
854
+ <span class="v">
855
+ <template v-if="typeof fv==='boolean'"><span :class="['pill',fv?'on':'off']">{{fv?'':'否'}}</span></template>
856
+ <template v-else-if="typeof fv==='number'"><span class="num">{{fv}}</span></template>
857
+ <template v-else-if="Array.isArray(fv)"><code style="font-size:10px">[{{fv.length}}项]</code></template>
858
+ <template v-else-if="typeof fv==='string'&&fv.length<=50"><code>{{fv||'""'}}</code></template>
859
+ <template v-else-if="typeof fv==='string'"><code :title="fv">{{fLen(fv,30)}}</code></template>
860
+ <template v-else><span style="color:var(--c-faint);font-size:11px">对象</span></template>
861
+ </span>
862
+ <button v-if="typeof fv!=='object'||Array.isArray(fv)" class="edit-btn" @click="startEditField(sec.key,fk,fv)" title="编辑">✎</button>
863
+ <div v-if="editingCfgField" class="edit-row">
864
+ <input v-if="Array.isArray(fv)||(typeof fv==='string'&&fv.length>50)" v-model="editingCfgField.val" @keyup.enter="saveEditField(sec.key,fk)"/>
865
+ <input v-else-if="typeof fv==='string'" v-model="editingCfgField.val" @keyup.enter="saveEditField(sec.key,fk)"/>
866
+ <select v-else-if="typeof fv==='boolean'" v-model="editingCfgField.val"><option value="true">是</option><option value="false">否</option></select>
867
+ <input v-else v-model="editingCfgField.val" type="number" @keyup.enter="saveEditField(sec.key,fk)"/>
868
+ <button class="btn-xs save" @click="saveEditField(sec.key,fk)">保存</button>
869
+ <button class="btn-xs" @click="cancelEditField">取消</button>
870
+ </div>
871
+ </div>
872
+ </template>
873
+ <div v-else style="color:var(--c-dim);font-size:13px;padding:8px 0">{{JSON.stringify(sec.fields)}}</div>
946
874
  </div>
947
875
  </div>
948
876
  </div>
949
- <div v-else style="color:var(--c-dim);font-size:13px;padding:20px 0">No config data</div>
950
- <div class="cfg-json-btn"><button class="btn" @click="editConfig">Edit JSON</button></div>
877
+ <div v-else style="color:var(--c-dim);font-size:13px;padding:20px 0">暂无配置数据</div>
951
878
  </div>
952
879
  </div>
953
880
 
954
- </div><!-- .content -->
955
- </div><!-- .main -->
956
- </div><!-- .layout -->
881
+ </div>
882
+ </div>
883
+ </div>
957
884
 
958
885
  <!-- ===== MODALS ===== -->
959
-
960
- <!-- CRUD Modal -->
961
886
  <div v-if="modal" class="modal-mask" @click.self="closeModal">
962
887
  <div class="modal">
963
- <h3>{{editing?'Edit':'New'}} {{modal}}</h3>
888
+ <h3>{{editing?'编辑':'新建'}} {{modal}}</h3>
964
889
 
890
+ <!-- Channel -->
965
891
  <template v-if="modal==='channel'">
966
- <div class="f-row"><label>Name</label><input v-model="form.name"/></div>
967
- <div class="f-row"><label>Adapter</label><select v-model="form.adapterType"><option>openai</option><option>gemini</option><option>claude</option></select></div>
968
- <div class="f-row"><label>Models (comma separated)</label><input v-model="form._models" :placeholder="(form.models||[]).join(', ')"/></div>
892
+ <div class="f-row"><label>名称</label><input v-model="form.name"/></div>
893
+ <div class="f-row"><label>适配器</label><select v-model="form.adapterType"><option>openai</option><option>gemini</option><option>claude</option></select></div>
894
+ <div class="f-row"><label>模型(逗号分隔)</label><input v-model="form._models" :placeholder="(form.models||[]).join(', ')"/></div>
969
895
  <div class="f-row"><label>Base URL</label><input v-model="form.options.baseUrl"/></div>
970
896
  <div class="f-row"><label>API Key</label><input v-model="form.options.apiKey" type="password"/></div>
971
- <div class="f-row"><label>Priority</label><input v-model.number="form.priority" type="number"/></div>
972
- <div class="f-row"><label>Weight</label><input v-model.number="form.weight" type="number"/></div>
973
- <div class="f-row"><label>Status</label><select v-model="form.status"><option>enabled</option><option>disabled</option></select></div>
974
-
975
- <!-- 模型高级配置 -->
976
- <div style="margin-top:16px;padding-top:16px;border-top:1px solid var(--c-border)">
977
- <h4 style="font-size:14px;font-weight:600;margin-bottom:12px;color:var(--c-accent)">Model Configuration</h4>
978
-
979
- <div class="f-row"><label>Model Name</label><input v-model="form.sendMessageOption.model"/></div>
980
- <div class="f-row"><label>Temperature</label><input v-model.number="form.sendMessageOption.temperature" type="number" step="0.1" min="0" max="2"/></div>
981
- <div class="f-row"><label>Max Token</label><input v-model.number="form.sendMessageOption.maxToken" type="number" min="1"/></div>
982
- <div class="f-row"><label>System Prompt</label><textarea v-model="form.sendMessageOption.systemOverride" placeholder="Override system prompt..."></textarea></div>
983
-
984
- <!-- Gemini 思考配置 -->
985
- <div v-if="form.adapterType==='gemini'" style="margin-top:12px;padding:12px;background:var(--c-surface2);border-radius:8px">
986
- <h5 style="font-size:13px;font-weight:600;margin-bottom:8px;color:var(--c-purple)">Gemini Thinking Configuration</h5>
987
- <div class="f-row">
988
- <label>Enable Reasoning</label>
989
- <select v-model="form.sendMessageOption.enableReasoning">
990
- <option :value="false">Disabled</option>
991
- <option :value="true">Enabled</option>
992
- </select>
993
- </div>
994
- <div class="f-row" v-if="form.sendMessageOption.enableReasoning">
995
- <label>Thinking Level</label>
996
- <select v-model="form.sendMessageOption.reasoningEffort">
997
- <option value="minimal">Minimal (Low Latency)</option>
998
- <option value="low">Low</option>
999
- <option value="medium">Medium (Balanced)</option>
1000
- <option value="high">High (Deep Reasoning)</option>
1001
- </select>
1002
- </div>
897
+ <div class="f-row"><label>优先级</label><input v-model.number="form.priority" type="number"/></div>
898
+ <div class="f-row"><label>权重</label><input v-model.number="form.weight" type="number"/></div>
899
+ <div class="f-row"><label>状态</label><select v-model="form.status"><option>enabled</option><option>disabled</option></select></div>
900
+
901
+ <div class="sub-section">
902
+ <h5 style="font-size:13px;font-weight:600;margin-bottom:10px;color:var(--c-accent)">✦ 模型配置</h5>
903
+ <div class="f-row"><label>模型名称</label><input v-model="form.sendMessageOption.model"/></div>
904
+ <div class="f-row"><label>温度</label><input v-model.number="form.sendMessageOption.temperature" type="number" step="0.1" min="0" max="2"/></div>
905
+ <div class="f-row"><label>最大 Token</label><input v-model.number="form.sendMessageOption.maxToken" type="number" min="1"/></div>
906
+ <div class="f-row"><label>系统提示词</label><textarea v-model="form.sendMessageOption.systemOverride" placeholder="覆盖系统提示词..."></textarea></div>
907
+
908
+ <div v-if="form.adapterType==='gemini'" class="sub-section">
909
+ <h5 class="gemini">Gemini 思考配置</h5>
910
+ <div class="f-row"><label>启用推理</label><select v-model="form.sendMessageOption.enableReasoning"><option :value="false">关闭</option><option :value="true">开启</option></select></div>
911
+ <div class="f-row" v-if="form.sendMessageOption.enableReasoning"><label>思考等级</label><select v-model="form.sendMessageOption.reasoningEffort"><option value="minimal">极低</option><option value="low">低</option><option value="medium">中</option><option value="high">高</option></select></div>
1003
912
  </div>
1004
-
1005
- <!-- 工具调用限制 -->
1006
- <div style="margin-top:12px;padding:12px;background:var(--c-surface2);border-radius:8px">
1007
- <h5 style="font-size:13px;font-weight:600;margin-bottom:8px;color:var(--c-yellow)">Tool Call Limits</h5>
1008
- <div class="f-row">
1009
- <label>Max Consecutive Calls</label>
1010
- <input v-model.number="form.sendMessageOption.toolCallLimit.maxConsecutiveCalls" type="number" min="1"/>
1011
- </div>
1012
- <div class="f-row">
1013
- <label>Max Consecutive Identical Calls</label>
1014
- <input v-model.number="form.sendMessageOption.toolCallLimit.maxConsecutiveIdenticalCalls" type="number" min="1"/>
1015
- </div>
1016
- </div>
1017
-
1018
- <!-- 高级配置(JSON 格式) -->
1019
- <div style="margin-top:12px">
1020
- <div class="f-row">
1021
- <label>Response Modalities (JSON array)</label>
1022
- <input v-model="form._responseModalities" :placeholder="JSON.stringify(form.sendMessageOption.responseModalities||[])"/>
1023
- </div>
1024
- <div class="f-row">
1025
- <label>Safety Settings (JSON array)</label>
1026
- <textarea v-model="form._safetySettings" class="mono" style="min-height:60px" :placeholder="JSON.stringify(form.sendMessageOption.safetySettings||[],null,2)"></textarea>
1027
- </div>
913
+
914
+ <div class="sub-section">
915
+ <h5 class="tool">工具调用限制</h5>
916
+ <div class="f-row"><label>最大连续调用</label><input v-model.number="form.sendMessageOption.toolCallLimit.maxConsecutiveCalls" type="number" min="1"/></div>
917
+ <div class="f-row"><label>最大连续相同调用</label><input v-model.number="form.sendMessageOption.toolCallLimit.maxConsecutiveIdenticalCalls" type="number" min="1"/></div>
1028
918
  </div>
919
+
920
+ <div class="f-row"><label>响应模态 (JSON)</label><input v-model="form._responseModalities" :placeholder="JSON.stringify(form.sendMessageOption.responseModalities||[])"/></div>
921
+ <div class="f-row"><label>安全设置 (JSON)</label><textarea v-model="form._safetySettings" class="mono" style="min-height:50px" :placeholder="JSON.stringify(form.sendMessageOption.safetySettings||[],null,2)"></textarea></div>
1029
922
  </div>
1030
923
  </template>
1031
924
 
925
+ <!-- Preset -->
1032
926
  <template v-if="modal==='preset'">
1033
- <div class="f-row"><label>Name</label><input v-model="form.name"/></div>
1034
- <div class="f-row"><label>Prefix</label><input v-model="form.prefix"/></div>
1035
- <div class="f-row"><label>Model</label><input v-model="form.sendMessageOption.model"/></div>
1036
- <div class="f-row"><label>Temperature</label><input v-model.number="form.sendMessageOption.temperature" type="number" step="0.1" min="0" max="2"/></div>
1037
- <div class="f-row"><label>Max Token</label><input v-model.number="form.sendMessageOption.maxToken" type="number" min="1"/></div>
1038
- <div class="f-row"><label>System Prompt</label><textarea v-model="form.sendMessageOption.systemOverride" placeholder="System prompt override..."></textarea></div>
1039
-
1040
- <!-- 高级配置 -->
1041
- <div style="margin-top:16px;padding-top:16px;border-top:1px solid var(--c-border)">
1042
- <h4 style="font-size:14px;font-weight:600;margin-bottom:12px;color:var(--c-accent)">Advanced Configuration</h4>
1043
-
1044
- <div class="f-row">
1045
- <label>Enable Reasoning</label>
1046
- <select v-model="form.sendMessageOption.enableReasoning">
1047
- <option :value="false">Disabled</option>
1048
- <option :value="true">Enabled</option>
1049
- </select>
1050
- </div>
1051
-
1052
- <div class="f-row" v-if="form.sendMessageOption.enableReasoning">
1053
- <label>Reasoning Effort</label>
1054
- <select v-model="form.sendMessageOption.reasoningEffort">
1055
- <option value="minimal">Minimal</option>
1056
- <option value="low">Low</option>
1057
- <option value="medium">Medium</option>
1058
- <option value="high">High</option>
1059
- </select>
1060
- </div>
1061
-
1062
- <div class="f-row">
1063
- <label>Disable History Read</label>
1064
- <select v-model="form.sendMessageOption.disableHistoryRead">
1065
- <option :value="false">No</option>
1066
- <option :value="true">Yes</option>
1067
- </select>
1068
- </div>
1069
-
1070
- <div class="f-row">
1071
- <label>Disable History Save</label>
1072
- <select v-model="form.sendMessageOption.disableHistorySave">
1073
- <option :value="false">No</option>
1074
- <option :value="true">Yes</option>
1075
- </select>
1076
- </div>
927
+ <div class="f-row"><label>名称</label><input v-model="form.name"/></div>
928
+ <div class="f-row"><label>前缀</label><input v-model="form.prefix"/></div>
929
+ <div class="f-row"><label>模型</label><input v-model="form.sendMessageOption.model"/></div>
930
+ <div class="f-row"><label>温度</label><input v-model.number="form.sendMessageOption.temperature" type="number" step="0.1" min="0" max="2"/></div>
931
+ <div class="f-row"><label>最大 Token</label><input v-model.number="form.sendMessageOption.maxToken" type="number" min="1"/></div>
932
+ <div class="f-row"><label>系统提示词</label><textarea v-model="form.sendMessageOption.systemOverride" placeholder="覆盖系统提示词..."></textarea></div>
933
+ <div class="sub-section">
934
+ <h5 style="font-size:13px;font-weight:600;margin-bottom:10px;color:var(--c-accent)">✦ 高级配置</h5>
935
+ <div class="f-row"><label>启用推理</label><select v-model="form.sendMessageOption.enableReasoning"><option :value="false">关闭</option><option :value="true">开启</option></select></div>
936
+ <div class="f-row" v-if="form.sendMessageOption.enableReasoning"><label>推理强度</label><select v-model="form.sendMessageOption.reasoningEffort"><option value="minimal">极低</option><option value="low">低</option><option value="medium">中</option><option value="high">高</option></select></div>
937
+ <div class="f-row"><label>禁用读历史</label><select v-model="form.sendMessageOption.disableHistoryRead"><option :value="false">否</option><option :value="true">是</option></select></div>
938
+ <div class="f-row"><label>禁用写历史</label><select v-model="form.sendMessageOption.disableHistorySave"><option :value="false">否</option><option :value="true">是</option></select></div>
939
+ <div class="f-row"><label>群聊上下文</label><select v-model="form.groupContext"><option :value="undefined">默认</option><option value="enabled">启用</option><option value="disabled">禁用</option><option value="use_system">使用系统预设</option></select></div>
940
+ <div class="f-row"><label>禁用系统提示词</label><select v-model="form.disableSystemInstructions"><option :value="false">否</option><option :value="true">是</option></select></div>
1077
941
  </div>
1078
942
  </template>
1079
943
 
944
+ <!-- Tool / Processor / Trigger -->
1080
945
  <template v-if="modal==='tool'||modal==='processor'||modal==='trigger'">
1081
- <div class="f-row"><label>Name</label><input v-model="form.name"/></div>
1082
- <div class="f-row" v-if="modal==='processor'"><label>Type</label><select v-model="form.type"><option>pre</option><option>post</option></select></div>
1083
- <div class="f-row"><label>Description</label><input v-model="form.description"/></div>
1084
- <div class="f-row"><label>Code</label><textarea v-model="form.code" class="mono" style="min-height:200px"></textarea></div>
946
+ <div class="f-row"><label>名称</label><input v-model="form.name"/></div>
947
+ <div class="f-row" v-if="modal==='processor'"><label>类型</label><select v-model="form.type"><option>pre</option><option>post</option></select></div>
948
+ <div class="f-row"><label>描述</label><input v-model="form.description"/></div>
949
+ <div class="f-row"><label>代码</label><textarea v-model="form.code" class="mono" style="min-height:200px"></textarea></div>
1085
950
  </template>
1086
951
 
952
+ <!-- ToolGroup -->
1087
953
  <template v-if="modal==='toolGroup'">
1088
- <div class="f-row"><label>Name</label><input v-model="form.name"/></div>
1089
- <div class="f-row"><label>Description</label><input v-model="form.description"/></div>
1090
- <div class="f-row"><label>Tool IDs (comma)</label><input v-model="form._toolIds" :placeholder="(form.toolIds||[]).join(', ')"/></div>
1091
- <div class="f-row"><label>Status</label><select v-model="form.status"><option>enabled</option><option>disabled</option></select></div>
954
+ <div class="f-row"><label>名称</label><input v-model="form.name"/></div>
955
+ <div class="f-row"><label>描述</label><input v-model="form.description"/></div>
956
+ <div class="f-row"><label>工具 ID(逗号分隔)</label><input v-model="form._toolIds" :placeholder="(form.toolIds||[]).join(', ')"/></div>
957
+ <div class="f-row"><label>状态</label><select v-model="form.status"><option>enabled</option><option>disabled</option></select></div>
1092
958
  </template>
1093
959
 
1094
960
  <div class="modal-btns">
1095
- <button class="btn" @click="closeModal">Cancel</button>
1096
- <button class="btn pri" @click="saveItem">Save</button>
961
+ <button class="btn" @click="closeModal">取消</button>
962
+ <button class="btn pri" @click="saveItem">保存</button>
1097
963
  </div>
1098
964
  </div>
1099
965
  </div>
@@ -1101,24 +967,21 @@ createApp({
1101
967
  <!-- Config JSON Edit Modal -->
1102
968
  <div v-if="editingConfig" class="modal-mask" @click.self="editingConfig=false">
1103
969
  <div class="modal">
1104
- <h3>Edit Configuration</h3>
1105
- <div class="f-row"><label>JSON</label><textarea v-model="configForm" class="mono" style="min-height:360px"></textarea></div>
1106
- <div class="modal-btns">
1107
- <button class="btn" @click="editingConfig=false">Cancel</button>
1108
- <button class="btn pri" @click="saveConfig">Save</button>
1109
- </div>
970
+ <h3>编辑配置 JSON</h3>
971
+ <div class="f-row"><textarea v-model="configForm" class="mono" style="min-height:400px"></textarea></div>
972
+ <div class="modal-btns"><button class="btn" @click="editingConfig=false">取消</button><button class="btn pri" @click="saveConfig">保存</button></div>
1110
973
  </div>
1111
974
  </div>
1112
975
 
1113
976
  <!-- Session Detail Modal -->
1114
977
  <div v-if="convDetail" class="modal-mask" @click.self="convDetail=null">
1115
978
  <div class="modal">
1116
- <h3>Session: {{convDetail.userId}}</h3>
1117
- <div class="f-row"><label>Conversation ID</label><input :value="convDetail.current?.conversationId||'-'" readonly/></div>
1118
- <div class="f-row"><label>Model</label><input :value="convDetail.current?.model||'-'" readonly/></div>
1119
- <div class="f-row"><label>Preset</label><input :value="convDetail.current?.presetId||'-'" readonly/></div>
1120
- <div class="f-row"><label>Full State</label><textarea readonly :value="JSON.stringify(convDetail,null,2)" class="mono" style="min-height:200px"></textarea></div>
1121
- <div class="modal-btns"><button class="btn" @click="convDetail=null">Close</button></div>
979
+ <h3>会话详情 · {{convDetail.userId}}</h3>
980
+ <div class="f-row"><label>对话 ID</label><input :value="convDetail.current?.conversationId||'-'" readonly/></div>
981
+ <div class="f-row"><label>模型</label><input :value="convDetail.current?.model||'-'" readonly/></div>
982
+ <div class="f-row"><label>预设</label><input :value="convDetail.current?.presetId||'-'" readonly/></div>
983
+ <div class="f-row"><label>完整状态</label><textarea readonly :value="JSON.stringify(convDetail,null,2)" class="mono" style="min-height:200px"></textarea></div>
984
+ <div class="modal-btns"><button class="btn" @click="convDetail=null">关闭</button></div>
1122
985
  </div>
1123
986
  </div>
1124
987
  `