@77-zhike/app-sdk 0.2.0-d7b1.18 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@77-zhike/app-sdk",
3
- "version": "0.2.0-d7b1.18",
3
+ "version": "0.2.0-d7b1.20",
4
4
  "description": "智客 Integration App 一期 SDK 与本地 Toolchain",
5
5
  "repository": {
6
6
  "type": "git",
@@ -67,5 +67,5 @@
67
67
  "optional": true
68
68
  }
69
69
  },
70
- "gitHead": "9494f3959fd49fe5dd4d2f500ab6d3c041c7db00"
70
+ "gitHead": "c7f1b793276cbde656dbd058731fcb8b51b95745"
71
71
  }
package/src/cli/index.js CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  assertExistingProjectTarget,
11
11
  initializeExistingProject,
12
12
  initializeProject,
13
+ organizationLabel,
13
14
  validateAppSlug,
14
15
  } from "./project-init.js";
15
16
  import { uploadAppBundle } from "./upload-bundle.js";
@@ -30,6 +31,7 @@ export async function runCli(
30
31
  fetchImpl = globalThis.fetch,
31
32
  openBrowser,
32
33
  selectWorkspace,
34
+ selectOrganization,
33
35
  isInteractive = Boolean(process.stdin.isTTY),
34
36
  developmentRunner = watchDevelopmentApp,
35
37
  } = {},
@@ -104,16 +106,7 @@ export async function runCli(
104
106
  return { command: "login" };
105
107
  }
106
108
  if (command === "init") {
107
- const here = rest.includes("--here");
108
- const slugArgs = rest.filter((arg) => arg !== "--here");
109
- if (slugArgs.length !== 1 || slugArgs[0].startsWith("-") ||
110
- rest.length !== (here ? 2 : 1)) {
111
- throw new ZhikeCliError(
112
- "zhike_init_usage_invalid",
113
- "用法:zhike init <app-slug> [--here]",
114
- );
115
- }
116
- const appSlug = validateAppSlug(slugArgs[0]);
109
+ const { appSlug, here, requestedOrganizationId } = _parseInitArguments(rest);
117
110
  const targetDirectory = here ? cwd : resolve(cwd, appSlug);
118
111
  if (here) await assertExistingProjectTarget(targetDirectory);
119
112
  else await assertEmptyTarget(targetDirectory);
@@ -132,9 +125,10 @@ export async function runCli(
132
125
  );
133
126
  const result = here
134
127
  ? await initializeExistingProject({ projectDirectory: targetDirectory, appSlug,
135
- apiBaseUrl: config.apiBaseUrl, authenticatedApi })
128
+ apiBaseUrl: config.apiBaseUrl, authenticatedApi, requestedOrganizationId, selectOrganization, isInteractive })
136
129
  : await initializeProject({ targetDirectory, appSlug, apiBaseUrl: config.apiBaseUrl,
137
- authenticatedApi, ...(scaffold ? { scaffold } : {}) });
130
+ authenticatedApi, requestedOrganizationId, selectOrganization, isInteractive, ...(scaffold ? { scaffold } : {}) });
131
+ output.write(`开发者空间:${organizationLabel(result.organization)}\n`);
138
132
  output.write(
139
133
  `${result.created ? "已创建" : "已复用"} App ${result.app.slug},项目位于 ${here ? "当前目录" : `${appSlug}/`}(总耗时 ${_formatElapsed(startedAt)})\n`,
140
134
  );
@@ -214,6 +208,26 @@ function _rejectArguments(args) {
214
208
  }
215
209
  }
216
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
+
217
231
  function _parseDevArguments(args) {
218
232
  if (args.length === 0) return null;
219
233
  if (args.length === 2 && args[0] === "--workspace" && args[1]) {
@@ -13,7 +13,7 @@ export const commandHelp = {
13
13
  version: "version [--json]",
14
14
  doctor: "doctor [--json]",
15
15
  login: "login",
16
- init: "init <app-slug> [--here]",
16
+ init: "init <app-slug> [--here] [--organization <organization-id>]",
17
17
  validate: "validate",
18
18
  build: "build",
19
19
  upload: "upload",
@@ -10,6 +10,8 @@ import {
10
10
  } from "node:fs/promises";
11
11
  import { constants } from "node:fs";
12
12
  import { basename, dirname, resolve } from "node:path";
13
+ import { createInterface } from "node:readline/promises";
14
+ import { stripVTControlCharacters } from "node:util";
13
15
 
14
16
  import { normalizeOrigin, TOOLING_VERSION } from "../config.js";
15
17
  import { ZhikeCliError } from "../errors.js";
@@ -21,21 +23,27 @@ export async function initializeProject({
21
23
  apiBaseUrl,
22
24
  authenticatedApi,
23
25
  scaffold = scaffoldProject,
26
+ requestedOrganizationId = null,
27
+ selectOrganization,
28
+ isInteractive = Boolean(process.stdin.isTTY),
24
29
  }) {
25
30
  validateAppSlug(appSlug);
26
31
  const target = resolve(targetDirectory);
27
32
  await assertEmptyTarget(target);
28
33
 
29
- const { app, organization, binding, created } = await resolveAppBinding({ appSlug, apiBaseUrl, authenticatedApi });
34
+ const { app, organization, binding, created } = await resolveAppBinding({ appSlug, apiBaseUrl, authenticatedApi,
35
+ requestedOrganizationId, selectOrganization, isInteractive });
30
36
  await scaffold({ targetDirectory: target, binding });
31
37
  return { app, organization, binding, created, targetDirectory: target };
32
38
  }
33
39
 
34
- export async function initializeExistingProject({ projectDirectory, appSlug, apiBaseUrl, authenticatedApi }) {
40
+ export async function initializeExistingProject({ projectDirectory, appSlug, apiBaseUrl, authenticatedApi,
41
+ requestedOrganizationId = null, selectOrganization, isInteractive = Boolean(process.stdin.isTTY) }) {
35
42
  validateAppSlug(appSlug);
36
43
  const target = resolve(projectDirectory);
37
44
  await assertExistingProjectTarget(target);
38
- const { app, organization, binding, created } = await resolveAppBinding({ appSlug, apiBaseUrl, authenticatedApi });
45
+ const { app, organization, binding, created } = await resolveAppBinding({ appSlug, apiBaseUrl, authenticatedApi,
46
+ requestedOrganizationId, selectOrganization, isInteractive });
39
47
  await mkdir(resolve(target, ".zhike"), { recursive: true });
40
48
  await writeFile(resolve(target, ".zhike", "project.json"), serializeProjectBinding(binding), { flag: "wx" });
41
49
  return { app, organization, binding, created, targetDirectory: target };
@@ -64,17 +72,33 @@ export async function assertExistingProjectTarget(projectDirectory) {
64
72
  }
65
73
  }
66
74
 
67
- async function resolveAppBinding({ appSlug, apiBaseUrl, authenticatedApi }) {
75
+ async function resolveAppBinding({ appSlug, apiBaseUrl, authenticatedApi,
76
+ requestedOrganizationId, selectOrganization, isInteractive }) {
68
77
  const organizations = await authenticatedApi.listOrganizations();
69
- 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) {
70
84
  throw new ZhikeCliError(
71
85
  "zhike_developer_organization_unavailable",
72
- organizations?.length === 0
73
- ? "请先在 Developer Console 激活开发者组织"
74
- : "当前账号存在多个开发者组织,D2-2 暂不支持隐式选择",
86
+ "请先在 Developer Console 激活开发者空间",
75
87
  );
76
88
  }
77
- 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
+ }
78
102
  let app = await authenticatedApi.getAppBySlug(organization.id, appSlug);
79
103
  let created = false;
80
104
  if (!app) {
@@ -99,6 +123,26 @@ async function resolveAppBinding({ appSlug, apiBaseUrl, authenticatedApi }) {
99
123
  return { app, organization, binding, created };
100
124
  }
101
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})`;
144
+ }
145
+
102
146
  export async function assertEmptyTarget(targetDirectory) {
103
147
  try {
104
148
  await stat(targetDirectory);
@@ -164,6 +164,8 @@ export interface PageDefinition {
164
164
  showInNavigation?: boolean;
165
165
  /** 默认显示宿主标题栏;false 时由 Page 自行提供标题。移动端仍保留返回导航。 */
166
166
  showHeader?: boolean;
167
+ /** 缺省 false,按内容自然滚动;true 由宿主提供扣除导航后的确定高度,内部滚动由 Page 布局负责。 */
168
+ fillHeight?: boolean;
167
169
  /** 纯 React Page component,只接收有界业务参数。 */
168
170
  Page: ComponentType<PageProps>;
169
171
  }
@@ -267,3 +269,6 @@ export type { RestrictedInputSchema, PreparePdfTemplateInput, OpenPdfDocumentInp
267
269
  export function preparePdfTemplate(input: import('./documents.js').PreparePdfTemplateInput): Promise<import('./documents.js').PreparedPdfTemplate | null>;
268
270
  /** Desktop/H5认证文件交付;started只表示已交浏览器,不证明磁盘保存。 */
269
271
  export function openPdfDocument(input: import('./documents.js').OpenPdfDocumentInput): Promise<import('./documents.js').OpenPdfDocumentResult>;
272
+
273
+ /** 以当前成员权限打开稳定报告产物;宿主控制预览,不执行App提供的HTML。 */
274
+ export function openArtifact(input: import('./interactions.js').OpenArtifactOptions): Promise<void>;
@@ -36,7 +36,7 @@ export function definePage(definition) {
36
36
  throw new TypeError("Page definition 必须是对象");
37
37
  }
38
38
  const keys = Object.keys(definition).sort();
39
- const allowed = new Set(["Page", "name", "icon", "order", "variant", "showInNavigation", "showHeader"]);
39
+ const allowed = new Set(["Page", "name", "icon", "order", "variant", "showInNavigation", "showHeader", "fillHeight"]);
40
40
  if (keys.some(key => !allowed.has(key))) {
41
41
  throw new TypeError("Page definition 包含不支持的字段");
42
42
  }
@@ -56,6 +56,9 @@ export function definePage(definition) {
56
56
  if (typeof definition.Page !== "function") {
57
57
  throw new TypeError("Page definition 必须提供 React Page component");
58
58
  }
59
+ if (definition.fillHeight !== undefined && typeof definition.fillHeight !== 'boolean') {
60
+ throw new TypeError('Page fillHeight 必须是布尔值');
61
+ }
59
62
  if (definition.showHeader !== undefined && typeof definition.showHeader !== 'boolean') {
60
63
  throw new TypeError('Page showHeader 必须是布尔值');
61
64
  }
@@ -70,6 +73,7 @@ export function definePage(definition) {
70
73
  type: "page",
71
74
  variant: definition.variant,
72
75
  ...(definition.showHeader === undefined ? {} : { showHeader: definition.showHeader }),
76
+ ...(definition.fillHeight === undefined ? {} : { fillHeight: definition.fillHeight }),
73
77
  ...(definition.showInNavigation === undefined ? {} : { showInNavigation: definition.showInNavigation }),
74
78
  });
75
79
  }
@@ -86,3 +90,5 @@ export function queryAnalysis() { return Promise.reject(new Error('runtime_analy
86
90
  export function openAssistant() { return Promise.reject(new Error('runtime_host_interaction_unavailable')); }
87
91
  export function preparePdfTemplate() { return Promise.reject(new Error('runtime_document_unsupported')); }
88
92
  export function openPdfDocument() { return Promise.reject(new Error('runtime_document_unsupported')); }
93
+
94
+ export function openArtifact() { return Promise.reject(new Error('runtime_host_interaction_unavailable')); }
@@ -1,3 +1,7 @@
1
+ export interface OpenArtifactOptions {
2
+ /** 私有报告的稳定ID;不承载身份或授予读取权限。 */
3
+ artifactId: string;
4
+ }
1
5
  export interface OpenAssistantOptions {
2
6
  /** App 内的场景名;同 App/场景跨页面续聊,Host 自动拼接 App 前缀。 */
3
7
  scenario: string;
@@ -11,9 +15,10 @@ export interface ToastOptions { message: string; tone?: 'info' | 'success' | 'er
11
15
  export interface ConfirmOptions { title: string; description?: string; confirmLabel?: string; cancelLabel?: string }
12
16
  export interface OpenExternalUrlOptions { url: string }
13
17
  export interface NavigateOptions { destination: import('./destinations.js').AppDestination }
14
- export type HostInteraction = {operation:'preparePdfTemplate';input:import('./documents.js').PreparePdfTemplateInput} | {operation:'openPdfDocument';input:import('./documents.js').OpenPdfDocumentInput} | {operation:'openAssistant';input:OpenAssistantOptions} | {operation:'showToast';input:ToastOptions} | {operation:'confirm';input:ConfirmOptions} | {operation:'navigate';input:NavigateOptions} | {operation:'openExternal';input:OpenExternalOptions} | {operation:'openExternalUrl';input:OpenExternalUrlOptions};
15
- export function assertHostInteraction(operation: unknown, input: unknown): Readonly<ToastOptions | ConfirmOptions | NavigateOptions | OpenExternalOptions | OpenExternalUrlOptions | OpenAssistantOptions>;
16
- export function createClientInteractions(invoke: (operation: string, input: ToastOptions | ConfirmOptions | NavigateOptions | OpenExternalOptions | OpenExternalUrlOptions | OpenAssistantOptions) => Promise<unknown>): {
18
+ export type HostInteraction = {operation:'openArtifact';input:OpenArtifactOptions} | {operation:'preparePdfTemplate';input:import('./documents.js').PreparePdfTemplateInput} | {operation:'openPdfDocument';input:import('./documents.js').OpenPdfDocumentInput} | {operation:'openAssistant';input:OpenAssistantOptions} | {operation:'showToast';input:ToastOptions} | {operation:'confirm';input:ConfirmOptions} | {operation:'navigate';input:NavigateOptions} | {operation:'openExternal';input:OpenExternalOptions} | {operation:'openExternalUrl';input:OpenExternalUrlOptions};
19
+ export function assertHostInteraction(operation: unknown, input: unknown): Readonly<OpenArtifactOptions | ToastOptions | ConfirmOptions | NavigateOptions | OpenExternalOptions | OpenExternalUrlOptions | OpenAssistantOptions>;
20
+ export function createClientInteractions(invoke: (operation: string, input: OpenArtifactOptions | ToastOptions | ConfirmOptions | NavigateOptions | OpenExternalOptions | OpenExternalUrlOptions | OpenAssistantOptions) => Promise<unknown>): {
21
+ openArtifact(input: OpenArtifactOptions): Promise<void>;
17
22
  openAssistant(input: OpenAssistantOptions): Promise<void>;
18
23
  showToast(input: ToastOptions): Promise<void>;
19
24
  confirm(input: ConfirmOptions): Promise<boolean>;
@@ -12,6 +12,11 @@ const externalUrl = value => {
12
12
  } catch { return false; }
13
13
  };
14
14
  export function assertHostInteraction(operation, input) {
15
+ if (operation === 'openArtifact') {
16
+ if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).length !== 1
17
+ || typeof input.artifactId !== 'string' || !/^[A-Za-z0-9]{26}$/.test(input.artifactId)) fail();
18
+ return Object.freeze({ artifactId: input.artifactId });
19
+ }
15
20
  if (['preparePdfTemplate', 'openPdfDocument'].includes(operation)) return assertDocumentInput(operation, input);
16
21
  if (!input || typeof input !== 'object' || Array.isArray(input)) fail();
17
22
  const allowed = operation === 'openAssistant' ? ['scenario','context','title'] : operation === 'showToast' ? ['message','tone'] : operation === 'confirm' ? ['title','description','confirmLabel','cancelLabel'] : operation === 'navigate' ? ['destination'] : operation === 'openExternal' ? ['href'] : operation === 'openExternalUrl' ? ['url'] : [];
@@ -36,6 +41,10 @@ export function assertHostInteraction(operation, input) {
36
41
  }
37
42
  export function createClientInteractions(invoke) {
38
43
  return Object.freeze({
44
+ openArtifact(input) {
45
+ const request = assertHostInteraction('openArtifact',input);
46
+ return invoke('openArtifact',request).then(value=>{if(value!==null)fail();});
47
+ },
39
48
  openAssistant(input) {
40
49
  const request = assertHostInteraction('openAssistant',input);
41
50
  return invoke('openAssistant',request).then(value=>{if(value!==null)fail();});
@@ -392,7 +392,7 @@ function createManifest({ clientBytes, serverBytes, sources }) {
392
392
  }
393
393
  : {}),
394
394
  },
395
- frontendExtensions: sources.frontendExtensions.map(({ key, type, entry, label, icon, objects, name, order, variant, showInNavigation, showHeader }) => ({
395
+ frontendExtensions: sources.frontendExtensions.map(({ key, type, entry, label, icon, objects, name, order, variant, showInNavigation, showHeader, fillHeight }) => ({
396
396
  artifact: "client",
397
397
  entry,
398
398
  key,
@@ -405,6 +405,7 @@ function createManifest({ clientBytes, serverBytes, sources }) {
405
405
  ...(variant === undefined ? {} : { variant }),
406
406
  ...(showInNavigation === undefined ? {} : { showInNavigation }),
407
407
  ...(showHeader === undefined ? {} : { showHeader }),
408
+ ...(fillHeight === undefined ? {} : { fillHeight }),
408
409
  })),
409
410
  lifecycleEvents: sources.lifecycleEvents.map(({ event, key }) => ({
410
411
  artifact: "server",
@@ -308,7 +308,7 @@ function parseWorkspacePage(sourceFile, relativePath, slug) {
308
308
  const object = requireObjectLiteral(call.arguments[0], relativePath);
309
309
  assertOnlyProperties(
310
310
  object,
311
- ["name", "icon", "order", "variant", "Page", "showInNavigation", "showHeader"],
311
+ ["name", "icon", "order", "variant", "Page", "showInNavigation", "showHeader", "fillHeight"],
312
312
  relativePath,
313
313
  "zhike_page_definition_invalid",
314
314
  "Page definition 包含不支持的字段",
@@ -360,6 +360,15 @@ function parseWorkspacePage(sourceFile, relativePath, slug) {
360
360
  }
361
361
  showHeader = headerProperty.initializer.kind === ts.SyntaxKind.TrueKeyword;
362
362
  }
363
+ const fillHeightProperty = findProperty(object, "fillHeight");
364
+ let fillHeight;
365
+ if (fillHeightProperty !== undefined) {
366
+ if (!ts.isPropertyAssignment(fillHeightProperty)
367
+ || ![ts.SyntaxKind.TrueKeyword, ts.SyntaxKind.FalseKeyword].includes(fillHeightProperty.initializer.kind)) {
368
+ throw toolchainError("zhike_page_fill_height_invalid", `${relativePath}: fillHeight 必须是静态布尔值`);
369
+ }
370
+ fillHeight = fillHeightProperty.initializer.kind === ts.SyntaxKind.TrueKeyword;
371
+ }
363
372
  const pageProperty = findProperty(object, "Page");
364
373
  if (
365
374
  !pageProperty ||
@@ -381,6 +390,7 @@ function parseWorkspacePage(sourceFile, relativePath, slug) {
381
390
  ...(order === undefined ? {} : { order }),
382
391
  ...(showInNavigation === undefined ? {} : { showInNavigation }),
383
392
  ...(showHeader === undefined ? {} : { showHeader }),
393
+ ...(fillHeight === undefined ? {} : { fillHeight }),
384
394
  variant,
385
395
  });
386
396
  }