openclacky 1.0.3 → 1.0.5
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 +34 -1
- data/benchmark/fixtures/sample_project/Gemfile +3 -0
- data/benchmark/fixtures/sample_project/lib/api_handler.rb +32 -0
- data/benchmark/fixtures/sample_project/lib/order_calculator.rb +23 -0
- data/benchmark/fixtures/sample_project/lib/user_renderer.rb +20 -0
- data/benchmark/fixtures/sample_project/spec/order_calculator_spec.rb +20 -0
- data/benchmark/results/EVALUATION_REPORT.md +165 -0
- data/benchmark/results/baseline_20260511_174424.json +128 -0
- data/benchmark/results/report_20260511_175256.json +271 -0
- data/benchmark/results/report_20260511_175444.json +271 -0
- data/benchmark/results/treatment_20260511_175103.json +130 -0
- data/benchmark/runner.rb +441 -0
- data/docs/proposals/2026-05-11-system-prompt-alignment.md +325 -0
- data/docs/proposals/2026-05-12-memory-mechanism-optimization.md +89 -0
- data/lib/clacky/agent/cost_tracker.rb +8 -2
- data/lib/clacky/agent/memory_updater.rb +41 -30
- data/lib/clacky/agent/skill_manager.rb +5 -2
- data/lib/clacky/agent/skill_reflector.rb +10 -1
- data/lib/clacky/agent.rb +4 -0
- data/lib/clacky/client.rb +15 -0
- data/lib/clacky/default_agents/base_prompt.md +20 -20
- data/lib/clacky/default_agents/coding/system_prompt.md +51 -1
- data/lib/clacky/default_skills/channel-setup/SKILL.md +190 -14
- data/lib/clacky/default_skills/channel-setup/discord_setup.rb +199 -0
- data/lib/clacky/default_skills/channel-setup/import_lark_skills.rb +97 -0
- data/lib/clacky/default_skills/onboard/SKILL.md +1 -1
- data/lib/clacky/default_skills/persist-memory/SKILL.md +59 -0
- data/lib/clacky/providers.rb +77 -10
- data/lib/clacky/server/channel/adapters/discord/adapter.rb +229 -0
- data/lib/clacky/server/channel/adapters/discord/api_client.rb +107 -0
- data/lib/clacky/server/channel/adapters/discord/gateway_client.rb +272 -0
- data/lib/clacky/server/channel/adapters/telegram/adapter.rb +375 -0
- data/lib/clacky/server/channel/adapters/telegram/api_client.rb +205 -0
- data/lib/clacky/server/channel/channel_config.rb +11 -0
- data/lib/clacky/server/channel.rb +2 -0
- data/lib/clacky/server/http_server.rb +69 -3
- data/lib/clacky/ui2/ui_controller.rb +2 -1
- data/lib/clacky/utils/file_processor.rb +71 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +44 -0
- data/lib/clacky/web/channels.js +16 -0
- data/lib/clacky/web/i18n.js +24 -2
- data/lib/clacky/web/index.html +6 -1
- data/lib/clacky/web/settings.js +4 -0
- data/lib/clacky/web/version.js +52 -1
- metadata +37 -2
|
@@ -3,7 +3,7 @@ users complete software development projects. You are responsible for developmen
|
|
|
3
3
|
|
|
4
4
|
Your role is to:
|
|
5
5
|
- Understand project requirements and translate them into technical solutions
|
|
6
|
-
- Write clean, maintainable
|
|
6
|
+
- Write clean, maintainable code
|
|
7
7
|
- Follow best practices and industry standards
|
|
8
8
|
- Explain technical concepts in simple terms when needed
|
|
9
9
|
- Proactively identify potential issues and suggest improvements
|
|
@@ -15,3 +15,53 @@ Working process:
|
|
|
15
15
|
3. You should frequently refer to the existing codebase. For unclear instructions,
|
|
16
16
|
prioritize understanding the codebase first before answering or taking action.
|
|
17
17
|
Always read relevant code files to understand the project structure, patterns, and conventions.
|
|
18
|
+
|
|
19
|
+
## Code Style
|
|
20
|
+
|
|
21
|
+
- **Default to writing no comments.** Only add one when the WHY is non-obvious: a hidden constraint, a subtle invariant, a workaround for a specific bug, or behavior that would surprise a reader.
|
|
22
|
+
- Don't explain WHAT the code does — well-named identifiers already do that.
|
|
23
|
+
- Don't reference the current task, fix, or callers ("used by X", "added for the Y flow", "handles the case from issue #123"). These belong in the PR description and rot as the codebase evolves.
|
|
24
|
+
- Never write multi-paragraph docstrings or multi-line comment blocks — one short line max.
|
|
25
|
+
|
|
26
|
+
## File Modification Rules
|
|
27
|
+
|
|
28
|
+
- **ALWAYS prefer `edit` over `write`.** Use `write` only for creating entirely new files or complete rewrites.
|
|
29
|
+
- When editing text from `file_reader` output, preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix.
|
|
30
|
+
- Ensure `old_string` is unique in the file. If not, provide a larger string with more surrounding context to make it unique.
|
|
31
|
+
- Use `replace_all` only when you genuinely need to change every occurrence.
|
|
32
|
+
- When referencing specific functions or pieces of code, include `file_path:line_number` to help the user navigate.
|
|
33
|
+
|
|
34
|
+
## Git Safety Protocol
|
|
35
|
+
|
|
36
|
+
- NEVER update git config (user.name, user.email, etc.)
|
|
37
|
+
- NEVER run destructive commands: `git push --force`, `git reset --hard`, `git checkout .`, `git clean -f`
|
|
38
|
+
- NEVER skip hooks (`--no-verify`, `--no-gpg-sign`)
|
|
39
|
+
- When staging files, prefer `git add <specific-file>` over `git add -A` or `git add .`
|
|
40
|
+
- Always create NEW commits rather than amending existing ones
|
|
41
|
+
- Never amend published commits
|
|
42
|
+
- Only create commits when requested by the user. If unclear, ask first.
|
|
43
|
+
|
|
44
|
+
## Error Handling
|
|
45
|
+
|
|
46
|
+
- Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees.
|
|
47
|
+
- Only validate at system boundaries (user input, external APIs).
|
|
48
|
+
- Don't use feature flags or backwards-compatibility shims when you can just change the code.
|
|
49
|
+
|
|
50
|
+
## Security
|
|
51
|
+
|
|
52
|
+
- Be careful not to introduce security vulnerabilities such as command injection, XSS, SQL injection, and other OWASP top 10 vulnerabilities.
|
|
53
|
+
- If you notice insecure code, immediately fix it.
|
|
54
|
+
- Prioritize writing safe, secure, and correct code.
|
|
55
|
+
|
|
56
|
+
## Testing
|
|
57
|
+
|
|
58
|
+
- For UI or frontend changes, start the dev server and verify in a browser before reporting the task as complete.
|
|
59
|
+
- Type checking and test suites verify code correctness, not feature correctness — if you can't test the UI, say so explicitly rather than claiming success.
|
|
60
|
+
- When the user asks you to run tests, do so and report the results.
|
|
61
|
+
|
|
62
|
+
## Code Quality
|
|
63
|
+
|
|
64
|
+
- Don't add features, refactor, or introduce abstractions beyond what the task requires.
|
|
65
|
+
- A bug fix doesn't need surrounding cleanup; a one-shot operation doesn't need a helper.
|
|
66
|
+
- Three similar lines is better than a premature abstraction.
|
|
67
|
+
- No half-finished implementations either.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: channel-setup
|
|
3
3
|
description: |
|
|
4
|
-
Configure IM platform channels (Feishu, WeCom, Weixin) for openclacky.
|
|
4
|
+
Configure IM platform channels (Feishu, WeCom, Weixin, Discord, Telegram) for openclacky.
|
|
5
5
|
Uses browser automation for navigation; guides the user to paste credentials and perform UI steps.
|
|
6
|
-
Trigger on: "channel setup", "setup feishu", "setup wecom", "setup weixin", "setup wechat", "
|
|
7
|
-
"channel status", "channel enable", "channel disable", "channel reconfigure", "channel doctor",
|
|
8
|
-
"send message to weixin", "send message to feishu", "send message to wecom".
|
|
6
|
+
Trigger on: "channel setup", "setup feishu", "setup wecom", "setup weixin", "setup wechat", "setup discord", "setup telegram",
|
|
7
|
+
"channel config", "channel status", "channel enable", "channel disable", "channel reconfigure", "channel doctor",
|
|
8
|
+
"send message to weixin", "send message to feishu", "send message to wecom", "send message to discord", "send message to telegram".
|
|
9
9
|
Subcommands: setup, status, enable <platform>, disable <platform>, reconfigure, doctor, send.
|
|
10
10
|
argument-hint: "setup | status | enable <platform> | disable <platform> | reconfigure | doctor | send <platform> <message>"
|
|
11
11
|
allowed-tools:
|
|
@@ -28,13 +28,13 @@ Configure IM platform channels for openclacky.
|
|
|
28
28
|
|
|
29
29
|
| User says | Subcommand |
|
|
30
30
|
|---|---|
|
|
31
|
-
| `channel setup`, `setup feishu`, `setup wecom`, `setup weixin`, `setup wechat` | setup |
|
|
31
|
+
| `channel setup`, `setup feishu`, `setup wecom`, `setup weixin`, `setup wechat`, `setup discord`, `setup telegram` | setup |
|
|
32
32
|
| `channel status` | status |
|
|
33
|
-
| `channel enable feishu/wecom/weixin` | enable |
|
|
34
|
-
| `channel disable feishu/wecom/weixin` | disable |
|
|
33
|
+
| `channel enable feishu/wecom/weixin/discord/telegram` | enable |
|
|
34
|
+
| `channel disable feishu/wecom/weixin/discord/telegram` | disable |
|
|
35
35
|
| `channel reconfigure` | reconfigure |
|
|
36
36
|
| `channel doctor` | doctor |
|
|
37
|
-
| `send <message> to weixin/feishu/wecom` | send |
|
|
37
|
+
| `send <message> to weixin/feishu/wecom/discord/telegram` | send |
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
|
@@ -51,7 +51,9 @@ Response shape (example):
|
|
|
51
51
|
{"channels":[
|
|
52
52
|
{"platform":"feishu","enabled":true,"running":true,"has_config":true,"app_id":"cli_xxx","domain":"https://open.feishu.cn","allowed_users":[]},
|
|
53
53
|
{"platform":"wecom","enabled":false,"running":false,"has_config":false,"bot_id":""},
|
|
54
|
-
{"platform":"weixin","enabled":true,"running":true,"has_config":true,"has_token":true,"base_url":"https://ilinkai.weixin.qq.com","allowed_users":[]}
|
|
54
|
+
{"platform":"weixin","enabled":true,"running":true,"has_config":true,"has_token":true,"base_url":"https://ilinkai.weixin.qq.com","allowed_users":[]},
|
|
55
|
+
{"platform":"discord","enabled":true,"running":true,"has_config":true,"has_token":true,"allowed_users":[]}
|
|
56
|
+
{"platform":"telegram","enabled":true,"running":true,"has_config":true,"has_token":true,"base_url":"https://api.telegram.org","parse_mode":"Markdown","allowed_users":[]}
|
|
55
57
|
]}
|
|
56
58
|
```
|
|
57
59
|
|
|
@@ -64,12 +66,16 @@ Platform Enabled Running Details
|
|
|
64
66
|
feishu ✅ yes ✅ yes app_id: cli_xxx...
|
|
65
67
|
wecom ❌ no ❌ no (not configured)
|
|
66
68
|
weixin ✅ yes ✅ yes has_token: true
|
|
69
|
+
discord ✅ yes ✅ yes has_token: true
|
|
70
|
+
telegram ✅ yes ✅ yes has_token: true
|
|
67
71
|
─────────────────────────────────────────────────────
|
|
68
72
|
```
|
|
69
73
|
|
|
70
74
|
- Feishu: show `app_id` (truncated to 12 chars)
|
|
71
75
|
- WeCom: show `bot_id` if present
|
|
72
76
|
- Weixin: show `has_token: true/false` (token value is never displayed)
|
|
77
|
+
- Discord: show `has_token: true/false` (token value is never displayed)
|
|
78
|
+
- Telegram: show `has_token: true/false` (bot token is never displayed)
|
|
73
79
|
|
|
74
80
|
If the API is unreachable or returns an empty list: "No channels configured yet. Run `/channel-setup setup` to get started."
|
|
75
81
|
|
|
@@ -83,6 +89,8 @@ Ask:
|
|
|
83
89
|
> 1. Feishu
|
|
84
90
|
> 2. WeCom (Enterprise WeChat)
|
|
85
91
|
> 3. Weixin (Personal WeChat via iLink QR login)
|
|
92
|
+
> 4. Discord
|
|
93
|
+
> 5. Telegram (Bot API)
|
|
86
94
|
|
|
87
95
|
---
|
|
88
96
|
|
|
@@ -100,7 +108,7 @@ ruby "SKILL_DIR/feishu_setup.rb"
|
|
|
100
108
|
- The script completed successfully.
|
|
101
109
|
- Config is already written to `~/.clacky/channels.yml`.
|
|
102
110
|
- Tell the user: "✅ Feishu channel configured automatically! The channel is ready."
|
|
103
|
-
- **
|
|
111
|
+
- **Skip Step 2 (manual fallback) and continue to Step 3.**
|
|
104
112
|
|
|
105
113
|
**If exit code is non-0:**
|
|
106
114
|
- Check stdout for the error message.
|
|
@@ -199,7 +207,61 @@ curl -s -X POST "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/in
|
|
|
199
207
|
-d '{"app_id":"<APP_ID>","app_secret":"<APP_SECRET>"}'
|
|
200
208
|
```
|
|
201
209
|
|
|
202
|
-
Check for `"code":0`. On success:
|
|
210
|
+
Check for `"code":0`. On success: continue to Step 3 (below).
|
|
211
|
+
|
|
212
|
+
##### Phase 9 — done
|
|
213
|
+
|
|
214
|
+
Step 2 ends here. **Continue to Step 3.**
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
#### Step 3 — Optional: install Feishu CLI
|
|
219
|
+
|
|
220
|
+
Reach here from either Step 1 success or end of Step 2. Read `app_id` and `app_secret` from `~/.clacky/channels.yml` (under `channels.feishu`) for the install commands below.
|
|
221
|
+
|
|
222
|
+
Call `request_user_feedback`:
|
|
223
|
+
|
|
224
|
+
zh:
|
|
225
|
+
```json
|
|
226
|
+
{
|
|
227
|
+
"question": "是否要安装「飞书 CLI」?装好之后 AI 可以帮你操作飞书云文档、电子表格、多维表格、知识库、日历、任务等几乎全部飞书能力,不只是聊天,而是能\"做事\"。不装也 OK。",
|
|
228
|
+
"options": ["启用", "跳过"]
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
en:
|
|
233
|
+
```json
|
|
234
|
+
{
|
|
235
|
+
"question": "Install Feishu CLI? With it, the AI can operate Feishu Docs, Sheets, Bitable, Wiki, Calendar, Tasks and almost every Feishu capability — not just chat, but actually get things done. Skipping is fine.",
|
|
236
|
+
"options": ["Enable", "Skip"]
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
If the user picks Skip, stop — setup is complete.
|
|
241
|
+
|
|
242
|
+
If the user picks Enable, run:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
lark-cli --version > /dev/null 2>&1 || npm install -g @larksuite/cli
|
|
246
|
+
echo -n "<APP_SECRET>" | lark-cli config init --app-id <APP_ID> --app-secret-stdin --brand feishu
|
|
247
|
+
npx -y skills add larksuite/cli -y -g
|
|
248
|
+
ruby "SKILL_DIR/import_lark_skills.rb"
|
|
249
|
+
lark-cli auth login --recommend
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The last command blocks up to 10 minutes waiting for browser authorization — make sure the runner's timeout is ≥ 600s.
|
|
253
|
+
|
|
254
|
+
Once you see the authorization URL in the command's stdout, tell the user (do **not** wait for a reply — the CLI's blocking poll will return on its own when authorization completes):
|
|
255
|
+
- zh: "请在浏览器中打开下方链接完成授权:\n<URL>"
|
|
256
|
+
- en: "Open this URL in your browser to authorize:\n<URL>"
|
|
257
|
+
|
|
258
|
+
**Do not kill and restart this command** — restarting invalidates the device code and breaks the link the user already opened. The "hang" is just polling; wait it out.
|
|
259
|
+
|
|
260
|
+
When `lark-cli auth login` returns successfully, tell the user:
|
|
261
|
+
- zh: "✅ 飞书 CLI 已就绪。"
|
|
262
|
+
- en: "✅ Feishu CLI is ready."
|
|
263
|
+
|
|
264
|
+
**Stop — setup is fully complete.**
|
|
203
265
|
|
|
204
266
|
---
|
|
205
267
|
|
|
@@ -290,6 +352,105 @@ Tell the user while waiting:
|
|
|
290
352
|
|
|
291
353
|
---
|
|
292
354
|
|
|
355
|
+
### Discord setup
|
|
356
|
+
|
|
357
|
+
Discord requires manual portal interaction (hCaptcha gates Application creation). The browser just navigates the user to the portal; the user clicks through and pastes the bot token + app id back.
|
|
358
|
+
|
|
359
|
+
#### Step 1 — Open the developer portal
|
|
360
|
+
|
|
361
|
+
Get the portal URL from the script and open it in the browser:
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
PORTAL_URL=$(ruby "SKILL_DIR/discord_setup.rb" --portal-url)
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Open it: `browser(action="navigate", url="<PORTAL_URL>")`. If the browser tool is not configured, invoke `browser-setup` first, then retry.
|
|
368
|
+
|
|
369
|
+
#### Step 2 — Guide the user through the portal (one round-trip)
|
|
370
|
+
|
|
371
|
+
Tell the user **all** of the following in a single message, then call `request_user_feedback` to collect the values in one reply:
|
|
372
|
+
|
|
373
|
+
> In the Discord Developer Portal I just opened:
|
|
374
|
+
>
|
|
375
|
+
> 1. Click **New Application** (top-right). Name it whatever you like (e.g. "Open Clacky"), check the ToS box, click **Create**.
|
|
376
|
+
> 2. In the left nav click **Bot**.
|
|
377
|
+
> 3. Scroll down to **Privileged Gateway Intents** and turn on **MESSAGE CONTENT INTENT**, then click **Save Changes**.
|
|
378
|
+
> 4. Scroll up, click **Reset Token** → **Yes, do it!**. Click **Copy** to copy the bot token. (This is the only time the token is shown — don't navigate away before copying.)
|
|
379
|
+
> 5. In the left nav click **General Information**. Copy the **Application ID**.
|
|
380
|
+
>
|
|
381
|
+
> Paste both values back here in this format (one line):
|
|
382
|
+
>
|
|
383
|
+
> `token=YOUR_BOT_TOKEN app_id=YOUR_APPLICATION_ID`
|
|
384
|
+
|
|
385
|
+
If the user is chatting in a non-English language, append the localized label in parens after each bolded English button name (e.g. `**Bot**(机器人)`). The English label stays primary — it's what they physically click in the portal.
|
|
386
|
+
|
|
387
|
+
Use `request_user_feedback` to collect the reply. Parse with tolerant regex (`token=\S+`, `app_id=\d+`).
|
|
388
|
+
|
|
389
|
+
If the reply is malformed (missing either field), apologise briefly and ask again with the exact same format reminder. Up to 3 retries; after that, surface the original message and stop.
|
|
390
|
+
|
|
391
|
+
#### Step 3 — Validate, save, invite, wait
|
|
392
|
+
|
|
393
|
+
1. Validate the token and save credentials:
|
|
394
|
+
```bash
|
|
395
|
+
ruby "SKILL_DIR/discord_setup.rb" --validate "<BOT_TOKEN>"
|
|
396
|
+
```
|
|
397
|
+
On success the script prints `{"bot_id":"...","username":"..."}` and the adapter starts.
|
|
398
|
+
|
|
399
|
+
2. Generate the invite URL using the application id from Step 2:
|
|
400
|
+
```bash
|
|
401
|
+
ruby "SKILL_DIR/discord_setup.rb" --invite-url "<APP_ID>"
|
|
402
|
+
```
|
|
403
|
+
Open it: `browser(action="navigate", url="<INVITE_URL>")`. Tell the user:
|
|
404
|
+
> Pick your server from the dropdown → **Continue** → **Authorize**. I'll detect when the bot joins.
|
|
405
|
+
>
|
|
406
|
+
> If the dropdown is empty, you don't have a server yet — open <https://discord.com/channels/@me>, click **Add a Server** (the **+** button on the left sidebar) → **Create My Own** → **For me and my friends** → name it → **Create**, then re-open the invite link.
|
|
407
|
+
|
|
408
|
+
3. Wait for the bot to join a guild (long-poll, 10 min timeout). Run with `timeout: 620`:
|
|
409
|
+
```bash
|
|
410
|
+
ruby "SKILL_DIR/discord_setup.rb" --watch-guild
|
|
411
|
+
```
|
|
412
|
+
On exit 0: "✅ Discord channel configured! Bot is in `<guild_name>`. Mention it or DM it from any channel."
|
|
413
|
+
On timeout: offer to re-open the invite URL — the bot token stays valid.
|
|
414
|
+
|
|
415
|
+
### Telegram setup (Bot API)
|
|
416
|
+
|
|
417
|
+
Telegram setup is by far the simplest — no browser automation, no QR. The user creates a bot via @BotFather and pastes the token here.
|
|
418
|
+
|
|
419
|
+
#### Step 1 — Create a bot via @BotFather
|
|
420
|
+
|
|
421
|
+
Tell the user:
|
|
422
|
+
|
|
423
|
+
> Open Telegram and start a chat with **@BotFather** (https://t.me/BotFather). Send `/newbot`, choose a display name and a username ending in `bot`. BotFather will reply with an HTTP API token that looks like `123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ`. Paste the token here.
|
|
424
|
+
>
|
|
425
|
+
> Optional: if your network blocks `api.telegram.org`, also tell me the base URL of your self-hosted Bot API server (e.g. `https://my-tg-proxy.example.com`). Otherwise leave it blank.
|
|
426
|
+
|
|
427
|
+
Wait for the user's reply. Parse the token (matches `^\d+:[\w-]{30,}$`).
|
|
428
|
+
|
|
429
|
+
#### Step 2 — Save credentials and validate
|
|
430
|
+
|
|
431
|
+
Call the server API. It calls `getMe` against the Bot API to validate the token before persisting:
|
|
432
|
+
|
|
433
|
+
```bash
|
|
434
|
+
curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/channels/telegram \
|
|
435
|
+
-H "Content-Type: application/json" \
|
|
436
|
+
-d '{"bot_token":"<TOKEN>","base_url":"<BASE_URL_OR_OMIT>"}'
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
- `200 { "ok": true }` — token validated and saved. The adapter starts long-polling immediately.
|
|
440
|
+
- `422 { "ok": false, "error": "..." }` — show the error (commonly "Unauthorized" → wrong token) and offer to retry.
|
|
441
|
+
|
|
442
|
+
On success:
|
|
443
|
+
|
|
444
|
+
> ✅ Telegram channel configured. Open your bot in Telegram and send any message to start chatting. In group chats, the bot only replies when you @-mention it.
|
|
445
|
+
|
|
446
|
+
#### Notes
|
|
447
|
+
|
|
448
|
+
- **Group chats**: Telegram bots respond only when @-mentioned or directly replied to (matches Feishu behaviour). For unrestricted reading in groups, the bot owner must disable "Group Privacy" in @BotFather (`/mybots → Bot Settings → Group Privacy → Turn off`), but `@-mention only` is recommended to avoid spam.
|
|
449
|
+
- **Self-hosted Bot API**: set `base_url` when `api.telegram.org` is unreachable. See https://github.com/tdlib/telegram-bot-api for the official self-hosted server.
|
|
450
|
+
- **`allowed_users`**: restrict which Telegram user IDs the bot will respond to. Find a user's numeric ID by messaging @userinfobot.
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
293
454
|
## `enable`
|
|
294
455
|
|
|
295
456
|
Call the server API to re-enable the platform (this reads from disk, sets enabled, saves, and hot-reloads):
|
|
@@ -337,15 +498,30 @@ Check each item, report ✅ / ❌ with remediation:
|
|
|
337
498
|
- Feishu: `app_id`, `app_secret` present and non-empty
|
|
338
499
|
- WeCom: `bot_id`, `secret` present and non-empty
|
|
339
500
|
- Weixin: `token` present and non-empty in `channels.yml`
|
|
501
|
+
- Discord: `bot_token` present and non-empty in `channels.yml`
|
|
502
|
+
- Telegram: `bot_token` present and non-empty
|
|
340
503
|
3. **Feishu credentials** (if enabled) — run the token API call, check `code=0`.
|
|
341
504
|
4. **Weixin token** (if enabled) — call `GET /api/channels` and check `has_token: true` for the weixin entry.
|
|
342
|
-
5. **
|
|
505
|
+
5. **Telegram credentials** (if enabled) — call `getMe` against the Bot API:
|
|
506
|
+
```bash
|
|
507
|
+
BOT_TOKEN=$(ruby -ryaml -e 'puts (YAML.load_file(File.expand_path("~/.clacky/channels.yml"))["channels"]["telegram"]["bot_token"] rescue "")')
|
|
508
|
+
BASE_URL=$(ruby -ryaml -e 'puts (YAML.load_file(File.expand_path("~/.clacky/channels.yml"))["channels"]["telegram"]["base_url"] || "https://api.telegram.org" rescue "https://api.telegram.org")')
|
|
509
|
+
curl -s "$BASE_URL/bot$BOT_TOKEN/getMe" | grep -q '"ok":true' && echo "✅ Telegram OK" || echo "❌ Telegram credentials rejected by getMe"
|
|
510
|
+
```
|
|
511
|
+
6. **WeCom credentials** (if enabled) — search today's log:
|
|
343
512
|
```bash
|
|
344
513
|
grep -iE "wecom adapter loop started|WeCom authentication failed|WeCom WS error response|WecomAdapter" \
|
|
345
514
|
~/.clacky/logger/clacky-$(date +%Y-%m-%d).log
|
|
346
515
|
```
|
|
347
516
|
- `WeCom authentication failed` or non-zero errcode → ❌ "WeCom credentials incorrect"
|
|
348
517
|
- `adapter loop started` with no auth error → ✅
|
|
518
|
+
6. **Discord credentials** (if enabled) — call `GET /api/channels` and check `has_token: true`. Search today's log:
|
|
519
|
+
```bash
|
|
520
|
+
grep -iE "DiscordAdapter|discord-gateway|/users/@me failed" \
|
|
521
|
+
~/.clacky/logger/clacky-$(date +%Y-%m-%d).log
|
|
522
|
+
```
|
|
523
|
+
- `/users/@me failed` → ❌ "Discord token invalid or revoked — re-run setup"
|
|
524
|
+
- `authenticated as` with no error → ✅
|
|
349
525
|
|
|
350
526
|
---
|
|
351
527
|
|
|
@@ -356,7 +532,7 @@ Proactively send a message to a user via an IM channel adapter.
|
|
|
356
532
|
### Parse the request
|
|
357
533
|
|
|
358
534
|
Extract two things from the user's instruction:
|
|
359
|
-
- **platform** — one of `weixin`, `feishu`, `wecom`
|
|
535
|
+
- **platform** — one of `weixin`, `feishu`, `wecom`, `discord`, `telegram`
|
|
360
536
|
- **message** — the text content to send
|
|
361
537
|
|
|
362
538
|
If the platform cannot be inferred, ask the user to clarify.
|
|
@@ -396,7 +572,7 @@ curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/channels/
|
|
|
396
572
|
### Constraints & notes
|
|
397
573
|
|
|
398
574
|
- **Weixin (iLink protocol)**: Every outbound message requires a `context_token` that is obtained from the most recent inbound message from that user. The token is cached in memory and reset on server restart. If the server was restarted since the user last wrote, the token is gone and the send will fail — the user must message the bot again.
|
|
399
|
-
- **Feishu / WeCom**: No token required. As long as the adapter is running and the `user_id` / `chat_id` is valid, the message will be delivered.
|
|
575
|
+
- **Feishu / WeCom / Discord / Telegram**: No per-message token required. As long as the adapter is running and the `user_id` / `chat_id` (or Discord channel/user id) is valid, the message will be delivered. For Telegram specifically, the `user_id` must be a Telegram chat_id that the bot can write to — the user must have sent at least one message to the bot first.
|
|
400
576
|
- This feature is intended for **proactive notifications** (e.g. task completion, reminders). It is not a replacement for the normal reply flow triggered by inbound messages.
|
|
401
577
|
|
|
402
578
|
---
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# discord_setup.rb — Discord channel setup helper.
|
|
5
|
+
#
|
|
6
|
+
# Discord's developer portal requires manual interaction (hCaptcha + private API), so the
|
|
7
|
+
# Agent uses the browser only as a container — it navigates to the portal and the user
|
|
8
|
+
# creates the App manually, then pastes back the bot token and application id. This
|
|
9
|
+
# script handles everything a shell can do: emit the portal URL, validate the token
|
|
10
|
+
# against /users/@me, save to the clacky server, generate the OAuth2 invite URL, and
|
|
11
|
+
# poll until the bot is in at least one guild.
|
|
12
|
+
#
|
|
13
|
+
# Modes:
|
|
14
|
+
# --portal-url Print the Discord developer portal URL (stdout, single line)
|
|
15
|
+
# --validate <token> Validate bot_token via /users/@me, then POST to server
|
|
16
|
+
# --invite-url <client_id> Print the OAuth2 invite URL (stdout, single line)
|
|
17
|
+
# --watch-guild Long-poll /users/@me/guilds via the saved token
|
|
18
|
+
# until at least one guild appears (or timeout)
|
|
19
|
+
# --bot-info <token> Print {id, username} JSON for an unsaved token
|
|
20
|
+
#
|
|
21
|
+
# Environment:
|
|
22
|
+
# CLACKY_SERVER_HOST default 127.0.0.1
|
|
23
|
+
# CLACKY_SERVER_PORT default 7070
|
|
24
|
+
|
|
25
|
+
require "json"
|
|
26
|
+
require "net/http"
|
|
27
|
+
require "net/https"
|
|
28
|
+
require "uri"
|
|
29
|
+
require "openssl"
|
|
30
|
+
require "cgi"
|
|
31
|
+
require "yaml"
|
|
32
|
+
|
|
33
|
+
DISCORD_API_BASE = "https://discord.com/api/v10"
|
|
34
|
+
DISCORD_OAUTH_BASE = "https://discord.com/oauth2/authorize"
|
|
35
|
+
DISCORD_PORTAL_URL = "https://discord.com/developers/applications"
|
|
36
|
+
DEFAULT_BOT_PERMS = "274877990912"
|
|
37
|
+
DEFAULT_BOT_SCOPES = "bot applications.commands"
|
|
38
|
+
WATCH_GUILD_DEADLINE = 10 * 60
|
|
39
|
+
WATCH_GUILD_INTERVAL = 3
|
|
40
|
+
USER_AGENT = "DiscordBot (https://github.com/clackyai/openclacky, 1.0)"
|
|
41
|
+
|
|
42
|
+
CLACKY_SERVER_URL = begin
|
|
43
|
+
host = ENV.fetch("CLACKY_SERVER_HOST", "127.0.0.1")
|
|
44
|
+
port = ENV.fetch("CLACKY_SERVER_PORT", "7070")
|
|
45
|
+
"http://#{host}:#{port}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def step(msg); $stderr.puts("[discord-setup] #{msg}"); end
|
|
49
|
+
def ok(msg); $stderr.puts("[discord-setup] #{msg}"); end
|
|
50
|
+
def warn!(msg); $stderr.puts("[discord-setup] #{msg}"); end
|
|
51
|
+
|
|
52
|
+
def fail!(msg, json: false)
|
|
53
|
+
if json
|
|
54
|
+
$stdout.puts(JSON.generate({ error: msg }))
|
|
55
|
+
else
|
|
56
|
+
$stderr.puts("[discord-setup] #{msg}")
|
|
57
|
+
end
|
|
58
|
+
exit 1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def discord_get(path, bot_token:, timeout: 15)
|
|
62
|
+
uri = URI("#{DISCORD_API_BASE}#{path}")
|
|
63
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
64
|
+
http.use_ssl = true
|
|
65
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
66
|
+
http.read_timeout = timeout
|
|
67
|
+
http.open_timeout = 10
|
|
68
|
+
|
|
69
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
|
70
|
+
req["Authorization"] = "Bot #{bot_token}"
|
|
71
|
+
req["User-Agent"] = USER_AGENT
|
|
72
|
+
req["Accept"] = "application/json"
|
|
73
|
+
|
|
74
|
+
res = http.request(req)
|
|
75
|
+
body = res.body.to_s
|
|
76
|
+
parsed = (JSON.parse(body) rescue nil)
|
|
77
|
+
|
|
78
|
+
unless res.is_a?(Net::HTTPSuccess)
|
|
79
|
+
msg = parsed.is_a?(Hash) ? (parsed["message"] || body.slice(0, 200)) : body.slice(0, 200)
|
|
80
|
+
raise "Discord HTTP #{res.code} #{path}: #{msg}"
|
|
81
|
+
end
|
|
82
|
+
parsed
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def saved_bot_token
|
|
86
|
+
yml_path = File.expand_path("~/.clacky/channels.yml")
|
|
87
|
+
return nil unless File.exist?(yml_path)
|
|
88
|
+
data = YAML.safe_load_file(yml_path, permitted_classes: [Symbol], aliases: true) rescue nil
|
|
89
|
+
data&.dig("channels", "discord", "bot_token") || data&.dig(:channels, :discord, :bot_token)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def save_to_server(bot_token:)
|
|
93
|
+
uri = URI("#{CLACKY_SERVER_URL}/api/channels/discord")
|
|
94
|
+
body = JSON.generate({ bot_token: bot_token })
|
|
95
|
+
|
|
96
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
97
|
+
http.read_timeout = 30
|
|
98
|
+
http.open_timeout = 5
|
|
99
|
+
|
|
100
|
+
req = Net::HTTP::Post.new(uri.path, "Content-Type" => "application/json")
|
|
101
|
+
req.body = body
|
|
102
|
+
|
|
103
|
+
res = http.request(req)
|
|
104
|
+
data = JSON.parse(res.body) rescue {}
|
|
105
|
+
|
|
106
|
+
unless res.is_a?(Net::HTTPSuccess) && data["ok"]
|
|
107
|
+
fail!("Failed to save Discord config: #{data["error"] || res.body.slice(0, 200)}")
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
mode_idx = ARGV.index { |a| a.start_with?("--") }
|
|
112
|
+
mode = mode_idx ? ARGV[mode_idx] : nil
|
|
113
|
+
arg = mode_idx ? ARGV[mode_idx + 1] : nil
|
|
114
|
+
|
|
115
|
+
case mode
|
|
116
|
+
when "--portal-url"
|
|
117
|
+
$stdout.puts(DISCORD_PORTAL_URL)
|
|
118
|
+
exit 0
|
|
119
|
+
|
|
120
|
+
when "--validate"
|
|
121
|
+
fail!("--validate requires <bot_token>") if arg.to_s.strip.empty?
|
|
122
|
+
bot_token = arg.strip
|
|
123
|
+
step("Validating bot token against Discord API...")
|
|
124
|
+
begin
|
|
125
|
+
me = discord_get("/users/@me", bot_token: bot_token)
|
|
126
|
+
rescue => e
|
|
127
|
+
fail!("Token validation failed: #{e.message}")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
bot_id = me["id"].to_s
|
|
131
|
+
username = me["username"].to_s
|
|
132
|
+
fail!("Empty bot id from /users/@me") if bot_id.empty?
|
|
133
|
+
|
|
134
|
+
ok("Authenticated as #{username} (id=#{bot_id})")
|
|
135
|
+
step("Saving credentials via clacky server...")
|
|
136
|
+
save_to_server(bot_token: bot_token)
|
|
137
|
+
ok("Discord channel configured")
|
|
138
|
+
|
|
139
|
+
$stdout.puts(JSON.generate({ bot_id: bot_id, username: username }))
|
|
140
|
+
exit 0
|
|
141
|
+
|
|
142
|
+
when "--bot-info"
|
|
143
|
+
fail!("--bot-info requires <bot_token>", json: true) if arg.to_s.strip.empty?
|
|
144
|
+
begin
|
|
145
|
+
me = discord_get("/users/@me", bot_token: arg.strip)
|
|
146
|
+
rescue => e
|
|
147
|
+
fail!(e.message, json: true)
|
|
148
|
+
end
|
|
149
|
+
$stdout.puts(JSON.generate({ bot_id: me["id"], username: me["username"] }))
|
|
150
|
+
exit 0
|
|
151
|
+
|
|
152
|
+
when "--invite-url"
|
|
153
|
+
fail!("--invite-url requires <client_id>") if arg.to_s.strip.empty?
|
|
154
|
+
client_id = arg.strip
|
|
155
|
+
url = "#{DISCORD_OAUTH_BASE}?client_id=#{CGI.escape(client_id)}" \
|
|
156
|
+
"&permissions=#{DEFAULT_BOT_PERMS}" \
|
|
157
|
+
"&scope=#{CGI.escape(DEFAULT_BOT_SCOPES)}"
|
|
158
|
+
$stdout.puts(url)
|
|
159
|
+
exit 0
|
|
160
|
+
|
|
161
|
+
when "--watch-guild"
|
|
162
|
+
bot_token = saved_bot_token
|
|
163
|
+
fail!("No saved bot_token in ~/.clacky/channels.yml — run --validate first") if bot_token.to_s.empty?
|
|
164
|
+
|
|
165
|
+
step("Waiting for the bot to be added to a guild (timeout: #{WATCH_GUILD_DEADLINE / 60} min)...")
|
|
166
|
+
deadline = Time.now + WATCH_GUILD_DEADLINE
|
|
167
|
+
|
|
168
|
+
loop do
|
|
169
|
+
fail!("Timed out waiting for the bot to join a guild. Open the invite URL again to retry.") if Time.now > deadline
|
|
170
|
+
|
|
171
|
+
begin
|
|
172
|
+
guilds = discord_get("/users/@me/guilds", bot_token: bot_token)
|
|
173
|
+
rescue => e
|
|
174
|
+
warn!("Poll error (will retry): #{e.message}")
|
|
175
|
+
sleep WATCH_GUILD_INTERVAL
|
|
176
|
+
next
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
if guilds.is_a?(Array) && !guilds.empty?
|
|
180
|
+
g = guilds.first
|
|
181
|
+
ok("Bot added to guild: #{g["name"]} (id=#{g["id"]})")
|
|
182
|
+
$stdout.puts(JSON.generate({ guild_id: g["id"], guild_name: g["name"], total: guilds.length }))
|
|
183
|
+
exit 0
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
sleep WATCH_GUILD_INTERVAL
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
else
|
|
190
|
+
$stderr.puts(<<~USAGE)
|
|
191
|
+
Usage:
|
|
192
|
+
ruby discord_setup.rb --portal-url
|
|
193
|
+
ruby discord_setup.rb --validate <bot_token>
|
|
194
|
+
ruby discord_setup.rb --bot-info <bot_token>
|
|
195
|
+
ruby discord_setup.rb --invite-url <client_id>
|
|
196
|
+
ruby discord_setup.rb --watch-guild
|
|
197
|
+
USAGE
|
|
198
|
+
exit 1
|
|
199
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
|
|
6
|
+
# Import lark-cli's official Skills from ~/.agents/skills/lark-* into
|
|
7
|
+
# ~/.clacky/skills/lark-imports/<name>/.
|
|
8
|
+
#
|
|
9
|
+
# Background:
|
|
10
|
+
# lark-cli ships ~24 SKILL.md files (lark-doc, lark-sheets, lark-base, ...)
|
|
11
|
+
# that teach the agent how to use `lark-cli`. They are normally installed
|
|
12
|
+
# under ~/.agents/skills/lark-*, which openclacky's SkillLoader does NOT
|
|
13
|
+
# scan. This importer copies them into ~/.clacky/skills/lark-imports/ so
|
|
14
|
+
# they become discoverable via the standard skill description-matching
|
|
15
|
+
# mechanism.
|
|
16
|
+
#
|
|
17
|
+
# This is intentionally a small, dedicated importer (not a generic external
|
|
18
|
+
# skills tool) — it only handles the lark-cli case for the feishu channel
|
|
19
|
+
# setup flow. Failures are non-fatal: the bot itself remains functional even
|
|
20
|
+
# if Skills cannot be exposed.
|
|
21
|
+
#
|
|
22
|
+
# Usage:
|
|
23
|
+
# importer = Clacky::ChannelSetup::LarkSkillsImporter.new
|
|
24
|
+
# result = importer.run
|
|
25
|
+
# # result => { copied: 24, skipped: 0, errors: [] }
|
|
26
|
+
|
|
27
|
+
module Clacky
|
|
28
|
+
module ChannelSetup
|
|
29
|
+
class LarkSkillsImporter
|
|
30
|
+
DEFAULT_SOURCE_DIR = File.join(Dir.home, '.agents', 'skills')
|
|
31
|
+
DEFAULT_TARGET_DIR = File.join(Dir.home, '.clacky', 'skills', 'lark-imports')
|
|
32
|
+
SKILL_PREFIX = 'lark-'
|
|
33
|
+
|
|
34
|
+
# @param source_dir [String] directory containing lark-cli installed skills
|
|
35
|
+
# @param target_dir [String] destination under ~/.clacky/skills/
|
|
36
|
+
def initialize(source_dir: DEFAULT_SOURCE_DIR, target_dir: DEFAULT_TARGET_DIR)
|
|
37
|
+
@source_dir = Pathname.new(source_dir).expand_path
|
|
38
|
+
@target_dir = Pathname.new(target_dir).expand_path
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Run the import. Returns a result hash; never raises on per-skill errors.
|
|
42
|
+
# @return [Hash] { copied: Integer, skipped: Integer, errors: Array<String> }
|
|
43
|
+
def run
|
|
44
|
+
return { copied: 0, skipped: 0, errors: ["source not found: #{@source_dir}"] } unless @source_dir.directory?
|
|
45
|
+
|
|
46
|
+
skill_dirs = discover_lark_skills
|
|
47
|
+
return { copied: 0, skipped: 0, errors: [] } if skill_dirs.empty?
|
|
48
|
+
|
|
49
|
+
FileUtils.mkdir_p(@target_dir)
|
|
50
|
+
|
|
51
|
+
copied = 0
|
|
52
|
+
errors = []
|
|
53
|
+
skill_dirs.each do |src|
|
|
54
|
+
begin
|
|
55
|
+
copy_skill(src)
|
|
56
|
+
copied += 1
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
errors << "#{src.basename}: #{e.message}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
{ copied: copied, skipped: 0, errors: errors }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Discover candidate lark-* skill directories under @source_dir.
|
|
66
|
+
# A directory qualifies when it (a) starts with "lark-" and (b) contains a SKILL.md.
|
|
67
|
+
# @return [Array<Pathname>]
|
|
68
|
+
private def discover_lark_skills
|
|
69
|
+
@source_dir.children
|
|
70
|
+
.select { |p| p.directory? && p.basename.to_s.start_with?(SKILL_PREFIX) }
|
|
71
|
+
.select { |p| p.join('SKILL.md').exist? }
|
|
72
|
+
.sort_by { |p| p.basename.to_s }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Copy a single skill directory into @target_dir, replacing any existing copy
|
|
76
|
+
# so re-runs always reflect the latest version.
|
|
77
|
+
# @param src [Pathname]
|
|
78
|
+
private def copy_skill(src)
|
|
79
|
+
dst = @target_dir.join(src.basename.to_s)
|
|
80
|
+
FileUtils.rm_rf(dst) if dst.exist?
|
|
81
|
+
FileUtils.mkdir_p(dst)
|
|
82
|
+
src.children.each { |child| FileUtils.cp_r(child, dst) }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# CLI entry point — invoked by SKILL.md after the user opts in to lark-cli.
|
|
89
|
+
# Usage:
|
|
90
|
+
# ruby import_lark_skills.rb
|
|
91
|
+
# Prints a one-line summary; exits 0 even when nothing to copy (treat empty
|
|
92
|
+
# source as a soft skip — the script may run before `npx skills add`).
|
|
93
|
+
if $PROGRAM_NAME == __FILE__
|
|
94
|
+
result = Clacky::ChannelSetup::LarkSkillsImporter.new.run
|
|
95
|
+
puts "[lark-import] copied=#{result[:copied]} errors=#{result[:errors].size}"
|
|
96
|
+
result[:errors].each { |e| warn "[lark-import] #{e}" }
|
|
97
|
+
end
|
|
@@ -55,7 +55,7 @@ Example (English):
|
|
|
55
55
|
> Let's take 30 seconds to personalize your experience — I'll ask just a couple of quick things.
|
|
56
56
|
|
|
57
57
|
Example (Chinese):
|
|
58
|
-
>
|
|
58
|
+
> 嗨!我是你的专属员工一号
|
|
59
59
|
> 只需 30 秒完成个性化设置,我会问你两个简单问题。
|
|
60
60
|
|
|
61
61
|
### A.3. Ask the user to name the AI (card)
|