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.
- package/README.md +218 -0
- package/examples/rag-workflow.json +63 -0
- package/examples/simple-llm.json +37 -0
- package/package.json +40 -0
- package/src/cli.js +78 -0
- package/src/lib/analyze.js +213 -0
- package/src/lib/cdp-client.js +115 -0
- package/src/lib/chrome.js +115 -0
- package/src/lib/commands/auto-basic.js +482 -0
- package/src/lib/commands/configure.js +158 -0
- package/src/lib/commands/doctor.js +184 -0
- package/src/lib/commands/list-libraries.js +33 -0
- package/src/lib/commands/manual-record.js +82 -0
- package/src/lib/commands/publish.js +63 -0
- package/src/lib/commands/sample-branch-edges.js +391 -0
- package/src/lib/commands/sample-extra-nodes.js +204 -0
- package/src/lib/commands/sample-trial-inputs.js +173 -0
- package/src/lib/commands/shared.js +457 -0
- package/src/lib/config.js +204 -0
- package/src/lib/recorder.js +316 -0
- package/src/lib/report.js +309 -0
- package/src/lib/schema-builder.js +431 -0
- package/src/lib/selectors.js +12 -0
- package/src/lib/steps.js +50 -0
- package/src/lib/util.js +93 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { summarizeWorkflowSchema, buildStaticAnalysis } from "./analyze.js";
|
|
3
|
+
import { safeJsonParse, writeText } from "./util.js";
|
|
4
|
+
|
|
5
|
+
const STEP_PRIMARY_PATTERNS = {
|
|
6
|
+
"step-01-list-load": ["/micro-app/exp/api/aigc/workflow/list"],
|
|
7
|
+
"step-02-create-workflow": ["/micro-app/exp/api/v2/aigc/workflow/addBasic"],
|
|
8
|
+
"step-03-detail-load": ["/micro-app/ai/api/v2/aigc/workflow/get"],
|
|
9
|
+
"step-04-add-intent-node": ["/micro-app/ai/api/aigc/workflow/update"],
|
|
10
|
+
"step-05-add-llm-node": ["/micro-app/ai/api/aigc/workflow/update"],
|
|
11
|
+
"step-06-save-workflow": ["/micro-app/ai/api/aigc/workflow/update"],
|
|
12
|
+
"step-07-trial-run": [
|
|
13
|
+
"/micro-app/ai/api/v2/aigc/workflow/test/execute",
|
|
14
|
+
"/micro-app/ai/api/aigc/workflow/test/config/save",
|
|
15
|
+
"/micro-app/ai/api/aigc/workflow/test/config",
|
|
16
|
+
"/micro-app/ai/api/aigc/workflow/test/progress",
|
|
17
|
+
],
|
|
18
|
+
"step-08-delete-workflow": ["/micro-app/exp/api/aigc/workflow/delete"],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const STEP_FALLBACK_SOURCES = {
|
|
22
|
+
"step-06-save-workflow": ["step-05-add-llm-node", "step-04-add-intent-node"],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function isNoiseRequest(request) {
|
|
26
|
+
const url = String(request?.url ?? "");
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
request?.method === "OPTIONS" ||
|
|
30
|
+
url.startsWith("chrome-extension://") ||
|
|
31
|
+
url.includes("tpm.tuyacn.com/tpm.gif") ||
|
|
32
|
+
url.includes("sentry.tuyacn.com/") ||
|
|
33
|
+
url.includes("static1.tuyacn.com/") ||
|
|
34
|
+
url.includes("feedback.tuya-inc.top") ||
|
|
35
|
+
url.includes("/_channel")
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function scoreRequest(stepId, request) {
|
|
40
|
+
if (!request || isNoiseRequest(request)) {
|
|
41
|
+
return Number.NEGATIVE_INFINITY;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const url = String(request.url ?? "");
|
|
45
|
+
const patterns = STEP_PRIMARY_PATTERNS[stepId] ?? [];
|
|
46
|
+
let score = 0;
|
|
47
|
+
|
|
48
|
+
if (url.includes("platform.tuya.com")) {
|
|
49
|
+
score += 5;
|
|
50
|
+
}
|
|
51
|
+
if (url.includes("/micro-app/")) {
|
|
52
|
+
score += 20;
|
|
53
|
+
}
|
|
54
|
+
if (request.method === "POST") {
|
|
55
|
+
score += 8;
|
|
56
|
+
}
|
|
57
|
+
if (String(request.responseHeaders?.["content-type"] ?? "").includes("json")) {
|
|
58
|
+
score += 4;
|
|
59
|
+
}
|
|
60
|
+
if (safeJsonParse(request.requestBody) || safeJsonParse(request.responseBody)) {
|
|
61
|
+
score += 4;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const schema = url.includes("workflow/update") ? summarizeWorkflowSchema(request) : null;
|
|
65
|
+
if (stepId === "step-06-save-workflow" && schema?.edgeCount > 0) {
|
|
66
|
+
score += 40;
|
|
67
|
+
}
|
|
68
|
+
if (stepId === "step-06-save-workflow" && schema?.nodeCount > 0) {
|
|
69
|
+
score += 8;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const matchedPattern = patterns
|
|
73
|
+
.filter((pattern) => url.includes(pattern))
|
|
74
|
+
.sort((left, right) => right.length - left.length)[0];
|
|
75
|
+
|
|
76
|
+
if (matchedPattern) {
|
|
77
|
+
score += 100;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (url.includes("workflow/update")) {
|
|
81
|
+
score += 15;
|
|
82
|
+
}
|
|
83
|
+
if (url.includes("workflow/test/execute")) {
|
|
84
|
+
score += 25;
|
|
85
|
+
}
|
|
86
|
+
if (url.includes("workflow/test/progress")) {
|
|
87
|
+
score += 20;
|
|
88
|
+
}
|
|
89
|
+
if (url.includes("workflow/test/config/save")) {
|
|
90
|
+
score += 18;
|
|
91
|
+
}
|
|
92
|
+
if (url.includes("workflow/test/")) {
|
|
93
|
+
score += 15;
|
|
94
|
+
}
|
|
95
|
+
if (url.includes("workflow/get") || url.includes("workflow/list") || url.includes("workflow/delete")) {
|
|
96
|
+
score += 12;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return score;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function expandSchemaJson(value) {
|
|
103
|
+
if (Array.isArray(value)) {
|
|
104
|
+
return value.map(expandSchemaJson);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!value || typeof value !== "object") {
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const output = {};
|
|
112
|
+
for (const [key, current] of Object.entries(value)) {
|
|
113
|
+
if (key === "schemaJson" && typeof current === "string") {
|
|
114
|
+
output[key] = safeJsonParse(current) ?? current;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
output[key] = expandSchemaJson(current);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return output;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeBody(body) {
|
|
125
|
+
const parsed = safeJsonParse(body);
|
|
126
|
+
return parsed ? expandSchemaJson(parsed) : body ?? null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function extractWorkflowId(request) {
|
|
130
|
+
const sources = [
|
|
131
|
+
request?.url,
|
|
132
|
+
request?.requestBody,
|
|
133
|
+
request?.responseBody,
|
|
134
|
+
]
|
|
135
|
+
.filter(Boolean)
|
|
136
|
+
.map(String);
|
|
137
|
+
|
|
138
|
+
for (const source of sources) {
|
|
139
|
+
const match = source.match(/[?&]id=([^&]+)/) ?? source.match(/workflowId["':= ]+([a-zA-Z0-9_-]+)/);
|
|
140
|
+
if (match) {
|
|
141
|
+
return match[1];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function detectEvidenceLevel(request) {
|
|
149
|
+
return request ? "实测抓包" : "静态推断";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function extractRelevantHeaders(request) {
|
|
153
|
+
if (!request) {
|
|
154
|
+
return {};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const headers = request.requestHeaders ?? {};
|
|
158
|
+
const relevantKeys = [
|
|
159
|
+
"authorization",
|
|
160
|
+
"cookie",
|
|
161
|
+
"csrf-token",
|
|
162
|
+
"x-csrf-token",
|
|
163
|
+
"x-requested-with",
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
return Object.fromEntries(
|
|
167
|
+
Object.entries(headers).filter(([key]) => relevantKeys.includes(key.toLowerCase())),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function findBestRequest(stepId, stepNetworks) {
|
|
172
|
+
const current = stepNetworks.find((entry) => entry.step.id === stepId);
|
|
173
|
+
const candidates = current?.network ?? [];
|
|
174
|
+
const sorted = [...candidates].sort((left, right) => scoreRequest(stepId, right) - scoreRequest(stepId, left));
|
|
175
|
+
|
|
176
|
+
if (sorted[0] && scoreRequest(stepId, sorted[0]) > 0) {
|
|
177
|
+
return { request: sorted[0], sourceStepId: stepId };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const fallbackStepId of STEP_FALLBACK_SOURCES[stepId] ?? []) {
|
|
181
|
+
const fallback = stepNetworks.find((entry) => entry.step.id === fallbackStepId);
|
|
182
|
+
const fallbackSorted = [...(fallback?.network ?? [])].sort(
|
|
183
|
+
(left, right) => scoreRequest(stepId, right) - scoreRequest(stepId, left),
|
|
184
|
+
);
|
|
185
|
+
if (fallbackSorted[0] && scoreRequest(stepId, fallbackSorted[0]) > 0) {
|
|
186
|
+
return { request: fallbackSorted[0], sourceStepId: fallbackStepId };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { request: candidates[0] ?? null, sourceStepId: stepId };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function buildNotes(step, primaryRequest, sourceStepId, network) {
|
|
194
|
+
const notes = [];
|
|
195
|
+
|
|
196
|
+
notes.push(`证据等级:${detectEvidenceLevel(primaryRequest)}`);
|
|
197
|
+
|
|
198
|
+
if (!primaryRequest) {
|
|
199
|
+
notes.push("该步骤未识别出主请求,请结合 raw network.json 和 static-analysis.md 复核。");
|
|
200
|
+
return notes.join(";");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const workflowId = extractWorkflowId(primaryRequest);
|
|
204
|
+
if (workflowId) {
|
|
205
|
+
notes.push(`workflowId 线索:${workflowId}`);
|
|
206
|
+
if (/^\d+$/.test(workflowId)) {
|
|
207
|
+
notes.push("workflowId 格式:十进制数字,表现为后端创建后返回");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (sourceStepId && sourceStepId !== step.id) {
|
|
212
|
+
notes.push(`主证据来自 ${sourceStepId}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const streamType =
|
|
216
|
+
primaryRequest.method === "WEBSOCKET"
|
|
217
|
+
? "WebSocket"
|
|
218
|
+
: String(primaryRequest.responseHeaders?.["content-type"] ?? "")
|
|
219
|
+
.includes("text/event-stream")
|
|
220
|
+
? "SSE"
|
|
221
|
+
: primaryRequest.responseBody?.includes("\n\n")
|
|
222
|
+
? "可能为分块流式响应"
|
|
223
|
+
: "非流式或未识别";
|
|
224
|
+
|
|
225
|
+
if (step.id === "step-07-trial-run") {
|
|
226
|
+
notes.push(`试运行流式格式:${streamType}`);
|
|
227
|
+
notes.push("前端实现:先调用 /workflow/test/execute,再轮询 /workflow/test/progress;未发现 SSE");
|
|
228
|
+
|
|
229
|
+
const executeBody = normalizeBody(primaryRequest?.responseBody);
|
|
230
|
+
if (typeof executeBody?.result === "string") {
|
|
231
|
+
notes.push(`executeId:${executeBody.result}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const progressRequest = (network ?? []).find((request) =>
|
|
235
|
+
String(request.url ?? "").includes("/workflow/test/progress"),
|
|
236
|
+
);
|
|
237
|
+
const progressBody = normalizeBody(progressRequest?.responseBody);
|
|
238
|
+
const progressResult = progressBody?.result;
|
|
239
|
+
if (progressResult) {
|
|
240
|
+
notes.push(
|
|
241
|
+
`progress 完成态:executeStatus=${progressResult.executeStatus},nodeResults=${progressResult.nodeResults?.length ?? 0},outputCards=${progressResult.outputCards?.length ?? 0}`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (step.id === "step-06-save-workflow") {
|
|
247
|
+
const schema = summarizeWorkflowSchema(primaryRequest);
|
|
248
|
+
if (schema) {
|
|
249
|
+
notes.push(
|
|
250
|
+
`保存 schema 摘要:nodes=${schema.nodeCount},edges=${schema.edgeCount},nodeTypes=${schema.nodeTypes.join(", ") || "unknown"}`,
|
|
251
|
+
);
|
|
252
|
+
if (schema.nodes.length > 0) {
|
|
253
|
+
notes.push(
|
|
254
|
+
`节点字段:id(${schema.nodes[0].id}) / type(${schema.nodes[0].type}) / meta.position(${schema.nodes[0].position}) / data(${schema.nodes[0].data})`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
if (schema.edgeCount === 0) {
|
|
258
|
+
notes.push("edges 结构:当前保存样本为空数组 []");
|
|
259
|
+
} else if (schema.edges.length > 0) {
|
|
260
|
+
const edgeFields = Object.entries(schema.edges[0])
|
|
261
|
+
.map(([key, value]) => `${key}(${value})`)
|
|
262
|
+
.join(" / ");
|
|
263
|
+
notes.push(
|
|
264
|
+
`edges 字段:${edgeFields}`,
|
|
265
|
+
);
|
|
266
|
+
if (schema.edgeSample) {
|
|
267
|
+
notes.push(`edges 样例:${JSON.stringify(schema.edgeSample)}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return notes.join(";");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function writeReport(outputDir, stepNetworks) {
|
|
277
|
+
const lines = ["# Tuya Workflow API Reverse Report", ""];
|
|
278
|
+
|
|
279
|
+
for (const { step, network } of stepNetworks) {
|
|
280
|
+
const { request: primary, sourceStepId } = findBestRequest(step.id, stepNetworks);
|
|
281
|
+
|
|
282
|
+
lines.push(`### ${step.title}`);
|
|
283
|
+
lines.push(`- 方法:${primary?.method ?? "未识别"}`);
|
|
284
|
+
lines.push(`- URL:${primary?.url ?? "未识别"}`);
|
|
285
|
+
lines.push(
|
|
286
|
+
`- 请求头(认证相关):${JSON.stringify(extractRelevantHeaders(primary), null, 2)}`,
|
|
287
|
+
);
|
|
288
|
+
lines.push(
|
|
289
|
+
`- 请求 Body / Query 参数(完整 JSON):${JSON.stringify(normalizeBody(primary?.requestBody), null, 2)}`,
|
|
290
|
+
);
|
|
291
|
+
lines.push(
|
|
292
|
+
`- 响应 Body(完整 JSON,至少记录关键字段):${JSON.stringify(normalizeBody(primary?.responseBody), null, 2)}`,
|
|
293
|
+
);
|
|
294
|
+
lines.push(`- 备注:${buildNotes(step, primary, sourceStepId, network)}`);
|
|
295
|
+
lines.push("");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const hits = buildStaticAnalysis(stepNetworks);
|
|
299
|
+
lines.push("## Static Hints");
|
|
300
|
+
if (hits.length === 0) {
|
|
301
|
+
lines.push("- 无额外静态关键词命中。");
|
|
302
|
+
} else {
|
|
303
|
+
for (const hit of hits.slice(0, 20)) {
|
|
304
|
+
lines.push(`- ${hit.stepTitle}: ${hit.method} ${hit.url} [${hit.matches.join(", ")}]`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
await writeText(path.join(outputDir, "report.md"), `${lines.join("\n")}\n`);
|
|
309
|
+
}
|