@77-zhike/app-sdk 0.2.0-d7b1.2 → 0.2.0-d7b1.20
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 +281 -0
- package/package.json +17 -3
- package/src/cli/api-client.js +4 -0
- package/src/cli/browser-auth.js +64 -18
- package/src/cli/credential-store.js +21 -0
- package/src/cli/development-logs.js +5 -2
- package/src/cli/index.js +59 -42
- package/src/cli/local-commands.js +6 -3
- package/src/cli/logs.js +56 -0
- package/src/cli/project-init.js +111 -179
- package/src/cli/windows-credential-bridge.ps1 +131 -0
- package/src/cli/windows-credential-store.js +112 -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/documents.d.ts +21 -0
- package/src/client/documents.js +39 -0
- package/src/client/external-url.js +9 -0
- package/src/client/index.d.ts +225 -3
- package/src/client/index.js +75 -3
- package/src/client/interactions.d.ts +23 -3
- package/src/client/interactions.js +53 -3
- package/src/client/points.generated.js +56 -0
- package/src/config.js +11 -7
- package/src/forms/array-contract.d.ts +32 -0
- package/src/forms/array-contract.js +138 -0
- package/src/forms/components.generated.js +2 -2
- package/src/forms/factory.js +21 -7
- package/src/forms/field-array.d.ts +16 -0
- package/src/forms/field-array.js +52 -0
- package/src/forms/index.d.ts +74 -3
- package/src/forms/index.js +2 -0
- package/src/forms/options.d.ts +7 -2
- package/src/forms/options.js +8 -1
- package/src/forms/record-options.d.ts +28 -0
- package/src/forms/record-options.js +92 -0
- package/src/forms/schema.d.ts +73 -9
- 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/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/server/agents/index.d.ts +62 -0
- package/src/server/agents/index.js +40 -0
- package/src/server/index.d.ts +60 -5
- package/src/server/index.js +43 -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/agents/discovery.js +126 -0
- package/src/toolchain/agents/exposure.js +31 -0
- package/src/toolchain/agents/manifest.js +84 -0
- package/src/toolchain/ai-capabilities.js +69 -0
- package/src/toolchain/allowed-packages.json +7 -0
- package/src/toolchain/dependency-policy.js +181 -0
- package/src/toolchain/index.js +0 -88
- package/src/toolchain/real-build.js +135 -27
- package/src/toolchain/source-discovery.js +330 -81
- 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 +4 -1
- package/src/ui/conversation.d.ts +81 -0
- package/src/ui/conversation.js +82 -0
- package/src/ui/data-table-model.d.ts +14 -0
- package/src/ui/data-table-model.js +98 -0
- package/src/ui/data-table.d.ts +70 -0
- package/src/ui/data-table.js +74 -0
- package/src/ui/events.d.ts +10 -0
- package/src/ui/factory.js +87 -3
- package/src/ui/index.d.ts +351 -8
- package/src/ui/index.js +1 -1
- 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/scroll-viewport.d.ts +22 -0
- package/src/ui/scroll-viewport.js +23 -0
- package/src/ui/slider.d.ts +27 -0
- package/src/ui/slider.js +26 -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/text-selection.d.ts +22 -0
- package/src/ui/text-selection.js +23 -0
- package/src/ui/validation.js +86 -0
- package/src/cli/validate-bundle.js +0 -36
package/src/cli/index.js
CHANGED
|
@@ -2,18 +2,22 @@ import { resolve } from "node:path";
|
|
|
2
2
|
|
|
3
3
|
import { loadCliConfig } from "../config.js";
|
|
4
4
|
import { asSafeCliError, ZhikeCliError } from "../errors.js";
|
|
5
|
-
import { buildAppBundle
|
|
5
|
+
import { buildAppBundle } from "../toolchain/index.js";
|
|
6
6
|
import { authenticateCli, openSystemBrowser } from "./browser-auth.js";
|
|
7
7
|
import { createSystemCredentialStore } from "./credential-store.js";
|
|
8
8
|
import {
|
|
9
9
|
assertEmptyTarget,
|
|
10
|
+
assertExistingProjectTarget,
|
|
11
|
+
initializeExistingProject,
|
|
10
12
|
initializeProject,
|
|
13
|
+
organizationLabel,
|
|
11
14
|
validateAppSlug,
|
|
12
15
|
} from "./project-init.js";
|
|
13
|
-
import { validateBundleUpload } from "./validate-bundle.js";
|
|
14
16
|
import { uploadAppBundle } from "./upload-bundle.js";
|
|
15
17
|
import { watchDevelopmentApp } from "./development-watch.js";
|
|
16
18
|
import { runLocalCommand } from "./local-commands.js";
|
|
19
|
+
import { readProjectBinding } from "../project-binding.js";
|
|
20
|
+
import { parseLogsArguments, queryAppLogs } from "./logs.js";
|
|
17
21
|
|
|
18
22
|
export async function runCli(
|
|
19
23
|
args,
|
|
@@ -27,6 +31,7 @@ export async function runCli(
|
|
|
27
31
|
fetchImpl = globalThis.fetch,
|
|
28
32
|
openBrowser,
|
|
29
33
|
selectWorkspace,
|
|
34
|
+
selectOrganization,
|
|
30
35
|
isInteractive = Boolean(process.stdin.isTTY),
|
|
31
36
|
developmentRunner = watchDevelopmentApp,
|
|
32
37
|
} = {},
|
|
@@ -43,7 +48,7 @@ export async function runCli(
|
|
|
43
48
|
: `${JSON.stringify(result.data, null, 2)}\n`);
|
|
44
49
|
return result;
|
|
45
50
|
}
|
|
46
|
-
if (rest.includes("--json")) {
|
|
51
|
+
if (command !== "logs" && rest.includes("--json")) {
|
|
47
52
|
throw new ZhikeCliError("zhike_json_not_supported", "此命令暂不支持 --json,请用 help --json 查询支持范围");
|
|
48
53
|
}
|
|
49
54
|
try {
|
|
@@ -58,6 +63,16 @@ export async function runCli(
|
|
|
58
63
|
);
|
|
59
64
|
return result;
|
|
60
65
|
}
|
|
66
|
+
if (command === "validate") {
|
|
67
|
+
_rejectArguments(rest);
|
|
68
|
+
const result = await buildAppBundle({
|
|
69
|
+
projectDirectory: cwd,
|
|
70
|
+
});
|
|
71
|
+
output.write(
|
|
72
|
+
`真实 App 源码校验通过:${result.artifacts.length} Artifacts,Bundle ${result.sha256} (${result.size} bytes);未写入 dist\n`,
|
|
73
|
+
);
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
61
76
|
|
|
62
77
|
const onProgress = (message) => output.write(`${message}\n`);
|
|
63
78
|
let remoteContext = null;
|
|
@@ -68,6 +83,14 @@ export async function runCli(
|
|
|
68
83
|
};
|
|
69
84
|
return remoteContext;
|
|
70
85
|
};
|
|
86
|
+
if (command === "logs") {
|
|
87
|
+
const parsed = parseLogsArguments(rest);
|
|
88
|
+
const { config, store } = getRemoteContext();
|
|
89
|
+
const binding = await readProjectBinding(cwd, config.apiBaseUrl);
|
|
90
|
+
const api = await authenticate({ ...config, credentialStore: store, fetchImpl, openBrowser,
|
|
91
|
+
onProgress: parsed.json ? () => {} : onProgress });
|
|
92
|
+
return await queryAppLogs({ api, binding, ...parsed, output });
|
|
93
|
+
}
|
|
71
94
|
if (command === "login") {
|
|
72
95
|
_rejectArguments(rest);
|
|
73
96
|
const { config, store } = getRemoteContext();
|
|
@@ -83,15 +106,10 @@ export async function runCli(
|
|
|
83
106
|
return { command: "login" };
|
|
84
107
|
}
|
|
85
108
|
if (command === "init") {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
const appSlug = validateAppSlug(rest[0]);
|
|
93
|
-
const targetDirectory = resolve(cwd, appSlug);
|
|
94
|
-
await assertEmptyTarget(targetDirectory);
|
|
109
|
+
const { appSlug, here, requestedOrganizationId } = _parseInitArguments(rest);
|
|
110
|
+
const targetDirectory = here ? cwd : resolve(cwd, appSlug);
|
|
111
|
+
if (here) await assertExistingProjectTarget(targetDirectory);
|
|
112
|
+
else await assertEmptyTarget(targetDirectory);
|
|
95
113
|
const { config, store } = getRemoteContext();
|
|
96
114
|
const startedAt = Date.now();
|
|
97
115
|
const authenticatedApi = await authenticate({
|
|
@@ -100,19 +118,19 @@ export async function runCli(
|
|
|
100
118
|
fetchImpl,
|
|
101
119
|
openBrowser,
|
|
102
120
|
onProgress,
|
|
121
|
+
allowBrowserAuthorization: true,
|
|
103
122
|
});
|
|
104
123
|
output.write(
|
|
105
|
-
`Developer CLI 登录成功(${_formatElapsed(startedAt)}
|
|
124
|
+
`Developer CLI 登录成功(${_formatElapsed(startedAt)}),正在${here ? "绑定当前项目" : "创建 App 和本地项目"}...\n`,
|
|
106
125
|
);
|
|
107
|
-
const result =
|
|
108
|
-
targetDirectory,
|
|
109
|
-
|
|
110
|
-
apiBaseUrl: config.apiBaseUrl,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
});
|
|
126
|
+
const result = here
|
|
127
|
+
? await initializeExistingProject({ projectDirectory: targetDirectory, appSlug,
|
|
128
|
+
apiBaseUrl: config.apiBaseUrl, authenticatedApi, requestedOrganizationId, selectOrganization, isInteractive })
|
|
129
|
+
: await initializeProject({ targetDirectory, appSlug, apiBaseUrl: config.apiBaseUrl,
|
|
130
|
+
authenticatedApi, requestedOrganizationId, selectOrganization, isInteractive, ...(scaffold ? { scaffold } : {}) });
|
|
131
|
+
output.write(`开发者空间:${organizationLabel(result.organization)}\n`);
|
|
114
132
|
output.write(
|
|
115
|
-
`${result.created ? "已创建" : "已复用"} App ${result.app.slug},项目位于 ${appSlug}
|
|
133
|
+
`${result.created ? "已创建" : "已复用"} App ${result.app.slug},项目位于 ${here ? "当前目录" : `${appSlug}/`}(总耗时 ${_formatElapsed(startedAt)})\n`,
|
|
116
134
|
);
|
|
117
135
|
return result;
|
|
118
136
|
}
|
|
@@ -175,26 +193,6 @@ export async function runCli(
|
|
|
175
193
|
if (result && !ready) await onReady(result);
|
|
176
194
|
return result;
|
|
177
195
|
}
|
|
178
|
-
if (command === "experimental" && rest[0] === "validate-bundle") {
|
|
179
|
-
_rejectArguments(rest.slice(1));
|
|
180
|
-
const { config, store } = getRemoteContext();
|
|
181
|
-
const authenticatedApi = await authenticate({
|
|
182
|
-
...config,
|
|
183
|
-
credentialStore: store,
|
|
184
|
-
fetchImpl,
|
|
185
|
-
openBrowser,
|
|
186
|
-
onProgress,
|
|
187
|
-
});
|
|
188
|
-
const result = await validateBundleUpload({
|
|
189
|
-
projectDirectory: cwd,
|
|
190
|
-
apiBaseUrl: config.apiBaseUrl,
|
|
191
|
-
authenticatedApi,
|
|
192
|
-
});
|
|
193
|
-
output.write(
|
|
194
|
-
`${result.upload.status === "accepted" ? "Bundle 已受理" : "Bundle 校验失败"}:${result.upload.id}\n`,
|
|
195
|
-
);
|
|
196
|
-
return result;
|
|
197
|
-
}
|
|
198
196
|
throw new ZhikeCliError(
|
|
199
197
|
"zhike_command_unknown",
|
|
200
198
|
`未知命令:${command}`,
|
|
@@ -210,6 +208,26 @@ function _rejectArguments(args) {
|
|
|
210
208
|
}
|
|
211
209
|
}
|
|
212
210
|
|
|
211
|
+
function _parseInitArguments(args) {
|
|
212
|
+
let here = false;
|
|
213
|
+
let requestedOrganizationId = null;
|
|
214
|
+
let slug = null;
|
|
215
|
+
const invalid = () => new ZhikeCliError("zhike_init_usage_invalid",
|
|
216
|
+
"用法:zhike init <app-slug> [--here] [--organization <organization-id>]");
|
|
217
|
+
for (let index = 0; index < args.length; index++) {
|
|
218
|
+
const arg = args[index];
|
|
219
|
+
if (arg === "--here" && !here) here = true;
|
|
220
|
+
else if (arg === "--organization" && !requestedOrganizationId) {
|
|
221
|
+
const value = args[++index];
|
|
222
|
+
if (!value || !/^[0-9A-HJKMNP-TV-Z]{26}$/.test(value)) throw invalid();
|
|
223
|
+
requestedOrganizationId = value;
|
|
224
|
+
} else if (!arg.startsWith("-") && !slug) slug = arg;
|
|
225
|
+
else throw invalid();
|
|
226
|
+
}
|
|
227
|
+
if (!slug) throw invalid();
|
|
228
|
+
return { appSlug: validateAppSlug(slug), here, requestedOrganizationId };
|
|
229
|
+
}
|
|
230
|
+
|
|
213
231
|
function _parseDevArguments(args) {
|
|
214
232
|
if (args.length === 0) return null;
|
|
215
233
|
if (args.length === 2 && args[0] === "--workspace" && args[1]) {
|
|
@@ -249,5 +267,4 @@ export {
|
|
|
249
267
|
initializeProject,
|
|
250
268
|
scaffoldProject,
|
|
251
269
|
} from "./project-init.js";
|
|
252
|
-
export { validateBundleUpload } from "./validate-bundle.js";
|
|
253
270
|
export { uploadAppBundle } from "./upload-bundle.js";
|
|
@@ -5,6 +5,7 @@ import { resolve } from "node:path";
|
|
|
5
5
|
import { loadCliConfig } from "../config.js";
|
|
6
6
|
import { ZhikeCliError } from "../errors.js";
|
|
7
7
|
import { readProjectBinding } from "../project-binding.js";
|
|
8
|
+
import { inspectSystemCredentialStore } from "./credential-store.js";
|
|
8
9
|
|
|
9
10
|
const metadata = JSON.parse(await readFile(new URL("../../package.json", import.meta.url), "utf8"));
|
|
10
11
|
export const commandHelp = {
|
|
@@ -12,10 +13,12 @@ export const commandHelp = {
|
|
|
12
13
|
version: "version [--json]",
|
|
13
14
|
doctor: "doctor [--json]",
|
|
14
15
|
login: "login",
|
|
15
|
-
init: "init <app-slug>",
|
|
16
|
+
init: "init <app-slug> [--here] [--organization <organization-id>]",
|
|
17
|
+
validate: "validate",
|
|
16
18
|
build: "build",
|
|
17
19
|
upload: "upload",
|
|
18
20
|
dev: "dev [--workspace <workspace-id>]",
|
|
21
|
+
logs: "logs --workspace <id> [--since <time|1h|1d>] [--until <time>] [--level log|warn|error] [--keyword <text>] [--version <id>] [--limit 1..100] [--cursor <cursor>] [--json]",
|
|
19
22
|
};
|
|
20
23
|
|
|
21
24
|
export function localEnvelope(data, status = "completed") {
|
|
@@ -30,7 +33,7 @@ export async function runLocalCommand(command, args, { cwd, environment }) {
|
|
|
30
33
|
throw new ZhikeCliError("zhike_help_usage_invalid", "用法:help [command] [--json]");
|
|
31
34
|
}
|
|
32
35
|
return localEnvelope({ commands: args.length ? { [args[0]]: commandHelp[args[0]] } : commandHelp,
|
|
33
|
-
jsonCommands: ["help", "version", "doctor"] });
|
|
36
|
+
jsonCommands: ["help", "version", "doctor", "logs"] });
|
|
34
37
|
}
|
|
35
38
|
if (args.length) throw new ZhikeCliError("zhike_arguments_invalid", "当前命令不接受额外参数");
|
|
36
39
|
if (command === "version") return localEnvelope({ package: metadata.name, version: metadata.version,
|
|
@@ -70,7 +73,7 @@ export async function runLocalCommand(command, args, { cwd, environment }) {
|
|
|
70
73
|
} catch {
|
|
71
74
|
checks.push({ name: "package", ok: false, code: "package_missing_or_invalid" });
|
|
72
75
|
}
|
|
73
|
-
checks.push(
|
|
76
|
+
checks.push(await inspectSystemCredentialStore());
|
|
74
77
|
const result = localEnvelope({ localReady: checks.every((check) => check.ok),
|
|
75
78
|
authentication: "not_checked", installation: "not_checked", checks });
|
|
76
79
|
result.target = config ? { environment: config.apiBaseUrl, appId: binding?.appId ?? null } : null;
|
package/src/cli/logs.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ZhikeCliError } from "../errors.js";
|
|
2
|
+
import { formatAppLog } from "./development-logs.js";
|
|
3
|
+
|
|
4
|
+
const fields = { "--workspace": "workspace_id", "--since": "from", "--until": "to",
|
|
5
|
+
"--level": "level", "--keyword": "query", "--version": "app_version_id",
|
|
6
|
+
"--cursor": "cursor", "--limit": "limit" };
|
|
7
|
+
|
|
8
|
+
/** 查询一页历史日志;不订阅、不构建,也不改变安装状态。 */
|
|
9
|
+
export function parseLogsArguments(args, now = Date.now()) {
|
|
10
|
+
const query = {}, seen = new Set();
|
|
11
|
+
let json = false;
|
|
12
|
+
const invalid = () => { throw new ZhikeCliError("zhike_logs_usage_invalid", "用法:zhike logs --workspace <id> [--since <带时区时间或 1h/1d>] [--until <带时区时间>] [--level log|warn|error] [--keyword <关键词>] [--version <id>] [--limit 1..100] [--cursor <游标>] [--json];最多查询最近三天"); };
|
|
13
|
+
for (let i = 0; i < args.length; i++) {
|
|
14
|
+
const flag = args[i];
|
|
15
|
+
if (seen.has(flag)) invalid();
|
|
16
|
+
seen.add(flag);
|
|
17
|
+
if (flag === "--json") { json = true; continue; }
|
|
18
|
+
if (!Object.hasOwn(fields, flag) || !args[i + 1] || args[i + 1].startsWith("--")) invalid();
|
|
19
|
+
query[fields[flag]] = args[++i];
|
|
20
|
+
}
|
|
21
|
+
if (!query.workspace_id || query.workspace_id.length > 128) invalid();
|
|
22
|
+
if (query.level && !["log", "warn", "error"].includes(query.level)) invalid();
|
|
23
|
+
if (query.limit && (!/^\d+$/.test(query.limit) || +query.limit < 1 || +query.limit > 100)) invalid();
|
|
24
|
+
if ((query.query?.length ?? 0) > 200 || (query.cursor?.length ?? 0) > 180 || (query.app_version_id?.length ?? 0) > 128) invalid();
|
|
25
|
+
const timestamp = (value, relative) => {
|
|
26
|
+
const match = relative && /^(\d+)(m|h|d)$/.exec(value);
|
|
27
|
+
const time = match ? now - +match[1] * { m: 60_000, h: 3_600_000, d: 86_400_000 }[match[2]]
|
|
28
|
+
: /^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/.test(value) ? Date.parse(value) : NaN;
|
|
29
|
+
if (!Number.isFinite(time)) invalid();
|
|
30
|
+
return time;
|
|
31
|
+
};
|
|
32
|
+
const end = query.to ? timestamp(query.to, false) : now;
|
|
33
|
+
const start = query.from ? timestamp(query.from, true) : end - 3_600_000;
|
|
34
|
+
if (start >= end || start < now - 3 * 86_400_000 || end > now) invalid();
|
|
35
|
+
query.from = new Date(start).toISOString();
|
|
36
|
+
query.to = new Date(end).toISOString();
|
|
37
|
+
return { query, json };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function queryAppLogs({ api, binding, query, json, output }) {
|
|
41
|
+
const page = await api.queryAppLogs(binding.appId, query);
|
|
42
|
+
if (!page || !Array.isArray(page.items) || !(page.next_cursor === null || typeof page.next_cursor === "string")) {
|
|
43
|
+
throw new ZhikeCliError("zhike_logs_response_invalid", "日志响应格式错误,请检查服务端版本");
|
|
44
|
+
}
|
|
45
|
+
const result = { schemaVersion: 1, status: "completed",
|
|
46
|
+
target: { environment: binding.platformEnvironment, appId: binding.appId, workspaceId: query.workspace_id },
|
|
47
|
+
data: { ...page, from: query.from, to: query.to },
|
|
48
|
+
effects: { business: "none", authentication: "session" }, error: null, nextAction: null };
|
|
49
|
+
if (json) output.write(`${JSON.stringify(result)}\n`);
|
|
50
|
+
else {
|
|
51
|
+
for (const entry of page.items) output.write(`${formatAppLog(binding.appSlug, entry)}\n`);
|
|
52
|
+
if (!page.items.length) output.write("当前条件下没有日志。\n");
|
|
53
|
+
if (page.next_cursor) output.write(`还有更早日志:保持筛选条件并使用 --since ${query.from} --until ${query.to} --cursor ${JSON.stringify(page.next_cursor)}\n`);
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
package/src/cli/project-init.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
2
2
|
import {
|
|
3
3
|
access,
|
|
4
4
|
mkdir,
|
|
5
|
+
readFile,
|
|
5
6
|
rename,
|
|
6
7
|
rm,
|
|
7
8
|
stat,
|
|
@@ -9,8 +10,10 @@ import {
|
|
|
9
10
|
} from "node:fs/promises";
|
|
10
11
|
import { constants } from "node:fs";
|
|
11
12
|
import { basename, dirname, resolve } from "node:path";
|
|
13
|
+
import { createInterface } from "node:readline/promises";
|
|
14
|
+
import { stripVTControlCharacters } from "node:util";
|
|
12
15
|
|
|
13
|
-
import { TOOLING_VERSION } from "../config.js";
|
|
16
|
+
import { normalizeOrigin, TOOLING_VERSION } from "../config.js";
|
|
14
17
|
import { ZhikeCliError } from "../errors.js";
|
|
15
18
|
import { serializeProjectBinding } from "../project-binding.js";
|
|
16
19
|
|
|
@@ -20,21 +23,82 @@ export async function initializeProject({
|
|
|
20
23
|
apiBaseUrl,
|
|
21
24
|
authenticatedApi,
|
|
22
25
|
scaffold = scaffoldProject,
|
|
26
|
+
requestedOrganizationId = null,
|
|
27
|
+
selectOrganization,
|
|
28
|
+
isInteractive = Boolean(process.stdin.isTTY),
|
|
23
29
|
}) {
|
|
24
30
|
validateAppSlug(appSlug);
|
|
25
31
|
const target = resolve(targetDirectory);
|
|
26
32
|
await assertEmptyTarget(target);
|
|
27
33
|
|
|
34
|
+
const { app, organization, binding, created } = await resolveAppBinding({ appSlug, apiBaseUrl, authenticatedApi,
|
|
35
|
+
requestedOrganizationId, selectOrganization, isInteractive });
|
|
36
|
+
await scaffold({ targetDirectory: target, binding });
|
|
37
|
+
return { app, organization, binding, created, targetDirectory: target };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function initializeExistingProject({ projectDirectory, appSlug, apiBaseUrl, authenticatedApi,
|
|
41
|
+
requestedOrganizationId = null, selectOrganization, isInteractive = Boolean(process.stdin.isTTY) }) {
|
|
42
|
+
validateAppSlug(appSlug);
|
|
43
|
+
const target = resolve(projectDirectory);
|
|
44
|
+
await assertExistingProjectTarget(target);
|
|
45
|
+
const { app, organization, binding, created } = await resolveAppBinding({ appSlug, apiBaseUrl, authenticatedApi,
|
|
46
|
+
requestedOrganizationId, selectOrganization, isInteractive });
|
|
47
|
+
await mkdir(resolve(target, ".zhike"), { recursive: true });
|
|
48
|
+
await writeFile(resolve(target, ".zhike", "project.json"), serializeProjectBinding(binding), { flag: "wx" });
|
|
49
|
+
return { app, organization, binding, created, targetDirectory: target };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function assertExistingProjectTarget(projectDirectory) {
|
|
53
|
+
let projectPackage;
|
|
54
|
+
try {
|
|
55
|
+
projectPackage = JSON.parse(await readFile(resolve(projectDirectory, "package.json"), "utf8"));
|
|
56
|
+
const source = await stat(resolve(projectDirectory, "src"));
|
|
57
|
+
if (!source.isDirectory() || (!projectPackage?.devDependencies?.["@77-zhike/app-sdk"] &&
|
|
58
|
+
!projectPackage?.dependencies?.["@77-zhike/app-sdk"])) {
|
|
59
|
+
throw new Error("not an App project");
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
throw new ZhikeCliError("zhike_existing_project_invalid", "当前目录不是包含 SDK 和 src 的 App 项目");
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await stat(resolve(projectDirectory, ".zhike", "project.json"));
|
|
66
|
+
throw new ZhikeCliError("zhike_project_already_bound", "当前项目已有 App 绑定,不能重复初始化");
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error instanceof ZhikeCliError) throw error;
|
|
69
|
+
if (error?.code !== "ENOENT") {
|
|
70
|
+
throw new ZhikeCliError("zhike_project_binding_check_failed", "无法检查当前项目的 App 绑定", { cause: error });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function resolveAppBinding({ appSlug, apiBaseUrl, authenticatedApi,
|
|
76
|
+
requestedOrganizationId, selectOrganization, isInteractive }) {
|
|
28
77
|
const organizations = await authenticatedApi.listOrganizations();
|
|
29
|
-
if (!Array.isArray(organizations) || organizations.
|
|
78
|
+
if (!Array.isArray(organizations) || organizations.some((item) =>
|
|
79
|
+
!item || !/^[0-9A-HJKMNP-TV-Z]{26}$/.test(item.id) || typeof item.name !== "string" || !item.name.trim()) ||
|
|
80
|
+
new Set(organizations.map(item => item.id)).size !== organizations.length) {
|
|
81
|
+
throw new ZhikeCliError("zhike_organizations_response_invalid", "Developer Server 返回了无效的开发者空间列表");
|
|
82
|
+
}
|
|
83
|
+
if (organizations.length === 0) {
|
|
30
84
|
throw new ZhikeCliError(
|
|
31
85
|
"zhike_developer_organization_unavailable",
|
|
32
|
-
|
|
33
|
-
? "请先在 Developer Console 激活开发者组织"
|
|
34
|
-
: "当前账号存在多个开发者组织,D2-2 暂不支持隐式选择",
|
|
86
|
+
"请先在 Developer Console 激活开发者空间",
|
|
35
87
|
);
|
|
36
88
|
}
|
|
37
|
-
|
|
89
|
+
let selectedId = requestedOrganizationId;
|
|
90
|
+
if (!selectedId && organizations.length === 1) selectedId = organizations[0].id;
|
|
91
|
+
if (!selectedId) {
|
|
92
|
+
if (!isInteractive && !selectOrganization) {
|
|
93
|
+
throw new ZhikeCliError("zhike_organization_selection_required",
|
|
94
|
+
"当前账号有多个开发者空间,非交互环境必须指定 --organization <organization-id>");
|
|
95
|
+
}
|
|
96
|
+
selectedId = await (selectOrganization || promptForOrganization)(organizations);
|
|
97
|
+
}
|
|
98
|
+
const organization = organizations.find(item => item.id === selectedId);
|
|
99
|
+
if (!organization) {
|
|
100
|
+
throw new ZhikeCliError("zhike_organization_not_available", "未选择可访问的开发者空间,请使用列表中的空间 ID");
|
|
101
|
+
}
|
|
38
102
|
let app = await authenticatedApi.getAppBySlug(organization.id, appSlug);
|
|
39
103
|
let created = false;
|
|
40
104
|
if (!app) {
|
|
@@ -56,8 +120,27 @@ export async function initializeProject({
|
|
|
56
120
|
organizationId: organization.id,
|
|
57
121
|
appSlug: app.slug,
|
|
58
122
|
};
|
|
59
|
-
|
|
60
|
-
|
|
123
|
+
return { app, organization, binding, created };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function promptForOrganization(organizations, { input = process.stdin, output = process.stdout } = {}) {
|
|
127
|
+
const prompt = createInterface({ input, output });
|
|
128
|
+
try {
|
|
129
|
+
output.write("请选择 App 所属的开发者空间:\n");
|
|
130
|
+
organizations.forEach((organization, index) => {
|
|
131
|
+
output.write(` ${index + 1}. ${organizationLabel(organization)}\n`);
|
|
132
|
+
});
|
|
133
|
+
const answer = (await prompt.question("输入序号:")).trim();
|
|
134
|
+
const index = /^\d+$/.test(answer) ? Number(answer) - 1 : -1;
|
|
135
|
+
return organizations[index]?.id ?? "";
|
|
136
|
+
} finally {
|
|
137
|
+
prompt.close();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function organizationLabel(organization) {
|
|
142
|
+
const name = stripVTControlCharacters(organization.name).replace(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/g, " ").trim();
|
|
143
|
+
return `${name} (${organization.id})`;
|
|
61
144
|
}
|
|
62
145
|
|
|
63
146
|
export async function assertEmptyTarget(targetDirectory) {
|
|
@@ -91,11 +174,24 @@ export async function assertEmptyTarget(targetDirectory) {
|
|
|
91
174
|
}
|
|
92
175
|
|
|
93
176
|
export async function scaffoldProject({ targetDirectory, binding }) {
|
|
177
|
+
const docsUrl = `${normalizeOrigin(binding.platformEnvironment, "platformEnvironment")}/docs/`;
|
|
178
|
+
const agentInstructions = `# 智客 App 开发
|
|
179
|
+
|
|
180
|
+
开始开发或修改此 App 前,先读取当前平台环境的开发文档:
|
|
181
|
+
|
|
182
|
+
- 文档索引:${docsUrl}llms.txt
|
|
183
|
+
- 开发文档:${docsUrl}
|
|
184
|
+
|
|
185
|
+
先读取文档索引,再按当前任务读取相关的 App SDK、CLI、扩展、连接和 API 文档;不要仅凭记忆猜测接口、参数或运行时能力。
|
|
186
|
+
每使用一个复合 UI 或 Form 组件,必须打开该组件的专属文档,核对允许的直属子节点、嵌套顺序和入口限制。TypeScript、zhike validate、zhike build 或上传成功,不代表组件树已经符合运行时合同。
|
|
187
|
+
索引中的相对链接以开发文档地址为基准解析。若无法访问文档,请明确说明访问失败,请用户提供相关文档,不要把猜测当作已确认能力。
|
|
188
|
+
项目绑定环境与 App 身份见 .zhike/project.json;切换平台环境后应同步更新上述文档地址。
|
|
189
|
+
`;
|
|
94
190
|
const temporaryDirectory = `${targetDirectory}.zhike-tmp-${randomBytes(6).toString("hex")}`;
|
|
95
191
|
try {
|
|
96
192
|
await mkdir(resolve(temporaryDirectory, ".zhike"), { recursive: true });
|
|
97
193
|
await mkdir(
|
|
98
|
-
resolve(temporaryDirectory, "src", "app", "extensions"
|
|
194
|
+
resolve(temporaryDirectory, "src", "app", "extensions"),
|
|
99
195
|
{ recursive: true },
|
|
100
196
|
);
|
|
101
197
|
await mkdir(resolve(temporaryDirectory, "src", "app", "webhooks"), {
|
|
@@ -107,6 +203,9 @@ export async function scaffoldProject({ targetDirectory, binding }) {
|
|
|
107
203
|
await mkdir(resolve(temporaryDirectory, "src", "server", "functions"), {
|
|
108
204
|
recursive: true,
|
|
109
205
|
});
|
|
206
|
+
for (const filename of ["AGENTS.md", "CLAUDE.md"]) {
|
|
207
|
+
await writeFile(resolve(temporaryDirectory, filename), agentInstructions, { flag: "wx" });
|
|
208
|
+
}
|
|
110
209
|
await writeFile(
|
|
111
210
|
resolve(temporaryDirectory, ".zhike", "project.json"),
|
|
112
211
|
serializeProjectBinding(binding),
|
|
@@ -122,10 +221,10 @@ export async function scaffoldProject({ targetDirectory, binding }) {
|
|
|
122
221
|
type: "module",
|
|
123
222
|
scripts: {
|
|
124
223
|
zhike: "zhike",
|
|
224
|
+
validate: "zhike validate",
|
|
125
225
|
build: "zhike build",
|
|
126
226
|
dev: "zhike dev",
|
|
127
227
|
upload: "zhike upload",
|
|
128
|
-
"validate:mock": "zhike experimental validate-bundle",
|
|
129
228
|
},
|
|
130
229
|
devDependencies: {
|
|
131
230
|
"@77-zhike/app-sdk": TOOLING_VERSION,
|
|
@@ -136,181 +235,14 @@ export async function scaffoldProject({ targetDirectory, binding }) {
|
|
|
136
235
|
)}\n`,
|
|
137
236
|
{ flag: "wx" },
|
|
138
237
|
);
|
|
139
|
-
await writeFile(
|
|
140
|
-
resolve(
|
|
141
|
-
temporaryDirectory,
|
|
142
|
-
"src",
|
|
143
|
-
"app",
|
|
144
|
-
"events",
|
|
145
|
-
"app-installed.event.ts",
|
|
146
|
-
),
|
|
147
|
-
`import { createWebhookHandler, ZHIKE_API_TOKEN, kv } from "@77-zhike/app-sdk/server";
|
|
148
|
-
|
|
149
|
-
export default async function appInstalled() {
|
|
150
|
-
const handler = await createWebhookHandler({ fileName: "qiqi-customer" });
|
|
151
|
-
const existing = await fetch("https://api-clm.77hub.com/v1/webhooks", {
|
|
152
|
-
headers: { authorization: \`Bearer \${ZHIKE_API_TOKEN}\` },
|
|
153
|
-
});
|
|
154
|
-
if (!existing.ok) throw new Error("webhook_list_failed");
|
|
155
|
-
const { items } = await existing.json() as { items: Array<{ subscriptions: Array<{ eventType: string }> }> };
|
|
156
|
-
const requiredEventTypes = ["record.created", "activity.updated"];
|
|
157
|
-
if (items.some((item) => requiredEventTypes.every((eventType) =>
|
|
158
|
-
item.subscriptions.some((subscription) => subscription.eventType === eventType)
|
|
159
|
-
))) return;
|
|
160
|
-
|
|
161
|
-
const response = await fetch("https://api-clm.77hub.com/v1/webhooks", {
|
|
162
|
-
method: "POST",
|
|
163
|
-
headers: {
|
|
164
|
-
authorization: \`Bearer \${ZHIKE_API_TOKEN}\`,
|
|
165
|
-
"content-type": "application/json",
|
|
166
|
-
},
|
|
167
|
-
body: JSON.stringify({
|
|
168
|
-
targetUrl: handler.url,
|
|
169
|
-
subscriptions: [{
|
|
170
|
-
eventType: "record.created",
|
|
171
|
-
filter: { field: "id.objectSlug", operator: "equals", value: "accounts" },
|
|
172
|
-
}, {
|
|
173
|
-
eventType: "activity.updated",
|
|
174
|
-
filter: null,
|
|
175
|
-
}],
|
|
176
|
-
}),
|
|
177
|
-
});
|
|
178
|
-
if (!response.ok) throw new Error("webhook_create_failed");
|
|
179
|
-
const created = await response.json() as { webhook: { id: string }; secret: string };
|
|
180
|
-
await kv.set(
|
|
181
|
-
\`webhook:\${created.webhook.id}\`,
|
|
182
|
-
JSON.stringify({ webhookId: created.webhook.id, secret: created.secret }),
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
`,
|
|
186
|
-
{ flag: "wx" },
|
|
187
|
-
);
|
|
188
|
-
await writeFile(
|
|
189
|
-
resolve(
|
|
190
|
-
temporaryDirectory,
|
|
191
|
-
"src",
|
|
192
|
-
"app",
|
|
193
|
-
"events",
|
|
194
|
-
"app-uninstalled.event.ts",
|
|
195
|
-
),
|
|
196
|
-
`import { deleteWebhookHandler, listWebhookHandlers, ZHIKE_API_TOKEN, kv } from "@77-zhike/app-sdk/server";\n\nexport default async function appUninstalled() {\n const response = await fetch("https://api-clm.77hub.com/v1/webhooks", {\n headers: { authorization: \`Bearer \${ZHIKE_API_TOKEN}\` },\n });\n if (!response.ok) throw new Error("webhook_list_failed");\n const { items } = await response.json() as { items: Array<{ id: string }> };\n for (const webhook of items) {\n const deleted = await fetch(\`https://api-clm.77hub.com/v1/webhooks/\${webhook.id}\`, {\n method: "DELETE",\n headers: { authorization: \`Bearer \${ZHIKE_API_TOKEN}\` },\n });\n if (deleted.status !== 204 && deleted.status !== 404) throw new Error("webhook_delete_failed");\n await kv.delete(\`webhook:\${webhook.id}\`);\n }\n for (const handler of await listWebhookHandlers()) {\n await deleteWebhookHandler(handler.id);\n }\n}\n`,
|
|
197
|
-
{ flag: "wx" },
|
|
198
|
-
);
|
|
199
|
-
await writeFile(
|
|
200
|
-
resolve(
|
|
201
|
-
temporaryDirectory,
|
|
202
|
-
"src",
|
|
203
|
-
"app",
|
|
204
|
-
"events",
|
|
205
|
-
"connection-added.event.ts",
|
|
206
|
-
),
|
|
207
|
-
`import { kv } from "@77-zhike/app-sdk/server";\nimport type { ConnectionAddedParams } from "@77-zhike/app-sdk/server";\n\nexport default async function connectionAdded({ connection }: ConnectionAddedParams) {\n const stateKey = \`connection:\${connection.id}:\${connection.generation}:initialized\`;\n if (await kv.get(stateKey)) return;\n // 在这里调用外部 OpenAPI 创建订阅,再持久化供应商返回的引用。\n await kv.set(stateKey, "initialized");\n}\n`,
|
|
208
|
-
{ flag: "wx" },
|
|
209
|
-
);
|
|
210
|
-
await writeFile(
|
|
211
|
-
resolve(
|
|
212
|
-
temporaryDirectory,
|
|
213
|
-
"src",
|
|
214
|
-
"app",
|
|
215
|
-
"events",
|
|
216
|
-
"connection-removed.event.ts",
|
|
217
|
-
),
|
|
218
|
-
`import { kv } from "@77-zhike/app-sdk/server";\nimport type { ConnectionRemovedParams } from "@77-zhike/app-sdk/server";\n\nexport default async function connectionRemoved({ connection }: ConnectionRemovedParams) {\n const stateKey = \`connection:\${connection.id}:\${connection.generation}:initialized\`;\n // 在这里读取供应商引用并调用外部 OpenAPI 删除订阅。\n await kv.delete(stateKey);\n}\n`,
|
|
219
|
-
{ flag: "wx" },
|
|
220
|
-
);
|
|
221
|
-
await writeFile(
|
|
222
|
-
resolve(
|
|
223
|
-
temporaryDirectory,
|
|
224
|
-
"src",
|
|
225
|
-
"app",
|
|
226
|
-
"extensions",
|
|
227
|
-
"customer-action",
|
|
228
|
-
"extension.tsx",
|
|
229
|
-
),
|
|
230
|
-
`import { defineFrontendExtension } from "@77-zhike/app-sdk/client";\nimport createFollowUpTask from "../../../server/functions/create-follow-up-task.server";\n\nexport default defineFrontendExtension({\n key: "customer-action",\n type: "record-action",\n label: "创建跟进任务",\n icon: "refresh-cw",\n objects: "accounts",\n async onTrigger({ recordId }: { recordId: string }) {\n await createFollowUpTask({ customerId: recordId });\n },\n});\n`,
|
|
231
|
-
{ flag: "wx" },
|
|
232
|
-
);
|
|
233
|
-
await writeFile(
|
|
234
|
-
resolve(
|
|
235
|
-
temporaryDirectory,
|
|
236
|
-
"src",
|
|
237
|
-
"server",
|
|
238
|
-
"functions",
|
|
239
|
-
"create-follow-up-task.server.ts",
|
|
240
|
-
),
|
|
241
|
-
`import { ZHIKE_API_TOKEN } from "@77-zhike/app-sdk/server";
|
|
242
|
-
|
|
243
|
-
export default async function(input: { customerId: string }) {
|
|
244
|
-
const response = await fetch("https://api-clm.77hub.com/v1/tasks", {
|
|
245
|
-
method: "POST",
|
|
246
|
-
headers: {
|
|
247
|
-
authorization: \`Bearer \${ZHIKE_API_TOKEN}\`,
|
|
248
|
-
"content-type": "application/json",
|
|
249
|
-
},
|
|
250
|
-
body: JSON.stringify({
|
|
251
|
-
title: "跟进客户",
|
|
252
|
-
relatedRecords: [{ objectSlug: "accounts", recordId: input.customerId }],
|
|
253
|
-
}),
|
|
254
|
-
});
|
|
255
|
-
if (!response.ok) throw new Error("task_create_failed");
|
|
256
|
-
const task = await response.json() as { id: { taskId: string } };
|
|
257
|
-
return { taskId: task.id.taskId };
|
|
258
|
-
}
|
|
259
|
-
`,
|
|
260
|
-
{ flag: "wx" },
|
|
261
|
-
);
|
|
262
|
-
await writeFile(
|
|
263
|
-
resolve(
|
|
264
|
-
temporaryDirectory,
|
|
265
|
-
"src",
|
|
266
|
-
"app",
|
|
267
|
-
"webhooks",
|
|
268
|
-
"qiqi-customer.webhook.ts",
|
|
269
|
-
),
|
|
270
|
-
`import { ZHIKE_API_TOKEN, kv } from "@77-zhike/app-sdk/server";
|
|
271
|
-
import type { WorkspaceWebhookBody } from "@77-zhike/app-sdk/server";
|
|
272
|
-
|
|
273
|
-
export default async function qiqiCustomer(request: Request) {
|
|
274
|
-
const idempotencyKey = request.headers.get("idempotency-key");
|
|
275
|
-
if (!idempotencyKey) throw new Error("webhook_idempotency_key_missing");
|
|
276
|
-
if (await kv.get(\`delivery:\${idempotencyKey}\`)) {
|
|
277
|
-
return new Response(null, { status: 200 });
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const { events } = await request.json() as WorkspaceWebhookBody;
|
|
281
|
-
for (const event of events) {
|
|
282
|
-
if (event.eventType === "activity.updated") {
|
|
283
|
-
const response = await fetch(
|
|
284
|
-
\`https://api-clm.77hub.com/v1/activities/\${encodeURIComponent(event.id.activityId)}\`,
|
|
285
|
-
{ headers: { authorization: \`Bearer \${ZHIKE_API_TOKEN}\` } },
|
|
286
|
-
);
|
|
287
|
-
if (!response.ok) throw new Error("activity_read_failed");
|
|
288
|
-
const activity = await response.json();
|
|
289
|
-
console.log("[qiqi-customer] current activity", activity);
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
if (event.eventType !== "record.created" || event.id.objectSlug !== "accounts") continue;
|
|
293
|
-
const response = await fetch(
|
|
294
|
-
\`https://api-clm.77hub.com/v1/objects/\${event.id.objectSlug}/records/\${event.id.recordId}\`,
|
|
295
|
-
{ headers: { authorization: \`Bearer \${ZHIKE_API_TOKEN}\` } },
|
|
296
|
-
);
|
|
297
|
-
if (!response.ok) throw new Error("record_read_failed");
|
|
298
|
-
const record = await response.json();
|
|
299
|
-
console.log("[qiqi-customer] current record values", record.values);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
await kv.set(\`delivery:\${idempotencyKey}\`, "completed", { ttlInSeconds: 172800 });
|
|
303
|
-
return new Response(null, { status: 200 });
|
|
304
|
-
}
|
|
305
|
-
`,
|
|
306
|
-
{ flag: "wx" },
|
|
307
|
-
);
|
|
308
238
|
await writeFile(
|
|
309
239
|
resolve(temporaryDirectory, "tsconfig.json"),
|
|
310
240
|
`${JSON.stringify(
|
|
311
241
|
{
|
|
312
242
|
compilerOptions: {
|
|
313
243
|
lib: ["ES2022", "DOM"],
|
|
244
|
+
jsx: "react-jsx",
|
|
245
|
+
jsxImportSource: "@77-zhike/app-sdk",
|
|
314
246
|
module: "ESNext",
|
|
315
247
|
moduleResolution: "Bundler",
|
|
316
248
|
noEmit: true,
|