openclacky 1.3.1 → 1.3.3
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 +44 -0
- data/Dockerfile +3 -0
- data/README.md +1 -1
- data/README_JA.md +237 -0
- data/lib/clacky/agent/session_serializer.rb +65 -11
- data/lib/clacky/agent/time_machine.rb +247 -26
- data/lib/clacky/agent.rb +12 -1
- data/lib/clacky/agent_config.rb +14 -2
- data/lib/clacky/brand_config.rb +1 -1
- data/lib/clacky/default_agents/_panels/git/panel.js +201 -0
- data/lib/clacky/default_agents/_panels/time_machine/panel.js +640 -0
- data/lib/clacky/default_agents/coding/profile.yml +3 -0
- data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
- data/lib/clacky/default_skills/cron-task-creator/SKILL.md +1 -1
- data/lib/clacky/default_skills/extend-openclacky/SKILL.md +6 -4
- data/lib/clacky/default_skills/media-gen/SKILL.md +30 -6
- data/lib/clacky/media/openai_compat.rb +64 -1
- data/lib/clacky/media/output_dir.rb +43 -0
- data/lib/clacky/message_history.rb +9 -0
- data/lib/clacky/server/channel/channel_manager.rb +26 -0
- data/lib/clacky/server/git_panel.rb +115 -0
- data/lib/clacky/server/http_server.rb +521 -13
- data/lib/clacky/server/server_master.rb +6 -4
- data/lib/clacky/utils/environment_detector.rb +16 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +512 -60
- data/lib/clacky/web/app.js +30 -7
- data/lib/clacky/web/components/code-editor.js +197 -0
- data/lib/clacky/web/{notify.js → components/notify.js} +1 -1
- data/lib/clacky/web/core/aside.js +112 -0
- data/lib/clacky/web/core/ext.js +387 -0
- data/lib/clacky/web/features/backup/store.js +92 -0
- data/lib/clacky/web/features/backup/view.js +94 -0
- data/lib/clacky/web/features/billing/store.js +163 -0
- data/lib/clacky/web/{billing.js → features/billing/view.js} +134 -242
- data/lib/clacky/web/features/brand/store.js +110 -0
- data/lib/clacky/web/{brand.js → features/brand/view.js} +49 -199
- data/lib/clacky/web/features/channels/store.js +103 -0
- data/lib/clacky/web/{channels.js → features/channels/view.js} +50 -127
- data/lib/clacky/web/features/creator/store.js +81 -0
- data/lib/clacky/web/{creator.js → features/creator/view.js} +53 -102
- data/lib/clacky/web/features/mcp/store.js +158 -0
- data/lib/clacky/web/{mcp.js → features/mcp/view.js} +57 -134
- data/lib/clacky/web/features/model-tester/store.js +77 -0
- data/lib/clacky/web/features/model-tester/view.js +7 -0
- data/lib/clacky/web/features/profile/store.js +170 -0
- data/lib/clacky/web/{profile.js → features/profile/view.js} +94 -144
- data/lib/clacky/web/features/share/store.js +145 -0
- data/lib/clacky/web/{share.js → features/share/view.js} +66 -202
- data/lib/clacky/web/features/skills/store.js +303 -0
- data/lib/clacky/web/features/skills/view.js +550 -0
- data/lib/clacky/web/features/tasks/store.js +135 -0
- data/lib/clacky/web/features/tasks/view.js +241 -0
- data/lib/clacky/web/features/trash/store.js +242 -0
- data/lib/clacky/web/{trash.js → features/trash/view.js} +102 -293
- data/lib/clacky/web/features/version/store.js +165 -0
- data/lib/clacky/web/features/version/view.js +323 -0
- data/lib/clacky/web/features/workspace/store.js +99 -0
- data/lib/clacky/web/features/workspace/view.js +305 -0
- data/lib/clacky/web/i18n.js +60 -6
- data/lib/clacky/web/index.html +117 -57
- data/lib/clacky/web/sessions.js +221 -25
- data/lib/clacky/web/settings.js +121 -25
- data/lib/clacky/web/skills.js +3 -821
- data/lib/clacky/web/vendor/codemirror/codemirror.min.js +29 -0
- data/lib/clacky.rb +1 -0
- metadata +45 -20
- data/lib/clacky/web/backup.js +0 -119
- data/lib/clacky/web/model-tester.js +0 -66
- data/lib/clacky/web/tasks.js +0 -365
- data/lib/clacky/web/version.js +0 -449
- data/lib/clacky/web/workspace.js +0 -212
- /data/lib/clacky/web/{notify.mp3 → assets/notify.mp3} +0 -0
- /data/lib/clacky/web/{datepicker.js → components/datepicker.js} +0 -0
- /data/lib/clacky/web/{onboard.js → components/onboard.js} +0 -0
- /data/lib/clacky/web/{sidebar.js → components/sidebar.js} +0 -0
- /data/lib/clacky/web/{marked.min.js → vendor/marked/marked.min.js} +0 -0
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
//
|
|
1
|
+
// ── Channels · view — platform metadata + rendering + DOM wiring ───────────
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// ant-design/ant-design-icons outlined (DingTalk).
|
|
3
|
+
// Owns PLATFORM_META (logos/labels), all card rendering, and event wiring.
|
|
4
|
+
// Reads data through ChannelsStore.state and reacts to store events. Toggle /
|
|
5
|
+
// test / setup go through store actions.
|
|
6
|
+
//
|
|
7
|
+
// Augments the `Channels` facade with onPanelShow.
|
|
8
|
+
//
|
|
9
|
+
// Depends on: ChannelsStore, I18n, global $ helper.
|
|
10
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const ChannelsView = (() => {
|
|
13
|
+
|
|
15
14
|
function PLATFORM_META() {
|
|
16
15
|
return {
|
|
17
16
|
feishu: {
|
|
@@ -65,37 +64,12 @@ const Channels = (() => {
|
|
|
65
64
|
};
|
|
66
65
|
}
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
async function onPanelShow() {
|
|
71
|
-
await _load();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ── Data Loading ─────────────────────────────────────────────────────────────
|
|
75
|
-
|
|
76
|
-
async function _load({ silent = false } = {}) {
|
|
77
|
-
const container = $("channels-list");
|
|
78
|
-
if (!container) return;
|
|
79
|
-
if (!silent) {
|
|
80
|
-
container.innerHTML = `<div class="channel-loading">${I18n.t("channels.loading")}</div>`;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
const res = await fetch("/api/channels");
|
|
85
|
-
const data = await res.json();
|
|
86
|
-
_render(data.channels || []);
|
|
87
|
-
} catch (e) {
|
|
88
|
-
container.innerHTML = `<div class="channel-error">${I18n.t("channels.loadError", { msg: _esc(e.message) })}</div>`;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ── Rendering ─────────────────────────────────────────────────────────────────
|
|
93
|
-
|
|
94
|
-
function _render(channels) {
|
|
67
|
+
function _render() {
|
|
95
68
|
const container = $("channels-list");
|
|
96
69
|
if (!container) return;
|
|
97
70
|
container.innerHTML = "";
|
|
98
71
|
|
|
72
|
+
const channels = ChannelsStore.state.channels;
|
|
99
73
|
const meta = PLATFORM_META();
|
|
100
74
|
const platformIds = Object.keys(meta);
|
|
101
75
|
const configured = [];
|
|
@@ -106,19 +80,8 @@ const Channels = (() => {
|
|
|
106
80
|
(serverData.has_config ? configured : unconfigured).push({ pid, serverData, meta: meta[pid] });
|
|
107
81
|
});
|
|
108
82
|
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
const section = _renderSection("connected", configured);
|
|
112
|
-
container.appendChild(section);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Unconfigured section
|
|
116
|
-
if (unconfigured.length > 0) {
|
|
117
|
-
const section = _renderSection("unconfigured", unconfigured);
|
|
118
|
-
container.appendChild(section);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Custom adapter development card
|
|
83
|
+
if (configured.length > 0) container.appendChild(_renderSection("connected", configured));
|
|
84
|
+
if (unconfigured.length > 0) container.appendChild(_renderSection("unconfigured", unconfigured));
|
|
122
85
|
container.appendChild(_renderCustomDevCard());
|
|
123
86
|
}
|
|
124
87
|
|
|
@@ -134,8 +97,7 @@ const Channels = (() => {
|
|
|
134
97
|
section.appendChild(header);
|
|
135
98
|
|
|
136
99
|
items.forEach(({ pid, serverData, meta }) => {
|
|
137
|
-
|
|
138
|
-
section.appendChild(card);
|
|
100
|
+
section.appendChild(_renderCard(pid, serverData, meta));
|
|
139
101
|
});
|
|
140
102
|
|
|
141
103
|
return section;
|
|
@@ -187,7 +149,6 @@ const Channels = (() => {
|
|
|
187
149
|
</div>
|
|
188
150
|
`;
|
|
189
151
|
|
|
190
|
-
// Bind events
|
|
191
152
|
card.querySelector(`#btn-test-${platform}`)?.addEventListener("click", () => _runTest(platform));
|
|
192
153
|
card.querySelector(`#btn-configure-${platform}`)?.addEventListener("click", () => _openSetup(platform));
|
|
193
154
|
card.querySelector(`#toggle-${platform}`)?.addEventListener("change", (ev) => _onToggle(platform, ev.target));
|
|
@@ -205,35 +166,18 @@ const Channels = (() => {
|
|
|
205
166
|
`;
|
|
206
167
|
}
|
|
207
168
|
|
|
208
|
-
// ── Status hint helpers ───────────────────────────────────────────────
|
|
209
|
-
|
|
210
169
|
function _statusHint(enabled, running, hasConfig) {
|
|
211
|
-
if (running) {
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
if (enabled) {
|
|
215
|
-
return `<p class="channel-status-hint hint-warn">⚠ ${I18n.t("channels.hint.enabledNotRunning")}</p>`;
|
|
216
|
-
}
|
|
217
|
-
if (hasConfig) {
|
|
218
|
-
return `<p class="channel-status-hint hint-idle">${I18n.t("channels.hint.disabled")}</p>`;
|
|
219
|
-
}
|
|
170
|
+
if (running) return `<p class="channel-status-hint hint-ok">✓ ${I18n.t("channels.hint.running")}</p>`;
|
|
171
|
+
if (enabled) return `<p class="channel-status-hint hint-warn">⚠ ${I18n.t("channels.hint.enabledNotRunning")}</p>`;
|
|
172
|
+
if (hasConfig) return `<p class="channel-status-hint hint-idle">${I18n.t("channels.hint.disabled")}</p>`;
|
|
220
173
|
return `<p class="channel-status-hint hint-idle">${I18n.t("channels.hint.notConfigured")}</p>`;
|
|
221
174
|
}
|
|
222
175
|
|
|
223
|
-
// ── Toggle handler ───────────────────────────────────────────────────────────
|
|
224
|
-
|
|
225
176
|
async function _onToggle(platform, checkbox) {
|
|
226
177
|
const desired = checkbox.checked;
|
|
227
178
|
checkbox.disabled = true;
|
|
228
179
|
try {
|
|
229
|
-
|
|
230
|
-
method: "PATCH",
|
|
231
|
-
headers: { "Content-Type": "application/json" },
|
|
232
|
-
body: JSON.stringify({ enabled: desired }),
|
|
233
|
-
});
|
|
234
|
-
const data = await res.json();
|
|
235
|
-
if (!res.ok || !data.ok) throw new Error(data.error || "toggle failed");
|
|
236
|
-
await _load({ silent: true });
|
|
180
|
+
await Channels.toggle(platform, desired);
|
|
237
181
|
} catch (e) {
|
|
238
182
|
checkbox.checked = !desired;
|
|
239
183
|
alert("Error: " + e.message);
|
|
@@ -242,53 +186,16 @@ const Channels = (() => {
|
|
|
242
186
|
}
|
|
243
187
|
}
|
|
244
188
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
// Run E2E test: open a session and send /channel-manager doctor
|
|
248
|
-
async function _runTest(platform) {
|
|
189
|
+
function _runTest(platform) {
|
|
249
190
|
const meta = PLATFORM_META()[platform];
|
|
250
|
-
|
|
191
|
+
return Channels.runTest(meta.testCmd, `Channel E2E Test — ${meta.name}`);
|
|
251
192
|
}
|
|
252
193
|
|
|
253
|
-
|
|
254
|
-
async function _openSetup(platform) {
|
|
194
|
+
function _openSetup(platform) {
|
|
255
195
|
const meta = PLATFORM_META()[platform];
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Create a session, add it to the list, navigate to it, and send the given command.
|
|
260
|
-
// Follows the same pattern as Skills.createInSession().
|
|
261
|
-
async function _sendToAgent(command, sessionName) {
|
|
262
|
-
try {
|
|
263
|
-
// Pick a session name in "Session N" style, consistent with other modules
|
|
264
|
-
const maxN = Sessions.all.reduce((max, s) => {
|
|
265
|
-
const m = s.name.match(/^Session (\d+)$/);
|
|
266
|
-
return m ? Math.max(max, parseInt(m[1], 10)) : max;
|
|
267
|
-
}, 0);
|
|
268
|
-
const name = sessionName || ("Session " + (maxN + 1));
|
|
269
|
-
|
|
270
|
-
const res = await fetch("/api/sessions", {
|
|
271
|
-
method: "POST",
|
|
272
|
-
headers: { "Content-Type": "application/json" },
|
|
273
|
-
body: JSON.stringify({ name, source: "setup" }),
|
|
274
|
-
});
|
|
275
|
-
const data = await res.json();
|
|
276
|
-
if (!res.ok) throw new Error(data.error || I18n.t("channels.sessionError"));
|
|
277
|
-
const session = data.session;
|
|
278
|
-
if (!session) throw new Error(I18n.t("channels.noSession"));
|
|
279
|
-
|
|
280
|
-
// Register in Sessions, refresh sidebar, queue command, then navigate
|
|
281
|
-
Sessions.add(session);
|
|
282
|
-
Sessions.renderList();
|
|
283
|
-
Sessions.setPendingMessage(session.id, command);
|
|
284
|
-
Sessions.select(session.id);
|
|
285
|
-
} catch (e) {
|
|
286
|
-
alert("Error: " + e.message);
|
|
287
|
-
}
|
|
196
|
+
return Channels.openSetup(meta.setupCmd, `Channel Setup — ${meta.name}`);
|
|
288
197
|
}
|
|
289
198
|
|
|
290
|
-
// ── Custom Adapter Development Card ──────────────────────────────────────────
|
|
291
|
-
|
|
292
199
|
function _renderCustomDevCard() {
|
|
293
200
|
const card = document.createElement("div");
|
|
294
201
|
card.className = "channel-card channel-card-custom-dev";
|
|
@@ -320,17 +227,12 @@ const Channels = (() => {
|
|
|
320
227
|
`;
|
|
321
228
|
|
|
322
229
|
card.querySelector("#btn-custom-dev-guide")?.addEventListener("click", () => {
|
|
323
|
-
|
|
324
|
-
I18n.t("channels.customDev.prompt"),
|
|
325
|
-
I18n.t("channels.customDev.title")
|
|
326
|
-
);
|
|
230
|
+
Channels.sendToAgent(I18n.t("channels.customDev.prompt"), I18n.t("channels.customDev.title"));
|
|
327
231
|
});
|
|
328
232
|
|
|
329
233
|
return card;
|
|
330
234
|
}
|
|
331
235
|
|
|
332
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
333
|
-
|
|
334
236
|
function _esc(str) {
|
|
335
237
|
return String(str || "")
|
|
336
238
|
.replace(/&/g, "&")
|
|
@@ -339,8 +241,29 @@ const Channels = (() => {
|
|
|
339
241
|
.replace(/"/g, """);
|
|
340
242
|
}
|
|
341
243
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
244
|
+
function _renderLoading() {
|
|
245
|
+
const container = $("channels-list");
|
|
246
|
+
if (container) container.innerHTML = `<div class="channel-loading">${I18n.t("channels.loading")}</div>`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function _renderError(payload) {
|
|
250
|
+
const container = $("channels-list");
|
|
251
|
+
if (container) container.innerHTML = `<div class="channel-error">${I18n.t("channels.loadError", { msg: _esc(payload.message) })}</div>`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function _subscribe() {
|
|
255
|
+
Channels.on("channels:loading", _renderLoading);
|
|
256
|
+
Channels.on("channels:changed", _render);
|
|
257
|
+
Channels.on("channels:error", _renderError);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const viewApi = {
|
|
261
|
+
init() { /* subscriptions wired at load; panel data loads on onPanelShow */ },
|
|
262
|
+
onPanelShow() { return Channels.load(); },
|
|
345
263
|
};
|
|
264
|
+
|
|
265
|
+
return { init: _subscribe, api: viewApi };
|
|
346
266
|
})();
|
|
267
|
+
|
|
268
|
+
Object.assign(Channels, ChannelsView.api);
|
|
269
|
+
ChannelsView.init();
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// ── Creator · store — cloud/local skill data + publish network ─────────────
|
|
2
|
+
//
|
|
3
|
+
// Owns the creator skill lists (cloud / local), loading flag, and the network
|
|
4
|
+
// calls (load catalog, publish a skill). It never renders.
|
|
5
|
+
//
|
|
6
|
+
// Emits store events the view reacts to; mirrors them to the extension bus via
|
|
7
|
+
// Clacky.ext.emit.
|
|
8
|
+
//
|
|
9
|
+
// `Creator` stays the single public facade.
|
|
10
|
+
//
|
|
11
|
+
// Depends on: Clacky.ext.
|
|
12
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const CreatorStore = (() => {
|
|
15
|
+
let _cloudSkills = [];
|
|
16
|
+
let _localSkills = [];
|
|
17
|
+
let _loading = false;
|
|
18
|
+
|
|
19
|
+
const _listeners = {};
|
|
20
|
+
|
|
21
|
+
function _on(event, handler) {
|
|
22
|
+
(_listeners[event] ||= []).push(handler);
|
|
23
|
+
return () => {
|
|
24
|
+
const list = _listeners[event];
|
|
25
|
+
const i = list ? list.indexOf(handler) : -1;
|
|
26
|
+
if (i >= 0) list.splice(i, 1);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function _emit(event, payload) {
|
|
31
|
+
(_listeners[event] || []).forEach((h) => h(payload));
|
|
32
|
+
if (window.Clacky && Clacky.ext) Clacky.ext.emit(event, payload);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const state = {
|
|
36
|
+
get cloudSkills() { return _cloudSkills; },
|
|
37
|
+
get localSkills() { return _localSkills; },
|
|
38
|
+
get loading() { return _loading; },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const Creator = {
|
|
42
|
+
on: _on,
|
|
43
|
+
state,
|
|
44
|
+
|
|
45
|
+
async load() {
|
|
46
|
+
if (_loading) return;
|
|
47
|
+
_loading = true;
|
|
48
|
+
_emit("creator:loading");
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch("/api/creator/skills");
|
|
51
|
+
const data = await res.json();
|
|
52
|
+
if (!res.ok) throw new Error(data.error || "Load failed");
|
|
53
|
+
|
|
54
|
+
_cloudSkills = data.cloud_skills || [];
|
|
55
|
+
_localSkills = data.local_skills || [];
|
|
56
|
+
_emit("creator:changed", { platformFetchError: data.platform_fetch_error || null });
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.error("[Creator] load failed", e);
|
|
59
|
+
_emit("creator:error", { message: e.message });
|
|
60
|
+
} finally {
|
|
61
|
+
_loading = false;
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
/** Publish (or update) a skill. Returns { ok, already_exists, error }. */
|
|
66
|
+
async publish(skillName, { force = false } = {}) {
|
|
67
|
+
const url = `/api/my-skills/${encodeURIComponent(skillName)}/publish${force ? "?force=true" : ""}`;
|
|
68
|
+
const res = await fetch(url, { method: "POST" });
|
|
69
|
+
const data = await res.json();
|
|
70
|
+
return {
|
|
71
|
+
ok: res.ok && !!data.ok,
|
|
72
|
+
already_exists: !!data.already_exists,
|
|
73
|
+
error: data.error || null,
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return Creator;
|
|
79
|
+
})();
|
|
80
|
+
|
|
81
|
+
const Creator = CreatorStore;
|
|
@@ -1,21 +1,16 @@
|
|
|
1
|
-
//
|
|
1
|
+
// ── Creator · view — Creator Hub rendering + publish UI + DOM wiring ───────
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// 3. Create New — opens a new session with /skill-creator
|
|
3
|
+
// Owns rendering of cloud/local skill cards, the publish progress UI (animation
|
|
4
|
+
// + overwrite confirm), the new-skill entry, and sidebar/panel visibility.
|
|
5
|
+
// Reads through CreatorStore.state; load/publish go through store actions.
|
|
7
6
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// ── Private state ────────────────────────────────────────────────────
|
|
13
|
-
let _cloudSkills = [];
|
|
14
|
-
let _localSkills = [];
|
|
15
|
-
let _loading = false;
|
|
16
|
-
let _domWired = false;
|
|
7
|
+
// Augments the `Creator` facade with onPanelShow and updateSidebarVisibility.
|
|
8
|
+
//
|
|
9
|
+
// Depends on: CreatorStore, I18n, Brand, Skills, Modal.
|
|
10
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
17
11
|
|
|
18
|
-
|
|
12
|
+
const CreatorView = (() => {
|
|
13
|
+
let _domWired = false;
|
|
19
14
|
|
|
20
15
|
function escapeHtml(s) {
|
|
21
16
|
return String(s ?? "")
|
|
@@ -27,46 +22,7 @@ const Creator = (() => {
|
|
|
27
22
|
return I18n.t ? I18n.t(key) : key;
|
|
28
23
|
}
|
|
29
24
|
|
|
30
|
-
//
|
|
31
|
-
function _hasLocalChanges(skill) {
|
|
32
|
-
if (!skill.local_modified_at || !skill.uploaded_at) return false;
|
|
33
|
-
try {
|
|
34
|
-
return new Date(skill.local_modified_at) > new Date(skill.uploaded_at);
|
|
35
|
-
} catch { return false; }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ── Data loading ─────────────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
async function _load() {
|
|
41
|
-
if (_loading) return;
|
|
42
|
-
_loading = true;
|
|
43
|
-
_renderLoading();
|
|
44
|
-
try {
|
|
45
|
-
const res = await fetch("/api/creator/skills");
|
|
46
|
-
const data = await res.json();
|
|
47
|
-
if (!res.ok) throw new Error(data.error || "Load failed");
|
|
48
|
-
|
|
49
|
-
_cloudSkills = data.cloud_skills || [];
|
|
50
|
-
_localSkills = data.local_skills || [];
|
|
51
|
-
_render();
|
|
52
|
-
|
|
53
|
-
if (data.platform_fetch_error) {
|
|
54
|
-
_showNotice(
|
|
55
|
-
I18n.lang() === "zh"
|
|
56
|
-
? `平台数据加载失败:${data.platform_fetch_error}`
|
|
57
|
-
: `Platform data unavailable: ${data.platform_fetch_error}`,
|
|
58
|
-
"warn"
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
} catch (e) {
|
|
62
|
-
console.error("[Creator] load failed", e);
|
|
63
|
-
_renderError(e.message);
|
|
64
|
-
} finally {
|
|
65
|
-
_loading = false;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ── Rendering ────────────────────────────────────────────────────────
|
|
25
|
+
// ── Loading / error ─────────────────────────────────────────────────────
|
|
70
26
|
|
|
71
27
|
function _renderLoading() {
|
|
72
28
|
const cloudList = document.getElementById("creator-cloud-list");
|
|
@@ -76,52 +32,57 @@ const Creator = (() => {
|
|
|
76
32
|
if (localList) localList.innerHTML = "";
|
|
77
33
|
}
|
|
78
34
|
|
|
79
|
-
function _renderError(
|
|
35
|
+
function _renderError(payload) {
|
|
80
36
|
const cloudList = document.getElementById("creator-cloud-list");
|
|
81
|
-
if (cloudList) cloudList.innerHTML = `<div class="creator-empty creator-error">${escapeHtml(
|
|
37
|
+
if (cloudList) cloudList.innerHTML = `<div class="creator-empty creator-error">${escapeHtml(payload.message)}</div>`;
|
|
82
38
|
const localList = document.getElementById("creator-local-list");
|
|
83
39
|
if (localList) localList.innerHTML = "";
|
|
84
40
|
}
|
|
85
41
|
|
|
86
|
-
function _render() {
|
|
42
|
+
function _render(payload) {
|
|
87
43
|
_renderCloudSection();
|
|
88
44
|
_renderLocalSection();
|
|
89
45
|
_wireNewSkillEntry();
|
|
46
|
+
|
|
47
|
+
if (payload && payload.platformFetchError) {
|
|
48
|
+
_showNotice(
|
|
49
|
+
I18n.lang() === "zh"
|
|
50
|
+
? `平台数据加载失败:${payload.platformFetchError}`
|
|
51
|
+
: `Platform data unavailable: ${payload.platformFetchError}`,
|
|
52
|
+
"warn"
|
|
53
|
+
);
|
|
54
|
+
}
|
|
90
55
|
}
|
|
91
56
|
|
|
92
57
|
function _renderCloudSection() {
|
|
93
|
-
const list
|
|
94
|
-
const block = document.getElementById("creator-cloud-block");
|
|
58
|
+
const list = document.getElementById("creator-cloud-list");
|
|
95
59
|
if (!list) return;
|
|
96
60
|
|
|
97
|
-
|
|
61
|
+
const cloudSkills = CreatorStore.state.cloudSkills;
|
|
62
|
+
if (cloudSkills.length === 0) {
|
|
98
63
|
list.innerHTML = `<div class="creator-empty">${_t("creator.cloud.empty")}</div>`;
|
|
99
64
|
return;
|
|
100
65
|
}
|
|
101
66
|
|
|
102
67
|
list.innerHTML = "";
|
|
103
|
-
|
|
104
|
-
list.appendChild(_buildCloudCard(skill));
|
|
105
|
-
});
|
|
68
|
+
cloudSkills.forEach(skill => list.appendChild(_buildCloudCard(skill)));
|
|
106
69
|
}
|
|
107
70
|
|
|
108
71
|
function _renderLocalSection() {
|
|
109
|
-
const list
|
|
110
|
-
const block = document.getElementById("creator-local-block");
|
|
72
|
+
const list = document.getElementById("creator-local-list");
|
|
111
73
|
if (!list) return;
|
|
112
74
|
|
|
113
|
-
|
|
75
|
+
const localSkills = CreatorStore.state.localSkills;
|
|
76
|
+
if (localSkills.length === 0) {
|
|
114
77
|
list.innerHTML = `<div class="creator-empty">${_t("creator.local.empty")}</div>`;
|
|
115
78
|
return;
|
|
116
79
|
}
|
|
117
80
|
|
|
118
81
|
list.innerHTML = "";
|
|
119
|
-
|
|
120
|
-
list.appendChild(_buildLocalCard(skill));
|
|
121
|
-
});
|
|
82
|
+
localSkills.forEach(skill => list.appendChild(_buildLocalCard(skill)));
|
|
122
83
|
}
|
|
123
84
|
|
|
124
|
-
// ── Cloud card
|
|
85
|
+
// ── Cloud card ───────────────────────────────────────────────────────────
|
|
125
86
|
|
|
126
87
|
function _buildCloudCard(skill) {
|
|
127
88
|
const card = document.createElement("div");
|
|
@@ -143,15 +104,10 @@ const Creator = (() => {
|
|
|
143
104
|
</span>`
|
|
144
105
|
: "";
|
|
145
106
|
|
|
146
|
-
// Has local changes indicator
|
|
147
107
|
const changesHtml = skill.has_local_changes
|
|
148
108
|
? `<span class="creator-changes-badge" title="${_t("creator.hasLocalChanges")}">● ${_t("creator.changed")}</span>`
|
|
149
109
|
: "";
|
|
150
110
|
|
|
151
|
-
// Action buttons:
|
|
152
|
-
// - local_present + has_local_changes → "Update" (publish) + "Iterate" (skill-creator)
|
|
153
|
-
// - local_present + no changes → grey disabled "Up to date" + "Iterate"
|
|
154
|
-
// - no local copy → nothing (can only iterate to create local first)
|
|
155
111
|
let actionBtnsHtml = "";
|
|
156
112
|
if (skill.local_present) {
|
|
157
113
|
if (skill.has_local_changes) {
|
|
@@ -220,15 +176,13 @@ const Creator = (() => {
|
|
|
220
176
|
|
|
221
177
|
if (skill.local_present) {
|
|
222
178
|
const iterBtn = card.querySelector(".btn-creator-iterate");
|
|
223
|
-
if (iterBtn)
|
|
224
|
-
iterBtn.addEventListener("click", () => _iterateSkill(skill.name));
|
|
225
|
-
}
|
|
179
|
+
if (iterBtn) iterBtn.addEventListener("click", () => _iterateSkill(skill.name));
|
|
226
180
|
}
|
|
227
181
|
|
|
228
182
|
return card;
|
|
229
183
|
}
|
|
230
184
|
|
|
231
|
-
// ── Local card
|
|
185
|
+
// ── Local card ─────────────────────────────────────────────────────────
|
|
232
186
|
|
|
233
187
|
function _buildLocalCard(skill) {
|
|
234
188
|
const card = document.createElement("div");
|
|
@@ -277,7 +231,7 @@ const Creator = (() => {
|
|
|
277
231
|
return card;
|
|
278
232
|
}
|
|
279
233
|
|
|
280
|
-
// ── Publish
|
|
234
|
+
// ── Publish UI ───────────────────────────────────────────────────────────
|
|
281
235
|
|
|
282
236
|
async function _publishSkill(skillName, publishBtn, progressWrap, progressBar, force, card, isUpdate) {
|
|
283
237
|
publishBtn.disabled = true;
|
|
@@ -300,27 +254,22 @@ const Creator = (() => {
|
|
|
300
254
|
let skipFinalReset = false;
|
|
301
255
|
|
|
302
256
|
try {
|
|
303
|
-
const
|
|
304
|
-
const res = await fetch(url, { method: "POST" });
|
|
305
|
-
const data = await res.json();
|
|
306
|
-
|
|
257
|
+
const result = await Creator.publish(skillName, { force });
|
|
307
258
|
clearInterval(animInterval);
|
|
308
259
|
|
|
309
|
-
if (!
|
|
310
|
-
alreadyExists =
|
|
311
|
-
throw new Error(
|
|
260
|
+
if (!result.ok) {
|
|
261
|
+
alreadyExists = result.already_exists;
|
|
262
|
+
throw new Error(result.error || "Publish failed");
|
|
312
263
|
}
|
|
313
264
|
|
|
314
|
-
// Success
|
|
315
265
|
progressBar.style.width = "100%";
|
|
316
266
|
progressBar.dataset.state = "success";
|
|
317
267
|
publishBtn.dataset.state = "success";
|
|
318
268
|
if (btnLabel) btnLabel.textContent = I18n.lang() === "zh" ? "已发布 ✓" : "Published ✓";
|
|
319
269
|
|
|
320
270
|
await new Promise(r => setTimeout(r, 1400));
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
skipFinalReset = true; // _load() re-renders, no need to reset manually
|
|
271
|
+
await Creator.load();
|
|
272
|
+
skipFinalReset = true;
|
|
324
273
|
|
|
325
274
|
} catch (e) {
|
|
326
275
|
clearInterval(animInterval);
|
|
@@ -368,14 +317,10 @@ const Creator = (() => {
|
|
|
368
317
|
}
|
|
369
318
|
}
|
|
370
319
|
|
|
371
|
-
// ── Iterate skill (open skill-creator session for an existing skill) ──
|
|
372
|
-
|
|
373
320
|
function _iterateSkill(skillName) {
|
|
374
321
|
Skills.createInSession(`/skill-creator ${I18n.t("creator.iterate.prompt")}${skillName}`);
|
|
375
322
|
}
|
|
376
323
|
|
|
377
|
-
// ── Create new skill entry ────────────────────────────────────────────
|
|
378
|
-
|
|
379
324
|
function _wireNewSkillEntry() {
|
|
380
325
|
const entry = document.getElementById("creator-new-entry");
|
|
381
326
|
if (!entry || entry.dataset.wired) return;
|
|
@@ -383,8 +328,6 @@ const Creator = (() => {
|
|
|
383
328
|
entry.addEventListener("click", () => Skills.createInSession());
|
|
384
329
|
}
|
|
385
330
|
|
|
386
|
-
// ── Notice bar ────────────────────────────────────────────────────────
|
|
387
|
-
|
|
388
331
|
function _showNotice(msg, type = "warn") {
|
|
389
332
|
const container = document.getElementById("creator-cloud-list");
|
|
390
333
|
if (!container) return;
|
|
@@ -396,9 +339,13 @@ const Creator = (() => {
|
|
|
396
339
|
container.prepend(el);
|
|
397
340
|
}
|
|
398
341
|
|
|
399
|
-
|
|
342
|
+
function _subscribe() {
|
|
343
|
+
Creator.on("creator:loading", _renderLoading);
|
|
344
|
+
Creator.on("creator:changed", _render);
|
|
345
|
+
Creator.on("creator:error", _renderError);
|
|
346
|
+
}
|
|
400
347
|
|
|
401
|
-
|
|
348
|
+
const viewApi = {
|
|
402
349
|
/** Show/hide the creator section.
|
|
403
350
|
* Hidden for brand consumer users (branded=true, userLicensed=false).
|
|
404
351
|
* Visible for creators (userLicensed=true) and unbranded users. */
|
|
@@ -415,7 +362,6 @@ const Creator = (() => {
|
|
|
415
362
|
_domWired = true;
|
|
416
363
|
_wireNewSkillEntry();
|
|
417
364
|
}
|
|
418
|
-
// Show promo banner and cloud lock for non-licensed users
|
|
419
365
|
const licensed = Brand.userLicensed;
|
|
420
366
|
const banner = document.getElementById("creator-promo-banner");
|
|
421
367
|
const lock = document.getElementById("creator-cloud-lock");
|
|
@@ -423,7 +369,12 @@ const Creator = (() => {
|
|
|
423
369
|
if (banner) banner.style.display = licensed ? "none" : "";
|
|
424
370
|
if (lock) lock.style.display = licensed ? "none" : "";
|
|
425
371
|
if (list) list.style.display = licensed ? "" : "none";
|
|
426
|
-
|
|
372
|
+
Creator.load();
|
|
427
373
|
},
|
|
428
374
|
};
|
|
375
|
+
|
|
376
|
+
return { init: _subscribe, api: viewApi };
|
|
429
377
|
})();
|
|
378
|
+
|
|
379
|
+
Object.assign(Creator, CreatorView.api);
|
|
380
|
+
CreatorView.init();
|