@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.
Files changed (96) hide show
  1. package/README.md +281 -0
  2. package/package.json +17 -3
  3. package/src/cli/api-client.js +4 -0
  4. package/src/cli/browser-auth.js +64 -18
  5. package/src/cli/credential-store.js +21 -0
  6. package/src/cli/development-logs.js +5 -2
  7. package/src/cli/index.js +59 -42
  8. package/src/cli/local-commands.js +6 -3
  9. package/src/cli/logs.js +56 -0
  10. package/src/cli/project-init.js +111 -179
  11. package/src/cli/windows-credential-bridge.ps1 +131 -0
  12. package/src/cli/windows-credential-store.js +112 -0
  13. package/src/client/analysis.d.ts +4 -0
  14. package/src/client/analysis.js +12 -0
  15. package/src/client/destinations.d.ts +25 -0
  16. package/src/client/destinations.js +70 -0
  17. package/src/client/documents.d.ts +21 -0
  18. package/src/client/documents.js +39 -0
  19. package/src/client/external-url.js +9 -0
  20. package/src/client/index.d.ts +225 -3
  21. package/src/client/index.js +75 -3
  22. package/src/client/interactions.d.ts +23 -3
  23. package/src/client/interactions.js +53 -3
  24. package/src/client/points.generated.js +56 -0
  25. package/src/config.js +11 -7
  26. package/src/forms/array-contract.d.ts +32 -0
  27. package/src/forms/array-contract.js +138 -0
  28. package/src/forms/components.generated.js +2 -2
  29. package/src/forms/factory.js +21 -7
  30. package/src/forms/field-array.d.ts +16 -0
  31. package/src/forms/field-array.js +52 -0
  32. package/src/forms/index.d.ts +74 -3
  33. package/src/forms/index.js +2 -0
  34. package/src/forms/options.d.ts +7 -2
  35. package/src/forms/options.js +8 -1
  36. package/src/forms/record-options.d.ts +28 -0
  37. package/src/forms/record-options.js +92 -0
  38. package/src/forms/schema.d.ts +73 -9
  39. package/src/forms/schema.js +128 -0
  40. package/src/forms/state.d.ts +30 -0
  41. package/src/forms/state.js +82 -0
  42. package/src/forms/validation.d.ts +9 -0
  43. package/src/forms/validation.js +81 -0
  44. package/src/forms/values.d.ts +5 -0
  45. package/src/forms/values.js +38 -0
  46. package/src/jsx-dev-runtime.d.ts +2 -0
  47. package/src/jsx-dev-runtime.js +1 -0
  48. package/src/jsx-runtime.d.ts +2 -0
  49. package/src/jsx-runtime.js +1 -0
  50. package/src/server/agents/index.d.ts +62 -0
  51. package/src/server/agents/index.js +40 -0
  52. package/src/server/index.d.ts +60 -5
  53. package/src/server/index.js +43 -0
  54. package/src/server/steps/index.d.ts +71 -0
  55. package/src/server/steps/index.js +15 -0
  56. package/src/server/triggers/index.d.ts +68 -0
  57. package/src/server/triggers/index.js +27 -0
  58. package/src/server/workflow-blocks/index.d.ts +72 -0
  59. package/src/server/workflow-blocks/index.js +32 -0
  60. package/src/toolchain/agents/discovery.js +126 -0
  61. package/src/toolchain/agents/exposure.js +31 -0
  62. package/src/toolchain/agents/manifest.js +84 -0
  63. package/src/toolchain/ai-capabilities.js +69 -0
  64. package/src/toolchain/allowed-packages.json +7 -0
  65. package/src/toolchain/dependency-policy.js +181 -0
  66. package/src/toolchain/index.js +0 -88
  67. package/src/toolchain/real-build.js +135 -27
  68. package/src/toolchain/source-discovery.js +330 -81
  69. package/src/toolchain/workflow-blocks/discovery.js +133 -0
  70. package/src/toolchain/workflow-steps/manifest.js +99 -0
  71. package/src/toolchain/workflow-triggers/manifest.js +47 -0
  72. package/src/ui/components.generated.js +4 -1
  73. package/src/ui/conversation.d.ts +81 -0
  74. package/src/ui/conversation.js +82 -0
  75. package/src/ui/data-table-model.d.ts +14 -0
  76. package/src/ui/data-table-model.js +98 -0
  77. package/src/ui/data-table.d.ts +70 -0
  78. package/src/ui/data-table.js +74 -0
  79. package/src/ui/events.d.ts +10 -0
  80. package/src/ui/factory.js +87 -3
  81. package/src/ui/index.d.ts +351 -8
  82. package/src/ui/index.js +1 -1
  83. package/src/ui/jsx.generated.d.ts +359 -0
  84. package/src/ui/native-dom.d.ts +49 -0
  85. package/src/ui/native-dom.js +44 -0
  86. package/src/ui/scroll-viewport.d.ts +22 -0
  87. package/src/ui/scroll-viewport.js +23 -0
  88. package/src/ui/slider.d.ts +27 -0
  89. package/src/ui/slider.js +26 -0
  90. package/src/ui/style.d.ts +5 -0
  91. package/src/ui/style.generated.js +2 -0
  92. package/src/ui/style.js +15 -0
  93. package/src/ui/text-selection.d.ts +22 -0
  94. package/src/ui/text-selection.js +23 -0
  95. package/src/ui/validation.js +86 -0
  96. 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, buildMockArtifact } from "../toolchain/index.js";
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
- if (rest.length !== 1 || rest[0].startsWith("-")) {
87
- throw new ZhikeCliError(
88
- "zhike_init_usage_invalid",
89
- "用法:zhike init <app-slug>",
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)}),正在创建 App 和本地项目...\n`,
124
+ `Developer CLI 登录成功(${_formatElapsed(startedAt)}),正在${here ? "绑定当前项目" : "创建 App 和本地项目"}...\n`,
106
125
  );
107
- const result = await initializeProject({
108
- targetDirectory,
109
- appSlug,
110
- apiBaseUrl: config.apiBaseUrl,
111
- authenticatedApi,
112
- ...(scaffold ? { scaffold } : {}),
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}/(总耗时 ${_formatElapsed(startedAt)})\n`,
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({ name: "secureStore", ok: process.platform === "darwin", platform: process.platform });
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;
@@ -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
+ }
@@ -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.length !== 1) {
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
- organizations?.length === 0
33
- ? "请先在 Developer Console 激活开发者组织"
34
- : "当前账号存在多个开发者组织,D2-2 暂不支持隐式选择",
86
+ "请先在 Developer Console 激活开发者空间",
35
87
  );
36
88
  }
37
- const organization = organizations[0];
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
- await scaffold({ targetDirectory: target, binding });
60
- return { app, organization, binding, created, targetDirectory: target };
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", "customer-action"),
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,