openclacky 0.8.7 → 0.8.8
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 +11 -0
- data/lib/clacky/default_skills/onboard/SKILL.md +97 -24
- data/lib/clacky/server/http_server.rb +30 -3
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +71 -0
- data/lib/clacky/web/app.js +11 -6
- data/lib/clacky/web/brand.js +12 -12
- data/lib/clacky/web/channels.js +39 -37
- data/lib/clacky/web/i18n.js +502 -0
- data/lib/clacky/web/index.html +85 -60
- data/lib/clacky/web/onboard.js +80 -28
- data/lib/clacky/web/sessions.js +6 -6
- data/lib/clacky/web/settings.js +50 -34
- data/lib/clacky/web/skills.js +36 -34
- data/lib/clacky/web/tasks.js +18 -18
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 94fe2f9fd0477231e7411c762aacfecc9eb15b87f027366d7598c73e738499c0
|
|
4
|
+
data.tar.gz: 41e84ac897d4d120dec9c7d88f69b35f82779adc2963a13dd099fd686d926ec3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1f28d84aab760ca2c53cc0c558ec82fb3b0bebc8b2fe5279021f3d0cc5c896ebe5dda7fc8f060f7d77609bb6b6aedffa064ef1897185db01a9b6f4e022364646
|
|
7
|
+
data.tar.gz: 64b71cd3c7793201f16600b100088cd739671c9c414252dadc6a593600f15271fc79a953877e59bc2014af929888384e3640ee78e7c1227c8b72e81f919ca72c
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.8] - 2026-03-13
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **i18n system with zh/en runtime switching**: WebUI now supports Chinese and English; all UI text is served through an `I18n` module and switches instantly without a page reload
|
|
14
|
+
- **Onboard language selection step**: first-time setup now opens with a language picker (中文 / English) before any configuration, so the entire onboard experience is conducted in the user's chosen language
|
|
15
|
+
- **Onboard "what's your name" step**: onboard flow now asks for the user's preferred name early on and addresses them by name throughout the rest of the setup
|
|
16
|
+
- **Chinese SOUL.md default**: when a user onboards in Chinese and skips the soul-setup conversation, a Chinese-language SOUL.md is written automatically so the assistant responds in Chinese by default
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **Onboard WS race condition**: fixed a bug where the first auto-triggered `/onboard` command was silently lost — the WebSocket `session_list` event arrived before the session view was active and redirected the UI to the welcome screen, hiding the agent's response
|
|
20
|
+
|
|
10
21
|
## [0.8.7] - 2026-03-13
|
|
11
22
|
|
|
12
23
|
### Added
|
|
@@ -14,22 +14,72 @@ All structured input is gathered through `request_user_feedback` cards — no fr
|
|
|
14
14
|
|
|
15
15
|
## Steps
|
|
16
16
|
|
|
17
|
+
### 0. Detect language
|
|
18
|
+
|
|
19
|
+
The user's language was set during the onboarding intro screen. The skill is invoked with
|
|
20
|
+
a `lang:` argument in the slash command, e.g. `/onboard lang:zh` or `/onboard lang:en`.
|
|
21
|
+
|
|
22
|
+
Check the invocation message for `lang:zh` or `lang:en`:
|
|
23
|
+
- If `lang:zh` is present → conduct the **entire** onboard in **Chinese**, write SOUL.md & USER.md in Chinese.
|
|
24
|
+
- Otherwise (or if missing) → use **English** throughout.
|
|
25
|
+
|
|
26
|
+
If the `lang:` argument is absent, infer from the user's first reply; default to English.
|
|
27
|
+
|
|
17
28
|
### 1. Greet the user
|
|
18
29
|
|
|
19
|
-
Send a short, warm welcome message (2–3 sentences).
|
|
20
|
-
|
|
30
|
+
Send a short, warm welcome message (2–3 sentences). Use the language determined in Step 0.
|
|
31
|
+
Do NOT ask any questions yet.
|
|
21
32
|
|
|
22
33
|
Example (English):
|
|
23
34
|
> Hi! I'm your personal assistant ⚡
|
|
24
35
|
> Let's take 30 seconds to personalize your experience — I'll ask just a couple of quick things.
|
|
25
36
|
|
|
26
|
-
|
|
37
|
+
Example (Chinese):
|
|
38
|
+
> 嗨!我是你的专属 AI 助手 ⚡
|
|
39
|
+
> 只需 30 秒完成个性化设置,我会问你两个简单问题。
|
|
40
|
+
|
|
41
|
+
### 2. Ask the user's name (card)
|
|
42
|
+
|
|
43
|
+
Call `request_user_feedback` to get the user's preferred name.
|
|
44
|
+
|
|
45
|
+
If `lang == "zh"`, use:
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"question": "先告诉我,我该怎么称呼你?"
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Otherwise (English):
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"question": "First, what should I call you?"
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Store the reply as `user.name` (default `"there"` for English, `"朋友"` for Chinese if blank).
|
|
60
|
+
|
|
61
|
+
### 3. Collect AI personality (card)
|
|
27
62
|
|
|
28
|
-
Call `request_user_feedback` with a card to set the assistant's
|
|
63
|
+
Call `request_user_feedback` with a card to set the assistant's personality.
|
|
64
|
+
Address the user by `user.name` in the question.
|
|
65
|
+
|
|
66
|
+
If `lang == "zh"`, use:
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"question": "好的,[user.name]!来设置一下你的助手吧。",
|
|
70
|
+
"options": [
|
|
71
|
+
"🎯 专业型 — 精准、结构化、不废话",
|
|
72
|
+
"😊 友好型 — 热情、鼓励、像一位博学的朋友",
|
|
73
|
+
"🎨 创意型 — 富有想象力,善用比喻,充满热情",
|
|
74
|
+
"⚡ 简洁型 — 极度简短,用要点,信噪比最高"
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
```
|
|
29
78
|
|
|
79
|
+
Otherwise (English):
|
|
30
80
|
```json
|
|
31
81
|
{
|
|
32
|
-
"question": "
|
|
82
|
+
"question": "Nice to meet you, [user.name]! Now let's set up your assistant.",
|
|
33
83
|
"options": [
|
|
34
84
|
"🎯 Professional — Precise, structured, minimal filler",
|
|
35
85
|
"😊 Friendly — Warm, encouraging, like a knowledgeable friend",
|
|
@@ -39,48 +89,49 @@ Call `request_user_feedback` with a card to set the assistant's name and persona
|
|
|
39
89
|
}
|
|
40
90
|
```
|
|
41
91
|
|
|
42
|
-
Also ask for a custom name in the same message if the platform supports a text field;
|
|
43
|
-
otherwise follow up with: "What should I call myself? (leave blank to keep 'Clacky')"
|
|
44
|
-
|
|
45
92
|
Map the chosen option to a personality key:
|
|
46
93
|
- Option 1 → `professional`
|
|
47
94
|
- Option 2 → `friendly`
|
|
48
95
|
- Option 3 → `creative`
|
|
49
96
|
- Option 4 → `concise`
|
|
50
97
|
|
|
51
|
-
Store: `ai.
|
|
98
|
+
Store: `ai.personality`.
|
|
52
99
|
|
|
53
|
-
###
|
|
100
|
+
### 4. Collect user profile (card)
|
|
54
101
|
|
|
55
|
-
Call `request_user_feedback` again
|
|
102
|
+
Call `request_user_feedback` again.
|
|
56
103
|
|
|
104
|
+
If `lang == "zh"`, use:
|
|
57
105
|
```json
|
|
58
106
|
{
|
|
59
|
-
"question": "
|
|
107
|
+
"question": "再简单了解一下你自己吧 —— 全部可选,随便填:\n• 职业\n• 最希望用 AI 做什么\n• 社交 / 作品链接(GitHub、微博、个人网站等)—— AI 会读取公开信息来了解你",
|
|
60
108
|
"options": []
|
|
61
109
|
}
|
|
62
110
|
```
|
|
63
111
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
112
|
+
Otherwise (English):
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"question": "Now a bit about you — all optional, skip anything you like.\n• Occupation\n• What you want to use AI for most\n• Social / portfolio links (GitHub, Twitter/X, personal site…) — AI will read them to learn about you",
|
|
116
|
+
"options": []
|
|
117
|
+
}
|
|
118
|
+
```
|
|
69
119
|
|
|
70
120
|
Parse the user's reply as free text; extract whatever they provide.
|
|
71
121
|
|
|
72
|
-
###
|
|
122
|
+
### 5. Learn from links (if any)
|
|
73
123
|
|
|
74
124
|
For each URL the user provided, use the `web_search` tool or fetch the page to read
|
|
75
125
|
publicly available info: bio, projects, tech stack, interests, writing style, etc.
|
|
76
126
|
Note key facts for the USER.md. Skip silently if a URL is unreachable.
|
|
77
127
|
|
|
78
|
-
###
|
|
128
|
+
### 6. Write SOUL.md
|
|
79
129
|
|
|
80
130
|
Write to `~/.clacky/agents/SOUL.md`.
|
|
81
131
|
|
|
82
132
|
Use `ai.name` and `ai.personality` to shape the content.
|
|
83
|
-
|
|
133
|
+
Write in the language determined in Step 0 (`zh` → Chinese, otherwise English).
|
|
134
|
+
If `lang == "zh"`, add a line: `**始终用中文回复用户。**` near the top of the Identity section.
|
|
84
135
|
|
|
85
136
|
**Personality style guide:**
|
|
86
137
|
|
|
@@ -113,7 +164,7 @@ I am [AI Name], a personal assistant and technical co-founder.
|
|
|
113
164
|
[2–3 sentences about how I approach tasks, matching the personality.]
|
|
114
165
|
```
|
|
115
166
|
|
|
116
|
-
###
|
|
167
|
+
### 7. Write USER.md
|
|
117
168
|
|
|
118
169
|
Write to `~/.clacky/agents/USER.md`.
|
|
119
170
|
|
|
@@ -121,7 +172,7 @@ Write to `~/.clacky/agents/USER.md`.
|
|
|
121
172
|
# User Profile
|
|
122
173
|
|
|
123
174
|
## About
|
|
124
|
-
- **Name**: [
|
|
175
|
+
- **Name**: [user.name, or "Not provided"]
|
|
125
176
|
- **Occupation**: [or "Not provided"]
|
|
126
177
|
- **Primary Goal**: [or "Not provided"]
|
|
127
178
|
|
|
@@ -133,9 +184,31 @@ Write to `~/.clacky/agents/USER.md`.
|
|
|
133
184
|
[1–2 sentences tailored to the user's goal and background.]
|
|
134
185
|
```
|
|
135
186
|
|
|
136
|
-
###
|
|
187
|
+
### 7b. Write USER.md (Chinese version, if applicable)
|
|
188
|
+
|
|
189
|
+
If `lang == "zh"`, write `~/.clacky/agents/USER.md` in Chinese:
|
|
190
|
+
|
|
191
|
+
```markdown
|
|
192
|
+
# 用户档案
|
|
193
|
+
|
|
194
|
+
## 基本信息
|
|
195
|
+
- **姓名**: [user.name,未填则写「未填写」]
|
|
196
|
+
- **职业**: [未填则写「未填写」]
|
|
197
|
+
- **主要目标**: [未填则写「未填写」]
|
|
198
|
+
|
|
199
|
+
## 背景与兴趣
|
|
200
|
+
[如有链接:3–5 条从公开信息中提取的要点。否则:写「暂无更多背景信息。」]
|
|
201
|
+
|
|
202
|
+
## 如何最好地帮助用户
|
|
203
|
+
[1–2 句话,根据用户目标和背景量身定制。]
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 8. Confirm and close
|
|
207
|
+
|
|
208
|
+
If `lang == "zh"`, reply:
|
|
209
|
+
> 全部设置完成!我已保存你的偏好。关掉这个对话,开启一个新对话就可以开始了 —— 尽情享用吧!🚀
|
|
137
210
|
|
|
138
|
-
|
|
211
|
+
Otherwise:
|
|
139
212
|
> All set! I've saved your preferences. Feel free to close this tab and start a fresh session — enjoy! 🚀
|
|
140
213
|
|
|
141
214
|
Do NOT open a new session — the UI handles navigation after the skill finishes.
|
|
@@ -96,6 +96,29 @@ module Clacky
|
|
|
96
96
|
- Breaking big goals into small, executable steps
|
|
97
97
|
MD
|
|
98
98
|
|
|
99
|
+
# Default SOUL.md for Chinese-language users.
|
|
100
|
+
DEFAULT_SOUL_MD_ZH = <<~MD.freeze
|
|
101
|
+
# Clacky — 助手灵魂
|
|
102
|
+
|
|
103
|
+
你是 Clacky,一位友好、能干的 AI 编程助手和技术联合创始人。
|
|
104
|
+
你思维敏锐、言简意赅、主动积极。你说话直接,不喜欢过度客套。
|
|
105
|
+
你热爱帮助用户打造优秀的软件产品。
|
|
106
|
+
|
|
107
|
+
**重要:始终用中文回复用户。**
|
|
108
|
+
|
|
109
|
+
## 性格特点
|
|
110
|
+
- 热情鼓励,但直接诚实
|
|
111
|
+
- 行动前先思考;简要说明你的推理过程
|
|
112
|
+
- 重行动而非空谈 —— 善用工具,写代码,交付结果
|
|
113
|
+
- 根据用户的风格调整语气和表达方式
|
|
114
|
+
|
|
115
|
+
## 核心能力
|
|
116
|
+
- 全栈软件开发(Ruby、Python、JS 等)
|
|
117
|
+
- 架构设计与代码审查
|
|
118
|
+
- 耐心细致地调试复杂问题
|
|
119
|
+
- 将大目标拆解为可执行的小步骤
|
|
120
|
+
MD
|
|
121
|
+
|
|
99
122
|
def initialize(host: "127.0.0.1", port: 7070, agent_config:, client_factory:, brand_test: false)
|
|
100
123
|
@host = host
|
|
101
124
|
@port = port
|
|
@@ -227,7 +250,7 @@ module Clacky
|
|
|
227
250
|
when ["GET", "/api/providers"] then api_list_providers(res)
|
|
228
251
|
when ["GET", "/api/onboard/status"] then api_onboard_status(res)
|
|
229
252
|
when ["POST", "/api/onboard/complete"] then api_onboard_complete(req, res)
|
|
230
|
-
when ["POST", "/api/onboard/skip-soul"] then api_onboard_skip_soul(res)
|
|
253
|
+
when ["POST", "/api/onboard/skip-soul"] then api_onboard_skip_soul(req, res)
|
|
231
254
|
when ["GET", "/api/store/skills"] then api_store_skills(res)
|
|
232
255
|
when ["GET", "/api/brand/status"] then api_brand_status(res)
|
|
233
256
|
when ["POST", "/api/brand/activate"] then api_brand_activate(req, res)
|
|
@@ -349,12 +372,16 @@ module Clacky
|
|
|
349
372
|
# POST /api/onboard/skip-soul
|
|
350
373
|
# Writes a minimal SOUL.md so the soul_setup phase is not re-triggered
|
|
351
374
|
# on the next server start when the user chooses to skip the conversation.
|
|
352
|
-
def api_onboard_skip_soul(res)
|
|
375
|
+
def api_onboard_skip_soul(req, res)
|
|
376
|
+
body = parse_json_body(req)
|
|
377
|
+
lang = body["lang"].to_s.strip
|
|
378
|
+
soul_content = lang == "zh" ? DEFAULT_SOUL_MD_ZH : DEFAULT_SOUL_MD
|
|
379
|
+
|
|
353
380
|
agents_dir = File.expand_path("~/.clacky/agents")
|
|
354
381
|
FileUtils.mkdir_p(agents_dir)
|
|
355
382
|
soul_path = File.join(agents_dir, "SOUL.md")
|
|
356
383
|
unless File.exist?(soul_path)
|
|
357
|
-
File.write(soul_path,
|
|
384
|
+
File.write(soul_path, soul_content)
|
|
358
385
|
end
|
|
359
386
|
json_response(res, 200, { ok: true })
|
|
360
387
|
end
|
data/lib/clacky/version.rb
CHANGED
data/lib/clacky/web/app.css
CHANGED
|
@@ -2344,6 +2344,77 @@ body {
|
|
|
2344
2344
|
background: var(--color-border-primary);
|
|
2345
2345
|
}
|
|
2346
2346
|
|
|
2347
|
+
/* ── Language selection phase ── */
|
|
2348
|
+
#onboard-phase-lang {
|
|
2349
|
+
width: 100%;
|
|
2350
|
+
display: flex;
|
|
2351
|
+
flex-direction: column;
|
|
2352
|
+
align-items: center;
|
|
2353
|
+
gap: 20px;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
.onboard-lang-prompt {
|
|
2357
|
+
font-size: 15px;
|
|
2358
|
+
color: var(--color-text-secondary);
|
|
2359
|
+
text-align: center;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
.onboard-lang-btns {
|
|
2363
|
+
display: flex;
|
|
2364
|
+
gap: 12px;
|
|
2365
|
+
justify-content: center;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
.onboard-lang-btn {
|
|
2369
|
+
background: var(--color-bg-secondary);
|
|
2370
|
+
border: 2px solid var(--color-border-primary);
|
|
2371
|
+
border-radius: 8px;
|
|
2372
|
+
color: var(--color-text-primary);
|
|
2373
|
+
font-size: 15px;
|
|
2374
|
+
font-weight: 600;
|
|
2375
|
+
padding: 12px 32px;
|
|
2376
|
+
cursor: pointer;
|
|
2377
|
+
transition: border-color .15s, background .15s, color .15s;
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
.onboard-lang-btn:hover {
|
|
2381
|
+
border-color: var(--color-accent-primary);
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
.onboard-lang-btn.active {
|
|
2385
|
+
background: var(--color-accent-primary);
|
|
2386
|
+
border-color: var(--color-accent-primary);
|
|
2387
|
+
color: #fff;
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
/* ── Settings language buttons ── */
|
|
2391
|
+
.settings-lang-btns {
|
|
2392
|
+
display: flex;
|
|
2393
|
+
gap: 8px;
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
.settings-lang-btn {
|
|
2397
|
+
background: var(--color-bg-secondary);
|
|
2398
|
+
border: 2px solid var(--color-border-primary);
|
|
2399
|
+
border-radius: 6px;
|
|
2400
|
+
color: var(--color-text-primary);
|
|
2401
|
+
font-size: 13px;
|
|
2402
|
+
font-weight: 600;
|
|
2403
|
+
padding: 6px 18px;
|
|
2404
|
+
cursor: pointer;
|
|
2405
|
+
transition: border-color .15s, background .15s, color .15s;
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
.settings-lang-btn:hover {
|
|
2409
|
+
border-color: var(--color-accent-primary);
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
.settings-lang-btn.active {
|
|
2413
|
+
background: var(--color-accent-primary);
|
|
2414
|
+
border-color: var(--color-accent-primary);
|
|
2415
|
+
color: #fff;
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2347
2418
|
/* ── Phase containers ── */
|
|
2348
2419
|
#onboard-phase-key,
|
|
2349
2420
|
#onboard-phase-soul {
|
data/lib/clacky/web/app.js
CHANGED
|
@@ -276,10 +276,15 @@ WS.onEvent(ev => {
|
|
|
276
276
|
Sessions.setAll(ev.sessions || []);
|
|
277
277
|
Sessions.renderList();
|
|
278
278
|
|
|
279
|
-
// Restore URL hash once on initial connect; ignore subsequent session_list events
|
|
279
|
+
// Restore URL hash once on initial connect; ignore subsequent session_list events.
|
|
280
|
+
// Skip if we are already on a session view (e.g. onboard flow navigated there
|
|
281
|
+
// before WS connected) — restoreFromHash would wrongly redirect to "welcome"
|
|
282
|
+
// because there is no hash set during onboarding.
|
|
280
283
|
if (!_initialRestoreDone) {
|
|
281
284
|
_initialRestoreDone = true;
|
|
282
|
-
Router.
|
|
285
|
+
if (Router.current !== "session") {
|
|
286
|
+
Router.restoreFromHash();
|
|
287
|
+
}
|
|
283
288
|
} else {
|
|
284
289
|
// If active session was deleted, go to welcome
|
|
285
290
|
if (Sessions.activeId && !Sessions.find(Sessions.activeId)) {
|
|
@@ -361,7 +366,7 @@ WS.onEvent(ev => {
|
|
|
361
366
|
|
|
362
367
|
case "progress":
|
|
363
368
|
if (ev.session_id !== Sessions.activeId) break;
|
|
364
|
-
if (ev.status === "start") Sessions.showProgress(ev.message || "
|
|
369
|
+
if (ev.status === "start") Sessions.showProgress(ev.message || I18n.t("chat.thinking"));
|
|
365
370
|
else Sessions.clearProgress();
|
|
366
371
|
break;
|
|
367
372
|
|
|
@@ -369,7 +374,7 @@ WS.onEvent(ev => {
|
|
|
369
374
|
if (ev.session_id !== Sessions.activeId) break;
|
|
370
375
|
Sessions.clearProgress();
|
|
371
376
|
Sessions.collapseToolGroup();
|
|
372
|
-
Sessions.appendInfo(`✓
|
|
377
|
+
Sessions.appendInfo(`✓ ${I18n.t("chat.done", { n: ev.iterations, cost: (ev.cost || 0).toFixed(4) })}`);
|
|
373
378
|
break;
|
|
374
379
|
|
|
375
380
|
case "request_confirmation":
|
|
@@ -381,7 +386,7 @@ WS.onEvent(ev => {
|
|
|
381
386
|
if (ev.session_id !== Sessions.activeId) break;
|
|
382
387
|
Sessions.clearProgress();
|
|
383
388
|
Sessions.collapseToolGroup();
|
|
384
|
-
Sessions.appendInfo("
|
|
389
|
+
Sessions.appendInfo(I18n.t("chat.interrupted"));
|
|
385
390
|
break;
|
|
386
391
|
|
|
387
392
|
// ── Info / errors ──────────────────────────────────────────────────
|
|
@@ -705,7 +710,7 @@ const SkillAC = (() => {
|
|
|
705
710
|
// Header label
|
|
706
711
|
const header = document.createElement("div");
|
|
707
712
|
header.className = "skill-ac-header";
|
|
708
|
-
header.textContent = "
|
|
713
|
+
header.textContent = I18n.t("sidebar.skills");
|
|
709
714
|
list.appendChild(header);
|
|
710
715
|
|
|
711
716
|
_items.forEach((skill, idx) => {
|
data/lib/clacky/web/brand.js
CHANGED
|
@@ -49,8 +49,8 @@ const Brand = (() => {
|
|
|
49
49
|
if (brandName) {
|
|
50
50
|
const title = $("brand-title");
|
|
51
51
|
const sub = $("brand-subtitle");
|
|
52
|
-
if (title) title.textContent = "
|
|
53
|
-
if (sub) sub.textContent = "
|
|
52
|
+
if (title) title.textContent = I18n.t("brand.activate.title", { name: brandName });
|
|
53
|
+
if (sub) sub.textContent = I18n.t("brand.activate.subtitle");
|
|
54
54
|
}
|
|
55
55
|
Router.navigate("brand");
|
|
56
56
|
_bindActivationPanel();
|
|
@@ -69,18 +69,18 @@ const Brand = (() => {
|
|
|
69
69
|
const key = $("brand-license-key").value.trim();
|
|
70
70
|
|
|
71
71
|
if (!key) {
|
|
72
|
-
_setResult(false, "
|
|
72
|
+
_setResult(false, I18n.t("settings.brand.enterKey"));
|
|
73
73
|
return;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
// In brand-test mode accept any non-empty key so developers can test without a real license.
|
|
77
77
|
if (!_testMode && !/^[0-9A-Fa-f]{8}(-[0-9A-Fa-f]{8}){4}$/.test(key)) {
|
|
78
|
-
_setResult(false, "
|
|
78
|
+
_setResult(false, I18n.t("settings.brand.invalidFormat"));
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
btn.disabled = true;
|
|
83
|
-
btn.textContent = "
|
|
83
|
+
btn.textContent = I18n.t("settings.brand.btn.activating");
|
|
84
84
|
_setResult(null, "");
|
|
85
85
|
|
|
86
86
|
try {
|
|
@@ -92,18 +92,18 @@ const Brand = (() => {
|
|
|
92
92
|
const data = await res.json();
|
|
93
93
|
|
|
94
94
|
if (data.ok) {
|
|
95
|
-
_setResult(true, "
|
|
95
|
+
_setResult(true, I18n.t("brand.activate.success"));
|
|
96
96
|
if (data.brand_name) _applyBrandName(data.brand_name);
|
|
97
97
|
setTimeout(_bootUI, 800);
|
|
98
98
|
} else {
|
|
99
|
-
_setResult(false, data.error || "
|
|
99
|
+
_setResult(false, data.error || I18n.t("settings.brand.activationFailed"));
|
|
100
100
|
btn.disabled = false;
|
|
101
|
-
btn.textContent = "
|
|
101
|
+
btn.textContent = I18n.t("settings.brand.btn.activate");
|
|
102
102
|
}
|
|
103
103
|
} catch (e) {
|
|
104
|
-
_setResult(false, "
|
|
104
|
+
_setResult(false, I18n.t("settings.brand.networkError") + e.message);
|
|
105
105
|
btn.disabled = false;
|
|
106
|
-
btn.textContent = "
|
|
106
|
+
btn.textContent = I18n.t("settings.brand.btn.activate");
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
@@ -124,8 +124,8 @@ const Brand = (() => {
|
|
|
124
124
|
const nodes = {
|
|
125
125
|
"page-title": name,
|
|
126
126
|
"sidebar-logo": name,
|
|
127
|
-
"onboard-title": "
|
|
128
|
-
"welcome-title": "
|
|
127
|
+
"onboard-title": I18n.t("onboard.welcome", { name }),
|
|
128
|
+
"welcome-title": I18n.t("onboard.welcome", { name })
|
|
129
129
|
};
|
|
130
130
|
Object.entries(nodes).forEach(([id, text]) => {
|
|
131
131
|
const el = $(id);
|
data/lib/clacky/web/channels.js
CHANGED
|
@@ -8,25 +8,27 @@
|
|
|
8
8
|
|
|
9
9
|
const Channels = (() => {
|
|
10
10
|
|
|
11
|
-
// Platform display metadata
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
11
|
+
// Platform display metadata (use accessor to pick up runtime language)
|
|
12
|
+
function PLATFORM_META() {
|
|
13
|
+
return {
|
|
14
|
+
feishu: {
|
|
15
|
+
logo: "飞",
|
|
16
|
+
logoClass: "channel-logo-feishu",
|
|
17
|
+
name: "Feishu / Lark",
|
|
18
|
+
desc: I18n.t("channels.feishu.desc"),
|
|
19
|
+
setupCmd: "/channel-setup setup feishu",
|
|
20
|
+
testCmd: "/channel-setup doctor",
|
|
21
|
+
},
|
|
22
|
+
wecom: {
|
|
23
|
+
logo: "微",
|
|
24
|
+
logoClass: "channel-logo-wecom",
|
|
25
|
+
name: "WeCom (企业微信)",
|
|
26
|
+
desc: I18n.t("channels.wecom.desc"),
|
|
27
|
+
setupCmd: "/channel-setup setup wecom",
|
|
28
|
+
testCmd: "/channel-setup doctor",
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
30
32
|
|
|
31
33
|
// ── Public API ──────────────────────────────────────────────────────────────
|
|
32
34
|
|
|
@@ -39,14 +41,14 @@ const Channels = (() => {
|
|
|
39
41
|
async function _load() {
|
|
40
42
|
const container = $("channels-list");
|
|
41
43
|
if (!container) return;
|
|
42
|
-
container.innerHTML = `<div class="channel-loading"
|
|
44
|
+
container.innerHTML = `<div class="channel-loading">${I18n.t("channels.loading")}</div>`;
|
|
43
45
|
|
|
44
46
|
try {
|
|
45
47
|
const res = await fetch("/api/channels");
|
|
46
48
|
const data = await res.json();
|
|
47
49
|
_render(data.channels || []);
|
|
48
50
|
} catch (e) {
|
|
49
|
-
container.innerHTML = `<div class="channel-error"
|
|
51
|
+
container.innerHTML = `<div class="channel-error">${I18n.t("channels.loadError", { msg: _esc(e.message) })}</div>`;
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
54
|
|
|
@@ -58,15 +60,15 @@ const Channels = (() => {
|
|
|
58
60
|
container.innerHTML = "";
|
|
59
61
|
|
|
60
62
|
// Merge server data with display metadata, show all known platforms
|
|
61
|
-
const
|
|
63
|
+
const meta = PLATFORM_META();
|
|
64
|
+
const platformIds = Object.keys(meta);
|
|
62
65
|
platformIds.forEach(pid => {
|
|
63
66
|
const serverData = channels.find(c => c.platform == pid) || {};
|
|
64
|
-
container.appendChild(_renderCard(pid, serverData));
|
|
67
|
+
container.appendChild(_renderCard(pid, serverData, meta[pid]));
|
|
65
68
|
});
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
function _renderCard(platform, data) {
|
|
69
|
-
const meta = PLATFORM_META[platform];
|
|
71
|
+
function _renderCard(platform, data, meta) {
|
|
70
72
|
const enabled = !!data.enabled;
|
|
71
73
|
const running = !!data.running;
|
|
72
74
|
|
|
@@ -97,13 +99,13 @@ const Channels = (() => {
|
|
|
97
99
|
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
|
98
100
|
<polyline points="22 4 12 14.01 9 11.01"/>
|
|
99
101
|
</svg>
|
|
100
|
-
|
|
102
|
+
${I18n.t("channels.btn.test")}
|
|
101
103
|
</button>
|
|
102
104
|
<button class="btn-channel-configure btn-primary" id="btn-configure-${_esc(platform)}">
|
|
103
105
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
104
106
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
|
105
107
|
</svg>
|
|
106
|
-
${enabled ? "
|
|
108
|
+
${enabled ? I18n.t("channels.btn.reconfigure") : I18n.t("channels.btn.setup")}
|
|
107
109
|
</button>
|
|
108
110
|
</div>
|
|
109
111
|
</div>
|
|
@@ -119,32 +121,32 @@ const Channels = (() => {
|
|
|
119
121
|
// ── Badge & status hint helpers ───────────────────────────────────────────────
|
|
120
122
|
|
|
121
123
|
function _badgeHtml(enabled, running) {
|
|
122
|
-
if (running) return `<span class="badge-running">●
|
|
123
|
-
if (enabled) return `<span class="badge-enabled">●
|
|
124
|
-
return `<span class="badge-disabled">○
|
|
124
|
+
if (running) return `<span class="badge-running">● ${I18n.t("channels.badge.running")}</span>`;
|
|
125
|
+
if (enabled) return `<span class="badge-enabled">● ${I18n.t("channels.badge.enabled")}</span>`;
|
|
126
|
+
return `<span class="badge-disabled">○ ${I18n.t("channels.badge.notConfigured")}</span>`;
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
function _statusHint(enabled, running) {
|
|
128
130
|
if (running) {
|
|
129
|
-
return `<p class="channel-status-hint hint-ok">✓
|
|
131
|
+
return `<p class="channel-status-hint hint-ok">✓ ${I18n.t("channels.hint.running")}</p>`;
|
|
130
132
|
}
|
|
131
133
|
if (enabled) {
|
|
132
|
-
return `<p class="channel-status-hint hint-warn">⚠
|
|
134
|
+
return `<p class="channel-status-hint hint-warn">⚠ ${I18n.t("channels.hint.enabledNotRunning")}</p>`;
|
|
133
135
|
}
|
|
134
|
-
return `<p class="channel-status-hint hint-idle"
|
|
136
|
+
return `<p class="channel-status-hint hint-idle">${I18n.t("channels.hint.notConfigured")}</p>`;
|
|
135
137
|
}
|
|
136
138
|
|
|
137
139
|
// ── Actions ───────────────────────────────────────────────────────────────────
|
|
138
140
|
|
|
139
141
|
// Run E2E test: open a session and send /channel-setup doctor
|
|
140
142
|
async function _runTest(platform) {
|
|
141
|
-
const meta = PLATFORM_META[platform];
|
|
143
|
+
const meta = PLATFORM_META()[platform];
|
|
142
144
|
await _sendToAgent(meta.testCmd, `Channel E2E Test — ${meta.name}`);
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
// Open setup: open a session and send /channel-setup setup <platform>
|
|
146
148
|
async function _openSetup(platform) {
|
|
147
|
-
const meta = PLATFORM_META[platform];
|
|
149
|
+
const meta = PLATFORM_META()[platform];
|
|
148
150
|
await _sendToAgent(meta.setupCmd, `Channel Setup — ${meta.name}`);
|
|
149
151
|
}
|
|
150
152
|
|
|
@@ -165,9 +167,9 @@ const Channels = (() => {
|
|
|
165
167
|
body: JSON.stringify({ name }),
|
|
166
168
|
});
|
|
167
169
|
const data = await res.json();
|
|
168
|
-
if (!res.ok) throw new Error(data.error || "
|
|
170
|
+
if (!res.ok) throw new Error(data.error || I18n.t("channels.sessionError"));
|
|
169
171
|
const session = data.session;
|
|
170
|
-
if (!session) throw new Error("
|
|
172
|
+
if (!session) throw new Error(I18n.t("channels.noSession"));
|
|
171
173
|
|
|
172
174
|
// Register in Sessions, refresh sidebar, queue command, then navigate
|
|
173
175
|
Sessions.add(session);
|