@sparkelf/dsh-task-bus 0.0.0-stage → 0.1.1
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/README.md +32 -2
- package/cordis.patch.yml +3 -0
- package/lib/client.js +2332 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +19 -0
- package/lib/index.js +485 -0
- package/package.json +87 -4
package/lib/index.js
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, isAbsolute, join } from "node:path";
|
|
3
|
+
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
4
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
5
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
6
|
+
//#region src/index.ts
|
|
7
|
+
const name = "task-bus";
|
|
8
|
+
const inject = [
|
|
9
|
+
"webServer",
|
|
10
|
+
"tools",
|
|
11
|
+
"sessionController",
|
|
12
|
+
"credentials",
|
|
13
|
+
"attachments"
|
|
14
|
+
];
|
|
15
|
+
const TASK_BUS_ROUTE = "/dsh-task-bus/api";
|
|
16
|
+
const TASK_CENTER_PATH = "/api/task-center";
|
|
17
|
+
const DATAOPS_CREDENTIAL_REF = "DATAOPS_ACCESS_TOKEN";
|
|
18
|
+
const resolveBase = () => {
|
|
19
|
+
const base = process.env.DATAOPS_AI_WORKSPACE_BACKEND_BASE_URL?.trim().replace(/\/+$/u, "");
|
|
20
|
+
if (!base) throw new Error("DataOps Task Center is not configured in this workspace");
|
|
21
|
+
return base;
|
|
22
|
+
};
|
|
23
|
+
/** Make approval links usable inside the DataOps iframe, where `/tasks` is not this origin. */
|
|
24
|
+
const absoluteApprovalLinks = (payload) => {
|
|
25
|
+
const origin = process.env.DATAOPS_PUBLIC_APP_ORIGIN?.trim().replace(/\/+$/u, "");
|
|
26
|
+
if (!origin || payload === null || typeof payload !== "object") return payload;
|
|
27
|
+
const fix = (value) => {
|
|
28
|
+
if (value === null || typeof value !== "object") return;
|
|
29
|
+
const item = value;
|
|
30
|
+
if (item.source !== void 0 && typeof item.source.deepLink === "string" && item.source.deepLink.startsWith("/")) item.source.deepLink = origin + item.source.deepLink;
|
|
31
|
+
};
|
|
32
|
+
const record = payload;
|
|
33
|
+
fix(record.item);
|
|
34
|
+
for (const item of record.items ?? []) fix(item);
|
|
35
|
+
return payload;
|
|
36
|
+
};
|
|
37
|
+
const createTaskCenterClient = (host) => {
|
|
38
|
+
const base = resolveBase();
|
|
39
|
+
return async (path, method = "GET", body) => {
|
|
40
|
+
const token = (await host.credentials.resolve(credentialRef(DATAOPS_CREDENTIAL_REF)))?.value?.trim();
|
|
41
|
+
if (!token) throw new Error("DataOps Task Center credential is not configured");
|
|
42
|
+
const response = await fetch(base + TASK_CENTER_PATH + path, {
|
|
43
|
+
method,
|
|
44
|
+
headers: {
|
|
45
|
+
"content-type": "application/json",
|
|
46
|
+
authorization: `Bearer ${token}`
|
|
47
|
+
},
|
|
48
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
49
|
+
});
|
|
50
|
+
const text = await response.text();
|
|
51
|
+
const payload = text ? JSON.parse(text) : null;
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
const message = payload !== null && typeof payload === "object" && "message" in payload ? String(payload.message) : `Task Center request failed (${response.status})`;
|
|
54
|
+
throw new Error(message);
|
|
55
|
+
}
|
|
56
|
+
return payload;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
const readJsonBody = async (request) => {
|
|
60
|
+
if (request.on === void 0) return void 0;
|
|
61
|
+
let body = "";
|
|
62
|
+
for await (const chunk of request) body += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
63
|
+
return body.trim() ? JSON.parse(body) : void 0;
|
|
64
|
+
};
|
|
65
|
+
const writeJson = (response, status, body) => {
|
|
66
|
+
response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
67
|
+
response.end(JSON.stringify(body));
|
|
68
|
+
};
|
|
69
|
+
const taskListPath = (search) => {
|
|
70
|
+
const params = new URLSearchParams(search);
|
|
71
|
+
if (!params.has("status")) params.set("status", "all");
|
|
72
|
+
return "/items?" + params.toString();
|
|
73
|
+
};
|
|
74
|
+
const genericListPath = (search) => {
|
|
75
|
+
const params = new URLSearchParams(search);
|
|
76
|
+
if (!params.has("scope")) params.set("scope", "mine");
|
|
77
|
+
if (!params.has("pageSize")) params.set("pageSize", "20");
|
|
78
|
+
return "/generic?" + params.toString();
|
|
79
|
+
};
|
|
80
|
+
const open = {
|
|
81
|
+
type: "object",
|
|
82
|
+
additionalProperties: true
|
|
83
|
+
};
|
|
84
|
+
/** Where this workspace stores the connector's own enabled state. */
|
|
85
|
+
const CONNECTOR_STATE_PATH = join(process.env.DSH_HOME?.trim() || "/workspace/.dataops/dsh", "task-bus", "connector-state.json");
|
|
86
|
+
const DEFAULT_CONNECTOR_STATE = {
|
|
87
|
+
enabled: false,
|
|
88
|
+
capabilities: { "approval.push": true }
|
|
89
|
+
};
|
|
90
|
+
/** Read the workspace connector state; a missing/corrupt file is a safe disabled state. */
|
|
91
|
+
async function readConnectorState(path = CONNECTOR_STATE_PATH) {
|
|
92
|
+
try {
|
|
93
|
+
const text = await readFile(path, "utf8");
|
|
94
|
+
const value = JSON.parse(text);
|
|
95
|
+
return {
|
|
96
|
+
enabled: value.enabled === true,
|
|
97
|
+
capabilities: {
|
|
98
|
+
...DEFAULT_CONNECTOR_STATE.capabilities,
|
|
99
|
+
...typeof value.capabilities === "object" && value.capabilities !== null ? value.capabilities : {}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
} catch {
|
|
103
|
+
return {
|
|
104
|
+
...DEFAULT_CONNECTOR_STATE,
|
|
105
|
+
capabilities: { ...DEFAULT_CONNECTOR_STATE.capabilities }
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Persist the connector state atomically; the host and UI read the same file. */
|
|
110
|
+
async function writeConnectorState(next, path = CONNECTOR_STATE_PATH) {
|
|
111
|
+
await mkdir(dirname(path), { recursive: true });
|
|
112
|
+
const temporary = path + ".tmp-" + process.pid.toString();
|
|
113
|
+
await writeFile(temporary, JSON.stringify(next, null, 2), "utf8");
|
|
114
|
+
await rename(temporary, path);
|
|
115
|
+
}
|
|
116
|
+
const renderJson = (_args, value) => [{
|
|
117
|
+
type: "text",
|
|
118
|
+
text: JSON.stringify(value, null, 2)
|
|
119
|
+
}];
|
|
120
|
+
const tool = (name, description, parameters, execute) => defineTool({
|
|
121
|
+
name,
|
|
122
|
+
description,
|
|
123
|
+
parameters,
|
|
124
|
+
output: {
|
|
125
|
+
schema: open,
|
|
126
|
+
render: renderJson
|
|
127
|
+
},
|
|
128
|
+
execute: (async (args) => execute(args))
|
|
129
|
+
});
|
|
130
|
+
const apply = (ctx) => {
|
|
131
|
+
const host = ctx;
|
|
132
|
+
const client = createTaskCenterClient(host);
|
|
133
|
+
ctx.effect(() => host.webServer.register({
|
|
134
|
+
kind: "prefix",
|
|
135
|
+
path: TASK_BUS_ROUTE,
|
|
136
|
+
handler: async (rawRequest, rawResponse) => {
|
|
137
|
+
const request = rawRequest;
|
|
138
|
+
const response = rawResponse;
|
|
139
|
+
const url = new URL(request.url ?? "/", "http://dsh.internal");
|
|
140
|
+
const route = url.pathname.slice(17).replace(/^\/+/u, "");
|
|
141
|
+
try {
|
|
142
|
+
if (route === "connector") {
|
|
143
|
+
if (request.method === "GET") {
|
|
144
|
+
writeJson(response, 200, await readConnectorState());
|
|
145
|
+
syncTools().catch(() => {
|
|
146
|
+
disposeTools();
|
|
147
|
+
});
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (request.method === "POST") {
|
|
151
|
+
const body = await readJsonBody(request);
|
|
152
|
+
if (typeof body?.enabled !== "boolean") throw new Error("connector state requires enabled:boolean");
|
|
153
|
+
const current = await readConnectorState();
|
|
154
|
+
const capabilities = typeof body.capabilities === "object" && body.capabilities !== null ? {
|
|
155
|
+
...current.capabilities,
|
|
156
|
+
...body.capabilities
|
|
157
|
+
} : current.capabilities;
|
|
158
|
+
await writeConnectorState({
|
|
159
|
+
enabled: body.enabled,
|
|
160
|
+
capabilities
|
|
161
|
+
});
|
|
162
|
+
await syncTools();
|
|
163
|
+
writeJson(response, 200, await readConnectorState());
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
writeJson(response, 405, { error: "connector state supports GET and POST" });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (request.method === "GET" && route === "health") {
|
|
170
|
+
resolveBase();
|
|
171
|
+
writeJson(response, 200, {
|
|
172
|
+
ok: true,
|
|
173
|
+
source: "dataops_approval"
|
|
174
|
+
});
|
|
175
|
+
syncTools();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (request.method === "GET" && route === "overview") {
|
|
179
|
+
writeJson(response, 200, absoluteApprovalLinks(await client("/overview")));
|
|
180
|
+
syncTools();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (request.method === "GET" && route === "attachment-materialize") {
|
|
184
|
+
const attachmentId = url.searchParams.get("attachmentId") ?? "";
|
|
185
|
+
const name = url.searchParams.get("name") ?? "attachment";
|
|
186
|
+
const bytes = Number(url.searchParams.get("bytes") ?? "0");
|
|
187
|
+
if (!attachmentId.startsWith("sha256:") || !Number.isSafeInteger(bytes) || bytes <= 0) throw new Error("attachment-materialize requires a sha256 attachmentId and byte count");
|
|
188
|
+
const attachments = ctx.attachments;
|
|
189
|
+
if (attachments === void 0) throw new Error("attachment store is unavailable");
|
|
190
|
+
const chunks = [];
|
|
191
|
+
for await (const chunk of attachments.readFileStream({
|
|
192
|
+
attachmentId,
|
|
193
|
+
name,
|
|
194
|
+
bytes
|
|
195
|
+
})) chunks.push(chunk);
|
|
196
|
+
const safeName = basename(name).replace(/[^\p{L}\p{N}._-]+/gu, "_") || "attachment";
|
|
197
|
+
const requestedCwd = url.searchParams.get("cwd")?.trim() ?? "";
|
|
198
|
+
const root = requestedCwd !== "" && isAbsolute(requestedCwd) ? requestedCwd : process.cwd();
|
|
199
|
+
const directory = join(root, ".dsh-attachment-preview");
|
|
200
|
+
await mkdir(directory, { recursive: true });
|
|
201
|
+
const relativePath = ".dsh-attachment-preview/" + attachmentId.slice(7, 19) + "-" + safeName;
|
|
202
|
+
await writeFile(join(root, relativePath), Buffer.concat(chunks));
|
|
203
|
+
writeJson(response, 200, {
|
|
204
|
+
path: relativePath,
|
|
205
|
+
absolutePath: join(root, relativePath),
|
|
206
|
+
name: safeName
|
|
207
|
+
});
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (request.method === "GET" && route === "count") {
|
|
211
|
+
writeJson(response, 200, await client("/count"));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (request.method === "GET" && route === "attachment") {
|
|
215
|
+
const taskId = url.searchParams.get("taskId")?.trim() ?? "";
|
|
216
|
+
const attachmentId = url.searchParams.get("attachmentId")?.trim() ?? "";
|
|
217
|
+
if (!/^\d+$/u.test(taskId) || !/^[0-9a-fA-F-]{36}$/u.test(attachmentId)) throw new Error("attachment requires numeric taskId and UUID attachmentId");
|
|
218
|
+
const token = (await host.credentials.resolve(credentialRef(DATAOPS_CREDENTIAL_REF)))?.value?.trim();
|
|
219
|
+
if (!token) throw new Error("DataOps Task Center credential is not configured");
|
|
220
|
+
const upstream = await fetch(resolveBase() + "/api/tasks/" + encodeURIComponent(taskId) + "/attachments/" + encodeURIComponent(attachmentId) + "/download", { headers: { authorization: `Bearer ${token}` } });
|
|
221
|
+
if (!upstream.ok) {
|
|
222
|
+
const text = await upstream.text();
|
|
223
|
+
throw new Error(text || `Attachment download failed (${upstream.status})`);
|
|
224
|
+
}
|
|
225
|
+
const buffer = Buffer.from(await upstream.arrayBuffer());
|
|
226
|
+
response.writeHead(upstream.status, {
|
|
227
|
+
"content-type": upstream.headers.get("content-type") ?? "application/octet-stream",
|
|
228
|
+
"content-disposition": upstream.headers.get("content-disposition") ?? "attachment",
|
|
229
|
+
"content-length": String(buffer.length),
|
|
230
|
+
"cache-control": "private, no-store"
|
|
231
|
+
});
|
|
232
|
+
response.end(buffer);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (request.method === "GET" && route === "items") {
|
|
236
|
+
writeJson(response, 200, absoluteApprovalLinks(await client(taskListPath(url.search))));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (request.method === "GET" && route === "generic") {
|
|
240
|
+
writeJson(response, 200, await client(genericListPath(url.search)));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (request.method === "POST" && route === "ai-accept") {
|
|
244
|
+
const body = await readJsonBody(request);
|
|
245
|
+
const id = typeof body?.id === "string" ? body.id : "";
|
|
246
|
+
if (!id) throw new Error("ai-accept requires a task id");
|
|
247
|
+
const task = id.startsWith("approval:") ? await client(`/items/${encodeURIComponent(id)}`) : id.startsWith("task:") ? await client(`/generic/${encodeURIComponent(id.slice(5))}`) : null;
|
|
248
|
+
if (task === null) throw new Error("ai-accept requires an approval:<id> or task:<uuid> id");
|
|
249
|
+
const card = task.item;
|
|
250
|
+
const title = card?.title ?? id;
|
|
251
|
+
const sessionId = `task-bus-${crypto.randomUUID()}`;
|
|
252
|
+
await host.sessionController.create({
|
|
253
|
+
sessionId,
|
|
254
|
+
...process.env.DATAOPS_AI_WORKSPACE_MOUNT_PATH === void 0 ? {} : { cwd: process.env.DATAOPS_AI_WORKSPACE_MOUNT_PATH }
|
|
255
|
+
});
|
|
256
|
+
const resolved = await host.sessionController.resolveAgent(sessionId);
|
|
257
|
+
const agent = "agent" in resolved ? resolved.agent : void 0;
|
|
258
|
+
if (agent === void 0) throw new Error("无法为任务绑定 AI 会话");
|
|
259
|
+
const prompt = String(body?.prompt ?? "") || `这是 DataOps 任务「${title}」(${id}),来源:${card?.source?.label ?? card?.sourceType ?? "Task Center"}。请先阅读任务上下文,整理材料并给出办理建议;不要替审批人通过、驳回或提交审批。`;
|
|
260
|
+
agent.followup(createUserMessage({
|
|
261
|
+
source: {
|
|
262
|
+
kind: "task-bus",
|
|
263
|
+
taskId: id
|
|
264
|
+
},
|
|
265
|
+
content: [{
|
|
266
|
+
type: "text",
|
|
267
|
+
text: prompt
|
|
268
|
+
}]
|
|
269
|
+
}));
|
|
270
|
+
writeJson(response, 201, {
|
|
271
|
+
sessionId,
|
|
272
|
+
taskId: id
|
|
273
|
+
});
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const transition = /^generic\/([^/]+)\/transition$/u.exec(route);
|
|
277
|
+
if (request.method === "POST" && transition !== null) {
|
|
278
|
+
writeJson(response, 200, await client(`/generic/${encodeURIComponent(transition[1])}/transition`, "POST", await readJsonBody(request)));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (request.method === "POST" && route === "generic") {
|
|
282
|
+
writeJson(response, 201, await client("/generic", "POST", await readJsonBody(request)));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const detail = /^generic\/([^/]+)$/u.exec(route);
|
|
286
|
+
if (request.method === "GET" && detail !== null) {
|
|
287
|
+
writeJson(response, 200, await client(`/generic/${encodeURIComponent(detail[1])}`));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const approval = /^items\/([^/]+)$/u.exec(route);
|
|
291
|
+
if (request.method === "GET" && approval !== null) {
|
|
292
|
+
writeJson(response, 200, absoluteApprovalLinks(await client(`/items/${encodeURIComponent(decodeURIComponent(approval[1]))}`)));
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
writeJson(response, 404, { error: "Unknown Task Bus route" });
|
|
296
|
+
} catch (error) {
|
|
297
|
+
writeJson(response, 502, { error: error instanceof Error ? error.message : String(error) });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}), "task-bus: browser API");
|
|
301
|
+
/**
|
|
302
|
+
* Tool definitions are static; only their registration is permission-dependent.
|
|
303
|
+
*
|
|
304
|
+
* This is the plugin-side disclosure boundary: the model never sees a tool the
|
|
305
|
+
* current DataOps principal cannot use. Authorization itself remains at the
|
|
306
|
+
* DataOps Task Center API; this layer only adjusts what the connector contributes.
|
|
307
|
+
*/
|
|
308
|
+
const toolDefinitions = [
|
|
309
|
+
{
|
|
310
|
+
permission: "workflow:task:read",
|
|
311
|
+
definition: tool("task_center_list", "List DataOps Task Center cards visible to the current workspace user, including approval tasks and generic Task Bus tasks. Approval cards are read-only projections of DataOps tasks.", {
|
|
312
|
+
status: {
|
|
313
|
+
type: "string",
|
|
314
|
+
enum: [
|
|
315
|
+
"all",
|
|
316
|
+
"pending",
|
|
317
|
+
"done"
|
|
318
|
+
]
|
|
319
|
+
},
|
|
320
|
+
scope: {
|
|
321
|
+
type: "string",
|
|
322
|
+
enum: [
|
|
323
|
+
"mine",
|
|
324
|
+
"created",
|
|
325
|
+
"all"
|
|
326
|
+
]
|
|
327
|
+
},
|
|
328
|
+
limit: { type: "number" }
|
|
329
|
+
}, async (args) => {
|
|
330
|
+
const status = typeof args.status === "string" ? args.status : "all";
|
|
331
|
+
const limit = typeof args.limit === "number" ? Math.max(1, Math.min(50, args.limit)) : 20;
|
|
332
|
+
const result = absoluteApprovalLinks(await client("/overview"));
|
|
333
|
+
const items = (result.items ?? []).filter((item) => {
|
|
334
|
+
if (status === "pending") return item.status !== "completed" && item.status !== "cancelled";
|
|
335
|
+
if (status === "done") return item.status === "completed" || item.status === "cancelled";
|
|
336
|
+
return true;
|
|
337
|
+
}).slice(0, limit);
|
|
338
|
+
return {
|
|
339
|
+
...result,
|
|
340
|
+
items,
|
|
341
|
+
total: items.length
|
|
342
|
+
};
|
|
343
|
+
})
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
permission: "workflow:task:read",
|
|
347
|
+
definition: tool("task_center_get", "Read one Task Center card by its stable id. Use approval:<id> for a DataOps approval task or task:<uuid> for a generic Task Bus task.", { id: {
|
|
348
|
+
type: "string",
|
|
349
|
+
required: true
|
|
350
|
+
} }, async (args) => {
|
|
351
|
+
const id = String(args.id ?? "");
|
|
352
|
+
if (id.startsWith("approval:")) return client(`/items/${encodeURIComponent(id)}`);
|
|
353
|
+
if (id.startsWith("task:")) return client(`/generic/${encodeURIComponent(id.slice(5))}`);
|
|
354
|
+
throw new Error("Task Center id must start with approval: or task:");
|
|
355
|
+
})
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
permission: "workflow:task:create",
|
|
359
|
+
definition: tool("task_center_create", [
|
|
360
|
+
"Create a generic Task Center card ONLY when the user explicitly asks you to create a task/card in this turn.",
|
|
361
|
+
"Never call this tool proactively, speculatively, as a side effect of another request, or merely because work could be tracked.",
|
|
362
|
+
"If the user did not explicitly ask for a new card, do not call it; ask for confirmation instead.",
|
|
363
|
+
"This never creates or mutates a DataOps approval task."
|
|
364
|
+
].join(" "), {
|
|
365
|
+
title: {
|
|
366
|
+
type: "string",
|
|
367
|
+
required: true
|
|
368
|
+
},
|
|
369
|
+
description: { type: "string" },
|
|
370
|
+
source_type: { type: "string" },
|
|
371
|
+
source_id: { type: "string" },
|
|
372
|
+
assignee_user_id: { type: "string" },
|
|
373
|
+
priority: { type: "string" },
|
|
374
|
+
user_requested: {
|
|
375
|
+
type: "boolean",
|
|
376
|
+
required: true,
|
|
377
|
+
description: "Must be true only when the user explicitly requested creation of this card in the current turn."
|
|
378
|
+
}
|
|
379
|
+
}, async (args) => {
|
|
380
|
+
if (args.user_requested !== true) throw new Error("task_center_create requires an explicit user request; do not create Task Center cards proactively");
|
|
381
|
+
return client("/generic", "POST", {
|
|
382
|
+
title: String(args.title ?? ""),
|
|
383
|
+
description: typeof args.description === "string" ? args.description : void 0,
|
|
384
|
+
sourceType: typeof args.source_type === "string" ? args.source_type : "agent",
|
|
385
|
+
sourceId: typeof args.source_id === "string" ? args.source_id : void 0,
|
|
386
|
+
assigneeUserId: typeof args.assignee_user_id === "string" ? args.assignee_user_id : void 0,
|
|
387
|
+
priority: typeof args.priority === "string" ? args.priority : void 0
|
|
388
|
+
});
|
|
389
|
+
})
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
permission: "workflow:task:execute",
|
|
393
|
+
definition: tool("task_center_transition", "Advance a generic Task Bus task. Approval tasks must be handled through the existing DataOps approval UI and APIs; this tool refuses approval ids.", {
|
|
394
|
+
id: {
|
|
395
|
+
type: "string",
|
|
396
|
+
required: true
|
|
397
|
+
},
|
|
398
|
+
action: {
|
|
399
|
+
type: "string",
|
|
400
|
+
required: true,
|
|
401
|
+
enum: [
|
|
402
|
+
"accept",
|
|
403
|
+
"start",
|
|
404
|
+
"submit",
|
|
405
|
+
"complete",
|
|
406
|
+
"cancel",
|
|
407
|
+
"block",
|
|
408
|
+
"unblock"
|
|
409
|
+
]
|
|
410
|
+
},
|
|
411
|
+
comment: { type: "string" }
|
|
412
|
+
}, async (args) => {
|
|
413
|
+
const id = String(args.id ?? "");
|
|
414
|
+
if (!id.startsWith("task:")) throw new Error("Only generic task:<uuid> cards can be transitioned here");
|
|
415
|
+
return client(`/generic/${encodeURIComponent(id.slice(5))}/transition`, "POST", {
|
|
416
|
+
action: String(args.action ?? ""),
|
|
417
|
+
comment: typeof args.comment === "string" ? args.comment : void 0
|
|
418
|
+
});
|
|
419
|
+
})
|
|
420
|
+
}
|
|
421
|
+
];
|
|
422
|
+
const toolDisposers = /* @__PURE__ */ new Map();
|
|
423
|
+
let syncGeneration = 0;
|
|
424
|
+
const disposeTools = () => {
|
|
425
|
+
for (const dispose of toolDisposers.values()) dispose();
|
|
426
|
+
toolDisposers.clear();
|
|
427
|
+
};
|
|
428
|
+
/** Read the current principal's DataOps permission keys without caching them per tool call. */
|
|
429
|
+
const loadPermissionKeys = async () => {
|
|
430
|
+
try {
|
|
431
|
+
const token = (await host.credentials.resolve(credentialRef(DATAOPS_CREDENTIAL_REF)))?.value?.trim();
|
|
432
|
+
if (!token) return null;
|
|
433
|
+
const response = await fetch(resolveBase() + "/api/ai/review/context", { headers: {
|
|
434
|
+
authorization: `Bearer ${token}`,
|
|
435
|
+
accept: "application/json"
|
|
436
|
+
} });
|
|
437
|
+
if (!response.ok) return null;
|
|
438
|
+
const body = await response.json();
|
|
439
|
+
const values = Array.isArray(body.permissions) ? body.permissions : Array.isArray(body.permissionKeys) ? body.permissionKeys : null;
|
|
440
|
+
return values === null ? null : new Set(values.filter((item) => typeof item === "string"));
|
|
441
|
+
} catch {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
/**
|
|
446
|
+
* Reconcile connector state, principal permissions and the registered tool set.
|
|
447
|
+
*
|
|
448
|
+
* Called at plugin start, after every browser API request, and after a connector
|
|
449
|
+
* enable/disable change. The tool execution path never repeats this work.
|
|
450
|
+
*/
|
|
451
|
+
const syncTools = async () => {
|
|
452
|
+
const generation = ++syncGeneration;
|
|
453
|
+
const state = await readConnectorState();
|
|
454
|
+
if (!state.enabled || state.capabilities["approval.push"] === false) {
|
|
455
|
+
disposeTools();
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const permissions = await loadPermissionKeys();
|
|
459
|
+
if (permissions === null) {
|
|
460
|
+
disposeTools();
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (generation !== syncGeneration) return;
|
|
464
|
+
for (const item of toolDefinitions) {
|
|
465
|
+
const existing = toolDisposers.get(item.definition.name);
|
|
466
|
+
if (permissions.has(item.permission)) {
|
|
467
|
+
if (existing === void 0) toolDisposers.set(item.definition.name, host.tools.register(item.definition));
|
|
468
|
+
} else if (existing !== void 0) {
|
|
469
|
+
existing();
|
|
470
|
+
toolDisposers.delete(item.definition.name);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
ctx.effect(() => {
|
|
475
|
+
syncTools().catch(() => {
|
|
476
|
+
disposeTools();
|
|
477
|
+
});
|
|
478
|
+
return () => {
|
|
479
|
+
syncGeneration += 1;
|
|
480
|
+
disposeTools();
|
|
481
|
+
};
|
|
482
|
+
}, "task-bus: permission-aware tool disclosure");
|
|
483
|
+
};
|
|
484
|
+
//#endregion
|
|
485
|
+
export { CONNECTOR_STATE_PATH, TASK_BUS_ROUTE, apply, inject, name, readConnectorState, writeConnectorState };
|
package/package.json
CHANGED
|
@@ -1,6 +1,89 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sparkelf/dsh-task-bus",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "DataOps Task Center and Task Bus for the official DSH Web shell.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./lib/index.js",
|
|
7
|
+
"types": "./lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json",
|
|
14
|
+
"./client": {
|
|
15
|
+
"default": "./lib/client.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"dsh": {
|
|
19
|
+
"bundle": {
|
|
20
|
+
"patch": "./cordis.patch.yml"
|
|
21
|
+
},
|
|
22
|
+
"client": {
|
|
23
|
+
"inject": [
|
|
24
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
25
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
26
|
+
"@deepseek-ai/dsh-client-locale",
|
|
27
|
+
"@deepseek-ai/dsh-client-ui-primitives"
|
|
28
|
+
],
|
|
29
|
+
"platform": "web"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"lib/index.js",
|
|
34
|
+
"lib/index.d.ts",
|
|
35
|
+
"lib/client.js",
|
|
36
|
+
"lib/client.js.map",
|
|
37
|
+
"cordis.patch.yml",
|
|
38
|
+
"README.md"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsdown",
|
|
42
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
46
|
+
},
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/SparkElf/dsh-plugins-plus.git",
|
|
51
|
+
"directory": "packages/task-bus"
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
58
|
+
"@deepseek-ai/dsh-client-locale": ">=0.1.7-rc.2",
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-layout": ">=0.2.1-alpha.2",
|
|
60
|
+
"@deepseek-ai/dsh-client-ui-slots": ">=0.1.7-rc.2",
|
|
61
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.1.7-rc.2",
|
|
62
|
+
"@deepseek-ai/dsh-tools": ">=0.1.7-rc.2",
|
|
63
|
+
"react": ">=18.2.0 <20",
|
|
64
|
+
"@deepseek-ai/dsh-llm": ">=0.1.7-rc.2",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-sidebar": ">=0.2.1-alpha.2",
|
|
66
|
+
"@deepseek-ai/dsh-credentials": ">=0.1.7-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-conversation": ">=0.1.7-rc.2",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-primitives": ">=0.2.0-rc.2"
|
|
69
|
+
},
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@deepseek-ai/cordis": "4.0.4",
|
|
72
|
+
"@deepseek-ai/dsh-client-locale": "0.1.7-rc.2",
|
|
73
|
+
"@deepseek-ai/dsh-client-ui-layout": "0.2.1-alpha.2",
|
|
74
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.7-rc.2",
|
|
75
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.7-rc.2",
|
|
76
|
+
"@deepseek-ai/dsh-tools": "0.1.7-rc.2",
|
|
77
|
+
"@types/node": "^22.0.0",
|
|
78
|
+
"@types/react": "~18.3.1",
|
|
79
|
+
"@types/react-dom": "^18.3.0",
|
|
80
|
+
"react": "^18.2.0",
|
|
81
|
+
"react-dom": "^18.2.0",
|
|
82
|
+
"tsdown": "^0.22.2",
|
|
83
|
+
"@deepseek-ai/dsh-llm": "0.1.7-rc.2",
|
|
84
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "0.2.1-alpha.2",
|
|
85
|
+
"@deepseek-ai/dsh-credentials": "0.1.7-rc.2",
|
|
86
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.1.7-rc.2",
|
|
87
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.2"
|
|
88
|
+
}
|
|
89
|
+
}
|