openxiangda 1.0.83 → 1.0.84
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
CHANGED
|
@@ -34,12 +34,15 @@ openxiangda data-view status ticket_with_customer --profile dev
|
|
|
34
34
|
openxiangda resource validate --profile dev
|
|
35
35
|
openxiangda resource plan --profile dev
|
|
36
36
|
openxiangda resource publish --profile dev
|
|
37
|
+
openxiangda runtime deploy --profile dev
|
|
37
38
|
openxiangda inspect app --profile dev --json
|
|
38
39
|
openxiangda app snapshot APP_XXXX --profile dev --json
|
|
39
40
|
```
|
|
40
41
|
|
|
41
42
|
User tokens are stored in `~/.openxiangda/profiles.json` with `0600` permissions. Shared workspace environment values, including `APP_OSS_*`, can live in `~/.openxiangda/.env` and are inherited by new workspaces. Project `.env` files still work and override the global defaults. Project state is stored in `.openxiangda/state.json` and contains only profile-specific resource IDs.
|
|
42
43
|
|
|
44
|
+
React SPA workspaces publish their frontend with `openxiangda runtime deploy`. The default upload mode is staged multipart: each `dist/` file is uploaded separately, then the CLI submits a release manifest and activates it unless `--no-activate` is passed. Upload progress, timeout, and `traceId` are written to stderr; `--json` stdout remains the final JSON result. Use `--upload-timeout-ms <ms>` or `OPENXIANGDA_RUNTIME_UPLOAD_TIMEOUT_MS` to tune runtime upload/finalize timeouts. Use `--upload-mode legacy-json` only as a compatibility fallback for older platforms.
|
|
45
|
+
|
|
43
46
|
Feedback can be sent to a DingTalk custom robot from the CLI. AI agents should proactively report platform defects, missing capabilities, unclear design rules, repeated workarounds, implementation uncertainty, and user-visible UX gaps during development. Store the robot settings in `~/.openxiangda/.env`, not in project files:
|
|
44
47
|
|
|
45
48
|
```env
|
package/lib/cli.js
CHANGED
|
@@ -35,6 +35,8 @@ const { version: CURRENT_VERSION } = require('../package.json');
|
|
|
35
35
|
|
|
36
36
|
const NPM_PACKAGE_NAME = 'openxiangda';
|
|
37
37
|
const OFFICIAL_NPM_REGISTRY = 'https://registry.npmjs.org';
|
|
38
|
+
const RUNTIME_UPLOAD_DEFAULT_TIMEOUT_MS = 120000;
|
|
39
|
+
const RUNTIME_UPLOAD_CONCURRENCY = 3;
|
|
38
40
|
|
|
39
41
|
async function main(argv) {
|
|
40
42
|
const [command, ...rest] = argv;
|
|
@@ -114,7 +116,7 @@ Usage:
|
|
|
114
116
|
openxiangda permission form-group-list|form-group-create|form-group-bind
|
|
115
117
|
openxiangda settings get|save|indexes|indexes-save|data-management|data-management-save|public-access
|
|
116
118
|
openxiangda resource validate|plan|publish|pull [--profile name] [--json]
|
|
117
|
-
openxiangda runtime deploy [--profile name] [--dist dist] [--build-id id] [--no-build] [--no-activate] [--json]
|
|
119
|
+
openxiangda runtime deploy [--profile name] [--dist dist] [--build-id id] [--upload-mode staged|legacy-json] [--upload-timeout-ms ms] [--no-build] [--no-activate] [--json]
|
|
118
120
|
openxiangda runtime releases [--profile name] [--json]
|
|
119
121
|
openxiangda runtime activate <releaseId> [--profile name] [--json]
|
|
120
122
|
openxiangda inspect app|form|workflow|automation|permissions
|
|
@@ -2766,30 +2768,57 @@ async function runtime(args) {
|
|
|
2766
2768
|
const buildId = normalizeRuntimeBuildId(flags['build-id'] || createRuntimeBuildId());
|
|
2767
2769
|
const distDir = path.resolve(process.cwd(), flags.dist || 'dist');
|
|
2768
2770
|
const assetBaseUrl = buildRuntimeAssetBaseUrl(target.appType, buildId);
|
|
2771
|
+
const uploadMode = normalizeRuntimeUploadMode(flags['upload-mode']);
|
|
2772
|
+
const uploadTimeoutMs = normalizeRuntimeUploadTimeoutMs(flags['upload-timeout-ms']);
|
|
2773
|
+
const traceId = createRuntimeTraceId();
|
|
2769
2774
|
if (!flags['no-build']) {
|
|
2770
2775
|
runRuntimeBuild({
|
|
2771
2776
|
buildId,
|
|
2772
2777
|
appType: target.appType,
|
|
2773
2778
|
assetBaseUrl,
|
|
2774
2779
|
command: flags['build-command'],
|
|
2780
|
+
jsonOutput: Boolean(flags.json),
|
|
2775
2781
|
});
|
|
2776
2782
|
}
|
|
2777
2783
|
const files = collectRuntimeDistFiles(distDir, {
|
|
2778
2784
|
includeSourceMaps: Boolean(flags['include-sourcemaps']),
|
|
2779
2785
|
});
|
|
2780
2786
|
const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
|
|
2787
|
+
printRuntimeProgress(
|
|
2788
|
+
`runtime release upload: mode=${uploadMode} traceId=${traceId} files=${files.length} size=${formatBytes(totalBytes)} timeout=${uploadTimeoutMs}ms`
|
|
2789
|
+
);
|
|
2790
|
+
const releaseFiles =
|
|
2791
|
+
uploadMode === 'legacy-json'
|
|
2792
|
+
? files.map(file => ({
|
|
2793
|
+
path: file.path,
|
|
2794
|
+
size: file.size,
|
|
2795
|
+
sha256: file.sha256,
|
|
2796
|
+
contentType: file.contentType,
|
|
2797
|
+
contentBase64: file.buffer.toString('base64'),
|
|
2798
|
+
}))
|
|
2799
|
+
: await uploadRuntimeDistFilesStaged({
|
|
2800
|
+
config,
|
|
2801
|
+
profileName: target.profileName,
|
|
2802
|
+
appType: target.appType,
|
|
2803
|
+
buildId,
|
|
2804
|
+
files,
|
|
2805
|
+
traceId,
|
|
2806
|
+
timeoutMs: uploadTimeoutMs,
|
|
2807
|
+
});
|
|
2781
2808
|
const data = await requestWithAuth(
|
|
2782
2809
|
config,
|
|
2783
2810
|
target.profileName,
|
|
2784
2811
|
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/runtime/releases`,
|
|
2785
2812
|
{
|
|
2786
2813
|
method: 'POST',
|
|
2814
|
+
timeoutMs: uploadTimeoutMs,
|
|
2815
|
+
headers: { 'x-openxiangda-trace-id': traceId },
|
|
2787
2816
|
body: {
|
|
2788
2817
|
buildId,
|
|
2789
2818
|
version: flags.version || readWorkspaceVersion(),
|
|
2790
2819
|
releaseNotes: flags.notes || flags['release-notes'] || '',
|
|
2791
2820
|
activate: !flags['no-activate'],
|
|
2792
|
-
files,
|
|
2821
|
+
files: releaseFiles,
|
|
2793
2822
|
},
|
|
2794
2823
|
}
|
|
2795
2824
|
);
|
|
@@ -2803,6 +2832,8 @@ async function runtime(args) {
|
|
|
2803
2832
|
totalBytes,
|
|
2804
2833
|
assetBaseUrl,
|
|
2805
2834
|
activated: !flags['no-activate'],
|
|
2835
|
+
uploadMode,
|
|
2836
|
+
traceId,
|
|
2806
2837
|
};
|
|
2807
2838
|
if (flags.json) return writeJson(result);
|
|
2808
2839
|
print(
|
|
@@ -2810,6 +2841,8 @@ async function runtime(args) {
|
|
|
2810
2841
|
`runtime release 已上传: ${target.appType}`,
|
|
2811
2842
|
`build: ${buildId}`,
|
|
2812
2843
|
`files: ${files.length} (${formatBytes(totalBytes)})`,
|
|
2844
|
+
`upload: ${uploadMode}`,
|
|
2845
|
+
`traceId: ${traceId}`,
|
|
2813
2846
|
`assetBase: ${assetBaseUrl}`,
|
|
2814
2847
|
`active: ${result.activated ? 'yes' : 'no'}`,
|
|
2815
2848
|
].join('\n')
|
|
@@ -2822,11 +2855,16 @@ async function runtime(args) {
|
|
|
2822
2855
|
|
|
2823
2856
|
function runRuntimeBuild(options) {
|
|
2824
2857
|
const command = options.command || defaultRuntimeBuildCommand();
|
|
2825
|
-
|
|
2858
|
+
if (options.jsonOutput) {
|
|
2859
|
+
printRuntimeProgress(`构建 React SPA runtime: ${command}`);
|
|
2860
|
+
} else {
|
|
2861
|
+
print(`构建 React SPA runtime: ${command}`);
|
|
2862
|
+
}
|
|
2826
2863
|
const result = spawnSync(command, [], {
|
|
2827
2864
|
cwd: process.cwd(),
|
|
2828
2865
|
shell: true,
|
|
2829
|
-
stdio: 'inherit',
|
|
2866
|
+
stdio: options.jsonOutput ? 'pipe' : 'inherit',
|
|
2867
|
+
encoding: options.jsonOutput ? 'utf8' : undefined,
|
|
2830
2868
|
env: {
|
|
2831
2869
|
...process.env,
|
|
2832
2870
|
OPENXIANGDA_APP_TYPE: options.appType,
|
|
@@ -2835,6 +2873,10 @@ function runRuntimeBuild(options) {
|
|
|
2835
2873
|
OPENXIANGDA_RUNTIME_ASSET_BASE: options.assetBaseUrl,
|
|
2836
2874
|
},
|
|
2837
2875
|
});
|
|
2876
|
+
if (options.jsonOutput) {
|
|
2877
|
+
if (result.stdout) process.stderr.write(maskText(result.stdout));
|
|
2878
|
+
if (result.stderr) process.stderr.write(maskText(result.stderr));
|
|
2879
|
+
}
|
|
2838
2880
|
if (result.error) fail(`runtime build 无法启动: ${result.error.message}`);
|
|
2839
2881
|
if (result.status !== 0) fail(`runtime build 失败: exit ${result.status}`);
|
|
2840
2882
|
}
|
|
@@ -2856,7 +2898,7 @@ function collectRuntimeDistFiles(distDir, options = {}) {
|
|
|
2856
2898
|
}
|
|
2857
2899
|
const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
|
|
2858
2900
|
if (totalBytes > 25 * 1024 * 1024) {
|
|
2859
|
-
fail(`runtime dist 过大: ${formatBytes(totalBytes)},当前
|
|
2901
|
+
fail(`runtime dist 过大: ${formatBytes(totalBytes)},当前 runtime 发布通道上限为 25MB`);
|
|
2860
2902
|
}
|
|
2861
2903
|
return files;
|
|
2862
2904
|
}
|
|
@@ -2878,11 +2920,112 @@ function walkRuntimeDist(rootDir, currentDir, files, options) {
|
|
|
2878
2920
|
size: buffer.length,
|
|
2879
2921
|
sha256: crypto.createHash('sha256').update(buffer).digest('hex'),
|
|
2880
2922
|
contentType: inferRuntimeContentType(relative),
|
|
2881
|
-
|
|
2923
|
+
buffer,
|
|
2882
2924
|
});
|
|
2883
2925
|
}
|
|
2884
2926
|
}
|
|
2885
2927
|
|
|
2928
|
+
async function uploadRuntimeDistFilesStaged(options) {
|
|
2929
|
+
const files = options.files || [];
|
|
2930
|
+
let completed = 0;
|
|
2931
|
+
const results = await runWithConcurrency(
|
|
2932
|
+
files,
|
|
2933
|
+
RUNTIME_UPLOAD_CONCURRENCY,
|
|
2934
|
+
async file => {
|
|
2935
|
+
const uploaded = await uploadRuntimeDistFile(options, file);
|
|
2936
|
+
completed += 1;
|
|
2937
|
+
printRuntimeProgress(
|
|
2938
|
+
`runtime file uploaded [${completed}/${files.length}] ${file.path} ${formatBytes(file.size)} traceId=${options.traceId}`
|
|
2939
|
+
);
|
|
2940
|
+
return uploaded;
|
|
2941
|
+
}
|
|
2942
|
+
);
|
|
2943
|
+
return results.map(file => ({
|
|
2944
|
+
path: file.path,
|
|
2945
|
+
size: file.size,
|
|
2946
|
+
sha256: file.sha256,
|
|
2947
|
+
contentType: file.contentType,
|
|
2948
|
+
}));
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
async function uploadRuntimeDistFile(options, file) {
|
|
2952
|
+
const apiPath = apiPathWithQuery(
|
|
2953
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(options.appType)}/runtime/releases/files`,
|
|
2954
|
+
{
|
|
2955
|
+
buildId: options.buildId,
|
|
2956
|
+
path: file.path,
|
|
2957
|
+
size: file.size,
|
|
2958
|
+
sha256: file.sha256,
|
|
2959
|
+
contentType: file.contentType,
|
|
2960
|
+
}
|
|
2961
|
+
);
|
|
2962
|
+
return await requestFormWithAuth(
|
|
2963
|
+
options.config,
|
|
2964
|
+
options.profileName,
|
|
2965
|
+
apiPath,
|
|
2966
|
+
() => {
|
|
2967
|
+
const form = new FormData();
|
|
2968
|
+
form.append(
|
|
2969
|
+
'file',
|
|
2970
|
+
new Blob([file.buffer], { type: file.contentType }),
|
|
2971
|
+
path.basename(file.path)
|
|
2972
|
+
);
|
|
2973
|
+
return form;
|
|
2974
|
+
},
|
|
2975
|
+
{
|
|
2976
|
+
timeoutMs: options.timeoutMs,
|
|
2977
|
+
headers: { 'x-openxiangda-trace-id': options.traceId },
|
|
2978
|
+
}
|
|
2979
|
+
);
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
async function runWithConcurrency(items, concurrency, worker) {
|
|
2983
|
+
const results = new Array(items.length);
|
|
2984
|
+
let nextIndex = 0;
|
|
2985
|
+
let firstError = null;
|
|
2986
|
+
const workers = Array.from(
|
|
2987
|
+
{ length: Math.min(concurrency, items.length) },
|
|
2988
|
+
async () => {
|
|
2989
|
+
while (nextIndex < items.length && !firstError) {
|
|
2990
|
+
const index = nextIndex;
|
|
2991
|
+
nextIndex += 1;
|
|
2992
|
+
try {
|
|
2993
|
+
results[index] = await worker(items[index], index);
|
|
2994
|
+
} catch (error) {
|
|
2995
|
+
firstError = error;
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
);
|
|
3000
|
+
await Promise.all(workers);
|
|
3001
|
+
if (firstError) throw firstError;
|
|
3002
|
+
return results;
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
function normalizeRuntimeUploadMode(value) {
|
|
3006
|
+
const normalized = String(value || 'staged').trim().toLowerCase();
|
|
3007
|
+
if (normalized === 'staged' || normalized === 'legacy-json') return normalized;
|
|
3008
|
+
fail(`--upload-mode 只支持 staged 或 legacy-json,当前: ${value}`);
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
function normalizeRuntimeUploadTimeoutMs(value) {
|
|
3012
|
+
const candidate =
|
|
3013
|
+
value === undefined || value === null || value === ''
|
|
3014
|
+
? process.env.OPENXIANGDA_RUNTIME_UPLOAD_TIMEOUT_MS
|
|
3015
|
+
: value;
|
|
3016
|
+
const parsed = Number(candidate);
|
|
3017
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
3018
|
+
return RUNTIME_UPLOAD_DEFAULT_TIMEOUT_MS;
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
function createRuntimeTraceId() {
|
|
3022
|
+
return `oxd-${Date.now().toString(36)}-${crypto.randomBytes(4).toString('hex')}`;
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
function printRuntimeProgress(message) {
|
|
3026
|
+
process.stderr.write(`${maskText(message)}\n`);
|
|
3027
|
+
}
|
|
3028
|
+
|
|
2886
3029
|
function buildRuntimeAssetBaseUrl(appType, buildId) {
|
|
2887
3030
|
return `/service/openxiangda-api/v1/apps/${encodeURIComponent(appType)}/runtime/releases/by-build/${encodeURIComponent(buildId)}/files/`;
|
|
2888
3031
|
}
|
|
@@ -7087,7 +7230,7 @@ async function requestWithAuth(config, profileName, apiPath, options = {}) {
|
|
|
7087
7230
|
}
|
|
7088
7231
|
}
|
|
7089
7232
|
|
|
7090
|
-
async function requestFormWithAuth(config, profileName, apiPath, formDataFactory) {
|
|
7233
|
+
async function requestFormWithAuth(config, profileName, apiPath, formDataFactory, options = {}) {
|
|
7091
7234
|
const resolved = getProfile(config, profileName);
|
|
7092
7235
|
const profile = resolved.profile;
|
|
7093
7236
|
if (!profile.token?.accessToken) {
|
|
@@ -7099,7 +7242,8 @@ async function requestFormWithAuth(config, profileName, apiPath, formDataFactory
|
|
|
7099
7242
|
profile.baseUrl,
|
|
7100
7243
|
apiPath,
|
|
7101
7244
|
formDataFactory(),
|
|
7102
|
-
profile.token.accessToken
|
|
7245
|
+
profile.token.accessToken,
|
|
7246
|
+
options
|
|
7103
7247
|
);
|
|
7104
7248
|
return unwrapApi(payload);
|
|
7105
7249
|
} catch (error) {
|
|
@@ -7111,25 +7255,41 @@ async function requestFormWithAuth(config, profileName, apiPath, formDataFactory
|
|
|
7111
7255
|
profile.baseUrl,
|
|
7112
7256
|
apiPath,
|
|
7113
7257
|
formDataFactory(),
|
|
7114
|
-
profile.token.accessToken
|
|
7258
|
+
profile.token.accessToken,
|
|
7259
|
+
options
|
|
7115
7260
|
);
|
|
7116
7261
|
return unwrapApi(payload);
|
|
7117
7262
|
}
|
|
7118
7263
|
}
|
|
7119
7264
|
|
|
7120
|
-
async function requestForm(baseUrl, apiPath, formData, accessToken) {
|
|
7265
|
+
async function requestForm(baseUrl, apiPath, formData, accessToken, options = {}) {
|
|
7121
7266
|
if (typeof fetch !== 'function') {
|
|
7122
7267
|
throw new Error('当前 Node.js 版本不支持 fetch,请使用 Node.js 18 或更高版本');
|
|
7123
7268
|
}
|
|
7124
7269
|
const url = `${baseUrl.replace(/\/+$/, '')}${apiPath}`;
|
|
7125
|
-
const
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7270
|
+
const timeoutMs = normalizeRuntimeUploadTimeoutMs(options.timeoutMs);
|
|
7271
|
+
const controller = new AbortController();
|
|
7272
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
7273
|
+
let response;
|
|
7274
|
+
try {
|
|
7275
|
+
response = await fetch(url, {
|
|
7276
|
+
method: 'POST',
|
|
7277
|
+
headers: {
|
|
7278
|
+
accept: 'application/json',
|
|
7279
|
+
authorization: `Bearer ${accessToken}`,
|
|
7280
|
+
...(options.headers || {}),
|
|
7281
|
+
},
|
|
7282
|
+
body: formData,
|
|
7283
|
+
signal: controller.signal,
|
|
7284
|
+
});
|
|
7285
|
+
} catch (error) {
|
|
7286
|
+
if (isAbortError(error)) {
|
|
7287
|
+
throw new Error(maskText(`HTTP form request timed out after ${timeoutMs}ms: ${apiPath}`));
|
|
7288
|
+
}
|
|
7289
|
+
throw error;
|
|
7290
|
+
} finally {
|
|
7291
|
+
clearTimeout(timer);
|
|
7292
|
+
}
|
|
7133
7293
|
const text = await response.text();
|
|
7134
7294
|
let payload = null;
|
|
7135
7295
|
if (text) {
|
|
@@ -7146,6 +7306,13 @@ async function requestForm(baseUrl, apiPath, formData, accessToken) {
|
|
|
7146
7306
|
return payload;
|
|
7147
7307
|
}
|
|
7148
7308
|
|
|
7309
|
+
function isAbortError(error) {
|
|
7310
|
+
return (
|
|
7311
|
+
error?.name === 'AbortError' ||
|
|
7312
|
+
String(error?.message || '').toLowerCase().includes('aborted')
|
|
7313
|
+
);
|
|
7314
|
+
}
|
|
7315
|
+
|
|
7149
7316
|
async function refreshProfile(config, profileName) {
|
|
7150
7317
|
const { profile } = getProfile(config, profileName);
|
|
7151
7318
|
if (!profile.token?.refreshToken) {
|
package/lib/http.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
const { maskText } = require('./utils');
|
|
2
2
|
|
|
3
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 60000;
|
|
4
|
+
|
|
3
5
|
async function requestJson(baseUrl, apiPath, options = {}) {
|
|
4
6
|
if (typeof fetch !== 'function') {
|
|
5
7
|
throw new Error('当前 Node.js 版本不支持 fetch,请使用 Node.js 18 或更高版本');
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
const url = `${baseUrl.replace(/\/+$/, '')}${apiPath}`;
|
|
11
|
+
const timeoutMs = normalizeTimeoutMs(options.timeoutMs, DEFAULT_REQUEST_TIMEOUT_MS);
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
9
14
|
const headers = {
|
|
10
15
|
accept: 'application/json',
|
|
11
16
|
...(options.headers || {}),
|
|
@@ -17,12 +22,23 @@ async function requestJson(baseUrl, apiPath, options = {}) {
|
|
|
17
22
|
headers.authorization = `Bearer ${options.accessToken}`;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
let response;
|
|
26
|
+
try {
|
|
27
|
+
response = await fetch(url, {
|
|
28
|
+
method: options.method || 'GET',
|
|
29
|
+
headers,
|
|
30
|
+
body:
|
|
31
|
+
options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
32
|
+
signal: controller.signal,
|
|
33
|
+
});
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (isAbortError(error)) {
|
|
36
|
+
throw new Error(maskText(`HTTP request timed out after ${timeoutMs}ms: ${apiPath}`));
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
} finally {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
}
|
|
26
42
|
|
|
27
43
|
const text = await response.text();
|
|
28
44
|
let payload = null;
|
|
@@ -42,6 +58,19 @@ async function requestJson(baseUrl, apiPath, options = {}) {
|
|
|
42
58
|
return payload;
|
|
43
59
|
}
|
|
44
60
|
|
|
61
|
+
function normalizeTimeoutMs(value, fallback) {
|
|
62
|
+
const parsed = Number(value);
|
|
63
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
64
|
+
return fallback;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isAbortError(error) {
|
|
68
|
+
return (
|
|
69
|
+
error?.name === 'AbortError' ||
|
|
70
|
+
String(error?.message || '').toLowerCase().includes('aborted')
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
45
74
|
module.exports = {
|
|
46
75
|
requestJson,
|
|
47
76
|
};
|
|
@@ -37,6 +37,7 @@ OpenXiangda supports two workspace modes. Classic `sy-lowcode-app-workspace` pub
|
|
|
37
37
|
### Hard rules — always
|
|
38
38
|
|
|
39
39
|
- ✅ Publish classic workspaces through `openxiangda workspace publish --profile <name>`; publish React SPA workspaces through `openxiangda resource publish` + `openxiangda runtime deploy`.
|
|
40
|
+
- ✅ `runtime deploy` defaults to staged multipart `dist/` uploads plus a final manifest. Use `--upload-mode legacy-json` only for old platform servers.
|
|
40
41
|
- ✅ User token lives in `~/.openxiangda/profiles.json`; project state in `.openxiangda/state.json` (IDs only).
|
|
41
42
|
- ✅ Each profile (dev / prod / ...) has its own `appType` and resource IDs; never copy a `formUuid` / `pageId` / `workflowId` / `automationId` across profiles.
|
|
42
43
|
- ✅ Run `openxiangda update check --json` at the start of substantial work; if `updateAvailable`, run `openxiangda update install` and `openxiangda skill install --force`.
|
|
@@ -64,7 +64,7 @@ openxiangda resource publish --profile <name>
|
|
|
64
64
|
openxiangda runtime deploy --profile <name>
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
`runtime deploy` builds and activates the app-level SPA release for `/view/:appType/*`.
|
|
67
|
+
`runtime deploy` builds and activates the app-level SPA release for `/view/:appType/*`. It uploads `dist/` through staged multipart file uploads by default; use `--upload-mode legacy-json` only as an older-server fallback.
|
|
68
68
|
Publish React SPA forms separately with `openxiangda workspace publish --form <formCode>`;
|
|
69
69
|
with `runtimeMode: "react-spa"` this is schema-only and does not require OSS.
|
|
70
70
|
|
|
@@ -43,7 +43,7 @@ openxiangda resource publish --profile <name>
|
|
|
43
43
|
openxiangda runtime deploy --profile <name>
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
`runtime deploy` builds the Vite app with a release-specific asset base, uploads `dist/` to the platform runtime release store, and activates the release unless `--no-activate` is passed.
|
|
46
|
+
`runtime deploy` builds the Vite app with a release-specific asset base, uploads `dist/` to the platform runtime release store, and activates the release unless `--no-activate` is passed. The default upload mode is staged multipart file upload plus a final release manifest; use `--upload-mode legacy-json` only for older platform servers that have not been upgraded.
|
|
47
47
|
|
|
48
48
|
For React SPA forms, still use targeted form publish to create/bind the form and
|
|
49
49
|
sync schema:
|
|
@@ -282,8 +282,11 @@ openxiangda runtime deploy --profile prod
|
|
|
282
282
|
It is responsible for:
|
|
283
283
|
|
|
284
284
|
1. Running the workspace build command with `OPENXIANGDA_APP_TYPE`, `OPENXIANGDA_BUILD_ID`, and `OPENXIANGDA_RUNTIME_ASSET_BASE`.
|
|
285
|
-
2. Uploading `dist/` to `/openxiangda-api/v1/apps/:appType/runtime/releases
|
|
286
|
-
3.
|
|
285
|
+
2. Uploading each `dist/` file to `/openxiangda-api/v1/apps/:appType/runtime/releases/files` with a shared trace id.
|
|
286
|
+
3. Submitting the final release manifest to `/openxiangda-api/v1/apps/:appType/runtime/releases` without inline base64 content.
|
|
287
|
+
4. Activating the new runtime release so `/view/:appType/*` enters the React app.
|
|
288
|
+
|
|
289
|
+
Runtime upload/finalize timeout defaults to 120s. Override with `--upload-timeout-ms <ms>` or `OPENXIANGDA_RUNTIME_UPLOAD_TIMEOUT_MS`; progress and `traceId` are written to stderr, while `--json` keeps stdout as final JSON only.
|
|
287
290
|
|
|
288
291
|
## References
|
|
289
292
|
|