@goodandready/dsh-cron 0.1.5 → 0.1.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.
@@ -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,35 @@ 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
+ [data-pane=conversation], [class*=centerCol] {
32
+ position: relative !important;
33
+ }
34
+ [${VIEW_ATTR}] {
35
+ z-index: 60;
36
+ background: var(--dsw-alias-bg-base, #171717);
37
+ display: none;
38
+ position: absolute;
39
+ inset: 0;
40
+ }
41
+ html[${ACTIVE_ATTR}] [${VIEW_ATTR}] {
42
+ display: block !important;
43
+ }
44
+ html[${ACTIVE_ATTR}] [data-pane=conversation] > *:not([${VIEW_ATTR}]),
45
+ html[${ACTIVE_ATTR}] [class*=centerCol] > *:not([${VIEW_ATTR}]) {
46
+ display: none !important;
47
+ }
48
+
49
+ .dsh-cron-entry-clone { display: flex; align-items: center; gap: 8px; }
50
+ .dsh-cron-entry-icon { display: flex; align-items: center; justify-content: center; flex: none; width: 16px; height: 16px; }
38
51
  .dsh-cron-entry-label { text-overflow: ellipsis; overflow: hidden; }
39
52
  [data-dsh-frame][data-sidebar-collapsed] .dsh-cron-entry-label { display: none; }
40
53
 
@@ -67,6 +80,7 @@ window.__ModuleLoader__.load({
67
80
  .dsh-cron-task-info { flex: 1; min-width: 0; }
68
81
  .dsh-cron-task-title { font-size: 15px; font-weight: 500; margin-bottom: 3px; }
69
82
  .dsh-cron-task-sched { font-size: 13px; color: #9ca3af; }
83
+ .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
84
  .dsh-cron-task-actions { display: flex; gap: 8px; }
71
85
  .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
86
  .dsh-cron-icon-btn:hover { background: #2a2a2a; color: #fff; }
@@ -81,15 +95,25 @@ window.__ModuleLoader__.load({
81
95
  .dsh-cron-rec-desc { font-size: 13px; color: #71717a; }
82
96
 
83
97
  .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); }
98
+ .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
99
  .dsh-cron-modal-title { font-size: 18px; font-weight: 600; margin-bottom: 18px; }
86
100
  .dsh-cron-form-group { margin-bottom: 16px; }
87
101
  .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; }
102
+ .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; }
103
+ .dsh-cron-form-group select { cursor: pointer; }
89
104
  .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; }
105
+ .dsh-cron-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
106
+ .dsh-cron-modal-foot { display: flex; justify-content: flex-end; align-items: center; gap: 10px; margin-top: 20px; }
107
+ .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; }
108
+ .dsh-cron-btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
92
109
  .dsh-cron-btn-secondary { background: transparent; color: #999; border: 1px solid #383838; border-radius: 8px; padding: 7px 16px; font-size: 13.5px; cursor: pointer; }
110
+ .dsh-cron-chat-box { position: relative; background: #141414; border: 1px solid #333; border-radius: 12px; padding: 12px; }
111
+ .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; }
112
+ .dsh-cron-chat-foot { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; padding-top: 8px; border-top: 1px solid #222; }
113
+ .dsh-cron-chat-hint { font-size: 12px; color: #666; }
114
+ .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; }
115
+ .dsh-cron-send-btn:hover:not(:disabled) { background: #fff; transform: translateY(-1px); }
116
+ .dsh-cron-send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
93
117
 
94
118
  .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
119
  .dsh-cron-card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
@@ -129,23 +153,16 @@ window.__ModuleLoader__.load({
129
153
  };
130
154
  }
131
155
 
132
- function startNewSession(ctx) {
133
- if (ctx) {
134
- const uiWorkspace = typeof ctx.get === 'function' ? ctx.get('uiWorkspace') : ctx.uiWorkspace;
135
- if (uiWorkspace && typeof uiWorkspace.startSession === 'function') {
136
- uiWorkspace.startSession();
137
- return;
138
- }
139
- const sessions = typeof ctx.get === 'function' ? ctx.get('sessions') : ctx.sessions;
140
- if (sessions && typeof sessions.create === 'function') {
141
- sessions.create({}).then((sessionId) => {
142
- if (sessions.open) sessions.open(sessionId);
143
- }).catch(() => {});
144
- return;
145
- }
156
+ function openSession(ctx, sessionId) {
157
+ if (!sessionId) return false;
158
+ const sessions = ctx && typeof ctx.get === 'function' ? ctx.get('sessions') : ctx?.sessions;
159
+ if (!sessions || typeof sessions.open !== 'function') return false;
160
+ try {
161
+ sessions.open(sessionId);
162
+ return true;
163
+ } catch {
164
+ return false;
146
165
  }
147
- const btn = nativeButton();
148
- if (btn) btn.click();
149
166
  }
150
167
 
151
168
  function CronScreen(props) {
@@ -155,21 +172,36 @@ window.__ModuleLoader__.load({
155
172
  const [tasks, setTasks] = React.useState([]);
156
173
  const [recs, setRecs] = React.useState([]);
157
174
  const [dropdownOpen, setDropdownOpen] = React.useState(false);
158
- const [modalOpen, setModalOpen] = React.useState(false);
175
+
176
+ // Manual modal state
177
+ const [manualModalOpen, setManualModalOpen] = React.useState(false);
159
178
  const [formTitle, setFormTitle] = React.useState('');
160
179
  const [formSchedule, setFormSchedule] = React.useState('');
161
180
  const [formPrompt, setFormPrompt] = React.useState('');
181
+ const [formProvider, setFormProvider] = React.useState('');
182
+ const [formModel, setFormModel] = React.useState('');
183
+
184
+ // DSH dialog state
185
+ const [dshModalOpen, setDshModalOpen] = React.useState(false);
186
+ const [dshPrompt, setDshPrompt] = React.useState('');
187
+ const [dshLoading, setDshLoading] = React.useState(false);
188
+
189
+ // Models list state
190
+ const [providers, setProviders] = React.useState([]);
191
+ const [models, setModels] = React.useState([]);
192
+ const [defaultModelInfo, setDefaultModelInfo] = React.useState(null);
162
193
 
163
194
  React.useEffect(() => {
164
195
  const onKey = (e) => {
165
196
  if (e.key !== 'Escape') return;
166
- if (modalOpen) { setModalOpen(false); return; }
197
+ if (manualModalOpen) { setManualModalOpen(false); return; }
198
+ if (dshModalOpen) { setDshModalOpen(false); return; }
167
199
  if (dropdownOpen) { setDropdownOpen(false); return; }
168
200
  if (onClose) onClose();
169
201
  };
170
202
  document.addEventListener('keydown', onKey);
171
203
  return () => document.removeEventListener('keydown', onKey);
172
- }, [modalOpen, dropdownOpen, onClose]);
204
+ }, [manualModalOpen, dshModalOpen, dropdownOpen, onClose]);
173
205
 
174
206
  const fetchTasks = async () => {
175
207
  try {
@@ -182,7 +214,21 @@ window.__ModuleLoader__.load({
182
214
  } catch (err) {}
183
215
  };
184
216
 
217
+ const loadModels = async (providerToFetch) => {
218
+ try {
219
+ const url = '/dsh-cron/models' + (providerToFetch ? '?provider=' + encodeURIComponent(providerToFetch) : '');
220
+ const res = await fetch(url);
221
+ const data = await res.json();
222
+ if (data) {
223
+ if (data.providers) setProviders(data.providers);
224
+ if (data.models) setModels(data.models);
225
+ if (data.current) setDefaultModelInfo(data.current);
226
+ }
227
+ } catch (err) {}
228
+ };
229
+
185
230
  React.useEffect(() => { fetchTasks(); }, [tab, query]);
231
+ React.useEffect(() => { loadModels(); }, []);
186
232
 
187
233
  const handleToggleTask = async (id) => {
188
234
  try {
@@ -207,21 +253,34 @@ window.__ModuleLoader__.load({
207
253
  } catch (err) {}
208
254
  };
209
255
 
210
- const openCreateModal = (preset) => {
256
+ const openManualModal = (preset) => {
211
257
  setDropdownOpen(false);
212
258
  if (preset) {
213
259
  setFormTitle(preset.title || '');
214
260
  setFormSchedule(preset.schedule || '');
215
261
  setFormPrompt(preset.prompt || '');
262
+ setFormProvider(defaultModelInfo?.provider || '');
263
+ setFormModel(defaultModelInfo?.model || '');
264
+ if (defaultModelInfo?.provider) loadModels(defaultModelInfo.provider);
216
265
  } else {
217
266
  setFormTitle('');
218
267
  setFormSchedule('0 9 * * 1-5');
219
268
  setFormPrompt('');
269
+ setFormProvider(defaultModelInfo?.provider || '');
270
+ setFormModel(defaultModelInfo?.model || '');
271
+ if (defaultModelInfo?.provider) loadModels(defaultModelInfo.provider);
220
272
  }
221
- setModalOpen(true);
273
+ setManualModalOpen(true);
274
+ };
275
+
276
+ const handleProviderChange = (e) => {
277
+ const prov = e.target.value;
278
+ setFormProvider(prov);
279
+ setFormModel('');
280
+ loadModels(prov);
222
281
  };
223
282
 
224
- const handleSave = async (e) => {
283
+ const handleSaveManual = async (e) => {
225
284
  e.preventDefault();
226
285
  try {
227
286
  const res = await fetch('/dsh-cron/tasks', {
@@ -230,38 +289,54 @@ window.__ModuleLoader__.load({
230
289
  body: JSON.stringify({
231
290
  title: formTitle,
232
291
  schedule: formSchedule,
233
- prompt: formPrompt
292
+ prompt: formPrompt,
293
+ provider: formProvider || undefined,
294
+ model: formModel || undefined,
234
295
  })
235
296
  });
236
297
  const data = await res.json();
237
298
  if (!data.ok) { alert(data.error || 'Ошибка'); return; }
238
- setModalOpen(false);
299
+ setManualModalOpen(false);
239
300
  fetchTasks();
240
301
  } catch (err) { alert(err.message); }
241
302
  };
242
303
 
243
- const handleCreateWithDSH = () => {
304
+ const openDshModal = () => {
244
305
  setDropdownOpen(false);
245
- if (onClose) onClose();
246
- else toggle.set(false);
247
-
248
- startNewSession(ctx);
249
-
250
- const promptText = 'Помоги мне настроить новую запланированную задачу cron: ';
251
- let attempts = 0;
252
- const fillTextarea = () => {
253
- const textarea = document.querySelector('textarea[placeholder*="message"], textarea[class*="prompt"], textarea[placeholder*="сообщение"], textarea[placeholder*="спросить"]');
254
- if (textarea) {
255
- textarea.value = promptText;
256
- textarea.dispatchEvent(new Event('input', { bubbles: true }));
257
- textarea.focus();
258
- textarea.setSelectionRange(promptText.length, promptText.length);
259
- } else if (attempts < 10) {
260
- attempts++;
261
- setTimeout(fillTextarea, 150);
306
+ setDshPrompt('');
307
+ setDshModalOpen(true);
308
+ };
309
+
310
+ const handleStartWithDSH = async () => {
311
+ const text = dshPrompt.trim();
312
+ if (!text) return;
313
+
314
+ setDshLoading(true);
315
+ try {
316
+ const res = await fetch('/dsh-cron/chat/start', {
317
+ method: 'POST',
318
+ headers: { 'Content-Type': 'application/json' },
319
+ body: JSON.stringify({ prompt: text })
320
+ });
321
+ const data = await res.json();
322
+ if (!data.ok || !data.sessionId) {
323
+ alert(data.error || 'Не удалось запустить диалог с агентом');
324
+ setDshLoading(false);
325
+ return;
262
326
  }
263
- };
264
- setTimeout(fillTextarea, 100);
327
+
328
+ setDshModalOpen(false);
329
+ if (onClose) onClose();
330
+ else toggle.set(false);
331
+
332
+ if (!openSession(ctx, data.sessionId)) {
333
+ alert('Сессия создана (' + data.sessionId + '), перейдите к ней в списке чатов');
334
+ }
335
+ } catch (err) {
336
+ alert('Ошибка запуска: ' + err.message);
337
+ } finally {
338
+ setDshLoading(false);
339
+ }
265
340
  };
266
341
 
267
342
  return React.createElement('div', { className: 'dsh-cron-overlay' },
@@ -291,8 +366,8 @@ window.__ModuleLoader__.load({
291
366
  React.createElement('span', { dangerouslySetInnerHTML: { __html: ICON_CHEVRON_DOWN } })
292
367
  ),
293
368
  dropdownOpen && React.createElement('div', { className: 'dsh-cron-dropdown' },
294
- React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: handleCreateWithDSH }, '💬 Создать с DSH'),
295
- React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: () => openCreateModal(null) }, '✏️ Настроить вручную')
369
+ React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: openDshModal }, '💬 Создать с DSH'),
370
+ React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: () => openManualModal(null) }, '✏️ Настроить вручную')
296
371
  )
297
372
  )
298
373
  ),
@@ -312,46 +387,58 @@ window.__ModuleLoader__.load({
312
387
  className: 'dsh-cron-tab',
313
388
  'data-active': tab === t ? 'true' : undefined,
314
389
  onClick: () => setTab(t)
315
- },
316
- t === 'all' ? 'Все' : t === 'active' ? 'Активные' : t === 'paused' ? 'Приостановленные' : 'Завершено'
317
- )
390
+ }, t === 'all' ? 'Все' : t === 'active' ? 'Активные' : t === 'paused' ? 'На паузе' : 'Завершенные')
318
391
  )
319
392
  ),
320
393
  React.createElement('div', { className: 'dsh-cron-task-list' },
321
- tasks.length === 0 ? React.createElement('div', { style: { padding: '24px', textAlign: 'center', color: '#666' } }, 'Нет задач в выбранном статусе') :
322
- tasks.map((task) =>
323
- React.createElement('div', { key: task.id, className: 'dsh-cron-task-item' },
324
- React.createElement('button', {
325
- className: 'dsh-cron-task-status-btn',
326
- 'data-active': task.status === 'active' ? 'true' : undefined,
327
- onClick: () => handleToggleTask(task.id),
328
- dangerouslySetInnerHTML: { __html: task.status === 'active' ? ICON_PLAY : ICON_PAUSE }
329
- }),
330
- React.createElement('div', { className: 'dsh-cron-task-info' },
331
- React.createElement('div', { className: 'dsh-cron-task-title' }, task.title),
332
- React.createElement('div', { className: 'dsh-cron-task-sched' }, task.scheduleText || task.schedule)
333
- ),
334
- React.createElement('div', { className: 'dsh-cron-task-actions' },
335
- React.createElement('button', { className: 'dsh-cron-icon-btn', onClick: () => handleRunNow(task.id) }, 'Запустить'),
336
- React.createElement('button', { className: 'dsh-cron-icon-btn', onClick: () => handleDelete(task.id) }, 'Удалить')
394
+ tasks.length === 0
395
+ ? React.createElement('div', { style: { color: '#777', padding: '16px 0', fontSize: '14px' } }, 'Нет запланированных задач')
396
+ : tasks.map((task) =>
397
+ React.createElement('div', { key: task.id, className: 'dsh-cron-task-item' },
398
+ React.createElement('button', {
399
+ className: 'dsh-cron-task-status-btn',
400
+ 'data-active': task.status === 'active' ? 'true' : undefined,
401
+ title: task.status === 'active' ? 'Приостановить' : 'Возобновить',
402
+ onClick: () => handleToggleTask(task.id)
403
+ }, React.createElement('span', { dangerouslySetInnerHTML: { __html: task.status === 'active' ? ICON_PAUSE : ICON_PLAY } })),
404
+ React.createElement('div', { className: 'dsh-cron-task-info' },
405
+ React.createElement('div', { className: 'dsh-cron-task-title' },
406
+ task.title,
407
+ task.model ? React.createElement('span', { className: 'dsh-cron-task-model-tag' }, task.model.split('/').pop()) : null
408
+ ),
409
+ React.createElement('div', { className: 'dsh-cron-task-sched' },
410
+ task.scheduleText || task.schedule,
411
+ task.nextRunAt ? ` · след: ${new Date(task.nextRunAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` : ''
412
+ )
413
+ ),
414
+ React.createElement('div', { className: 'dsh-cron-task-actions' },
415
+ React.createElement('button', {
416
+ className: 'dsh-cron-icon-btn',
417
+ title: 'Запустить немедленно',
418
+ onClick: () => handleRunNow(task.id)
419
+ }, 'Запустить'),
420
+ React.createElement('button', {
421
+ className: 'dsh-cron-icon-btn',
422
+ title: 'Удалить задачу',
423
+ onClick: () => handleDelete(task.id)
424
+ }, React.createElement('span', { dangerouslySetInnerHTML: { __html: ICON_TRASH } }))
425
+ )
426
+ )
337
427
  )
338
- )
339
- )
340
428
  ),
341
- React.createElement('div', { className: 'dsh-cron-recs-title' }, 'Рекомендации'),
429
+ React.createElement('div', { className: 'dsh-cron-recs-title' }, 'Рекомендуемые задачи'),
342
430
  React.createElement('div', { className: 'dsh-cron-recs-list' },
343
431
  recs.map((rec) =>
344
432
  React.createElement('div', {
345
433
  key: rec.id,
346
434
  className: 'dsh-cron-rec-card',
347
- onClick: () => openCreateModal(rec)
435
+ onClick: () => openManualModal(rec)
348
436
  },
349
- React.createElement('div', {
350
- className: 'dsh-cron-rec-icon',
351
- dangerouslySetInnerHTML: {
437
+ React.createElement('div', { className: 'dsh-cron-rec-icon' },
438
+ React.createElement('span', { dangerouslySetInnerHTML: {
352
439
  __html: rec.icon === 'bell' ? ICON_BELL : rec.icon === 'clipboard' ? ICON_CLIPBOARD : ICON_ACTIVITY
353
- }
354
- }),
440
+ } })
441
+ ),
355
442
  React.createElement('div', { className: 'dsh-cron-rec-content' },
356
443
  React.createElement('div', { className: 'dsh-cron-rec-title' },
357
444
  rec.title,
@@ -363,10 +450,12 @@ window.__ModuleLoader__.load({
363
450
  )
364
451
  )
365
452
  ),
366
- modalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setModalOpen(false) },
453
+
454
+ // 1. Manual creation modal
455
+ manualModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setManualModalOpen(false) },
367
456
  React.createElement('div', { className: 'dsh-cron-modal', onClick: (e) => e.stopPropagation() },
368
457
  React.createElement('div', { className: 'dsh-cron-modal-title' }, 'Настройка запланированной задачи'),
369
- React.createElement('form', { onSubmit: handleSave },
458
+ React.createElement('form', { onSubmit: handleSaveManual },
370
459
  React.createElement('div', { className: 'dsh-cron-form-group' },
371
460
  React.createElement('label', null, 'Название задачи'),
372
461
  React.createElement('input', {
@@ -385,6 +474,28 @@ window.__ModuleLoader__.load({
385
474
  onChange: (e) => setFormSchedule(e.target.value)
386
475
  })
387
476
  ),
477
+ React.createElement('div', { className: 'dsh-cron-form-row' },
478
+ React.createElement('div', { className: 'dsh-cron-form-group' },
479
+ React.createElement('label', null, 'Провайдер LLM'),
480
+ React.createElement('select', {
481
+ value: formProvider,
482
+ onChange: handleProviderChange
483
+ },
484
+ React.createElement('option', { value: '' }, 'По умолчанию (DSH)'),
485
+ providers.map((p) => React.createElement('option', { key: p.id, value: p.id }, p.name || p.id))
486
+ )
487
+ ),
488
+ React.createElement('div', { className: 'dsh-cron-form-group' },
489
+ React.createElement('label', null, 'Модель LLM'),
490
+ React.createElement('select', {
491
+ value: formModel,
492
+ onChange: (e) => setFormModel(e.target.value)
493
+ },
494
+ React.createElement('option', { value: '' }, 'По умолчанию'),
495
+ models.map((m) => React.createElement('option', { key: m.id, value: m.id }, m.name || m.id))
496
+ )
497
+ )
498
+ ),
388
499
  React.createElement('div', { className: 'dsh-cron-form-group' },
389
500
  React.createElement('label', null, 'Промпт / Инструкция для агента'),
390
501
  React.createElement('textarea', {
@@ -398,7 +509,7 @@ window.__ModuleLoader__.load({
398
509
  React.createElement('button', {
399
510
  type: 'button',
400
511
  className: 'dsh-cron-btn-secondary',
401
- onClick: () => setModalOpen(false)
512
+ onClick: () => setManualModalOpen(false)
402
513
  }, 'Отмена'),
403
514
  React.createElement('button', {
404
515
  type: 'submit',
@@ -407,6 +518,56 @@ window.__ModuleLoader__.load({
407
518
  )
408
519
  )
409
520
  )
521
+ ),
522
+
523
+ // 2. "Создать с DSH" modal
524
+ dshModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => !dshLoading && setDshModalOpen(false) },
525
+ React.createElement('div', { className: 'dsh-cron-modal', onClick: (e) => e.stopPropagation() },
526
+ React.createElement('div', { className: 'dsh-cron-modal-title' }, 'Запланировать задачу с DSH Агентом'),
527
+ React.createElement('div', { className: 'dsh-cron-form-group' },
528
+ React.createElement('label', null, 'Опишите задачу и желаемую периодичность:'),
529
+ React.createElement('div', { className: 'dsh-cron-chat-box' },
530
+ React.createElement('textarea', {
531
+ className: 'dsh-cron-chat-input',
532
+ rows: 4,
533
+ autoFocus: true,
534
+ disabled: dshLoading,
535
+ placeholder: 'Например: проверяй каждые 2 часа свободное место на сервере и наличие упавших systemd-сервисов, присылай алерт только если что-то не так...',
536
+ value: dshPrompt,
537
+ onKeyDown: (e) => {
538
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
539
+ e.preventDefault();
540
+ handleStartWithDSH();
541
+ }
542
+ },
543
+ onChange: (e) => setDshPrompt(e.target.value)
544
+ }),
545
+ React.createElement('div', { className: 'dsh-cron-chat-foot' },
546
+ React.createElement('span', { className: 'dsh-cron-chat-hint' }, 'Ctrl+Enter для отправки'),
547
+ React.createElement('button', {
548
+ type: 'button',
549
+ className: 'dsh-cron-send-btn',
550
+ disabled: dshLoading || !dshPrompt.trim(),
551
+ title: 'Отправить агенту',
552
+ onClick: handleStartWithDSH
553
+ },
554
+ React.createElement('span', {
555
+ style: { display: 'flex', alignItems: 'center' },
556
+ dangerouslySetInnerHTML: { __html: ICON_SEND }
557
+ })
558
+ )
559
+ )
560
+ )
561
+ ),
562
+ React.createElement('div', { className: 'dsh-cron-modal-foot' },
563
+ React.createElement('button', {
564
+ type: 'button',
565
+ className: 'dsh-cron-btn-secondary',
566
+ disabled: dshLoading,
567
+ onClick: () => setDshModalOpen(false)
568
+ }, 'Отмена')
569
+ )
570
+ )
410
571
  )
411
572
  );
412
573
  }
@@ -611,4 +772,4 @@ window.__ModuleLoader__.load({
611
772
  module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'] };
612
773
  return module.exports;
613
774
  }
614
- });
775
+ })
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.5",
3
+ "version": "0.1.7",
4
4
  "description": "Scheduled cron tasks, background automation and agent execution for DeepSeek Harness.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",