openclacky 1.3.6 → 1.3.7

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.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/lib/clacky/agent.rb +8 -1
  4. data/lib/clacky/agent_profile.rb +121 -53
  5. data/lib/clacky/brand_config.rb +309 -6
  6. data/lib/clacky/cli.rb +4 -57
  7. data/lib/clacky/default_extensions/coding/ext.yml +18 -0
  8. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +95 -0
  9. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +254 -0
  10. data/lib/clacky/default_extensions/ext-studio/ext.yml +32 -0
  11. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +1269 -0
  12. data/lib/clacky/default_extensions/ext-studio/skills/ext-debug/SKILL.md +71 -0
  13. data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +74 -0
  14. data/lib/clacky/default_extensions/ext-studio/skills/ext-scaffold/SKILL.md +65 -0
  15. data/lib/clacky/default_extensions/general/ext.yml +18 -0
  16. data/lib/clacky/default_extensions/git/ext.yml +12 -0
  17. data/lib/clacky/{default_agents/_panels/git/panel.js → default_extensions/git/panels/git/view.js} +1 -2
  18. data/lib/clacky/default_extensions/meeting/{handler.rb → api/handler.rb} +78 -26
  19. data/lib/clacky/default_extensions/meeting/ext.yml +15 -0
  20. data/lib/clacky/default_extensions/meeting/{meeting.js → panels/meeting/view.js} +266 -89
  21. data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +2 -1
  22. data/lib/clacky/default_extensions/time_machine/ext.yml +12 -0
  23. data/lib/clacky/{default_agents/_panels/time_machine/panel.js → default_extensions/time_machine/panels/time_machine/view.js} +58 -47
  24. data/lib/clacky/default_skills/channel-manager/discord_setup.rb +1 -1
  25. data/lib/clacky/default_skills/extend-openclacky/SKILL.md +83 -18
  26. data/lib/clacky/{api_extension.rb → extension/api_extension.rb} +27 -11
  27. data/lib/clacky/extension/api_loader.rb +136 -0
  28. data/lib/clacky/extension/cli_commands.rb +226 -0
  29. data/lib/clacky/{server/api_extension_dispatcher.rb → extension/dispatcher.rb} +22 -9
  30. data/lib/clacky/extension/hook_loader.rb +77 -0
  31. data/lib/clacky/extension/loader.rb +483 -0
  32. data/lib/clacky/extension/packager.rb +226 -0
  33. data/lib/clacky/{patch_loader.rb → extension/patch_loader.rb} +45 -0
  34. data/lib/clacky/extension/scaffold/template_renderer.rb +64 -0
  35. data/lib/clacky/extension/scaffold/templates/full/README.md.erb +48 -0
  36. data/lib/clacky/extension/scaffold/templates/full/agents/designer.md +5 -0
  37. data/lib/clacky/extension/scaffold/templates/full/api/handler.rb.erb +16 -0
  38. data/lib/clacky/extension/scaffold/templates/full/channels/noop.rb.erb +40 -0
  39. data/lib/clacky/extension/scaffold/templates/full/ext.yml.erb +41 -0
  40. data/lib/clacky/extension/scaffold/templates/full/hooks/audit.rb +9 -0
  41. data/lib/clacky/extension/scaffold/templates/full/panels/dashboard/view.js.erb +194 -0
  42. data/lib/clacky/extension/scaffold/templates/full/patches/audit.rb +15 -0
  43. data/lib/clacky/extension/scaffold/templates/full/skills/__slug__-skill/SKILL.md.erb +8 -0
  44. data/lib/clacky/extension/scaffold/templates/hello/api/handler.rb.erb +9 -0
  45. data/lib/clacky/extension/scaffold/templates/hello/ext.yml.erb +12 -0
  46. data/lib/clacky/extension/scaffold/templates/hello/panels/hello/view.js.erb +31 -0
  47. data/lib/clacky/extension/scaffold.rb +55 -0
  48. data/lib/clacky/extension/verifier.rb +196 -0
  49. data/lib/clacky/identity.rb +70 -0
  50. data/lib/clacky/locales/en.rb +19 -1
  51. data/lib/clacky/locales/zh.rb +19 -1
  52. data/lib/clacky/platform_http_client.rb +18 -15
  53. data/lib/clacky/server/channel/extension_adapter_loader.rb +32 -0
  54. data/lib/clacky/server/channel.rb +6 -0
  55. data/lib/clacky/server/http_server.rb +313 -144
  56. data/lib/clacky/server/session_registry.rb +9 -2
  57. data/lib/clacky/skill_loader.rb +28 -1
  58. data/lib/clacky/ui2/ui_controller.rb +9 -6
  59. data/lib/clacky/version.rb +1 -1
  60. data/lib/clacky/web/app.css +748 -573
  61. data/lib/clacky/web/app.js +63 -15
  62. data/lib/clacky/web/auth.js +2 -0
  63. data/lib/clacky/web/components/notify.js +2 -0
  64. data/lib/clacky/web/components/sidebar.js +1 -5
  65. data/lib/clacky/web/core/ext.js +316 -79
  66. data/lib/clacky/web/features/backup/store.js +1 -0
  67. data/lib/clacky/web/features/backup/view.js +0 -1
  68. data/lib/clacky/web/features/brand/view.js +13 -5
  69. data/lib/clacky/web/features/extensions/store.js +184 -0
  70. data/lib/clacky/web/features/extensions/view.js +389 -0
  71. data/lib/clacky/web/features/new-session/store.js +186 -0
  72. data/lib/clacky/web/features/new-session/view.js +319 -0
  73. data/lib/clacky/web/features/share/view.js +63 -14
  74. data/lib/clacky/web/features/skills/store.js +3 -1
  75. data/lib/clacky/web/features/skills/view.js +1 -1
  76. data/lib/clacky/web/features/tasks/view.js +12 -0
  77. data/lib/clacky/web/features/trash/store.js +24 -6
  78. data/lib/clacky/web/features/trash/view.js +46 -9
  79. data/lib/clacky/web/features/workspace/store.js +2 -0
  80. data/lib/clacky/web/features/workspace/view.js +0 -1
  81. data/lib/clacky/web/i18n.js +118 -106
  82. data/lib/clacky/web/index.html +138 -188
  83. data/lib/clacky/web/sessions.js +64 -269
  84. data/lib/clacky/web/settings.js +1 -1
  85. data/lib/clacky/web/skills.js +2 -0
  86. data/lib/clacky/web/theme.js +2 -0
  87. data/lib/clacky/web/ws-dispatcher.js +1 -0
  88. data/lib/clacky/web/ws.js +2 -0
  89. data/lib/clacky.rb +21 -10
  90. metadata +55 -24
  91. data/benchmark/runner.rb +0 -441
  92. data/lib/clacky/api_extension_loader.rb +0 -168
  93. data/lib/clacky/default_agents/SOUL.md +0 -3
  94. data/lib/clacky/default_agents/USER.md +0 -1
  95. data/lib/clacky/default_agents/coding/profile.yml +0 -5
  96. data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
  97. data/lib/clacky/default_agents/general/profile.yml +0 -2
  98. data/lib/clacky/default_extensions/meeting/meta.yml +0 -3
  99. data/lib/clacky/web/features/creator/store.js +0 -81
  100. data/lib/clacky/web/features/creator/view.js +0 -380
  101. /data/lib/clacky/{default_agents → default_extensions/coding/agents}/coding/system_prompt.md +0 -0
  102. /data/lib/clacky/{default_agents → default_extensions/general/agents}/general/system_prompt.md +0 -0
  103. /data/lib/clacky/{default_agents/base_prompt.md → prompts/base.md} +0 -0
@@ -1,168 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fileutils"
4
-
5
- module Clacky
6
- # Discovers and loads user-defined HTTP API extensions from
7
- # ~/.clacky/api_ext/<name>/handler.rb. Each handler is expected to define a
8
- # subclass of Clacky::ApiExtension; the subclass is auto-registered with the
9
- # framework and its routes become available under /api/ext/<name>/.
10
- #
11
- # A broken extension (syntax error, missing base class, route conflict) is
12
- # isolated: skipped with a logged warning, never aborts the load of others.
13
- module ApiExtensionLoader
14
- DEFAULT_DIR = File.expand_path("~/.clacky/api_ext")
15
- BUILTIN_DIR = File.expand_path("../default_extensions", __FILE__)
16
- DISABLED_DIR = "_disabled"
17
-
18
- Result = Struct.new(:loaded, :skipped, keyword_init: true)
19
-
20
- class << self
21
- def load_all(dir: DEFAULT_DIR, builtin: true)
22
- result = Result.new(loaded: [], skipped: [])
23
- Clacky::ApiExtension.reset_registry!
24
-
25
- # Load built-in (gem-shipped) extensions first (lowest priority)
26
- if builtin && Dir.exist?(BUILTIN_DIR)
27
- Dir.glob(File.join(BUILTIN_DIR, "*", "handler.rb")).sort.each do |handler_path|
28
- ext_dir = File.dirname(handler_path)
29
- ext_id = File.basename(ext_dir)
30
- next if ext_id.start_with?("_")
31
- load_one(ext_id, ext_dir, handler_path, result)
32
- end
33
- end
34
-
35
- # Load user extensions (higher priority — same ext_id overwrites built-in)
36
- if Dir.exist?(dir)
37
- Dir.glob(File.join(dir, "*", "handler.rb")).sort.each do |handler_path|
38
- ext_dir = File.dirname(handler_path)
39
- ext_id = File.basename(ext_dir)
40
- next if ext_id == DISABLED_DIR || ext_id.start_with?("_")
41
- load_one(ext_id, ext_dir, handler_path, result)
42
- end
43
- end
44
-
45
- @last_result = result
46
- log_summary(result)
47
- result
48
- end
49
-
50
- def last_result
51
- @last_result || load_all
52
- end
53
-
54
- def load_one(ext_id, ext_dir, handler_path, result)
55
- meta = read_meta(ext_dir)
56
- before = Clacky::ApiExtension.pending_subclasses.size
57
-
58
- require handler_path
59
-
60
- new_subclasses = Clacky::ApiExtension.pending_subclasses[before..] || []
61
- klass = new_subclasses.last
62
-
63
- unless klass
64
- result.skipped << [ext_id, "no Clacky::ApiExtension subclass defined in handler.rb"]
65
- log_skip(ext_id, result.skipped.last[1])
66
- return
67
- end
68
-
69
- klass.ext_id = ext_id
70
- klass.ext_dir = ext_dir
71
- klass.meta = meta
72
-
73
- if klass.routes.empty?
74
- result.skipped << [ext_id, "no routes declared (use get/post/... DSL)"]
75
- log_skip(ext_id, result.skipped.last[1])
76
- Clacky::ApiExtension.registry.delete(ext_id)
77
- return
78
- end
79
-
80
- if (gap = validate_public_endpoints(klass, meta))
81
- result.skipped << [ext_id, gap]
82
- log_skip(ext_id, gap)
83
- return
84
- end
85
-
86
- Clacky::ApiExtension.register(ext_id, klass)
87
- result.loaded << ext_id
88
- public_count = klass.public_paths.size
89
- suffix = public_count > 0 ? " (#{public_count} public)" : ""
90
- Clacky::Logger.info("[ApiExtensionLoader] Loaded '#{ext_id}' — #{klass.routes.size} route(s)#{suffix}")
91
- rescue StandardError, ScriptError => e
92
- result.skipped << [ext_id, e.message]
93
- log_skip(ext_id, e.message)
94
- end
95
-
96
- private def read_meta(ext_dir)
97
- path = File.join(ext_dir, "meta.yml")
98
- return {} unless File.exist?(path)
99
-
100
- YAMLCompat.load_file(path) || {}
101
- rescue StandardError => e
102
- Clacky::Logger.warn("[ApiExtensionLoader] Failed to read meta.yml in #{ext_dir}: #{e.message}")
103
- {}
104
- end
105
-
106
- private def validate_public_endpoints(klass, meta)
107
- return nil if klass.public_paths.empty?
108
- return nil if meta["public"] == true
109
-
110
- "uses public_endpoint but meta.yml is missing 'public: true'"
111
- end
112
-
113
- private def log_skip(ext_id, reason)
114
- Clacky::Logger.warn("[ApiExtensionLoader] Skipped '#{ext_id}': #{reason}")
115
- end
116
-
117
- private def log_summary(result)
118
- return if result.loaded.empty? && result.skipped.empty?
119
-
120
- total_routes = result.loaded.sum { |id| Clacky::ApiExtension.registry[id]&.routes&.size || 0 }
121
- Clacky::Logger.info("[ApiExtensionLoader] #{result.loaded.size} extension(s), #{total_routes} route(s); #{result.skipped.size} skipped")
122
- end
123
-
124
- # Generate a starter handler.rb at ~/.clacky/api_ext/<name>/handler.rb.
125
- # Returns the path to the generated file.
126
- def scaffold(name, dir: DEFAULT_DIR)
127
- slug = name.to_s.strip.downcase.gsub(/[^a-z0-9_-]+/, "-").gsub(/\A-+|-+\z/, "")
128
- raise ArgumentError, "invalid api_ext name: #{name.inspect}" if slug.empty?
129
-
130
- target_dir = File.join(dir, slug)
131
- path = File.join(target_dir, "handler.rb")
132
- raise ArgumentError, "api_ext already exists: #{path}" if File.exist?(path)
133
-
134
- FileUtils.mkdir_p(target_dir)
135
- File.write(path, skeleton(slug))
136
- path
137
- end
138
-
139
- private def skeleton(slug)
140
- const = slug.split(/[-_]/).map(&:capitalize).join + "Ext"
141
- <<~RUBY
142
- # frozen_string_literal: true
143
-
144
- # Custom HTTP API extension mounted at /api/ext/#{slug}/
145
- # Scaffolded by `clacky api_ext_new #{slug}` — fill in the routes you need.
146
- class #{const} < Clacky::ApiExtension
147
- get "/hello" do
148
- json(message: "hello from #{slug}")
149
- end
150
-
151
- # Examples — uncomment and adapt:
152
- #
153
- # post "/items" do
154
- # body = json_body
155
- # error!("name required", status: 422) unless body["name"]
156
- # File.write(data_path("items.json"), body.to_json)
157
- # json(ok: true)
158
- # end
159
- #
160
- # get "/items/:id" do
161
- # json(id: params[:id])
162
- # end
163
- end
164
- RUBY
165
- end
166
- end
167
- end
168
- end
@@ -1,3 +0,0 @@
1
- You are calm, precise, and helpful. You communicate clearly and concisely.
2
- You are honest about uncertainty and ask for clarification when needed.
3
- You take initiative but respect the user's preferences and decisions.
@@ -1 +0,0 @@
1
- (No user profile configured yet. To personalize, create ~/.clacky/agents/USER.md)
@@ -1,5 +0,0 @@
1
- name: coding
2
- description: AI coding assistant and technical co-founder
3
- panels:
4
- - git
5
- - time_machine
File without changes
@@ -1,2 +0,0 @@
1
- name: general
2
- description: A versatile digital employee living on your computer
@@ -1,3 +0,0 @@
1
- name: meeting
2
- description: Real-time meeting transcription and AI assistant
3
- version: "0.1.0"
@@ -1,81 +0,0 @@
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,380 +0,0 @@
1
- // ── Creator · view — Creator Hub rendering + publish UI + DOM wiring ───────
2
- //
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.
6
- //
7
- // Augments the `Creator` facade with onPanelShow and updateSidebarVisibility.
8
- //
9
- // Depends on: CreatorStore, I18n, Brand, Skills, Modal.
10
- // ───────────────────────────────────────────────────────────────────────────
11
-
12
- const CreatorView = (() => {
13
- let _domWired = false;
14
-
15
- function escapeHtml(s) {
16
- return String(s ?? "")
17
- .replace(/&/g, "&amp;").replace(/</g, "&lt;")
18
- .replace(/>/g, "&gt;").replace(/"/g, "&quot;");
19
- }
20
-
21
- function _t(key) {
22
- return I18n.t ? I18n.t(key) : key;
23
- }
24
-
25
- // ── Loading / error ─────────────────────────────────────────────────────
26
-
27
- function _renderLoading() {
28
- const cloudList = document.getElementById("creator-cloud-list");
29
- const localList = document.getElementById("creator-local-list");
30
- const placeholder = `<div class="creator-loading">${_t("creator.loading")}</div>`;
31
- if (cloudList) cloudList.innerHTML = placeholder;
32
- if (localList) localList.innerHTML = "";
33
- }
34
-
35
- function _renderError(payload) {
36
- const cloudList = document.getElementById("creator-cloud-list");
37
- if (cloudList) cloudList.innerHTML = `<div class="creator-empty creator-error">${escapeHtml(payload.message)}</div>`;
38
- const localList = document.getElementById("creator-local-list");
39
- if (localList) localList.innerHTML = "";
40
- }
41
-
42
- function _render(payload) {
43
- _renderCloudSection();
44
- _renderLocalSection();
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
- }
55
- }
56
-
57
- function _renderCloudSection() {
58
- const list = document.getElementById("creator-cloud-list");
59
- if (!list) return;
60
-
61
- const cloudSkills = CreatorStore.state.cloudSkills;
62
- if (cloudSkills.length === 0) {
63
- list.innerHTML = `<div class="creator-empty">${_t("creator.cloud.empty")}</div>`;
64
- return;
65
- }
66
-
67
- list.innerHTML = "";
68
- cloudSkills.forEach(skill => list.appendChild(_buildCloudCard(skill)));
69
- }
70
-
71
- function _renderLocalSection() {
72
- const list = document.getElementById("creator-local-list");
73
- if (!list) return;
74
-
75
- const localSkills = CreatorStore.state.localSkills;
76
- if (localSkills.length === 0) {
77
- list.innerHTML = `<div class="creator-empty">${_t("creator.local.empty")}</div>`;
78
- return;
79
- }
80
-
81
- list.innerHTML = "";
82
- localSkills.forEach(skill => list.appendChild(_buildLocalCard(skill)));
83
- }
84
-
85
- // ── Cloud card ───────────────────────────────────────────────────────────
86
-
87
- function _buildCloudCard(skill) {
88
- const card = document.createElement("div");
89
- card.className = "creator-skill-card creator-cloud-card";
90
- card.dataset.name = skill.name;
91
-
92
- const versionHtml = skill.version
93
- ? `<span class="creator-version">v${escapeHtml(skill.version)}</span>`
94
- : "";
95
-
96
- const downloadHtml = typeof skill.download_count === "number"
97
- ? `<span class="creator-dl-count" title="${_t("creator.downloads")}">
98
- <svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
99
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
100
- <polyline points="7 10 12 15 17 10"/>
101
- <line x1="12" y1="15" x2="12" y2="3"/>
102
- </svg>
103
- ${skill.download_count}
104
- </span>`
105
- : "";
106
-
107
- const changesHtml = skill.has_local_changes
108
- ? `<span class="creator-changes-badge" title="${_t("creator.hasLocalChanges")}">● ${_t("creator.changed")}</span>`
109
- : "";
110
-
111
- let actionBtnsHtml = "";
112
- if (skill.local_present) {
113
- if (skill.has_local_changes) {
114
- actionBtnsHtml = `
115
- <div class="creator-upload-wrap">
116
- <button class="btn-creator-publish" data-state="">
117
- <svg class="btn-upload-icon" xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
118
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
119
- <polyline points="17 8 12 3 7 8"/>
120
- <line x1="12" y1="3" x2="12" y2="15"/>
121
- </svg>
122
- <span class="btn-upload-label">${_t("creator.btn.update")}</span>
123
- </button>
124
- <div class="skill-upload-progress-wrap" style="display:none">
125
- <div class="skill-upload-progress-bar"></div>
126
- </div>
127
- </div>
128
- <button class="btn-creator-iterate" title="${_t("creator.btn.iterate")}">
129
- <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
130
- <polyline points="23 4 23 10 17 10"/>
131
- <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
132
- </svg>
133
- ${_t("creator.btn.iterate")}
134
- </button>`;
135
- } else {
136
- actionBtnsHtml = `
137
- <button class="btn-creator-up-to-date" disabled title="${_t("creator.btn.upToDate")}">
138
- <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
139
- <polyline points="20 6 9 17 4 12"/>
140
- </svg>
141
- ${_t("creator.btn.upToDate")}
142
- </button>
143
- <button class="btn-creator-iterate" title="${_t("creator.btn.iterate")}">
144
- <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
145
- <polyline points="23 4 23 10 17 10"/>
146
- <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
147
- </svg>
148
- ${_t("creator.btn.iterate")}
149
- </button>`;
150
- }
151
- }
152
-
153
- card.innerHTML = `
154
- <div class="creator-card-main">
155
- <div class="creator-card-info">
156
- <div class="creator-card-title">
157
- <span class="creator-skill-name">${escapeHtml(skill.name)}</span>
158
- <span class="creator-badge creator-badge-published">${_t("creator.badge.published")}</span>
159
- ${changesHtml}
160
- </div>
161
- <div class="creator-card-desc">${escapeHtml(skill.description || "")}</div>
162
- <div class="creator-card-meta">${versionHtml}${downloadHtml}</div>
163
- </div>
164
- <div class="creator-card-actions">${actionBtnsHtml}</div>
165
- </div>`;
166
-
167
- if (skill.local_present && skill.has_local_changes) {
168
- const btn = card.querySelector(".btn-creator-publish");
169
- const wrap = card.querySelector(".skill-upload-progress-wrap");
170
- const bar = card.querySelector(".skill-upload-progress-bar");
171
- btn.addEventListener("click", () => {
172
- if (btn.disabled) return;
173
- _publishSkill(skill.name, btn, wrap, bar, true, card, true);
174
- });
175
- }
176
-
177
- if (skill.local_present) {
178
- const iterBtn = card.querySelector(".btn-creator-iterate");
179
- if (iterBtn) iterBtn.addEventListener("click", () => _iterateSkill(skill.name));
180
- }
181
-
182
- return card;
183
- }
184
-
185
- // ── Local card ─────────────────────────────────────────────────────────
186
-
187
- function _buildLocalCard(skill) {
188
- const card = document.createElement("div");
189
- card.className = "creator-skill-card creator-local-card";
190
- card.dataset.name = skill.name;
191
-
192
- const shadowHtml = skill.shadowing_brand
193
- ? `<span class="creator-shadow-badge" title="${_t("creator.shadow.tooltip")}">⚡ ${_t("creator.shadow.label")}</span>`
194
- : "";
195
-
196
- card.innerHTML = `
197
- <div class="creator-card-main">
198
- <div class="creator-card-info">
199
- <div class="creator-card-title">
200
- <span class="creator-skill-name">${escapeHtml(skill.name)}</span>
201
- <span class="creator-badge creator-badge-local">${_t("creator.badge.unpublished")}</span>
202
- ${shadowHtml}
203
- </div>
204
- <div class="creator-card-desc">${escapeHtml(skill.description || "")}</div>
205
- </div>
206
- <div class="creator-card-actions">
207
- <div class="creator-upload-wrap">
208
- <button class="btn-creator-publish" data-state="">
209
- <svg class="btn-upload-icon" xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
210
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
211
- <polyline points="17 8 12 3 7 8"/>
212
- <line x1="12" y1="3" x2="12" y2="15"/>
213
- </svg>
214
- <span class="btn-upload-label">${_t("creator.btn.publish")}</span>
215
- </button>
216
- <div class="skill-upload-progress-wrap" style="display:none">
217
- <div class="skill-upload-progress-bar"></div>
218
- </div>
219
- </div>
220
- </div>
221
- </div>`;
222
-
223
- const btn = card.querySelector(".btn-creator-publish");
224
- const wrap = card.querySelector(".skill-upload-progress-wrap");
225
- const bar = card.querySelector(".skill-upload-progress-bar");
226
- btn.addEventListener("click", () => {
227
- if (btn.disabled) return;
228
- _publishSkill(skill.name, btn, wrap, bar, false, card, false);
229
- });
230
-
231
- return card;
232
- }
233
-
234
- // ── Publish UI ───────────────────────────────────────────────────────────
235
-
236
- async function _publishSkill(skillName, publishBtn, progressWrap, progressBar, force, card, isUpdate) {
237
- publishBtn.disabled = true;
238
- publishBtn.dataset.state = "uploading";
239
- const btnLabel = publishBtn.querySelector(".btn-upload-label");
240
- if (btnLabel) btnLabel.textContent = I18n.lang() === "zh" ? "发布中…" : "Publishing…";
241
-
242
- progressWrap.style.display = "block";
243
- progressBar.style.width = "0%";
244
-
245
- let animPct = 0;
246
- const animInterval = setInterval(() => {
247
- const remaining = 85 - animPct;
248
- animPct += Math.max(1, remaining * 0.08);
249
- if (animPct > 85) animPct = 85;
250
- progressBar.style.width = animPct + "%";
251
- }, 150);
252
-
253
- let alreadyExists = false;
254
- let skipFinalReset = false;
255
-
256
- try {
257
- const result = await Creator.publish(skillName, { force });
258
- clearInterval(animInterval);
259
-
260
- if (!result.ok) {
261
- alreadyExists = result.already_exists;
262
- throw new Error(result.error || "Publish failed");
263
- }
264
-
265
- progressBar.style.width = "100%";
266
- progressBar.dataset.state = "success";
267
- publishBtn.dataset.state = "success";
268
- if (btnLabel) btnLabel.textContent = I18n.lang() === "zh" ? "已发布 ✓" : "Published ✓";
269
-
270
- await new Promise(r => setTimeout(r, 1400));
271
- await Creator.load();
272
- skipFinalReset = true;
273
-
274
- } catch (e) {
275
- clearInterval(animInterval);
276
-
277
- progressBar.style.width = "100%";
278
- progressBar.dataset.state = "error";
279
- publishBtn.dataset.state = "error";
280
- if (btnLabel) btnLabel.textContent = I18n.lang() === "zh" ? "发布失败" : "Failed";
281
- console.error("[Creator] publish failed", e);
282
- publishBtn.title = e.message;
283
- await new Promise(r => setTimeout(r, 2000));
284
-
285
- if (alreadyExists && !force) {
286
- skipFinalReset = true;
287
-
288
- publishBtn.disabled = false;
289
- publishBtn.dataset.state = "";
290
- if (btnLabel) btnLabel.textContent = I18n.lang() === "zh" ? "更新" : "Update";
291
- progressWrap.style.display = "none";
292
- progressBar.style.width = "0%";
293
- delete progressBar.dataset.state;
294
-
295
- const confirmed = await Modal.confirm(
296
- I18n.lang() === "zh"
297
- ? `"${skillName}" 已存在于平台。\n\n是否用当前版本覆盖?`
298
- : `"${skillName}" already exists on the platform.\n\nOverwrite with the current version?`
299
- );
300
- if (confirmed) {
301
- _publishSkill(skillName, publishBtn, progressWrap, progressBar, true, card, isUpdate);
302
- }
303
- }
304
- } finally {
305
- if (!skipFinalReset) {
306
- publishBtn.disabled = false;
307
- publishBtn.dataset.state = "";
308
- if (btnLabel) {
309
- btnLabel.textContent = isUpdate
310
- ? (I18n.lang() === "zh" ? "更新" : "Update")
311
- : (I18n.lang() === "zh" ? "发布" : "Publish");
312
- }
313
- progressWrap.style.display = "none";
314
- progressBar.style.width = "0%";
315
- delete progressBar.dataset.state;
316
- }
317
- }
318
- }
319
-
320
- function _iterateSkill(skillName) {
321
- Skills.createInSession(`/skill-creator ${I18n.t("creator.iterate.prompt")}${skillName}`);
322
- }
323
-
324
- function _wireNewSkillEntry() {
325
- const entry = document.getElementById("creator-new-entry");
326
- if (!entry || entry.dataset.wired) return;
327
- entry.dataset.wired = "1";
328
- entry.addEventListener("click", () => Skills.createInSession());
329
- }
330
-
331
- function _showNotice(msg, type = "warn") {
332
- const container = document.getElementById("creator-cloud-list");
333
- if (!container) return;
334
- const old = container.querySelector(".creator-notice");
335
- if (old) old.remove();
336
- const el = document.createElement("div");
337
- el.className = `creator-notice creator-notice-${type}`;
338
- el.textContent = msg;
339
- container.prepend(el);
340
- }
341
-
342
- function _subscribe() {
343
- Creator.on("creator:loading", _renderLoading);
344
- Creator.on("creator:changed", _render);
345
- Creator.on("creator:error", _renderError);
346
- }
347
-
348
- const viewApi = {
349
- /** Show/hide the creator section.
350
- * Hidden for brand consumer users (branded=true, userLicensed=false).
351
- * Visible for creators (userLicensed=true) and unbranded users. */
352
- updateSidebarVisibility() {
353
- const section = document.getElementById("creator-section");
354
- if (!section) return;
355
- const isBrandConsumer = Brand.branded && !Brand.userLicensed;
356
- section.style.display = isBrandConsumer ? "none" : "";
357
- },
358
-
359
- /** Called by Router when the creator panel becomes active. */
360
- onPanelShow() {
361
- if (!_domWired) {
362
- _domWired = true;
363
- _wireNewSkillEntry();
364
- }
365
- const licensed = Brand.userLicensed;
366
- const banner = document.getElementById("creator-promo-banner");
367
- const lock = document.getElementById("creator-cloud-lock");
368
- const list = document.getElementById("creator-cloud-list");
369
- if (banner) banner.style.display = licensed ? "none" : "";
370
- if (lock) lock.style.display = licensed ? "none" : "";
371
- if (list) list.style.display = licensed ? "" : "none";
372
- Creator.load();
373
- },
374
- };
375
-
376
- return { init: _subscribe, api: viewApi };
377
- })();
378
-
379
- Object.assign(Creator, CreatorView.api);
380
- CreatorView.init();