@simonyea/holysheep-cli 1.7.54 → 1.7.55

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.
@@ -5,869 +5,532 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>HolySheep WebUI</title>
7
7
  <style>
8
- /* ── Reset & Base ──────────────────────────────────────────────────────── */
9
8
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
10
9
  :root {
11
- --bg: #f5f6fa; --surface: #fff; --surface2: #eef0f4;
12
- --text: #1a1a2e; --text2: #666; --border: #dde;
13
- --primary: #e8a46a; --primary-dim: rgba(232,164,106,0.15);
14
- --success: #22c55e; --success-dim: rgba(34,197,94,0.12);
15
- --warning: #f59e0b; --warning-dim: rgba(245,158,11,0.12);
16
- --error: #ef4444; --error-dim: rgba(239,68,68,0.12);
17
- --radius: 10px; --shadow: 0 1px 3px rgba(0,0,0,0.06);
18
- --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
19
- --mono: "SF Mono", "Cascadia Code", "Fira Code", Consolas, monospace;
10
+ --bg: #f5f6fa; --surface: #fff; --surface2: #f0f1f5;
11
+ --text: #1a1a2e; --text2: #777; --border: #e2e4ea;
12
+ --primary: #e8a46a; --primary-dim: rgba(232,164,106,0.12);
13
+ --success: #22c55e; --success-dim: rgba(34,197,94,0.10);
14
+ --warning: #f59e0b; --warning-dim: rgba(245,158,11,0.10);
15
+ --error: #ef4444; --error-dim: rgba(239,68,68,0.10);
16
+ --radius: 12px; --shadow: 0 1px 4px rgba(0,0,0,0.06);
17
+ --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
18
+ --mono: "SF Mono", "Cascadia Code", Consolas, monospace;
20
19
  }
21
20
  @media (prefers-color-scheme: dark) {
22
21
  :root {
23
- --bg: #0f0f1a; --surface: #1a1a2e; --surface2: #222240;
24
- --text: #e8e8f0; --text2: #888; --border: #333350;
25
- --shadow: 0 1px 3px rgba(0,0,0,0.3);
22
+ --bg: #0d0d1a; --surface: #181828; --surface2: #1e1e32;
23
+ --text: #e4e4f0; --text2: #888; --border: #2a2a44;
24
+ --shadow: 0 1px 4px rgba(0,0,0,0.3);
26
25
  }
27
26
  }
28
27
  html { font-size: 15px; }
29
28
  body { font-family: var(--font); background: var(--bg); color: var(--text); min-height: 100vh; }
30
29
  a { color: var(--primary); text-decoration: none; }
31
30
  a:hover { text-decoration: underline; }
32
- button { font-family: var(--font); cursor: pointer; border: none; border-radius: 6px; padding: 8px 16px; font-size: 0.9rem; transition: all .15s; }
33
- input[type="text"], input[type="password"] {
34
- font-family: var(--mono); background: var(--surface2); color: var(--text); border: 1px solid var(--border);
35
- border-radius: 6px; padding: 8px 12px; font-size: 0.9rem; width: 100%; outline: none;
36
- }
37
- input:focus { border-color: var(--primary); box-shadow: 0 0 0 2px var(--primary-dim); }
38
-
39
- /* ── Layout ────────────────────────────────────────────────────────────── */
40
- .app { max-width: 960px; margin: 0 auto; padding: 20px; }
41
- .header { display: flex; align-items: center; justify-content: space-between; padding: 16px 0; margin-bottom: 8px; }
42
- .header h1 { font-size: 1.4rem; font-weight: 700; }
43
- .header h1 span { color: var(--primary); }
44
- .header .ver { color: var(--text2); font-size: 0.8rem; }
45
-
46
- /* ── Tabs ──────────────────────────────────────────────────────────────── */
47
- .tabs { display: flex; gap: 4px; border-bottom: 2px solid var(--border); margin-bottom: 20px; }
48
- .tab { padding: 10px 18px; font-size: 0.9rem; color: var(--text2); background: none; border-radius: 8px 8px 0 0;
49
- position: relative; transition: color .15s; }
50
- .tab:hover { color: var(--text); background: var(--surface); }
51
- .tab.active { color: var(--primary); font-weight: 600; }
52
- .tab.active::after { content: ''; position: absolute; bottom: -2px; left: 0; right: 0; height: 2px; background: var(--primary); }
53
-
54
- /* ── Cards ─────────────────────────────────────────────────────────────── */
55
- .card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; margin-bottom: 16px; box-shadow: var(--shadow); }
56
- .card h3 { font-size: 1rem; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
57
- .card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
58
- .stat { text-align: center; padding: 12px; }
59
- .stat .value { font-size: 1.6rem; font-weight: 700; color: var(--primary); }
60
- .stat .label { font-size: 0.8rem; color: var(--text2); margin-top: 4px; }
61
-
62
- /* ── Buttons ───────────────────────────────────────────────────────────── */
63
- .btn-primary { background: var(--primary); color: #fff; font-weight: 600; }
64
- .btn-primary:hover { filter: brightness(1.1); }
65
- .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
66
- .btn-secondary { background: var(--surface2); color: var(--text); border: 1px solid var(--border); }
67
- .btn-secondary:hover { background: var(--border); }
68
- .btn-danger { background: var(--error); color: #fff; }
69
- .btn-danger:hover { filter: brightness(1.1); }
70
- .btn-sm { padding: 5px 10px; font-size: 0.8rem; }
71
- .btn-group { display: flex; gap: 8px; flex-wrap: wrap; }
72
31
 
73
- /* ── Status indicators ─────────────────────────────────────────────────── */
74
- .dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
32
+ /* Layout */
33
+ .app { max-width: 720px; margin: 0 auto; padding: 16px 20px 40px; }
34
+
35
+ /* Header */
36
+ .header { display: flex; align-items: center; justify-content: space-between; padding: 16px 0 12px; }
37
+ .header-left { display: flex; align-items: center; gap: 12px; }
38
+ .header-left h1 { font-size: 1.3rem; font-weight: 700; }
39
+ .header-left h1 span { color: var(--primary); }
40
+ .header-left a { font-size: 0.8rem; color: var(--text2); }
41
+ .header-right { display: flex; align-items: center; gap: 12px; }
42
+ .lang-btn { background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: 4px 10px; font-size: 0.75rem; color: var(--text2); cursor: pointer; font-family: var(--font); }
43
+ .lang-btn:hover { border-color: var(--primary); color: var(--primary); }
44
+ .ver { color: var(--text2); font-size: 0.8rem; }
45
+
46
+ /* Sections */
47
+ .section { margin-bottom: 24px; }
48
+ .section-title { font-size: 1rem; font-weight: 600; margin-bottom: 12px; display: flex; align-items: center; justify-content: space-between; }
49
+ .section-title .hint { font-size: 0.8rem; color: var(--text2); font-weight: 400; }
50
+
51
+ /* Cards */
52
+ .card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 18px 20px; box-shadow: var(--shadow); }
53
+
54
+ /* Account */
55
+ .account-card { margin-bottom: 24px; }
56
+ .account-logged-in .account-top { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
57
+ .account-top .dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
75
58
  .dot-ok { background: var(--success); }
76
59
  .dot-warn { background: var(--warning); }
77
60
  .dot-err { background: var(--error); }
78
61
  .dot-gray { background: var(--text2); }
79
- .badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.75rem; font-weight: 600; }
62
+ .account-top .status-text { font-weight: 600; }
63
+ .account-top .key-text { font-family: var(--mono); font-size: 0.85rem; color: var(--text2); }
64
+ .account-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin: 16px 0 12px; text-align: center; }
65
+ .account-stats .stat-val { font-size: 1.2rem; font-weight: 700; color: var(--primary); }
66
+ .account-stats .stat-lbl { font-size: 0.75rem; color: var(--text2); margin-top: 2px; }
67
+ .account-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
68
+ .login-form { display: flex; gap: 8px; align-items: center; margin-top: 12px; }
69
+ .login-form input { flex: 1; font-family: var(--mono); background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 8px 12px; font-size: 0.9rem; outline: none; }
70
+ .login-form input:focus { border-color: var(--primary); box-shadow: 0 0 0 2px var(--primary-dim); }
71
+ .register-link { font-size: 0.8rem; color: var(--text2); margin-top: 8px; }
72
+
73
+ /* Buttons */
74
+ .btn { font-family: var(--font); cursor: pointer; border: none; border-radius: 6px; padding: 7px 14px; font-size: 0.85rem; transition: all .15s; display: inline-flex; align-items: center; gap: 6px; }
75
+ .btn-primary { background: var(--primary); color: #fff; font-weight: 600; }
76
+ .btn-primary:hover { filter: brightness(1.08); }
77
+ .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
78
+ .btn-outline { background: transparent; color: var(--text); border: 1px solid var(--border); }
79
+ .btn-outline:hover { border-color: var(--primary); color: var(--primary); }
80
+ .btn-danger { background: transparent; color: var(--error); border: 1px solid var(--error-dim); }
81
+ .btn-danger:hover { background: var(--error-dim); }
82
+ .btn-sm { padding: 5px 10px; font-size: 0.8rem; }
83
+ .btn-link { background: none; color: var(--primary); padding: 0; font-size: 0.85rem; }
84
+ .btn-link:hover { text-decoration: underline; }
85
+
86
+ /* Tool cards */
87
+ .tool-grid { display: flex; flex-direction: column; gap: 10px; }
88
+ .tool-card { display: flex; align-items: center; gap: 14px; padding: 16px 18px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); transition: border-color .15s; }
89
+ .tool-card:hover { border-color: color-mix(in srgb, var(--primary) 40%, var(--border)); }
90
+ .tool-card .tool-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
91
+ .tool-card .tool-body { flex: 1; min-width: 0; }
92
+ .tool-card .tool-name { font-weight: 600; font-size: 0.95rem; }
93
+ .tool-card .tool-meta { font-size: 0.8rem; color: var(--text2); margin-top: 2px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
94
+ .tool-card .tool-hint { font-size: 0.78rem; color: var(--text2); margin-top: 4px; }
95
+ .tool-card .tool-actions { display: flex; gap: 6px; flex-shrink: 0; }
96
+ .badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.72rem; font-weight: 600; }
80
97
  .badge-ok { background: var(--success-dim); color: var(--success); }
81
98
  .badge-warn { background: var(--warning-dim); color: var(--warning); }
82
- .badge-err { background: var(--error-dim); color: var(--error); }
83
99
  .badge-gray { background: var(--surface2); color: var(--text2); }
84
100
 
85
- /* ── Check list ────────────────────────────────────────────────────────── */
86
- .check-row { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--border); }
87
- .check-row:last-child { border-bottom: none; }
88
- .check-label { flex: 1; }
89
- .check-detail { color: var(--text2); font-size: 0.85rem; font-family: var(--mono); }
90
-
91
- /* ── Tool cards ────────────────────────────────────────────────────────── */
92
- .tool-card { display: flex; align-items: center; gap: 12px; padding: 14px; background: var(--surface);
93
- border: 1px solid var(--border); border-radius: var(--radius); }
94
- .tool-icon { font-size: 1.5rem; width: 40px; text-align: center; }
95
- .tool-info { flex: 1; }
96
- .tool-info .name { font-weight: 600; font-size: 0.95rem; }
97
- .tool-info .meta { font-size: 0.8rem; color: var(--text2); margin-top: 2px; }
98
- .tool-actions { display: flex; gap: 6px; }
99
-
100
- /* ── Console ───────────────────────────────────────────────────────────── */
101
- .console { background: #111; color: #ccc; font-family: var(--mono); font-size: 0.8rem; padding: 14px;
102
- border-radius: var(--radius); max-height: 300px; overflow-y: auto; white-space: pre-wrap; word-break: break-all;
103
- line-height: 1.5; margin-top: 12px; }
101
+ /* Console */
102
+ .console-area { margin-top: 12px; display: none; }
103
+ .console-area.open { display: block; }
104
+ .console-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
105
+ .console-header span { font-size: 0.85rem; font-weight: 600; }
106
+ .console { background: #111; color: #ccc; font-family: var(--mono); font-size: 0.78rem; padding: 12px; border-radius: 8px; max-height: 260px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; line-height: 1.6; }
104
107
  .console .ok { color: #4ade80; }
105
108
  .console .err { color: #f87171; }
106
109
  .console .warn { color: #fbbf24; }
107
110
  .console .info { color: #60a5fa; }
108
111
 
109
- /* ── Wizard steps ──────────────────────────────────────────────────────── */
110
- .steps { display: flex; gap: 0; margin-bottom: 20px; }
111
- .step { flex: 1; text-align: center; padding: 10px 6px; font-size: 0.8rem; color: var(--text2);
112
- border-bottom: 3px solid var(--border); transition: all .2s; }
113
- .step.active { color: var(--primary); border-color: var(--primary); font-weight: 600; }
114
- .step.done { color: var(--success); border-color: var(--success); }
115
-
116
- /* ── Checkbox group ────────────────────────────────────────────────────── */
117
- .checkbox-group { display: flex; flex-direction: column; gap: 8px; }
118
- .checkbox-item { display: flex; align-items: center; gap: 10px; padding: 10px 14px; background: var(--surface2);
119
- border-radius: 8px; cursor: pointer; transition: background .15s; border: 1px solid transparent; }
120
- .checkbox-item:hover { border-color: var(--primary); }
121
- .checkbox-item input { width: 18px; height: 18px; accent-color: var(--primary); cursor: pointer; }
122
- .checkbox-item .cb-label { font-weight: 500; }
123
- .checkbox-item .cb-desc { font-size: 0.8rem; color: var(--text2); }
124
-
125
- /* ── Page visibility ───────────────────────────────────────────────────── */
126
- .page { display: none; }
127
- .page.active { display: block; }
128
-
129
- /* ── Misc ──────────────────────────────────────────────────────────────── */
130
- .loading { color: var(--text2); padding: 20px; text-align: center; }
131
- .mt { margin-top: 12px; }
132
- .mb { margin-bottom: 12px; }
133
- .flex-between { display: flex; justify-content: space-between; align-items: center; }
134
- .text-sm { font-size: 0.85rem; }
135
- .text-muted { color: var(--text2); }
136
- .mono { font-family: var(--mono); }
137
- hr { border: none; border-top: 1px solid var(--border); margin: 16px 0; }
112
+ /* Environment */
113
+ .env-table { width: 100%; }
114
+ .env-row { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 0.85rem; }
115
+ .env-row:last-child { border-bottom: none; }
116
+ .env-key { font-family: var(--mono); font-size: 0.8rem; flex: 1; }
117
+ .env-val { font-size: 0.8rem; }
118
+ .env-rc { font-size: 0.8rem; color: var(--text2); margin-top: 10px; }
119
+
120
+ /* Footer */
121
+ .footer { border-top: 1px solid var(--border); padding-top: 20px; margin-top: 32px; text-align: center; }
122
+ .footer-brand { font-size: 1.1rem; font-weight: 700; margin-bottom: 8px; }
123
+ .footer-brand span { color: var(--primary); }
124
+ .footer-links { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; font-size: 0.85rem; margin-bottom: 8px; }
125
+ .footer-sub { font-size: 0.78rem; color: var(--text2); }
126
+
127
+ /* Misc */
128
+ .loading { color: var(--text2); font-size: 0.85rem; }
138
129
  .hidden { display: none !important; }
130
+ .mono { font-family: var(--mono); }
139
131
  </style>
140
132
  </head>
141
133
  <body>
142
134
  <div class="app" id="app">
143
- <!-- Header -->
144
135
  <div class="header">
145
- <h1>🐑 <span>HolySheep</span> WebUI</h1>
146
- <span class="ver" id="version"></span>
147
- </div>
148
-
149
- <!-- Tabs -->
150
- <div class="tabs" id="tabs">
151
- <button class="tab active" data-page="dashboard" id="tab-dashboard"></button>
152
- <button class="tab" data-page="setup" id="tab-setup"></button>
153
- <button class="tab" data-page="tools" id="tab-tools"></button>
154
- <button class="tab" data-page="doctor" id="tab-doctor"></button>
155
- </div>
156
-
157
- <!-- ── Dashboard ─────────────────────────────────────────────────────── -->
158
- <div class="page active" id="page-dashboard">
159
- <div class="card-grid">
160
- <div class="card">
161
- <h3 id="lbl-login-status"></h3>
162
- <div id="login-status-content" class="loading"></div>
163
- </div>
164
- <div class="card">
165
- <h3 id="lbl-balance"></h3>
166
- <div id="balance-content" class="loading"></div>
167
- </div>
168
- </div>
169
- <div class="card">
170
- <h3 id="lbl-quick-actions"></h3>
171
- <div class="btn-group">
172
- <button class="btn-primary" onclick="switchTab('setup')" id="btn-go-setup"></button>
173
- <button class="btn-secondary" onclick="switchTab('doctor')" id="btn-go-doctor"></button>
174
- <button class="btn-secondary" onclick="runUpgrade()" id="btn-go-upgrade"></button>
175
- </div>
176
- </div>
177
- </div>
178
-
179
- <!-- ── Setup Wizard ──────────────────────────────────────────────────── -->
180
- <div class="page" id="page-setup">
181
- <div class="steps" id="setup-steps"></div>
182
- <div id="setup-content"></div>
183
- </div>
184
-
185
- <!-- ── Tools ─────────────────────────────────────────────────────────── -->
186
- <div class="page" id="page-tools">
187
- <div class="flex-between mb">
188
- <h3 id="lbl-tools-title"></h3>
189
- <button class="btn-secondary btn-sm" onclick="loadTools()" id="btn-refresh-tools"></button>
136
+ <div class="header-left">
137
+ <h1><span>HolySheep</span></h1>
138
+ <a href="https://holysheep.ai" target="_blank">holysheep.ai</a>
190
139
  </div>
191
- <div id="tools-list" class="loading"></div>
192
- </div>
193
-
194
- <!-- ── Doctor ────────────────────────────────────────────────────────── -->
195
- <div class="page" id="page-doctor">
196
- <div class="flex-between mb">
197
- <h3 id="lbl-doctor-title"></h3>
198
- <button class="btn-secondary btn-sm" onclick="loadDoctor()" id="btn-refresh-doctor"></button>
140
+ <div class="header-right">
141
+ <button class="lang-btn" onclick="toggleLang()" id="lang-btn"></button>
142
+ <span class="ver" id="version"></span>
199
143
  </div>
200
- <div id="doctor-content" class="loading"></div>
201
144
  </div>
202
145
 
203
- <!-- ── Upgrade Modal ─────────────────────────────────────────────────── -->
204
- <div id="upgrade-modal" class="hidden">
205
- <div class="card">
206
- <div class="flex-between mb">
207
- <h3 id="lbl-upgrade-title"></h3>
208
- <button class="btn-secondary btn-sm" onclick="closeUpgrade()">✕</button>
209
- </div>
210
- <div class="console" id="upgrade-console"></div>
146
+ <div id="account-section" class="section"></div>
147
+ <div id="tools-section" class="section"></div>
148
+ <div id="console-section" class="console-area" id="console-area">
149
+ <div class="console-header">
150
+ <span id="console-title"></span>
151
+ <button class="btn btn-sm btn-outline" onclick="closeConsole()" id="console-close-btn"></button>
211
152
  </div>
153
+ <div class="console" id="console-output"></div>
212
154
  </div>
155
+ <div id="env-section" class="section"></div>
156
+ <div id="footer-section"></div>
213
157
  </div>
214
158
 
215
159
  <script>
216
- // ── i18n ──────────────────────────────────────────────────────────────────
160
+ // ── i18n ─────────────────────────────────────────────────────────────────────
217
161
  const I18N = {
218
- 'zh': {
219
- dashboard: '仪表盘', setup: '配置向导', tools: '工具管理', doctor: '环境诊断',
220
- loginStatus: '登录状态', balance: '余额', quickActions: '快捷操作',
221
- loggedIn: '已登录', notLoggedIn: '未登录', keyValid: 'Key 有效', keyInvalid: 'Key 已失效',
222
- checking: '检查中...', login: '登录', logout: '退出登录', recharge: '充值',
223
- balanceLabel: '余额', todayCost: '今日消费', monthCost: '本月消费', totalCalls: '累计调用',
224
- goSetup: '一键配置', goDoctor: '环境检查', goUpgrade: '升级工具',
225
- apiKey: 'API Key', apiKeyPlaceholder: '请输入 API Key (cr_xxx)',
226
- validate: '验证', validating: '验证中...',
227
- next: '下一步', prev: '上一步', startSetup: '开始配置', done: '完成',
228
- selectModels: '选择模型', selectTools: '选择工具', configuring: '配置中', summary: '配置结果',
162
+ zh: {
163
+ loggedIn: '已登录', notLoggedIn: '未登录', login: '登录', logout: '退出登录',
164
+ balance: '余额', today: '今日消费', month: '本月消费', calls: '累计调用',
165
+ recharge: '充值', register: '没有账号?去注册',
166
+ apiKeyPlaceholder: '请输入 API Key (cr_xxx)',
167
+ tools: 'AI 工具', toolsHint: '一键配置使用 HolySheep API',
229
168
  installed: '已安装', notInstalled: '未安装', configured: '已配置', notConfigured: '未配置',
230
- version: '版本', install: '安装', configure: '配置', reset: '重置', upgrade: '升级',
231
- autoInstall: '自动安装未安装的工具',
232
- confirmReset: '确认重置所有 HolySheep 配置?此操作不可撤销。',
233
- resetSuccess: '已重置所有配置', resetFail: '重置失败',
234
- refresh: '刷新', upgradeTitle: '升级工具',
235
- nodeVersion: 'Node.js 版本', envVars: '环境变量', set: '已设置', notSet: '未设置',
236
- connectivity: 'API 连通性', connected: '连通', notConnected: '未连通',
237
- modelsAvailable: '个模型可用', toolsStatus: '工具状态',
238
- savedAt: '保存时间',
239
- step1: 'API Key', step2: '选择模型', step3: '选择工具', step4: '执行配置', step5: '完成',
240
- configOk: '配置成功', configErr: '配置失败', configManual: '需手动配置',
241
- configSkip: '已跳过', configWarning: '有警告',
242
- hotReload: '热切换,无需重启', needRestart: '重启终端生效',
243
- close: '关闭', noApiKey: '未登录',
244
- loginFirst: '请先登录后查看', times: '次',
169
+ configure: '一键配置', reconfigure: '重新配置', reset: '重置', install: '安装',
170
+ installManual: '手动安装',
171
+ env: '环境变量', cleanConflicts: '清理冲突变量', cleaning: '清理中...',
172
+ set: '已设置', notSet: '未设置',
173
+ shellConfig: 'Shell 配置', managedBlock: '有托管块', noManagedBlock: '无托管块',
174
+ docs: '使用文档', pricing: '价格', support: '联系支持',
175
+ slogan: '官方 Claude / GPT / Gemini API 代理 · ¥1 = $1',
176
+ checking: '加载中...', close: '关闭', log: '操作日志',
177
+ confirmReset: '确认重置此工具的 HolySheep 配置?',
178
+ configSuccess: '配置成功', configFailed: '配置失败',
179
+ installSuccess: '安装完成', installFailed: '安装失败',
180
+ needLogin: '请先登录', cleanDone: '已清理',
181
+ hotReload: '已生效,无需重启', needRestart: '重启终端后生效',
182
+ launch: '启动命令',
245
183
  },
246
- 'en': {
247
- dashboard: 'Dashboard', setup: 'Setup', tools: 'Tools', doctor: 'Diagnostics',
248
- loginStatus: 'Login Status', balance: 'Balance', quickActions: 'Quick Actions',
249
- loggedIn: 'Logged In', notLoggedIn: 'Not Logged In', keyValid: 'Key Valid', keyInvalid: 'Key Invalid',
250
- checking: 'Checking...', login: 'Login', logout: 'Logout', recharge: 'Recharge',
251
- balanceLabel: 'Balance', todayCost: 'Today', monthCost: 'Month', totalCalls: 'Total Calls',
252
- goSetup: 'Setup', goDoctor: 'Diagnostics', goUpgrade: 'Upgrade',
253
- apiKey: 'API Key', apiKeyPlaceholder: 'Enter API Key (cr_xxx)',
254
- validate: 'Validate', validating: 'Validating...',
255
- next: 'Next', prev: 'Back', startSetup: 'Start', done: 'Done',
256
- selectModels: 'Select Models', selectTools: 'Select Tools', configuring: 'Configuring', summary: 'Summary',
257
- installed: 'Installed', notInstalled: 'Not Installed', configured: 'Configured', notConfigured: 'Not Configured',
258
- version: 'Version', install: 'Install', configure: 'Configure', reset: 'Reset', upgrade: 'Upgrade',
259
- autoInstall: 'Auto-install missing tools',
260
- confirmReset: 'Reset all HolySheep config? This cannot be undone.',
261
- resetSuccess: 'All config reset', resetFail: 'Reset failed',
262
- refresh: 'Refresh', upgradeTitle: 'Upgrade Tools',
263
- nodeVersion: 'Node.js Version', envVars: 'Environment Variables', set: 'Set', notSet: 'Not Set',
264
- connectivity: 'API Connectivity', connected: 'Connected', notConnected: 'Not Connected',
265
- modelsAvailable: 'models available', toolsStatus: 'Tool Status',
266
- savedAt: 'Saved at',
267
- step1: 'API Key', step2: 'Models', step3: 'Tools', step4: 'Configure', step5: 'Done',
268
- configOk: 'Configured', configErr: 'Failed', configManual: 'Manual Config',
269
- configSkip: 'Skipped', configWarning: 'Warning',
270
- hotReload: 'Hot reload, no restart', needRestart: 'Restart terminal to apply',
271
- close: 'Close', noApiKey: 'Not logged in',
272
- loginFirst: 'Please login first', times: 'calls',
184
+ en: {
185
+ loggedIn: 'Logged in', notLoggedIn: 'Not logged in', login: 'Login', logout: 'Logout',
186
+ balance: 'Balance', today: 'Today', month: 'This Month', calls: 'Total Calls',
187
+ recharge: 'Recharge', register: 'No account? Register',
188
+ apiKeyPlaceholder: 'Enter API Key (cr_xxx)',
189
+ tools: 'AI Tools', toolsHint: 'One-click setup for HolySheep API',
190
+ installed: 'Installed', notInstalled: 'Not installed', configured: 'Configured', notConfigured: 'Not configured',
191
+ configure: 'Configure', reconfigure: 'Reconfigure', reset: 'Reset', install: 'Install',
192
+ installManual: 'Manual install',
193
+ env: 'Environment Variables', cleanConflicts: 'Clean Conflicts', cleaning: 'Cleaning...',
194
+ set: 'Set', notSet: 'Not set',
195
+ shellConfig: 'Shell Config', managedBlock: 'managed block', noManagedBlock: 'no managed block',
196
+ docs: 'Docs', pricing: 'Pricing', support: 'Support',
197
+ slogan: 'Official Claude / GPT / Gemini API Proxy',
198
+ checking: 'Loading...', close: 'Close', log: 'Activity Log',
199
+ confirmReset: 'Reset HolySheep config for this tool?',
200
+ configSuccess: 'Configured', configFailed: 'Config failed',
201
+ installSuccess: 'Installed', installFailed: 'Install failed',
202
+ needLogin: 'Please login first', cleanDone: 'Cleaned',
203
+ hotReload: 'Active, no restart needed', needRestart: 'Restart terminal to apply',
204
+ launch: 'Launch',
273
205
  },
274
206
  }
275
- const lang = navigator.language.startsWith('zh') ? 'zh' : 'en'
207
+
208
+ let lang = localStorage.getItem('hs-lang') || (navigator.language.startsWith('zh') ? 'zh' : 'en')
276
209
  const t = (k) => I18N[lang]?.[k] || I18N['en'][k] || k
277
210
 
278
- // ── State ─────────────────────────────────────────────────────────────────
279
- let currentTab = 'dashboard'
280
- let setupStep = 0
281
- let setupState = { apiKey: '', models: [], toolIds: [], autoInstall: true }
282
- let allModels = []
283
- let allTools = []
284
-
285
- // ── Init ──────────────────────────────────────────────────────────────────
286
- document.addEventListener('DOMContentLoaded', () => {
287
- // Set i18n labels
288
- document.getElementById('tab-dashboard').textContent = t('dashboard')
289
- document.getElementById('tab-setup').textContent = t('setup')
290
- document.getElementById('tab-tools').textContent = t('tools')
291
- document.getElementById('tab-doctor').textContent = t('doctor')
292
- document.getElementById('lbl-login-status').textContent = t('loginStatus')
293
- document.getElementById('lbl-balance').textContent = t('balance')
294
- document.getElementById('lbl-quick-actions').textContent = t('quickActions')
295
- document.getElementById('btn-go-setup').textContent = t('goSetup')
296
- document.getElementById('btn-go-doctor').textContent = t('goDoctor')
297
- document.getElementById('btn-go-upgrade').textContent = t('goUpgrade')
298
- document.getElementById('lbl-tools-title').textContent = t('tools')
299
- document.getElementById('btn-refresh-tools').textContent = t('refresh')
300
- document.getElementById('lbl-doctor-title').textContent = t('doctor')
301
- document.getElementById('btn-refresh-doctor').textContent = t('refresh')
302
- document.getElementById('lbl-upgrade-title').textContent = t('upgradeTitle')
303
-
304
- // Tab clicks
305
- document.querySelectorAll('.tab').forEach(tab => {
306
- tab.addEventListener('click', () => switchTab(tab.dataset.page))
307
- })
211
+ function toggleLang() {
212
+ lang = lang === 'zh' ? 'en' : 'zh'
213
+ localStorage.setItem('hs-lang', lang)
214
+ init()
215
+ }
308
216
 
309
- // Load initial data
310
- loadDashboard()
311
- })
312
-
313
- function switchTab(page) {
314
- currentTab = page
315
- document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.page === page))
316
- document.querySelectorAll('.page').forEach(p => p.classList.toggle('active', p.id === 'page-' + page))
317
- if (page === 'dashboard') loadDashboard()
318
- if (page === 'tools') loadTools()
319
- if (page === 'doctor') loadDoctor()
320
- if (page === 'setup') initSetup()
217
+ // ── State ────────────────────────────────────────────────────────────────────
218
+ let busy = false
219
+
220
+ // ── Init ─────────────────────────────────────────────────────────────────────
221
+ document.addEventListener('DOMContentLoaded', init)
222
+
223
+ async function init() {
224
+ document.getElementById('lang-btn').textContent = lang === 'zh' ? 'EN' : '中文'
225
+ document.getElementById('console-close-btn').textContent = t('close')
226
+ loadAccount()
227
+ loadTools()
228
+ loadEnv()
229
+ renderFooter()
321
230
  }
322
231
 
323
- // ── API helpers ───────────────────────────────────────────────────────────
232
+ // ── API helper ───────────────────────────────────────────────────────────────
324
233
  async function api(path, opts) {
325
234
  const res = await fetch('/api/' + path, opts)
326
235
  return res.json()
327
236
  }
328
237
 
329
- // ── Dashboard ─────────────────────────────────────────────────────────────
330
- async function loadDashboard() {
331
- loadStatus()
332
- loadBalance()
333
- }
238
+ // ── Account section ──────────────────────────────────────────────────────────
239
+ async function loadAccount() {
240
+ const el = document.getElementById('account-section')
241
+ el.innerHTML = `<div class="card account-card"><span class="loading">${t('checking')}</span></div>`
334
242
 
335
- async function loadStatus() {
336
- const el = document.getElementById('login-status-content')
337
- el.innerHTML = `<span class="text-muted">${t('checking')}</span>`
338
- try {
339
- const data = await api('status')
340
- document.getElementById('version').textContent = 'v' + data.version
341
- if (data.loggedIn) {
342
- el.innerHTML = `
343
- <div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
344
- <span class="dot dot-ok"></span>
345
- <span style="font-weight:600">${t('loggedIn')}</span>
346
- </div>
347
- <div class="text-sm mono" style="margin-bottom:4px">Key: ${esc(data.apiKey)}</div>
348
- ${data.savedAt ? `<div class="text-sm text-muted">${t('savedAt')}: ${new Date(data.savedAt).toLocaleString()}</div>` : ''}
349
- <div class="mt">
350
- <button class="btn-danger btn-sm" onclick="doLogout()">${t('logout')}</button>
351
- </div>`
352
- } else {
353
- el.innerHTML = `
354
- <div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
355
- <span class="dot dot-gray"></span>
356
- <span>${t('notLoggedIn')}</span>
357
- </div>
358
- <div style="display:flex;gap:8px;align-items:center;margin-top:8px">
359
- <input type="password" id="dash-key" placeholder="${t('apiKeyPlaceholder')}" style="flex:1">
360
- <button class="btn-primary btn-sm" onclick="doLogin()">${t('login')}</button>
361
- </div>
362
- <div class="text-sm text-muted mt">
363
- <a href="https://holysheep.ai/register" target="_blank">holysheep.ai/register</a>
364
- </div>`
365
- }
366
- } catch (e) {
367
- el.innerHTML = `<span class="text-muted">Error: ${esc(e.message)}</span>`
368
- }
369
- }
243
+ const [status, balance] = await Promise.allSettled([api('status'), api('balance')])
244
+ const s = status.value || {}
245
+ const b = balance.value || {}
370
246
 
371
- async function loadBalance() {
372
- const el = document.getElementById('balance-content')
373
- el.innerHTML = `<span class="text-muted">${t('checking')}</span>`
374
- try {
375
- const data = await api('balance')
376
- if (data.error) {
377
- el.innerHTML = `<span class="text-muted">${data.error === '未登录' ? t('loginFirst') : esc(data.error)}</span>`
378
- return
379
- }
380
- el.innerHTML = `
381
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
382
- <div class="stat"><div class="value">$${data.balance.toFixed(2)}</div><div class="label">${t('balanceLabel')}</div></div>
383
- <div class="stat"><div class="value">$${data.todayCost.toFixed(4)}</div><div class="label">${t('todayCost')}</div></div>
384
- <div class="stat"><div class="value">$${data.monthCost.toFixed(4)}</div><div class="label">${t('monthCost')}</div></div>
385
- <div class="stat"><div class="value">${data.totalCalls.toLocaleString()}</div><div class="label">${t('totalCalls')}</div></div>
247
+ document.getElementById('version').textContent = 'v' + (s.version || '')
248
+
249
+ if (s.loggedIn) {
250
+ const hasBalance = !b.error && typeof b.balance === 'number'
251
+ el.innerHTML = `<div class="card account-card account-logged-in">
252
+ <div class="account-top">
253
+ <span class="dot dot-ok"></span>
254
+ <span class="status-text">${t('loggedIn')}</span>
255
+ <span class="key-text">${esc(s.apiKey)}</span>
386
256
  </div>
387
- <div class="mt text-sm"><a href="https://holysheep.ai/app/recharge" target="_blank">${t('recharge')} →</a></div>`
388
- } catch (e) {
389
- el.innerHTML = `<span class="text-muted">${t('loginFirst')}</span>`
257
+ ${hasBalance ? `
258
+ <div class="account-stats">
259
+ <div><div class="stat-val">$${fmtNum(b.balance)}</div><div class="stat-lbl">${t('balance')}</div></div>
260
+ <div><div class="stat-val">$${fmtNum(b.todayCost)}</div><div class="stat-lbl">${t('today')}</div></div>
261
+ <div><div class="stat-val">$${fmtNum(b.monthCost)}</div><div class="stat-lbl">${t('month')}</div></div>
262
+ <div><div class="stat-val">${b.totalCalls.toLocaleString()}</div><div class="stat-lbl">${t('calls')}</div></div>
263
+ </div>` : ''}
264
+ <div class="account-actions">
265
+ <a class="btn btn-primary btn-sm" href="https://holysheep.ai/app/recharge" target="_blank">${t('recharge')} &rarr;</a>
266
+ <button class="btn btn-danger btn-sm" onclick="doLogout()">${t('logout')}</button>
267
+ </div>
268
+ </div>`
269
+ } else {
270
+ el.innerHTML = `<div class="card account-card">
271
+ <div style="display:flex;align-items:center;gap:8px">
272
+ <span class="dot dot-gray"></span>
273
+ <span>${t('notLoggedIn')}</span>
274
+ </div>
275
+ <div class="login-form">
276
+ <input type="password" id="login-key" placeholder="${t('apiKeyPlaceholder')}">
277
+ <button class="btn btn-primary" onclick="doLogin()" id="login-btn">${t('login')}</button>
278
+ </div>
279
+ <div class="register-link"><a href="https://holysheep.ai/register" target="_blank">${t('register')}</a></div>
280
+ </div>`
390
281
  }
391
282
  }
392
283
 
393
284
  async function doLogin() {
394
- const input = document.getElementById('dash-key')
285
+ const input = document.getElementById('login-key')
395
286
  const key = input.value.trim()
396
287
  if (!key) return
397
- input.disabled = true
288
+ const btn = document.getElementById('login-btn')
289
+ btn.disabled = true
398
290
  try {
399
- const data = await api('login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: key }) })
400
- if (data.success) { loadDashboard() } else { alert(data.message) }
291
+ const r = await api('login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: key }) })
292
+ if (r.success) { init() } else { alert(r.message) }
401
293
  } catch (e) { alert(e.message) }
402
- input.disabled = false
294
+ btn.disabled = false
403
295
  }
404
296
 
405
297
  async function doLogout() {
406
298
  await api('logout', { method: 'POST' })
407
- loadDashboard()
299
+ init()
408
300
  }
409
301
 
410
- // ── Setup Wizard ──────────────────────────────────────────────────────────
411
- async function initSetup() {
412
- setupStep = 0
413
- setupState = { apiKey: '', models: [], toolIds: [], autoInstall: true }
414
- // Load models & tools
415
- try {
416
- allModels = await api('models')
417
- allTools = await api('tools')
418
- const status = await api('status')
419
- if (status.loggedIn) {
420
- const config = await api('whoami')
421
- // We have the masked key, but we need the real one for setup
422
- // Check if we already have a key saved
423
- setupState.apiKey = '__saved__'
424
- }
425
- } catch {}
426
- renderSetup()
427
- }
302
+ // ── Tools section ────────────────────────────────────────────────────────────
303
+ async function loadTools() {
304
+ const el = document.getElementById('tools-section')
305
+ el.innerHTML = `<div class="section-title"><span>${t('tools')}</span><span class="hint">${t('toolsHint')}</span></div>
306
+ <div class="tool-grid"><span class="loading">${t('checking')}</span></div>`
428
307
 
429
- function renderSetup() {
430
- const stepsEl = document.getElementById('setup-steps')
431
- const contentEl = document.getElementById('setup-content')
432
- const stepLabels = [t('step1'), t('step2'), t('step3'), t('step4'), t('step5')]
433
- stepsEl.innerHTML = stepLabels.map((s, i) =>
434
- `<div class="step ${i === setupStep ? 'active' : i < setupStep ? 'done' : ''}">${i + 1}. ${s}</div>`
435
- ).join('')
436
-
437
- if (setupStep === 0) renderSetupStep1(contentEl)
438
- else if (setupStep === 1) renderSetupStep2(contentEl)
439
- else if (setupStep === 2) renderSetupStep3(contentEl)
440
- else if (setupStep === 3) renderSetupStep4(contentEl)
441
- else if (setupStep === 4) renderSetupStep5(contentEl)
308
+ const tools = await api('tools')
309
+ const grid = el.querySelector('.tool-grid')
310
+ grid.innerHTML = tools.map(tool => renderToolCard(tool)).join('')
442
311
  }
443
312
 
444
- function renderSetupStep1(el) {
445
- const saved = setupState.apiKey === '__saved__'
446
- el.innerHTML = `
447
- <div class="card">
448
- <h3>${t('apiKey')}</h3>
449
- ${saved ? `
450
- <div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
451
- <span class="dot dot-ok"></span>
452
- <span>${t('loggedIn')}</span>
453
- </div>
454
- <p class="text-sm text-muted mb">${lang === 'zh' ? '将使用已保存的 API Key 继续配置' : 'Will use saved API Key'}</p>
455
- <p class="text-sm text-muted mb">${lang === 'zh' ? '或输入新的 Key:' : 'Or enter a new Key:'}</p>
456
- ` : ''}
457
- <div style="display:flex;gap:8px">
458
- <input type="password" id="setup-key" placeholder="${t('apiKeyPlaceholder')}" style="flex:1">
459
- <button class="btn-secondary btn-sm" onclick="validateSetupKey()" id="setup-validate-btn">${t('validate')}</button>
460
- </div>
461
- <div id="setup-key-status" class="text-sm mt"></div>
462
- <div class="mt" style="text-align:right">
463
- <button class="btn-primary" onclick="setupNext()">${t('next')}</button>
464
- </div>
465
- </div>`
466
- }
313
+ function renderToolCard(tool) {
314
+ let dotClass, statusBadges, actions, hintLine
467
315
 
468
- async function validateSetupKey() {
469
- const input = document.getElementById('setup-key')
470
- const status = document.getElementById('setup-key-status')
471
- const key = input.value.trim()
472
- if (!key) return
473
- if (!key.startsWith('cr_')) { status.innerHTML = `<span style="color:var(--error)">Key must start with cr_</span>`; return }
474
- status.innerHTML = `<span class="text-muted">${t('validating')}</span>`
475
- const btn = document.getElementById('setup-validate-btn')
476
- btn.disabled = true
477
- try {
478
- const res = await api('login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: key }) })
479
- if (res.success) {
480
- setupState.apiKey = key
481
- status.innerHTML = `<span style="color:var(--success)">✓ ${t('keyValid')}</span>`
482
- loadDashboard()
316
+ if (!tool.installed) {
317
+ dotClass = 'dot-gray'
318
+ statusBadges = `<span class="badge badge-gray">${t('notInstalled')}</span>`
319
+ if (tool.canAutoInstall) {
320
+ actions = `<button class="btn btn-primary btn-sm" onclick="doInstallTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('install')}</button>`
483
321
  } else {
484
- status.innerHTML = `<span style="color:var(--error)">✗ ${res.message}</span>`
322
+ actions = `<span class="badge badge-gray" style="font-weight:400">${t('installManual')}: ${esc(tool.installCmd)}</span>`
485
323
  }
486
- } catch (e) {
487
- status.innerHTML = `<span style="color:var(--error)">✗ ${e.message}</span>`
324
+ hintLine = ''
325
+ } else if (!tool.configured) {
326
+ dotClass = 'dot-warn'
327
+ statusBadges = `<span class="badge badge-ok">${t('installed')}</span> <span class="badge badge-warn">${t('notConfigured')}</span>`
328
+ actions = `<button class="btn btn-primary btn-sm" onclick="doConfigureTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('configure')}</button>`
329
+ hintLine = tool.version ? `<span class="mono" style="font-size:0.78rem;color:var(--text2)">${esc(tool.version)}</span>` : ''
330
+ } else {
331
+ dotClass = 'dot-ok'
332
+ statusBadges = `<span class="badge badge-ok">${t('installed')}</span> <span class="badge badge-ok">${t('configured')}</span>`
333
+ actions = `<button class="btn btn-outline btn-sm" onclick="doConfigureTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('reconfigure')}</button>
334
+ <button class="btn btn-danger btn-sm" onclick="doResetTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('reset')}</button>`
335
+ hintLine = tool.version ? `<span class="mono" style="font-size:0.78rem;color:var(--text2)">${esc(tool.version)}</span>` : ''
488
336
  }
489
- btn.disabled = false
490
- }
491
337
 
492
- function setupNext() {
493
- if (setupStep === 0) {
494
- const input = document.getElementById('setup-key')
495
- if (input && input.value.trim()) setupState.apiKey = input.value.trim()
496
- if (!setupState.apiKey) { alert(lang === 'zh' ? '请输入或验证 API Key' : 'Please enter API Key'); return }
497
- }
498
- if (setupStep === 1) {
499
- setupState.models = [...document.querySelectorAll('#model-checks input:checked')].map(c => c.value)
500
- if (setupState.models.length === 0) { alert(lang === 'zh' ? '请至少选择一个模型' : 'Select at least one model'); return }
501
- }
502
- if (setupStep === 2) {
503
- setupState.toolIds = [...document.querySelectorAll('#tool-checks input:checked')].map(c => c.value)
504
- setupState.autoInstall = document.getElementById('auto-install-toggle')?.checked || false
505
- if (setupState.toolIds.length === 0) { alert(lang === 'zh' ? '请至少选择一个工具' : 'Select at least one tool'); return }
506
- }
507
- setupStep++
508
- renderSetup()
509
- if (setupStep === 3) executeSetup()
510
- }
511
-
512
- function setupPrev() {
513
- if (setupStep > 0) { setupStep--; renderSetup() }
338
+ const launchLine = tool.installed && tool.launchCmd
339
+ ? `<div class="tool-hint">${t('launch')}: <code class="mono">${esc(tool.launchCmd)}</code></div>`
340
+ : ''
341
+ const hintText = tool.installed && tool.hint
342
+ ? `<div class="tool-hint">${esc(tool.hint)}</div>`
343
+ : ''
344
+
345
+ return `<div class="tool-card" id="tool-${tool.id}">
346
+ <div class="tool-dot ${dotClass}"></div>
347
+ <div class="tool-body">
348
+ <div class="tool-name">${esc(tool.name)}</div>
349
+ <div class="tool-meta">${statusBadges} ${hintLine}</div>
350
+ ${hintText}${launchLine}
351
+ </div>
352
+ <div class="tool-actions">${actions}</div>
353
+ </div>`
514
354
  }
515
355
 
516
- function renderSetupStep2(el) {
517
- el.innerHTML = `
518
- <div class="card">
519
- <h3>${t('selectModels')}</h3>
520
- <div class="checkbox-group" id="model-checks">
521
- ${allModels.map(m => `
522
- <label class="checkbox-item">
523
- <input type="checkbox" value="${m.id}" checked>
524
- <div>
525
- <div class="cb-label">${esc(m.label)}</div>
526
- <div class="cb-desc">${esc(m.id)} — ${esc(m.desc)}</div>
527
- </div>
528
- </label>
529
- `).join('')}
530
- </div>
531
- <div class="mt" style="display:flex;justify-content:space-between">
532
- <button class="btn-secondary" onclick="setupPrev()">${t('prev')}</button>
533
- <button class="btn-primary" onclick="setupNext()">${t('next')}</button>
534
- </div>
535
- </div>`
536
- }
356
+ // ── Tool actions ─────────────────────────────────────────────────────────────
357
+ async function doConfigureTool(id, name) {
358
+ const status = await api('status')
359
+ if (!status.loggedIn) { alert(t('needLogin')); return }
360
+ if (busy) return
361
+ busy = true
362
+ openConsole(`${t('configure')}: ${name}`)
363
+
364
+ await streamSSE('/api/tool/configure', { toolId: id }, (ev) => {
365
+ if (ev.type === 'progress') appendLog(ev.message, 'info')
366
+ else if (ev.type === 'result') {
367
+ const cls = ev.status === 'ok' ? 'ok' : ev.status === 'warning' ? 'warn' : 'err'
368
+ appendLog(`${ev.status === 'ok' ? '✓' : '⚠'} ${ev.message}`, cls)
369
+ if (ev.file) appendLog(` → ${ev.file}`, 'info')
370
+ if (ev.hot) appendLog(` ${t('hotReload')}`, 'ok')
371
+ else if (ev.status === 'ok') appendLog(` ${t('needRestart')}`, 'warn')
372
+ if (ev.steps) ev.steps.forEach(s => appendLog(` · ${s}`, 'info'))
373
+ }
374
+ else if (ev.type === 'error') appendLog(`✗ ${ev.message}`, 'err')
375
+ else if (ev.type === 'done') {
376
+ appendLog(ev.success ? `\n✓ ${t('configSuccess')}` : `\n✗ ${t('configFailed')}`, ev.success ? 'ok' : 'err')
377
+ }
378
+ })
537
379
 
538
- function renderSetupStep3(el) {
539
- const installed = allTools.filter(t => t.installed)
540
- const notInstalled = allTools.filter(t => !t.installed)
541
- el.innerHTML = `
542
- <div class="card">
543
- <h3>${t('selectTools')}</h3>
544
- <div class="checkbox-group" id="tool-checks">
545
- ${installed.length ? `<div class="text-sm text-muted mb" style="padding:4px 0">${t('installed')}</div>` : ''}
546
- ${installed.map(tool => `
547
- <label class="checkbox-item">
548
- <input type="checkbox" value="${tool.id}" checked>
549
- <div style="flex:1">
550
- <div class="cb-label">${esc(tool.name)} <span class="badge badge-ok">${t('installed')}</span>
551
- ${tool.configured ? `<span class="badge badge-ok">${t('configured')}</span>` : ''}
552
- </div>
553
- <div class="cb-desc">${tool.version || ''}</div>
554
- </div>
555
- </label>
556
- `).join('')}
557
- ${notInstalled.length ? `<div class="text-sm text-muted mb" style="padding:8px 0 4px">${t('notInstalled')}</div>` : ''}
558
- ${notInstalled.map(tool => `
559
- <label class="checkbox-item">
560
- <input type="checkbox" value="${tool.id}">
561
- <div style="flex:1">
562
- <div class="cb-label">${esc(tool.name)} <span class="badge badge-gray">${t('notInstalled')}</span>
563
- ${tool.canAutoInstall ? `<span class="badge badge-warn">${lang === 'zh' ? '可自动安装' : 'Auto-installable'}</span>` : ''}
564
- </div>
565
- <div class="cb-desc">${esc(tool.installCmd)}</div>
566
- </div>
567
- </label>
568
- `).join('')}
569
- </div>
570
- <label class="checkbox-item mt" style="background:var(--primary-dim)">
571
- <input type="checkbox" id="auto-install-toggle" checked>
572
- <div class="cb-label">${t('autoInstall')}</div>
573
- </label>
574
- <div class="mt" style="display:flex;justify-content:space-between">
575
- <button class="btn-secondary" onclick="setupPrev()">${t('prev')}</button>
576
- <button class="btn-primary" onclick="setupNext()">${t('startSetup')}</button>
577
- </div>
578
- </div>`
380
+ busy = false
381
+ loadTools()
579
382
  }
580
383
 
581
- function renderSetupStep4(el) {
582
- el.innerHTML = `
583
- <div class="card">
584
- <h3>${t('configuring')}...</h3>
585
- <div class="console" id="setup-console"></div>
586
- </div>`
384
+ async function doResetTool(id, name) {
385
+ if (!confirm(t('confirmReset'))) return
386
+ try {
387
+ await api('tool/reset', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ toolId: id }) })
388
+ loadTools()
389
+ } catch (e) { alert(e.message) }
587
390
  }
588
391
 
589
- async function executeSetup() {
590
- const consoleEl = document.getElementById('setup-console')
591
- const actualKey = setupState.apiKey === '__saved__' ? null : setupState.apiKey
592
-
593
- // If using saved key, we need to get it from the backend
594
- let keyToUse = actualKey
595
- if (!keyToUse) {
596
- // Fetch real key from server config — we'll send a special marker
597
- // Actually, the server can read the saved key itself
598
- keyToUse = '__use_saved__'
599
- }
392
+ async function doInstallTool(id, name) {
393
+ if (busy) return
394
+ busy = true
395
+ openConsole(`${t('install')}: ${name}`)
600
396
 
601
- const body = JSON.stringify({
602
- apiKey: keyToUse,
603
- models: setupState.models,
604
- toolIds: setupState.toolIds,
605
- autoInstall: setupState.autoInstall,
397
+ await streamSSE('/api/tool/install', { toolId: id }, (ev) => {
398
+ if (ev.type === 'progress') appendLog(ev.message, 'info')
399
+ else if (ev.type === 'output') appendLogRaw(ev.text)
400
+ else if (ev.type === 'done') {
401
+ appendLog(ev.success ? `\n✓ ${t('installSuccess')}` : `\n✗ ${t('installFailed')}`, ev.success ? 'ok' : 'err')
402
+ }
606
403
  })
607
404
 
608
- try {
609
- const res = await fetch('/api/setup', {
610
- method: 'POST',
611
- headers: { 'Content-Type': 'application/json' },
612
- body,
613
- })
614
-
615
- const reader = res.body.getReader()
616
- const decoder = new TextDecoder()
617
- let buffer = ''
618
-
619
- while (true) {
620
- const { done, value } = await reader.read()
621
- if (done) break
622
- buffer += decoder.decode(value, { stream: true })
623
- const parts = buffer.split('\n\n')
624
- buffer = parts.pop()
625
- for (const part of parts) {
626
- const line = part.split('\n').find(l => l.startsWith('data: '))
627
- if (!line) continue
628
- const data = JSON.parse(line.slice(6))
629
- appendConsole(consoleEl, data)
630
- if (data.type === 'done') {
631
- setupState.summary = data.summary
632
- setupStep = 4
633
- renderSetup()
634
- return
635
- }
636
- }
637
- }
638
- } catch (e) {
639
- appendConsole(consoleEl, { type: 'error', message: e.message })
640
- }
405
+ busy = false
406
+ loadTools()
641
407
  }
642
408
 
643
- function appendConsole(el, data) {
644
- if (data.type === 'progress') {
645
- const cls = data.status === 'ok' ? 'ok' : data.status === 'error' ? 'err' : data.status === 'warning' ? 'warn' : 'info'
646
- el.innerHTML += `<span class="${cls}">${data.status === 'ok' ? '✓' : data.status === 'error' ? '' : '→'} ${esc(data.message)}</span>\n`
647
- } else if (data.type === 'output') {
648
- el.innerHTML += esc(data.text)
649
- } else if (data.type === 'error') {
650
- el.innerHTML += `<span class="err">✗ ${esc(data.message)}</span>\n`
651
- }
652
- el.scrollTop = el.scrollHeight
409
+ // ── Console ──────────────────────────────────────────────────────────────────
410
+ function openConsole(title) {
411
+ const area = document.getElementById('console-section')
412
+ document.getElementById('console-title').textContent = title || t('log')
413
+ document.getElementById('console-output').innerHTML = ''
414
+ area.classList.add('open')
415
+ area.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
653
416
  }
654
417
 
655
- function renderSetupStep5(el) {
656
- const s = setupState.summary || { ok: 0, errors: 0, total: 0, results: [] }
657
- const results = s.results || []
658
- el.innerHTML = `
659
- <div class="card">
660
- <h3>✅ ${t('done')}</h3>
661
- <div style="display:flex;gap:20px;margin:16px 0">
662
- <div class="stat"><div class="value" style="color:var(--success)">${s.ok}</div><div class="label">${t('configOk')}</div></div>
663
- <div class="stat"><div class="value" style="color:var(--error)">${s.errors}</div><div class="label">${t('configErr')}</div></div>
664
- </div>
665
- <div>
666
- ${results.map(r => {
667
- const icon = r.status === 'ok' ? '✓' : r.status === 'error' ? '✗' : r.status === 'manual' ? '⚠' : r.status === 'skip' ? '○' : '!'
668
- const badge = r.status === 'ok' ? 'badge-ok' : r.status === 'error' ? 'badge-err' : 'badge-warn'
669
- const label = t('config' + r.status.charAt(0).toUpperCase() + r.status.slice(1)) || r.status
670
- return `
671
- <div class="check-row">
672
- <span>${icon}</span>
673
- <span class="check-label">${esc(r.tool)}</span>
674
- <span class="badge ${badge}">${label}</span>
675
- ${r.file ? `<span class="check-detail">${esc(r.file)}</span>` : ''}
676
- ${r.hot ? `<span class="text-sm text-muted">${t('hotReload')}</span>` : ''}
677
- </div>`
678
- }).join('')}
679
- </div>
680
- <div class="mt" style="text-align:right">
681
- <button class="btn-primary" onclick="switchTab('dashboard')">${t('done')}</button>
682
- </div>
683
- </div>`
418
+ function closeConsole() {
419
+ document.getElementById('console-section').classList.remove('open')
684
420
  }
685
421
 
686
- // ── Tools ─────────────────────────────────────────────────────────────────
687
- async function loadTools() {
688
- const el = document.getElementById('tools-list')
689
- el.innerHTML = `<span class="text-muted">${t('checking')}</span>`
690
- try {
691
- const tools = await api('tools')
692
- allTools = tools
693
- el.innerHTML = `<div class="card-grid">${tools.map(tool => `
694
- <div class="tool-card">
695
- <div class="tool-info">
696
- <div class="name">${esc(tool.name)}</div>
697
- <div class="meta">
698
- ${tool.installed
699
- ? `<span class="badge badge-ok">${t('installed')}</span> ${tool.configured ? `<span class="badge badge-ok">${t('configured')}</span>` : `<span class="badge badge-warn">${t('notConfigured')}</span>`}`
700
- : `<span class="badge badge-gray">${t('notInstalled')}</span>`}
701
- ${tool.version ? `<span class="text-muted" style="margin-left:4px">${esc(tool.version)}</span>` : ''}
702
- </div>
703
- ${tool.hint ? `<div class="meta">${esc(tool.hint)}</div>` : ''}
704
- ${tool.launchCmd ? `<div class="meta mono">${esc(tool.launchCmd)}</div>` : ''}
705
- </div>
706
- <div class="tool-actions">
707
- ${!tool.installed && tool.canAutoInstall ? `<button class="btn-secondary btn-sm" onclick="installTool('${tool.id}','${esc(tool.name)}')">${t('install')}</button>` : ''}
708
- </div>
709
- </div>
710
- `).join('')}</div>`
711
- } catch (e) {
712
- el.innerHTML = `<span class="text-muted">Error: ${esc(e.message)}</span>`
713
- }
422
+ function appendLog(msg, cls) {
423
+ const el = document.getElementById('console-output')
424
+ el.innerHTML += `<span class="${cls || ''}">${esc(msg)}</span>\n`
425
+ el.scrollTop = el.scrollHeight
714
426
  }
715
427
 
716
- async function installTool(id, name) {
717
- const el = document.getElementById('tools-list')
718
- const consoleHtml = `<div class="card mt"><h3>${t('install')}: ${esc(name)}</h3><div class="console" id="install-console"></div></div>`
719
- el.insertAdjacentHTML('beforeend', consoleHtml)
720
- const consoleEl = document.getElementById('install-console')
428
+ function appendLogRaw(text) {
429
+ const el = document.getElementById('console-output')
430
+ el.innerHTML += esc(text)
431
+ el.scrollTop = el.scrollHeight
432
+ }
721
433
 
722
- try {
723
- const res = await fetch('/api/tool/install', {
724
- method: 'POST',
725
- headers: { 'Content-Type': 'application/json' },
726
- body: JSON.stringify({ toolId: id }),
727
- })
728
- const reader = res.body.getReader()
729
- const decoder = new TextDecoder()
730
- let buffer = ''
731
- while (true) {
732
- const { done, value } = await reader.read()
733
- if (done) break
734
- buffer += decoder.decode(value, { stream: true })
735
- const parts = buffer.split('\n\n')
736
- buffer = parts.pop()
737
- for (const part of parts) {
738
- const line = part.split('\n').find(l => l.startsWith('data: '))
739
- if (!line) continue
740
- const data = JSON.parse(line.slice(6))
741
- if (data.type === 'output') consoleEl.innerHTML += esc(data.text)
742
- else if (data.type === 'done') {
743
- consoleEl.innerHTML += data.success
744
- ? `\n<span class="ok">✓ ${lang === 'zh' ? '安装完成' : 'Installed'}</span>\n`
745
- : `\n<span class="err">✗ ${lang === 'zh' ? '安装失败' : 'Install failed'}</span>\n`
746
- setTimeout(() => loadTools(), 1000)
747
- }
748
- consoleEl.scrollTop = consoleEl.scrollHeight
749
- }
434
+ // ── SSE helper ───────────────────────────────────────────────────────────────
435
+ async function streamSSE(url, body, onEvent) {
436
+ const res = await fetch(url, {
437
+ method: 'POST',
438
+ headers: { 'Content-Type': 'application/json' },
439
+ body: JSON.stringify(body),
440
+ })
441
+ const reader = res.body.getReader()
442
+ const decoder = new TextDecoder()
443
+ let buffer = ''
444
+ while (true) {
445
+ const { done, value } = await reader.read()
446
+ if (done) break
447
+ buffer += decoder.decode(value, { stream: true })
448
+ const parts = buffer.split('\n\n')
449
+ buffer = parts.pop()
450
+ for (const part of parts) {
451
+ const line = part.split('\n').find(l => l.startsWith('data: '))
452
+ if (!line) continue
453
+ try { onEvent(JSON.parse(line.slice(6))) } catch {}
750
454
  }
751
- } catch (e) {
752
- consoleEl.innerHTML += `<span class="err">Error: ${esc(e.message)}</span>`
753
455
  }
754
456
  }
755
457
 
756
- // ── Doctor ─────────────────────────────────────────────────────────────────
757
- async function loadDoctor() {
758
- const el = document.getElementById('doctor-content')
759
- el.innerHTML = `<span class="text-muted">${t('checking')}</span>`
760
- try {
761
- const d = await api('doctor')
762
- el.innerHTML = `
763
- <div class="card">
764
- <h3>${t('nodeVersion')}</h3>
765
- <div class="check-row">
766
- <span class="dot ${d.node.ok ? 'dot-ok' : 'dot-err'}"></span>
767
- <span class="check-label">Node.js ${esc(d.node.version)}</span>
768
- <span class="badge ${d.node.ok ? 'badge-ok' : 'badge-err'}">${d.node.ok ? '≥ 16 ✓' : '< 16 ✗'}</span>
769
- </div>
770
- </div>
771
-
772
- <div class="card">
773
- <h3>API Key</h3>
774
- <div class="check-row">
775
- <span class="dot ${d.apiKey.set ? 'dot-ok' : 'dot-err'}"></span>
776
- <span class="check-label">${d.apiKey.set ? esc(d.apiKey.masked) : t('notSet')}</span>
777
- </div>
778
- </div>
779
-
780
- <div class="card">
781
- <h3>${t('envVars')}</h3>
782
- ${Object.entries(d.envVars).map(([k, v]) => `
783
- <div class="check-row">
784
- <span class="dot ${v ? 'dot-ok' : 'dot-gray'}"></span>
785
- <span class="check-label mono text-sm">${k}</span>
786
- <span class="check-detail">${v ? t('set') : t('notSet')}</span>
787
- </div>
788
- `).join('')}
789
- </div>
790
-
791
- <div class="card">
792
- <h3>${t('connectivity')}</h3>
793
- <div class="check-row">
794
- <span class="dot ${d.connectivity.ok ? 'dot-ok' : 'dot-err'}"></span>
795
- <span class="check-label">${d.connectivity.ok ? t('connected') : t('notConnected')}</span>
796
- ${d.connectivity.ok ? `<span class="check-detail">${d.connectivity.modelCount} ${t('modelsAvailable')}</span>` : ''}
797
- </div>
798
- </div>
458
+ // ── Environment section ──────────────────────────────────────────────────────
459
+ async function loadEnv() {
460
+ const el = document.getElementById('env-section')
461
+ el.innerHTML = `<div class="section-title">${t('env')}</div><div class="card"><span class="loading">${t('checking')}</span></div>`
462
+
463
+ const data = await api('env')
464
+ const vars = data.vars || {}
465
+ const rcFiles = data.rcFiles || []
466
+
467
+ let rows = ''
468
+ for (const [k, v] of Object.entries(vars)) {
469
+ const isSet = v !== null
470
+ rows += `<div class="env-row">
471
+ <span class="dot ${isSet ? 'dot-ok' : 'dot-gray'}" style="width:8px;height:8px"></span>
472
+ <span class="env-key">${k}</span>
473
+ <span class="env-val" style="color:var(${isSet ? '--success' : '--text2'})">${isSet ? t('set') : t('notSet')}</span>
474
+ </div>`
475
+ }
799
476
 
800
- <div class="card">
801
- <h3>${t('toolsStatus')}</h3>
802
- ${d.tools.map(tool => `
803
- <div class="check-row">
804
- <span class="dot ${tool.installed ? (tool.configured ? 'dot-ok' : 'dot-warn') : 'dot-gray'}"></span>
805
- <span class="check-label">${esc(tool.name)}</span>
806
- ${tool.installed
807
- ? `<span class="badge ${tool.configured ? 'badge-ok' : 'badge-warn'}">${tool.configured ? t('configured') : t('notConfigured')}</span>
808
- ${tool.version ? `<span class="check-detail">${esc(tool.version)}</span>` : ''}`
809
- : `<span class="badge badge-gray">${t('notInstalled')}</span>`}
810
- </div>
811
- `).join('')}
812
- </div>`
813
- } catch (e) {
814
- el.innerHTML = `<span class="text-muted">Error: ${esc(e.message)}</span>`
477
+ let rcInfo = ''
478
+ if (rcFiles.length) {
479
+ rcInfo = `<div class="env-rc">${t('shellConfig')}: ${rcFiles.map(f =>
480
+ `<span class="mono">${esc(f.path)}</span> (${f.hasManagedBlock ? t('managedBlock') : t('noManagedBlock')})`
481
+ ).join(', ')}</div>`
815
482
  }
816
- }
817
483
 
818
- // ── Upgrade ───────────────────────────────────────────────────────────────
819
- async function runUpgrade() {
820
- const modal = document.getElementById('upgrade-modal')
821
- const consoleEl = document.getElementById('upgrade-console')
822
- modal.classList.remove('hidden')
823
- consoleEl.innerHTML = ''
484
+ el.innerHTML = `<div class="section-title">
485
+ <span>${t('env')}</span>
486
+ <button class="btn btn-outline btn-sm" onclick="doCleanEnv(this)">${t('cleanConflicts')}</button>
487
+ </div>
488
+ <div class="card">
489
+ <div class="env-table">${rows}</div>
490
+ ${rcInfo}
491
+ </div>`
492
+ }
824
493
 
494
+ async function doCleanEnv(btn) {
495
+ btn.disabled = true
496
+ btn.textContent = t('cleaning')
825
497
  try {
826
- const res = await fetch('/api/upgrade', { method: 'POST' })
827
- const reader = res.body.getReader()
828
- const decoder = new TextDecoder()
829
- let buffer = ''
830
- while (true) {
831
- const { done, value } = await reader.read()
832
- if (done) break
833
- buffer += decoder.decode(value, { stream: true })
834
- const parts = buffer.split('\n\n')
835
- buffer = parts.pop()
836
- for (const part of parts) {
837
- const line = part.split('\n').find(l => l.startsWith('data: '))
838
- if (!line) continue
839
- const data = JSON.parse(line.slice(6))
840
- if (data.type === 'tool') {
841
- const cls = data.status === 'ok' ? 'ok' : data.status === 'error' ? 'err' : data.status === 'not-installed' ? 'warn' : 'info'
842
- const msg = data.status === 'not-installed' ? `${data.name}: ${t('notInstalled')}`
843
- : data.status === 'upgrading' ? `${data.name}: ${t('upgrade')}... (${data.localVer || '?'})`
844
- : data.status === 'ok' ? `${data.name}: ✓ ${data.localVer || '?'} → ${data.newVer || 'latest'}`
845
- : `${data.name}: ✗ ${t('configErr')}`
846
- consoleEl.innerHTML += `<span class="${cls}">${msg}</span>\n`
847
- } else if (data.type === 'output') {
848
- consoleEl.innerHTML += esc(data.text)
849
- } else if (data.type === 'done') {
850
- consoleEl.innerHTML += `\n<span class="ok">✓ ${t('done')}</span>\n`
851
- }
852
- consoleEl.scrollTop = consoleEl.scrollHeight
853
- }
854
- }
498
+ const r = await api('env/clean', { method: 'POST' })
499
+ btn.textContent = `✓ ${t('cleanDone')}`
500
+ setTimeout(() => loadEnv(), 1500)
855
501
  } catch (e) {
856
- consoleEl.innerHTML += `<span class="err">Error: ${esc(e.message)}</span>`
502
+ btn.textContent = e.message
857
503
  }
858
504
  }
859
505
 
860
- function closeUpgrade() {
861
- document.getElementById('upgrade-modal').classList.add('hidden')
506
+ // ── Footer ───────────────────────────────────────────────────────────────────
507
+ function renderFooter() {
508
+ document.getElementById('footer-section').innerHTML = `<div class="footer">
509
+ <div class="footer-brand">🐑 <span>HolySheep</span></div>
510
+ <div class="footer-links">
511
+ <a href="https://holysheep.ai" target="_blank">${t('docs')}</a>
512
+ <a href="https://holysheep.ai/app/recharge" target="_blank">${t('recharge')}</a>
513
+ <a href="https://holysheep.ai/register" target="_blank">${lang === 'zh' ? '注册' : 'Register'}</a>
514
+ <a href="https://holysheep.ai/pricing" target="_blank">${t('pricing')}</a>
515
+ </div>
516
+ <div class="footer-sub">${t('slogan')}</div>
517
+ </div>`
862
518
  }
863
519
 
864
- // ── Util ──────────────────────────────────────────────────────────────────
520
+ // ── Util ─────────────────────────────────────────────────────────────────────
865
521
  function esc(s) {
866
522
  if (!s) return ''
867
523
  const d = document.createElement('div')
868
524
  d.textContent = String(s)
869
525
  return d.innerHTML
870
526
  }
527
+
528
+ function fmtNum(n) {
529
+ if (n >= 10000) return Math.floor(n).toLocaleString()
530
+ if (n >= 100) return n.toFixed(1)
531
+ if (n >= 1) return n.toFixed(2)
532
+ return n.toFixed(4)
533
+ }
871
534
  </script>
872
535
  </body>
873
536
  </html>