openclacky 0.7.9 → 0.8.0
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/.clacky/skills/gem-release/SKILL.md +33 -4
- data/CHANGELOG.md +22 -0
- data/docs/openclacky_cloud_api_reference.md +584 -0
- data/lib/clacky/agent/session_serializer.rb +2 -1
- data/lib/clacky/agent/skill_manager.rb +3 -1
- data/lib/clacky/agent/tool_executor.rb +10 -1
- data/lib/clacky/agent.rb +6 -0
- data/lib/clacky/agent_config.rb +9 -9
- data/lib/clacky/brand_config.rb +444 -0
- data/lib/clacky/cli.rb +100 -1
- data/lib/clacky/default_skills/onboard/SKILL.md +5 -0
- data/lib/clacky/server/http_server.rb +243 -8
- data/lib/clacky/server/scheduler.rb +1 -3
- data/lib/clacky/server/web_ui_controller.rb +3 -0
- data/lib/clacky/tools/browser.rb +212 -0
- data/lib/clacky/tools/file_reader.rb +13 -2
- data/lib/clacky/tools/glob.rb +3 -3
- data/lib/clacky/tools/grep.rb +4 -3
- data/lib/clacky/tools/run_project.rb +4 -2
- data/lib/clacky/tools/safe_shell.rb +7 -7
- data/lib/clacky/tools/shell.rb +6 -2
- data/lib/clacky/tools/trash_manager.rb +2 -2
- data/lib/clacky/ui2/components/welcome_banner.rb +68 -18
- data/lib/clacky/utils/file_ignore_helper.rb +1 -1
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +303 -0
- data/lib/clacky/web/app.js +19 -16
- data/lib/clacky/web/brand.js +157 -0
- data/lib/clacky/web/index.html +82 -9
- data/lib/clacky/web/sessions.js +157 -15
- data/lib/clacky/web/settings.js +131 -1
- data/lib/clacky/web/skills.js +162 -5
- data/lib/clacky.rb +1 -0
- data/scripts/install.sh +106 -1
- metadata +19 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 847578e565b36373d9941ed108df7b68a78a3b485681f22f606637f90f3f6a49
|
|
4
|
+
data.tar.gz: 638855b903c61169486422f5d791a3eb7a7b4751cdee5a3b757abaf4fdd32ff2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 981a682c39c603d101c7755e3bdeecf053519e2eb041120311a4877c2aac9370bbc24cf57ead31595636592eb75513c6844faf9418cf378bb1c767e506b560e0
|
|
7
|
+
data.tar.gz: f7cbdcadf728ddc1825d9f11827f3dc8843981005a59cb5851391580d3f91e25b7f2f32730b928702ea3ad638c54e7c2f2134d0d16d47173dd37816d51e290e5
|
|
@@ -190,10 +190,38 @@ To use this skill, simply say:
|
|
|
190
190
|
- Commit CHANGELOG.md changes
|
|
191
191
|
- Push to remote repository
|
|
192
192
|
|
|
193
|
-
### 7. Final
|
|
194
|
-
|
|
195
|
-
-
|
|
196
|
-
|
|
193
|
+
### 7. Final Summary
|
|
194
|
+
|
|
195
|
+
Present a clear, user-facing release summary after all steps complete:
|
|
196
|
+
|
|
197
|
+
**Format:**
|
|
198
|
+
```
|
|
199
|
+
🎉 v{version} released successfully!
|
|
200
|
+
|
|
201
|
+
📦 What's new for users:
|
|
202
|
+
|
|
203
|
+
**New Features**
|
|
204
|
+
- [translate each "Added" item into plain user-facing language]
|
|
205
|
+
|
|
206
|
+
**Improvements**
|
|
207
|
+
- [translate each "Improved" item into plain user-facing language]
|
|
208
|
+
|
|
209
|
+
**Bug Fixes**
|
|
210
|
+
- [translate each "Fixed" item into plain user-facing language]
|
|
211
|
+
|
|
212
|
+
🔗 Links:
|
|
213
|
+
- RubyGems: https://rubygems.org/gems/openclacky/versions/{version}
|
|
214
|
+
- GitHub Release: https://github.com/clacky-ai/open-clacky/releases/tag/v{version}
|
|
215
|
+
|
|
216
|
+
Install/upgrade: gem install openclacky
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Rules for writing the summary:**
|
|
220
|
+
- Write from the user's perspective — what can they now do, or what problem is now fixed
|
|
221
|
+
- Avoid technical jargon (no "cursor-paginated", "frontmatter", "REST API" — explain what it means instead)
|
|
222
|
+
- Skip "More" / chore items unless they directly affect users
|
|
223
|
+
- Keep each bullet to one sentence, action-oriented
|
|
224
|
+
- Example translation: `fix: expand ~ in file system tools path arguments` → "File paths starting with `~` (home directory) now work correctly in all file tools"
|
|
197
225
|
|
|
198
226
|
## Commands Used
|
|
199
227
|
|
|
@@ -248,6 +276,7 @@ gh release create vX.Y.Z \
|
|
|
248
276
|
- CHANGELOG.md updated with release notes
|
|
249
277
|
- GitHub Release created and visible at https://github.com/clacky-ai/open-clacky/releases
|
|
250
278
|
- No build or deployment errors
|
|
279
|
+
- User-facing release summary presented at the end
|
|
251
280
|
|
|
252
281
|
## Error Handling
|
|
253
282
|
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.0] - 2026-03-06
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- White-label brand licensing system: customize the web UI with your own name, logo, colors, and skills via `brand_config.yml`
|
|
14
|
+
- Brand skills tab in the web UI with private badge, shown only when brand skills are configured
|
|
15
|
+
- Slash command prompt rule: skill invocations (e.g. `/skill-name`) are now expanded inside the agent at run time, enabling mid-session skill triggering
|
|
16
|
+
|
|
17
|
+
### Improved
|
|
18
|
+
- Server-side brand name rendering eliminates the first-paint brand name flash in the web UI
|
|
19
|
+
- Collapsible tool call blocks in the web UI — long tool outputs are now grouped and collapsed by default
|
|
20
|
+
- `safe_shell` now catches `ArgumentError` in addition to `BadQuotedString` for more robust command parsing
|
|
21
|
+
- Eliminated `Dir.chdir` global state in session handling, fixing race conditions in concurrent sessions
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- Skill slash commands are now expanded inside `agent.run` so that `/onboard` and similar commands work correctly when triggered mid-session
|
|
25
|
+
- Observer state machine handles `awaiting` state transitions properly
|
|
26
|
+
|
|
27
|
+
### More
|
|
28
|
+
- Disabled ClaudeCode `ANTHROPIC_API_KEY` environment variable fallback in `AgentConfig` for cleaner env isolation
|
|
29
|
+
- Updated gemspec, lockfile, and install script
|
|
30
|
+
- Added web asset syntax specs and brand config specs
|
|
31
|
+
|
|
10
32
|
## [0.7.9] - 2026-03-07
|
|
11
33
|
|
|
12
34
|
### Added
|
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
# OpenClacky License API — 接口文档
|
|
2
|
+
|
|
3
|
+
**Base URL**: `https://your-platform.com`
|
|
4
|
+
**Content-Type**: `application/json`
|
|
5
|
+
**协议说明**: License Key **全程不通过网络传输**,所有认证均基于 HMAC-SHA256 知识证明。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 接口目录
|
|
10
|
+
|
|
11
|
+
| # | 接口名称 | 方法 | 路径 | 说明 |
|
|
12
|
+
|---|---------|------|------|------|
|
|
13
|
+
| 1 | [激活 License](#1-激活-license) | POST | `/api/v1/licenses/activate` | 首次激活,绑定设备 |
|
|
14
|
+
| 2 | [获取 Skills 列表](#2-获取-skills-列表) | POST | `/api/v1/licenses/skills` | 查询许可范围内的 Skill |
|
|
15
|
+
| 3 | [心跳检测](#4-心跳检测) | POST | `/api/v1/licenses/heartbeat` | 定期验证许可有效性 |
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 通用错误码
|
|
20
|
+
|
|
21
|
+
所有接口均使用统一的错误响应格式:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"status": "error",
|
|
26
|
+
"code": "<错误码>"
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| HTTP 状态码 | code | 说明 |
|
|
31
|
+
|------------|------|------|
|
|
32
|
+
| 400 | `missing_params` | 缺少必填参数 |
|
|
33
|
+
| 401 | `invalid_proof` | 激活证明验证失败 |
|
|
34
|
+
| 401 | `invalid_signature` | 请求签名验证失败 |
|
|
35
|
+
| 401 | `nonce_replayed` | Nonce 重放攻击,请求已被拒绝 |
|
|
36
|
+
| 401 | `timestamp_expired` | 时间戳超出允许范围(±5 分钟) |
|
|
37
|
+
| 401 | `user_id_mismatch` | user_id 与 License 不匹配 |
|
|
38
|
+
| 401 | `device_revoked` | 设备已被撤销(heartbeat 专用) |
|
|
39
|
+
| 403 | `license_revoked` | License 已被撤销 |
|
|
40
|
+
| 403 | `license_expired` | License 已过期 |
|
|
41
|
+
| 403 | `device_revoked` | 设备已被撤销 |
|
|
42
|
+
| 403 | `device_limit_reached` | 已达到设备数量上限 |
|
|
43
|
+
| 403 | `invalid_status` | License 状态不允许操作 |
|
|
44
|
+
| 404 | `invalid_license` | License 不存在 |
|
|
45
|
+
| 404 | `device_not_found` | 设备未激活,请先调用激活接口 |
|
|
46
|
+
| 429 | `rate_limited` | 请求过于频繁,请稍后重试 |
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 1. 激活 License
|
|
51
|
+
|
|
52
|
+
**POST** `/api/v1/licenses/activate`
|
|
53
|
+
|
|
54
|
+
首次在设备上激活 License。使用 HMAC 知识证明,License Key 本身不发送到服务器。
|
|
55
|
+
激活成功后,服务器返回许可范围内的 Skills 列表及有效期。
|
|
56
|
+
|
|
57
|
+
> 同一设备重复激活幂等安全(不会消耗新的设备配额)。
|
|
58
|
+
|
|
59
|
+
### 请求参数
|
|
60
|
+
|
|
61
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
62
|
+
|------|------|------|------|
|
|
63
|
+
| `key_hash` | string | 是 | `SHA256(license_key)` 的 hex 字符串(64 字符),用于服务器查找 License |
|
|
64
|
+
| `user_id` | string | 是 | 从 License Key 结构中提取的用户 ID(十进制字符串) |
|
|
65
|
+
| `device_id` | string | 是 | 设备唯一标识符(建议 32 字符 hex,参见设备 ID 算法) |
|
|
66
|
+
| `timestamp` | string | 是 | 当前 Unix 时间戳(秒,字符串格式),需与服务器时间误差 ≤ 5 分钟 |
|
|
67
|
+
| `nonce` | string | 是 | 32 字符随机 hex,每次请求必须唯一,10 分钟内不可重用 |
|
|
68
|
+
| `proof` | string | 是 | HMAC-SHA256 激活证明(64 字符 hex),计算方式见下方 |
|
|
69
|
+
| `device_info` | object | 否 | 设备元数据(如 OS、版本号等),仅用于管理展示 |
|
|
70
|
+
|
|
71
|
+
**proof 计算方式:**
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
message = "activate:{key_hash}:{user_id}:{device_id}:{timestamp}:{nonce}"
|
|
75
|
+
proof = HMAC-SHA256(license_key, message) // hex 编码
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 响应参数(成功 200)
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"status": "success",
|
|
83
|
+
"data": {
|
|
84
|
+
"status": "active",
|
|
85
|
+
"expires_at": "2027-01-01T00:00:00Z",
|
|
86
|
+
"device_id": "a1b2c3d4...",
|
|
87
|
+
"device_limit": 3,
|
|
88
|
+
"activated_devices": 1,
|
|
89
|
+
"skills": [
|
|
90
|
+
{
|
|
91
|
+
"id": 42,
|
|
92
|
+
"name": "Code Review Bot",
|
|
93
|
+
"version": "1.2.0",
|
|
94
|
+
"encrypted": false,
|
|
95
|
+
"checksum": "sha256hex..."
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
| 字段 | 类型 | 说明 |
|
|
103
|
+
|------|------|------|
|
|
104
|
+
| `data.status` | string | License 状态:`active` / `assigned` |
|
|
105
|
+
| `data.expires_at` | string | ISO 8601 过期时间,null 表示永久有效 |
|
|
106
|
+
| `data.device_id` | string | 当前设备 ID(回显) |
|
|
107
|
+
| `data.device_limit` | integer | 最大可激活设备数 |
|
|
108
|
+
| `data.activated_devices` | integer | 当前已激活设备数 |
|
|
109
|
+
| `data.skills` | array | License 授权的 Skill 列表(简要信息) |
|
|
110
|
+
| `data.skills[].id` | integer | Skill ID |
|
|
111
|
+
| `data.skills[].name` | string | Skill 名称 |
|
|
112
|
+
| `data.skills[].version` | string | 当前版本号 |
|
|
113
|
+
| `data.skills[].encrypted` | boolean | 是否加密 |
|
|
114
|
+
| `data.skills[].checksum` | string | 最新版本校验码 |
|
|
115
|
+
|
|
116
|
+
### curl 示例
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# 1. 本地计算(伪代码,实际由 SDK 完成)
|
|
120
|
+
KEY="0000002A-00000007-DEADBEEF-CAFEBABE-A1B2C3D4"
|
|
121
|
+
KEY_HASH=$(echo -n "$KEY" | sha256sum | awk '{print $1}')
|
|
122
|
+
DEVICE_ID="a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
|
|
123
|
+
TS=$(date +%s)
|
|
124
|
+
NONCE=$(openssl rand -hex 16)
|
|
125
|
+
MSG="activate:${KEY_HASH}:42:${DEVICE_ID}:${TS}:${NONCE}"
|
|
126
|
+
PROOF=$(echo -n "$MSG" | openssl dgst -sha256 -hmac "$KEY" | awk '{print $2}')
|
|
127
|
+
|
|
128
|
+
# 2. 发起请求
|
|
129
|
+
curl -s -X POST https://your-platform.com/api/v1/licenses/activate \
|
|
130
|
+
-H "Content-Type: application/json" \
|
|
131
|
+
-d "{
|
|
132
|
+
\"key_hash\": \"${KEY_HASH}\",
|
|
133
|
+
\"user_id\": \"42\",
|
|
134
|
+
\"device_id\": \"${DEVICE_ID}\",
|
|
135
|
+
\"timestamp\": \"${TS}\",
|
|
136
|
+
\"nonce\": \"${NONCE}\",
|
|
137
|
+
\"proof\": \"${PROOF}\",
|
|
138
|
+
\"device_info\": {\"os\": \"macOS\", \"version\": \"14.0\", \"app_version\": \"1.0.0\"}
|
|
139
|
+
}"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**成功响应示例:**
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"status": "success",
|
|
146
|
+
"data": {
|
|
147
|
+
"status": "active",
|
|
148
|
+
"expires_at": "2027-03-06T10:00:00Z",
|
|
149
|
+
"device_id": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
|
|
150
|
+
"device_limit": 3,
|
|
151
|
+
"activated_devices": 1,
|
|
152
|
+
"skills": [
|
|
153
|
+
{ "id": 42, "name": "Code Review Bot", "version": "1.2.0",
|
|
154
|
+
"encrypted": false, "checksum": "abcdef1234..." }
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 2. 获取 Skills 列表
|
|
163
|
+
|
|
164
|
+
**POST** `/api/v1/licenses/skills`
|
|
165
|
+
|
|
166
|
+
获取当前 License 授权范围内的全部 Skill,包含版本信息和下载地址。
|
|
167
|
+
支持按可见性(公开/私有)和关键词过滤。
|
|
168
|
+
|
|
169
|
+
### 请求参数
|
|
170
|
+
|
|
171
|
+
**认证参数(必填,同 heartbeat / check_version):**
|
|
172
|
+
|
|
173
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
174
|
+
|------|------|------|------|
|
|
175
|
+
| `user_id` | string | 是 | 从 License Key 提取的用户 ID(十进制字符串) |
|
|
176
|
+
| `device_id` | string | 是 | 已激活的设备 ID |
|
|
177
|
+
| `timestamp` | string | 是 | 当前 Unix 时间戳(秒),与服务器误差 ≤ 5 分钟 |
|
|
178
|
+
| `nonce` | string | 是 | 32 字符随机 hex,每次请求唯一 |
|
|
179
|
+
| `signature` | string | 是 | HMAC-SHA256 请求签名(64 字符 hex),计算方式见下方 |
|
|
180
|
+
|
|
181
|
+
**signature 计算方式:**
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
message = "{user_id}:{device_id}:{timestamp}:{nonce}"
|
|
185
|
+
signature = HMAC-SHA256(license_key, message) // hex 编码
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**过滤参数(可选):**
|
|
189
|
+
|
|
190
|
+
| 参数 | 类型 | 默认值 | 说明 |
|
|
191
|
+
|------|------|--------|------|
|
|
192
|
+
| `visibility` | string | `all` | `public`(公开)/ `private`(私有)/ `all`(全部) |
|
|
193
|
+
| `keyword` | string | — | 关键词,匹配 Skill 的 name 或 description(不区分大小写) |
|
|
194
|
+
|
|
195
|
+
### 响应参数(成功 200)
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"status": "success",
|
|
200
|
+
"total": 2,
|
|
201
|
+
"expires_at": "2027-01-01T00:00:00Z",
|
|
202
|
+
"skills": [
|
|
203
|
+
{
|
|
204
|
+
"id": 42,
|
|
205
|
+
"name": "Code Review Bot",
|
|
206
|
+
"slug": "code-review-bot",
|
|
207
|
+
"description": "Automated code review using AI",
|
|
208
|
+
"visibility": "public",
|
|
209
|
+
"version": "1.2.0",
|
|
210
|
+
"encrypted": false,
|
|
211
|
+
"emoji": null,
|
|
212
|
+
"download_count": 1024,
|
|
213
|
+
"latest_version": {
|
|
214
|
+
"version": "1.2.0",
|
|
215
|
+
"checksum": "a3f8b2c1d4e5...",
|
|
216
|
+
"release_notes": "Fix edge case in Python parsing",
|
|
217
|
+
"published_at": "2026-03-01T00:00:00Z",
|
|
218
|
+
"download_url": "https://bucket.s3.region.amazonaws.com/skills/abc.zip"
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
]
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
| 字段 | 类型 | 说明 |
|
|
226
|
+
|------|------|------|
|
|
227
|
+
| `total` | integer | 符合条件的 Skill 总数 |
|
|
228
|
+
| `expires_at` | string | License 过期时间(ISO 8601) |
|
|
229
|
+
| `skills[].id` | integer | Skill ID |
|
|
230
|
+
| `skills[].name` | string | Skill 名称 |
|
|
231
|
+
| `skills[].slug` | string | URL 友好标识符 |
|
|
232
|
+
| `skills[].description` | string | Skill 描述 |
|
|
233
|
+
| `skills[].visibility` | string | `public` / `private` |
|
|
234
|
+
| `skills[].version` | string | 当前版本号(SemVer) |
|
|
235
|
+
| `skills[].encrypted` | boolean | 是否加密 |
|
|
236
|
+
| `skills[].emoji` | string/null | 图标 Emoji |
|
|
237
|
+
| `skills[].download_count` | integer | 累计下载次数 |
|
|
238
|
+
| `skills[].latest_version` | object/null | 最新版本详情,无版本时为 null |
|
|
239
|
+
| `skills[].latest_version.version` | string | 版本号 |
|
|
240
|
+
| `skills[].latest_version.checksum` | string | 文件 SHA256 校验码 |
|
|
241
|
+
| `skills[].latest_version.release_notes` | string | 更新说明 |
|
|
242
|
+
| `skills[].latest_version.published_at` | string | 发布时间(ISO 8601) |
|
|
243
|
+
| `skills[].latest_version.download_url` | string/null | 文件下载直链(S3 公开 URL,无过期时间) |
|
|
244
|
+
|
|
245
|
+
### curl 示例
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
TS=$(date +%s)
|
|
249
|
+
NONCE=$(openssl rand -hex 16)
|
|
250
|
+
USER_ID="42"
|
|
251
|
+
DEVICE_ID="a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
|
|
252
|
+
KEY="0000002A-00000007-DEADBEEF-CAFEBABE-A1B2C3D4"
|
|
253
|
+
MSG="${USER_ID}:${DEVICE_ID}:${TS}:${NONCE}"
|
|
254
|
+
SIG=$(echo -n "$MSG" | openssl dgst -sha256 -hmac "$KEY" | awk '{print $2}')
|
|
255
|
+
|
|
256
|
+
# 获取全部 Skills
|
|
257
|
+
curl -s -X POST https://your-platform.com/api/v1/licenses/skills \
|
|
258
|
+
-H "Content-Type: application/json" \
|
|
259
|
+
-d "{
|
|
260
|
+
\"user_id\": \"${USER_ID}\",
|
|
261
|
+
\"device_id\": \"${DEVICE_ID}\",
|
|
262
|
+
\"timestamp\": \"${TS}\",
|
|
263
|
+
\"nonce\": \"${NONCE}\",
|
|
264
|
+
\"signature\": \"${SIG}\"
|
|
265
|
+
}"
|
|
266
|
+
|
|
267
|
+
# 只看公开 Skills,且名称包含 "code"
|
|
268
|
+
curl -s -X POST https://your-platform.com/api/v1/licenses/skills \
|
|
269
|
+
-H "Content-Type: application/json" \
|
|
270
|
+
-d "{
|
|
271
|
+
\"user_id\": \"${USER_ID}\",
|
|
272
|
+
\"device_id\": \"${DEVICE_ID}\",
|
|
273
|
+
\"timestamp\": \"${TS}\",
|
|
274
|
+
\"nonce\": \"${NONCE}\",
|
|
275
|
+
\"signature\": \"${SIG}\",
|
|
276
|
+
\"visibility\": \"public\",
|
|
277
|
+
\"keyword\": \"code\"
|
|
278
|
+
}"
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## 3. 心跳检测
|
|
284
|
+
|
|
285
|
+
**POST** `/api/v1/licenses/heartbeat`
|
|
286
|
+
|
|
287
|
+
轻量级 License 有效性确认,建议每 1 天调用一次(或使用 SDK `start_heartbeat!` 自动化)。
|
|
288
|
+
服务器同步更新设备的 `last_seen_at` 时间。
|
|
289
|
+
|
|
290
|
+
### 请求参数
|
|
291
|
+
|
|
292
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
293
|
+
|------|------|------|------|
|
|
294
|
+
| `user_id` | string | 是 | 用户 ID |
|
|
295
|
+
| `device_id` | string | 是 | 设备 ID |
|
|
296
|
+
| `timestamp` | string | 是 | Unix 时间戳 |
|
|
297
|
+
| `nonce` | string | 是 | 随机 hex,每次唯一 |
|
|
298
|
+
| `signature` | string | 是 | HMAC-SHA256 签名 |
|
|
299
|
+
|
|
300
|
+
### 响应参数(成功 200)
|
|
301
|
+
|
|
302
|
+
```json
|
|
303
|
+
{
|
|
304
|
+
"status": "ok",
|
|
305
|
+
"expires_at": "2027-01-01T00:00:00Z"
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
| 字段 | 类型 | 说明 |
|
|
310
|
+
|------|------|------|
|
|
311
|
+
| `status` | string | 固定为 `ok` |
|
|
312
|
+
| `expires_at` | string | License 过期时间(ISO 8601) |
|
|
313
|
+
|
|
314
|
+
### curl 示例
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
TS=$(date +%s)
|
|
318
|
+
NONCE=$(openssl rand -hex 16)
|
|
319
|
+
USER_ID="42"
|
|
320
|
+
DEVICE_ID="a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
|
|
321
|
+
KEY="0000002A-00000007-DEADBEEF-CAFEBABE-A1B2C3D4"
|
|
322
|
+
MSG="${USER_ID}:${DEVICE_ID}:${TS}:${NONCE}"
|
|
323
|
+
SIG=$(echo -n "$MSG" | openssl dgst -sha256 -hmac "$KEY" | awk '{print $2}')
|
|
324
|
+
|
|
325
|
+
curl -s -X POST https://your-platform.com/api/v1/licenses/heartbeat \
|
|
326
|
+
-H "Content-Type: application/json" \
|
|
327
|
+
-d "{
|
|
328
|
+
\"user_id\": \"${USER_ID}\",
|
|
329
|
+
\"device_id\": \"${DEVICE_ID}\",
|
|
330
|
+
\"timestamp\": \"${TS}\",
|
|
331
|
+
\"nonce\": \"${NONCE}\",
|
|
332
|
+
\"signature\": \"${SIG}\"
|
|
333
|
+
}"
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**响应示例:**
|
|
337
|
+
```json
|
|
338
|
+
{ "status": "ok", "expires_at": "2027-03-06T10:00:00Z" }
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## License 算法与验证说明
|
|
344
|
+
|
|
345
|
+
### 一、License Key 格式
|
|
346
|
+
|
|
347
|
+
License Key 是一个 **40 个 hex 字符、5 组 8 字符**的字符串:
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
UUUUUUUU - PPPPPPPP - RRRRRRRR - RRRRRRRR - CCCCCCCC
|
|
351
|
+
│ │ │ │
|
|
352
|
+
user_id plan_id random(8字节熵) HMAC校验位
|
|
353
|
+
(uint32) (uint32) 64-bit 随机数 (前4字节)
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**示例:**
|
|
357
|
+
```
|
|
358
|
+
0000002A-00000007-DEADBEEF-CAFEBABE-A1B2C3D4
|
|
359
|
+
│ │ │ │
|
|
360
|
+
user=42 plan=7 随机 8 字节 HMAC checksum
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**字段说明:**
|
|
364
|
+
|
|
365
|
+
| 段 | 长度 | 说明 |
|
|
366
|
+
|----|------|------|
|
|
367
|
+
| `UUUUUUUU` | 8 hex | `user_id` 大端序 uint32,客户端本地可直接解析 |
|
|
368
|
+
| `PPPPPPPP` | 8 hex | `plan_id` 大端序 uint32,标识许可计划 |
|
|
369
|
+
| `RRRRRRRR-RRRRRRRR` | 16 hex | 8 字节 SecureRandom,保证每个 Key 全局唯一 |
|
|
370
|
+
| `CCCCCCCC` | 8 hex | `HMAC-SHA256(LICENSE_SECRET, U+P+R)[0..3]` 的 hex,4 字节完整性校验 |
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
### 二、Key 生成算法(服务端)
|
|
375
|
+
|
|
376
|
+
```ruby
|
|
377
|
+
# 服务端生成(app/models/license.rb)
|
|
378
|
+
def generate_key
|
|
379
|
+
u = [user_id].pack('N').unpack1('H*').upcase # uint32 → 8 hex
|
|
380
|
+
p = [plan_id].pack('N').unpack1('H*').upcase # uint32 → 8 hex
|
|
381
|
+
r = SecureRandom.bytes(8).unpack1('H*').upcase # 8 字节随机 → 16 hex
|
|
382
|
+
|
|
383
|
+
payload = u + p + r # 32 hex = 16 字节
|
|
384
|
+
checksum = HMAC-SHA256(LICENSE_SECRET, payload)[0, 4] # 取前 4 字节
|
|
385
|
+
checksum = checksum.unpack1('H*').upcase # → 8 hex
|
|
386
|
+
|
|
387
|
+
key = "#{u}-#{p}-#{r[0..7]}-#{r[8..15]}-#{checksum}"
|
|
388
|
+
key_hash = SHA256(key) # 用于激活时的查找索引
|
|
389
|
+
end
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
**安全属性:**
|
|
393
|
+
- `user_id`/`plan_id` 明文嵌入,客户端无需联网即可解析
|
|
394
|
+
- `random` 8 字节(64-bit 熵)保证唯一性,碰撞概率 ≈ 2^(-32) 在 40 亿次生成前
|
|
395
|
+
- `HMAC checksum` 使用独立密钥 `LICENSE_SECRET`,篡改任意字符均使校验失败
|
|
396
|
+
- `key_hash = SHA256(key)` 存储在数据库,作为激活时的查找索引(无法反推 key)
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
### 三、客户端本地解析(无需联网)
|
|
401
|
+
|
|
402
|
+
```ruby
|
|
403
|
+
# 客户端本地解析(sdk/openclacky_license_client.rb)
|
|
404
|
+
hex = key.delete('-').upcase # 去掉分隔符
|
|
405
|
+
user_id = hex[0..7].to_i(16) # 前 8 hex → integer
|
|
406
|
+
plan_id = hex[8..15].to_i(16) # 次 8 hex → integer
|
|
407
|
+
key_hash = SHA256(key) # 64 字符 hex
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
### 四、激活流程:HMAC 知识证明
|
|
413
|
+
|
|
414
|
+
激活阶段的核心问题:**如何向服务器证明持有 license_key,但又不发送 license_key?**
|
|
415
|
+
|
|
416
|
+
**解决方案:HMAC 知识证明(Zero-Transmission Proof)**
|
|
417
|
+
|
|
418
|
+
```
|
|
419
|
+
┌─ 客户端 ──────────────────────────────────────────────────┐
|
|
420
|
+
│ │
|
|
421
|
+
│ 1. 本地解析: │
|
|
422
|
+
│ user_id = key[0..7].to_i(16) │
|
|
423
|
+
│ key_hash = SHA256(license_key) │
|
|
424
|
+
│ │
|
|
425
|
+
│ 2. 构造证明消息: │
|
|
426
|
+
│ message = "activate:{key_hash}:{user_id}: │
|
|
427
|
+
│ {device_id}:{timestamp}:{nonce}" │
|
|
428
|
+
│ │
|
|
429
|
+
│ 3. 计算 HMAC 证明: │
|
|
430
|
+
│ proof = HMAC-SHA256(license_key, message) │
|
|
431
|
+
│ │
|
|
432
|
+
│ 4. 发送(不含 license_key): │
|
|
433
|
+
│ { key_hash, user_id, device_id, timestamp, │
|
|
434
|
+
│ nonce, proof, device_info } │
|
|
435
|
+
└───────────────────────────────────────────────────────────┘
|
|
436
|
+
│ HTTP POST │
|
|
437
|
+
▼ ▼
|
|
438
|
+
┌─ 服务端 ──────────────────────────────────────────────────┐
|
|
439
|
+
│ │
|
|
440
|
+
│ 1. 通过 key_hash 查找 License(O(1) 索引) │
|
|
441
|
+
│ 2. 验证时间戳(±5 分钟) │
|
|
442
|
+
│ 3. 验证 nonce(10 分钟 TTL,防重放) │
|
|
443
|
+
│ 4. 重建 message,使用数据库中存储的 license.key 计算: │
|
|
444
|
+
│ expected = HMAC-SHA256(license.key, message) │
|
|
445
|
+
│ 5. 常量时间比较:expected == proof │
|
|
446
|
+
│ 6. 验证通过 → 绑定设备,记录激活时间 │
|
|
447
|
+
│ │
|
|
448
|
+
└───────────────────────────────────────────────────────────┘
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
**为什么安全?**
|
|
452
|
+
- 只有持有 `license_key` 的客户端才能计算出正确的 `proof`
|
|
453
|
+
- `key_hash = SHA256(key)` 是单向函数,服务器无法从 `key_hash` 反推 `key`
|
|
454
|
+
- `nonce` 一次性使用,攻击者截获请求后无法重放(10 分钟缓存过期)
|
|
455
|
+
- `timestamp` ±5 分钟窗口,进一步限制重放时间窗口
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
### 五、后续请求:签名认证
|
|
460
|
+
|
|
461
|
+
激活后的所有 API 调用(`skills`、`heartbeat`)使用简化的请求签名:
|
|
462
|
+
|
|
463
|
+
```
|
|
464
|
+
┌─ 客户端 ───────────────────────────────────────┐
|
|
465
|
+
│ │
|
|
466
|
+
│ message = "{user_id}:{device_id}: │
|
|
467
|
+
│ {timestamp}:{nonce}" │
|
|
468
|
+
│ signature = HMAC-SHA256(license_key, message) │
|
|
469
|
+
│ │
|
|
470
|
+
│ 发送:{ user_id, device_id, timestamp, │
|
|
471
|
+
│ nonce, signature, ...业务参数 } │
|
|
472
|
+
└─────────────────────────────────────────────────┘
|
|
473
|
+
│
|
|
474
|
+
▼
|
|
475
|
+
┌─ 服务端 ───────────────────────────────────────┐
|
|
476
|
+
│ │
|
|
477
|
+
│ 1. 通过 (device_id + user_id) 查找设备/License│
|
|
478
|
+
│ 2. 验证时间戳(±5 分钟) │
|
|
479
|
+
│ 3. 验证 nonce(防重放) │
|
|
480
|
+
│ 4. Cross-check: │
|
|
481
|
+
│ params.user_id == license_plan.user_id │
|
|
482
|
+
│ 5. 重算签名并比较 │
|
|
483
|
+
│ 6. 更新 last_seen_at │
|
|
484
|
+
│ │
|
|
485
|
+
└─────────────────────────────────────────────────┘
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
**Cross-check 的意义:**
|
|
489
|
+
攻击者即使猜到了别人的 `device_id`,也无法伪造请求——因为必须同时满足:
|
|
490
|
+
1. `user_id` 与 License 绑定的 `user_id` 一致
|
|
491
|
+
2. HMAC 签名正确(需要持有 `license_key`)
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
### 六、稳定设备 ID 算法
|
|
496
|
+
|
|
497
|
+
SDK 提供 `LicenseClient.stable_device_id` 方法,基于机器指纹生成稳定的 32 字符 hex 设备 ID:
|
|
498
|
+
|
|
499
|
+
```ruby
|
|
500
|
+
# 优先级:/etc/machine-id → /var/lib/dbus/machine-id → hostname
|
|
501
|
+
sources = ['/etc/machine-id', '/var/lib/dbus/machine-id']
|
|
502
|
+
content = sources.find { |f| File.exist?(f) }
|
|
503
|
+
&.then { |f| File.read(f).strip }
|
|
504
|
+
|
|
505
|
+
device_id = if content
|
|
506
|
+
SHA256(content)[0, 32]
|
|
507
|
+
else
|
|
508
|
+
SHA256("#{Socket.gethostname}-openclacky")[0, 32]
|
|
509
|
+
end
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**特性:**
|
|
513
|
+
- 同一机器每次调用返回相同值(幂等)
|
|
514
|
+
- 重新安装 App 不影响设备 ID(基于硬件标识)
|
|
515
|
+
- 不含任何可识别个人身份的信息
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
### 七、安全防御矩阵
|
|
520
|
+
|
|
521
|
+
| 威胁 | 防御机制 |
|
|
522
|
+
|------|----------|
|
|
523
|
+
| License Key 网络截获 | Key 全程不传输;激活用 `key_hash + HMAC proof` |
|
|
524
|
+
| 激活 proof 重放 | `timestamp ±5min` + `nonce` 唯一性(Cache TTL 10min) |
|
|
525
|
+
| 伪造请求(不持有 key) | HMAC-SHA256 签名,key 作为签名密钥 |
|
|
526
|
+
| Device ID 碰撞伪冒 | 每次请求 cross-check `user_id == license_plan.user_id` |
|
|
527
|
+
| 伪造 License Key 格式 | Key 含 `HMAC(LICENSE_SECRET, payload)` 校验位,服务端快速拒绝 |
|
|
528
|
+
| SHA256(key) 碰撞攻击 | 碰撞空间 2^256,实际不可行 |
|
|
529
|
+
| 撤销不传播 | 每次请求查询 `device.revoked_at` + `license.status` |
|
|
530
|
+
| 暴力枚举 key_hash | `rack-attack`:激活 10 次/小时/IP,API 120 次/小时/设备 |
|
|
531
|
+
| 时序攻击(签名比较) | `ActiveSupport::SecurityUtils.secure_compare` 常量时间 |
|
|
532
|
+
| 多设备超限 | `device_limit = license_plan.seats`,激活时强制检查 |
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
### 八、速率限制
|
|
537
|
+
|
|
538
|
+
| 规则 | 限制 | 周期 | 维度 |
|
|
539
|
+
|------|------|------|------|
|
|
540
|
+
| 激活接口 | 10 次 | 1 小时 | 客户端 IP |
|
|
541
|
+
| 所有 License API | 120 次 | 1 小时 | device_id |
|
|
542
|
+
|
|
543
|
+
触发限制时响应:
|
|
544
|
+
```json
|
|
545
|
+
HTTP 429
|
|
546
|
+
{ "status": "error", "code": "rate_limited", "message": "Rate limit exceeded. Please try again later." }
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
响应头包含 `Retry-After: <seconds>`。
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
### 九、SDK 快速接入(Ruby)
|
|
554
|
+
|
|
555
|
+
```ruby
|
|
556
|
+
require_relative 'sdk/openclacky_license_client'
|
|
557
|
+
|
|
558
|
+
# 初始化(仅需一次)
|
|
559
|
+
client = OpenClacky::LicenseClient.new(
|
|
560
|
+
base_url: "https://your-platform.com",
|
|
561
|
+
device_info: { os: RUBY_PLATFORM, app_version: "1.0.0" },
|
|
562
|
+
store: OpenClacky::LicenseStore.new("~/.myapp/license.json")
|
|
563
|
+
)
|
|
564
|
+
client.load_license("0000002A-00000007-DEADBEEF-CAFEBABE-A1B2C3D4")
|
|
565
|
+
|
|
566
|
+
# 激活(首次运行)
|
|
567
|
+
license_info = client.activate!
|
|
568
|
+
|
|
569
|
+
# 获取 Skills 列表
|
|
570
|
+
result = client.list_skills # 全部
|
|
571
|
+
result = client.list_skills(visibility: 'public') # 仅公开
|
|
572
|
+
result = client.list_skills(keyword: 'code review') # 关键词搜索
|
|
573
|
+
|
|
574
|
+
# 下载 Skill(获取直链后自行下载)
|
|
575
|
+
skill = result['skills'].first
|
|
576
|
+
download_url = skill['latest_version']['download_url']
|
|
577
|
+
|
|
578
|
+
# 启动后台心跳(每 10 分钟自动检测)
|
|
579
|
+
client.start_heartbeat!(
|
|
580
|
+
on_revoked: ->(e) { puts "License 已撤销:#{e.message}"; exit 1 },
|
|
581
|
+
on_expired: ->(e) { puts "License 已过期,请续费" },
|
|
582
|
+
on_error: ->(e) { puts "心跳失败(将自动重试):#{e.message}" }
|
|
583
|
+
)
|
|
584
|
+
```
|
|
@@ -169,8 +169,9 @@ module Clacky
|
|
|
169
169
|
|
|
170
170
|
page.each do |round|
|
|
171
171
|
msg = round[:user_msg]
|
|
172
|
+
display_text = extract_text_from_content(msg[:content])
|
|
172
173
|
# Emit user message with its timestamp for dedup on the frontend
|
|
173
|
-
ui.show_user_message(
|
|
174
|
+
ui.show_user_message(display_text, created_at: msg[:created_at])
|
|
174
175
|
|
|
175
176
|
round[:events].each do |ev|
|
|
176
177
|
case ev[:role].to_s
|