@77-zhike/app-sdk 0.2.0-d7b1.4 → 0.2.0-d7b1.6
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 +27 -0
- package/package.json +1 -1
- package/src/cli/api-client.js +4 -0
- package/src/cli/index.js +11 -1
- package/src/cli/local-commands.js +2 -1
- package/src/cli/logs.js +56 -0
- package/src/cli/project-init.js +16 -1
- package/src/client/index.d.ts +5 -0
- package/src/client/index.js +1 -0
- package/src/client/interactions.d.ts +5 -3
- package/src/client/interactions.js +15 -2
- package/src/toolchain/real-build.js +4 -2
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# 智客 App SDK 与 CLI
|
|
2
|
+
|
|
3
|
+
## 统一构建
|
|
4
|
+
|
|
5
|
+
`zhike dev` 是监听、构建、上传和应用开发安装的快捷工作流,不是另一种运行时。
|
|
6
|
+
它与 `zhike build` 使用相同的 JSX 编译、压缩和 production 条件;仅额外携带源码映射。
|
|
7
|
+
修复前已上传的开发产物不会自动改变,升级 SDK 后须重新运行 dev 或重新构建上传。
|
|
8
|
+
|
|
9
|
+
## 查询历史日志
|
|
10
|
+
|
|
11
|
+
在包含 `.zhike/project.json` 的 App 项目目录中执行:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
zhike logs --workspace <workspace-id> --since 1h
|
|
15
|
+
zhike logs --workspace <workspace-id> --since 1d --level error --keyword timeout --json
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- 使用项目绑定的 App 和环境、现有 Developer CLI 登录身份;不会构建、上传或安装 App。
|
|
19
|
+
- 默认查询最近一小时,最多最近三天,与 Console 历史查询一致。
|
|
20
|
+
- `--since` 接受带时区的 ISO 时间或 `30m`、`1h`、`1d` 等相对时间;`--until` 接受带时区的 ISO 时间。
|
|
21
|
+
- `--level` 为 `log`、`warn` 或 `error`;`--version` 指定执行版本,`--keyword` 搜索日志正文。
|
|
22
|
+
- 每次读取一页,按从新到旧排列;`--limit` 为 1..100,默认 100。
|
|
23
|
+
- 下一页使用返回的 `next_cursor` 作为 `--cursor`,同时保持 Workspace、过滤条件及返回的 `from/to` 窗口不变。
|
|
24
|
+
- `--json` 输出单个 JSON 结果;失败进程返回非零状态。日志正文是不可信内容,不应当作命令执行。
|
|
25
|
+
- 服务端需要部署支持 `/api/developer/cli/apps/{app_id}/logs` 的 Developer Server;旧服务端不会自动回退到 Web 凭据或实时订阅。
|
|
26
|
+
|
|
27
|
+
`zhike dev` 的实时订阅仍是独立用途,七天断线补读窗口不代表历史查询可超过三天。
|
package/package.json
CHANGED
package/src/cli/api-client.js
CHANGED
|
@@ -46,6 +46,10 @@ export class DeveloperApiClient {
|
|
|
46
46
|
return this.request("/api/developer/organizations");
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
async queryAppLogs(appId, query) {
|
|
50
|
+
return this.request(`/api/developer/cli/apps/${encodeURIComponent(appId)}/logs?${new URLSearchParams(query)}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
49
53
|
async getAppBySlug(organizationId, slug) {
|
|
50
54
|
const path = `/api/developer/organizations/${encodeURIComponent(
|
|
51
55
|
organizationId,
|
package/src/cli/index.js
CHANGED
|
@@ -14,6 +14,8 @@ import { validateBundleUpload } from "./validate-bundle.js";
|
|
|
14
14
|
import { uploadAppBundle } from "./upload-bundle.js";
|
|
15
15
|
import { watchDevelopmentApp } from "./development-watch.js";
|
|
16
16
|
import { runLocalCommand } from "./local-commands.js";
|
|
17
|
+
import { readProjectBinding } from "../project-binding.js";
|
|
18
|
+
import { parseLogsArguments, queryAppLogs } from "./logs.js";
|
|
17
19
|
|
|
18
20
|
export async function runCli(
|
|
19
21
|
args,
|
|
@@ -43,7 +45,7 @@ export async function runCli(
|
|
|
43
45
|
: `${JSON.stringify(result.data, null, 2)}\n`);
|
|
44
46
|
return result;
|
|
45
47
|
}
|
|
46
|
-
if (rest.includes("--json")) {
|
|
48
|
+
if (command !== "logs" && rest.includes("--json")) {
|
|
47
49
|
throw new ZhikeCliError("zhike_json_not_supported", "此命令暂不支持 --json,请用 help --json 查询支持范围");
|
|
48
50
|
}
|
|
49
51
|
try {
|
|
@@ -68,6 +70,14 @@ export async function runCli(
|
|
|
68
70
|
};
|
|
69
71
|
return remoteContext;
|
|
70
72
|
};
|
|
73
|
+
if (command === "logs") {
|
|
74
|
+
const parsed = parseLogsArguments(rest);
|
|
75
|
+
const { config, store } = getRemoteContext();
|
|
76
|
+
const binding = await readProjectBinding(cwd, config.apiBaseUrl);
|
|
77
|
+
const api = await authenticate({ ...config, credentialStore: store, fetchImpl, openBrowser,
|
|
78
|
+
onProgress: parsed.json ? () => {} : onProgress });
|
|
79
|
+
return await queryAppLogs({ api, binding, ...parsed, output });
|
|
80
|
+
}
|
|
71
81
|
if (command === "login") {
|
|
72
82
|
_rejectArguments(rest);
|
|
73
83
|
const { config, store } = getRemoteContext();
|
|
@@ -16,6 +16,7 @@ export const commandHelp = {
|
|
|
16
16
|
build: "build",
|
|
17
17
|
upload: "upload",
|
|
18
18
|
dev: "dev [--workspace <workspace-id>]",
|
|
19
|
+
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
20
|
};
|
|
20
21
|
|
|
21
22
|
export function localEnvelope(data, status = "completed") {
|
|
@@ -30,7 +31,7 @@ export async function runLocalCommand(command, args, { cwd, environment }) {
|
|
|
30
31
|
throw new ZhikeCliError("zhike_help_usage_invalid", "用法:help [command] [--json]");
|
|
31
32
|
}
|
|
32
33
|
return localEnvelope({ commands: args.length ? { [args[0]]: commandHelp[args[0]] } : commandHelp,
|
|
33
|
-
jsonCommands: ["help", "version", "doctor"] });
|
|
34
|
+
jsonCommands: ["help", "version", "doctor", "logs"] });
|
|
34
35
|
}
|
|
35
36
|
if (args.length) throw new ZhikeCliError("zhike_arguments_invalid", "当前命令不接受额外参数");
|
|
36
37
|
if (command === "version") return localEnvelope({ package: metadata.name, version: metadata.version,
|
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
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { constants } from "node:fs";
|
|
11
11
|
import { basename, dirname, resolve } from "node:path";
|
|
12
12
|
|
|
13
|
-
import { TOOLING_VERSION } from "../config.js";
|
|
13
|
+
import { normalizeOrigin, TOOLING_VERSION } from "../config.js";
|
|
14
14
|
import { ZhikeCliError } from "../errors.js";
|
|
15
15
|
import { serializeProjectBinding } from "../project-binding.js";
|
|
16
16
|
|
|
@@ -91,6 +91,18 @@ export async function assertEmptyTarget(targetDirectory) {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
export async function scaffoldProject({ targetDirectory, binding }) {
|
|
94
|
+
const docsUrl = `${normalizeOrigin(binding.platformEnvironment, "platformEnvironment")}/docs/`;
|
|
95
|
+
const agentInstructions = `# 智客 App 开发
|
|
96
|
+
|
|
97
|
+
开始开发或修改此 App 前,先读取当前平台环境的开发文档:
|
|
98
|
+
|
|
99
|
+
- 文档索引:${docsUrl}llms.txt
|
|
100
|
+
- 开发文档:${docsUrl}
|
|
101
|
+
|
|
102
|
+
先读取文档索引,再按当前任务读取相关的 App SDK、CLI、扩展、连接和 API 文档;不要仅凭记忆猜测接口、参数或运行时能力。
|
|
103
|
+
索引中的相对链接以开发文档地址为基准解析。若无法访问文档,请明确说明访问失败,请用户提供相关文档,不要把猜测当作已确认能力。
|
|
104
|
+
项目绑定环境与 App 身份见 .zhike/project.json;切换平台环境后应同步更新上述文档地址。
|
|
105
|
+
`;
|
|
94
106
|
const temporaryDirectory = `${targetDirectory}.zhike-tmp-${randomBytes(6).toString("hex")}`;
|
|
95
107
|
try {
|
|
96
108
|
await mkdir(resolve(temporaryDirectory, ".zhike"), { recursive: true });
|
|
@@ -107,6 +119,9 @@ export async function scaffoldProject({ targetDirectory, binding }) {
|
|
|
107
119
|
await mkdir(resolve(temporaryDirectory, "src", "server", "functions"), {
|
|
108
120
|
recursive: true,
|
|
109
121
|
});
|
|
122
|
+
for (const filename of ["AGENTS.md", "CLAUDE.md"]) {
|
|
123
|
+
await writeFile(resolve(temporaryDirectory, filename), agentInstructions, { flag: "wx" });
|
|
124
|
+
}
|
|
110
125
|
await writeFile(
|
|
111
126
|
resolve(temporaryDirectory, ".zhike", "project.json"),
|
|
112
127
|
serializeProjectBinding(binding),
|
package/src/client/index.d.ts
CHANGED
|
@@ -50,3 +50,8 @@ export type { ToastOptions, ConfirmOptions } from './interactions.js';
|
|
|
50
50
|
export function showToast(options: import('./interactions.js').ToastOptions): Promise<void>;
|
|
51
51
|
/** 用户取消返回 false;所属页面关闭时拒绝,不能当作确认。 */
|
|
52
52
|
export function confirm(options: import('./interactions.js').ConfirmOptions): Promise<boolean>;
|
|
53
|
+
/**
|
|
54
|
+
* 由 Web Host 在新页签打开绝对 HTTPS URL;不返回窗口或远端加载状态。
|
|
55
|
+
* @platform Web
|
|
56
|
+
*/
|
|
57
|
+
export function openExternalUrl(url: string): Promise<void>;
|
package/src/client/index.js
CHANGED
|
@@ -20,3 +20,4 @@ export function defineFrontendExtension(definition) {
|
|
|
20
20
|
// 受管 Guest 注入实际请求出口;脱离前端运行环境不能调用产品交互。
|
|
21
21
|
export function showToast() { return Promise.reject(new Error('runtime_host_interaction_unavailable')); }
|
|
22
22
|
export function confirm() { return Promise.reject(new Error('runtime_host_interaction_unavailable')); }
|
|
23
|
+
export function openExternalUrl() { return Promise.reject(new Error('runtime_host_interaction_unavailable')); }
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export interface ToastOptions { message: string; tone?: 'info' | 'success' | 'error' }
|
|
2
2
|
export interface ConfirmOptions { title: string; description?: string; confirmLabel?: string; cancelLabel?: string }
|
|
3
|
-
export
|
|
4
|
-
export
|
|
5
|
-
export function
|
|
3
|
+
export interface OpenExternalUrlOptions { url: string }
|
|
4
|
+
export type HostInteraction = {operation:'showToast';input:ToastOptions} | {operation:'confirm';input:ConfirmOptions} | {operation:'openExternalUrl';input:OpenExternalUrlOptions};
|
|
5
|
+
export function assertHostInteraction(operation: unknown, input: unknown): Readonly<ToastOptions | ConfirmOptions | OpenExternalUrlOptions>;
|
|
6
|
+
export function createClientInteractions(invoke: (operation: string, input: ToastOptions | ConfirmOptions | OpenExternalUrlOptions) => Promise<unknown>): {
|
|
6
7
|
showToast(input: ToastOptions): Promise<void>;
|
|
7
8
|
confirm(input: ConfirmOptions): Promise<boolean>;
|
|
9
|
+
openExternalUrl(url: string): Promise<void>;
|
|
8
10
|
};
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
const fail = () => { throw new Error('runtime_interaction_invalid'); };
|
|
2
2
|
const text = (value, max, required = false) => value === undefined ? !required : typeof value === 'string' && value.length <= max && (!required || value.length > 0);
|
|
3
|
+
const externalUrl = value => {
|
|
4
|
+
if (!text(value,8192,true) || value.trim() !== value || /[\u0000-\u001f\u007f]/.test(value)) return false;
|
|
5
|
+
try {
|
|
6
|
+
const parsed = new URL(value);
|
|
7
|
+
return parsed.protocol === 'https:' && parsed.hostname.length > 0 && !parsed.username && !parsed.password;
|
|
8
|
+
} catch { return false; }
|
|
9
|
+
};
|
|
3
10
|
export function assertHostInteraction(operation, input) {
|
|
4
11
|
if (!input || typeof input !== 'object' || Array.isArray(input)) fail();
|
|
5
|
-
const allowed = operation === 'showToast' ? ['message','tone'] : operation === 'confirm' ? ['title','description','confirmLabel','cancelLabel'] : [];
|
|
12
|
+
const allowed = operation === 'showToast' ? ['message','tone'] : operation === 'confirm' ? ['title','description','confirmLabel','cancelLabel'] : operation === 'openExternalUrl' ? ['url'] : [];
|
|
6
13
|
if (!allowed.length || Object.keys(input).some(key=>!allowed.includes(key))) fail();
|
|
7
14
|
if (operation === 'showToast') {
|
|
8
15
|
if (!text(input.message,500,true) || (input.tone !== undefined && !['info','success','error'].includes(input.tone))) fail();
|
|
9
|
-
} else if (
|
|
16
|
+
} else if (operation === 'confirm') {
|
|
17
|
+
if (!text(input.title,200,true) || !text(input.description,2000) || !text(input.confirmLabel,60) || !text(input.cancelLabel,60)) fail();
|
|
18
|
+
} else if (!externalUrl(input.url)) fail();
|
|
10
19
|
return Object.freeze(Object.fromEntries(Object.entries(input).filter(([,value])=>value!==undefined)));
|
|
11
20
|
}
|
|
12
21
|
export function createClientInteractions(invoke) {
|
|
@@ -19,5 +28,9 @@ export function createClientInteractions(invoke) {
|
|
|
19
28
|
const request = assertHostInteraction('confirm',input);
|
|
20
29
|
return invoke('confirm',request).then(value=>{if(typeof value!=='boolean')fail();return value;});
|
|
21
30
|
},
|
|
31
|
+
openExternalUrl(url) {
|
|
32
|
+
const request = assertHostInteraction('openExternalUrl',{url});
|
|
33
|
+
return invoke('openExternalUrl',request).then(value=>{if(value!==null)fail();});
|
|
34
|
+
},
|
|
22
35
|
});
|
|
23
36
|
}
|
|
@@ -156,13 +156,15 @@ async function buildRemoteReactClient(projectDirectory, sources, development) {
|
|
|
156
156
|
`${JSON.stringify(item.key)}: extension${i}`).join(",");
|
|
157
157
|
const result = await build({
|
|
158
158
|
absWorkingDir: projectDirectory, bundle: true, charset: "utf8", format: "cjs",
|
|
159
|
-
legalComments: "none", logLevel: "silent", minify:
|
|
159
|
+
legalComments: "none", logLevel: "silent", minify: true, platform: "browser",
|
|
160
160
|
external: ["@77-zhike/app-sdk/runtime", "react", "react/jsx-runtime", "react/jsx-dev-runtime", "@77-zhike/app-sdk/client", "@77-zhike/app-sdk/ui", "@77-zhike/app-sdk/forms"],
|
|
161
161
|
plugins: [createSdkBuildPlugin(projectDirectory, sources, "remote-client")],
|
|
162
162
|
sourcemap: development ? "inline" : false,
|
|
163
163
|
banner: { js: 'export function createClientApp(platformModules) {\nconst module = { exports: {} };\nconst exports = module.exports;\nconst require = name => { if (!Object.hasOwn(platformModules, name)) throw new Error("runtime_module_unavailable"); return platformModules[name]; };\nconst Promise = require("@77-zhike/app-sdk/runtime").Promise;' },
|
|
164
164
|
footer: { js: 'return module.exports;\n}' },
|
|
165
|
-
|
|
165
|
+
// dev 只增加源码映射;编译语义与正式发布一致,不能依赖宿主的 React 开发构建。
|
|
166
|
+
jsx: "automatic", jsxDev: false,
|
|
167
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
166
168
|
stdin: { contents: `${imports}\nexport const extensions = Object.freeze({${entries}});`, loader: "tsx", resolveDir: projectDirectory, sourcefile: "zhike-client-entry.tsx" },
|
|
167
169
|
// 原生 await 不经过 Promise.then;降级后的 helper 必须使用工厂内受管 Promise。
|
|
168
170
|
target: "es2022", supported: { "async-await": false, "async-generator": false }, treeShaking: true, write: false,
|