tuya-platform-cli 0.1.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.
@@ -0,0 +1,391 @@
1
+ import path from "node:path";
2
+ import { ensureDir, writeJson, writeText } from "../util.js";
3
+ import {
4
+ buildNetworkCapture,
5
+ createWorkflowViaPage,
6
+ getBrowserContext,
7
+ getWorkflowViaPage,
8
+ pageFetchJson,
9
+ readWorkflowSchema,
10
+ updateWorkflowViaPage,
11
+ } from "./shared.js";
12
+
13
+ function buildBranchWorkflowSchema({ region }) {
14
+ return {
15
+ editVersionNo: 1,
16
+ nodes: [
17
+ {
18
+ id: "100001",
19
+ type: "Start",
20
+ meta: { position: { x: 100, y: 320 } },
21
+ data: {
22
+ title: "Start",
23
+ nodeMeta: {
24
+ description: "支持中间过程的消息输出,支持流式和非流式两种方式",
25
+ title: "开始",
26
+ },
27
+ outputs: [
28
+ {
29
+ disableEdit: false,
30
+ id: "USER_TEXT",
31
+ name: "USER_TEXT",
32
+ required: true,
33
+ type: "string",
34
+ },
35
+ {
36
+ disableEdit: false,
37
+ id: "USER_IMAGE",
38
+ name: "USER_IMAGE",
39
+ required: false,
40
+ type: "image",
41
+ },
42
+ ],
43
+ version: 1,
44
+ },
45
+ },
46
+ {
47
+ id: "selectBranchAZ001",
48
+ type: "Select",
49
+ meta: { position: { x: 420, y: 320 } },
50
+ data: {
51
+ title: "选择器_双分支",
52
+ nodeMeta: {
53
+ title: "选择器",
54
+ description: "连接多个下游分支,若设定的条件成立则仅运行对应的分支,若均不成立则只运行“否则”分支",
55
+ },
56
+ exceptionSetting: {
57
+ timeout: 60,
58
+ retryTimes: 0,
59
+ strategy: 0,
60
+ portId: "",
61
+ },
62
+ inputs: [],
63
+ outputs: [],
64
+ nodeParam: {
65
+ selectConditions: {
66
+ 0: {
67
+ portId: "condition_branch_a_port",
68
+ conditionList: [
69
+ {
70
+ conditionId: "condition_branch_a",
71
+ prerequisites: {
72
+ id: "pre_branch_a",
73
+ name: "USER_TEXT",
74
+ type: "string",
75
+ isRef: 1,
76
+ constantValue: "",
77
+ },
78
+ postcondition: {
79
+ id: "post_branch_a",
80
+ name: "",
81
+ type: "string",
82
+ isRef: 0,
83
+ constantValue: "走A分支",
84
+ },
85
+ judge: "=",
86
+ },
87
+ ],
88
+ conditionRelation: "&&",
89
+ },
90
+ "-1": {
91
+ portId: "condition_branch_else_port",
92
+ },
93
+ },
94
+ },
95
+ },
96
+ },
97
+ {
98
+ id: "llmBranchAAZ001",
99
+ type: "LLM",
100
+ meta: { position: { x: 780, y: 180 } },
101
+ data: {
102
+ title: "分支A回答",
103
+ nodeMeta: {
104
+ description: "调用大语言模型,使用变量和提示词生成回复",
105
+ title: "大模型",
106
+ },
107
+ exceptionSetting: {
108
+ timeout: 60,
109
+ retryTimes: 0,
110
+ strategy: 0,
111
+ portId: "",
112
+ },
113
+ inputs: [
114
+ {
115
+ constantValue: "",
116
+ disableEdit: false,
117
+ id: "inputBranchA",
118
+ isRef: 1,
119
+ name: "input",
120
+ refId: "USER_TEXT",
121
+ refName: "USER_TEXT",
122
+ refNode: "100001",
123
+ type: "string",
124
+ },
125
+ ],
126
+ nodeParam: {
127
+ model: "160",
128
+ outputType: 1,
129
+ skills: [],
130
+ systemPrompt: "你是分支A测试助手。",
131
+ useHistory: 0,
132
+ userPrompt: "如果用户输入命中A分支,请返回“A分支已执行”。否则也给出简短回答。",
133
+ },
134
+ outputs: [
135
+ {
136
+ disableEdit: false,
137
+ id: "outputBranchA",
138
+ name: "output",
139
+ type: "string",
140
+ },
141
+ ],
142
+ version: 1,
143
+ visionInputs: [],
144
+ },
145
+ },
146
+ {
147
+ id: "llmBranchBAZ001",
148
+ type: "LLM",
149
+ meta: { position: { x: 780, y: 460 } },
150
+ data: {
151
+ title: "分支B回答",
152
+ nodeMeta: {
153
+ description: "调用大语言模型,使用变量和提示词生成回复",
154
+ title: "大模型",
155
+ },
156
+ exceptionSetting: {
157
+ timeout: 60,
158
+ retryTimes: 0,
159
+ strategy: 0,
160
+ portId: "",
161
+ },
162
+ inputs: [
163
+ {
164
+ constantValue: "",
165
+ disableEdit: false,
166
+ id: "inputBranchB",
167
+ isRef: 1,
168
+ name: "input",
169
+ refId: "USER_TEXT",
170
+ refName: "USER_TEXT",
171
+ refNode: "100001",
172
+ type: "string",
173
+ },
174
+ ],
175
+ nodeParam: {
176
+ model: "160",
177
+ outputType: 1,
178
+ skills: [],
179
+ systemPrompt: "你是分支B测试助手。",
180
+ useHistory: 0,
181
+ userPrompt: "如果用户输入未命中A分支,请返回“B分支已执行”。否则也给出简短回答。",
182
+ },
183
+ outputs: [
184
+ {
185
+ disableEdit: false,
186
+ id: "outputBranchB",
187
+ name: "output",
188
+ type: "string",
189
+ },
190
+ ],
191
+ version: 1,
192
+ visionInputs: [],
193
+ },
194
+ },
195
+ {
196
+ id: "end_branch_a",
197
+ type: "End",
198
+ meta: { position: { x: 1140, y: 180 } },
199
+ data: {
200
+ title: "分支A结束",
201
+ nodeMeta: {
202
+ description: "支持中间过程的消息输出,支持流式和非流式两种方式",
203
+ title: "输出",
204
+ },
205
+ nodeParam: {
206
+ isStream: 0,
207
+ outputContent: "",
208
+ saveRecord: true,
209
+ tts: true,
210
+ },
211
+ inputs: [
212
+ {
213
+ constantValue: "",
214
+ disableEdit: false,
215
+ id: "inputEndA",
216
+ isRef: 1,
217
+ name: "output",
218
+ refId: "outputBranchA",
219
+ refName: "output",
220
+ refNode: "llmBranchAAZ001",
221
+ type: "string",
222
+ },
223
+ ],
224
+ visionInputs: [],
225
+ audioInputs: [],
226
+ outputs: [],
227
+ version: 1,
228
+ },
229
+ },
230
+ {
231
+ id: "end_branch_b",
232
+ type: "End",
233
+ meta: { position: { x: 1140, y: 460 } },
234
+ data: {
235
+ title: "分支B结束",
236
+ nodeMeta: {
237
+ description: "支持中间过程的消息输出,支持流式和非流式两种方式",
238
+ title: "输出",
239
+ },
240
+ nodeParam: {
241
+ isStream: 0,
242
+ outputContent: "",
243
+ saveRecord: true,
244
+ tts: true,
245
+ },
246
+ inputs: [
247
+ {
248
+ constantValue: "",
249
+ disableEdit: false,
250
+ id: "inputEndB",
251
+ isRef: 1,
252
+ name: "output",
253
+ refId: "outputBranchB",
254
+ refName: "output",
255
+ refNode: "llmBranchBAZ001",
256
+ type: "string",
257
+ },
258
+ ],
259
+ visionInputs: [],
260
+ audioInputs: [],
261
+ outputs: [],
262
+ version: 1,
263
+ },
264
+ },
265
+ ],
266
+ edges: [
267
+ { sourceNodeID: "100001", targetNodeID: "selectBranchAZ001" },
268
+ { sourceNodeID: "selectBranchAZ001", targetNodeID: "llmBranchAAZ001" },
269
+ { sourceNodeID: "selectBranchAZ001", targetNodeID: "llmBranchBAZ001" },
270
+ { sourceNodeID: "llmBranchAAZ001", targetNodeID: "end_branch_a" },
271
+ { sourceNodeID: "llmBranchBAZ001", targetNodeID: "end_branch_b" },
272
+ ],
273
+ workflow: {
274
+ region,
275
+ },
276
+ };
277
+ }
278
+
279
+ function buildSummaryMarkdown(artifact) {
280
+ return [
281
+ "# Branch Edges Sample",
282
+ "",
283
+ `- Workflow: \`${artifact.workflowName}\` (${artifact.workflowId})`,
284
+ `- Region: \`${artifact.region}\``,
285
+ `- Edge count: \`${artifact.savedSchema.edges.length}\``,
286
+ `- Node types: \`${artifact.savedSchema.nodes.map((item) => item.type).join(", ")}\``,
287
+ "",
288
+ "## Selector",
289
+ `- Type: \`${artifact.selectorNode.type}\``,
290
+ `- Branch keys: \`${Object.keys(
291
+ artifact.selectorNode.data?.nodeParam?.selectConditions ?? {},
292
+ ).join(", ")}\``,
293
+ "",
294
+ ].join("\n");
295
+ }
296
+
297
+ export async function runSampleBranchEdges(config) {
298
+ await ensureDir(config.outputDir);
299
+
300
+ const { browser, context, page } = await getBrowserContext(config.remoteDebuggingPort);
301
+ const capture = buildNetworkCapture(
302
+ context,
303
+ /workflow\/update|workflow\/get|workflow\/list|addBasic/i,
304
+ );
305
+ let createdWorkflowId = null;
306
+
307
+ try {
308
+ await page.bringToFront();
309
+ await page.goto(config.baseUrl, { waitUntil: "domcontentloaded" }).catch(() => {});
310
+ await page.waitForTimeout(4_000);
311
+
312
+ const workflowName = config.workflowName ?? `样本-多分支-${Date.now().toString().slice(-6)}`;
313
+ const created = await createWorkflowViaPage(page, {
314
+ workflowName,
315
+ workflowDesc: "CLI branch edges sample",
316
+ workflowRegion: config.region,
317
+ workflowType: 1,
318
+ });
319
+ createdWorkflowId = created.workflowId;
320
+
321
+ const detailBefore = await getWorkflowViaPage(page, created.workflowId);
322
+ const currentEditVersionNo = detailBefore.json?.result?.editVersionNo ?? 1;
323
+ const schemaJson = buildBranchWorkflowSchema({ region: config.region });
324
+
325
+ const updated = await updateWorkflowViaPage(page, {
326
+ workflowId: created.workflowId,
327
+ editVersionNo: currentEditVersionNo,
328
+ schemaJson,
329
+ });
330
+
331
+ const detailAfter = await getWorkflowViaPage(page, created.workflowId);
332
+ const savedSchema = readWorkflowSchema(detailAfter);
333
+ const selectorNode = savedSchema.nodes.find((item) => item.type === "Select") ?? null;
334
+
335
+ if (!selectorNode) {
336
+ throw new Error("Branch edges sample failed: selector node missing after workflow/get");
337
+ }
338
+
339
+ if ((savedSchema.edges ?? []).length < 5) {
340
+ throw new Error("Branch edges sample failed: expected at least 5 edges");
341
+ }
342
+
343
+ const artifact = {
344
+ generatedAt: new Date().toISOString(),
345
+ workflowId: created.workflowId,
346
+ workflowName,
347
+ region: config.region,
348
+ create: created,
349
+ getBefore: detailBefore,
350
+ update: updated,
351
+ getAfter: detailAfter,
352
+ savedSchema,
353
+ selectorNode,
354
+ events: capture.events,
355
+ };
356
+
357
+ await writeJson(
358
+ path.join(config.outputDir, `branch-edges-${created.workflowId}.json`),
359
+ artifact,
360
+ );
361
+ await writeText(
362
+ path.join(config.outputDir, "branch-edges.md"),
363
+ buildSummaryMarkdown(artifact),
364
+ );
365
+
366
+ console.log(
367
+ JSON.stringify(
368
+ {
369
+ outputDir: config.outputDir,
370
+ workflowId: created.workflowId,
371
+ workflowName,
372
+ edgeCount: savedSchema.edges.length,
373
+ },
374
+ null,
375
+ 2,
376
+ ),
377
+ );
378
+ } finally {
379
+ if (createdWorkflowId) {
380
+ try {
381
+ await pageFetchJson(page, "/micro-app/exp/api/v2/aigc/workflow/delete", {
382
+ body: { params: { workflowId: createdWorkflowId } },
383
+ });
384
+ } catch {
385
+ console.error(`Warning: failed to clean up workflow ${createdWorkflowId}`);
386
+ }
387
+ }
388
+ capture.dispose();
389
+ await browser.close();
390
+ }
391
+ }
@@ -0,0 +1,204 @@
1
+ import path from "node:path";
2
+ import { ensureDir, writeJson, writeText } from "../util.js";
3
+ import {
4
+ assertNodePresent,
5
+ buildNetworkCapture,
6
+ createWorkflowViaPage,
7
+ extractSchemaNode,
8
+ getBrowserContext,
9
+ pageFetchJson,
10
+ } from "./shared.js";
11
+
12
+ async function openEditor(page, workflowId, baseUrl) {
13
+ await page.goto(`${baseUrl}/edit?workflowId=${workflowId}`, {
14
+ waitUntil: "domcontentloaded",
15
+ });
16
+ await page.waitForTimeout(7_000);
17
+ }
18
+
19
+ async function deleteWorkflowById(page, workflowId, { baseOrigin } = {}) {
20
+ try {
21
+ await pageFetchJson(page, "/micro-app/exp/api/v2/aigc/workflow/delete", {
22
+ body: { params: { workflowId } },
23
+ baseOrigin,
24
+ });
25
+ } catch {
26
+ console.error(`Warning: failed to clean up workflow ${workflowId}`);
27
+ }
28
+ }
29
+
30
+ async function addNodeFromPanel(page, nodeName) {
31
+ await page.getByRole("button", { name: "添加节点" }).click();
32
+ await page.waitForTimeout(1_000);
33
+ await page.getByText(nodeName, { exact: true }).last().click({ force: true });
34
+ await page.waitForTimeout(6_000);
35
+ }
36
+
37
+ async function captureLoopSample(page, context, outputDir, { namePrefix, region, baseUrl }) {
38
+ const workflowName = `${namePrefix}-循环`;
39
+ const capture = buildNetworkCapture(context);
40
+ let workflowId = null;
41
+
42
+ try {
43
+ const created = await createWorkflowViaPage(page, {
44
+ workflowName,
45
+ workflowDesc: "CLI Loop sample",
46
+ workflowRegion: region,
47
+ workflowType: 1,
48
+ });
49
+ workflowId = created.workflowId;
50
+
51
+ await openEditor(page, created.workflowId, baseUrl);
52
+ await addNodeFromPanel(page, "循环");
53
+
54
+ const snapshot = extractSchemaNode(capture.events, "Loop");
55
+ const loopNode = assertNodePresent(snapshot, "Loop");
56
+ const artifact = {
57
+ workflowName,
58
+ workflowId: created.workflowId,
59
+ createResponse: created.json,
60
+ bodyText: (await page.locator("body").innerText()).slice(0, 8_000),
61
+ events: capture.events,
62
+ loopNode,
63
+ schemaJson: snapshot.schemaJson,
64
+ };
65
+
66
+ await writeJson(
67
+ path.join(outputDir, `loop-node-sample-${created.workflowId}.json`),
68
+ artifact,
69
+ );
70
+
71
+ return artifact;
72
+ } finally {
73
+ capture.dispose();
74
+ if (workflowId) {
75
+ await deleteWorkflowById(page, workflowId);
76
+ }
77
+ }
78
+ }
79
+
80
+ async function captureMonitorSample(page, context, outputDir, { namePrefix, region, baseUrl }) {
81
+ const workflowName = `${namePrefix}-定时监听`;
82
+ const capture = buildNetworkCapture(context);
83
+ let workflowId = null;
84
+
85
+ try {
86
+ const created = await createWorkflowViaPage(page, {
87
+ workflowName,
88
+ workflowDesc: "CLI Monitor sample",
89
+ workflowRegion: region,
90
+ workflowType: 1,
91
+ });
92
+ workflowId = created.workflowId;
93
+
94
+ await openEditor(page, created.workflowId, baseUrl);
95
+ await addNodeFromPanel(page, "定时监听");
96
+
97
+ const snapshot = extractSchemaNode(capture.events, "Monitor");
98
+ const monitorNode = assertNodePresent(snapshot, "Monitor");
99
+ const artifact = {
100
+ workflowName,
101
+ workflowId: created.workflowId,
102
+ createResponse: created.json,
103
+ bodyText: (await page.locator("body").innerText()).slice(0, 8_000),
104
+ events: capture.events,
105
+ monitorNode,
106
+ schemaJson: snapshot.schemaJson,
107
+ };
108
+
109
+ await writeJson(
110
+ path.join(outputDir, `monitor-node-sample-${created.workflowId}.json`),
111
+ artifact,
112
+ );
113
+
114
+ return artifact;
115
+ } finally {
116
+ capture.dispose();
117
+ if (workflowId) {
118
+ await deleteWorkflowById(page, workflowId);
119
+ }
120
+ }
121
+ }
122
+
123
+ function buildSummaryMarkdown({ loopArtifact, monitorArtifact }) {
124
+ return [
125
+ "# Extra Node Samples",
126
+ "",
127
+ `- Loop workflow: \`${loopArtifact.workflowName}\` (${loopArtifact.workflowId})`,
128
+ `- Monitor workflow: \`${monitorArtifact.workflowName}\` (${monitorArtifact.workflowId})`,
129
+ "",
130
+ "## Loop",
131
+ `- Type: \`${loopArtifact.loopNode?.type ?? "unknown"}\``,
132
+ `- Blocks: \`${(loopArtifact.loopNode?.blocks ?? []).map((item) => item.type).join(", ")}\``,
133
+ `- loopType: \`${loopArtifact.loopNode?.data?.nodeParam?.loopType ?? ""}\``,
134
+ "",
135
+ "## Monitor",
136
+ `- Type: \`${monitorArtifact.monitorNode?.type ?? "unknown"}\``,
137
+ `- Blocks: \`${(monitorArtifact.monitorNode?.blocks ?? []).map((item) => item.type).join(", ")}\``,
138
+ `- time: \`${monitorArtifact.monitorNode?.data?.nodeParam?.time ?? ""}\``,
139
+ `- useOutput: \`${monitorArtifact.monitorNode?.data?.nodeParam?.useOutput ?? ""}\``,
140
+ "",
141
+ ].join("\n");
142
+ }
143
+
144
+ export async function runSampleExtraNodes(config) {
145
+ await ensureDir(config.outputDir);
146
+
147
+ const { browser, context, page } = await getBrowserContext(config.remoteDebuggingPort);
148
+
149
+ try {
150
+ await page.bringToFront();
151
+ await page.goto(config.baseUrl, { waitUntil: "domcontentloaded" }).catch(() => {});
152
+ await page.waitForTimeout(4_000);
153
+
154
+ const namePrefix = config.workflowName ?? `样本-${Date.now().toString().slice(-6)}`;
155
+ const loopArtifact = await captureLoopSample(page, context, config.outputDir, {
156
+ namePrefix,
157
+ region: config.region,
158
+ baseUrl: config.baseUrl,
159
+ });
160
+ const monitorArtifact = await captureMonitorSample(page, context, config.outputDir, {
161
+ namePrefix,
162
+ region: config.region,
163
+ baseUrl: config.baseUrl,
164
+ });
165
+
166
+ const summary = {
167
+ generatedAt: new Date().toISOString(),
168
+ command: "sample-extra-nodes",
169
+ region: config.region,
170
+ loop: {
171
+ workflowId: loopArtifact.workflowId,
172
+ workflowName: loopArtifact.workflowName,
173
+ nodeType: loopArtifact.loopNode?.type ?? null,
174
+ },
175
+ monitor: {
176
+ workflowId: monitorArtifact.workflowId,
177
+ workflowName: monitorArtifact.workflowName,
178
+ nodeType: monitorArtifact.monitorNode?.type ?? null,
179
+ },
180
+ files: {
181
+ loop: `loop-node-sample-${loopArtifact.workflowId}.json`,
182
+ monitor: `monitor-node-sample-${monitorArtifact.workflowId}.json`,
183
+ },
184
+ };
185
+
186
+ await writeJson(path.join(config.outputDir, "extra-node-samples-summary.json"), summary);
187
+ await writeJson(path.join(config.outputDir, "extra-node-samples-extended.json"), {
188
+ loopNode: loopArtifact.loopNode,
189
+ monitorNode: monitorArtifact.monitorNode,
190
+ workflowIds: {
191
+ loop: loopArtifact.workflowId,
192
+ monitor: monitorArtifact.workflowId,
193
+ },
194
+ });
195
+ await writeText(
196
+ path.join(config.outputDir, "extra-node-samples.md"),
197
+ buildSummaryMarkdown({ loopArtifact, monitorArtifact }),
198
+ );
199
+
200
+ console.log(JSON.stringify({ outputDir: config.outputDir, summary }, null, 2));
201
+ } finally {
202
+ await browser.close();
203
+ }
204
+ }