openclacky 1.2.7 → 1.2.9
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 +26 -0
- data/lib/clacky/agent.rb +3 -0
- data/lib/clacky/agent_config.rb +91 -7
- data/lib/clacky/billing/billing_store.rb +107 -3
- data/lib/clacky/cli.rb +105 -0
- data/lib/clacky/client.rb +38 -5
- data/lib/clacky/default_skills/channel-manager/SKILL.md +33 -110
- data/lib/clacky/default_skills/deploy/SKILL.md +2 -1
- data/lib/clacky/default_skills/extend-openclacky/SKILL.md +39 -0
- data/lib/clacky/default_skills/mcp-manager/SKILL.md +0 -7
- data/lib/clacky/default_skills/media-gen/SKILL.md +128 -0
- data/lib/clacky/media/base.rb +68 -0
- data/lib/clacky/media/gemini.rb +36 -0
- data/lib/clacky/media/generator.rb +78 -0
- data/lib/clacky/media/openai_compat.rb +168 -0
- data/lib/clacky/patch_loader.rb +282 -0
- data/lib/clacky/providers.rb +82 -0
- data/lib/clacky/server/channel/adapters/base.rb +4 -0
- data/lib/clacky/server/channel/channel_manager.rb +1 -1
- data/lib/clacky/server/channel/user_adapter_loader.rb +177 -0
- data/lib/clacky/server/channel.rb +5 -0
- data/lib/clacky/server/http_server.rb +236 -25
- data/lib/clacky/server/scheduler.rb +1 -4
- data/lib/clacky/shell_hook_loader.rb +181 -0
- data/lib/clacky/telemetry.rb +11 -5
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +326 -24
- data/lib/clacky/web/billing.js +117 -22
- data/lib/clacky/web/i18n.js +84 -6
- data/lib/clacky/web/index.html +14 -2
- data/lib/clacky/web/model-tester.js +58 -0
- data/lib/clacky/web/onboard.js +17 -30
- data/lib/clacky/web/settings.js +322 -97
- data/lib/clacky.rb +9 -0
- data/scripts/build/lib/network.sh +61 -30
- data/scripts/install.sh +61 -30
- data/scripts/install_browser.sh +61 -30
- data/scripts/install_full.sh +61 -30
- data/scripts/install_rails_deps.sh +61 -30
- data/scripts/install_system_deps.sh +61 -30
- metadata +12 -3
- data/lib/clacky/default_skills/channel-manager/feishu_setup.rb +0 -574
data/lib/clacky/web/i18n.js
CHANGED
|
@@ -490,6 +490,35 @@ const I18n = (() => {
|
|
|
490
490
|
"settings.models.empty": "No models configured. Click \"+ Add Model\" to add one.",
|
|
491
491
|
"settings.models.badge.default": "Default",
|
|
492
492
|
"settings.models.badge.lite": "Lite",
|
|
493
|
+
"settings.media.title": "Media Generation",
|
|
494
|
+
"settings.media.desc": "Optional. Image / video / audio generation models.",
|
|
495
|
+
"settings.media.loading": "Loading…",
|
|
496
|
+
"settings.media.error": "Failed to load: {{msg}}",
|
|
497
|
+
"settings.media.kind.image": "Image",
|
|
498
|
+
"settings.media.kind.video": "Video",
|
|
499
|
+
"settings.media.kind.audio": "Audio",
|
|
500
|
+
"settings.media.source.off": "Off",
|
|
501
|
+
"settings.media.source.auto": "Auto",
|
|
502
|
+
"settings.media.source.custom": "Custom",
|
|
503
|
+
"settings.media.field.model": "Model",
|
|
504
|
+
"settings.media.field.baseUrl": "Base URL",
|
|
505
|
+
"settings.media.field.apiKey": "API Key",
|
|
506
|
+
"settings.media.field.provider":"Provider",
|
|
507
|
+
"settings.media.off.hint": "Disabled.",
|
|
508
|
+
"settings.media.auto.followsDefault": "Follows default chat model",
|
|
509
|
+
"settings.media.auto.noDefaultModel": "Set a default chat model first.",
|
|
510
|
+
"settings.media.auto.unsupported": "Current provider has no built-in model for this kind. Switch to Custom.",
|
|
511
|
+
"settings.media.auto.comingSoon": "Not available yet — no built-in providers.",
|
|
512
|
+
"settings.media.auto.disabledTitle": "No built-in provider available — use Custom.",
|
|
513
|
+
"settings.media.action.edit": "Edit",
|
|
514
|
+
"settings.media.action.save": "Save",
|
|
515
|
+
"settings.media.action.cancel": "Cancel",
|
|
516
|
+
"settings.media.action.saving": "Saving…",
|
|
517
|
+
"settings.media.action.saved": "Saved",
|
|
518
|
+
"settings.media.apiKey.placeholder": "Enter API key",
|
|
519
|
+
"settings.media.apiKey.required": "API key required",
|
|
520
|
+
"settings.media.model.required": "Model name required",
|
|
521
|
+
"settings.media.baseUrl.required": "Base URL required",
|
|
493
522
|
"settings.models.field.quicksetup": "Quick Setup",
|
|
494
523
|
"settings.models.field.model": "Model",
|
|
495
524
|
"settings.models.field.baseurl": "Base URL",
|
|
@@ -686,15 +715,25 @@ const I18n = (() => {
|
|
|
686
715
|
"billing.cacheHit": "Cache Hit",
|
|
687
716
|
"billing.inputCacheHit": "Input (Cache Hit)",
|
|
688
717
|
"billing.inputCacheMiss": "Input (Cache Miss)",
|
|
689
|
-
"billing.
|
|
690
|
-
"billing.tokenUsage": "Token Usage",
|
|
718
|
+
"billing.totalInput": "Total Input",
|
|
719
|
+
"billing.output": "Output", "billing.tokenUsage": "Token Usage",
|
|
691
720
|
"billing.costTrend": "Cost Trend",
|
|
692
721
|
"billing.noData": "No data available",
|
|
693
722
|
|
|
694
723
|
"error.insufficient_credit": "Insufficient LLM credit. Please top up your account to continue.",
|
|
695
724
|
"error.insufficient_credit.action": "Top up",
|
|
696
|
-
},
|
|
697
725
|
|
|
726
|
+
"billing.sessions": "Sessions",
|
|
727
|
+
"billing.sessionId": "Session",
|
|
728
|
+
"billing.tokens": "Tokens",
|
|
729
|
+
"billing.lastRequest": "Last Request",
|
|
730
|
+
"billing.noSessions": "No session data",
|
|
731
|
+
"billing.deletedSessions": "Deleted Sessions",
|
|
732
|
+
"billing.headerTotal": "Total",
|
|
733
|
+
"billing.headerHit": "Hit",
|
|
734
|
+
"billing.headerMiss": "Miss",
|
|
735
|
+
"billing.headerOutput": "Output",
|
|
736
|
+
},
|
|
698
737
|
zh: {
|
|
699
738
|
// ── Sidebar ──
|
|
700
739
|
"sidebar.chat": "会话",
|
|
@@ -1170,6 +1209,35 @@ const I18n = (() => {
|
|
|
1170
1209
|
"settings.models.empty": "暂未配置模型,点击「+ 添加模型」添加。",
|
|
1171
1210
|
"settings.models.badge.default": "默认",
|
|
1172
1211
|
"settings.models.badge.lite": "轻量",
|
|
1212
|
+
"settings.media.title": "媒体生成",
|
|
1213
|
+
"settings.media.desc": "可选。图片 / 视频 / 音频 生成模型。",
|
|
1214
|
+
"settings.media.loading": "加载中…",
|
|
1215
|
+
"settings.media.error": "加载失败:{{msg}}",
|
|
1216
|
+
"settings.media.kind.image": "图片",
|
|
1217
|
+
"settings.media.kind.video": "视频",
|
|
1218
|
+
"settings.media.kind.audio": "音频",
|
|
1219
|
+
"settings.media.source.off": "关闭",
|
|
1220
|
+
"settings.media.source.auto": "自动",
|
|
1221
|
+
"settings.media.source.custom": "自定义",
|
|
1222
|
+
"settings.media.field.model": "模型",
|
|
1223
|
+
"settings.media.field.baseUrl": "Base URL",
|
|
1224
|
+
"settings.media.field.apiKey": "API Key",
|
|
1225
|
+
"settings.media.field.provider":"服务商",
|
|
1226
|
+
"settings.media.off.hint": "已关闭。",
|
|
1227
|
+
"settings.media.auto.followsDefault": "跟随默认聊天模型",
|
|
1228
|
+
"settings.media.auto.noDefaultModel": "请先设置默认聊天模型。",
|
|
1229
|
+
"settings.media.auto.unsupported": "当前服务商无内置模型,请切换到「自定义」。",
|
|
1230
|
+
"settings.media.auto.comingSoon": "暂无内置服务商,敬请期待。",
|
|
1231
|
+
"settings.media.auto.disabledTitle": "暂无内置服务商,请使用自定义。",
|
|
1232
|
+
"settings.media.action.edit": "编辑",
|
|
1233
|
+
"settings.media.action.save": "保存",
|
|
1234
|
+
"settings.media.action.cancel": "取消",
|
|
1235
|
+
"settings.media.action.saving": "保存中…",
|
|
1236
|
+
"settings.media.action.saved": "已保存",
|
|
1237
|
+
"settings.media.apiKey.placeholder": "请输入 API Key",
|
|
1238
|
+
"settings.media.apiKey.required": "请填写 API Key",
|
|
1239
|
+
"settings.media.model.required": "请填写模型名称",
|
|
1240
|
+
"settings.media.baseUrl.required": "请填写 Base URL",
|
|
1173
1241
|
"settings.models.field.quicksetup": "快速配置",
|
|
1174
1242
|
"settings.models.field.model": "Model",
|
|
1175
1243
|
"settings.models.field.baseurl": "Base URL",
|
|
@@ -1366,16 +1434,26 @@ const I18n = (() => {
|
|
|
1366
1434
|
"billing.cacheHit": "缓存命中",
|
|
1367
1435
|
"billing.inputCacheHit": "输入(命中缓存)",
|
|
1368
1436
|
"billing.inputCacheMiss": "输入(未命中缓存)",
|
|
1369
|
-
"billing.
|
|
1370
|
-
"billing.
|
|
1437
|
+
"billing.totalTokens": "Token 总消耗",
|
|
1438
|
+
"billing.totalInput": "总输入",
|
|
1439
|
+
"billing.output": "输出", "billing.tokenUsage": "Token 用量",
|
|
1371
1440
|
"billing.costTrend": "费用趋势",
|
|
1372
1441
|
"billing.noData": "暂无数据",
|
|
1373
1442
|
|
|
1374
1443
|
"error.insufficient_credit": "LLM(大模型)余额不足,请充值后继续使用。",
|
|
1375
1444
|
"error.insufficient_credit.action": "去充值",
|
|
1445
|
+
"billing.sessions": "会话消耗",
|
|
1446
|
+
"billing.sessionId": "会话",
|
|
1447
|
+
"billing.tokens": "Token",
|
|
1448
|
+
"billing.lastRequest": "最后请求",
|
|
1449
|
+
"billing.noSessions": "暂无会话数据",
|
|
1450
|
+
"billing.deletedSessions": "已删除会话",
|
|
1451
|
+
"billing.headerTotal": "总消耗",
|
|
1452
|
+
"billing.headerHit": "命中",
|
|
1453
|
+
"billing.headerMiss": "未命中",
|
|
1454
|
+
"billing.headerOutput": "输出",
|
|
1376
1455
|
}
|
|
1377
1456
|
};
|
|
1378
|
-
|
|
1379
1457
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
1380
1458
|
let _lang = localStorage.getItem(STORAGE_KEY) || DEFAULT_LANG;
|
|
1381
1459
|
|
data/lib/clacky/web/index.html
CHANGED
|
@@ -771,6 +771,17 @@
|
|
|
771
771
|
</div>
|
|
772
772
|
<div id="model-cards"></div>
|
|
773
773
|
</section>
|
|
774
|
+
|
|
775
|
+
<!-- Media generation section -->
|
|
776
|
+
<section class="settings-section" id="media-section">
|
|
777
|
+
<div class="settings-section-title">
|
|
778
|
+
<span data-i18n="settings.media.title">Media Generation</span>
|
|
779
|
+
</div>
|
|
780
|
+
<div class="settings-section-desc" data-i18n="settings.media.desc">
|
|
781
|
+
Generate images, video, and audio using your configured providers. "Auto" follows your default chat model.
|
|
782
|
+
</div>
|
|
783
|
+
<div id="media-rows"></div>
|
|
784
|
+
</section>
|
|
774
785
|
</div>
|
|
775
786
|
|
|
776
787
|
<!-- ══ Tab: UI ══ -->
|
|
@@ -1052,8 +1063,8 @@
|
|
|
1052
1063
|
<div id="model-modal-test-result" class="model-test-result"></div>
|
|
1053
1064
|
|
|
1054
1065
|
<label class="model-field model-field-checkbox" id="model-modal-default-field">
|
|
1055
|
-
<input type="checkbox" id="model-modal-set-default">
|
|
1056
|
-
<span data-i18n="settings.models.field.setDefault">Set as default model</span>
|
|
1066
|
+
<input type="checkbox" id="model-modal-set-default" class="field-checkbox">
|
|
1067
|
+
<span class="field-label" data-i18n="settings.models.field.setDefault">Set as default model</span>
|
|
1057
1068
|
</label>
|
|
1058
1069
|
</div>
|
|
1059
1070
|
<div class="modal-footer">
|
|
@@ -1288,6 +1299,7 @@
|
|
|
1288
1299
|
<script src="/skills.js"></script>
|
|
1289
1300
|
<script src="/channels.js"></script>
|
|
1290
1301
|
<script src="/mcp.js"></script>
|
|
1302
|
+
<script src="/model-tester.js"></script>
|
|
1291
1303
|
<script src="/settings.js"></script>
|
|
1292
1304
|
<script src="/billing.js"></script>
|
|
1293
1305
|
<script src="/onboard.js"></script>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Shared helpers for the model config UI flows.
|
|
2
|
+
// Used by both the onboarding wizard and the settings model modal.
|
|
3
|
+
window.ModelTester = (function () {
|
|
4
|
+
// Test a model connection.
|
|
5
|
+
// Returns one of:
|
|
6
|
+
// { ok: true, base_url, message } — connected, no rewrite
|
|
7
|
+
// { ok: true, base_url, message, rewrote: true } — connected, base_url auto-corrected (/v1 appended)
|
|
8
|
+
// { ok: false, message } — failed (server-reported or network)
|
|
9
|
+
async function testConnection({ model, base_url, api_key, anthropic_format, index } = {}) {
|
|
10
|
+
const body = { model, base_url, api_key };
|
|
11
|
+
if (typeof index === "number") body.index = index;
|
|
12
|
+
if (anthropic_format) body.anthropic_format = true;
|
|
13
|
+
|
|
14
|
+
let data;
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch("/api/config/test", {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: { "Content-Type": "application/json" },
|
|
19
|
+
body: JSON.stringify(body)
|
|
20
|
+
});
|
|
21
|
+
data = await res.json();
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return { ok: false, message: e.message };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!data.ok) return { ok: false, message: data.message || "" };
|
|
27
|
+
|
|
28
|
+
if (data.effective_base_url && data.effective_base_url !== base_url) {
|
|
29
|
+
return { ok: true, base_url: data.effective_base_url, message: data.message || "", rewrote: true };
|
|
30
|
+
}
|
|
31
|
+
return { ok: true, base_url, message: data.message || "" };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Persist a model config (create or update).
|
|
35
|
+
// existingId === null/undefined → POST /api/config/models (create).
|
|
36
|
+
// existingId === string → PATCH /api/config/models/:id (update).
|
|
37
|
+
// Returns { ok: bool, error? }.
|
|
38
|
+
async function saveModel(payload, { existingId } = {}) {
|
|
39
|
+
const url = existingId
|
|
40
|
+
? `/api/config/models/${encodeURIComponent(existingId)}`
|
|
41
|
+
: "/api/config/models";
|
|
42
|
+
const method = existingId ? "PATCH" : "POST";
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const res = await fetch(url, {
|
|
46
|
+
method,
|
|
47
|
+
headers: { "Content-Type": "application/json" },
|
|
48
|
+
body: JSON.stringify(payload)
|
|
49
|
+
});
|
|
50
|
+
const data = await res.json();
|
|
51
|
+
return data.ok ? { ok: true } : { ok: false, error: data.error || "" };
|
|
52
|
+
} catch (e) {
|
|
53
|
+
return { ok: false, error: e.message };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { testConnection, saveModel };
|
|
58
|
+
})();
|
data/lib/clacky/web/onboard.js
CHANGED
|
@@ -417,43 +417,30 @@ const Onboard = (() => {
|
|
|
417
417
|
_setResult(null, "");
|
|
418
418
|
|
|
419
419
|
// Step 1: test connection
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const data = await res.json();
|
|
427
|
-
if (!data.ok) {
|
|
428
|
-
_setResult(false, data.message || (zh ? "连接失败。" : "Connection failed."));
|
|
429
|
-
btn.disabled = false;
|
|
430
|
-
btn.textContent = I18n.t("onboard.key.btn.test");
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
} catch (e) {
|
|
434
|
-
_setResult(false, e.message);
|
|
420
|
+
const testResult = await ModelTester.testConnection({
|
|
421
|
+
model, base_url: baseUrl, api_key: apiKey, index: 0
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
if (!testResult.ok) {
|
|
425
|
+
_setResult(false, testResult.message || (zh ? "连接失败。" : "Connection failed."));
|
|
435
426
|
btn.disabled = false;
|
|
436
427
|
btn.textContent = I18n.t("onboard.key.btn.test");
|
|
437
428
|
return;
|
|
438
429
|
}
|
|
439
430
|
|
|
431
|
+
let effectiveBaseUrl = testResult.base_url;
|
|
432
|
+
if (testResult.rewrote) {
|
|
433
|
+
const baseInput = document.getElementById("setup-base-url");
|
|
434
|
+
if (baseInput) baseInput.value = effectiveBaseUrl;
|
|
435
|
+
}
|
|
436
|
+
|
|
440
437
|
// Step 2: save config
|
|
441
438
|
btn.textContent = I18n.t("onboard.key.saving");
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
});
|
|
448
|
-
const data = await res.json();
|
|
449
|
-
if (!data.ok) {
|
|
450
|
-
_setResult(false, data.error || (zh ? "保存失败。" : "Save failed."));
|
|
451
|
-
btn.disabled = false;
|
|
452
|
-
btn.textContent = I18n.t("onboard.key.btn.test");
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
} catch (e) {
|
|
456
|
-
_setResult(false, e.message);
|
|
439
|
+
const saveResult = await ModelTester.saveModel({
|
|
440
|
+
type: "default", model, base_url: effectiveBaseUrl, api_key: apiKey
|
|
441
|
+
});
|
|
442
|
+
if (!saveResult.ok) {
|
|
443
|
+
_setResult(false, saveResult.error || (zh ? "保存失败。" : "Save failed."));
|
|
457
444
|
btn.disabled = false;
|
|
458
445
|
btn.textContent = I18n.t("onboard.key.btn.test");
|
|
459
446
|
return;
|