@goodandready/dsh-cron 0.1.4 → 0.1.6

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.
@@ -0,0 +1,62 @@
1
+ export async function chatStartHandler(ctx, req, res, parseJsonBody, sendJson, randomUUID) {
2
+ try {
3
+ if (req.method !== 'POST') {
4
+ sendJson(res, 405, { ok: false, error: 'Method not allowed' });
5
+ return;
6
+ }
7
+
8
+ const body = await parseJsonBody(req);
9
+ const userPrompt = (body.prompt || '').trim();
10
+ if (!userPrompt) {
11
+ sendJson(res, 400, { ok: false, error: 'Текст задачи не может быть пустым' });
12
+ return;
13
+ }
14
+
15
+ const agents = ctx.agents;
16
+ if (!agents || typeof agents.create !== 'function') {
17
+ sendJson(res, 500, { ok: false, error: 'Служба agents недоступна в DSH' });
18
+ return;
19
+ }
20
+
21
+ let createUserMessage = (m) => m;
22
+ try {
23
+ const llmModule = await import('@deepseek-ai/dsh-llm');
24
+ if (llmModule && typeof llmModule.createUserMessage === 'function') {
25
+ createUserMessage = llmModule.createUserMessage;
26
+ }
27
+ } catch {}
28
+
29
+ let mintSessionId = () => `cron-setup-${randomUUID()}`;
30
+ try {
31
+ const sessionModule = await import('@deepseek-ai/dsh-session');
32
+ if (sessionModule && typeof sessionModule.SessionId === 'function') {
33
+ const sid = mintSessionId();
34
+ mintSessionId = () => sessionModule.SessionId(sid);
35
+ }
36
+ } catch {}
37
+
38
+ const defaultSel = ctx.get('agentDefaultModel')?.currentSelection?.();
39
+ const provider = body.provider || defaultSel?.provider;
40
+ const model = body.model || defaultSel?.model;
41
+
42
+ const handle = await agents.create({
43
+ sessionId: mintSessionId(),
44
+ meta: {},
45
+ agentOptions: provider && model ? { provider, model } : undefined,
46
+ });
47
+
48
+ const { buildAgentCronPrompt } = await import('./prompt.js');
49
+ const fullPrompt = buildAgentCronPrompt(userPrompt);
50
+
51
+ await handle.agent.whenIdle();
52
+ handle.agent.followup(createUserMessage({
53
+ content: [{ type: 'text', text: fullPrompt }],
54
+ source: { kind: 'plugin', plugin: 'dsh-cron', form: 'cron-setup' },
55
+ }));
56
+
57
+ sendJson(res, 200, { ok: true, sessionId: handle.agent.session.id });
58
+ } catch (err) {
59
+ ctx.logger?.warn?.(`[dsh-cron] chat/start error: ${err.message}`);
60
+ sendJson(res, 500, { ok: false, error: err.message });
61
+ }
62
+ }
package/lib/client.js CHANGED
@@ -1,12 +1,12 @@
1
1
  window.__ModuleLoader__.load({
2
2
  id: '@goodandready/dsh-cron',
3
3
  factory: (require) => {
4
- var module = { exports: {} };
5
- var exports = module.exports;
6
- const React = require('react');
7
- const ReactDOM = require('react-dom/client');
4
+ var module = { exports: {} }
5
+ var exports = module.exports
6
+ const React = window.React || require('react');
7
+ const ReactDOM = window.ReactDOM || require('react-dom/client');
8
8
 
9
- const NS = 'dsh-cron';
9
+ const NS = 'cron';
10
10
  const PANEL = 'cron';
11
11
  const ACTIVATE_EVENT = 'dsh-panel-activate';
12
12
  const ACTIVE_ATTR = 'data-dsh-cron-active';
@@ -19,22 +19,26 @@ window.__ModuleLoader__.load({
19
19
  const ICON_TIMER = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>';
20
20
  const ICON_PLAY = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 21"></polygon></svg>';
21
21
  const ICON_PAUSE = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>';
22
- const ICON_SEARCH = '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>';
23
- const ICON_CHEVRON_DOWN = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"></polyline></svg>';
24
- const ICON_BELL = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="#f59e0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>';
25
- const ICON_CLIPBOARD = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="#a855f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>';
26
- const ICON_ACTIVITY = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="#10b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>';
22
+ const ICON_TRASH = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>';
23
+ const ICON_SEARCH = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>';
24
+ const ICON_CHEVRON_DOWN = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>';
25
+ const ICON_BELL = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>';
26
+ const ICON_CLIPBOARD = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>';
27
+ const ICON_ACTIVITY = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>';
28
+ const ICON_SEND = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>';
27
29
 
28
30
  const STYLES = `
29
- [data-pane=conversation],[class*=centerCol]{position:relative;}
30
- [data-dsh-cron-view]{z-index:60;background:var(--dsw-alias-bg-base,#171717);display:none;position:absolute;inset:0;}
31
- html[${ACTIVE_ATTR}]:not([data-dsh-ssh-active]) [data-dsh-cron-view]{display:block !important;}
32
- html[${ACTIVE_ATTR}]:not([data-dsh-ssh-active]) [data-pane=conversation]>:not([data-dsh-cron-view]),
33
- html[${ACTIVE_ATTR}]:not([data-dsh-ssh-active]) [class*=centerCol]>:not([data-dsh-cron-view]){display:none !important;}
34
-
35
- .dsh-cron-entry-clone { align-items: center; gap: 8px; display: flex; }
36
- .dsh-cron-entry-clone[data-active] { font-weight: 600; }
37
- .dsh-cron-entry-icon { flex: none; justify-content: center; align-items: center; display: inline-flex; }
31
+ html[${ACTIVE_ATTR}] [data-pane=conversation],
32
+ html[${ACTIVE_ATTR}] [class*=centerCol] {
33
+ position: relative !important;
34
+ }
35
+ html[${ACTIVE_ATTR}] [data-pane=conversation] > *:not([${VIEW_ATTR}]),
36
+ html[${ACTIVE_ATTR}] [class*=centerCol] > *:not([${VIEW_ATTR}]) {
37
+ visibility: hidden !important;
38
+ }
39
+
40
+ .dsh-cron-entry-clone { display: flex; align-items: center; gap: 8px; }
41
+ .dsh-cron-entry-icon { display: flex; align-items: center; justify-content: center; flex: none; width: 16px; height: 16px; }
38
42
  .dsh-cron-entry-label { text-overflow: ellipsis; overflow: hidden; }
39
43
  [data-dsh-frame][data-sidebar-collapsed] .dsh-cron-entry-label { display: none; }
40
44
 
@@ -67,6 +71,7 @@ window.__ModuleLoader__.load({
67
71
  .dsh-cron-task-info { flex: 1; min-width: 0; }
68
72
  .dsh-cron-task-title { font-size: 15px; font-weight: 500; margin-bottom: 3px; }
69
73
  .dsh-cron-task-sched { font-size: 13px; color: #9ca3af; }
74
+ .dsh-cron-task-model-tag { display: inline-block; font-size: 11px; padding: 2px 7px; border-radius: 4px; background: rgba(255,255,255,0.06); color: #aaa; margin-left: 8px; vertical-align: middle; }
70
75
  .dsh-cron-task-actions { display: flex; gap: 8px; }
71
76
  .dsh-cron-icon-btn { background: transparent; border: 1px solid #333; border-radius: 8px; padding: 6px 12px; font-size: 12.5px; color: #aaa; cursor: pointer; }
72
77
  .dsh-cron-icon-btn:hover { background: #2a2a2a; color: #fff; }
@@ -81,15 +86,25 @@ window.__ModuleLoader__.load({
81
86
  .dsh-cron-rec-desc { font-size: 13px; color: #71717a; }
82
87
 
83
88
  .dsh-cron-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 1000; }
84
- .dsh-cron-modal { background: var(--dsw-alias-bg-layer-2, #1f1f1f); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 16px; width: 100%; max-width: 520px; padding: 24px; box-shadow: 0 20px 40px rgba(0,0,0,0.6); }
89
+ .dsh-cron-modal { background: var(--dsw-alias-bg-layer-2, #1f1f1f); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 16px; width: 100%; max-width: 540px; padding: 24px; box-shadow: 0 20px 40px rgba(0,0,0,0.6); box-sizing: border-box; }
85
90
  .dsh-cron-modal-title { font-size: 18px; font-weight: 600; margin-bottom: 18px; }
86
91
  .dsh-cron-form-group { margin-bottom: 16px; }
87
92
  .dsh-cron-form-group label { display: block; font-size: 13px; color: #aaa; margin-bottom: 6px; }
88
- .dsh-cron-form-group input, .dsh-cron-form-group textarea { width: 100%; background: #141414; border: 1px solid #333; border-radius: 8px; padding: 9px 12px; color: #fff; font-size: 13.5px; outline: none; box-sizing: border-box; }
93
+ .dsh-cron-form-group input, .dsh-cron-form-group textarea, .dsh-cron-form-group select { width: 100%; background: #141414; border: 1px solid #333; border-radius: 8px; padding: 9px 12px; color: #fff; font-size: 13.5px; outline: none; box-sizing: border-box; }
94
+ .dsh-cron-form-group select { cursor: pointer; }
89
95
  .dsh-cron-form-group textarea { min-height: 80px; resize: vertical; }
90
- .dsh-cron-modal-foot { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; }
91
- .dsh-cron-btn-primary { background: #ededed; color: #111; border: none; border-radius: 8px; padding: 7px 16px; font-size: 13.5px; font-weight: 500; cursor: pointer; }
96
+ .dsh-cron-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
97
+ .dsh-cron-modal-foot { display: flex; justify-content: flex-end; align-items: center; gap: 10px; margin-top: 20px; }
98
+ .dsh-cron-btn-primary { background: #ededed; color: #111; border: none; border-radius: 8px; padding: 7px 16px; font-size: 13.5px; font-weight: 500; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; }
99
+ .dsh-cron-btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
92
100
  .dsh-cron-btn-secondary { background: transparent; color: #999; border: 1px solid #383838; border-radius: 8px; padding: 7px 16px; font-size: 13.5px; cursor: pointer; }
101
+ .dsh-cron-chat-box { position: relative; background: #141414; border: 1px solid #333; border-radius: 12px; padding: 12px; }
102
+ .dsh-cron-chat-input { width: 100%; background: transparent; border: none; color: #fff; font-size: 14px; min-height: 100px; resize: vertical; outline: none; box-sizing: border-box; line-height: 1.5; }
103
+ .dsh-cron-chat-foot { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; padding-top: 8px; border-top: 1px solid #222; }
104
+ .dsh-cron-chat-hint { font-size: 12px; color: #666; }
105
+ .dsh-cron-send-btn { width: 34px; height: 34px; border-radius: 8px; background: #ededed; color: #111; border: none; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: transform 0.1s, background 0.15s; }
106
+ .dsh-cron-send-btn:hover:not(:disabled) { background: #fff; transform: translateY(-1px); }
107
+ .dsh-cron-send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
93
108
 
94
109
  .dsh-cron-card { background: var(--dsw-alias-bg-layer-2, #1f1f1f); border: 1px solid var(--dsw-alias-border-l1, #333); border-radius: 12px; padding: 18px 20px; margin-bottom: 16px; }
95
110
  .dsh-cron-card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
@@ -129,28 +144,55 @@ window.__ModuleLoader__.load({
129
144
  };
130
145
  }
131
146
 
147
+ function openSession(ctx, sessionId) {
148
+ if (!sessionId) return false;
149
+ const sessions = ctx && typeof ctx.get === 'function' ? ctx.get('sessions') : ctx?.sessions;
150
+ if (!sessions || typeof sessions.open !== 'function') return false;
151
+ try {
152
+ sessions.open(sessionId);
153
+ return true;
154
+ } catch {
155
+ return false;
156
+ }
157
+ }
158
+
132
159
  function CronScreen(props) {
133
- const { toggle, onClose } = props;
160
+ const { ctx, toggle, onClose } = props;
134
161
  const [tab, setTab] = React.useState('all');
135
162
  const [query, setQuery] = React.useState('');
136
163
  const [tasks, setTasks] = React.useState([]);
137
164
  const [recs, setRecs] = React.useState([]);
138
165
  const [dropdownOpen, setDropdownOpen] = React.useState(false);
139
- const [modalOpen, setModalOpen] = React.useState(false);
166
+
167
+ // Manual modal state
168
+ const [manualModalOpen, setManualModalOpen] = React.useState(false);
140
169
  const [formTitle, setFormTitle] = React.useState('');
141
170
  const [formSchedule, setFormSchedule] = React.useState('');
142
171
  const [formPrompt, setFormPrompt] = React.useState('');
172
+ const [formProvider, setFormProvider] = React.useState('');
173
+ const [formModel, setFormModel] = React.useState('');
174
+
175
+ // DSH dialog state
176
+ const [dshModalOpen, setDshModalOpen] = React.useState(false);
177
+ const [dshPrompt, setDshPrompt] = React.useState('');
178
+ const [dshLoading, setDshLoading] = React.useState(false);
179
+
180
+ // Models list state
181
+ const [providers, setProviders] = React.useState([]);
182
+ const [models, setModels] = React.useState([]);
183
+ const [defaultModelInfo, setDefaultModelInfo] = React.useState(null);
143
184
 
144
185
  React.useEffect(() => {
145
186
  const onKey = (e) => {
146
187
  if (e.key !== 'Escape') return;
147
- if (modalOpen) { setModalOpen(false); return; }
188
+ if (manualModalOpen) { setManualModalOpen(false); return; }
189
+ if (dshModalOpen) { setDshModalOpen(false); return; }
148
190
  if (dropdownOpen) { setDropdownOpen(false); return; }
149
191
  if (onClose) onClose();
150
192
  };
151
193
  document.addEventListener('keydown', onKey);
152
194
  return () => document.removeEventListener('keydown', onKey);
153
- }, [modalOpen, dropdownOpen, onClose]);
195
+ }, [manualModalOpen, dshModalOpen, dropdownOpen, onClose]);
154
196
 
155
197
  const fetchTasks = async () => {
156
198
  try {
@@ -163,7 +205,21 @@ window.__ModuleLoader__.load({
163
205
  } catch (err) {}
164
206
  };
165
207
 
208
+ const loadModels = async (providerToFetch) => {
209
+ try {
210
+ const url = '/dsh-cron/models' + (providerToFetch ? '?provider=' + encodeURIComponent(providerToFetch) : '');
211
+ const res = await fetch(url);
212
+ const data = await res.json();
213
+ if (data) {
214
+ if (data.providers) setProviders(data.providers);
215
+ if (data.models) setModels(data.models);
216
+ if (data.current) setDefaultModelInfo(data.current);
217
+ }
218
+ } catch (err) {}
219
+ };
220
+
166
221
  React.useEffect(() => { fetchTasks(); }, [tab, query]);
222
+ React.useEffect(() => { loadModels(); }, []);
167
223
 
168
224
  const handleToggleTask = async (id) => {
169
225
  try {
@@ -188,21 +244,34 @@ window.__ModuleLoader__.load({
188
244
  } catch (err) {}
189
245
  };
190
246
 
191
- const openCreateModal = (preset) => {
247
+ const openManualModal = (preset) => {
192
248
  setDropdownOpen(false);
193
249
  if (preset) {
194
250
  setFormTitle(preset.title || '');
195
251
  setFormSchedule(preset.schedule || '');
196
252
  setFormPrompt(preset.prompt || '');
253
+ setFormProvider(defaultModelInfo?.provider || '');
254
+ setFormModel(defaultModelInfo?.model || '');
255
+ if (defaultModelInfo?.provider) loadModels(defaultModelInfo.provider);
197
256
  } else {
198
257
  setFormTitle('');
199
258
  setFormSchedule('0 9 * * 1-5');
200
259
  setFormPrompt('');
260
+ setFormProvider(defaultModelInfo?.provider || '');
261
+ setFormModel(defaultModelInfo?.model || '');
262
+ if (defaultModelInfo?.provider) loadModels(defaultModelInfo.provider);
201
263
  }
202
- setModalOpen(true);
264
+ setManualModalOpen(true);
265
+ };
266
+
267
+ const handleProviderChange = (e) => {
268
+ const prov = e.target.value;
269
+ setFormProvider(prov);
270
+ setFormModel('');
271
+ loadModels(prov);
203
272
  };
204
273
 
205
- const handleSave = async (e) => {
274
+ const handleSaveManual = async (e) => {
206
275
  e.preventDefault();
207
276
  try {
208
277
  const res = await fetch('/dsh-cron/tasks', {
@@ -211,27 +280,54 @@ window.__ModuleLoader__.load({
211
280
  body: JSON.stringify({
212
281
  title: formTitle,
213
282
  schedule: formSchedule,
214
- prompt: formPrompt
283
+ prompt: formPrompt,
284
+ provider: formProvider || undefined,
285
+ model: formModel || undefined,
215
286
  })
216
287
  });
217
288
  const data = await res.json();
218
289
  if (!data.ok) { alert(data.error || 'Ошибка'); return; }
219
- setModalOpen(false);
290
+ setManualModalOpen(false);
220
291
  fetchTasks();
221
292
  } catch (err) { alert(err.message); }
222
293
  };
223
294
 
224
- const handleCreateWithDSH = () => {
295
+ const openDshModal = () => {
225
296
  setDropdownOpen(false);
226
- if (onClose) onClose();
227
- else toggle.set(false);
228
- setTimeout(() => {
229
- const textarea = document.querySelector('textarea[placeholder*="message"], textarea[class*="prompt"]');
230
- if (textarea) {
231
- textarea.value = 'Помоги мне настроить новую запланированную задачу cron: ';
232
- textarea.focus();
297
+ setDshPrompt('');
298
+ setDshModalOpen(true);
299
+ };
300
+
301
+ const handleStartWithDSH = async () => {
302
+ const text = dshPrompt.trim();
303
+ if (!text) return;
304
+
305
+ setDshLoading(true);
306
+ try {
307
+ const res = await fetch('/dsh-cron/chat/start', {
308
+ method: 'POST',
309
+ headers: { 'Content-Type': 'application/json' },
310
+ body: JSON.stringify({ prompt: text })
311
+ });
312
+ const data = await res.json();
313
+ if (!data.ok || !data.sessionId) {
314
+ alert(data.error || 'Не удалось запустить диалог с агентом');
315
+ setDshLoading(false);
316
+ return;
317
+ }
318
+
319
+ setDshModalOpen(false);
320
+ if (onClose) onClose();
321
+ else toggle.set(false);
322
+
323
+ if (!openSession(ctx, data.sessionId)) {
324
+ alert('Сессия создана (' + data.sessionId + '), перейдите к ней в списке чатов');
233
325
  }
234
- }, 150);
326
+ } catch (err) {
327
+ alert('Ошибка запуска: ' + err.message);
328
+ } finally {
329
+ setDshLoading(false);
330
+ }
235
331
  };
236
332
 
237
333
  return React.createElement('div', { className: 'dsh-cron-overlay' },
@@ -261,8 +357,8 @@ window.__ModuleLoader__.load({
261
357
  React.createElement('span', { dangerouslySetInnerHTML: { __html: ICON_CHEVRON_DOWN } })
262
358
  ),
263
359
  dropdownOpen && React.createElement('div', { className: 'dsh-cron-dropdown' },
264
- React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: handleCreateWithDSH }, '💬 Создать с DSH'),
265
- React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: () => openCreateModal(null) }, '✏️ Настроить вручную')
360
+ React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: openDshModal }, '💬 Создать с DSH'),
361
+ React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: () => openManualModal(null) }, '✏️ Настроить вручную')
266
362
  )
267
363
  )
268
364
  ),
@@ -282,46 +378,58 @@ window.__ModuleLoader__.load({
282
378
  className: 'dsh-cron-tab',
283
379
  'data-active': tab === t ? 'true' : undefined,
284
380
  onClick: () => setTab(t)
285
- },
286
- t === 'all' ? 'Все' : t === 'active' ? 'Активные' : t === 'paused' ? 'Приостановленные' : 'Завершено'
287
- )
381
+ }, t === 'all' ? 'Все' : t === 'active' ? 'Активные' : t === 'paused' ? 'На паузе' : 'Завершенные')
288
382
  )
289
383
  ),
290
384
  React.createElement('div', { className: 'dsh-cron-task-list' },
291
- tasks.length === 0 ? React.createElement('div', { style: { padding: '24px', textAlign: 'center', color: '#666' } }, 'Нет задач в выбранном статусе') :
292
- tasks.map((task) =>
293
- React.createElement('div', { key: task.id, className: 'dsh-cron-task-item' },
294
- React.createElement('button', {
295
- className: 'dsh-cron-task-status-btn',
296
- 'data-active': task.status === 'active' ? 'true' : undefined,
297
- onClick: () => handleToggleTask(task.id),
298
- dangerouslySetInnerHTML: { __html: task.status === 'active' ? ICON_PLAY : ICON_PAUSE }
299
- }),
300
- React.createElement('div', { className: 'dsh-cron-task-info' },
301
- React.createElement('div', { className: 'dsh-cron-task-title' }, task.title),
302
- React.createElement('div', { className: 'dsh-cron-task-sched' }, task.scheduleText || task.schedule)
303
- ),
304
- React.createElement('div', { className: 'dsh-cron-task-actions' },
305
- React.createElement('button', { className: 'dsh-cron-icon-btn', onClick: () => handleRunNow(task.id) }, 'Запустить'),
306
- React.createElement('button', { className: 'dsh-cron-icon-btn', onClick: () => handleDelete(task.id) }, 'Удалить')
385
+ tasks.length === 0
386
+ ? React.createElement('div', { style: { color: '#777', padding: '16px 0', fontSize: '14px' } }, 'Нет запланированных задач')
387
+ : tasks.map((task) =>
388
+ React.createElement('div', { key: task.id, className: 'dsh-cron-task-item' },
389
+ React.createElement('button', {
390
+ className: 'dsh-cron-task-status-btn',
391
+ 'data-active': task.status === 'active' ? 'true' : undefined,
392
+ title: task.status === 'active' ? 'Приостановить' : 'Возобновить',
393
+ onClick: () => handleToggleTask(task.id)
394
+ }, React.createElement('span', { dangerouslySetInnerHTML: { __html: task.status === 'active' ? ICON_PAUSE : ICON_PLAY } })),
395
+ React.createElement('div', { className: 'dsh-cron-task-info' },
396
+ React.createElement('div', { className: 'dsh-cron-task-title' },
397
+ task.title,
398
+ task.model ? React.createElement('span', { className: 'dsh-cron-task-model-tag' }, task.model.split('/').pop()) : null
399
+ ),
400
+ React.createElement('div', { className: 'dsh-cron-task-sched' },
401
+ task.scheduleText || task.schedule,
402
+ task.nextRunAt ? ` · след: ${new Date(task.nextRunAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` : ''
403
+ )
404
+ ),
405
+ React.createElement('div', { className: 'dsh-cron-task-actions' },
406
+ React.createElement('button', {
407
+ className: 'dsh-cron-icon-btn',
408
+ title: 'Запустить немедленно',
409
+ onClick: () => handleRunNow(task.id)
410
+ }, 'Запустить'),
411
+ React.createElement('button', {
412
+ className: 'dsh-cron-icon-btn',
413
+ title: 'Удалить задачу',
414
+ onClick: () => handleDelete(task.id)
415
+ }, React.createElement('span', { dangerouslySetInnerHTML: { __html: ICON_TRASH } }))
416
+ )
417
+ )
307
418
  )
308
- )
309
- )
310
419
  ),
311
- React.createElement('div', { className: 'dsh-cron-recs-title' }, 'Рекомендации'),
420
+ React.createElement('div', { className: 'dsh-cron-recs-title' }, 'Рекомендуемые задачи'),
312
421
  React.createElement('div', { className: 'dsh-cron-recs-list' },
313
422
  recs.map((rec) =>
314
423
  React.createElement('div', {
315
424
  key: rec.id,
316
425
  className: 'dsh-cron-rec-card',
317
- onClick: () => openCreateModal(rec)
426
+ onClick: () => openManualModal(rec)
318
427
  },
319
- React.createElement('div', {
320
- className: 'dsh-cron-rec-icon',
321
- dangerouslySetInnerHTML: {
428
+ React.createElement('div', { className: 'dsh-cron-rec-icon' },
429
+ React.createElement('span', { dangerouslySetInnerHTML: {
322
430
  __html: rec.icon === 'bell' ? ICON_BELL : rec.icon === 'clipboard' ? ICON_CLIPBOARD : ICON_ACTIVITY
323
- }
324
- }),
431
+ } })
432
+ ),
325
433
  React.createElement('div', { className: 'dsh-cron-rec-content' },
326
434
  React.createElement('div', { className: 'dsh-cron-rec-title' },
327
435
  rec.title,
@@ -333,10 +441,12 @@ window.__ModuleLoader__.load({
333
441
  )
334
442
  )
335
443
  ),
336
- modalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setModalOpen(false) },
444
+
445
+ // 1. Manual creation modal
446
+ manualModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setManualModalOpen(false) },
337
447
  React.createElement('div', { className: 'dsh-cron-modal', onClick: (e) => e.stopPropagation() },
338
448
  React.createElement('div', { className: 'dsh-cron-modal-title' }, 'Настройка запланированной задачи'),
339
- React.createElement('form', { onSubmit: handleSave },
449
+ React.createElement('form', { onSubmit: handleSaveManual },
340
450
  React.createElement('div', { className: 'dsh-cron-form-group' },
341
451
  React.createElement('label', null, 'Название задачи'),
342
452
  React.createElement('input', {
@@ -355,6 +465,28 @@ window.__ModuleLoader__.load({
355
465
  onChange: (e) => setFormSchedule(e.target.value)
356
466
  })
357
467
  ),
468
+ React.createElement('div', { className: 'dsh-cron-form-row' },
469
+ React.createElement('div', { className: 'dsh-cron-form-group' },
470
+ React.createElement('label', null, 'Провайдер LLM'),
471
+ React.createElement('select', {
472
+ value: formProvider,
473
+ onChange: handleProviderChange
474
+ },
475
+ React.createElement('option', { value: '' }, 'По умолчанию (DSH)'),
476
+ providers.map((p) => React.createElement('option', { key: p.id, value: p.id }, p.name || p.id))
477
+ )
478
+ ),
479
+ React.createElement('div', { className: 'dsh-cron-form-group' },
480
+ React.createElement('label', null, 'Модель LLM'),
481
+ React.createElement('select', {
482
+ value: formModel,
483
+ onChange: (e) => setFormModel(e.target.value)
484
+ },
485
+ React.createElement('option', { value: '' }, 'По умолчанию'),
486
+ models.map((m) => React.createElement('option', { key: m.id, value: m.id }, m.name || m.id))
487
+ )
488
+ )
489
+ ),
358
490
  React.createElement('div', { className: 'dsh-cron-form-group' },
359
491
  React.createElement('label', null, 'Промпт / Инструкция для агента'),
360
492
  React.createElement('textarea', {
@@ -368,7 +500,7 @@ window.__ModuleLoader__.load({
368
500
  React.createElement('button', {
369
501
  type: 'button',
370
502
  className: 'dsh-cron-btn-secondary',
371
- onClick: () => setModalOpen(false)
503
+ onClick: () => setManualModalOpen(false)
372
504
  }, 'Отмена'),
373
505
  React.createElement('button', {
374
506
  type: 'submit',
@@ -377,6 +509,56 @@ window.__ModuleLoader__.load({
377
509
  )
378
510
  )
379
511
  )
512
+ ),
513
+
514
+ // 2. "Создать с DSH" modal
515
+ dshModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => !dshLoading && setDshModalOpen(false) },
516
+ React.createElement('div', { className: 'dsh-cron-modal', onClick: (e) => e.stopPropagation() },
517
+ React.createElement('div', { className: 'dsh-cron-modal-title' }, 'Запланировать задачу с DSH Агентом'),
518
+ React.createElement('div', { className: 'dsh-cron-form-group' },
519
+ React.createElement('label', null, 'Опишите задачу и желаемую периодичность:'),
520
+ React.createElement('div', { className: 'dsh-cron-chat-box' },
521
+ React.createElement('textarea', {
522
+ className: 'dsh-cron-chat-input',
523
+ rows: 4,
524
+ autoFocus: true,
525
+ disabled: dshLoading,
526
+ placeholder: 'Например: проверяй каждые 2 часа свободное место на сервере и наличие упавших systemd-сервисов, присылай алерт только если что-то не так...',
527
+ value: dshPrompt,
528
+ onKeyDown: (e) => {
529
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
530
+ e.preventDefault();
531
+ handleStartWithDSH();
532
+ }
533
+ },
534
+ onChange: (e) => setDshPrompt(e.target.value)
535
+ }),
536
+ React.createElement('div', { className: 'dsh-cron-chat-foot' },
537
+ React.createElement('span', { className: 'dsh-cron-chat-hint' }, 'Ctrl+Enter для отправки'),
538
+ React.createElement('button', {
539
+ type: 'button',
540
+ className: 'dsh-cron-send-btn',
541
+ disabled: dshLoading || !dshPrompt.trim(),
542
+ title: 'Отправить агенту',
543
+ onClick: handleStartWithDSH
544
+ },
545
+ React.createElement('span', {
546
+ style: { display: 'flex', alignItems: 'center' },
547
+ dangerouslySetInnerHTML: { __html: ICON_SEND }
548
+ })
549
+ )
550
+ )
551
+ )
552
+ ),
553
+ React.createElement('div', { className: 'dsh-cron-modal-foot' },
554
+ React.createElement('button', {
555
+ type: 'button',
556
+ className: 'dsh-cron-btn-secondary',
557
+ disabled: dshLoading,
558
+ onClick: () => setDshModalOpen(false)
559
+ }, 'Отмена')
560
+ )
561
+ )
380
562
  )
381
563
  );
382
564
  }
@@ -552,13 +734,11 @@ window.__ModuleLoader__.load({
552
734
  ensureStyles();
553
735
  const toggle = createToggle();
554
736
 
555
- // Карточка в Настройках -> Плагины
556
737
  const cardSlot = registerFirst(ctx, [
557
738
  { name: 'settings.plugin.item', key: NS, inject: () => ({ ctx, toggle }) },
558
739
  { name: 'settings.section', id: '@goodandready/dsh-cron', order: 34, label: () => 'Cron Задачи', inject: () => ({ ctx, toggle }) }
559
740
  ], CronSettingsCard);
560
741
 
561
- // Чип в шапке диалога
562
742
  const chipSlot = registerFirst(ctx, [
563
743
  { name: 'conversation.session.header.utilities', id: '@goodandready/dsh-cron.chip', order: 27, inject: () => ({ ctx, toggle }) }
564
744
  ], () => React.createElement('button', {
@@ -583,4 +763,4 @@ window.__ModuleLoader__.load({
583
763
  module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'] };
584
764
  return module.exports;
585
765
  }
586
- });
766
+ })
package/lib/index.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import { TaskStore } from './store.js';
2
2
  import { TaskScheduler, parseScheduleExpression } from './scheduler.js';
3
3
  import { SessionRunner } from './runner.js';
4
+ import { getModelsHandler } from './models-handler.js';
5
+ import { chatStartHandler } from './chat-start.js';
4
6
  import { defineTool } from '@deepseek-ai/dsh-tools';
7
+ import { randomUUID } from 'node:crypto';
5
8
 
6
9
  export const name = '@goodandready/dsh-cron';
7
- export const inject = ['tools', 'webServer', 'settings'];
10
+ export const inject = ['tools', 'webServer', 'settings', 'llm', 'agents', 'agentDefaultModel'];
8
11
 
9
12
  function parseJsonBody(req) {
10
13
  return new Promise((resolve, reject) => {
@@ -101,6 +104,8 @@ export function apply(ctx, config) {
101
104
  type: body.type || 'prompt',
102
105
  delivery: body.delivery || 'current',
103
106
  status: body.status || 'active',
107
+ provider: body.provider || undefined,
108
+ model: body.model || undefined,
104
109
  });
105
110
  scheduler.scheduleTask(task);
106
111
  sendJson(res, 200, { ok: true, task });
@@ -114,6 +119,24 @@ export function apply(ctx, config) {
114
119
  }
115
120
  }), 'dsh-cron: /tasks');
116
121
 
122
+ // GET /dsh-cron/models
123
+ ctx.effect(() => ctx.webServer.register({
124
+ kind: 'exact',
125
+ path: '/dsh-cron/models',
126
+ handler: async (req, res) => {
127
+ await getModelsHandler(ctx, req, res, sendJson);
128
+ }
129
+ }), 'dsh-cron: /models');
130
+
131
+ // POST /dsh-cron/chat/start
132
+ ctx.effect(() => ctx.webServer.register({
133
+ kind: 'exact',
134
+ path: '/dsh-cron/chat/start',
135
+ handler: async (req, res) => {
136
+ await chatStartHandler(ctx, req, res, parseJsonBody, sendJson, randomUUID);
137
+ }
138
+ }), 'dsh-cron: /chat/start');
139
+
117
140
  // Prefix routes for actions (/dsh-cron/action/*)
118
141
  ctx.effect(() => ctx.webServer.register({
119
142
  kind: 'prefix',
@@ -128,11 +151,11 @@ export function apply(ctx, config) {
128
151
  const taskId = toggleMatch[1];
129
152
  const task = store.get(taskId);
130
153
  if (!task) {
131
- sendJson(res, 404, { ok: false, error: 'Задача не найдена' });
154
+ sendJson(res, 404, { ok: false, error: 'Task not found' });
132
155
  return;
133
156
  }
134
- const updated = task.status === 'active'
135
- ? scheduler.pauseTask(taskId)
157
+ const updated = task.status === 'active'
158
+ ? scheduler.pauseTask(taskId)
136
159
  : scheduler.resumeTask(taskId);
137
160
  sendJson(res, 200, { ok: true, task: updated });
138
161
  return;
@@ -141,13 +164,8 @@ export function apply(ctx, config) {
141
164
  const runMatch = pathname.match(/^\/dsh-cron\/action\/([^/]+)\/run$/);
142
165
  if (req.method === 'POST' && runMatch) {
143
166
  const taskId = runMatch[1];
144
- const task = store.get(taskId);
145
- if (!task) {
146
- sendJson(res, 404, { ok: false, error: 'Задача не найдена' });
147
- return;
148
- }
149
167
  scheduler.runTask(taskId);
150
- sendJson(res, 200, { ok: true, message: 'Запуск инициирован' });
168
+ sendJson(res, 200, { ok: true });
151
169
  return;
152
170
  }
153
171
 
@@ -175,7 +193,9 @@ export function apply(ctx, config) {
175
193
  title: { type: 'string', required: true, description: 'Краткое название задачи' },
176
194
  schedule: { type: 'string', required: true, description: 'Выражение cron или интервал (every 30m, 0 9 * * 1-5, daily)' },
177
195
  prompt: { type: 'string', required: true, description: 'Инструкция для агента при запуске' },
178
- delivery: { type: 'string', description: 'Режим запуска: current или isolated' }
196
+ delivery: { type: 'string', description: 'Режим запуска: current или isolated' },
197
+ provider: { type: 'string', description: 'ID провайдера LLM для выполнения задачи' },
198
+ model: { type: 'string', description: 'ID модели LLM для выполнения задачи' }
179
199
  },
180
200
  output: {
181
201
  schema: {
@@ -190,7 +210,7 @@ export function apply(ctx, config) {
190
210
  return [{ type: 'text', text: value.message }];
191
211
  }
192
212
  },
193
- execute: async ({ title, schedule, prompt, delivery }) => {
213
+ execute: async ({ title, schedule, prompt, delivery, provider, model }) => {
194
214
  try {
195
215
  const parsed = parseScheduleExpression(schedule);
196
216
  const task = store.set({
@@ -199,6 +219,8 @@ export function apply(ctx, config) {
199
219
  scheduleText: parsed.humanText,
200
220
  prompt,
201
221
  delivery: delivery || 'isolated',
222
+ provider: provider || undefined,
223
+ model: model || undefined,
202
224
  status: 'active'
203
225
  });
204
226
  scheduler.scheduleTask(task);
@@ -346,4 +368,4 @@ export function apply(ctx, config) {
346
368
  return;
347
369
  }
348
370
  });
349
- }
371
+ }
@@ -0,0 +1,32 @@
1
+ export async function getModelsHandler(ctx, req, res, sendJson) {
2
+ const current = ctx.get('agentDefaultModel')?.currentSelection?.() ?? null;
3
+ let providers = [];
4
+ try {
5
+ if (ctx.llm && typeof ctx.llm.listProviders === 'function') {
6
+ providers = (ctx.llm.listProviders() ?? []).map((p) => ({ id: p.id, name: p.name ?? p.id }));
7
+ }
8
+ } catch {
9
+ providers = [];
10
+ }
11
+
12
+ const url = new URL(req.url, 'http://127.0.0.1');
13
+ const wanted = url.searchParams.get('provider') || '';
14
+ if (wanted === '') {
15
+ sendJson(res, 200, { current, providers, models: [] });
16
+ return;
17
+ }
18
+
19
+ try {
20
+ let rows = [];
21
+ if (ctx.llm && typeof ctx.llm.listModels === 'function') {
22
+ rows = await ctx.llm.listModels(wanted);
23
+ }
24
+ sendJson(res, 200, {
25
+ current,
26
+ providers,
27
+ models: (rows ?? []).map((m) => ({ id: m.id, name: m.name ?? m.id })),
28
+ });
29
+ } catch {
30
+ sendJson(res, 200, { current, providers, models: [] });
31
+ }
32
+ }
package/lib/prompt.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Builds the initial prompt for starting a session with DSH agent to configure a cron task.
3
+ * @param {string} userText
4
+ * @returns {string}
5
+ */
6
+ export function buildAgentCronPrompt(userText) {
7
+ const text = (userText || '').trim();
8
+ return `Пользователь хочет запланировать cron-задачу через DSH Cron:
9
+ "${text}"
10
+
11
+ ИНСТРУКЦИЯ ДЛЯ АГЕНТА:
12
+ 1. Твоя цель — помочь пользователю настроить и зарегистрировать запланированную задачу с помощью инструмента \`cron_schedule_task\`.
13
+ 2. Предложи подходящую и экономичную LLM-модель для выполнения этого крона (например, \`Qwen/Qwen3.8-Flash\` через Command Code или \`deepseek/deepseek-v4-flash\`), уточни у пользователя, устраивает ли она его или он предпочитает другую подключенную модель.
14
+ 3. Уточни и сформулируй точное расписание cron (например, "0 * * * *" или интервал вроде "every 1h").
15
+ 4. Примени Silent Rule (правило тишины): если задача мониторинга/проверки не обнаружила проблем, критических изменений или ошибок, агент должен возвращать ПУСТОЙ ответ или короткое уведомление без лишнего шума.
16
+ 5. После согласования параметров ОБЯЗАТЕЛЬНО вызови инструмент \`cron_schedule_task\`, передав title, schedule, prompt и delivery ("isolated" или "current").`;
17
+ }
package/lib/runner.js CHANGED
@@ -9,7 +9,51 @@ export class SessionRunner {
9
9
  async execute(task) {
10
10
  console.log(`[dsh-cron] executing task "${task.title}" (${task.id})`);
11
11
 
12
- // Check if session / agent service is present in DSH context
12
+ // 1. If ctx.agents is available, run through agent session
13
+ if (this.ctx && this.ctx.agents && typeof this.ctx.agents.create === 'function') {
14
+ try {
15
+ const { randomUUID } = await import('node:crypto');
16
+ let createUserMessage = (m) => m;
17
+ try {
18
+ const llmModule = await import('@deepseek-ai/dsh-llm');
19
+ if (llmModule && typeof llmModule.createUserMessage === 'function') {
20
+ createUserMessage = llmModule.createUserMessage;
21
+ }
22
+ } catch {}
23
+
24
+ let mintSessionId = () => `cron-exec-${task.id}-${randomUUID()}`;
25
+ try {
26
+ const sessionModule = await import('@deepseek-ai/dsh-session');
27
+ if (sessionModule && typeof sessionModule.SessionId === 'function') {
28
+ const sid = mintSessionId();
29
+ mintSessionId = () => sessionModule.SessionId(sid);
30
+ }
31
+ } catch {}
32
+
33
+ const defaultSel = this.ctx.get?.('agentDefaultModel')?.currentSelection?.();
34
+ const provider = task.provider || defaultSel?.provider;
35
+ const model = task.model || defaultSel?.model;
36
+
37
+ const handle = await this.ctx.agents.create({
38
+ sessionId: mintSessionId(),
39
+ meta: {},
40
+ agentOptions: provider && model ? { provider, model } : undefined,
41
+ });
42
+
43
+ await handle.agent.whenIdle();
44
+ handle.agent.followup(createUserMessage({
45
+ content: [{ type: 'text', text: task.prompt }],
46
+ source: { kind: 'plugin', plugin: 'dsh-cron', form: 'cron-execute' },
47
+ }));
48
+
49
+ return `[dsh-cron] Agent started session ${handle.agent.session?.id || 'cron'}`;
50
+ } catch (err) {
51
+ console.error(`[dsh-cron] agent execution failed:`, err.message);
52
+ throw err;
53
+ }
54
+ }
55
+
56
+ // 2. Check if legacy session service is present
13
57
  if (this.ctx && this.ctx.session && typeof this.ctx.session.create === 'function') {
14
58
  try {
15
59
  const session = await this.ctx.session.create({
@@ -27,7 +71,7 @@ export class SessionRunner {
27
71
  }
28
72
  }
29
73
 
30
- // Direct shell / script task fallback
74
+ // 3. Direct shell / script task fallback
31
75
  if (task.type === 'script' && task.prompt) {
32
76
  const { exec } = await import('node:child_process');
33
77
  const { promisify } = await import('node:util');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cron",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Scheduled cron tasks, background automation and agent execution for DeepSeek Harness.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",