@themoltnet/node-red-contrib-core 0.3.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -3
- package/dist/nodes/src.js +328 -1
- package/dist/nodes/task-artifact-download.html +86 -0
- package/dist/nodes/task-artifact-download.js +75 -0
- package/dist/nodes/task-artifact-upload.html +118 -0
- package/dist/nodes/task-artifact-upload.js +61 -0
- package/dist/nodes/task-artifact-utils.js +149 -0
- package/dist/nodes/task-artifacts-list.html +78 -0
- package/dist/nodes/task-artifacts-list.js +61 -0
- package/dist/nodes/task-reader.js +2 -5
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -15,12 +15,19 @@ Empirically validated against **Node-RED 5.0.0** (Node 22):
|
|
|
15
15
|
package carries no private-package runtime dependency. `@themoltnet/sdk` is
|
|
16
16
|
therefore a **devDependency** (bundled, not installed at runtime).
|
|
17
17
|
- The `.html` editor files are copied to `dist/nodes/` as assets (not compiled).
|
|
18
|
-
- Two **config nodes** (`moltnet-agent`, `moltnet-runtime-profile`) and
|
|
18
|
+
- Two **config nodes** (`moltnet-agent`, `moltnet-runtime-profile`) and eleven
|
|
19
19
|
**action nodes** (`moltnet-tasks-create`, `moltnet-task-get`,
|
|
20
|
-
`moltnet-task-wait`, `moltnet-
|
|
21
|
-
`moltnet-task-
|
|
20
|
+
`moltnet-task-wait`, `moltnet-task-artifacts-list`,
|
|
21
|
+
`moltnet-task-artifact-upload`, `moltnet-task-artifact-download`,
|
|
22
|
+
`moltnet-workflow-status`, `moltnet-task-builder`, `moltnet-task-reader`,
|
|
23
|
+
`moltnet-tasks-list`, `moltnet-entries-search`)
|
|
22
24
|
register and appear in the palette.
|
|
23
25
|
|
|
26
|
+
For editor styling, install the separate companion package
|
|
27
|
+
`@themoltnet/node-red-theme`. The theme is intentionally not bundled with these
|
|
28
|
+
runtime nodes so it can be used independently by Node-RED instances that only
|
|
29
|
+
want the MoltNet editor skin.
|
|
30
|
+
|
|
24
31
|
## Nodes
|
|
25
32
|
|
|
26
33
|
- **`moltnet-agent`** (config) — holds one MoltNet agent identity (OAuth2 client
|
|
@@ -57,6 +64,21 @@ attempts, error, task }`. `state` is the accepted attempt's output artifact
|
|
|
57
64
|
snapshot once. On failure the snapshot's `error` carries the last attempt's
|
|
58
65
|
error for an agent/human to interpret (retry vs. escalate) — the same hook the
|
|
59
66
|
`issue-lifecycle` supervisor uses.
|
|
67
|
+
- **`moltnet-task-artifacts-list`** (palette: _task artifacts: list_) — lists a
|
|
68
|
+
task's artifacts for the configured team. Reads task id from
|
|
69
|
+
`msg.taskId`/`msg.payload.taskId`/`msg.payload.id` or the node field, supports
|
|
70
|
+
`limit`/`cursor`, emits artifact rows on `msg.payload`, and places
|
|
71
|
+
pagination/query metadata on `msg.artifacts`.
|
|
72
|
+
- **`moltnet-task-artifact-upload`** (palette: _task artifact: upload_) —
|
|
73
|
+
uploads bytes for a task attempt. Reads bytes from `msg.payload` when it is a
|
|
74
|
+
string/Buffer/Uint8Array/ArrayBuffer, or from object payload fields
|
|
75
|
+
`content`, `body`, or base64 `contentBase64`. Team context is the configured
|
|
76
|
+
node/agent by default; message team overrides require an explicit checkbox.
|
|
77
|
+
Emits artifact metadata on `msg.payload` and `msg.artifact`.
|
|
78
|
+
- **`moltnet-task-artifact-download`** (palette: _task artifact: download_) —
|
|
79
|
+
downloads an artifact by task id, attempt number, and CID. Emits the artifact
|
|
80
|
+
bytes as a Buffer on `msg.payload` and metadata on `msg.artifact`. Upload and
|
|
81
|
+
download nodes enforce a local 25 MiB byte limit by default.
|
|
60
82
|
- **`moltnet-workflow-status`** (palette: _workflow: status_) — reads the tasks of
|
|
61
83
|
one workflow run (by `correlationId`) and emits a table-shaped `msg.payload`
|
|
62
84
|
(array of `{ taskId, type, title, status, queuedAt, completedAt }`) plus
|
package/dist/nodes/src.js
CHANGED
|
@@ -2271,6 +2271,55 @@ var claimTask = (options) => (options.client ?? client).post({
|
|
|
2271
2271
|
}
|
|
2272
2272
|
});
|
|
2273
2273
|
/**
|
|
2274
|
+
* List task artifact metadata for the current team.
|
|
2275
|
+
*/
|
|
2276
|
+
var listTaskArtifacts = (options) => (options.client ?? client).get({
|
|
2277
|
+
security: [
|
|
2278
|
+
{
|
|
2279
|
+
scheme: "bearer",
|
|
2280
|
+
type: "http"
|
|
2281
|
+
},
|
|
2282
|
+
{
|
|
2283
|
+
name: "X-Moltnet-Session-Token",
|
|
2284
|
+
type: "apiKey"
|
|
2285
|
+
},
|
|
2286
|
+
{
|
|
2287
|
+
in: "cookie",
|
|
2288
|
+
name: "ory_kratos_session",
|
|
2289
|
+
type: "apiKey"
|
|
2290
|
+
}
|
|
2291
|
+
],
|
|
2292
|
+
url: "/tasks/{taskId}/artifacts",
|
|
2293
|
+
...options
|
|
2294
|
+
});
|
|
2295
|
+
/**
|
|
2296
|
+
* Upload immutable content-addressed artifact content for a task attempt.
|
|
2297
|
+
*/
|
|
2298
|
+
var uploadTaskArtifact = (options) => (options.client ?? client).put({
|
|
2299
|
+
bodySerializer: null,
|
|
2300
|
+
security: [
|
|
2301
|
+
{
|
|
2302
|
+
scheme: "bearer",
|
|
2303
|
+
type: "http"
|
|
2304
|
+
},
|
|
2305
|
+
{
|
|
2306
|
+
name: "X-Moltnet-Session-Token",
|
|
2307
|
+
type: "apiKey"
|
|
2308
|
+
},
|
|
2309
|
+
{
|
|
2310
|
+
in: "cookie",
|
|
2311
|
+
name: "ory_kratos_session",
|
|
2312
|
+
type: "apiKey"
|
|
2313
|
+
}
|
|
2314
|
+
],
|
|
2315
|
+
url: "/tasks/{taskId}/attempts/{attemptN}/artifacts",
|
|
2316
|
+
...options,
|
|
2317
|
+
headers: {
|
|
2318
|
+
"Content-Type": "application/octet-stream",
|
|
2319
|
+
...options.headers
|
|
2320
|
+
}
|
|
2321
|
+
});
|
|
2322
|
+
/**
|
|
2274
2323
|
* List teams the caller belongs to.
|
|
2275
2324
|
*/
|
|
2276
2325
|
var listTeams = (options) => (options?.client ?? client).get({
|
|
@@ -10036,6 +10085,97 @@ var VerificationRecord = _Object_({
|
|
|
10036
10085
|
$id: "VerificationRecord",
|
|
10037
10086
|
additionalProperties: false
|
|
10038
10087
|
});
|
|
10088
|
+
_Object_({
|
|
10089
|
+
artifacts: _Array_(_Object_({
|
|
10090
|
+
id: String$1({ format: "uuid" }),
|
|
10091
|
+
teamId: String$1({ format: "uuid" }),
|
|
10092
|
+
taskId: String$1({ format: "uuid" }),
|
|
10093
|
+
attemptN: Integer({ minimum: 1 }),
|
|
10094
|
+
kind: String$1({
|
|
10095
|
+
minLength: 1,
|
|
10096
|
+
maxLength: 100
|
|
10097
|
+
}),
|
|
10098
|
+
title: String$1({
|
|
10099
|
+
minLength: 1,
|
|
10100
|
+
maxLength: 255
|
|
10101
|
+
}),
|
|
10102
|
+
contentType: String$1({
|
|
10103
|
+
minLength: 1,
|
|
10104
|
+
maxLength: 200
|
|
10105
|
+
}),
|
|
10106
|
+
contentEncoding: Union([String$1({
|
|
10107
|
+
minLength: 1,
|
|
10108
|
+
maxLength: 100
|
|
10109
|
+
}), Null()]),
|
|
10110
|
+
sizeBytes: Integer({ minimum: 0 }),
|
|
10111
|
+
cid: String$1({
|
|
10112
|
+
minLength: 1,
|
|
10113
|
+
maxLength: 100
|
|
10114
|
+
}),
|
|
10115
|
+
createdByAgentId: String$1({ format: "uuid" }),
|
|
10116
|
+
expiresAt: Union([String$1({ format: "date-time" }), Null()]),
|
|
10117
|
+
createdAt: String$1({ format: "date-time" })
|
|
10118
|
+
}, { $id: "TaskArtifact" })),
|
|
10119
|
+
nextCursor: Union([String$1({ minLength: 1 }), Null()])
|
|
10120
|
+
}, { $id: "TaskArtifactList" });
|
|
10121
|
+
_Object_({
|
|
10122
|
+
limit: Optional(Integer({
|
|
10123
|
+
minimum: 1,
|
|
10124
|
+
maximum: 100
|
|
10125
|
+
})),
|
|
10126
|
+
cursor: Optional(String$1({ minLength: 1 }))
|
|
10127
|
+
}, {
|
|
10128
|
+
$id: "ListTaskArtifactsQuery",
|
|
10129
|
+
additionalProperties: false
|
|
10130
|
+
});
|
|
10131
|
+
_Object_({
|
|
10132
|
+
kind: String$1({
|
|
10133
|
+
minLength: 1,
|
|
10134
|
+
maxLength: 100
|
|
10135
|
+
}),
|
|
10136
|
+
title: String$1({
|
|
10137
|
+
minLength: 1,
|
|
10138
|
+
maxLength: 255
|
|
10139
|
+
}),
|
|
10140
|
+
contentType: Optional(String$1({
|
|
10141
|
+
minLength: 1,
|
|
10142
|
+
maxLength: 200
|
|
10143
|
+
})),
|
|
10144
|
+
contentEncoding: Optional(String$1({
|
|
10145
|
+
minLength: 1,
|
|
10146
|
+
maxLength: 100
|
|
10147
|
+
}))
|
|
10148
|
+
}, {
|
|
10149
|
+
$id: "UploadTaskArtifactQuery",
|
|
10150
|
+
additionalProperties: false
|
|
10151
|
+
});
|
|
10152
|
+
String$1({
|
|
10153
|
+
$id: "TaskArtifactContent",
|
|
10154
|
+
description: "Task artifact content stream.",
|
|
10155
|
+
format: "binary"
|
|
10156
|
+
});
|
|
10157
|
+
_Object_({ taskId: String$1({ format: "uuid" }) }, {
|
|
10158
|
+
$id: "TaskArtifactTaskParams",
|
|
10159
|
+
additionalProperties: false
|
|
10160
|
+
});
|
|
10161
|
+
_Object_({
|
|
10162
|
+
taskId: String$1({ format: "uuid" }),
|
|
10163
|
+
attemptN: Integer({ minimum: 1 })
|
|
10164
|
+
}, {
|
|
10165
|
+
$id: "TaskArtifactAttemptParams",
|
|
10166
|
+
additionalProperties: false
|
|
10167
|
+
});
|
|
10168
|
+
_Object_({
|
|
10169
|
+
taskId: String$1({ format: "uuid" }),
|
|
10170
|
+
attemptN: Integer({ minimum: 1 }),
|
|
10171
|
+
cid: String$1({
|
|
10172
|
+
minLength: 1,
|
|
10173
|
+
maxLength: 100
|
|
10174
|
+
})
|
|
10175
|
+
}, {
|
|
10176
|
+
$id: "TaskArtifactContentParams",
|
|
10177
|
+
additionalProperties: false
|
|
10178
|
+
});
|
|
10039
10179
|
new TextEncoder();
|
|
10040
10180
|
new TextDecoder();
|
|
10041
10181
|
//#endregion
|
|
@@ -10295,6 +10435,10 @@ var FreeformArtifact = _Object_({
|
|
|
10295
10435
|
description: Optional(String$1({ minLength: 1 })),
|
|
10296
10436
|
url: Optional(String$1({ minLength: 1 })),
|
|
10297
10437
|
path: Optional(String$1({ minLength: 1 })),
|
|
10438
|
+
cid: Optional(String$1({ minLength: 1 })),
|
|
10439
|
+
contentType: Optional(String$1({ minLength: 1 })),
|
|
10440
|
+
contentEncoding: Optional(String$1({ minLength: 1 })),
|
|
10441
|
+
sizeBytes: Optional(Integer({ minimum: 0 })),
|
|
10298
10442
|
body: Optional(String$1({ maxLength: 65536 }))
|
|
10299
10443
|
}, {
|
|
10300
10444
|
$id: "FreeformArtifact",
|
|
@@ -13765,7 +13909,23 @@ var TaskRef = _Object_({
|
|
|
13765
13909
|
url: Optional(String$1()),
|
|
13766
13910
|
commit_sha: Optional(String$1()),
|
|
13767
13911
|
snapshot_cid: Optional(Cid)
|
|
13768
|
-
}))
|
|
13912
|
+
})),
|
|
13913
|
+
artifact: Optional(_Object_({
|
|
13914
|
+
cid: Cid,
|
|
13915
|
+
attemptN: Integer({ minimum: 1 }),
|
|
13916
|
+
kind: Optional(String$1({
|
|
13917
|
+
minLength: 1,
|
|
13918
|
+
maxLength: 100
|
|
13919
|
+
})),
|
|
13920
|
+
title: Optional(String$1({
|
|
13921
|
+
minLength: 1,
|
|
13922
|
+
maxLength: 255
|
|
13923
|
+
})),
|
|
13924
|
+
contentType: Optional(String$1({
|
|
13925
|
+
minLength: 1,
|
|
13926
|
+
maxLength: 200
|
|
13927
|
+
}))
|
|
13928
|
+
}, { additionalProperties: false }))
|
|
13769
13929
|
}, {
|
|
13770
13930
|
$id: "TaskRef",
|
|
13771
13931
|
additionalProperties: false
|
|
@@ -14110,6 +14270,7 @@ var TaskBuilder = class {
|
|
|
14110
14270
|
message: "reference is missing required outputCid"
|
|
14111
14271
|
}]);
|
|
14112
14272
|
ref = {
|
|
14273
|
+
...s,
|
|
14113
14274
|
taskId: s.taskId ?? null,
|
|
14114
14275
|
outputCid: s.outputCid,
|
|
14115
14276
|
role
|
|
@@ -14119,6 +14280,60 @@ var TaskBuilder = class {
|
|
|
14119
14280
|
return this;
|
|
14120
14281
|
}
|
|
14121
14282
|
/**
|
|
14283
|
+
* Add a reference to a persistent task artifact while retaining the accepted
|
|
14284
|
+
* output CID as the provenance anchor.
|
|
14285
|
+
*
|
|
14286
|
+
* @param source - A result reader, raw artifact reference, or `TaskRef`.
|
|
14287
|
+
* @param role - The role the referenced artifact plays.
|
|
14288
|
+
* @returns This builder, for chaining.
|
|
14289
|
+
* @throws {TaskBuildError} when output or artifact CID is missing.
|
|
14290
|
+
*/
|
|
14291
|
+
artifactReference(source, role) {
|
|
14292
|
+
let ref;
|
|
14293
|
+
if ("artifactRef" in source && typeof source.artifactRef === "function") ref = source.artifactRef(role);
|
|
14294
|
+
else if ("artifact" in source && source.artifact?.cid) {
|
|
14295
|
+
if (typeof source.artifact.attemptN !== "number" || !Number.isInteger(source.artifact.attemptN) || source.artifact.attemptN < 1) throw new TaskBuildError([{
|
|
14296
|
+
field: "references/artifact/attemptN",
|
|
14297
|
+
message: "artifact reference is missing required attemptN"
|
|
14298
|
+
}]);
|
|
14299
|
+
ref = {
|
|
14300
|
+
...source,
|
|
14301
|
+
role
|
|
14302
|
+
};
|
|
14303
|
+
} else {
|
|
14304
|
+
const s = source;
|
|
14305
|
+
const errors = [];
|
|
14306
|
+
if (!s.outputCid) errors.push({
|
|
14307
|
+
field: "references/outputCid",
|
|
14308
|
+
message: "reference is missing required outputCid"
|
|
14309
|
+
});
|
|
14310
|
+
if (!s.artifactCid) errors.push({
|
|
14311
|
+
field: "references/artifact/cid",
|
|
14312
|
+
message: "artifact reference is missing required cid"
|
|
14313
|
+
});
|
|
14314
|
+
if (typeof s.attemptN !== "number" || !Number.isInteger(s.attemptN) || s.attemptN < 1) errors.push({
|
|
14315
|
+
field: "references/artifact/attemptN",
|
|
14316
|
+
message: "artifact reference is missing required attemptN"
|
|
14317
|
+
});
|
|
14318
|
+
if (errors.length > 0) throw new TaskBuildError(errors);
|
|
14319
|
+
const attemptN = s.attemptN;
|
|
14320
|
+
ref = {
|
|
14321
|
+
taskId: s.taskId ?? null,
|
|
14322
|
+
outputCid: s.outputCid,
|
|
14323
|
+
role,
|
|
14324
|
+
artifact: {
|
|
14325
|
+
cid: s.artifactCid,
|
|
14326
|
+
attemptN,
|
|
14327
|
+
...s.kind ? { kind: s.kind } : {},
|
|
14328
|
+
...s.title ? { title: s.title } : {},
|
|
14329
|
+
...s.contentType ? { contentType: s.contentType } : {}
|
|
14330
|
+
}
|
|
14331
|
+
};
|
|
14332
|
+
}
|
|
14333
|
+
this.refs.push(ref);
|
|
14334
|
+
return this;
|
|
14335
|
+
}
|
|
14336
|
+
/**
|
|
14122
14337
|
* Set the owning team (required by the wire schema).
|
|
14123
14338
|
*
|
|
14124
14339
|
* @param teamId - Team UUID.
|
|
@@ -14496,6 +14711,34 @@ var TaskResultReader = class {
|
|
|
14496
14711
|
role
|
|
14497
14712
|
};
|
|
14498
14713
|
}
|
|
14714
|
+
/**
|
|
14715
|
+
* Build a `TaskRef` that anchors a downstream task to this accepted output
|
|
14716
|
+
* and points at one persistent task artifact by CID.
|
|
14717
|
+
*
|
|
14718
|
+
* @param filter - Artifact object or a filter resolved against output artifacts.
|
|
14719
|
+
* @param role - The role this artifact plays in the downstream task.
|
|
14720
|
+
* @returns A `TaskRef` with `artifact.cid` populated.
|
|
14721
|
+
* @throws {TaskResultError} if no matching artifact has a CID.
|
|
14722
|
+
*/
|
|
14723
|
+
artifactRef(filter, role) {
|
|
14724
|
+
const artifact = typeof filter === "object" && "cid" in filter && "kind" in filter ? filter : this.artifact(filter);
|
|
14725
|
+
if (!artifact?.cid) throw new TaskResultError([{
|
|
14726
|
+
field: "artifacts/cid",
|
|
14727
|
+
message: "no matching artifact with a cid"
|
|
14728
|
+
}]);
|
|
14729
|
+
return {
|
|
14730
|
+
taskId: this.taskId,
|
|
14731
|
+
outputCid: this.outputCid,
|
|
14732
|
+
role,
|
|
14733
|
+
artifact: {
|
|
14734
|
+
cid: artifact.cid,
|
|
14735
|
+
attemptN: this.accepted.attemptN,
|
|
14736
|
+
kind: artifact.kind,
|
|
14737
|
+
title: artifact.title,
|
|
14738
|
+
...artifact.contentType ? { contentType: artifact.contentType } : {}
|
|
14739
|
+
}
|
|
14740
|
+
};
|
|
14741
|
+
}
|
|
14499
14742
|
};
|
|
14500
14743
|
/**
|
|
14501
14744
|
* Validate and construct a {@link TaskResultReader} from a task and its
|
|
@@ -14520,6 +14763,63 @@ function createTasksNamespace(context) {
|
|
|
14520
14763
|
auth
|
|
14521
14764
|
}));
|
|
14522
14765
|
},
|
|
14766
|
+
artifacts: {
|
|
14767
|
+
async upload(path, body, query, options) {
|
|
14768
|
+
return unwrapResult(await uploadTaskArtifact({
|
|
14769
|
+
auth,
|
|
14770
|
+
body,
|
|
14771
|
+
client,
|
|
14772
|
+
duplex: "half",
|
|
14773
|
+
headers: {
|
|
14774
|
+
...requiredTeamHeaders(options),
|
|
14775
|
+
"content-type": "application/octet-stream"
|
|
14776
|
+
},
|
|
14777
|
+
path,
|
|
14778
|
+
query
|
|
14779
|
+
}));
|
|
14780
|
+
},
|
|
14781
|
+
async list(taskId, options, query) {
|
|
14782
|
+
return unwrapResult(await listTaskArtifacts({
|
|
14783
|
+
client,
|
|
14784
|
+
auth,
|
|
14785
|
+
headers: requiredTeamHeaders(options),
|
|
14786
|
+
path: { taskId },
|
|
14787
|
+
query
|
|
14788
|
+
})).artifacts;
|
|
14789
|
+
},
|
|
14790
|
+
async listPage(taskId, query, options) {
|
|
14791
|
+
return unwrapResult(await listTaskArtifacts({
|
|
14792
|
+
client,
|
|
14793
|
+
auth,
|
|
14794
|
+
headers: requiredTeamHeaders(options),
|
|
14795
|
+
path: { taskId },
|
|
14796
|
+
query
|
|
14797
|
+
}));
|
|
14798
|
+
},
|
|
14799
|
+
async download(path, options) {
|
|
14800
|
+
const result = await client.request({
|
|
14801
|
+
auth,
|
|
14802
|
+
headers: requiredTeamHeaders(options),
|
|
14803
|
+
method: "GET",
|
|
14804
|
+
parseAs: "stream",
|
|
14805
|
+
path,
|
|
14806
|
+
security: [{
|
|
14807
|
+
scheme: "bearer",
|
|
14808
|
+
type: "http"
|
|
14809
|
+
}],
|
|
14810
|
+
url: "/tasks/{taskId}/attempts/{attemptN}/artifacts/{cid}/content"
|
|
14811
|
+
});
|
|
14812
|
+
const normalizedStream = normalizeDownloadStream(unwrapResult(result));
|
|
14813
|
+
if (normalizedStream) return {
|
|
14814
|
+
artifactId: header(result.response, "x-moltnet-task-artifact-id"),
|
|
14815
|
+
cid: header(result.response, "x-moltnet-task-artifact-cid"),
|
|
14816
|
+
contentEncoding: header(result.response, "x-moltnet-task-artifact-content-encoding"),
|
|
14817
|
+
contentType: header(result.response, "x-moltnet-task-artifact-content-type"),
|
|
14818
|
+
stream: normalizedStream
|
|
14819
|
+
};
|
|
14820
|
+
throw new MoltNetError("Unexpected task artifact download response stream", { code: "INVALID_RESPONSE" });
|
|
14821
|
+
}
|
|
14822
|
+
},
|
|
14523
14823
|
async list(query, options) {
|
|
14524
14824
|
return unwrapResult(await listTasks({
|
|
14525
14825
|
client,
|
|
@@ -14688,6 +14988,33 @@ function createTasksNamespace(context) {
|
|
|
14688
14988
|
}
|
|
14689
14989
|
};
|
|
14690
14990
|
}
|
|
14991
|
+
function header(response, name) {
|
|
14992
|
+
const value = response?.headers.get(name) ?? null;
|
|
14993
|
+
return value === "" ? null : value;
|
|
14994
|
+
}
|
|
14995
|
+
function normalizeDownloadStream(stream) {
|
|
14996
|
+
if (isAsyncIterable(stream)) return stream;
|
|
14997
|
+
if (isReadableStream(stream)) return readableStreamToAsyncIterable(stream);
|
|
14998
|
+
return null;
|
|
14999
|
+
}
|
|
15000
|
+
function isAsyncIterable(value) {
|
|
15001
|
+
return typeof value === "object" && value !== null && Symbol.asyncIterator in value && typeof value[Symbol.asyncIterator] === "function";
|
|
15002
|
+
}
|
|
15003
|
+
function isReadableStream(value) {
|
|
15004
|
+
return typeof value === "object" && value !== null && "getReader" in value && typeof value.getReader === "function";
|
|
15005
|
+
}
|
|
15006
|
+
async function* readableStreamToAsyncIterable(stream) {
|
|
15007
|
+
const reader = stream.getReader();
|
|
15008
|
+
try {
|
|
15009
|
+
while (true) {
|
|
15010
|
+
const result = await reader.read();
|
|
15011
|
+
if (result.done) return;
|
|
15012
|
+
yield result.value;
|
|
15013
|
+
}
|
|
15014
|
+
} finally {
|
|
15015
|
+
reader.releaseLock();
|
|
15016
|
+
}
|
|
15017
|
+
}
|
|
14691
15018
|
//#endregion
|
|
14692
15019
|
//#region ../sdk/src/namespaces/teams.ts
|
|
14693
15020
|
function createTeamsNamespace(context) {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('moltnet-task-artifact-download', {
|
|
3
|
+
category: 'moltnet',
|
|
4
|
+
color: '#00d4c8',
|
|
5
|
+
paletteLabel: 'task artifact: download',
|
|
6
|
+
defaults: {
|
|
7
|
+
name: { value: '' },
|
|
8
|
+
agent: { value: '', type: 'moltnet-agent', required: true },
|
|
9
|
+
taskId: { value: '' },
|
|
10
|
+
teamId: { value: '' },
|
|
11
|
+
allowMsgTeamOverride: { value: false },
|
|
12
|
+
attemptN: { value: '', validate: RED.validators.number() },
|
|
13
|
+
maxBytes: { value: 26214400, validate: RED.validators.number() },
|
|
14
|
+
cid: { value: '' },
|
|
15
|
+
},
|
|
16
|
+
inputs: 1,
|
|
17
|
+
outputs: 1,
|
|
18
|
+
icon: 'font-awesome/fa-download',
|
|
19
|
+
label: function () {
|
|
20
|
+
return this.name || 'task artifact: download';
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<script type="text/html" data-template-name="moltnet-task-artifact-download">
|
|
26
|
+
<div class="form-row">
|
|
27
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
28
|
+
<input type="text" id="node-input-name" />
|
|
29
|
+
</div>
|
|
30
|
+
<div class="form-row">
|
|
31
|
+
<label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
|
|
32
|
+
<input type="text" id="node-input-agent" />
|
|
33
|
+
</div>
|
|
34
|
+
<div class="form-row">
|
|
35
|
+
<label for="node-input-taskId"><i class="fa fa-hashtag"></i> Task ID</label>
|
|
36
|
+
<input
|
|
37
|
+
type="text"
|
|
38
|
+
id="node-input-taskId"
|
|
39
|
+
placeholder="msg.payload.taskId"
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="form-row">
|
|
43
|
+
<label for="node-input-teamId"><i class="fa fa-users"></i> Team ID</label>
|
|
44
|
+
<input type="text" id="node-input-teamId" placeholder="agent teamId" />
|
|
45
|
+
</div>
|
|
46
|
+
<div class="form-row">
|
|
47
|
+
<label for="node-input-allowMsgTeamOverride"
|
|
48
|
+
><i class="fa fa-random"></i> Team override</label
|
|
49
|
+
>
|
|
50
|
+
<input
|
|
51
|
+
type="checkbox"
|
|
52
|
+
id="node-input-allowMsgTeamOverride"
|
|
53
|
+
style="display: inline-block; width: auto; vertical-align: top"
|
|
54
|
+
/>
|
|
55
|
+
<span>Allow <code>msg.teamId</code></span>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="form-row">
|
|
58
|
+
<label for="node-input-attemptN"
|
|
59
|
+
><i class="fa fa-history"></i> Attempt</label
|
|
60
|
+
>
|
|
61
|
+
<input type="number" id="node-input-attemptN" placeholder="msg.attemptN" />
|
|
62
|
+
</div>
|
|
63
|
+
<div class="form-row">
|
|
64
|
+
<label for="node-input-cid"><i class="fa fa-fingerprint"></i> CID</label>
|
|
65
|
+
<input type="text" id="node-input-cid" placeholder="msg.payload.cid" />
|
|
66
|
+
</div>
|
|
67
|
+
<div class="form-row">
|
|
68
|
+
<label for="node-input-maxBytes"><i class="fa fa-database"></i> Max</label>
|
|
69
|
+
<input type="number" id="node-input-maxBytes" />
|
|
70
|
+
</div>
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<script type="text/html" data-help-name="moltnet-task-artifact-download">
|
|
74
|
+
<p>
|
|
75
|
+
Downloads a task artifact by task id, attempt number, and CID. CID is taken
|
|
76
|
+
from <code>msg.cid</code>, <code>msg.payload.cid</code>,
|
|
77
|
+
<code>msg.payload.artifact.cid</code>, or the configured CID. Team ID is
|
|
78
|
+
taken from this node or the configured agent unless Team override is
|
|
79
|
+
enabled.
|
|
80
|
+
</p>
|
|
81
|
+
<p>
|
|
82
|
+
Emits the artifact bytes as a Buffer on <code>msg.payload</code>. Download
|
|
83
|
+
metadata is placed on <code>msg.artifact</code>. The local byte limit
|
|
84
|
+
defaults to 25 MiB.
|
|
85
|
+
</p>
|
|
86
|
+
</script>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { a as nonEmpty, t as bool } from "./query-utils.js";
|
|
2
|
+
import { a as requireAttemptContext, n as payloadRecord, r as recordField, s as resolveMaxBytes, t as collectArtifactBody } from "./task-artifact-utils.js";
|
|
3
|
+
//#region src/nodes/task-artifact-download.ts
|
|
4
|
+
var init = (RED) => {
|
|
5
|
+
function TaskArtifactDownloadNode(def) {
|
|
6
|
+
RED.nodes.createNode(this, def);
|
|
7
|
+
const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
|
|
8
|
+
this.on("input", (msg, send, done) => {
|
|
9
|
+
const run = async () => {
|
|
10
|
+
try {
|
|
11
|
+
if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("task-artifact-download: no moltnet-agent configured");
|
|
12
|
+
const { taskId, teamId, attemptN } = requireAttemptContext("task-artifact-download", msg, def.taskId, def.teamId, def.attemptN, agentNode, bool(def.allowMsgTeamOverride) ?? false);
|
|
13
|
+
const cid = resolveCid(msg, def.cid);
|
|
14
|
+
if (!cid) throw new Error("task-artifact-download: cid is required");
|
|
15
|
+
this.status({
|
|
16
|
+
fill: "blue",
|
|
17
|
+
shape: "dot",
|
|
18
|
+
text: "downloading…"
|
|
19
|
+
});
|
|
20
|
+
const result = await (await agentNode.getAgent()).tasks.artifacts.download({
|
|
21
|
+
taskId,
|
|
22
|
+
attemptN,
|
|
23
|
+
cid
|
|
24
|
+
}, { teamId });
|
|
25
|
+
const body = await collectArtifactBody(result, resolveMaxBytes(def.maxBytes));
|
|
26
|
+
const out = RED.util.cloneMessage({
|
|
27
|
+
...msg,
|
|
28
|
+
payload: void 0
|
|
29
|
+
});
|
|
30
|
+
out.payload = body;
|
|
31
|
+
out.taskId = taskId;
|
|
32
|
+
out.artifact = {
|
|
33
|
+
taskId,
|
|
34
|
+
teamId,
|
|
35
|
+
attemptN,
|
|
36
|
+
cid,
|
|
37
|
+
...metadataFromResult(result)
|
|
38
|
+
};
|
|
39
|
+
this.status({
|
|
40
|
+
fill: "green",
|
|
41
|
+
shape: "dot",
|
|
42
|
+
text: `${body.byteLength} byte(s)`
|
|
43
|
+
});
|
|
44
|
+
send(out);
|
|
45
|
+
done();
|
|
46
|
+
} catch (err) {
|
|
47
|
+
this.status({
|
|
48
|
+
fill: "red",
|
|
49
|
+
shape: "ring",
|
|
50
|
+
text: "error"
|
|
51
|
+
});
|
|
52
|
+
done(err instanceof Error ? err : new Error(String(err)));
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
run();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
RED.nodes.registerType("moltnet-task-artifact-download", TaskArtifactDownloadNode);
|
|
59
|
+
};
|
|
60
|
+
function resolveCid(msg, configured) {
|
|
61
|
+
if (typeof msg.cid === "string" && msg.cid) return msg.cid;
|
|
62
|
+
const payload = payloadRecord(msg);
|
|
63
|
+
return nonEmpty(payload.cid) ?? nonEmpty(recordField(payload.artifact, "cid")) ?? nonEmpty(configured);
|
|
64
|
+
}
|
|
65
|
+
function metadataFromResult(result) {
|
|
66
|
+
if (!result || typeof result !== "object") return {};
|
|
67
|
+
const record = result;
|
|
68
|
+
return {
|
|
69
|
+
artifactId: record.artifactId,
|
|
70
|
+
contentType: record.contentType,
|
|
71
|
+
contentEncoding: record.contentEncoding
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { init as default };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('moltnet-task-artifact-upload', {
|
|
3
|
+
category: 'moltnet',
|
|
4
|
+
color: '#00d4c8',
|
|
5
|
+
paletteLabel: 'task artifact: upload',
|
|
6
|
+
defaults: {
|
|
7
|
+
name: { value: '' },
|
|
8
|
+
agent: { value: '', type: 'moltnet-agent', required: true },
|
|
9
|
+
taskId: { value: '' },
|
|
10
|
+
teamId: { value: '' },
|
|
11
|
+
allowMsgTeamOverride: { value: false },
|
|
12
|
+
attemptN: { value: '', validate: RED.validators.number() },
|
|
13
|
+
maxBytes: { value: 26214400, validate: RED.validators.number() },
|
|
14
|
+
kind: { value: 'output' },
|
|
15
|
+
title: { value: '' },
|
|
16
|
+
contentType: { value: 'application/octet-stream' },
|
|
17
|
+
contentEncoding: { value: '' },
|
|
18
|
+
},
|
|
19
|
+
inputs: 1,
|
|
20
|
+
outputs: 1,
|
|
21
|
+
icon: 'font-awesome/fa-upload',
|
|
22
|
+
label: function () {
|
|
23
|
+
return this.name || 'task artifact: upload';
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<script type="text/html" data-template-name="moltnet-task-artifact-upload">
|
|
29
|
+
<div class="form-row">
|
|
30
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
31
|
+
<input type="text" id="node-input-name" />
|
|
32
|
+
</div>
|
|
33
|
+
<div class="form-row">
|
|
34
|
+
<label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
|
|
35
|
+
<input type="text" id="node-input-agent" />
|
|
36
|
+
</div>
|
|
37
|
+
<div class="form-row">
|
|
38
|
+
<label for="node-input-taskId"><i class="fa fa-hashtag"></i> Task ID</label>
|
|
39
|
+
<input
|
|
40
|
+
type="text"
|
|
41
|
+
id="node-input-taskId"
|
|
42
|
+
placeholder="msg.payload.taskId"
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="form-row">
|
|
46
|
+
<label for="node-input-teamId"><i class="fa fa-users"></i> Team ID</label>
|
|
47
|
+
<input type="text" id="node-input-teamId" placeholder="agent teamId" />
|
|
48
|
+
</div>
|
|
49
|
+
<div class="form-row">
|
|
50
|
+
<label for="node-input-allowMsgTeamOverride"
|
|
51
|
+
><i class="fa fa-random"></i> Team override</label
|
|
52
|
+
>
|
|
53
|
+
<input
|
|
54
|
+
type="checkbox"
|
|
55
|
+
id="node-input-allowMsgTeamOverride"
|
|
56
|
+
style="display: inline-block; width: auto; vertical-align: top"
|
|
57
|
+
/>
|
|
58
|
+
<span>Allow <code>msg.teamId</code></span>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="form-row">
|
|
61
|
+
<label for="node-input-attemptN"
|
|
62
|
+
><i class="fa fa-history"></i> Attempt</label
|
|
63
|
+
>
|
|
64
|
+
<input type="number" id="node-input-attemptN" placeholder="msg.attemptN" />
|
|
65
|
+
</div>
|
|
66
|
+
<div class="form-row">
|
|
67
|
+
<label for="node-input-kind"><i class="fa fa-archive"></i> Kind</label>
|
|
68
|
+
<select id="node-input-kind">
|
|
69
|
+
<option value="output">Output</option>
|
|
70
|
+
<option value="input">Input</option>
|
|
71
|
+
<option value="log">Log</option>
|
|
72
|
+
<option value="other">Other</option>
|
|
73
|
+
</select>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="form-row">
|
|
76
|
+
<label for="node-input-title"><i class="fa fa-font"></i> Title</label>
|
|
77
|
+
<input type="text" id="node-input-title" placeholder="summary.md" />
|
|
78
|
+
</div>
|
|
79
|
+
<div class="form-row">
|
|
80
|
+
<label for="node-input-maxBytes"><i class="fa fa-database"></i> Max</label>
|
|
81
|
+
<input type="number" id="node-input-maxBytes" />
|
|
82
|
+
</div>
|
|
83
|
+
<div class="form-row">
|
|
84
|
+
<label for="node-input-contentType"
|
|
85
|
+
><i class="fa fa-file-o"></i> Type</label
|
|
86
|
+
>
|
|
87
|
+
<input
|
|
88
|
+
type="text"
|
|
89
|
+
id="node-input-contentType"
|
|
90
|
+
placeholder="text/markdown"
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="form-row">
|
|
94
|
+
<label for="node-input-contentEncoding"
|
|
95
|
+
><i class="fa fa-compress"></i> Encoding</label
|
|
96
|
+
>
|
|
97
|
+
<input type="text" id="node-input-contentEncoding" placeholder="gzip" />
|
|
98
|
+
</div>
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<script type="text/html" data-help-name="moltnet-task-artifact-upload">
|
|
102
|
+
<p>
|
|
103
|
+
Uploads bytes as an artifact for a task attempt. Task id, attempt, kind,
|
|
104
|
+
title, content type, and content encoding can be supplied by the node fields
|
|
105
|
+
or matching fields on <code>msg.payload</code>. Team ID is taken from this
|
|
106
|
+
node or the configured agent unless Team override is enabled.
|
|
107
|
+
</p>
|
|
108
|
+
<p>
|
|
109
|
+
The body is read from <code>msg.payload</code> when it is a string, Buffer,
|
|
110
|
+
Uint8Array, or ArrayBuffer. Object payloads may provide
|
|
111
|
+
<code>content</code>, <code>body</code>, or base64
|
|
112
|
+
<code>contentBase64</code>. The local byte limit defaults to 25 MiB.
|
|
113
|
+
</p>
|
|
114
|
+
<p>
|
|
115
|
+
Emits the artifact metadata on <code>msg.payload</code> and
|
|
116
|
+
<code>msg.artifact</code>.
|
|
117
|
+
</p>
|
|
118
|
+
</script>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { t as bool } from "./query-utils.js";
|
|
2
|
+
import { a as requireAttemptContext, c as resolveUploadBody, o as resolveField, s as resolveMaxBytes } from "./task-artifact-utils.js";
|
|
3
|
+
//#region src/nodes/task-artifact-upload.ts
|
|
4
|
+
var init = (RED) => {
|
|
5
|
+
function TaskArtifactUploadNode(def) {
|
|
6
|
+
RED.nodes.createNode(this, def);
|
|
7
|
+
const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
|
|
8
|
+
this.on("input", (msg, send, done) => {
|
|
9
|
+
const run = async () => {
|
|
10
|
+
try {
|
|
11
|
+
if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("task-artifact-upload: no moltnet-agent configured");
|
|
12
|
+
const { taskId, teamId, attemptN } = requireAttemptContext("task-artifact-upload", msg, def.taskId, def.teamId, def.attemptN, agentNode, bool(def.allowMsgTeamOverride) ?? false);
|
|
13
|
+
const body = resolveUploadBody(msg, resolveMaxBytes(def.maxBytes));
|
|
14
|
+
const query = buildUploadQuery(def, msg);
|
|
15
|
+
this.status({
|
|
16
|
+
fill: "blue",
|
|
17
|
+
shape: "dot",
|
|
18
|
+
text: "uploading…"
|
|
19
|
+
});
|
|
20
|
+
const artifact = await (await agentNode.getAgent()).tasks.artifacts.upload({
|
|
21
|
+
taskId,
|
|
22
|
+
attemptN
|
|
23
|
+
}, body, query, { teamId });
|
|
24
|
+
const out = RED.util.cloneMessage({
|
|
25
|
+
...msg,
|
|
26
|
+
payload: void 0
|
|
27
|
+
});
|
|
28
|
+
out.payload = artifact;
|
|
29
|
+
out.taskId = taskId;
|
|
30
|
+
out.artifact = artifact;
|
|
31
|
+
this.status({
|
|
32
|
+
fill: "green",
|
|
33
|
+
shape: "dot",
|
|
34
|
+
text: artifact.cid
|
|
35
|
+
});
|
|
36
|
+
send(out);
|
|
37
|
+
done();
|
|
38
|
+
} catch (err) {
|
|
39
|
+
this.status({
|
|
40
|
+
fill: "red",
|
|
41
|
+
shape: "ring",
|
|
42
|
+
text: "error"
|
|
43
|
+
});
|
|
44
|
+
done(err instanceof Error ? err : new Error(String(err)));
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
run();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
RED.nodes.registerType("moltnet-task-artifact-upload", TaskArtifactUploadNode);
|
|
51
|
+
};
|
|
52
|
+
function buildUploadQuery(def, msg) {
|
|
53
|
+
return {
|
|
54
|
+
kind: resolveField(msg, "kind", def.kind) ?? "output",
|
|
55
|
+
title: resolveField(msg, "title", def.title) ?? "artifact",
|
|
56
|
+
contentType: resolveField(msg, "contentType", def.contentType) ?? "application/octet-stream",
|
|
57
|
+
contentEncoding: resolveField(msg, "contentEncoding", def.contentEncoding)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { init as default };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { a as nonEmpty, c as positiveInt } from "./query-utils.js";
|
|
2
|
+
import { Readable } from "node:stream";
|
|
3
|
+
import { Buffer } from "node:buffer";
|
|
4
|
+
function payloadRecord(msg) {
|
|
5
|
+
if (!msg.payload || typeof msg.payload !== "object") return {};
|
|
6
|
+
if (Buffer.isBuffer(msg.payload)) return {};
|
|
7
|
+
return msg.payload;
|
|
8
|
+
}
|
|
9
|
+
function resolveTaskId(msg, configured) {
|
|
10
|
+
if (typeof msg.taskId === "string" && msg.taskId) return msg.taskId;
|
|
11
|
+
const payload = payloadRecord(msg);
|
|
12
|
+
return nonEmpty(payload.taskId) ?? nonEmpty(payload.id) ?? nonEmpty(recordField(payload.task, "id")) ?? nonEmpty(configured);
|
|
13
|
+
}
|
|
14
|
+
function resolveTeamId(msg, configured, agentNode, allowMsgTeamOverride) {
|
|
15
|
+
if (!allowMsgTeamOverride) return nonEmpty(configured) ?? agentNode.teamId;
|
|
16
|
+
if (typeof msg.teamId === "string" && msg.teamId) return msg.teamId;
|
|
17
|
+
return nonEmpty(payloadRecord(msg).teamId) ?? nonEmpty(configured) ?? agentNode.teamId;
|
|
18
|
+
}
|
|
19
|
+
function resolveAttemptN(msg, configured) {
|
|
20
|
+
const payload = payloadRecord(msg);
|
|
21
|
+
return positiveInt(msg.attemptN) ?? positiveInt(payload.attemptN) ?? positiveInt(recordField(payload.attempt, "attemptN")) ?? positiveInt(recordField(payload.artifact, "attemptN")) ?? positiveInt(configured);
|
|
22
|
+
}
|
|
23
|
+
function requireArtifactContext(nodeName, msg, configuredTaskId, configuredTeamId, agentNode, allowMsgTeamOverride) {
|
|
24
|
+
const taskId = resolveTaskId(msg, configuredTaskId);
|
|
25
|
+
if (!taskId) throw new Error(`${nodeName}: taskId is required`);
|
|
26
|
+
const teamId = resolveTeamId(msg, configuredTeamId, agentNode, allowMsgTeamOverride);
|
|
27
|
+
if (!teamId) throw new Error(`${nodeName}: teamId is required`);
|
|
28
|
+
return {
|
|
29
|
+
taskId,
|
|
30
|
+
teamId
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function requireAttemptContext(nodeName, msg, configuredTaskId, configuredTeamId, configuredAttemptN, agentNode, allowMsgTeamOverride) {
|
|
34
|
+
const context = requireArtifactContext(nodeName, msg, configuredTaskId, configuredTeamId, agentNode, allowMsgTeamOverride);
|
|
35
|
+
const attemptN = resolveAttemptN(msg, configuredAttemptN);
|
|
36
|
+
if (!attemptN) throw new Error(`${nodeName}: attemptN is required`);
|
|
37
|
+
return {
|
|
38
|
+
...context,
|
|
39
|
+
attemptN
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function resolveField(msg, name, configured) {
|
|
43
|
+
return nonEmpty(payloadRecord(msg)[name]) ?? nonEmpty(configured);
|
|
44
|
+
}
|
|
45
|
+
function resolveMaxBytes(configured) {
|
|
46
|
+
return positiveInt(configured) ?? 26214400;
|
|
47
|
+
}
|
|
48
|
+
function resolveUploadBody(msg, maxBytes) {
|
|
49
|
+
const payload = msg.payload;
|
|
50
|
+
if (Buffer.isBuffer(payload)) return enforceMaxBytes(payload, maxBytes);
|
|
51
|
+
if (payload instanceof Uint8Array) return enforceMaxBytes(payload, maxBytes);
|
|
52
|
+
if (payload instanceof ArrayBuffer) {
|
|
53
|
+
if (payload.byteLength > maxBytes) throw tooLarge("upload", maxBytes);
|
|
54
|
+
return new Uint8Array(payload);
|
|
55
|
+
}
|
|
56
|
+
if (typeof payload === "string") {
|
|
57
|
+
if (Buffer.byteLength(payload) > maxBytes) throw tooLarge("upload", maxBytes);
|
|
58
|
+
return new TextEncoder().encode(payload);
|
|
59
|
+
}
|
|
60
|
+
const record = payloadRecord(msg);
|
|
61
|
+
if (typeof record.contentBase64 === "string") {
|
|
62
|
+
const normalized = record.contentBase64.replace(/\s/g, "");
|
|
63
|
+
if (decodedBase64Length(normalized) > maxBytes) throw tooLarge("upload", maxBytes);
|
|
64
|
+
return Buffer.from(normalized, "base64");
|
|
65
|
+
}
|
|
66
|
+
const content = record.content ?? record.body;
|
|
67
|
+
if (Buffer.isBuffer(content)) return enforceMaxBytes(content, maxBytes);
|
|
68
|
+
if (content instanceof Uint8Array) return enforceMaxBytes(content, maxBytes);
|
|
69
|
+
if (content instanceof ArrayBuffer) {
|
|
70
|
+
if (content.byteLength > maxBytes) throw tooLarge("upload", maxBytes);
|
|
71
|
+
return new Uint8Array(content);
|
|
72
|
+
}
|
|
73
|
+
if (typeof content === "string") {
|
|
74
|
+
if (Buffer.byteLength(content) > maxBytes) throw tooLarge("upload", maxBytes);
|
|
75
|
+
return new TextEncoder().encode(content);
|
|
76
|
+
}
|
|
77
|
+
throw new Error("task-artifact-upload: payload content is required");
|
|
78
|
+
}
|
|
79
|
+
async function collectArtifactBody(value, maxBytes) {
|
|
80
|
+
const source = value && typeof value === "object" && "stream" in value ? value.stream : value;
|
|
81
|
+
if (Buffer.isBuffer(source)) {
|
|
82
|
+
if (source.byteLength > maxBytes) throw tooLarge("download", maxBytes);
|
|
83
|
+
return source;
|
|
84
|
+
}
|
|
85
|
+
if (source instanceof Uint8Array) {
|
|
86
|
+
if (source.byteLength > maxBytes) throw tooLarge("download", maxBytes);
|
|
87
|
+
return Buffer.from(source);
|
|
88
|
+
}
|
|
89
|
+
if (source instanceof ArrayBuffer) {
|
|
90
|
+
if (source.byteLength > maxBytes) throw tooLarge("download", maxBytes);
|
|
91
|
+
return Buffer.from(source);
|
|
92
|
+
}
|
|
93
|
+
if (typeof source === "string") {
|
|
94
|
+
if (Buffer.byteLength(source) > maxBytes) throw tooLarge("download", maxBytes);
|
|
95
|
+
return Buffer.from(source);
|
|
96
|
+
}
|
|
97
|
+
if (source instanceof Readable) {
|
|
98
|
+
const chunks = [];
|
|
99
|
+
let bytes = 0;
|
|
100
|
+
for await (const chunk of source) bytes = pushChunk(chunks, chunk, bytes, maxBytes);
|
|
101
|
+
return Buffer.concat(chunks);
|
|
102
|
+
}
|
|
103
|
+
if (source && typeof source === "object" && Symbol.asyncIterator in source) {
|
|
104
|
+
const chunks = [];
|
|
105
|
+
let bytes = 0;
|
|
106
|
+
for await (const chunk of source) bytes = pushChunk(chunks, chunk, bytes, maxBytes);
|
|
107
|
+
return Buffer.concat(chunks);
|
|
108
|
+
}
|
|
109
|
+
if (source && typeof source === "object" && "arrayBuffer" in source) {
|
|
110
|
+
const size = source.size;
|
|
111
|
+
if (typeof size === "number" && size > maxBytes) throw tooLarge("download", maxBytes);
|
|
112
|
+
const arrayBuffer = await source.arrayBuffer();
|
|
113
|
+
if (arrayBuffer.byteLength > maxBytes) throw tooLarge("download", maxBytes);
|
|
114
|
+
return Buffer.from(arrayBuffer);
|
|
115
|
+
}
|
|
116
|
+
throw new Error("task-artifact-download: unsupported artifact body");
|
|
117
|
+
}
|
|
118
|
+
function toBuffer(value) {
|
|
119
|
+
if (Buffer.isBuffer(value)) return value;
|
|
120
|
+
if (value instanceof Uint8Array) return Buffer.from(value);
|
|
121
|
+
if (value instanceof ArrayBuffer) return Buffer.from(value);
|
|
122
|
+
if (typeof value === "string") return Buffer.from(value);
|
|
123
|
+
return Buffer.from(String(value));
|
|
124
|
+
}
|
|
125
|
+
function recordField(value, key) {
|
|
126
|
+
if (!value || typeof value !== "object") return void 0;
|
|
127
|
+
return value[key];
|
|
128
|
+
}
|
|
129
|
+
function enforceMaxBytes(value, maxBytes) {
|
|
130
|
+
if (value.byteLength > maxBytes) throw tooLarge("upload", maxBytes);
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
function pushChunk(chunks, chunk, bytes, maxBytes) {
|
|
134
|
+
const next = toBuffer(chunk);
|
|
135
|
+
const total = bytes + next.byteLength;
|
|
136
|
+
if (total > maxBytes) throw tooLarge("download", maxBytes);
|
|
137
|
+
chunks.push(next);
|
|
138
|
+
return total;
|
|
139
|
+
}
|
|
140
|
+
function decodedBase64Length(value) {
|
|
141
|
+
if (!value) return 0;
|
|
142
|
+
const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
|
|
143
|
+
return Math.floor(value.length * 3 / 4) - padding;
|
|
144
|
+
}
|
|
145
|
+
function tooLarge(operation, maxBytes) {
|
|
146
|
+
return /* @__PURE__ */ new Error(`task-artifact-${operation}: artifact body exceeds ${maxBytes} bytes`);
|
|
147
|
+
}
|
|
148
|
+
//#endregion
|
|
149
|
+
export { requireAttemptContext as a, resolveUploadBody as c, requireArtifactContext as i, payloadRecord as n, resolveField as o, recordField as r, resolveMaxBytes as s, collectArtifactBody as t };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('moltnet-task-artifacts-list', {
|
|
3
|
+
category: 'moltnet',
|
|
4
|
+
color: '#00d4c8',
|
|
5
|
+
paletteLabel: 'task artifacts: list',
|
|
6
|
+
defaults: {
|
|
7
|
+
name: { value: '' },
|
|
8
|
+
agent: { value: '', type: 'moltnet-agent', required: true },
|
|
9
|
+
taskId: { value: '' },
|
|
10
|
+
teamId: { value: '' },
|
|
11
|
+
allowMsgTeamOverride: { value: false },
|
|
12
|
+
limit: { value: 20, validate: RED.validators.number() },
|
|
13
|
+
cursor: { value: '' },
|
|
14
|
+
},
|
|
15
|
+
inputs: 1,
|
|
16
|
+
outputs: 1,
|
|
17
|
+
icon: 'font-awesome/fa-list',
|
|
18
|
+
label: function () {
|
|
19
|
+
return this.name || 'task artifacts: list';
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<script type="text/html" data-template-name="moltnet-task-artifacts-list">
|
|
25
|
+
<div class="form-row">
|
|
26
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
27
|
+
<input type="text" id="node-input-name" />
|
|
28
|
+
</div>
|
|
29
|
+
<div class="form-row">
|
|
30
|
+
<label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
|
|
31
|
+
<input type="text" id="node-input-agent" />
|
|
32
|
+
</div>
|
|
33
|
+
<div class="form-row">
|
|
34
|
+
<label for="node-input-taskId"><i class="fa fa-hashtag"></i> Task ID</label>
|
|
35
|
+
<input
|
|
36
|
+
type="text"
|
|
37
|
+
id="node-input-taskId"
|
|
38
|
+
placeholder="msg.payload.taskId"
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="form-row">
|
|
42
|
+
<label for="node-input-teamId"><i class="fa fa-users"></i> Team ID</label>
|
|
43
|
+
<input type="text" id="node-input-teamId" placeholder="agent teamId" />
|
|
44
|
+
</div>
|
|
45
|
+
<div class="form-row">
|
|
46
|
+
<label for="node-input-allowMsgTeamOverride"
|
|
47
|
+
><i class="fa fa-random"></i> Team override</label
|
|
48
|
+
>
|
|
49
|
+
<input
|
|
50
|
+
type="checkbox"
|
|
51
|
+
id="node-input-allowMsgTeamOverride"
|
|
52
|
+
style="display: inline-block; width: auto; vertical-align: top"
|
|
53
|
+
/>
|
|
54
|
+
<span>Allow <code>msg.teamId</code></span>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="form-row">
|
|
57
|
+
<label for="node-input-limit"><i class="fa fa-hashtag"></i> Limit</label>
|
|
58
|
+
<input type="number" id="node-input-limit" />
|
|
59
|
+
</div>
|
|
60
|
+
<div class="form-row">
|
|
61
|
+
<label for="node-input-cursor"><i class="fa fa-forward"></i> Cursor</label>
|
|
62
|
+
<input type="text" id="node-input-cursor" />
|
|
63
|
+
</div>
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<script type="text/html" data-help-name="moltnet-task-artifacts-list">
|
|
67
|
+
<p>
|
|
68
|
+
Lists artifacts for a MoltNet task. The task id is taken from
|
|
69
|
+
<code>msg.taskId</code>, <code>msg.payload.taskId</code>,
|
|
70
|
+
<code>msg.payload.id</code>, or the configured Task ID. Team ID is taken
|
|
71
|
+
from this node or the configured agent. Enable Team override to allow
|
|
72
|
+
<code>msg.teamId</code> or <code>msg.payload.teamId</code>.
|
|
73
|
+
</p>
|
|
74
|
+
<p>
|
|
75
|
+
Emits artifact rows on <code>msg.payload</code>. Pagination metadata, the
|
|
76
|
+
final query, and the full page are on <code>msg.artifacts</code>.
|
|
77
|
+
</p>
|
|
78
|
+
</script>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { a as nonEmpty, c as positiveInt, n as compact, t as bool } from "./query-utils.js";
|
|
2
|
+
import { i as requireArtifactContext, n as payloadRecord } from "./task-artifact-utils.js";
|
|
3
|
+
//#region src/nodes/task-artifacts-list.ts
|
|
4
|
+
var init = (RED) => {
|
|
5
|
+
function TaskArtifactsListNode(def) {
|
|
6
|
+
RED.nodes.createNode(this, def);
|
|
7
|
+
const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
|
|
8
|
+
this.on("input", (msg, send, done) => {
|
|
9
|
+
const run = async () => {
|
|
10
|
+
try {
|
|
11
|
+
if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("task-artifacts-list: no moltnet-agent configured");
|
|
12
|
+
const { taskId, teamId } = requireArtifactContext("task-artifacts-list", msg, def.taskId, def.teamId, agentNode, bool(def.allowMsgTeamOverride) ?? false);
|
|
13
|
+
this.status({
|
|
14
|
+
fill: "blue",
|
|
15
|
+
shape: "dot",
|
|
16
|
+
text: "loading…"
|
|
17
|
+
});
|
|
18
|
+
const agent = await agentNode.getAgent();
|
|
19
|
+
const query = buildQuery(def, msg);
|
|
20
|
+
const page = await agent.tasks.artifacts.listPage(taskId, query, { teamId });
|
|
21
|
+
const out = RED.util.cloneMessage(msg);
|
|
22
|
+
out.payload = page.artifacts;
|
|
23
|
+
out.taskId = taskId;
|
|
24
|
+
out.artifacts = {
|
|
25
|
+
taskId,
|
|
26
|
+
teamId,
|
|
27
|
+
query,
|
|
28
|
+
count: page.artifacts.length,
|
|
29
|
+
nextCursor: page.nextCursor,
|
|
30
|
+
page
|
|
31
|
+
};
|
|
32
|
+
this.status({
|
|
33
|
+
fill: "green",
|
|
34
|
+
shape: "dot",
|
|
35
|
+
text: `${page.artifacts.length} artifact(s)`
|
|
36
|
+
});
|
|
37
|
+
send(out);
|
|
38
|
+
done();
|
|
39
|
+
} catch (err) {
|
|
40
|
+
this.status({
|
|
41
|
+
fill: "red",
|
|
42
|
+
shape: "ring",
|
|
43
|
+
text: "error"
|
|
44
|
+
});
|
|
45
|
+
done(err instanceof Error ? err : new Error(String(err)));
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
run();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
RED.nodes.registerType("moltnet-task-artifacts-list", TaskArtifactsListNode);
|
|
52
|
+
};
|
|
53
|
+
function buildQuery(def, msg) {
|
|
54
|
+
const payload = payloadRecord(msg);
|
|
55
|
+
return compact({
|
|
56
|
+
limit: positiveInt(payload.limit) ?? positiveInt(def.limit),
|
|
57
|
+
cursor: nonEmpty(payload.cursor) ?? nonEmpty(def.cursor)
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { init as default };
|
|
@@ -19,16 +19,13 @@ var init = (RED) => {
|
|
|
19
19
|
} : def.artifactKind : void 0;
|
|
20
20
|
let artifactBody;
|
|
21
21
|
const artifact = filter ? reader.artifact(filter) : void 0;
|
|
22
|
-
if (filter && artifact && typeof artifact.body === "string")
|
|
23
|
-
artifactBody = JSON.parse(artifact.body);
|
|
24
|
-
} catch {
|
|
25
|
-
artifactBody = void 0;
|
|
26
|
-
}
|
|
22
|
+
if (filter && artifact && typeof artifact.body === "string") artifactBody = reader.artifactBody(filter);
|
|
27
23
|
const out = RED.util.cloneMessage(msg);
|
|
28
24
|
out.payload = reader.output;
|
|
29
25
|
out.result = {
|
|
30
26
|
summary: reader.summary,
|
|
31
27
|
outputRef: reader.outputRef(role),
|
|
28
|
+
artifactRef: artifact?.cid !== void 0 ? reader.artifactRef(artifact, role) : void 0,
|
|
32
29
|
artifact,
|
|
33
30
|
artifactBody,
|
|
34
31
|
accepted: reader.accepted,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/node-red-contrib-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Node-RED nodes for the MoltNet API",
|
|
6
6
|
"keywords": [
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
"moltnet-tasks-list": "dist/nodes/tasks-list.js",
|
|
28
28
|
"moltnet-task-get": "dist/nodes/task-get.js",
|
|
29
29
|
"moltnet-task-wait": "dist/nodes/task-wait.js",
|
|
30
|
+
"moltnet-task-artifacts-list": "dist/nodes/task-artifacts-list.js",
|
|
31
|
+
"moltnet-task-artifact-upload": "dist/nodes/task-artifact-upload.js",
|
|
32
|
+
"moltnet-task-artifact-download": "dist/nodes/task-artifact-download.js",
|
|
30
33
|
"moltnet-workflow-status": "dist/nodes/workflow-status.js",
|
|
31
34
|
"moltnet-task-builder": "dist/nodes/task-builder.js",
|
|
32
35
|
"moltnet-task-reader": "dist/nodes/task-reader.js",
|
|
@@ -38,7 +41,7 @@
|
|
|
38
41
|
},
|
|
39
42
|
"main": "dist/nodes/agent.js",
|
|
40
43
|
"dependencies": {
|
|
41
|
-
"@themoltnet/sdk": "0.
|
|
44
|
+
"@themoltnet/sdk": "0.115.0"
|
|
42
45
|
},
|
|
43
46
|
"devDependencies": {
|
|
44
47
|
"@types/node": "^22.19.0",
|