@hhuangpanda/dsh-automation 0.0.0-stage → 0.3.34
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.
- package/LICENSE +22 -0
- package/README.md +133 -2
- package/README.zh.md +134 -0
- package/cordis.patch.yml +3 -0
- package/docs/images/automation-overview-en.png +0 -0
- package/docs/images/automation-overview-zh.png +0 -0
- package/docs/reference.md +55 -0
- package/docs/reference.zh.md +55 -0
- package/lib/agent-configuration.d.ts +27 -0
- package/lib/agent-configuration.js +217 -0
- package/lib/api.d.ts +5 -0
- package/lib/api.js +253 -0
- package/lib/client/AutomationPanel.d.ts +9 -0
- package/lib/client/EmptyState.d.ts +10 -0
- package/lib/client/ModelSelection.d.ts +17 -0
- package/lib/client/ScheduleSection.d.ts +32 -0
- package/lib/client/SchedulerHealth.d.ts +10 -0
- package/lib/client/SkillPicker.d.ts +15 -0
- package/lib/client/TaskDetail.d.ts +34 -0
- package/lib/client/TaskEditor.d.ts +38 -0
- package/lib/client/TaskRow.d.ts +12 -0
- package/lib/client/editor-layout.d.ts +20 -0
- package/lib/client/i18n.d.ts +15 -0
- package/lib/client/icons.d.ts +30 -0
- package/lib/client/index.d.ts +6 -0
- package/lib/client/locales.d.ts +260 -0
- package/lib/client/rrule-editor.d.ts +15 -0
- package/lib/client/session-selection.d.ts +4 -0
- package/lib/client/shared.d.ts +77 -0
- package/lib/client/store.d.ts +40 -0
- package/lib/client.js +3014 -0
- package/lib/controller.d.ts +36 -0
- package/lib/controller.js +154 -0
- package/lib/domain.d.ts +44 -0
- package/lib/domain.js +473 -0
- package/lib/errors.d.ts +14 -0
- package/lib/errors.js +16 -0
- package/lib/im-delivery.d.ts +30 -0
- package/lib/im-delivery.js +69 -0
- package/lib/index.d.ts +25 -0
- package/lib/index.js +92 -0
- package/lib/recurrence.d.ts +14 -0
- package/lib/recurrence.js +203 -0
- package/lib/runner.d.ts +27 -0
- package/lib/runner.js +295 -0
- package/lib/runtime-marker.d.ts +3 -0
- package/lib/runtime-marker.js +2 -0
- package/lib/scheduler.d.ts +58 -0
- package/lib/scheduler.js +287 -0
- package/lib/store.d.ts +14 -0
- package/lib/store.js +108 -0
- package/lib/tool-configuration.d.ts +8 -0
- package/lib/tool-configuration.js +220 -0
- package/lib/tool-support.d.ts +67 -0
- package/lib/tool-support.js +47 -0
- package/lib/tools.d.ts +6 -0
- package/lib/tools.js +140 -0
- package/lib/types.d.ts +465 -0
- package/lib/types.js +99 -0
- package/lib/validation.d.ts +34 -0
- package/lib/validation.js +99 -0
- package/package.json +163 -4
- package/screenshots.json +1 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
53
|
+
import { ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
54
|
+
import { standingMountFor } from '@deepseek-ai/dsh-agent-preset-registry';
|
|
55
|
+
import { isUserInvocable, renderSkillContent } from '@deepseek-ai/dsh-skill';
|
|
56
|
+
import { assertProviderModelPair, validateReasoningExecution } from './validation.js';
|
|
57
|
+
import '@deepseek-ai/dsh-agent-preset-registry';
|
|
58
|
+
import '@deepseek-ai/dsh-llm';
|
|
59
|
+
import '@deepseek-ai/dsh-permission-presets';
|
|
60
|
+
import '@deepseek-ai/dsh-skill';
|
|
61
|
+
export class AgentConfiguration {
|
|
62
|
+
ctx;
|
|
63
|
+
constructor(ctx) {
|
|
64
|
+
this.ctx = ctx;
|
|
65
|
+
}
|
|
66
|
+
async options(cwd, agentPreset, provider, model) {
|
|
67
|
+
assertProviderModelPair(provider, model);
|
|
68
|
+
let reasoning;
|
|
69
|
+
if (provider && model) {
|
|
70
|
+
try {
|
|
71
|
+
const info = await this.ctx.llm.resolveModelInfo(provider, model);
|
|
72
|
+
reasoning = {
|
|
73
|
+
provider, model,
|
|
74
|
+
efforts: info.reasoning?.efforts.map(({ id, name, description }) => ({ id, name, ...(description === undefined ? {} : { description }) })) ?? [],
|
|
75
|
+
...(info.reasoning?.defaultEffort === undefined ? {} : { defaultEffort: info.reasoning.defaultEffort }),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
reasoning = { provider, model, efforts: [], error: message(error) };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const presets = await this.ctx.agentPresets.list();
|
|
83
|
+
const providers = this.ctx.llm.listProviders();
|
|
84
|
+
const modelResults = await Promise.all(providers.map(async (provider) => {
|
|
85
|
+
try {
|
|
86
|
+
const models = await this.ctx.llm.listModels(provider.id);
|
|
87
|
+
return { provider, models };
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
return { provider, error: message(error) };
|
|
91
|
+
}
|
|
92
|
+
}));
|
|
93
|
+
let skills = [];
|
|
94
|
+
const candidatePreset = presets.find((preset) => preset.id === (agentPreset ?? this.ctx.agentPresets.defaultId));
|
|
95
|
+
if (candidatePreset !== undefined && candidatePreset.broken === undefined) {
|
|
96
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
97
|
+
try {
|
|
98
|
+
const scope = __addDisposableResource(env_1, await this.ctx.agentPresets.acquireScope(agentPreset), true);
|
|
99
|
+
skills = (await this.ctx.skills.list({ cwd, scope: scope.key })).filter(isUserInvocable);
|
|
100
|
+
}
|
|
101
|
+
catch (e_1) {
|
|
102
|
+
env_1.error = e_1;
|
|
103
|
+
env_1.hasError = true;
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
const result_1 = __disposeResources(env_1);
|
|
107
|
+
if (result_1)
|
|
108
|
+
await result_1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
...(reasoning === undefined ? {} : { reasoning }),
|
|
113
|
+
presets: presets.map((preset) => ({
|
|
114
|
+
id: preset.id,
|
|
115
|
+
name: preset.name ?? preset.id,
|
|
116
|
+
...(preset.description === undefined ? {} : { description: preset.description }),
|
|
117
|
+
...(preset.broken === undefined ? {} : { broken: preset.broken }),
|
|
118
|
+
default: preset.id === this.ctx.agentPresets.defaultId,
|
|
119
|
+
})),
|
|
120
|
+
models: modelResults.flatMap((result) => result.models === undefined ? [] : [{
|
|
121
|
+
provider: result.provider.id,
|
|
122
|
+
name: result.provider.name,
|
|
123
|
+
models: result.models.map((model) => ({
|
|
124
|
+
id: model.id,
|
|
125
|
+
name: model.name,
|
|
126
|
+
...(model.description === undefined ? {} : { description: model.description }),
|
|
127
|
+
})),
|
|
128
|
+
}]),
|
|
129
|
+
modelFailures: modelResults.flatMap((result) => result.error === undefined ? [] : [{ provider: result.provider.id, error: result.error }]),
|
|
130
|
+
permissions: this.ctx.permissionPresets.names.map((id) => {
|
|
131
|
+
const option = this.ctx.permissionPresets.optionOf(id);
|
|
132
|
+
const preset = this.ctx.permissionPresets.resolve(id);
|
|
133
|
+
return {
|
|
134
|
+
id,
|
|
135
|
+
name: option.name,
|
|
136
|
+
...(option.description === undefined ? {} : { description: option.description }),
|
|
137
|
+
sandbox: preset.sandbox,
|
|
138
|
+
approval: preset.approval,
|
|
139
|
+
default: id === this.ctx.permissionPresets.defaultPreset,
|
|
140
|
+
};
|
|
141
|
+
}),
|
|
142
|
+
skills: skills.map((skill) => ({
|
|
143
|
+
name: skill.name,
|
|
144
|
+
description: skill.description,
|
|
145
|
+
...(skill.whenToUse === undefined ? {} : { whenToUse: skill.whenToUse }),
|
|
146
|
+
modelInvocable: skill.invocation.modelInvocable,
|
|
147
|
+
source: skill.source,
|
|
148
|
+
provider: skill.provider,
|
|
149
|
+
})),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
permissionName(permissionPreset) {
|
|
153
|
+
if (!this.ctx.permissionPresets.names.includes(permissionPreset))
|
|
154
|
+
return undefined;
|
|
155
|
+
return this.ctx.permissionPresets.optionOf(permissionPreset).name;
|
|
156
|
+
}
|
|
157
|
+
async validate(execution, permissionPreset, options = {}) {
|
|
158
|
+
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
159
|
+
try {
|
|
160
|
+
if (execution.target?.mode === 'pinned-session') {
|
|
161
|
+
execution = { ...execution, agentPreset: undefined, provider: undefined, model: undefined, reasoningEffort: undefined, skills: [] };
|
|
162
|
+
}
|
|
163
|
+
const preset = await this.ctx.agentPresets.resolve(execution.agentPreset);
|
|
164
|
+
if (preset.broken !== undefined)
|
|
165
|
+
throw new Error(`Agent preset ${preset.id} is unavailable: ${preset.broken}`);
|
|
166
|
+
assertProviderModelPair(execution.provider, execution.model, { allowLegacyPartialModel: options.allowLegacyPartialModel === true });
|
|
167
|
+
validateReasoningExecution(execution);
|
|
168
|
+
if (execution.provider !== undefined && execution.model !== undefined) {
|
|
169
|
+
await this.ctx.llm.resolveCallConfig({
|
|
170
|
+
provider: execution.provider, model: execution.model,
|
|
171
|
+
...(execution.reasoningEffort === undefined ? {} : { reasoningEffort: ReasoningEffortId(execution.reasoningEffort) }),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const scope = __addDisposableResource(env_2, await this.ctx.agentPresets.acquireScope(execution.agentPreset), true);
|
|
175
|
+
for (const name of execution.skills) {
|
|
176
|
+
const skill = await this.ctx.skills.get(name, { cwd: execution.cwd, scope: scope.key });
|
|
177
|
+
if (skill === undefined)
|
|
178
|
+
throw new Error(`Selected skill ${name} is unavailable.`);
|
|
179
|
+
if (!isUserInvocable(skill))
|
|
180
|
+
throw new Error(`Selected skill ${name} is not user-invocable.`);
|
|
181
|
+
}
|
|
182
|
+
this.ctx.permissionPresets.resolve(permissionPreset);
|
|
183
|
+
}
|
|
184
|
+
catch (e_2) {
|
|
185
|
+
env_2.error = e_2;
|
|
186
|
+
env_2.hasError = true;
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
const result_2 = __disposeResources(env_2);
|
|
190
|
+
if (result_2)
|
|
191
|
+
await result_2;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async loadSelectedSkills(agent, task) {
|
|
195
|
+
if (task.execution.skills.length === 0)
|
|
196
|
+
return [];
|
|
197
|
+
const skills = this.ctx.agentPresets.serviceFor(agent, 'skills') ?? this.ctx.skills;
|
|
198
|
+
const scope = standingMountFor(agent.ctx)?.key;
|
|
199
|
+
const loaded = [];
|
|
200
|
+
for (const name of task.execution.skills) {
|
|
201
|
+
const skill = await skills.get(name, { cwd: task.execution.cwd, scope });
|
|
202
|
+
if (skill === undefined)
|
|
203
|
+
throw new Error(`Selected skill ${name} is unavailable.`);
|
|
204
|
+
if (!isUserInvocable(skill))
|
|
205
|
+
throw new Error(`Selected skill ${name} is not user-invocable.`);
|
|
206
|
+
loaded.push({
|
|
207
|
+
text: renderSkillContent(skill),
|
|
208
|
+
source: { kind: 'skill-invocation', name, form: 'instructions' },
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return loaded;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function message(error) {
|
|
215
|
+
return error instanceof Error ? error.message : String(error);
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=agent-configuration.js.map
|
package/lib/api.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { AutomationController } from './controller.js';
|
|
3
|
+
import '@deepseek-ai/dsh-host-webserver';
|
|
4
|
+
export declare function registerAutomationApi(ctx: Context, controller: AutomationController): () => void;
|
|
5
|
+
//# sourceMappingURL=api.d.ts.map
|
package/lib/api.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { assertOverrideId, assertProviderModelPair, assertReasoningEffort } from './validation.js';
|
|
2
|
+
import { AutomationDomainError } from './domain.js';
|
|
3
|
+
import { AutomationDeliverySchema, AutomationPermissionPresetSchema, AutomationScheduleSchema, NotificationPolicySchema } from './types.js';
|
|
4
|
+
import '@deepseek-ai/dsh-host-webserver';
|
|
5
|
+
const API_ROOT = '/api/automation/v1';
|
|
6
|
+
const MAX_BODY_BYTES = 16 * 1024;
|
|
7
|
+
function send(res, status, value) {
|
|
8
|
+
const body = `${JSON.stringify(value)}\n`;
|
|
9
|
+
res.writeHead(status, {
|
|
10
|
+
'content-type': 'application/json; charset=utf-8',
|
|
11
|
+
'content-length': Buffer.byteLength(body),
|
|
12
|
+
'cache-control': 'no-store',
|
|
13
|
+
'x-content-type-options': 'nosniff',
|
|
14
|
+
});
|
|
15
|
+
res.end(body);
|
|
16
|
+
}
|
|
17
|
+
function sameOrigin(req) {
|
|
18
|
+
const origin = req.headers.origin;
|
|
19
|
+
if (origin === undefined)
|
|
20
|
+
return true;
|
|
21
|
+
try {
|
|
22
|
+
return new URL(origin).host === req.headers.host;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function readJson(req) {
|
|
29
|
+
let size = 0;
|
|
30
|
+
const chunks = [];
|
|
31
|
+
for await (const chunk of req) {
|
|
32
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
33
|
+
size += buffer.length;
|
|
34
|
+
if (size > MAX_BODY_BYTES)
|
|
35
|
+
throw new Error('Request body is too large.');
|
|
36
|
+
chunks.push(buffer);
|
|
37
|
+
}
|
|
38
|
+
if (chunks.length === 0)
|
|
39
|
+
return {};
|
|
40
|
+
const value = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
41
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
42
|
+
throw new Error('Request body must be a JSON object.');
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function parseUpdate(body, current) {
|
|
46
|
+
if (Object.keys(body).some((key) => !['name', 'prompt', 'schedule', 'notificationPolicy', 'pauseAfterConsecutiveFailures', 'permissionPreset', 'confirmPermissionChange', 'execution', 'confirmSessionTargetChange', 'delivery', 'confirmDeliveryChange'].includes(key))) {
|
|
47
|
+
throw new Error('Update body contains an unknown field.');
|
|
48
|
+
}
|
|
49
|
+
if (body.name !== undefined && typeof body.name !== 'string')
|
|
50
|
+
throw new Error('name must be a string.');
|
|
51
|
+
if (body.prompt !== undefined && typeof body.prompt !== 'string')
|
|
52
|
+
throw new Error('prompt must be a string.');
|
|
53
|
+
if (body.pauseAfterConsecutiveFailures !== undefined && typeof body.pauseAfterConsecutiveFailures !== 'boolean') {
|
|
54
|
+
throw new Error('pauseAfterConsecutiveFailures must be boolean.');
|
|
55
|
+
}
|
|
56
|
+
const delivery = body.delivery === undefined || body.delivery === null ? body.delivery : AutomationDeliverySchema.parse(body.delivery);
|
|
57
|
+
if (body.confirmDeliveryChange !== undefined && typeof body.confirmDeliveryChange !== 'boolean')
|
|
58
|
+
throw new Error('confirmDeliveryChange must be boolean.');
|
|
59
|
+
const schedule = body.schedule === undefined ? undefined : AutomationScheduleSchema.parse(body.schedule);
|
|
60
|
+
const notificationPolicy = body.notificationPolicy === undefined ? undefined : NotificationPolicySchema.parse(body.notificationPolicy);
|
|
61
|
+
const permissionPreset = body.permissionPreset === undefined ? undefined : AutomationPermissionPresetSchema.parse(body.permissionPreset);
|
|
62
|
+
if (permissionPreset !== undefined && permissionPreset !== current.security.permissionPreset && body.confirmPermissionChange !== true) {
|
|
63
|
+
throw new Error('confirmPermissionChange must be true when changing permissions.');
|
|
64
|
+
}
|
|
65
|
+
const execution = parseExecutionPatch(body.execution, current.execution);
|
|
66
|
+
if (body.confirmSessionTargetChange !== undefined && typeof body.confirmSessionTargetChange !== 'boolean') {
|
|
67
|
+
throw new Error('confirmSessionTargetChange must be boolean.');
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
...(delivery === undefined ? {} : { delivery }),
|
|
71
|
+
...(body.confirmDeliveryChange === true ? { deliveryChangeConfirmed: true } : {}),
|
|
72
|
+
...(body.name === undefined ? {} : { name: body.name }),
|
|
73
|
+
...(body.prompt === undefined ? {} : { prompt: body.prompt }),
|
|
74
|
+
...(schedule === undefined ? {} : { schedule }),
|
|
75
|
+
...(notificationPolicy === undefined ? {} : { notificationPolicy }),
|
|
76
|
+
...(body.pauseAfterConsecutiveFailures === undefined ? {} : { pauseAfterConsecutiveFailures: body.pauseAfterConsecutiveFailures }),
|
|
77
|
+
...(permissionPreset === undefined ? {} : { permissionPreset }),
|
|
78
|
+
...(permissionPreset === undefined || body.confirmPermissionChange !== true ? {} : { permissionChangeConfirmed: true }),
|
|
79
|
+
...(execution === undefined ? {} : { execution: { ...execution, ...(execution.target !== undefined && body.confirmSessionTargetChange === true ? { sessionTargetConfirmed: true } : {}) } }),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function parseExecutionPatch(value, execution) {
|
|
83
|
+
if (value === undefined)
|
|
84
|
+
return undefined;
|
|
85
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
86
|
+
throw new Error('execution must be an object.');
|
|
87
|
+
const input = value;
|
|
88
|
+
if (Object.keys(input).length === 0)
|
|
89
|
+
throw new Error('execution must contain at least one field.');
|
|
90
|
+
if (Object.keys(input).some((key) => !['agentPreset', 'provider', 'model', 'reasoningEffort', 'skills', 'target'].includes(key))) {
|
|
91
|
+
throw new Error('execution contains an unknown field.');
|
|
92
|
+
}
|
|
93
|
+
const nullableString = (key) => {
|
|
94
|
+
const entry = input[key];
|
|
95
|
+
if (entry !== undefined && entry !== null && typeof entry !== 'string')
|
|
96
|
+
throw new Error(`execution.${key} must be a string or null.`);
|
|
97
|
+
assertOverrideId(entry, `execution.${key}`);
|
|
98
|
+
return entry;
|
|
99
|
+
};
|
|
100
|
+
const agentPreset = nullableString('agentPreset');
|
|
101
|
+
const provider = nullableString('provider');
|
|
102
|
+
const model = nullableString('model');
|
|
103
|
+
const reasoningEffort = input.reasoningEffort;
|
|
104
|
+
if (reasoningEffort !== undefined && reasoningEffort !== null && typeof reasoningEffort !== 'string')
|
|
105
|
+
throw new Error('execution.reasoningEffort must be a string or null.');
|
|
106
|
+
assertReasoningEffort(reasoningEffort);
|
|
107
|
+
assertProviderModelPair(provider, model);
|
|
108
|
+
if (input.skills !== undefined && (!Array.isArray(input.skills) || input.skills.some((name) => typeof name !== 'string'))) {
|
|
109
|
+
throw new Error('execution.skills must be an array of strings.');
|
|
110
|
+
}
|
|
111
|
+
const skills = input.skills;
|
|
112
|
+
let target;
|
|
113
|
+
if (input.target !== undefined) {
|
|
114
|
+
if (typeof input.target !== 'object' || input.target === null || Array.isArray(input.target))
|
|
115
|
+
throw new Error('execution.target must be an object.');
|
|
116
|
+
const value = input.target;
|
|
117
|
+
if (value.mode === 'fresh' && Object.keys(value).every((key) => key === 'mode'))
|
|
118
|
+
target = { mode: 'fresh' };
|
|
119
|
+
else if (value.mode === 'pinned-session' && Object.keys(value).every((key) => key === 'mode' || key === 'sessionId') && typeof value.sessionId === 'string' && value.sessionId.trim()) {
|
|
120
|
+
target = { mode: 'pinned-session', sessionId: value.sessionId, workspaceId: execution.workspaceId, cwd: execution.cwd, fallback: 'fail' };
|
|
121
|
+
}
|
|
122
|
+
else
|
|
123
|
+
throw new Error('execution.target is invalid.');
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
...(agentPreset === undefined ? {} : { agentPreset }),
|
|
127
|
+
...(provider === undefined ? {} : { provider }),
|
|
128
|
+
...(model === undefined ? {} : { model }),
|
|
129
|
+
...(reasoningEffort === undefined ? {} : { reasoningEffort }),
|
|
130
|
+
...(skills === undefined ? {} : { skills }),
|
|
131
|
+
...(target === undefined ? {} : { target }),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export function registerAutomationApi(ctx, controller) {
|
|
135
|
+
return ctx.webServer.register({
|
|
136
|
+
kind: 'prefix',
|
|
137
|
+
path: API_ROOT,
|
|
138
|
+
async handler(req, res) {
|
|
139
|
+
const rejection = ctx.connection.requestRejection(req);
|
|
140
|
+
if (rejection !== undefined) {
|
|
141
|
+
send(res, rejection, { error: rejection === 401 ? 'Unauthorized' : 'Forbidden' });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!sameOrigin(req)) {
|
|
145
|
+
send(res, 403, { error: 'Cross-origin requests are not allowed.' });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (req.method !== 'GET' && req.headers['x-dsh-automation'] !== '1') {
|
|
149
|
+
send(res, 403, { error: 'Missing Automation request header.' });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const url = new URL(req.url ?? API_ROOT, 'http://localhost');
|
|
153
|
+
const suffix = url.pathname.slice(API_ROOT.length);
|
|
154
|
+
try {
|
|
155
|
+
if (req.method === 'POST' && suffix === '/notifications/read') {
|
|
156
|
+
await controller.markNotificationsRead();
|
|
157
|
+
send(res, 200, { read: true });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (req.method === 'GET' && (suffix === '' || suffix === '/tasks')) {
|
|
161
|
+
send(res, 200, { tasks: controller.list(), scheduler: controller.schedulerHealth() });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (req.method === 'GET' && suffix === '/delivery-options') {
|
|
165
|
+
const botId = url.searchParams.get('botId') ?? undefined;
|
|
166
|
+
if (botId !== undefined && (!botId.trim() || botId.length > 256))
|
|
167
|
+
throw new Error('Invalid dsh-im bot id.');
|
|
168
|
+
send(res, 200, { options: await controller.deliveryOptions(botId) });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const runMatch = /^\/tasks\/([^/]+)\/runs\/([^/]+)$/.exec(suffix);
|
|
172
|
+
if (runMatch !== null) {
|
|
173
|
+
if (req.method !== 'DELETE') {
|
|
174
|
+
send(res, 405, { error: 'Method not allowed.' });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const [, rawTaskId = '', rawRunId = ''] = runMatch;
|
|
178
|
+
const taskId = decodeURIComponent(rawTaskId);
|
|
179
|
+
const runId = decodeURIComponent(rawRunId);
|
|
180
|
+
if (![taskId, runId].every((id) => /^[A-Za-z0-9_-]{1,256}$/.test(id))) {
|
|
181
|
+
throw new Error('Invalid automation task or run id.');
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
send(res, 200, { deleted: await controller.deleteRun(taskId, runId) });
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
if (!(error instanceof AutomationDomainError))
|
|
188
|
+
throw error;
|
|
189
|
+
if (error.code === 'task_not_found')
|
|
190
|
+
send(res, 404, { error: error.message });
|
|
191
|
+
else if (error.code === 'run_in_progress')
|
|
192
|
+
send(res, 409, { error: error.message });
|
|
193
|
+
else
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const match = /^\/tasks\/([^/]+)(?:\/(run|pause|resume|stop|options))?$/.exec(suffix);
|
|
199
|
+
if (match?.[1] === undefined) {
|
|
200
|
+
send(res, 404, { error: 'Automation API route not found.' });
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const id = decodeURIComponent(match[1]);
|
|
204
|
+
const action = match[2];
|
|
205
|
+
if (req.method === 'GET' && action === 'options') {
|
|
206
|
+
const candidate = url.searchParams.has('agentPreset') ? url.searchParams.get('agentPreset') || null : undefined;
|
|
207
|
+
const provider = url.searchParams.get('provider') ?? undefined;
|
|
208
|
+
const model = url.searchParams.get('model') ?? undefined;
|
|
209
|
+
assertProviderModelPair(provider, model);
|
|
210
|
+
send(res, 200, { options: await controller.options(id, candidate, provider, model) });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (req.method === 'PATCH' && action === undefined) {
|
|
214
|
+
send(res, 200, { task: await controller.update(id, parseUpdate(await readJson(req), controller.get(id))) });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (req.method === 'DELETE' && action === undefined) {
|
|
218
|
+
const deleted = await controller.delete(id);
|
|
219
|
+
send(res, deleted ? 200 : 404, { deleted });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (req.method !== 'POST') {
|
|
223
|
+
send(res, 405, { error: 'Method not allowed.' });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (action === 'run') {
|
|
227
|
+
send(res, 202, { run: await controller.runNow(id) });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (action === 'stop') {
|
|
231
|
+
send(res, 202, await controller.stop(id));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (action === 'pause') {
|
|
235
|
+
send(res, 200, { task: await controller.pause(id) });
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (action === 'resume') {
|
|
239
|
+
const body = await readJson(req);
|
|
240
|
+
if (body.runNow !== undefined && typeof body.runNow !== 'boolean')
|
|
241
|
+
throw new Error('runNow must be boolean.');
|
|
242
|
+
send(res, 200, { task: await controller.resume(id, { runNow: body.runNow === true }) });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
send(res, 404, { error: 'Automation API route not found.' });
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
send(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
//# sourceMappingURL=api.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
4
|
+
type AutomationPanelProps = PropsRuntime<'shell.overlay'> & {
|
|
5
|
+
ctx: Context;
|
|
6
|
+
};
|
|
7
|
+
export declare function AutomationPanel({ ctx, useSessions, useWorkspaces }: AutomationPanelProps): React.JSX.Element | null;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=AutomationPanel.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { t as translate } from './i18n.js';
|
|
3
|
+
/** Templates offered when no automation exists yet. */
|
|
4
|
+
export declare function EmptyState({ t, creating, disabled, onStart }: {
|
|
5
|
+
t: typeof translate;
|
|
6
|
+
creating: string | undefined;
|
|
7
|
+
disabled: boolean;
|
|
8
|
+
onStart: (id: string, prompt: string) => void;
|
|
9
|
+
}): React.JSX.Element;
|
|
10
|
+
//# sourceMappingURL=EmptyState.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { t as translate } from './i18n.js';
|
|
3
|
+
import type { SelectOption } from './shared.js';
|
|
4
|
+
/** Composer-style navigation, controlled by the task draft rather than a live Session. */
|
|
5
|
+
export declare function ModelSelection({ model, effort, models, efforts, disabled, modelDisabled, effortDisabled, onModel, onEffort, t }: {
|
|
6
|
+
model: string;
|
|
7
|
+
effort: string;
|
|
8
|
+
models: readonly SelectOption[];
|
|
9
|
+
efforts: readonly SelectOption[];
|
|
10
|
+
disabled: boolean;
|
|
11
|
+
modelDisabled: boolean;
|
|
12
|
+
effortDisabled: boolean;
|
|
13
|
+
onModel: (value: string) => void;
|
|
14
|
+
onEffort: (value: string) => void;
|
|
15
|
+
t: typeof translate;
|
|
16
|
+
}): React.JSX.Element;
|
|
17
|
+
//# sourceMappingURL=ModelSelection.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { AutomationTaskView } from '../types.js';
|
|
3
|
+
import { t as translate } from './i18n.js';
|
|
4
|
+
import { type CommonRRule } from './rrule-editor.js';
|
|
5
|
+
/** Keep both schedule modes while editing, and serialize only on valid submission. */
|
|
6
|
+
export declare function useScheduleEditor(task: AutomationTaskView): {
|
|
7
|
+
kind: "once" | "recurring";
|
|
8
|
+
setKind: React.Dispatch<React.SetStateAction<"once" | "recurring">>;
|
|
9
|
+
onceAt: string;
|
|
10
|
+
setOnceAt: React.Dispatch<React.SetStateAction<string>>;
|
|
11
|
+
rrule: string;
|
|
12
|
+
setRrule: React.Dispatch<React.SetStateAction<string>>;
|
|
13
|
+
advancedRule: boolean;
|
|
14
|
+
setAdvancedRule: React.Dispatch<React.SetStateAction<boolean>>;
|
|
15
|
+
commonRule: CommonRRule;
|
|
16
|
+
setCommonRule: React.Dispatch<React.SetStateAction<CommonRRule>>;
|
|
17
|
+
timeZone: string;
|
|
18
|
+
setTimeZone: React.Dispatch<React.SetStateAction<string>>;
|
|
19
|
+
startAt: string;
|
|
20
|
+
setStartAt: React.Dispatch<React.SetStateAction<string>>;
|
|
21
|
+
parsedRawRule: CommonRRule | undefined;
|
|
22
|
+
scheduleChanged: boolean;
|
|
23
|
+
valid: boolean;
|
|
24
|
+
getSchedule: () => AutomationTaskView["schedule"] | undefined;
|
|
25
|
+
};
|
|
26
|
+
/** One-time and recurring controls share the form's schedule draft. */
|
|
27
|
+
export declare function ScheduleSection({ schedule, saving, t }: {
|
|
28
|
+
schedule: ReturnType<typeof useScheduleEditor>;
|
|
29
|
+
saving: boolean;
|
|
30
|
+
t: typeof translate;
|
|
31
|
+
}): React.JSX.Element;
|
|
32
|
+
//# sourceMappingURL=ScheduleSection.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { AutomationSchedulerHealth } from '../types.js';
|
|
3
|
+
import type { t as translate } from './i18n.js';
|
|
4
|
+
/** Scheduler state as a dot whose hover card carries every health field. */
|
|
5
|
+
export declare function SchedulerHealth({ health, locale, t }: {
|
|
6
|
+
health: AutomationSchedulerHealth | undefined;
|
|
7
|
+
locale: string;
|
|
8
|
+
t: typeof translate;
|
|
9
|
+
}): React.JSX.Element | null;
|
|
10
|
+
//# sourceMappingURL=SchedulerHealth.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { AgentConfigurationOptions } from '../types.js';
|
|
3
|
+
import { t as translate } from './i18n.js';
|
|
4
|
+
/**
|
|
5
|
+
* Filterable skill picker showing each skill's description and invocability,
|
|
6
|
+
* with the current selection restated as removable chips.
|
|
7
|
+
*/
|
|
8
|
+
export declare function SkillPicker({ skills, options, disabled, t, onChange }: {
|
|
9
|
+
skills: readonly string[];
|
|
10
|
+
options: AgentConfigurationOptions | undefined;
|
|
11
|
+
disabled: boolean;
|
|
12
|
+
t: typeof translate;
|
|
13
|
+
onChange: (next: string[]) => void;
|
|
14
|
+
}): React.JSX.Element;
|
|
15
|
+
//# sourceMappingURL=SkillPicker.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
3
|
+
import type { AutomationTaskView } from '../types.js';
|
|
4
|
+
import { t as translate } from './i18n.js';
|
|
5
|
+
/** Human sentence for a task's schedule, whichever kind it is. */
|
|
6
|
+
export declare function scheduleLabel(task: AutomationTaskView, locale: string, t: typeof translate): string;
|
|
7
|
+
/** Display name for a permission preset, preferring the Host's own label. */
|
|
8
|
+
export declare function permissionLabel(preset: string, t: typeof translate, displayName?: string): string;
|
|
9
|
+
/** Callbacks the detail pane needs from the panel. */
|
|
10
|
+
export interface TaskDetailActions {
|
|
11
|
+
run: () => void;
|
|
12
|
+
stop: () => void;
|
|
13
|
+
pause: () => void;
|
|
14
|
+
resume: (runNow: boolean) => void;
|
|
15
|
+
edit: () => void;
|
|
16
|
+
requestDelete: () => void;
|
|
17
|
+
requestDeleteRun: (run: AutomationTaskView['runs'][number], trigger: HTMLButtonElement) => void;
|
|
18
|
+
openSession: (sessionId: SessionId) => void;
|
|
19
|
+
back: () => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A focused overview with run history and configuration available on demand.
|
|
23
|
+
* @param props.task - the selected automation.
|
|
24
|
+
* @param props.pending - a write for this task is in flight.
|
|
25
|
+
* @returns the detail pane.
|
|
26
|
+
*/
|
|
27
|
+
export declare function TaskDetail({ task, locale, t, pending, actions }: {
|
|
28
|
+
task: AutomationTaskView;
|
|
29
|
+
locale: string;
|
|
30
|
+
t: typeof translate;
|
|
31
|
+
pending: boolean;
|
|
32
|
+
actions: TaskDetailActions;
|
|
33
|
+
}): React.JSX.Element;
|
|
34
|
+
//# sourceMappingURL=TaskDetail.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { SessionListState } from '@deepseek-ai/dsh-api-session-controller/client';
|
|
3
|
+
import type { AutomationDelivery, AutomationExecutionPatch, AutomationTaskView } from '../types.js';
|
|
4
|
+
import { t as translate } from './i18n.js';
|
|
5
|
+
/** The PATCH body shape the controller accepts. */
|
|
6
|
+
export type TaskUpdateBody = Partial<Pick<AutomationTaskView, 'name' | 'prompt' | 'schedule' | 'notificationPolicy' | 'pauseAfterConsecutiveFailures'>> & {
|
|
7
|
+
permissionPreset?: AutomationTaskView['security']['permissionPreset'];
|
|
8
|
+
confirmPermissionChange?: true;
|
|
9
|
+
confirmSessionTargetChange?: true;
|
|
10
|
+
delivery?: AutomationDelivery | null;
|
|
11
|
+
confirmDeliveryChange?: true;
|
|
12
|
+
execution?: Omit<AutomationExecutionPatch, 'target' | 'sessionTargetConfirmed'> & {
|
|
13
|
+
target?: {
|
|
14
|
+
mode: 'fresh';
|
|
15
|
+
} | {
|
|
16
|
+
mode: 'pinned-session';
|
|
17
|
+
sessionId: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Edit one automation in place in the detail pane.
|
|
23
|
+
* @param props.task - the automation being edited.
|
|
24
|
+
* @param props.saving - a write is in flight; every control locks.
|
|
25
|
+
* @returns the editor form.
|
|
26
|
+
*/
|
|
27
|
+
export declare function TaskEditor({ task, sessions, workspaceSessionIds, refreshSessions, saving, t, onSave, onCancel, onDirtyChange }: {
|
|
28
|
+
task: AutomationTaskView;
|
|
29
|
+
sessions: SessionListState;
|
|
30
|
+
workspaceSessionIds: readonly string[];
|
|
31
|
+
refreshSessions: () => Promise<void>;
|
|
32
|
+
saving: boolean;
|
|
33
|
+
t: typeof translate;
|
|
34
|
+
onSave: (body: TaskUpdateBody) => void;
|
|
35
|
+
onCancel: () => void;
|
|
36
|
+
onDirtyChange?: (dirty: boolean) => void;
|
|
37
|
+
}): React.JSX.Element;
|
|
38
|
+
//# sourceMappingURL=TaskEditor.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { AutomationTaskView } from '../types.js';
|
|
3
|
+
import type { t as translate } from './i18n.js';
|
|
4
|
+
/** One scannable row in the master list. */
|
|
5
|
+
export declare function TaskRow({ task, locale, t, selected, onSelect }: {
|
|
6
|
+
task: AutomationTaskView;
|
|
7
|
+
locale: string;
|
|
8
|
+
t: typeof translate;
|
|
9
|
+
selected: boolean;
|
|
10
|
+
onSelect: () => void;
|
|
11
|
+
}): React.JSX.Element;
|
|
12
|
+
//# sourceMappingURL=TaskRow.d.ts.map
|