@simonyea/holysheep-cli 1.7.54 → 1.7.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/commands/webui.js +7 -14
- package/src/index.js +2 -2
- package/src/tools/codex.js +6 -8
- package/src/webui/index.html +420 -744
- package/src/webui/server.js +143 -8
package/src/webui/index.html
CHANGED
|
@@ -5,869 +5,545 @@
|
|
|
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: #
|
|
12
|
-
--text: #1a1a2e; --text2: #
|
|
13
|
-
--primary: #e8a46a; --primary-dim: rgba(232,164,106,0.
|
|
14
|
-
--success: #22c55e; --success-dim: rgba(34,197,94,0.
|
|
15
|
-
--warning: #f59e0b; --warning-dim: rgba(245,158,11,0.
|
|
16
|
-
--error: #ef4444; --error-dim: rgba(239,68,68,0.
|
|
17
|
-
--radius:
|
|
18
|
-
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
19
|
-
--mono: "SF Mono", "Cascadia Code",
|
|
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: #
|
|
24
|
-
--text: #
|
|
25
|
-
--shadow: 0 1px
|
|
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
|
-
/*
|
|
74
|
-
.
|
|
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
|
-
.
|
|
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
|
-
/*
|
|
86
|
-
.
|
|
87
|
-
.
|
|
88
|
-
.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
.
|
|
95
|
-
.
|
|
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-area.busy .console-header span::after {
|
|
105
|
+
content: ' '; display: inline-block; width: 6px; height: 6px; border-radius: 50%;
|
|
106
|
+
background: var(--primary); margin-left: 8px; animation: pulse 1s infinite;
|
|
107
|
+
}
|
|
108
|
+
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.2; } }
|
|
109
|
+
.console-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
|
|
110
|
+
.console-header span { font-size: 0.85rem; font-weight: 600; }
|
|
111
|
+
.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
112
|
.console .ok { color: #4ade80; }
|
|
105
113
|
.console .err { color: #f87171; }
|
|
106
114
|
.console .warn { color: #fbbf24; }
|
|
107
115
|
.console .info { color: #60a5fa; }
|
|
108
116
|
|
|
109
|
-
/*
|
|
110
|
-
.
|
|
111
|
-
.
|
|
112
|
-
|
|
113
|
-
.
|
|
114
|
-
.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
.
|
|
119
|
-
|
|
120
|
-
.
|
|
121
|
-
.
|
|
122
|
-
.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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; }
|
|
117
|
+
/* Environment */
|
|
118
|
+
.env-table { width: 100%; }
|
|
119
|
+
.env-row { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 0.85rem; }
|
|
120
|
+
.env-row:last-child { border-bottom: none; }
|
|
121
|
+
.env-key { font-family: var(--mono); font-size: 0.8rem; flex: 1; }
|
|
122
|
+
.env-val { font-size: 0.8rem; }
|
|
123
|
+
.env-rc { font-size: 0.8rem; color: var(--text2); margin-top: 10px; }
|
|
124
|
+
|
|
125
|
+
/* Footer */
|
|
126
|
+
.footer { border-top: 1px solid var(--border); padding-top: 20px; margin-top: 32px; text-align: center; }
|
|
127
|
+
.footer-brand { font-size: 1.1rem; font-weight: 700; margin-bottom: 8px; }
|
|
128
|
+
.footer-brand span { color: var(--primary); }
|
|
129
|
+
.footer-links { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; font-size: 0.85rem; margin-bottom: 8px; }
|
|
130
|
+
.footer-sub { font-size: 0.78rem; color: var(--text2); }
|
|
131
|
+
|
|
132
|
+
/* Misc */
|
|
133
|
+
.loading { color: var(--text2); font-size: 0.85rem; }
|
|
138
134
|
.hidden { display: none !important; }
|
|
135
|
+
.mono { font-family: var(--mono); }
|
|
139
136
|
</style>
|
|
140
137
|
</head>
|
|
141
138
|
<body>
|
|
142
139
|
<div class="app" id="app">
|
|
143
|
-
<!-- Header -->
|
|
144
140
|
<div class="header">
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
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>
|
|
141
|
+
<div class="header-left">
|
|
142
|
+
<h1><span>HolySheep</span></h1>
|
|
143
|
+
<a href="https://holysheep.ai" target="_blank">holysheep.ai</a>
|
|
168
144
|
</div>
|
|
169
|
-
<div class="
|
|
170
|
-
<
|
|
171
|
-
<
|
|
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>
|
|
145
|
+
<div class="header-right">
|
|
146
|
+
<button class="lang-btn" onclick="toggleLang()" id="lang-btn"></button>
|
|
147
|
+
<span class="ver" id="version"></span>
|
|
176
148
|
</div>
|
|
177
149
|
</div>
|
|
178
150
|
|
|
179
|
-
|
|
180
|
-
<div
|
|
181
|
-
|
|
182
|
-
<div
|
|
183
|
-
|
|
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>
|
|
190
|
-
</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>
|
|
199
|
-
</div>
|
|
200
|
-
<div id="doctor-content" class="loading"></div>
|
|
201
|
-
</div>
|
|
202
|
-
|
|
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>
|
|
151
|
+
<div id="account-section" class="section"></div>
|
|
152
|
+
<div id="tools-section" class="section"></div>
|
|
153
|
+
<div id="console-section" class="console-area" id="console-area">
|
|
154
|
+
<div class="console-header">
|
|
155
|
+
<span id="console-title"></span>
|
|
156
|
+
<button class="btn btn-sm btn-outline" onclick="closeConsole()" id="console-close-btn"></button>
|
|
211
157
|
</div>
|
|
158
|
+
<div class="console" id="console-output"></div>
|
|
212
159
|
</div>
|
|
160
|
+
<div id="env-section" class="section"></div>
|
|
161
|
+
<div id="footer-section"></div>
|
|
213
162
|
</div>
|
|
214
163
|
|
|
215
164
|
<script>
|
|
216
|
-
// ── i18n
|
|
165
|
+
// ── i18n ─────────────────────────────────────────────────────────────────────
|
|
217
166
|
const I18N = {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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: '配置结果',
|
|
167
|
+
zh: {
|
|
168
|
+
loggedIn: '已登录', notLoggedIn: '未登录', login: '登录', logout: '退出登录',
|
|
169
|
+
balance: '余额', today: '今日消费', month: '本月消费', calls: '累计调用',
|
|
170
|
+
recharge: '充值', register: '没有账号?去注册',
|
|
171
|
+
apiKeyPlaceholder: '请输入 API Key (cr_xxx)',
|
|
172
|
+
tools: 'AI 工具', toolsHint: '一键配置使用 HolySheep API',
|
|
229
173
|
installed: '已安装', notInstalled: '未安装', configured: '已配置', notConfigured: '未配置',
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
hotReload: '
|
|
243
|
-
|
|
244
|
-
loginFirst: '请先登录后查看', times: '次',
|
|
174
|
+
configure: '一键配置', reconfigure: '重新配置', reset: '重置', install: '安装',
|
|
175
|
+
installManual: '手动安装',
|
|
176
|
+
env: '环境变量', cleanConflicts: '清理冲突变量', cleaning: '清理中...',
|
|
177
|
+
set: '已设置', notSet: '未设置',
|
|
178
|
+
shellConfig: 'Shell 配置', managedBlock: '有托管块', noManagedBlock: '无托管块',
|
|
179
|
+
docs: '使用文档', pricing: '价格', support: '联系支持',
|
|
180
|
+
slogan: '官方 Claude / GPT / Gemini API 代理 · ¥1 = $1',
|
|
181
|
+
checking: '加载中...', close: '关闭', log: '操作日志',
|
|
182
|
+
confirmReset: '确认重置此工具的 HolySheep 配置?',
|
|
183
|
+
configSuccess: '配置成功', configFailed: '配置失败',
|
|
184
|
+
installSuccess: '安装完成', installFailed: '安装失败',
|
|
185
|
+
needLogin: '请先登录', cleanDone: '已清理',
|
|
186
|
+
hotReload: '已生效,无需重启', needRestart: '重启终端后生效',
|
|
187
|
+
launch: '启动命令',
|
|
245
188
|
},
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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',
|
|
189
|
+
en: {
|
|
190
|
+
loggedIn: 'Logged in', notLoggedIn: 'Not logged in', login: 'Login', logout: 'Logout',
|
|
191
|
+
balance: 'Balance', today: 'Today', month: 'This Month', calls: 'Total Calls',
|
|
192
|
+
recharge: 'Recharge', register: 'No account? Register',
|
|
193
|
+
apiKeyPlaceholder: 'Enter API Key (cr_xxx)',
|
|
194
|
+
tools: 'AI Tools', toolsHint: 'One-click setup for HolySheep API',
|
|
195
|
+
installed: 'Installed', notInstalled: 'Not installed', configured: 'Configured', notConfigured: 'Not configured',
|
|
196
|
+
configure: 'Configure', reconfigure: 'Reconfigure', reset: 'Reset', install: 'Install',
|
|
197
|
+
installManual: 'Manual install',
|
|
198
|
+
env: 'Environment Variables', cleanConflicts: 'Clean Conflicts', cleaning: 'Cleaning...',
|
|
199
|
+
set: 'Set', notSet: 'Not set',
|
|
200
|
+
shellConfig: 'Shell Config', managedBlock: 'managed block', noManagedBlock: 'no managed block',
|
|
201
|
+
docs: 'Docs', pricing: 'Pricing', support: 'Support',
|
|
202
|
+
slogan: 'Official Claude / GPT / Gemini API Proxy',
|
|
203
|
+
checking: 'Loading...', close: 'Close', log: 'Activity Log',
|
|
204
|
+
confirmReset: 'Reset HolySheep config for this tool?',
|
|
205
|
+
configSuccess: 'Configured', configFailed: 'Config failed',
|
|
206
|
+
installSuccess: 'Installed', installFailed: 'Install failed',
|
|
207
|
+
needLogin: 'Please login first', cleanDone: 'Cleaned',
|
|
208
|
+
hotReload: 'Active, no restart needed', needRestart: 'Restart terminal to apply',
|
|
209
|
+
launch: 'Launch',
|
|
273
210
|
},
|
|
274
211
|
}
|
|
275
|
-
|
|
212
|
+
|
|
213
|
+
let lang = localStorage.getItem('hs-lang') || (navigator.language.startsWith('zh') ? 'zh' : 'en')
|
|
276
214
|
const t = (k) => I18N[lang]?.[k] || I18N['en'][k] || k
|
|
277
215
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
})
|
|
216
|
+
function toggleLang() {
|
|
217
|
+
lang = lang === 'zh' ? 'en' : 'zh'
|
|
218
|
+
localStorage.setItem('hs-lang', lang)
|
|
219
|
+
init()
|
|
220
|
+
}
|
|
308
221
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
document.
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
222
|
+
// ── State ────────────────────────────────────────────────────────────────────
|
|
223
|
+
let busy = false
|
|
224
|
+
|
|
225
|
+
// ── Init ─────────────────────────────────────────────────────────────────────
|
|
226
|
+
document.addEventListener('DOMContentLoaded', init)
|
|
227
|
+
|
|
228
|
+
async function init() {
|
|
229
|
+
document.getElementById('lang-btn').textContent = lang === 'zh' ? 'EN' : '中文'
|
|
230
|
+
document.getElementById('console-close-btn').textContent = t('close')
|
|
231
|
+
loadAccount()
|
|
232
|
+
loadTools()
|
|
233
|
+
loadEnv()
|
|
234
|
+
renderFooter()
|
|
321
235
|
}
|
|
322
236
|
|
|
323
|
-
// ── API
|
|
237
|
+
// ── API helper ───────────────────────────────────────────────────────────────
|
|
324
238
|
async function api(path, opts) {
|
|
325
239
|
const res = await fetch('/api/' + path, opts)
|
|
326
240
|
return res.json()
|
|
327
241
|
}
|
|
328
242
|
|
|
329
|
-
// ──
|
|
330
|
-
async function
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
243
|
+
// ── Account section ──────────────────────────────────────────────────────────
|
|
244
|
+
async function loadAccount() {
|
|
245
|
+
const el = document.getElementById('account-section')
|
|
246
|
+
el.innerHTML = `<div class="card account-card"><span class="loading">${t('checking')}</span></div>`
|
|
334
247
|
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
|
|
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
|
-
}
|
|
248
|
+
const [status, balance] = await Promise.allSettled([api('status'), api('balance')])
|
|
249
|
+
const s = status.value || {}
|
|
250
|
+
const b = balance.value || {}
|
|
370
251
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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>
|
|
252
|
+
document.getElementById('version').textContent = 'v' + (s.version || '')
|
|
253
|
+
|
|
254
|
+
if (s.loggedIn) {
|
|
255
|
+
const hasBalance = !b.error && typeof b.balance === 'number'
|
|
256
|
+
el.innerHTML = `<div class="card account-card account-logged-in">
|
|
257
|
+
<div class="account-top">
|
|
258
|
+
<span class="dot dot-ok"></span>
|
|
259
|
+
<span class="status-text">${t('loggedIn')}</span>
|
|
260
|
+
<span class="key-text">${esc(s.apiKey)}</span>
|
|
386
261
|
</div>
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
262
|
+
${hasBalance ? `
|
|
263
|
+
<div class="account-stats">
|
|
264
|
+
<div><div class="stat-val">$${fmtNum(b.balance)}</div><div class="stat-lbl">${t('balance')}</div></div>
|
|
265
|
+
<div><div class="stat-val">$${fmtNum(b.todayCost)}</div><div class="stat-lbl">${t('today')}</div></div>
|
|
266
|
+
<div><div class="stat-val">$${fmtNum(b.monthCost)}</div><div class="stat-lbl">${t('month')}</div></div>
|
|
267
|
+
<div><div class="stat-val">${b.totalCalls.toLocaleString()}</div><div class="stat-lbl">${t('calls')}</div></div>
|
|
268
|
+
</div>` : ''}
|
|
269
|
+
<div class="account-actions">
|
|
270
|
+
<a class="btn btn-primary btn-sm" href="https://holysheep.ai/app/recharge" target="_blank">${t('recharge')} →</a>
|
|
271
|
+
<button class="btn btn-danger btn-sm" onclick="doLogout()">${t('logout')}</button>
|
|
272
|
+
</div>
|
|
273
|
+
</div>`
|
|
274
|
+
} else {
|
|
275
|
+
el.innerHTML = `<div class="card account-card">
|
|
276
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
277
|
+
<span class="dot dot-gray"></span>
|
|
278
|
+
<span>${t('notLoggedIn')}</span>
|
|
279
|
+
</div>
|
|
280
|
+
<div class="login-form">
|
|
281
|
+
<input type="password" id="login-key" placeholder="${t('apiKeyPlaceholder')}">
|
|
282
|
+
<button class="btn btn-primary" onclick="doLogin()" id="login-btn">${t('login')}</button>
|
|
283
|
+
</div>
|
|
284
|
+
<div class="register-link"><a href="https://holysheep.ai/register" target="_blank">${t('register')}</a></div>
|
|
285
|
+
</div>`
|
|
390
286
|
}
|
|
391
287
|
}
|
|
392
288
|
|
|
393
289
|
async function doLogin() {
|
|
394
|
-
const input = document.getElementById('
|
|
290
|
+
const input = document.getElementById('login-key')
|
|
395
291
|
const key = input.value.trim()
|
|
396
292
|
if (!key) return
|
|
397
|
-
|
|
293
|
+
const btn = document.getElementById('login-btn')
|
|
294
|
+
btn.disabled = true
|
|
398
295
|
try {
|
|
399
|
-
const
|
|
400
|
-
if (
|
|
296
|
+
const r = await api('login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: key }) })
|
|
297
|
+
if (r.success) { init() } else { alert(r.message) }
|
|
401
298
|
} catch (e) { alert(e.message) }
|
|
402
|
-
|
|
299
|
+
btn.disabled = false
|
|
403
300
|
}
|
|
404
301
|
|
|
405
302
|
async function doLogout() {
|
|
406
303
|
await api('logout', { method: 'POST' })
|
|
407
|
-
|
|
304
|
+
init()
|
|
408
305
|
}
|
|
409
306
|
|
|
410
|
-
// ──
|
|
411
|
-
async function
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
}
|
|
307
|
+
// ── Tools section ────────────────────────────────────────────────────────────
|
|
308
|
+
async function loadTools() {
|
|
309
|
+
const el = document.getElementById('tools-section')
|
|
310
|
+
el.innerHTML = `<div class="section-title"><span>${t('tools')}</span><span class="hint">${t('toolsHint')}</span></div>
|
|
311
|
+
<div class="tool-grid"><span class="loading">${t('checking')}</span></div>`
|
|
428
312
|
|
|
429
|
-
|
|
430
|
-
const
|
|
431
|
-
|
|
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)
|
|
313
|
+
const tools = await api('tools')
|
|
314
|
+
const grid = el.querySelector('.tool-grid')
|
|
315
|
+
grid.innerHTML = tools.map(tool => renderToolCard(tool)).join('')
|
|
442
316
|
}
|
|
443
317
|
|
|
444
|
-
function
|
|
445
|
-
|
|
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
|
-
}
|
|
318
|
+
function renderToolCard(tool) {
|
|
319
|
+
let dotClass, statusBadges, actions, hintLine
|
|
467
320
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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()
|
|
321
|
+
if (!tool.installed) {
|
|
322
|
+
dotClass = 'dot-gray'
|
|
323
|
+
statusBadges = `<span class="badge badge-gray">${t('notInstalled')}</span>`
|
|
324
|
+
if (tool.canAutoInstall) {
|
|
325
|
+
actions = `<button class="btn btn-primary btn-sm" onclick="doInstallTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('install')}</button>`
|
|
483
326
|
} else {
|
|
484
|
-
|
|
327
|
+
actions = `<span class="badge badge-gray" style="font-weight:400">${t('installManual')}: ${esc(tool.installCmd)}</span>`
|
|
485
328
|
}
|
|
486
|
-
|
|
487
|
-
|
|
329
|
+
hintLine = ''
|
|
330
|
+
} else if (!tool.configured) {
|
|
331
|
+
dotClass = 'dot-warn'
|
|
332
|
+
statusBadges = `<span class="badge badge-ok">${t('installed')}</span> <span class="badge badge-warn">${t('notConfigured')}</span>`
|
|
333
|
+
actions = `<button class="btn btn-primary btn-sm" onclick="doConfigureTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('configure')}</button>`
|
|
334
|
+
hintLine = tool.version ? `<span class="mono" style="font-size:0.78rem;color:var(--text2)">${esc(tool.version)}</span>` : ''
|
|
335
|
+
} else {
|
|
336
|
+
dotClass = 'dot-ok'
|
|
337
|
+
statusBadges = `<span class="badge badge-ok">${t('installed')}</span> <span class="badge badge-ok">${t('configured')}</span>`
|
|
338
|
+
actions = `<button class="btn btn-outline btn-sm" onclick="doConfigureTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('reconfigure')}</button>
|
|
339
|
+
<button class="btn btn-danger btn-sm" onclick="doResetTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('reset')}</button>`
|
|
340
|
+
hintLine = tool.version ? `<span class="mono" style="font-size:0.78rem;color:var(--text2)">${esc(tool.version)}</span>` : ''
|
|
488
341
|
}
|
|
489
|
-
btn.disabled = false
|
|
490
|
-
}
|
|
491
342
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
renderSetup()
|
|
509
|
-
if (setupStep === 3) executeSetup()
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
function setupPrev() {
|
|
513
|
-
if (setupStep > 0) { setupStep--; renderSetup() }
|
|
343
|
+
const launchLine = tool.installed && tool.launchCmd
|
|
344
|
+
? `<div class="tool-hint">${t('launch')}: <code class="mono">${esc(tool.launchCmd)}</code></div>`
|
|
345
|
+
: ''
|
|
346
|
+
const hintText = tool.installed && tool.hint
|
|
347
|
+
? `<div class="tool-hint">${esc(tool.hint)}</div>`
|
|
348
|
+
: ''
|
|
349
|
+
|
|
350
|
+
return `<div class="tool-card" id="tool-${tool.id}">
|
|
351
|
+
<div class="tool-dot ${dotClass}"></div>
|
|
352
|
+
<div class="tool-body">
|
|
353
|
+
<div class="tool-name">${esc(tool.name)}</div>
|
|
354
|
+
<div class="tool-meta">${statusBadges} ${hintLine}</div>
|
|
355
|
+
${hintText}${launchLine}
|
|
356
|
+
</div>
|
|
357
|
+
<div class="tool-actions">${actions}</div>
|
|
358
|
+
</div>`
|
|
514
359
|
}
|
|
515
360
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
361
|
+
// ── Tool actions ─────────────────────────────────────────────────────────────
|
|
362
|
+
async function doConfigureTool(id, name) {
|
|
363
|
+
const status = await api('status')
|
|
364
|
+
if (!status.loggedIn) { alert(t('needLogin')); return }
|
|
365
|
+
if (busy) return
|
|
366
|
+
busy = true
|
|
367
|
+
openConsole(`${t('configure')}: ${name}`)
|
|
368
|
+
document.getElementById('console-section').classList.add('busy')
|
|
369
|
+
|
|
370
|
+
await streamSSE('/api/tool/configure', { toolId: id }, (ev) => {
|
|
371
|
+
if (ev.type === 'progress') appendLog(ev.message, 'info')
|
|
372
|
+
else if (ev.type === 'result') {
|
|
373
|
+
const cls = ev.status === 'ok' ? 'ok' : ev.status === 'warning' ? 'warn' : 'err'
|
|
374
|
+
appendLog(`${ev.status === 'ok' ? '✓' : '⚠'} ${ev.message}`, cls)
|
|
375
|
+
if (ev.file) appendLog(` → ${ev.file}`, 'info')
|
|
376
|
+
if (ev.hot) appendLog(` ${t('hotReload')}`, 'ok')
|
|
377
|
+
else if (ev.status === 'ok') appendLog(` ${t('needRestart')}`, 'warn')
|
|
378
|
+
if (ev.steps) ev.steps.forEach(s => appendLog(` · ${s}`, 'info'))
|
|
379
|
+
}
|
|
380
|
+
else if (ev.type === 'error') appendLog(`✗ ${ev.message}`, 'err')
|
|
381
|
+
else if (ev.type === 'done') {
|
|
382
|
+
appendLog(ev.success ? `\n✓ ${t('configSuccess')}` : `\n✗ ${t('configFailed')}`, ev.success ? 'ok' : 'err')
|
|
383
|
+
if (ev.dashboardUrl) {
|
|
384
|
+
appendLog(`\n→ ${ev.dashboardUrl}`, 'ok')
|
|
385
|
+
window.open(ev.dashboardUrl, '_blank')
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
})
|
|
537
389
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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>`
|
|
390
|
+
document.getElementById('console-section').classList.remove('busy')
|
|
391
|
+
busy = false
|
|
392
|
+
loadTools()
|
|
579
393
|
}
|
|
580
394
|
|
|
581
|
-
function
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
395
|
+
async function doResetTool(id, name) {
|
|
396
|
+
if (!confirm(t('confirmReset'))) return
|
|
397
|
+
try {
|
|
398
|
+
await api('tool/reset', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ toolId: id }) })
|
|
399
|
+
loadTools()
|
|
400
|
+
} catch (e) { alert(e.message) }
|
|
587
401
|
}
|
|
588
402
|
|
|
589
|
-
async function
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const body = JSON.stringify({
|
|
602
|
-
apiKey: keyToUse,
|
|
603
|
-
models: setupState.models,
|
|
604
|
-
toolIds: setupState.toolIds,
|
|
605
|
-
autoInstall: setupState.autoInstall,
|
|
403
|
+
async function doInstallTool(id, name) {
|
|
404
|
+
if (busy) return
|
|
405
|
+
busy = true
|
|
406
|
+
openConsole(`${t('install')}: ${name}`)
|
|
407
|
+
document.getElementById('console-section').classList.add('busy')
|
|
408
|
+
|
|
409
|
+
await streamSSE('/api/tool/install', { toolId: id }, (ev) => {
|
|
410
|
+
if (ev.type === 'progress') appendLog(ev.message, 'info')
|
|
411
|
+
else if (ev.type === 'output') appendLogRaw(ev.text)
|
|
412
|
+
else if (ev.type === 'done') {
|
|
413
|
+
appendLog(ev.success ? `\n✓ ${t('installSuccess')}` : `\n✗ ${t('installFailed')}`, ev.success ? 'ok' : 'err')
|
|
414
|
+
}
|
|
606
415
|
})
|
|
607
416
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
-
}
|
|
417
|
+
document.getElementById('console-section').classList.remove('busy')
|
|
418
|
+
busy = false
|
|
419
|
+
loadTools()
|
|
641
420
|
}
|
|
642
421
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
el.innerHTML += `<span class="err">✗ ${esc(data.message)}</span>\n`
|
|
651
|
-
}
|
|
652
|
-
el.scrollTop = el.scrollHeight
|
|
422
|
+
// ── Console ──────────────────────────────────────────────────────────────────
|
|
423
|
+
function openConsole(title) {
|
|
424
|
+
const area = document.getElementById('console-section')
|
|
425
|
+
document.getElementById('console-title').textContent = title || t('log')
|
|
426
|
+
document.getElementById('console-output').innerHTML = ''
|
|
427
|
+
area.classList.add('open')
|
|
428
|
+
area.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
|
653
429
|
}
|
|
654
430
|
|
|
655
|
-
function
|
|
656
|
-
|
|
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>`
|
|
431
|
+
function closeConsole() {
|
|
432
|
+
document.getElementById('console-section').classList.remove('open')
|
|
684
433
|
}
|
|
685
434
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
el.
|
|
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
|
-
}
|
|
435
|
+
function appendLog(msg, cls) {
|
|
436
|
+
const el = document.getElementById('console-output')
|
|
437
|
+
el.innerHTML += `<span class="${cls || ''}">${esc(msg)}</span>\n`
|
|
438
|
+
el.scrollTop = el.scrollHeight
|
|
714
439
|
}
|
|
715
440
|
|
|
716
|
-
|
|
717
|
-
const el = document.getElementById('
|
|
718
|
-
|
|
719
|
-
el.
|
|
720
|
-
|
|
441
|
+
function appendLogRaw(text) {
|
|
442
|
+
const el = document.getElementById('console-output')
|
|
443
|
+
el.innerHTML += esc(text)
|
|
444
|
+
el.scrollTop = el.scrollHeight
|
|
445
|
+
}
|
|
721
446
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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
|
-
}
|
|
447
|
+
// ── SSE helper ───────────────────────────────────────────────────────────────
|
|
448
|
+
async function streamSSE(url, body, onEvent) {
|
|
449
|
+
const res = await fetch(url, {
|
|
450
|
+
method: 'POST',
|
|
451
|
+
headers: { 'Content-Type': 'application/json' },
|
|
452
|
+
body: JSON.stringify(body),
|
|
453
|
+
})
|
|
454
|
+
const reader = res.body.getReader()
|
|
455
|
+
const decoder = new TextDecoder()
|
|
456
|
+
let buffer = ''
|
|
457
|
+
while (true) {
|
|
458
|
+
const { done, value } = await reader.read()
|
|
459
|
+
if (done) break
|
|
460
|
+
buffer += decoder.decode(value, { stream: true })
|
|
461
|
+
const parts = buffer.split('\n\n')
|
|
462
|
+
buffer = parts.pop()
|
|
463
|
+
for (const part of parts) {
|
|
464
|
+
const line = part.split('\n').find(l => l.startsWith('data: '))
|
|
465
|
+
if (!line) continue
|
|
466
|
+
try { onEvent(JSON.parse(line.slice(6))) } catch {}
|
|
750
467
|
}
|
|
751
|
-
} catch (e) {
|
|
752
|
-
consoleEl.innerHTML += `<span class="err">Error: ${esc(e.message)}</span>`
|
|
753
468
|
}
|
|
754
469
|
}
|
|
755
470
|
|
|
756
|
-
// ──
|
|
757
|
-
async function
|
|
758
|
-
const el = document.getElementById('
|
|
759
|
-
el.innerHTML = `<
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
</
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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>
|
|
471
|
+
// ── Environment section ──────────────────────────────────────────────────────
|
|
472
|
+
async function loadEnv() {
|
|
473
|
+
const el = document.getElementById('env-section')
|
|
474
|
+
el.innerHTML = `<div class="section-title">${t('env')}</div><div class="card"><span class="loading">${t('checking')}</span></div>`
|
|
475
|
+
|
|
476
|
+
const data = await api('env')
|
|
477
|
+
const vars = data.vars || {}
|
|
478
|
+
const rcFiles = data.rcFiles || []
|
|
479
|
+
|
|
480
|
+
let rows = ''
|
|
481
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
482
|
+
const isSet = v !== null
|
|
483
|
+
rows += `<div class="env-row">
|
|
484
|
+
<span class="dot ${isSet ? 'dot-ok' : 'dot-gray'}" style="width:8px;height:8px"></span>
|
|
485
|
+
<span class="env-key">${k}</span>
|
|
486
|
+
<span class="env-val" style="color:var(${isSet ? '--success' : '--text2'})">${isSet ? t('set') : t('notSet')}</span>
|
|
487
|
+
</div>`
|
|
488
|
+
}
|
|
799
489
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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>`
|
|
490
|
+
let rcInfo = ''
|
|
491
|
+
if (rcFiles.length) {
|
|
492
|
+
rcInfo = `<div class="env-rc">${t('shellConfig')}: ${rcFiles.map(f =>
|
|
493
|
+
`<span class="mono">${esc(f.path)}</span> (${f.hasManagedBlock ? t('managedBlock') : t('noManagedBlock')})`
|
|
494
|
+
).join(', ')}</div>`
|
|
815
495
|
}
|
|
816
|
-
}
|
|
817
496
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
497
|
+
el.innerHTML = `<div class="section-title">
|
|
498
|
+
<span>${t('env')}</span>
|
|
499
|
+
<button class="btn btn-outline btn-sm" onclick="doCleanEnv(this)">${t('cleanConflicts')}</button>
|
|
500
|
+
</div>
|
|
501
|
+
<div class="card">
|
|
502
|
+
<div class="env-table">${rows}</div>
|
|
503
|
+
${rcInfo}
|
|
504
|
+
</div>`
|
|
505
|
+
}
|
|
824
506
|
|
|
507
|
+
async function doCleanEnv(btn) {
|
|
508
|
+
btn.disabled = true
|
|
509
|
+
btn.textContent = t('cleaning')
|
|
825
510
|
try {
|
|
826
|
-
const
|
|
827
|
-
|
|
828
|
-
|
|
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
|
-
}
|
|
511
|
+
const r = await api('env/clean', { method: 'POST' })
|
|
512
|
+
btn.textContent = `✓ ${t('cleanDone')}`
|
|
513
|
+
setTimeout(() => loadEnv(), 1500)
|
|
855
514
|
} catch (e) {
|
|
856
|
-
|
|
515
|
+
btn.textContent = e.message
|
|
857
516
|
}
|
|
858
517
|
}
|
|
859
518
|
|
|
860
|
-
|
|
861
|
-
|
|
519
|
+
// ── Footer ───────────────────────────────────────────────────────────────────
|
|
520
|
+
function renderFooter() {
|
|
521
|
+
document.getElementById('footer-section').innerHTML = `<div class="footer">
|
|
522
|
+
<div class="footer-brand">🐑 <span>HolySheep</span></div>
|
|
523
|
+
<div class="footer-links">
|
|
524
|
+
<a href="https://holysheep.ai" target="_blank">${t('docs')}</a>
|
|
525
|
+
<a href="https://holysheep.ai/app/recharge" target="_blank">${t('recharge')}</a>
|
|
526
|
+
<a href="https://holysheep.ai/register" target="_blank">${lang === 'zh' ? '注册' : 'Register'}</a>
|
|
527
|
+
<a href="https://holysheep.ai/pricing" target="_blank">${t('pricing')}</a>
|
|
528
|
+
</div>
|
|
529
|
+
<div class="footer-sub">${t('slogan')}</div>
|
|
530
|
+
</div>`
|
|
862
531
|
}
|
|
863
532
|
|
|
864
|
-
// ── Util
|
|
533
|
+
// ── Util ─────────────────────────────────────────────────────────────────────
|
|
865
534
|
function esc(s) {
|
|
866
535
|
if (!s) return ''
|
|
867
536
|
const d = document.createElement('div')
|
|
868
537
|
d.textContent = String(s)
|
|
869
538
|
return d.innerHTML
|
|
870
539
|
}
|
|
540
|
+
|
|
541
|
+
function fmtNum(n) {
|
|
542
|
+
if (n >= 10000) return Math.floor(n).toLocaleString()
|
|
543
|
+
if (n >= 100) return n.toFixed(1)
|
|
544
|
+
if (n >= 1) return n.toFixed(2)
|
|
545
|
+
return n.toFixed(4)
|
|
546
|
+
}
|
|
871
547
|
</script>
|
|
872
548
|
</body>
|
|
873
549
|
</html>
|