@77-zhike/app-sdk 0.2.0-d7b1.10
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 +251 -0
- package/package.json +71 -0
- package/src/bin.js +15 -0
- package/src/cli/api-client.js +249 -0
- package/src/cli/browser-auth.js +212 -0
- package/src/cli/credential-store.js +101 -0
- package/src/cli/development-app.js +77 -0
- package/src/cli/development-logs.js +183 -0
- package/src/cli/development-watch.js +205 -0
- package/src/cli/index.js +256 -0
- package/src/cli/local-commands.js +80 -0
- package/src/cli/logs.js +56 -0
- package/src/cli/project-init.js +259 -0
- package/src/cli/upload-bundle.js +73 -0
- package/src/client/analysis.d.ts +4 -0
- package/src/client/analysis.js +12 -0
- package/src/client/destinations.d.ts +25 -0
- package/src/client/destinations.js +70 -0
- package/src/client/external-url.js +9 -0
- package/src/client/index.d.ts +249 -0
- package/src/client/index.js +72 -0
- package/src/client/interactions.d.ts +23 -0
- package/src/client/interactions.js +62 -0
- package/src/client/points.generated.js +67 -0
- package/src/config.js +68 -0
- package/src/development-workspace.js +154 -0
- package/src/errors.js +20 -0
- package/src/forms/array-contract.d.ts +32 -0
- package/src/forms/array-contract.js +138 -0
- package/src/forms/components.generated.js +4 -0
- package/src/forms/factory.js +41 -0
- package/src/forms/field-array.d.ts +16 -0
- package/src/forms/field-array.js +52 -0
- package/src/forms/index.d.ts +118 -0
- package/src/forms/index.js +5 -0
- package/src/forms/normalize-options.js +19 -0
- package/src/forms/options.d.ts +30 -0
- package/src/forms/options.js +55 -0
- package/src/forms/record-options.d.ts +28 -0
- package/src/forms/record-options.js +92 -0
- package/src/forms/schema.d.ts +82 -0
- package/src/forms/schema.js +128 -0
- package/src/forms/state.d.ts +30 -0
- package/src/forms/state.js +82 -0
- package/src/forms/validation.d.ts +9 -0
- package/src/forms/validation.js +81 -0
- package/src/forms/values.d.ts +5 -0
- package/src/forms/values.js +38 -0
- package/src/index.d.ts +13 -0
- package/src/index.js +2 -0
- package/src/jsx-dev-runtime.d.ts +2 -0
- package/src/jsx-dev-runtime.js +1 -0
- package/src/jsx-runtime.d.ts +2 -0
- package/src/jsx-runtime.js +1 -0
- package/src/project-binding.js +84 -0
- package/src/server/index.d.ts +318 -0
- package/src/server/index.js +242 -0
- package/src/server/steps/index.d.ts +71 -0
- package/src/server/steps/index.js +15 -0
- package/src/server/triggers/index.d.ts +68 -0
- package/src/server/triggers/index.js +27 -0
- package/src/server/workflow-blocks/index.d.ts +72 -0
- package/src/server/workflow-blocks/index.js +32 -0
- package/src/toolchain/allowed-packages.json +7 -0
- package/src/toolchain/canonical-json.js +17 -0
- package/src/toolchain/dependency-policy.js +181 -0
- package/src/toolchain/index.js +15 -0
- package/src/toolchain/real-build.js +459 -0
- package/src/toolchain/source-discovery.js +919 -0
- package/src/toolchain/transport-tar.js +168 -0
- package/src/toolchain/webhook-response.js +50 -0
- package/src/toolchain/workflow-blocks/discovery.js +133 -0
- package/src/toolchain/workflow-steps/manifest.js +99 -0
- package/src/toolchain/workflow-triggers/manifest.js +47 -0
- package/src/ui/components.generated.js +5 -0
- package/src/ui/events.d.ts +10 -0
- package/src/ui/factory.js +70 -0
- package/src/ui/icons.generated.js +2 -0
- package/src/ui/index.d.ts +343 -0
- package/src/ui/index.js +4 -0
- package/src/ui/jsx.generated.d.ts +359 -0
- package/src/ui/native-dom.d.ts +49 -0
- package/src/ui/native-dom.js +44 -0
- package/src/ui/style.d.ts +5 -0
- package/src/ui/style.generated.js +2 -0
- package/src/ui/style.js +15 -0
- package/src/ui/validation.js +86 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { ZhikeCliError } from "../errors.js";
|
|
2
|
+
|
|
3
|
+
export const APP_BUNDLE_TAR_MAX_BYTES = 4 * 1024 * 1024;
|
|
4
|
+
export const APP_BUNDLE_MEMBER_MAX_BYTES = 2 * 1024 * 1024;
|
|
5
|
+
export const APP_BUNDLE_ALLOWED_MEMBERS = Object.freeze([
|
|
6
|
+
"manifest.json",
|
|
7
|
+
"client.js",
|
|
8
|
+
"server.js",
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
const BLOCK_SIZE = 512;
|
|
12
|
+
|
|
13
|
+
export function createTransportTar(
|
|
14
|
+
entries,
|
|
15
|
+
{ mtime = 0, order = null, uid = 0, gid = 0, mode = 0o644 } = {},
|
|
16
|
+
) {
|
|
17
|
+
const byName = new Map(entries.map((entry) => [entry.name, entry]));
|
|
18
|
+
const names = order || entries.map((entry) => entry.name);
|
|
19
|
+
if (names.length !== entries.length || new Set(names).size !== names.length) {
|
|
20
|
+
throw tarError("zhike_bundle_tar_invalid", "TAR 成员顺序包含缺失或重复成员");
|
|
21
|
+
}
|
|
22
|
+
const chunks = [];
|
|
23
|
+
for (const name of names) {
|
|
24
|
+
const entry = byName.get(name);
|
|
25
|
+
if (!entry || !APP_BUNDLE_ALLOWED_MEMBERS.includes(name)) {
|
|
26
|
+
throw tarError("zhike_bundle_tar_member_invalid", `TAR 成员不受支持:${name}`);
|
|
27
|
+
}
|
|
28
|
+
if (!Buffer.isBuffer(entry.bytes) || entry.bytes.byteLength > APP_BUNDLE_MEMBER_MAX_BYTES) {
|
|
29
|
+
throw tarError("zhike_bundle_tar_member_too_large", `TAR 成员无效或超限:${name}`);
|
|
30
|
+
}
|
|
31
|
+
const header = createHeader({
|
|
32
|
+
name,
|
|
33
|
+
size: entry.bytes.byteLength,
|
|
34
|
+
mtime,
|
|
35
|
+
uid,
|
|
36
|
+
gid,
|
|
37
|
+
mode,
|
|
38
|
+
});
|
|
39
|
+
chunks.push(header, entry.bytes);
|
|
40
|
+
const remainder = entry.bytes.byteLength % BLOCK_SIZE;
|
|
41
|
+
if (remainder !== 0) chunks.push(Buffer.alloc(BLOCK_SIZE - remainder));
|
|
42
|
+
}
|
|
43
|
+
chunks.push(Buffer.alloc(BLOCK_SIZE * 2));
|
|
44
|
+
const result = Buffer.concat(chunks);
|
|
45
|
+
if (result.byteLength > APP_BUNDLE_TAR_MAX_BYTES) {
|
|
46
|
+
throw tarError("zhike_bundle_tar_too_large", "App Bundle TAR 超过一期上限");
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function parseTransportTar(bytes) {
|
|
52
|
+
if (!Buffer.isBuffer(bytes) || bytes.byteLength > APP_BUNDLE_TAR_MAX_BYTES) {
|
|
53
|
+
throw tarError("zhike_bundle_tar_invalid", "App Bundle TAR 无效或超限");
|
|
54
|
+
}
|
|
55
|
+
const entries = new Map();
|
|
56
|
+
let offset = 0;
|
|
57
|
+
let zeroBlocks = 0;
|
|
58
|
+
while (offset + BLOCK_SIZE <= bytes.byteLength) {
|
|
59
|
+
const header = bytes.subarray(offset, offset + BLOCK_SIZE);
|
|
60
|
+
offset += BLOCK_SIZE;
|
|
61
|
+
if (header.every((value) => value === 0)) {
|
|
62
|
+
zeroBlocks += 1;
|
|
63
|
+
if (zeroBlocks === 2) break;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (zeroBlocks > 0) {
|
|
67
|
+
throw tarError("zhike_bundle_tar_invalid", "TAR 结束块不完整");
|
|
68
|
+
}
|
|
69
|
+
assertHeaderChecksum(header);
|
|
70
|
+
const name = readString(header, 0, 100);
|
|
71
|
+
const type = readString(header, 156, 1) || "0";
|
|
72
|
+
const linkName = readString(header, 157, 100);
|
|
73
|
+
if (
|
|
74
|
+
!APP_BUNDLE_ALLOWED_MEMBERS.includes(name) ||
|
|
75
|
+
name.startsWith("/") ||
|
|
76
|
+
name.includes("..") ||
|
|
77
|
+
name.includes("\\") ||
|
|
78
|
+
type !== "0" ||
|
|
79
|
+
linkName
|
|
80
|
+
) {
|
|
81
|
+
throw tarError("zhike_bundle_tar_member_invalid", "TAR 包含未知路径或 link 成员");
|
|
82
|
+
}
|
|
83
|
+
if (entries.has(name)) {
|
|
84
|
+
throw tarError("zhike_bundle_tar_member_duplicate", `TAR 成员重复:${name}`);
|
|
85
|
+
}
|
|
86
|
+
const size = readOctal(header, 124, 12);
|
|
87
|
+
if (size > APP_BUNDLE_MEMBER_MAX_BYTES || offset + size > bytes.byteLength) {
|
|
88
|
+
throw tarError("zhike_bundle_tar_member_too_large", `TAR 成员超限:${name}`);
|
|
89
|
+
}
|
|
90
|
+
entries.set(name, Buffer.from(bytes.subarray(offset, offset + size)));
|
|
91
|
+
offset += Math.ceil(size / BLOCK_SIZE) * BLOCK_SIZE;
|
|
92
|
+
}
|
|
93
|
+
if (zeroBlocks !== 2 || entries.size === 0) {
|
|
94
|
+
throw tarError("zhike_bundle_tar_invalid", "TAR 缺少成员或标准结束块");
|
|
95
|
+
}
|
|
96
|
+
if (bytes.subarray(offset).some((value) => value !== 0)) {
|
|
97
|
+
throw tarError("zhike_bundle_tar_invalid", "TAR 结束块后包含非零数据");
|
|
98
|
+
}
|
|
99
|
+
return entries;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function createHeader({ name, size, mtime, uid, gid, mode }) {
|
|
103
|
+
const header = Buffer.alloc(BLOCK_SIZE);
|
|
104
|
+
writeString(header, name, 0, 100);
|
|
105
|
+
writeOctal(header, mode, 100, 8);
|
|
106
|
+
writeOctal(header, uid, 108, 8);
|
|
107
|
+
writeOctal(header, gid, 116, 8);
|
|
108
|
+
writeOctal(header, size, 124, 12);
|
|
109
|
+
writeOctal(header, mtime, 136, 12);
|
|
110
|
+
header.fill(0x20, 148, 156);
|
|
111
|
+
writeString(header, "0", 156, 1);
|
|
112
|
+
writeString(header, "ustar", 257, 6);
|
|
113
|
+
writeString(header, "00", 263, 2);
|
|
114
|
+
writeOctal(header, checksum(header), 148, 8);
|
|
115
|
+
return header;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function assertHeaderChecksum(header) {
|
|
119
|
+
const declared = readOctal(header, 148, 8);
|
|
120
|
+
const copy = Buffer.from(header);
|
|
121
|
+
copy.fill(0x20, 148, 156);
|
|
122
|
+
if (declared !== checksum(copy)) {
|
|
123
|
+
throw tarError("zhike_bundle_tar_checksum_invalid", "TAR header checksum 无效");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function checksum(header) {
|
|
128
|
+
let total = 0;
|
|
129
|
+
for (const value of header) total += value;
|
|
130
|
+
return total;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function writeString(buffer, value, offset, length) {
|
|
134
|
+
const bytes = Buffer.from(value, "utf8");
|
|
135
|
+
if (bytes.byteLength > length) {
|
|
136
|
+
throw tarError("zhike_bundle_tar_member_invalid", "TAR 字段超长");
|
|
137
|
+
}
|
|
138
|
+
bytes.copy(buffer, offset);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function readString(buffer, offset, length) {
|
|
142
|
+
const end = buffer.indexOf(0, offset);
|
|
143
|
+
const boundary = end === -1 || end > offset + length ? offset + length : end;
|
|
144
|
+
return buffer.subarray(offset, boundary).toString("utf8");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function writeOctal(buffer, value, offset, length) {
|
|
148
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
149
|
+
throw tarError("zhike_bundle_tar_invalid", "TAR 数值字段无效");
|
|
150
|
+
}
|
|
151
|
+
const text = value.toString(8).padStart(length - 2, "0");
|
|
152
|
+
if (text.length > length - 2) {
|
|
153
|
+
throw tarError("zhike_bundle_tar_invalid", "TAR 数值字段超限");
|
|
154
|
+
}
|
|
155
|
+
writeString(buffer, `${text}\0 `, offset, length);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function readOctal(buffer, offset, length) {
|
|
159
|
+
const text = readString(buffer, offset, length).trim();
|
|
160
|
+
if (!/^[0-7]+$/.test(text)) {
|
|
161
|
+
throw tarError("zhike_bundle_tar_invalid", "TAR octal 字段无效");
|
|
162
|
+
}
|
|
163
|
+
return Number.parseInt(text, 8);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function tarError(code, message) {
|
|
167
|
+
return new ZhikeCliError(code, message);
|
|
168
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// 此函数被嵌入生成的 server.js,不能捕获构建进程变量或依赖 Node API。
|
|
2
|
+
export async function serializeWebhookResponse(response) {
|
|
3
|
+
const fail = () => { throw new Error("runtime_webhook_response_invalid"); };
|
|
4
|
+
if (!(response instanceof Response) || response.status < 200 || response.status > 599) fail();
|
|
5
|
+
const encoder = new TextEncoder();
|
|
6
|
+
const headers = [];
|
|
7
|
+
let headerBytes = 0;
|
|
8
|
+
const omitted = new Set(["content-length", "transfer-encoding", "connection", "keep-alive", "te", "trailer", "upgrade", "proxy-authenticate", "proxy-authorization", "set-cookie"]);
|
|
9
|
+
const connectionHeaders = new Set((response.headers.get("connection") ?? "").split(",").map(value => value.trim().toLowerCase()));
|
|
10
|
+
for (const [name, value] of response.headers) {
|
|
11
|
+
headerBytes += encoder.encode(name + value).byteLength;
|
|
12
|
+
if (headerBytes > 16384 || encoder.encode(value).byteLength > 4096 || /[\r\n\0]/.test(value)) fail();
|
|
13
|
+
if (!omitted.has(name) && !connectionHeaders.has(name)) headers.push([name, value]);
|
|
14
|
+
}
|
|
15
|
+
if ([...response.headers].length > 64) fail();
|
|
16
|
+
const encoding = response.headers.get("content-encoding");
|
|
17
|
+
if (encoding && encoding.trim().toLowerCase() !== "identity") fail();
|
|
18
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
19
|
+
const [mediaType, ...parameters] = contentType.toLowerCase().split(";").map(value => value.trim());
|
|
20
|
+
if (parameters.some(value => value.startsWith("charset=") && !/^charset=(?:utf-8|"utf-8")$/.test(value))) fail();
|
|
21
|
+
const chunks = [];
|
|
22
|
+
let length = 0;
|
|
23
|
+
const reader = response.body?.getReader();
|
|
24
|
+
if (reader) {
|
|
25
|
+
try {
|
|
26
|
+
while (true) {
|
|
27
|
+
const { done, value } = await reader.read();
|
|
28
|
+
if (done) break;
|
|
29
|
+
length += value.byteLength;
|
|
30
|
+
if (length > 32768) {
|
|
31
|
+
// 不等待用户流的 cancel Promise;它可能永远不完成。
|
|
32
|
+
void reader.cancel().catch(() => {});
|
|
33
|
+
fail();
|
|
34
|
+
}
|
|
35
|
+
chunks.push(value);
|
|
36
|
+
}
|
|
37
|
+
} finally {
|
|
38
|
+
reader.releaseLock();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (length && ([204, 205, 304].includes(response.status) ||
|
|
42
|
+
!(mediaType.startsWith("text/") || mediaType === "application/json" || /^application\/[a-z0-9.+-]+\+json$/.test(mediaType)))) fail();
|
|
43
|
+
const bytes = new Uint8Array(length);
|
|
44
|
+
let offset = 0;
|
|
45
|
+
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
|
|
46
|
+
let body;
|
|
47
|
+
try { body = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes); }
|
|
48
|
+
catch { fail(); }
|
|
49
|
+
return { status: response.status, headers: headers.filter(([name]) => name !== "content-encoding"), body };
|
|
50
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
|
|
3
|
+
import { ZhikeCliError } from "../../errors.js";
|
|
4
|
+
import { validateWorkflowStepDefinition } from "../workflow-steps/manifest.js";
|
|
5
|
+
import { validateWorkflowTriggerDefinition } from "../workflow-triggers/manifest.js";
|
|
6
|
+
|
|
7
|
+
const HANDLERS = Object.freeze({
|
|
8
|
+
step: Object.freeze({ execute: "defineWorkflowBlockExecute" }),
|
|
9
|
+
trigger: Object.freeze({
|
|
10
|
+
activate: "defineWorkflowBlockActivate",
|
|
11
|
+
trigger: "defineWorkflowBlockTrigger",
|
|
12
|
+
deactivate: "defineWorkflowBlockDeactivate",
|
|
13
|
+
}),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function invalid(path, detail) {
|
|
17
|
+
throw new ZhikeCliError("zhike_workflow_block_invalid", `${path}: ${detail}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function literal(node, path) {
|
|
21
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
22
|
+
if (ts.isNumericLiteral(node)) return Number(node.text);
|
|
23
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
24
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
25
|
+
if (node.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
26
|
+
if (ts.isArrayLiteralExpression(node)) return node.elements.map(item => literal(item, path));
|
|
27
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
28
|
+
const result = {};
|
|
29
|
+
for (const property of node.properties) {
|
|
30
|
+
if (!ts.isPropertyAssignment(property) ||
|
|
31
|
+
!(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))) {
|
|
32
|
+
invalid(path, "声明只能包含静态 JSON 属性");
|
|
33
|
+
}
|
|
34
|
+
const key = property.name.text;
|
|
35
|
+
if (Object.hasOwn(result, key)) invalid(path, `重复字段 ${key}`);
|
|
36
|
+
result[key] = literal(property.initializer, `${path}.${key}`);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
invalid(path, "声明必须是 JSON 字面量,不能读取变量或运行时代码");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function namedImports(sourceFile, moduleName = "@77-zhike/app-sdk/server") {
|
|
44
|
+
return new Set(sourceFile.statements.flatMap(statement =>
|
|
45
|
+
ts.isImportDeclaration(statement) && statement.moduleSpecifier.text === moduleName &&
|
|
46
|
+
statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings)
|
|
47
|
+
? statement.importClause.namedBindings.elements
|
|
48
|
+
.filter(item => !item.propertyName).map(item => item.name.text)
|
|
49
|
+
: []));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseDefinition(sourceFile, relativePath) {
|
|
53
|
+
if (!namedImports(sourceFile).has("defineWorkflowBlock")) {
|
|
54
|
+
invalid(relativePath, "block.ts 必须直接导入 defineWorkflowBlock");
|
|
55
|
+
}
|
|
56
|
+
const exports = sourceFile.statements.filter(statement =>
|
|
57
|
+
ts.isExportAssignment(statement) && !statement.isExportEquals);
|
|
58
|
+
if (exports.length !== 1 || !ts.isCallExpression(exports[0].expression)) {
|
|
59
|
+
invalid(relativePath, "block.ts 必须直接默认导出唯一的 defineWorkflowBlock({...})");
|
|
60
|
+
}
|
|
61
|
+
const call = exports[0].expression;
|
|
62
|
+
if (!ts.isIdentifier(call.expression) || call.expression.text !== "defineWorkflowBlock" ||
|
|
63
|
+
call.arguments.length !== 1) {
|
|
64
|
+
invalid(relativePath, "block.ts 必须直接默认导出 defineWorkflowBlock({...})");
|
|
65
|
+
}
|
|
66
|
+
return literal(call.arguments[0], relativePath);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function defaultBlockImport(sourceFile, relativePath) {
|
|
70
|
+
const imports = sourceFile.statements.filter(statement =>
|
|
71
|
+
ts.isImportDeclaration(statement) && ["./block", "./block.js"].includes(statement.moduleSpecifier.text) &&
|
|
72
|
+
statement.importClause?.name);
|
|
73
|
+
if (imports.length !== 1) invalid(relativePath, "处理器必须从 ./block.js 默认导入当前 Block");
|
|
74
|
+
return imports[0].importClause.name.text;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function validateHandler(sourceFile, relativePath, api) {
|
|
78
|
+
if (!namedImports(sourceFile).has(api)) {
|
|
79
|
+
invalid(relativePath, `必须直接导入 ${api}`);
|
|
80
|
+
}
|
|
81
|
+
const blockName = defaultBlockImport(sourceFile, relativePath);
|
|
82
|
+
const exports = sourceFile.statements.filter(statement =>
|
|
83
|
+
ts.isExportAssignment(statement) && !statement.isExportEquals);
|
|
84
|
+
if (exports.length !== 1 || !ts.isCallExpression(exports[0].expression)) {
|
|
85
|
+
invalid(relativePath, `必须直接默认导出 ${api}(block, async (...) => ...)`);
|
|
86
|
+
}
|
|
87
|
+
const call = exports[0].expression;
|
|
88
|
+
if (!ts.isIdentifier(call.expression) || call.expression.text !== api || call.arguments.length !== 2 ||
|
|
89
|
+
!ts.isIdentifier(call.arguments[0]) || call.arguments[0].text !== blockName ||
|
|
90
|
+
!(ts.isArrowFunction(call.arguments[1]) || ts.isFunctionExpression(call.arguments[1])) ||
|
|
91
|
+
!call.arguments[1].modifiers?.some(item => item.kind === ts.SyntaxKind.AsyncKeyword)) {
|
|
92
|
+
invalid(relativePath, `必须直接默认导出 ${api}(block, async (...) => ...)`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function discoverWorkflowBlock(sourceFile, relativePath, directoryKey, sourceFilesByPath) {
|
|
97
|
+
const value = parseDefinition(sourceFile, relativePath);
|
|
98
|
+
if (!["step", "trigger"].includes(value.type)) {
|
|
99
|
+
invalid(relativePath, 'type 必须是 "step" 或 "trigger"');
|
|
100
|
+
}
|
|
101
|
+
const handlers = HANDLERS[value.type];
|
|
102
|
+
const handlerPaths = {};
|
|
103
|
+
for (const [name, api] of Object.entries(handlers)) {
|
|
104
|
+
const handlerPath = `app/blocks/${directoryKey}/${name}.ts`;
|
|
105
|
+
const handlerSource = sourceFilesByPath.get(handlerPath);
|
|
106
|
+
if (!handlerSource) invalid(relativePath, `${value.type} Block 缺少 ${name}.ts`);
|
|
107
|
+
validateHandler(handlerSource, handlerPath, api);
|
|
108
|
+
handlerPaths[name] = handlerPath;
|
|
109
|
+
}
|
|
110
|
+
const unexpected = value.type === "step"
|
|
111
|
+
? ["activate", "trigger", "deactivate"].filter(name => sourceFilesByPath.has(`app/blocks/${directoryKey}/${name}.ts`))
|
|
112
|
+
: ["execute"].filter(name => sourceFilesByPath.has(`app/blocks/${directoryKey}/${name}.ts`));
|
|
113
|
+
if (unexpected.length > 0) invalid(relativePath, `${value.type} Block 不应包含 ${unexpected.join(", ")}.ts`);
|
|
114
|
+
|
|
115
|
+
const { type, ...definition } = value;
|
|
116
|
+
try {
|
|
117
|
+
if (type === "step") {
|
|
118
|
+
const step = validateWorkflowStepDefinition(definition, relativePath, directoryKey);
|
|
119
|
+
return Object.freeze({ ...step, type, handlerPaths: Object.freeze(handlerPaths) });
|
|
120
|
+
}
|
|
121
|
+
const trigger = validateWorkflowTriggerDefinition(definition, relativePath, directoryKey);
|
|
122
|
+
return Object.freeze({ ...trigger, type, handlerPaths: Object.freeze({
|
|
123
|
+
activate: handlerPaths.activate,
|
|
124
|
+
onEvent: handlerPaths.trigger,
|
|
125
|
+
deactivate: handlerPaths.deactivate,
|
|
126
|
+
}) });
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error?.code === "zhike_workflow_step_invalid" || error?.code === "zhike_workflow_trigger_invalid") {
|
|
129
|
+
invalid(relativePath, error.message);
|
|
130
|
+
}
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { ZhikeCliError } from "../../errors.js";
|
|
2
|
+
|
|
3
|
+
const ID = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
4
|
+
const FIELD = /^[a-z][a-zA-Z0-9_]{0,63}$/;
|
|
5
|
+
const DISPLAY = new Set(["title", "description", "examples"]);
|
|
6
|
+
|
|
7
|
+
function invalid(path, detail) {
|
|
8
|
+
throw new ZhikeCliError("zhike_workflow_step_invalid", `${path}: ${detail}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function object(value, path) {
|
|
12
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) invalid(path, "必须是对象");
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function exactKeys(value, allowed, path) {
|
|
17
|
+
for (const key of Object.keys(value)) {
|
|
18
|
+
if (!allowed.has(key)) invalid(path, `不支持字段 ${key}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function display(value, path) {
|
|
23
|
+
for (const key of ["title", "description"]) {
|
|
24
|
+
if (value[key] !== undefined && (typeof value[key] !== "string" || value[key].length > 300)) {
|
|
25
|
+
invalid(path, `${key} 无效`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (value.examples !== undefined && (
|
|
29
|
+
!Array.isArray(value.examples) || value.examples.length > 3 ||
|
|
30
|
+
value.examples.some(item => !["string", "number", "boolean"].includes(typeof item))
|
|
31
|
+
)) invalid(path, "examples 无效");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function schema(value, path) {
|
|
35
|
+
object(value, path);
|
|
36
|
+
exactKeys(value, new Set(["type", "properties", "required", "additionalProperties", ...DISPLAY]), path);
|
|
37
|
+
if (value.type !== "object" || value.additionalProperties !== false) invalid(path, "只支持封闭 object schema");
|
|
38
|
+
object(value.properties, `${path}.properties`);
|
|
39
|
+
const fields = Object.entries(value.properties);
|
|
40
|
+
if (fields.length > 32) invalid(path, "字段超过 32 个");
|
|
41
|
+
const required = value.required ?? [];
|
|
42
|
+
if (!Array.isArray(required) || new Set(required).size !== required.length ||
|
|
43
|
+
required.some(key => typeof key !== "string" || !Object.hasOwn(value.properties, key))) {
|
|
44
|
+
invalid(path, "required 必须引用唯一的已声明字段");
|
|
45
|
+
}
|
|
46
|
+
for (const [key, field] of fields) {
|
|
47
|
+
if (!FIELD.test(key)) invalid(path, `字段名 ${key} 无效`);
|
|
48
|
+
object(field, `${path}.${key}`);
|
|
49
|
+
const fieldPath = `${path}.properties.${key}`;
|
|
50
|
+
const allowed = field.type === "string"
|
|
51
|
+
? new Set(["type", "minLength", "enum", ...DISPLAY])
|
|
52
|
+
: new Set(["type", ...DISPLAY]);
|
|
53
|
+
exactKeys(field, allowed, fieldPath);
|
|
54
|
+
if (!["string", "number", "boolean"].includes(field.type)) invalid(fieldPath, "只支持 string/number/boolean");
|
|
55
|
+
if (field.minLength !== undefined && (!Number.isInteger(field.minLength) || field.minLength < 0 || field.minLength > 1024)) {
|
|
56
|
+
invalid(fieldPath, "minLength 无效");
|
|
57
|
+
}
|
|
58
|
+
if (field.enum !== undefined && (
|
|
59
|
+
!Array.isArray(field.enum) || field.enum.length < 1 || field.enum.length > 32 ||
|
|
60
|
+
field.enum.some(item => typeof item !== "string" || item.length > 300) ||
|
|
61
|
+
new Set(field.enum).size !== field.enum.length
|
|
62
|
+
)) invalid(fieldPath, "enum 无效");
|
|
63
|
+
display(field, fieldPath);
|
|
64
|
+
}
|
|
65
|
+
display(value, path);
|
|
66
|
+
if (Buffer.byteLength(JSON.stringify(value), "utf8") > 16 * 1024) invalid(path, "schema 超过 16 KiB");
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function validateWorkflowStepDefinition(value, relativePath, fileKey) {
|
|
71
|
+
object(value, relativePath);
|
|
72
|
+
exactKeys(value, new Set(["key", "title", "description", "connection", "configSchema", "outcomes"]), relativePath);
|
|
73
|
+
if (!ID.test(value.key) || value.key !== fileKey) invalid(relativePath, "Step key 必须与文件名一致");
|
|
74
|
+
if (typeof value.title !== "string" || value.title.length < 1 || value.title.length > 80) invalid(relativePath, "title 无效");
|
|
75
|
+
if (value.description !== undefined && (typeof value.description !== "string" || value.description.length > 300)) invalid(relativePath, "description 无效");
|
|
76
|
+
if (!["workspace", "none"].includes(value.connection)) invalid(relativePath, "connection 无效");
|
|
77
|
+
schema(value.configSchema, `${relativePath}.configSchema`);
|
|
78
|
+
if (!Array.isArray(value.outcomes) || value.outcomes.length < 1 || value.outcomes.length > 8) {
|
|
79
|
+
invalid(relativePath, "outcomes 必须包含 1–8 个静态出口");
|
|
80
|
+
}
|
|
81
|
+
const ids = new Set();
|
|
82
|
+
for (const [index, outcome] of value.outcomes.entries()) {
|
|
83
|
+
const path = `${relativePath}.outcomes[${index}]`;
|
|
84
|
+
object(outcome, path);
|
|
85
|
+
exactKeys(outcome, new Set(["id", "label", "dataSchema"]), path);
|
|
86
|
+
if (!ID.test(outcome.id) || ["in", "error"].includes(outcome.id) || ids.has(outcome.id)) {
|
|
87
|
+
invalid(path, "出口 ID 重复、保留或不合法");
|
|
88
|
+
}
|
|
89
|
+
ids.add(outcome.id);
|
|
90
|
+
if (typeof outcome.label !== "string" || outcome.label.length < 1 || outcome.label.length > 80) invalid(path, "label 无效");
|
|
91
|
+
if (outcome.dataSchema !== null) schema(outcome.dataSchema, `${path}.dataSchema`);
|
|
92
|
+
}
|
|
93
|
+
return Object.freeze({
|
|
94
|
+
...value,
|
|
95
|
+
artifact: "server",
|
|
96
|
+
entry: `steps/${value.key}`,
|
|
97
|
+
relativePath,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ZhikeCliError } from "../../errors.js";
|
|
2
|
+
import { validateWorkflowStepDefinition } from "../workflow-steps/manifest.js";
|
|
3
|
+
|
|
4
|
+
const HEADER = /^[a-z][a-z0-9-]{0,63}$/;
|
|
5
|
+
const BLOCKED_HEADERS = new Set(["authorization", "cookie", "set-cookie", "host", "content-length"]);
|
|
6
|
+
|
|
7
|
+
function invalid(path, detail) {
|
|
8
|
+
throw new ZhikeCliError("zhike_workflow_trigger_invalid", `${path}: ${detail}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function validateWorkflowTriggerDefinition(value, relativePath, fileKey) {
|
|
12
|
+
if (!value || typeof value !== "object" || Array.isArray(value) ||
|
|
13
|
+
Object.keys(value).some(key => !["key", "title", "description", "connection", "requestHeaders", "configSchema", "outcomes"].includes(key))) {
|
|
14
|
+
invalid(relativePath, "Trigger 声明字段无效");
|
|
15
|
+
}
|
|
16
|
+
let common;
|
|
17
|
+
try {
|
|
18
|
+
const { requestHeaders, ...stepShape } = value;
|
|
19
|
+
common = validateWorkflowStepDefinition(stepShape, relativePath, fileKey);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
invalid(relativePath, error.message || "schema 无效");
|
|
22
|
+
}
|
|
23
|
+
if (value.outcomes.some(outcome => outcome.dataSchema === null)) {
|
|
24
|
+
invalid(relativePath, "Trigger 的每个出口必须声明数据 schema");
|
|
25
|
+
}
|
|
26
|
+
const requestHeaders = value.requestHeaders ?? [];
|
|
27
|
+
if (!Array.isArray(requestHeaders) || requestHeaders.length > 16 ||
|
|
28
|
+
requestHeaders.some(name => typeof name !== "string" || !HEADER.test(name) ||
|
|
29
|
+
BLOCKED_HEADERS.has(name) || name.startsWith("x-forwarded-") || name.startsWith("proxy-")) ||
|
|
30
|
+
new Set(requestHeaders).size !== requestHeaders.length) {
|
|
31
|
+
invalid(relativePath, "requestHeaders 必须是至多 16 个唯一的安全小写名称");
|
|
32
|
+
}
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
key: common.key, title: common.title,
|
|
35
|
+
...(common.description === undefined ? {} : { description: common.description }),
|
|
36
|
+
connection: common.connection,
|
|
37
|
+
configSchema: common.configSchema,
|
|
38
|
+
outcomes: common.outcomes,
|
|
39
|
+
requestHeaders: Object.freeze([...requestHeaders]),
|
|
40
|
+
entries: Object.freeze({
|
|
41
|
+
activate: `triggers/${common.key}/activate`,
|
|
42
|
+
onEvent: `triggers/${common.key}/onEvent`,
|
|
43
|
+
deactivate: `triggers/${common.key}/deactivate`,
|
|
44
|
+
}),
|
|
45
|
+
relativePath,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// 此文件由 scripts/generate-components.mjs 生成,请修改产品能力声明。
|
|
2
|
+
export const componentTypes = Object.freeze(["NativeDOM","Icon","Stack","SplitLayout","SplitLayoutMain","SplitLayoutAside","SplitLayoutActions","Section","Banner","Badge","LoadingState","Skeleton","Card","Grid","EmptyState","ZhikeLink","Button","SearchInput","FilterSelect","Switch","LookupCell","Tabs","TabsList","TabsTrigger","TabsContent","Table","TableHeader","TableBody","TableRow","TableCell","Avatar","DescriptionList","DescriptionListItem","DescriptionListMenu","DescriptionListMenuItem","Divider","Chart","DashboardSurface","DashboardLayout","DashboardPanel","MetricReadout","AnalysisView","Json","Link","StatusBadge","TextBlock","TypographyTitle","TypographyBody","TypographyCaption","TableHeaderCell","WidgetTextWidget","WidgetTitle","WidgetTextPrimary","WidgetTextSecondary","WidgetLoading","WidgetDecoration"]);
|
|
3
|
+
export const publicComponentNames = Object.freeze(["NativeDOM","Icon","Stack","SplitLayout","SplitLayoutMain","SplitLayoutAside","SplitLayoutActions","Section","Banner","Badge","LoadingState","Skeleton","Card","Grid","EmptyState","ZhikeLink","Button","SearchInput","FilterSelect","Switch","LookupCell","Tabs","TabsList","TabsTrigger","TabsContent","Table","TableHeader","TableBody","TableRow","TableCell","Avatar","DescriptionList","DescriptionListItem","DescriptionListMenu","DescriptionListMenuItem","Divider","Chart","DashboardSurface","DashboardLayout","DashboardPanel","MetricReadout","AnalysisView","Json","Link","StatusBadge","TextBlock","TypographyTitle","TypographyBody","TypographyCaption","TableHeaderCell","WidgetTextWidget","WidgetTitle","WidgetTextPrimary","WidgetTextSecondary","WidgetLoading","WidgetDecoration"]);
|
|
4
|
+
export const legacyComponentTypes = Object.freeze(["NativeDOM","Text","Icon","Stack","SplitLayout","SplitLayoutMain","SplitLayoutAside","SplitLayoutActions","Section","Banner","Badge","LoadingState","Skeleton","Card","Grid","EmptyState","ZhikeLink","Button","SearchInput","FilterSelect","Switch","LookupCell","Tabs","TabsList","TabsTrigger","TabsContent","Table","TableHeader","TableBody","TableRow","TableCell","Avatar","DescriptionList","DescriptionListItem","DescriptionListMenu","DescriptionListMenuItem","Divider","Chart","DashboardSurface","DashboardLayout","DashboardPanel","MetricReadout","AnalysisView","Json","Link","StatusBadge","TextBlock","TypographyTitle","TypographyBody","TypographyCaption","TableHeaderCell","WidgetTextWidget","WidgetTitle","WidgetTextPrimary","WidgetTextSecondary","WidgetLoading","WidgetDecoration"]);
|
|
5
|
+
export const badgeColors = Object.freeze(["amber","blue","cyan","green","grey","lavender","lime","orange","pink","purple","red","yellow"]);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { componentTypes, legacyComponentTypes } from './components.generated.js';
|
|
2
|
+
import { useNormalizedOptions } from '../forms/normalize-options.js';
|
|
3
|
+
import { assertExternalUrl } from '../client/external-url.js';
|
|
4
|
+
import { assertBannerActions, assertJsonValue, assertChartData, assertChartSelection, assertAnalysisViewData } from './validation.js';
|
|
5
|
+
import { assertNativeStyles } from './native-dom.js';
|
|
6
|
+
|
|
7
|
+
export function createUiComponents(React, { legacy = false, useOwner = () => null, openExternal = () => Promise.reject(new Error('runtime_host_interaction_unavailable')) } = {}) {
|
|
8
|
+
const TabValue = React.createContext(null);
|
|
9
|
+
const components = Object.fromEntries((legacy ? legacyComponentTypes : componentTypes).map(type => [type, function RemoteComponent(props) {
|
|
10
|
+
if (type === 'NativeDOM' && props.styles !== undefined) assertNativeStyles(props.styles);
|
|
11
|
+
if (!legacy && type === 'TextBlock') return React.createElement(type, { ...props, align: props.align ?? 'left' });
|
|
12
|
+
if (!legacy && type === 'Badge' && props.tone !== undefined) throw new Error('Badge.tone 已移除,请使用 StatusBadge');
|
|
13
|
+
if (type === 'Avatar') {
|
|
14
|
+
const { src, ...rest } = props;
|
|
15
|
+
return React.createElement(type, { ...rest, ...(src == null ? {} : { src: assertExternalUrl(src) }) });
|
|
16
|
+
}
|
|
17
|
+
if (type === 'AnalysisView') { assertAnalysisViewData(props.asset, props.result); return React.createElement(type, props); }
|
|
18
|
+
if (type === 'Chart') { assertChartData(props.data); assertChartSelection(props); return React.createElement(type, props); }
|
|
19
|
+
if (type === 'Json') {
|
|
20
|
+
assertJsonValue(props.data);
|
|
21
|
+
return React.createElement(type, props);
|
|
22
|
+
}
|
|
23
|
+
if (type === 'Banner') {
|
|
24
|
+
const { actions, onAction, ...rest } = props;
|
|
25
|
+
if (onAction !== undefined || (actions !== undefined && !Array.isArray(actions))) throw new Error('runtime_ui_contract_invalid');
|
|
26
|
+
if (!actions?.length) return React.createElement(type, rest);
|
|
27
|
+
// 显式复制 metadata 和 handler;调用期间 App 原地修改数组不能重定向旧动作。
|
|
28
|
+
const data = actions.map(action => ({ label: action.label, variant: action.variant }));
|
|
29
|
+
const handlers = actions.map(action => action.onTrigger);
|
|
30
|
+
assertBannerActions(data);
|
|
31
|
+
if (handlers.some(handler => typeof handler !== 'function')) throw new Error('runtime_ui_contract_invalid');
|
|
32
|
+
return React.createElement(type, { ...rest, actions: data, onAction(index) {
|
|
33
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= handlers.length) throw new Error('runtime_ui_contract_invalid');
|
|
34
|
+
return handlers[index]();
|
|
35
|
+
} });
|
|
36
|
+
}
|
|
37
|
+
if (type === 'Link') {
|
|
38
|
+
const owner = useOwner();
|
|
39
|
+
const { href: input, onOpen, ...rest } = props;
|
|
40
|
+
if (onOpen !== undefined) throw new Error('runtime_ui_contract_invalid');
|
|
41
|
+
const href = assertExternalUrl(input);
|
|
42
|
+
return React.createElement(type, { ...rest, href, onOpen: () => openExternal(owner, href) });
|
|
43
|
+
}
|
|
44
|
+
if (type === 'Tabs') return React.createElement(TabValue.Provider, { value: props.value }, React.createElement(type, props));
|
|
45
|
+
if (type === 'TabsContent') {
|
|
46
|
+
const value = React.useContext(TabValue);
|
|
47
|
+
return value === props.value ? React.createElement(type, props) : null;
|
|
48
|
+
}
|
|
49
|
+
if (type === 'LookupCell') {
|
|
50
|
+
const { options, value, onCommit, ...rest } = props;
|
|
51
|
+
const normalized = useNormalizedOptions(React, options);
|
|
52
|
+
return React.createElement(type, { ...rest, ...normalized, value: value ?? '', onCommit });
|
|
53
|
+
}
|
|
54
|
+
return React.createElement(type, props);
|
|
55
|
+
}]));
|
|
56
|
+
Object.assign(components.DescriptionList, { Item: components.DescriptionListItem, Menu: components.DescriptionListMenu });
|
|
57
|
+
components.DescriptionListMenu.Item = components.DescriptionListMenuItem;
|
|
58
|
+
Object.assign(components.Table, { Header: components.TableHeader, HeaderCell: components.TableHeaderCell, Body: components.TableBody, Row: components.TableRow, Cell: components.TableCell });
|
|
59
|
+
Object.assign(components.TableCell, { Avatar: components.Avatar, Badge: components.Badge });
|
|
60
|
+
Object.assign(components.SplitLayout, { Main: components.SplitLayoutMain, Aside: components.SplitLayoutAside, Actions: components.SplitLayoutActions });
|
|
61
|
+
components.Typography = Object.freeze({ Title: components.TypographyTitle, Body: components.TypographyBody, Caption: components.TypographyCaption });
|
|
62
|
+
components.Widget = Object.freeze({
|
|
63
|
+
TextWidget: components.WidgetTextWidget,
|
|
64
|
+
Title: components.WidgetTitle,
|
|
65
|
+
Text: Object.freeze({ Primary: components.WidgetTextPrimary, Secondary: components.WidgetTextSecondary }),
|
|
66
|
+
Loading: components.WidgetLoading,
|
|
67
|
+
Decoration: components.WidgetDecoration,
|
|
68
|
+
});
|
|
69
|
+
return components;
|
|
70
|
+
}
|