collavre 0.15.0 → 0.20.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/app/assets/stylesheets/collavre/code_highlight.css +13 -0
- data/app/assets/stylesheets/collavre/comments_popup.css +65 -5
- data/app/assets/stylesheets/collavre/creatives.css +5 -2
- data/app/controllers/collavre/admin/settings_controller.rb +9 -0
- data/app/controllers/collavre/attachments_controller.rb +1 -1
- data/app/controllers/collavre/comment_read_pointers_controller.rb +2 -2
- data/app/controllers/collavre/comments/snapshots_controller.rb +2 -7
- data/app/controllers/collavre/comments_controller.rb +23 -25
- data/app/controllers/collavre/creatives_controller.rb +22 -11
- data/app/controllers/collavre/emails_controller.rb +2 -0
- data/app/controllers/collavre/google_auth_controller.rb +1 -1
- data/app/controllers/collavre/inbox_items_controller.rb +1 -1
- data/app/controllers/collavre/invites_controller.rb +3 -0
- data/app/controllers/collavre/tasks_controller.rb +1 -1
- data/app/controllers/collavre/topics_controller.rb +27 -85
- data/app/controllers/concerns/collavre/comments/comment_scoping.rb +3 -0
- data/app/controllers/concerns/collavre/creative_permission_guard.rb +32 -0
- data/app/controllers/concerns/collavre/integration_setup.rb +17 -0
- data/app/helpers/collavre/application_helper.rb +30 -2
- data/app/javascript/components/InlineLexicalEditor.jsx +7 -3
- data/app/javascript/components/creative_tree_row.js +21 -1
- data/app/javascript/components/plugins/markdown_shortcuts_plugin.jsx +34 -0
- data/app/javascript/controllers/comment_controller.js +19 -1
- data/app/javascript/controllers/comment_version_controller.js +2 -1
- data/app/javascript/controllers/comments/form_controller.js +7 -4
- data/app/javascript/controllers/comments/list_controller.js +43 -4
- data/app/javascript/controllers/comments/popup_controller.js +45 -12
- data/app/javascript/controllers/comments/presence_controller.js +8 -0
- data/app/javascript/controllers/comments/topics_controller.js +50 -31
- data/app/javascript/creatives/tree_renderer.js +1 -0
- data/app/javascript/lib/__tests__/chat_history.test.js +31 -0
- data/app/javascript/lib/chat_history.js +12 -2
- data/app/javascript/lib/utils/markdown.js +32 -1
- data/app/javascript/modules/command_args_form.js +8 -0
- data/app/javascript/modules/creative_row_editor.js +12 -17
- data/app/javascript/modules/integration_wizard.js +162 -0
- data/app/jobs/collavre/compress_job.rb +1 -0
- data/app/jobs/collavre/creative_broadcast_job.rb +4 -1
- data/app/jobs/collavre/merge_comments_job.rb +1 -0
- data/app/jobs/collavre/trigger_loop_check_job.rb +1 -0
- data/app/jobs/collavre/trigger_loop_verify_job.rb +1 -0
- data/app/models/collavre/calendar_event.rb +0 -4
- data/app/models/collavre/comment/broadcastable.rb +1 -1
- data/app/models/collavre/comment.rb +17 -2
- data/app/models/collavre/comment_snapshot.rb +0 -1
- data/app/models/collavre/creative/describable.rb +10 -1
- data/app/models/collavre/creative/realtime_broadcastable.rb +17 -5
- data/app/models/collavre/creative.rb +43 -1
- data/app/models/collavre/current.rb +1 -1
- data/app/models/collavre/inbox_item.rb +0 -4
- data/app/models/collavre/system_setting.rb +10 -1
- data/app/models/collavre/task.rb +17 -8
- data/app/models/collavre/user.rb +11 -1
- data/app/models/concerns/collavre/ai_agent_resolvable.rb +0 -8
- data/app/services/collavre/ai_agent/message_builder.rb +32 -15
- data/app/services/collavre/ai_agent/response_finalizer.rb +2 -1
- data/app/services/collavre/ai_agent/session_context_resolver.rb +50 -0
- data/app/services/collavre/ai_agent_service.rb +14 -2
- data/app/services/collavre/ai_client.rb +13 -0
- data/app/services/collavre/command_menu_service.rb +23 -3
- data/app/services/collavre/comments/command_processor.rb +1 -1
- data/app/services/collavre/comments/mcp_command.rb +27 -8
- data/app/services/collavre/comments/mcp_command_builder.rb +4 -3
- data/app/services/collavre/creatives/tree_builder.rb +3 -0
- data/app/services/collavre/google_calendar_service.rb +32 -6
- data/app/services/collavre/markdown_converter.rb +15 -20
- data/app/services/collavre/mcp_service.rb +4 -4
- data/app/services/collavre/orchestration/agent_orchestrator.rb +2 -2
- data/app/services/collavre/tools/creative_batch_service.rb +5 -1
- data/app/services/collavre/tools/cron_create_service.rb +9 -6
- data/app/services/collavre/topic_branch_service.rb +2 -2
- data/app/views/collavre/admin/settings/_system_tab.html.erb +11 -0
- data/app/views/collavre/comments/_comment.html.erb +7 -1
- data/app/views/collavre/comments/_comments_popup.html.erb +4 -1
- data/config/locales/admin.en.yml +3 -0
- data/config/locales/admin.ko.yml +3 -0
- data/config/locales/comments.en.yml +6 -1
- data/config/locales/comments.ko.yml +6 -1
- data/db/migrate/20251126040752_add_description_to_creatives.rb +1 -1
- data/db/migrate/20260415000000_create_main_topics_for_existing_creatives.rb +69 -0
- data/lib/collavre/version.rb +1 -1
- metadata +21 -4
- data/app/jobs/collavre/permission_cache_cleanup_job.rb +0 -36
- data/app/models/collavre/variation.rb +0 -5
- data/app/services/collavre/creatives/path_exporter.rb +0 -131
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration Wizard — shared helpers for integration modal wizards.
|
|
3
|
+
*
|
|
4
|
+
* Provides common utilities used across Slack, Notion, and future
|
|
5
|
+
* integration wizard UIs: CSRF handling, error display, step management,
|
|
6
|
+
* OAuth popups, fetch wrappers, and modal close behavior.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* import { csrfToken, showError, ... } from 'collavre/modules/integration_wizard'
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Read the Rails CSRF token from the meta tag.
|
|
14
|
+
* @returns {string|undefined}
|
|
15
|
+
*/
|
|
16
|
+
export function csrfToken() {
|
|
17
|
+
return document.querySelector('meta[name="csrf-token"]')?.content;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Display an error message inside the given element.
|
|
22
|
+
* @param {HTMLElement} el - The error container element
|
|
23
|
+
* @param {string} message - Error text to display
|
|
24
|
+
*/
|
|
25
|
+
export function showError(el, message) {
|
|
26
|
+
if (!message) return;
|
|
27
|
+
el.textContent = message;
|
|
28
|
+
el.style.display = 'block';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Hide and clear the error element.
|
|
33
|
+
* @param {HTMLElement} el - The error container element
|
|
34
|
+
*/
|
|
35
|
+
export function clearError(el) {
|
|
36
|
+
el.style.display = 'none';
|
|
37
|
+
el.textContent = '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Show/hide step panes by step name.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} currentStep - The active step name
|
|
44
|
+
* @param {string[]} stepNames - All step names in order
|
|
45
|
+
* @param {string} prefix - DOM id prefix, e.g. 'slack-step' produces ids like 'slack-step-connect'
|
|
46
|
+
*/
|
|
47
|
+
export function updateStepVisibility(currentStep, stepNames, prefix) {
|
|
48
|
+
stepNames.forEach(function (step) {
|
|
49
|
+
const el = document.getElementById(`${prefix}-${step}`);
|
|
50
|
+
if (!el) return;
|
|
51
|
+
el.style.display = (step === currentStep) ? 'block' : 'none';
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Open a centered OAuth popup window and poll for its closure.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} windowName - Name for window.open (e.g. 'slack-auth-window')
|
|
59
|
+
* @param {Object} options
|
|
60
|
+
* @param {number} [options.width=600] - Popup width
|
|
61
|
+
* @param {number} [options.height=700] - Popup height
|
|
62
|
+
* @param {string} [options.url=''] - URL to open (pass '' if submitting a form to the window)
|
|
63
|
+
* @param {Function} [options.onClose] - Callback invoked when the popup is closed
|
|
64
|
+
* @returns {Window|null} The opened window reference
|
|
65
|
+
*/
|
|
66
|
+
export function openOAuthPopup(windowName, options = {}) {
|
|
67
|
+
const width = options.width || 600;
|
|
68
|
+
const height = options.height || 700;
|
|
69
|
+
const left = (screen.width - width) / 2;
|
|
70
|
+
const top = (screen.height - height) / 2;
|
|
71
|
+
const url = options.url || '';
|
|
72
|
+
|
|
73
|
+
const authWindow = window.open(
|
|
74
|
+
url,
|
|
75
|
+
windowName,
|
|
76
|
+
`width=${width},height=${height},left=${left},top=${top},scrollbars=yes,resizable=yes`
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (authWindow && options.onClose) {
|
|
80
|
+
const checkClosed = setInterval(() => {
|
|
81
|
+
if (authWindow.closed) {
|
|
82
|
+
clearInterval(checkClosed);
|
|
83
|
+
setTimeout(() => options.onClose(), 1000);
|
|
84
|
+
}
|
|
85
|
+
}, 1000);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return authWindow;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Perform a fetch request with CSRF token and JSON headers.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} url - Request URL
|
|
95
|
+
* @param {Object} [options={}]
|
|
96
|
+
* @param {string} [options.method='GET'] - HTTP method
|
|
97
|
+
* @param {Object} [options.body] - Body payload (will be JSON-stringified)
|
|
98
|
+
* @param {Object} [options.headers] - Additional headers (merged with defaults)
|
|
99
|
+
* @returns {Promise<Response>}
|
|
100
|
+
*/
|
|
101
|
+
export function fetchWithCsrf(url, options = {}) {
|
|
102
|
+
const method = options.method || 'GET';
|
|
103
|
+
const headers = {
|
|
104
|
+
'X-CSRF-Token': csrfToken(),
|
|
105
|
+
'Accept': 'application/json',
|
|
106
|
+
...options.headers,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Include Content-Type for requests with a body
|
|
110
|
+
if (options.body) {
|
|
111
|
+
headers['Content-Type'] = 'application/json';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const fetchOptions = { method, headers };
|
|
115
|
+
if (options.body) {
|
|
116
|
+
fetchOptions.body = JSON.stringify(options.body);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return fetch(url, fetchOptions);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Hide a list of DOM elements (set display to 'none') and optionally clear their content.
|
|
124
|
+
*
|
|
125
|
+
* @param {Array<{el: HTMLElement|null, clear?: boolean}>} items
|
|
126
|
+
* Each item has an `el` (may be null) and an optional `clear` flag.
|
|
127
|
+
* When `clear` is true, textContent or innerHTML is also reset.
|
|
128
|
+
*/
|
|
129
|
+
export function resetElements(items) {
|
|
130
|
+
items.forEach(function ({ el, clear }) {
|
|
131
|
+
if (!el) return;
|
|
132
|
+
el.style.display = 'none';
|
|
133
|
+
if (clear) {
|
|
134
|
+
if ('innerHTML' in el && el.tagName !== 'INPUT') {
|
|
135
|
+
el.textContent = '';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Set up common modal close behavior: close button, backdrop click, and Escape key.
|
|
143
|
+
*
|
|
144
|
+
* @param {HTMLElement} modal - The modal overlay element
|
|
145
|
+
* @param {HTMLElement} closeBtn - The close button element
|
|
146
|
+
* @param {Function} onClose - Callback to execute on close (e.g. resetWizard)
|
|
147
|
+
*/
|
|
148
|
+
export function setupModalClose(modal, closeBtn, onClose) {
|
|
149
|
+
if (closeBtn) {
|
|
150
|
+
closeBtn.addEventListener('click', function () {
|
|
151
|
+
modal.style.display = 'none';
|
|
152
|
+
onClose();
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
modal.addEventListener('click', function (e) {
|
|
157
|
+
if (e.target === modal) {
|
|
158
|
+
modal.style.display = 'none';
|
|
159
|
+
onClose();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -49,6 +49,7 @@ module Collavre
|
|
|
49
49
|
model: agent.llm_model,
|
|
50
50
|
system_prompt: system_prompt,
|
|
51
51
|
llm_api_key: agent.llm_api_key || agent.creator&.llm_api_key,
|
|
52
|
+
gateway_url: agent.gateway_url.presence || agent.creator&.gateway_url,
|
|
52
53
|
context: {
|
|
53
54
|
creative: creative,
|
|
54
55
|
user: agent,
|
|
@@ -233,8 +233,11 @@ module Collavre
|
|
|
233
233
|
|
|
234
234
|
svg = raw.dup
|
|
235
235
|
if options[:class]
|
|
236
|
-
|
|
236
|
+
escaped_class = ERB::Util.html_escape(options[:class])
|
|
237
|
+
svg.sub!(/<svg\b/, %(<svg class="#{escaped_class}"))
|
|
237
238
|
end
|
|
239
|
+
# Safe: SVG source is a static asset file bundled with the app;
|
|
240
|
+
# user-supplied class attribute is escaped above via html_escape.
|
|
238
241
|
svg.html_safe # rubocop:disable Rails/OutputSafety
|
|
239
242
|
end
|
|
240
243
|
|
|
@@ -48,6 +48,7 @@ module Collavre
|
|
|
48
48
|
model: agent.llm_model,
|
|
49
49
|
system_prompt: SYSTEM_PROMPT,
|
|
50
50
|
llm_api_key: agent.llm_api_key || agent.creator&.llm_api_key,
|
|
51
|
+
gateway_url: agent.gateway_url.presence || agent.creator&.gateway_url,
|
|
51
52
|
context: {
|
|
52
53
|
creative: creative,
|
|
53
54
|
user: agent,
|
|
@@ -233,6 +233,7 @@ module Collavre
|
|
|
233
233
|
model: verifier.llm_model,
|
|
234
234
|
system_prompt: LLM_FALLBACK_SYSTEM_PROMPT,
|
|
235
235
|
llm_api_key: verifier.llm_api_key || verifier.creator&.llm_api_key,
|
|
236
|
+
gateway_url: verifier.gateway_url.presence || verifier.creator&.gateway_url,
|
|
236
237
|
context: {}
|
|
237
238
|
)
|
|
238
239
|
|
|
@@ -151,6 +151,7 @@ module Collavre
|
|
|
151
151
|
model: verifier.llm_model,
|
|
152
152
|
system_prompt: VERIFY_SYSTEM_PROMPT,
|
|
153
153
|
llm_api_key: verifier.llm_api_key || verifier.creator&.llm_api_key,
|
|
154
|
+
gateway_url: verifier.gateway_url.presence || verifier.creator&.gateway_url,
|
|
154
155
|
context: {}
|
|
155
156
|
)
|
|
156
157
|
|
|
@@ -27,7 +27,7 @@ module Collavre
|
|
|
27
27
|
pointers = CommentReadPointer.where(user_id: user_ids, creative: origin).index_by(&:user_id)
|
|
28
28
|
present_user_ids = CommentPresenceStore.list(origin.id)
|
|
29
29
|
|
|
30
|
-
public_count = origin.comments.
|
|
30
|
+
public_count = origin.comments.public_only.count
|
|
31
31
|
private_counts = origin.comments
|
|
32
32
|
.where(private: true, user_id: user_ids)
|
|
33
33
|
.group(:user_id)
|
|
@@ -13,9 +13,12 @@ module Collavre
|
|
|
13
13
|
belongs_to :user, class_name: Collavre.configuration.user_class_name, optional: true
|
|
14
14
|
belongs_to :approver, class_name: Collavre.configuration.user_class_name, optional: true
|
|
15
15
|
belongs_to :action_executed_by, class_name: Collavre.configuration.user_class_name, optional: true
|
|
16
|
+
belongs_to :task, class_name: "Collavre::Task", optional: true
|
|
16
17
|
belongs_to :topic, class_name: "Collavre::Topic", optional: true
|
|
17
18
|
belongs_to :quoted_comment, class_name: "Collavre::Comment", optional: true
|
|
18
19
|
|
|
20
|
+
scope :public_only, -> { where(private: false) }
|
|
21
|
+
|
|
19
22
|
scope :visible_to, ->(user) {
|
|
20
23
|
where(
|
|
21
24
|
"comments.private = ? OR comments.user_id = ? OR comments.approver_id = ?",
|
|
@@ -49,6 +52,7 @@ module Collavre
|
|
|
49
52
|
|
|
50
53
|
before_validation :use_origin_creative
|
|
51
54
|
before_validation :assign_default_user, on: :create
|
|
55
|
+
before_validation :assign_main_topic, on: :create
|
|
52
56
|
before_save :apply_link_previews, if: :should_apply_link_previews?
|
|
53
57
|
after_create_commit :dispatch_to_orchestration
|
|
54
58
|
after_create_commit :resume_trigger_loop_if_awaiting
|
|
@@ -104,8 +108,10 @@ module Collavre
|
|
|
104
108
|
end
|
|
105
109
|
|
|
106
110
|
def cancel_pending_tasks
|
|
107
|
-
# Cancel tasks triggered by this comment
|
|
108
|
-
|
|
111
|
+
# Cancel tasks triggered by this comment (no creative_id scoping —
|
|
112
|
+
# CommentMoveService can change comment.creative_id without updating
|
|
113
|
+
# existing tasks, so scoping would miss moved-comment tasks)
|
|
114
|
+
Task.where(status: %w[pending running queued]).find_each do |task|
|
|
109
115
|
if task.trigger_event_payload&.dig("comment", "id") == id
|
|
110
116
|
task.update!(status: "cancelled")
|
|
111
117
|
end
|
|
@@ -133,6 +139,7 @@ module Collavre
|
|
|
133
139
|
return unless user_id # nil user = system message
|
|
134
140
|
return if user&.ai_user? # AI replies use A2aDispatcher, not this callback
|
|
135
141
|
return unless creative
|
|
142
|
+
return if creative.inbox?
|
|
136
143
|
|
|
137
144
|
SystemEvents::Dispatcher.dispatch("comment_created", dispatch_payload)
|
|
138
145
|
rescue StandardError => e
|
|
@@ -205,6 +212,14 @@ module Collavre
|
|
|
205
212
|
)
|
|
206
213
|
end
|
|
207
214
|
|
|
215
|
+
def assign_main_topic
|
|
216
|
+
return if topic_id.present?
|
|
217
|
+
return unless creative
|
|
218
|
+
|
|
219
|
+
fallback = user || Collavre.current_user || creative.user
|
|
220
|
+
self.topic = creative.main_topic(fallback_user: fallback)
|
|
221
|
+
end
|
|
222
|
+
|
|
208
223
|
def use_origin_creative
|
|
209
224
|
return unless creative
|
|
210
225
|
self.creative = creative.effective_origin
|
|
@@ -6,6 +6,7 @@ module Collavre
|
|
|
6
6
|
included do
|
|
7
7
|
validates :description, presence: true, unless: -> { origin_id.present? }
|
|
8
8
|
validate :description_cannot_change_if_has_origin, on: :update
|
|
9
|
+
validate :description_cannot_change_if_github_source, on: :update
|
|
9
10
|
|
|
10
11
|
before_save :sanitize_description_html
|
|
11
12
|
after_destroy_commit :purge_description_attachments
|
|
@@ -60,7 +61,7 @@ module Collavre
|
|
|
60
61
|
blob.purge
|
|
61
62
|
rescue ActiveRecord::RecordNotFound, ActiveSupport::MessageVerifier::InvalidSignature
|
|
62
63
|
Rails.logger.warn("Creative##{id}: could not find blob for signed_id=#{signed_id}")
|
|
63
|
-
rescue => e
|
|
64
|
+
rescue StandardError => e
|
|
64
65
|
Rails.logger.error("Creative##{id}: failed to purge blob #{signed_id}: #{e.message}")
|
|
65
66
|
end
|
|
66
67
|
end
|
|
@@ -82,6 +83,14 @@ module Collavre
|
|
|
82
83
|
errors.add(:description, "cannot be changed directly when linked to an origin")
|
|
83
84
|
end
|
|
84
85
|
end
|
|
86
|
+
|
|
87
|
+
def description_cannot_change_if_github_source
|
|
88
|
+
return unless will_save_change_to_description?
|
|
89
|
+
return unless github_markdown?
|
|
90
|
+
return if @skip_github_validation
|
|
91
|
+
|
|
92
|
+
errors.add(:description, "cannot be changed directly for GitHub synced content")
|
|
93
|
+
end
|
|
85
94
|
end
|
|
86
95
|
end
|
|
87
96
|
end
|
|
@@ -27,7 +27,9 @@ module Collavre
|
|
|
27
27
|
# Skip broadcast when only progress changed (cascade from update_parent_progress).
|
|
28
28
|
# The original creative's broadcast already includes ancestor progress in its payload,
|
|
29
29
|
# so receivers update parent rows without needing separate broadcasts per ancestor.
|
|
30
|
-
|
|
30
|
+
# Exception: MCP requests must always broadcast because the browser has no HTTP
|
|
31
|
+
# response to update from — WebSocket is the only delivery channel.
|
|
32
|
+
return if progress_only_change? && !Collavre::Current.mcp_request
|
|
31
33
|
|
|
32
34
|
enqueue_broadcast(:updated, broadcast_node_payload)
|
|
33
35
|
rescue StandardError => e
|
|
@@ -58,11 +60,10 @@ module Collavre
|
|
|
58
60
|
def broadcast_creative_destroyed
|
|
59
61
|
return if @_destroy_broadcast_users.blank?
|
|
60
62
|
|
|
61
|
-
current_user_id = Collavre.current_user&.id
|
|
62
63
|
CreativeBroadcastJob.perform_later(
|
|
63
64
|
id,
|
|
64
65
|
"destroyed",
|
|
65
|
-
current_user_id:
|
|
66
|
+
current_user_id: broadcast_excludable_user_id,
|
|
66
67
|
payload: @_destroy_payload,
|
|
67
68
|
options: {
|
|
68
69
|
destroy_user_ids: @_destroy_broadcast_users.map(&:id),
|
|
@@ -134,11 +135,13 @@ module Collavre
|
|
|
134
135
|
|
|
135
136
|
# Enqueue broadcast as a background job to avoid blocking the request cycle.
|
|
136
137
|
# Payload is built synchronously (needs fresh DB state), then delivery is async.
|
|
138
|
+
# For MCP requests, current_user_id is nil so the job broadcasts to ALL users
|
|
139
|
+
# including the requester (whose browser relies solely on WebSocket updates).
|
|
137
140
|
def enqueue_broadcast(action, payload)
|
|
138
141
|
CreativeBroadcastJob.perform_later(
|
|
139
142
|
id,
|
|
140
143
|
action.to_s,
|
|
141
|
-
current_user_id:
|
|
144
|
+
current_user_id: broadcast_excludable_user_id,
|
|
142
145
|
payload: payload
|
|
143
146
|
)
|
|
144
147
|
end
|
|
@@ -153,7 +156,7 @@ module Collavre
|
|
|
153
156
|
CreativeBroadcastJob.perform_later(
|
|
154
157
|
creative_ids,
|
|
155
158
|
"batch_created",
|
|
156
|
-
current_user_id:
|
|
159
|
+
current_user_id: broadcast_excludable_user_id
|
|
157
160
|
)
|
|
158
161
|
end
|
|
159
162
|
|
|
@@ -251,6 +254,15 @@ module Collavre
|
|
|
251
254
|
Rails.logger.error "[CreativeBroadcast] Error finding users: #{e.message}"
|
|
252
255
|
[]
|
|
253
256
|
end
|
|
257
|
+
# Returns nil for MCP requests so broadcast includes all users;
|
|
258
|
+
# returns current_user's ID for web requests so the requester is excluded.
|
|
259
|
+
def self.broadcast_excludable_user_id
|
|
260
|
+
Collavre::Current.mcp_request ? nil : Collavre.current_user&.id
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def broadcast_excludable_user_id
|
|
264
|
+
RealtimeBroadcastable.broadcast_excludable_user_id
|
|
265
|
+
end
|
|
254
266
|
end
|
|
255
267
|
end
|
|
256
268
|
end
|
|
@@ -13,7 +13,7 @@ module Collavre
|
|
|
13
13
|
after_save :touch_subtree_on_move, if: :saved_change_to_parent_id?
|
|
14
14
|
after_save :fire_drop_trigger_on_move, if: :saved_change_to_parent_id?
|
|
15
15
|
after_create_commit :fire_drop_trigger_on_create, if: :parent_id?
|
|
16
|
-
|
|
16
|
+
after_create :create_main_topic
|
|
17
17
|
|
|
18
18
|
include Linkable
|
|
19
19
|
include Permissible
|
|
@@ -34,11 +34,19 @@ module Collavre
|
|
|
34
34
|
scope :inboxes, -> { where("data->>'kind' = 'inbox'") }
|
|
35
35
|
|
|
36
36
|
SYSTEM_TOPIC_NAME = "System"
|
|
37
|
+
MAIN_TOPIC_NAME = "Main"
|
|
38
|
+
CONTENT_TOPIC_NAME = "Content"
|
|
37
39
|
|
|
38
40
|
def inbox?
|
|
39
41
|
data&.dig("kind") == "inbox"
|
|
40
42
|
end
|
|
41
43
|
|
|
44
|
+
attr_accessor :skip_github_validation
|
|
45
|
+
|
|
46
|
+
def github_markdown?
|
|
47
|
+
data.is_a?(Hash) && data.dig("source", "type") == "github_markdown"
|
|
48
|
+
end
|
|
49
|
+
|
|
42
50
|
# Find or create the "System" topic for this inbox creative.
|
|
43
51
|
# Re-creates it if the user deletes it.
|
|
44
52
|
def system_topic(fallback_user: user)
|
|
@@ -47,6 +55,18 @@ module Collavre
|
|
|
47
55
|
end
|
|
48
56
|
end
|
|
49
57
|
|
|
58
|
+
def main_topic(fallback_user: user)
|
|
59
|
+
topics.find_or_create_by!(name: MAIN_TOPIC_NAME) do |topic|
|
|
60
|
+
topic.user = fallback_user
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def content_topic(fallback_user: user)
|
|
65
|
+
topics.find_or_create_by!(name: CONTENT_TOPIC_NAME) do |topic|
|
|
66
|
+
topic.user = fallback_user
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
50
70
|
# Find or create the inbox creative for a given user.
|
|
51
71
|
# Places it as a root creative (no parent) owned by the user.
|
|
52
72
|
def self.inbox_for(user)
|
|
@@ -124,6 +144,11 @@ module Collavre
|
|
|
124
144
|
Array(data&.dig("context_ids"))
|
|
125
145
|
end
|
|
126
146
|
|
|
147
|
+
# Returns the directly-configured disabled context IDs for this creative.
|
|
148
|
+
def disabled_context_ids
|
|
149
|
+
Array(data&.dig("disabled_context_ids"))
|
|
150
|
+
end
|
|
151
|
+
|
|
127
152
|
# Returns the effective context IDs: own + inherited from ancestors (deduplicated).
|
|
128
153
|
def effective_context_ids(visited_ids = Set.new)
|
|
129
154
|
return [] if visited_ids.include?(id)
|
|
@@ -134,6 +159,16 @@ module Collavre
|
|
|
134
159
|
(own + parent_ctx).uniq
|
|
135
160
|
end
|
|
136
161
|
|
|
162
|
+
# Returns the effective disabled context IDs: own + inherited from ancestors (deduplicated).
|
|
163
|
+
def effective_disabled_context_ids(visited_ids = Set.new)
|
|
164
|
+
return [] if visited_ids.include?(id)
|
|
165
|
+
|
|
166
|
+
visited_ids.add(id)
|
|
167
|
+
own = disabled_context_ids
|
|
168
|
+
parent_disabled = parent&.effective_disabled_context_ids(visited_ids) || []
|
|
169
|
+
(own + parent_disabled).uniq
|
|
170
|
+
end
|
|
171
|
+
|
|
137
172
|
# Returns context creatives (excludes self to avoid duplication).
|
|
138
173
|
def context_creatives
|
|
139
174
|
ids = effective_context_ids
|
|
@@ -287,5 +322,12 @@ module Collavre
|
|
|
287
322
|
|
|
288
323
|
DropTriggerJob.perform_later(parent_id, id)
|
|
289
324
|
end
|
|
325
|
+
|
|
326
|
+
def create_main_topic
|
|
327
|
+
effective_user = user || Collavre.current_user
|
|
328
|
+
return unless effective_user
|
|
329
|
+
|
|
330
|
+
topics.create!(name: MAIN_TOPIC_NAME, user: effective_user)
|
|
331
|
+
end
|
|
290
332
|
end
|
|
291
333
|
end
|
|
@@ -33,6 +33,9 @@ module Collavre
|
|
|
33
33
|
DEFAULT_LIGHT_THEME_ID = nil
|
|
34
34
|
DEFAULT_DARK_THEME_ID = nil
|
|
35
35
|
|
|
36
|
+
# Default LLM request timeout (seconds)
|
|
37
|
+
DEFAULT_LLM_REQUEST_TIMEOUT_SECONDS = 1800
|
|
38
|
+
|
|
36
39
|
# Default creative display settings (system-wide)
|
|
37
40
|
DEFAULT_DISPLAY_LEVEL = 6
|
|
38
41
|
DEFAULT_COMPLETION_MARK = ""
|
|
@@ -57,7 +60,7 @@ module Collavre
|
|
|
57
60
|
password_reset_rate_limit password_reset_rate_period_minutes
|
|
58
61
|
api_rate_limit api_rate_period_minutes auth_providers_disabled
|
|
59
62
|
creatives_login_required home_page_path default_light_theme_id default_dark_theme_id
|
|
60
|
-
display_level completion_mark
|
|
63
|
+
display_level completion_mark llm_request_timeout_seconds
|
|
61
64
|
].each { |k| Rails.cache.delete("system_setting:#{k}") }
|
|
62
65
|
end
|
|
63
66
|
|
|
@@ -181,5 +184,11 @@ module Collavre
|
|
|
181
184
|
value = cached_value("completion_mark")
|
|
182
185
|
value.nil? ? DEFAULT_COMPLETION_MARK : value
|
|
183
186
|
end
|
|
187
|
+
|
|
188
|
+
# LLM request timeout (seconds)
|
|
189
|
+
def self.llm_request_timeout_seconds
|
|
190
|
+
value = cached_value("llm_request_timeout_seconds")&.to_i
|
|
191
|
+
value.nil? || value < 30 ? DEFAULT_LLM_REQUEST_TIMEOUT_SECONDS : value
|
|
192
|
+
end
|
|
184
193
|
end
|
|
185
194
|
end
|
data/app/models/collavre/task.rb
CHANGED
|
@@ -7,10 +7,12 @@ module Collavre
|
|
|
7
7
|
belongs_to :parent_task, class_name: "Collavre::Task", optional: true
|
|
8
8
|
has_many :sub_tasks, class_name: "Collavre::Task", foreign_key: :parent_task_id, dependent: :destroy
|
|
9
9
|
belongs_to :creative, class_name: "Collavre::Creative", optional: true
|
|
10
|
+
has_one :reply_comment, class_name: "Collavre::Comment", foreign_key: :task_id, dependent: :nullify
|
|
10
11
|
|
|
11
12
|
validates :name, presence: true
|
|
12
13
|
|
|
13
14
|
after_update_commit :check_trigger_loop_completion, if: :trigger_loop_candidate?
|
|
15
|
+
after_update_commit :broadcast_stop_button_removal, if: :became_terminal?
|
|
14
16
|
|
|
15
17
|
scope :running_for_topic, ->(topic_id, creative_id = nil) {
|
|
16
18
|
rel = where(topic_id: topic_id, status: "running")
|
|
@@ -32,14 +34,6 @@ module Collavre
|
|
|
32
34
|
false
|
|
33
35
|
end
|
|
34
36
|
|
|
35
|
-
def workflow_parent?
|
|
36
|
-
workflow_state.present? && parent_task_id.nil?
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def all_sub_tasks_done?
|
|
40
|
-
sub_tasks.where.not(status: %w[done cancelled]).empty?
|
|
41
|
-
end
|
|
42
|
-
|
|
43
37
|
private
|
|
44
38
|
|
|
45
39
|
def trigger_loop_candidate?
|
|
@@ -56,6 +50,21 @@ module Collavre
|
|
|
56
50
|
trigger_topic_id.nil? || trigger_topic_id == topic_id
|
|
57
51
|
end
|
|
58
52
|
|
|
53
|
+
def became_terminal?
|
|
54
|
+
saved_change_to_attribute?("status") && status.in?(%w[done cancelled failed])
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def broadcast_stop_button_removal
|
|
58
|
+
comment = reply_comment
|
|
59
|
+
return unless comment
|
|
60
|
+
|
|
61
|
+
comment.broadcast_replace_to(
|
|
62
|
+
[ comment.creative, :comments ],
|
|
63
|
+
partial: "collavre/comments/comment",
|
|
64
|
+
locals: { comment: comment, streaming: false }
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
59
68
|
def check_trigger_loop_completion
|
|
60
69
|
loop_config = creative.data&.dig("trigger", "loop")
|
|
61
70
|
cooldown = (loop_config&.dig("cooldown_seconds") || 10).to_i
|
data/app/models/collavre/user.rb
CHANGED
|
@@ -77,7 +77,8 @@ module Collavre
|
|
|
77
77
|
"chat_history" => 50,
|
|
78
78
|
"chat_history_size" => 100_000,
|
|
79
79
|
"creative_children_level" => nil
|
|
80
|
-
}
|
|
80
|
+
},
|
|
81
|
+
"session" => { "enabled" => nil }
|
|
81
82
|
}.freeze
|
|
82
83
|
|
|
83
84
|
# Default creative children level when agent_conf doesn't specify one
|
|
@@ -112,6 +113,15 @@ module Collavre
|
|
|
112
113
|
level.nil? ? DEFAULT_CREATIVE_CHILDREN_LEVEL : level.to_i
|
|
113
114
|
end
|
|
114
115
|
|
|
116
|
+
# Whether this agent uses stateful sessions (incremental messaging).
|
|
117
|
+
# nil in agent_conf = auto-detect by vendor (openclaw → true).
|
|
118
|
+
def supports_session?
|
|
119
|
+
explicit = parsed_agent_conf.dig("session", "enabled")
|
|
120
|
+
return ActiveModel::Type::Boolean.new.cast(explicit) unless explicit.nil?
|
|
121
|
+
|
|
122
|
+
llm_vendor&.downcase == "openclaw"
|
|
123
|
+
end
|
|
124
|
+
|
|
115
125
|
encrypts :llm_api_key, deterministic: false
|
|
116
126
|
encrypts :google_access_token, deterministic: false
|
|
117
127
|
encrypts :google_refresh_token, deterministic: false
|