openclacky 0.9.2 → 0.9.4
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -0
- data/docs/security-design.md +109 -0
- data/lib/clacky/agent/message_compressor_helper.rb +82 -69
- data/lib/clacky/agent/session_serializer.rb +9 -1
- data/lib/clacky/agent/skill_manager.rb +7 -0
- data/lib/clacky/agent.rb +11 -3
- data/lib/clacky/banner.rb +65 -0
- data/lib/clacky/block_font.rb +331 -0
- data/lib/clacky/brand_config.rb +73 -5
- data/lib/clacky/client.rb +129 -633
- data/lib/clacky/default_skills/activate-license/SKILL.md +118 -0
- data/lib/clacky/default_skills/channel-setup/SKILL.md +10 -20
- data/lib/clacky/message_format/anthropic.rb +241 -0
- data/lib/clacky/message_format/open_ai.rb +135 -0
- data/lib/clacky/server/channel/adapters/wecom/adapter.rb +2 -0
- data/lib/clacky/server/channel/adapters/wecom/ws_client.rb +13 -0
- data/lib/clacky/server/http_server.rb +12 -2
- data/lib/clacky/session_manager.rb +7 -2
- data/lib/clacky/tools/browser.rb +109 -280
- data/lib/clacky/ui2/block_font.rb +10 -0
- data/lib/clacky/ui2/components/welcome_banner.rb +23 -22
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +588 -6
- data/lib/clacky/web/app.js +30 -15
- data/lib/clacky/web/brand.js +141 -9
- data/lib/clacky/web/i18n.js +28 -2
- data/lib/clacky/web/index.html +142 -127
- data/lib/clacky/web/onboard.js +192 -225
- data/lib/clacky/web/sessions.js +12 -8
- data/lib/clacky/web/settings.js +57 -4
- data/lib/clacky.rb +2 -0
- data/scripts/install.sh +60 -15
- metadata +8 -1
data/lib/clacky/web/onboard.js
CHANGED
|
@@ -1,80 +1,74 @@
|
|
|
1
|
-
// onboard.js — First-run
|
|
1
|
+
// onboard.js — First-run setup flow
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// Phase 2 (soul_setup): Open a dedicated session and invoke the /onboard skill,
|
|
5
|
-
// which uses interactive cards to collect preferences and
|
|
6
|
-
// write SOUL.md + USER.md.
|
|
3
|
+
// Two distinct phases, now cleanly separated:
|
|
7
4
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
5
|
+
// key_setup → Show the full-screen setup-panel (language + API key).
|
|
6
|
+
// Hard block: nothing works without an API key.
|
|
7
|
+
// On success, automatically launches the /onboard session.
|
|
8
|
+
//
|
|
9
|
+
// soul_setup → API key is already configured, SOUL.md is missing.
|
|
10
|
+
// Automatically creates an /onboard session and boots the UI —
|
|
11
|
+
// no blocking panel shown, user lands directly in the session.
|
|
12
|
+
//
|
|
13
|
+
// The old onboard-panel (with phase-lang / phase-key / phase-soul) is gone.
|
|
14
|
+
// setup-panel handles the mandatory first-run setup.
|
|
15
|
+
// /onboard skill handles the optional personalisation inside a chat session.
|
|
10
16
|
|
|
11
17
|
const Onboard = (() => {
|
|
12
|
-
let _providers
|
|
13
|
-
let
|
|
14
|
-
let _selectedLang = I18n.lang(); // language chosen in phase-lang
|
|
18
|
+
let _providers = [];
|
|
19
|
+
let _selectedLang = I18n.lang(); // language chosen during setup
|
|
15
20
|
|
|
16
21
|
// ── Public API ──────────────────────────────────────────────────────────────
|
|
17
22
|
|
|
18
|
-
// Check onboard status and show panel if needed.
|
|
19
|
-
// Returns { needsOnboard, phase } so the caller can decide whether to block
|
|
20
|
-
// normal UI boot (only "key_setup" is a hard block; "soul_setup" is soft).
|
|
21
23
|
async function check() {
|
|
22
24
|
try {
|
|
23
25
|
const res = await fetch("/api/onboard/status");
|
|
24
26
|
const data = await res.json();
|
|
25
27
|
if (!data.needs_onboard) return { needsOnboard: false, phase: null };
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
const phase = data.phase;
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (_phase === "soul_setup" && window.location.hash.includes("session/")) {
|
|
34
|
-
return { needsOnboard: false, phase: null };
|
|
31
|
+
if (phase === "key_setup") {
|
|
32
|
+
// Mandatory: show full-screen setup panel, block boot.
|
|
33
|
+
_showSetup();
|
|
34
|
+
return { needsOnboard: true, phase };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
if (phase === "soul_setup") {
|
|
38
|
+
// Skip any blocking panel — just auto-launch the /onboard session.
|
|
39
|
+
// If the user already has an onboard session in progress (hash has a
|
|
40
|
+
// session id), restore it instead of creating a duplicate.
|
|
41
|
+
if (window.location.hash.includes("session/")) {
|
|
42
|
+
return { needsOnboard: false, phase: null };
|
|
43
|
+
}
|
|
44
|
+
await _launchOnboardSession();
|
|
45
|
+
return { needsOnboard: true, phase };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { needsOnboard: false, phase: null };
|
|
49
|
+
} catch (_) {
|
|
41
50
|
return { needsOnboard: false, phase: null };
|
|
42
51
|
}
|
|
43
52
|
}
|
|
44
53
|
|
|
45
|
-
// ──
|
|
54
|
+
// ── Setup panel (key_setup) ─────────────────────────────────────────────────
|
|
46
55
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
Router.navigate("
|
|
50
|
-
|
|
51
|
-
// Render the empty session list placeholder immediately (WS is not connected yet
|
|
52
|
-
// during onboarding, so renderList() would never be called otherwise).
|
|
56
|
+
function _showSetup() {
|
|
57
|
+
document.body.classList.add("setup-mode");
|
|
58
|
+
Router.navigate("setup");
|
|
53
59
|
Sessions.renderList();
|
|
54
60
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
_bindLangPhase(phase);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function _showPhase(which) {
|
|
61
|
-
$("onboard-phase-lang").style.display = which === "lang" ? "" : "none";
|
|
62
|
-
$("onboard-phase-key").style.display = which === "key" ? "" : "none";
|
|
63
|
-
$("onboard-phase-soul").style.display = which === "soul" ? "" : "none";
|
|
64
|
-
$("onboard-steps").style.display = which === "lang" ? "none" : "";
|
|
65
|
-
$("step-dot-1").className = "onboard-step" + (which === "key" ? " active" : " done");
|
|
66
|
-
$("step-dot-2").className = "onboard-step" + (which === "soul" ? " active" : "");
|
|
61
|
+
_selectedLang = I18n.lang();
|
|
62
|
+
_bindLangStep();
|
|
67
63
|
}
|
|
68
64
|
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const btnNext = $("onboard-btn-lang-next");
|
|
65
|
+
// Step 1 — language selection
|
|
66
|
+
function _bindLangStep() {
|
|
67
|
+
const btnEn = $("setup-btn-lang-en");
|
|
68
|
+
const btnZh = $("setup-btn-lang-zh");
|
|
69
|
+
const btnNext = $("setup-btn-lang-next");
|
|
75
70
|
|
|
76
|
-
|
|
77
|
-
_updateLangBtns(I18n.lang());
|
|
71
|
+
_updateLangBtns(_selectedLang);
|
|
78
72
|
|
|
79
73
|
btnEn.addEventListener("click", () => {
|
|
80
74
|
_selectedLang = "en";
|
|
@@ -89,174 +83,183 @@ const Onboard = (() => {
|
|
|
89
83
|
});
|
|
90
84
|
|
|
91
85
|
btnNext.addEventListener("click", async () => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
_bindKeyPhase();
|
|
96
|
-
} else {
|
|
97
|
-
// Key is already configured — skip the soul confirmation page and
|
|
98
|
-
// jump straight into the onboard session.
|
|
99
|
-
await _startSoulSession();
|
|
100
|
-
}
|
|
86
|
+
_showSetupStep("key");
|
|
87
|
+
await _loadProviders();
|
|
88
|
+
_bindKeyStep();
|
|
101
89
|
});
|
|
102
90
|
}
|
|
103
91
|
|
|
104
92
|
function _updateLangBtns(lang) {
|
|
105
|
-
const btnEn
|
|
106
|
-
const btnZh
|
|
93
|
+
const btnEn = $("setup-btn-lang-en");
|
|
94
|
+
const btnZh = $("setup-btn-lang-zh");
|
|
95
|
+
const btnNext = $("setup-btn-lang-next");
|
|
107
96
|
if (!btnEn || !btnZh) return;
|
|
108
97
|
btnEn.classList.toggle("active", lang === "en");
|
|
109
98
|
btnZh.classList.toggle("active", lang === "zh");
|
|
110
|
-
// Update the Continue button label after language switch
|
|
111
|
-
const btnNext = $("onboard-btn-lang-next");
|
|
112
99
|
if (btnNext) btnNext.textContent = lang === "zh" ? "继续 →" : "Continue →";
|
|
113
100
|
}
|
|
114
101
|
|
|
115
|
-
|
|
102
|
+
function _showSetupStep(step) {
|
|
103
|
+
$("setup-phase-lang").style.display = step === "lang" ? "" : "none";
|
|
104
|
+
$("setup-phase-key").style.display = step === "key" ? "" : "none";
|
|
105
|
+
$("setup-dot-1").className = "setup-step" + (step === "lang" ? " active" : " done");
|
|
106
|
+
$("setup-dot-2").className = "setup-step" + (step === "key" ? " active" : "");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Step 2 — API key setup
|
|
110
|
+
// Guard: providers are loaded only once; dropdown is bound only once.
|
|
111
|
+
let _providersLoaded = false;
|
|
112
|
+
let _dropdownBound = false;
|
|
116
113
|
|
|
117
114
|
async function _loadProviders() {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
115
|
+
// Fetch providers only once; on Back→Next, re-render from cache.
|
|
116
|
+
if (!_providersLoaded) {
|
|
117
|
+
try {
|
|
118
|
+
const res = await fetch("/api/providers");
|
|
119
|
+
const data = await res.json();
|
|
120
|
+
_providers = data.providers || [];
|
|
121
|
+
_providersLoaded = true;
|
|
122
|
+
} catch (_) { /* ignore */ }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Always re-render options (dropdown is cleared on each visit to Step 2)
|
|
126
|
+
_renderProviderOptions();
|
|
127
|
+
// Bind event listeners only once (delegation-based, safe to skip on re-entry)
|
|
128
|
+
_bindCustomDropdown();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function _renderProviderOptions() {
|
|
132
|
+
const dropdown = $("setup-provider-dropdown");
|
|
133
|
+
// Clear any previously rendered options before re-rendering
|
|
134
|
+
dropdown.innerHTML = "";
|
|
135
|
+
|
|
136
|
+
_providers.forEach(p => {
|
|
137
|
+
const opt = document.createElement("div");
|
|
138
|
+
opt.className = "custom-select-option";
|
|
139
|
+
opt.dataset.value = p.id;
|
|
140
|
+
opt.textContent = p.name;
|
|
141
|
+
dropdown.appendChild(opt);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Always append "Custom" as the last option
|
|
145
|
+
const custom = document.createElement("div");
|
|
146
|
+
custom.className = "custom-select-option";
|
|
147
|
+
custom.dataset.value = "__custom__";
|
|
148
|
+
custom.dataset.i18n = "onboard.provider.custom";
|
|
149
|
+
custom.textContent = I18n.t("onboard.provider.custom");
|
|
150
|
+
dropdown.appendChild(custom);
|
|
135
151
|
}
|
|
136
152
|
|
|
137
153
|
function _bindCustomDropdown() {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
154
|
+
if (_dropdownBound) return; // listeners already attached
|
|
155
|
+
_dropdownBound = true;
|
|
156
|
+
|
|
157
|
+
const wrapper = $("setup-provider-wrapper");
|
|
158
|
+
const trigger = wrapper.querySelector(".custom-select-trigger");
|
|
159
|
+
const dropdown = wrapper.querySelector(".custom-select-dropdown");
|
|
141
160
|
const valueSpan = trigger.querySelector(".custom-select-value");
|
|
142
|
-
const options = dropdown.querySelectorAll(".custom-select-option");
|
|
143
161
|
|
|
144
|
-
|
|
145
|
-
trigger.addEventListener("click", (e) => {
|
|
162
|
+
trigger.addEventListener("click", e => {
|
|
146
163
|
e.stopPropagation();
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
dropdown.classList.remove("open");
|
|
150
|
-
trigger.classList.remove("open");
|
|
151
|
-
} else {
|
|
152
|
-
dropdown.classList.add("open");
|
|
153
|
-
trigger.classList.add("open");
|
|
154
|
-
}
|
|
164
|
+
const open = dropdown.classList.toggle("open");
|
|
165
|
+
trigger.classList.toggle("open", open);
|
|
155
166
|
});
|
|
156
167
|
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (value) {
|
|
182
|
-
const preset = _providers.find(p => p.id === value);
|
|
183
|
-
if (preset) {
|
|
184
|
-
$("onboard-model").value = preset.default_model || "";
|
|
185
|
-
$("onboard-base-url").value = preset.base_url || "";
|
|
186
|
-
}
|
|
168
|
+
// Use event delegation on the dropdown container — works for any option
|
|
169
|
+
// including dynamically added ones (no need to re-bind on Back/Next).
|
|
170
|
+
dropdown.addEventListener("click", e => {
|
|
171
|
+
e.stopPropagation();
|
|
172
|
+
const opt = e.target.closest(".custom-select-option");
|
|
173
|
+
if (!opt) return;
|
|
174
|
+
|
|
175
|
+
const value = opt.dataset.value;
|
|
176
|
+
valueSpan.textContent = opt.textContent;
|
|
177
|
+
valueSpan.classList.toggle("placeholder", !value);
|
|
178
|
+
dropdown.querySelectorAll(".custom-select-option").forEach(o => o.classList.remove("selected"));
|
|
179
|
+
opt.classList.add("selected");
|
|
180
|
+
dropdown.classList.remove("open");
|
|
181
|
+
trigger.classList.remove("open");
|
|
182
|
+
|
|
183
|
+
if (value === "__custom__") {
|
|
184
|
+
// Custom: clear presets so the user can fill in their own values
|
|
185
|
+
$("setup-model").value = "";
|
|
186
|
+
$("setup-base-url").value = "";
|
|
187
|
+
} else if (value) {
|
|
188
|
+
const preset = _providers.find(p => p.id === value);
|
|
189
|
+
if (preset) {
|
|
190
|
+
$("setup-model").value = preset.default_model || "";
|
|
191
|
+
$("setup-base-url").value = preset.base_url || "";
|
|
187
192
|
}
|
|
188
|
-
}
|
|
193
|
+
}
|
|
189
194
|
});
|
|
190
195
|
|
|
191
|
-
//
|
|
196
|
+
// Single global click-outside listener
|
|
192
197
|
document.addEventListener("click", () => {
|
|
193
198
|
dropdown.classList.remove("open");
|
|
194
199
|
trigger.classList.remove("open");
|
|
195
200
|
});
|
|
196
201
|
}
|
|
197
202
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
eyeIcon.innerHTML = `
|
|
217
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
218
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
|
219
|
-
`;
|
|
220
|
-
}
|
|
203
|
+
// Guard: key-step listeners are attached only once
|
|
204
|
+
let _keyStepBound = false;
|
|
205
|
+
|
|
206
|
+
function _bindKeyStep() {
|
|
207
|
+
if (_keyStepBound) return;
|
|
208
|
+
_keyStepBound = true;
|
|
209
|
+
|
|
210
|
+
// Toggle key visibility
|
|
211
|
+
const toggleBtn = $("setup-toggle-key");
|
|
212
|
+
const keyInput = $("setup-api-key");
|
|
213
|
+
const eyeIcon = toggleBtn.querySelector("svg");
|
|
214
|
+
|
|
215
|
+
toggleBtn.addEventListener("click", () => {
|
|
216
|
+
const isPassword = keyInput.type === "password";
|
|
217
|
+
keyInput.type = isPassword ? "text" : "password";
|
|
218
|
+
eyeIcon.innerHTML = isPassword
|
|
219
|
+
? `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"></path>`
|
|
220
|
+
: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>`;
|
|
221
221
|
});
|
|
222
222
|
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
$("setup-btn-test").addEventListener("click", _testAndSave);
|
|
224
|
+
|
|
225
|
+
// Back to Step 1
|
|
226
|
+
$("setup-btn-back").addEventListener("click", () => {
|
|
227
|
+
_showSetupStep("lang");
|
|
228
|
+
});
|
|
225
229
|
}
|
|
226
230
|
|
|
227
231
|
async function _testAndSave() {
|
|
228
|
-
const btn = $("
|
|
229
|
-
const model = $("
|
|
230
|
-
const baseUrl = $("
|
|
231
|
-
const apiKey = $("
|
|
232
|
+
const btn = $("setup-btn-test");
|
|
233
|
+
const model = $("setup-model").value.trim();
|
|
234
|
+
const baseUrl = $("setup-base-url").value.trim();
|
|
235
|
+
const apiKey = $("setup-api-key").value.trim();
|
|
236
|
+
const zh = _selectedLang === "zh";
|
|
232
237
|
|
|
233
238
|
if (!model || !baseUrl || !apiKey) {
|
|
234
|
-
|
|
235
|
-
? "请填写模型、Base URL 和 API Key。"
|
|
236
|
-
: "Please fill in Model, Base URL and API Key.");
|
|
239
|
+
_setResult(false, zh ? "请填写模型、Base URL 和 API Key。" : "Please fill in Model, Base URL and API Key.");
|
|
237
240
|
return;
|
|
238
241
|
}
|
|
239
242
|
|
|
240
243
|
btn.disabled = true;
|
|
241
244
|
btn.textContent = I18n.t("onboard.key.testing");
|
|
242
|
-
|
|
245
|
+
_setResult(null, "");
|
|
243
246
|
|
|
244
247
|
// Step 1: test connection
|
|
245
248
|
try {
|
|
246
|
-
const
|
|
249
|
+
const res = await fetch("/api/config/test", {
|
|
247
250
|
method: "POST",
|
|
248
251
|
headers: { "Content-Type": "application/json" },
|
|
249
252
|
body: JSON.stringify({ model, base_url: baseUrl, api_key: apiKey, index: 0 })
|
|
250
253
|
});
|
|
251
|
-
const
|
|
252
|
-
if (!
|
|
253
|
-
|
|
254
|
+
const data = await res.json();
|
|
255
|
+
if (!data.ok) {
|
|
256
|
+
_setResult(false, data.message || (zh ? "连接失败。" : "Connection failed."));
|
|
254
257
|
btn.disabled = false;
|
|
255
258
|
btn.textContent = I18n.t("onboard.key.btn.test");
|
|
256
259
|
return;
|
|
257
260
|
}
|
|
258
261
|
} catch (e) {
|
|
259
|
-
|
|
262
|
+
_setResult(false, e.message);
|
|
260
263
|
btn.disabled = false;
|
|
261
264
|
btn.textContent = I18n.t("onboard.key.btn.test");
|
|
262
265
|
return;
|
|
@@ -265,93 +268,68 @@ const Onboard = (() => {
|
|
|
265
268
|
// Step 2: save config
|
|
266
269
|
btn.textContent = I18n.t("onboard.key.saving");
|
|
267
270
|
try {
|
|
268
|
-
const
|
|
271
|
+
const res = await fetch("/api/config", {
|
|
269
272
|
method: "POST",
|
|
270
273
|
headers: { "Content-Type": "application/json" },
|
|
271
274
|
body: JSON.stringify({
|
|
272
275
|
models: [{ type: "default", model, base_url: baseUrl, api_key: apiKey, anthropic_format: false }]
|
|
273
276
|
})
|
|
274
277
|
});
|
|
275
|
-
const
|
|
276
|
-
if (!
|
|
277
|
-
|
|
278
|
+
const data = await res.json();
|
|
279
|
+
if (!data.ok) {
|
|
280
|
+
_setResult(false, data.error || (zh ? "保存失败。" : "Save failed."));
|
|
278
281
|
btn.disabled = false;
|
|
279
282
|
btn.textContent = I18n.t("onboard.key.btn.test");
|
|
280
283
|
return;
|
|
281
284
|
}
|
|
282
285
|
} catch (e) {
|
|
283
|
-
|
|
286
|
+
_setResult(false, e.message);
|
|
284
287
|
btn.disabled = false;
|
|
285
288
|
btn.textContent = I18n.t("onboard.key.btn.test");
|
|
286
289
|
return;
|
|
287
290
|
}
|
|
288
291
|
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
setTimeout(() =>
|
|
292
|
-
_startSoulSession();
|
|
293
|
-
}, 600);
|
|
292
|
+
// Success — show brief feedback then auto-launch /onboard session
|
|
293
|
+
_setResult(true, zh ? "连接成功!" : "Connected!");
|
|
294
|
+
setTimeout(() => _launchOnboardSession(), 600);
|
|
294
295
|
}
|
|
295
296
|
|
|
296
|
-
function
|
|
297
|
-
const el = $("
|
|
298
|
-
if (
|
|
297
|
+
function _setResult(ok, msg) {
|
|
298
|
+
const el = $("setup-test-result");
|
|
299
|
+
if (!el) return;
|
|
300
|
+
if (ok === null) { el.textContent = ""; el.className = "setup-test-result"; return; }
|
|
299
301
|
el.textContent = ok ? "✓ " + msg : "✗ " + msg;
|
|
300
|
-
el.className = "
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// ── Phase 2: Soul setup ──────────────────────────────────────────────────────
|
|
304
|
-
|
|
305
|
-
function _bindSoulPhase() {
|
|
306
|
-
$("onboard-btn-start-soul").addEventListener("click", _startSoulSession);
|
|
307
|
-
$("onboard-btn-skip").addEventListener("click", _skipSoul);
|
|
302
|
+
el.className = "setup-test-result " + (ok ? "result-ok" : "result-fail");
|
|
308
303
|
}
|
|
309
304
|
|
|
310
|
-
//
|
|
311
|
-
// Pattern: identical to Tasks.createInSession() — create session → boot UI
|
|
312
|
-
// → select session (triggers WS subscribe) → send /onboard slash command.
|
|
313
|
-
async function _startSoulSession() {
|
|
314
|
-
const btn = $("onboard-btn-start-soul");
|
|
315
|
-
btn.disabled = true;
|
|
316
|
-
btn.textContent = I18n.t("settings.personalize.btn.starting");
|
|
305
|
+
// ── /onboard session launcher ───────────────────────────────────────────────
|
|
317
306
|
|
|
307
|
+
// Create a dedicated session and send the /onboard slash command.
|
|
308
|
+
// Called after key_setup succeeds AND on soul_setup phase (auto, no panel shown).
|
|
309
|
+
async function _launchOnboardSession() {
|
|
318
310
|
try {
|
|
319
|
-
// Ensure config is persisted, then create the onboard session
|
|
320
311
|
await _complete();
|
|
321
312
|
const res = await fetch("/api/sessions", {
|
|
322
313
|
method: "POST",
|
|
323
314
|
headers: { "Content-Type": "application/json" },
|
|
324
315
|
body: JSON.stringify({ name: "✨ Onboard" })
|
|
325
316
|
});
|
|
326
|
-
const data
|
|
317
|
+
const data = await res.json();
|
|
327
318
|
const session = data.session;
|
|
328
319
|
if (!session) throw new Error("No session returned");
|
|
329
320
|
|
|
330
|
-
// Register the session and set up navigation BEFORE connecting WS.
|
|
331
|
-
// This ensures Router.current === "session" when the first session_list
|
|
332
|
-
// event arrives, preventing restoreFromHash() from wrongly redirecting
|
|
333
|
-
// to "welcome" (which would happen because there is no hash set during onboard).
|
|
334
321
|
Sessions.add(session);
|
|
335
322
|
Sessions.renderList();
|
|
336
323
|
Sessions.setPendingMessage(session.id, `/onboard lang:${_selectedLang}`);
|
|
337
324
|
Sessions.select(session.id);
|
|
338
325
|
|
|
339
|
-
// Boot WS + sidebar data. WS.connect() is async; the subscribe message
|
|
340
|
-
// queued by Sessions.select() will be flushed once the socket opens.
|
|
341
326
|
_bootUI();
|
|
342
|
-
} catch (
|
|
343
|
-
|
|
344
|
-
|
|
327
|
+
} catch (_) {
|
|
328
|
+
// Fallback: just boot normally if session creation fails
|
|
329
|
+
_bootUI();
|
|
345
330
|
}
|
|
346
331
|
}
|
|
347
332
|
|
|
348
|
-
async function _skipSoul() {
|
|
349
|
-
// Write a default SOUL.md so onboard isn't re-triggered, then boot normally
|
|
350
|
-
await _complete();
|
|
351
|
-
await _ensureSoulFile();
|
|
352
|
-
_bootUI();
|
|
353
|
-
}
|
|
354
|
-
|
|
355
333
|
// POST /api/onboard/complete — persists config, creates default session if missing.
|
|
356
334
|
async function _complete() {
|
|
357
335
|
try {
|
|
@@ -360,24 +338,13 @@ const Onboard = (() => {
|
|
|
360
338
|
} catch (_) { return null; }
|
|
361
339
|
}
|
|
362
340
|
|
|
363
|
-
// POST /api/onboard/skip-soul — writes a minimal default SOUL.md (lang-aware).
|
|
364
|
-
async function _ensureSoulFile() {
|
|
365
|
-
try {
|
|
366
|
-
await fetch("/api/onboard/skip-soul", {
|
|
367
|
-
method: "POST",
|
|
368
|
-
headers: { "Content-Type": "application/json" },
|
|
369
|
-
body: JSON.stringify({ lang: _selectedLang })
|
|
370
|
-
});
|
|
371
|
-
} catch (_) { /* ignore */ }
|
|
372
|
-
}
|
|
373
|
-
|
|
374
341
|
// Boot the normal UI (WS + sessions sidebar + tasks + skills).
|
|
375
|
-
// WS.connect() is idempotent (guards against double-connect internally).
|
|
376
342
|
function _bootUI() {
|
|
343
|
+
document.body.classList.remove("setup-mode");
|
|
377
344
|
WS.connect();
|
|
378
345
|
Tasks.load();
|
|
379
346
|
Skills.load();
|
|
380
347
|
}
|
|
381
348
|
|
|
382
|
-
return { check, startSoulSession:
|
|
349
|
+
return { check, startSoulSession: _launchOnboardSession };
|
|
383
350
|
})();
|
data/lib/clacky/web/sessions.js
CHANGED
|
@@ -737,19 +737,23 @@ const Sessions = (() => {
|
|
|
737
737
|
const cost = ev.cost || 0;
|
|
738
738
|
const costStr = `$${cost.toFixed(5)}`;
|
|
739
739
|
|
|
740
|
+
// Always-visible: label, delta, cache indicator, cost
|
|
741
|
+
// Detail fields (Input/Output/Total) are hidden until hover
|
|
740
742
|
el.innerHTML =
|
|
741
743
|
`<span class="tu-label">[Tokens]</span>` +
|
|
742
744
|
`<span class="tu-sep">|</span>` +
|
|
743
745
|
`<span class="tu-delta ${deltaCls}">${escapeHtml(deltaStr)}</span>` +
|
|
746
|
+
(cacheUsed ? `<span class="tu-sep">|</span><span class="tu-cache">[*]</span>` : "") +
|
|
744
747
|
`<span class="tu-sep">|</span>` +
|
|
745
|
-
|
|
746
|
-
`<span class="tu-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
748
|
+
`<span class="tu-cost">Cost: ${escapeHtml(costStr)}</span>` +
|
|
749
|
+
`<span class="tu-detail">` +
|
|
750
|
+
`<span class="tu-sep">|</span>` +
|
|
751
|
+
`<span class="tu-field">Input: <b>${escapeHtml(inputStr)}</b></span>` +
|
|
752
|
+
`<span class="tu-sep">|</span>` +
|
|
753
|
+
`<span class="tu-field">Output: <b>${(ev.completion_tokens || 0).toLocaleString()}</b></span>` +
|
|
754
|
+
`<span class="tu-sep">|</span>` +
|
|
755
|
+
`<span class="tu-field">Total: <b>${(ev.total_tokens || 0).toLocaleString()}</b></span>` +
|
|
756
|
+
`</span>`;
|
|
753
757
|
|
|
754
758
|
messages.appendChild(el);
|
|
755
759
|
if (!container) messages.scrollTop = messages.scrollHeight; // only auto-scroll for live events
|