openxiangda 1.0.103 → 1.0.105
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/lib/app-function-invoke.js +28 -0
- package/lib/cli.js +55 -9
- package/lib/design-gates.js +1 -1
- package/lib/design-review.js +220 -0
- package/lib/http.js +4 -1
- package/lib/workspace-init.js +19 -2
- package/package.json +2 -1
- package/packages/sdk/dist/runtime/index.cjs +136 -5
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.d.mts +1 -1
- package/packages/sdk/dist/runtime/index.d.ts +1 -1
- package/packages/sdk/dist/runtime/index.mjs +169 -38
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/react.cjs +134 -3
- package/packages/sdk/dist/runtime/react.cjs.map +1 -1
- package/packages/sdk/dist/runtime/react.d.mts +39 -7
- package/packages/sdk/dist/runtime/react.d.ts +39 -7
- package/packages/sdk/dist/runtime/react.mjs +143 -12
- package/packages/sdk/dist/runtime/react.mjs.map +1 -1
- package/packages/sdk/src/build-source/scripts/build-forms.runtime-entry.test.ts +1 -1
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +25 -0
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +33 -0
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +25 -0
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +33 -0
- package/templates/openxiangda-react-spa/AGENTS.md +3 -0
- package/templates/openxiangda-react-spa/package.json +4 -1
- package/templates/openxiangda-react-spa/scripts/deploy.mjs +42 -0
- package/templates/openxiangda-react-spa/scripts/guard-publish.mjs +21 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function buildOpenXiangdaFunctionInvokeRequest(appType, functionCode, body) {
|
|
2
|
+
return {
|
|
3
|
+
method: 'POST',
|
|
4
|
+
path: `/openxiangda-api/v1/apps/${encodeURIComponent(appType)}/functions/${encodeURIComponent(functionCode)}/invoke`,
|
|
5
|
+
body,
|
|
6
|
+
strictEnvelope: true,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function buildLegacyFunctionInvokeRequest(appType, functionCode, body) {
|
|
11
|
+
return {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
path: `/${encodeURIComponent(appType)}/v1/functions/${encodeURIComponent(functionCode)}/invoke.json`,
|
|
14
|
+
body,
|
|
15
|
+
strictEnvelope: true,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isHttpNotFound(error) {
|
|
20
|
+
const status = Number(error?.status || error?.statusCode || error?.response?.status);
|
|
21
|
+
return status === 404 || String(error?.message || '').includes('HTTP 404');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
buildLegacyFunctionInvokeRequest,
|
|
26
|
+
buildOpenXiangdaFunctionInvokeRequest,
|
|
27
|
+
isHttpNotFound,
|
|
28
|
+
};
|
package/lib/cli.js
CHANGED
|
@@ -20,6 +20,11 @@ const {
|
|
|
20
20
|
} = require('./config');
|
|
21
21
|
const { requestJson } = require('./http');
|
|
22
22
|
const { getSkillStatusReport, installSkills } = require('./skills');
|
|
23
|
+
const {
|
|
24
|
+
buildLegacyFunctionInvokeRequest,
|
|
25
|
+
buildOpenXiangdaFunctionInvokeRequest,
|
|
26
|
+
isHttpNotFound,
|
|
27
|
+
} = require('./app-function-invoke');
|
|
23
28
|
const { assertCanInitializeWorkspace, initWorkspace } = require('./workspace-init');
|
|
24
29
|
const { bootstrapWorkspace } = require('./workspace-bootstrap');
|
|
25
30
|
const {
|
|
@@ -31,6 +36,7 @@ const {
|
|
|
31
36
|
renderDesignTemplate,
|
|
32
37
|
renderResourceExplain,
|
|
33
38
|
} = require('./design-gates');
|
|
39
|
+
const { buildDesignReview, renderDesignReview } = require('./design-review');
|
|
34
40
|
const {
|
|
35
41
|
fail,
|
|
36
42
|
formatFetchError,
|
|
@@ -104,7 +110,7 @@ Usage:
|
|
|
104
110
|
openxiangda platform use <name>
|
|
105
111
|
openxiangda auth status|refresh|logout [--profile name]
|
|
106
112
|
openxiangda doctor [--profile name] [--app-type APP_XXX] [--json]
|
|
107
|
-
openxiangda design gates|template [--topic code] [--json]
|
|
113
|
+
openxiangda design gates|template|review [--topic code] [--json]
|
|
108
114
|
openxiangda env [--profile name]
|
|
109
115
|
openxiangda workspace init [dir] [--name package-name] [--runtime legacy|react-spa] [--install] [--profile name --app-type APP_XXX]
|
|
110
116
|
openxiangda workspace init [dir] --profile <name> --app-name <app-name> [--runtime legacy|react-spa] [--install]
|
|
@@ -475,7 +481,24 @@ async function design(args) {
|
|
|
475
481
|
return;
|
|
476
482
|
}
|
|
477
483
|
|
|
478
|
-
|
|
484
|
+
if (subcommand === 'review') {
|
|
485
|
+
const hasResources = fs.existsSync(path.join(process.cwd(), 'src', 'resources'));
|
|
486
|
+
const manifest = hasResources ? loadWorkspaceResources() : null;
|
|
487
|
+
const validation = manifest ? validateWorkspaceResources(manifest) : {
|
|
488
|
+
errors: [],
|
|
489
|
+
warnings: ['未发现 src/resources,无法审查资源 manifest'],
|
|
490
|
+
};
|
|
491
|
+
const result = buildDesignReview({
|
|
492
|
+
cwd: process.cwd(),
|
|
493
|
+
manifest,
|
|
494
|
+
validation,
|
|
495
|
+
});
|
|
496
|
+
if (flags.json) return writeJson(result);
|
|
497
|
+
print(renderDesignReview(result));
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
fail('用法: openxiangda design gates|template|review [--topic code] [--json]');
|
|
479
502
|
}
|
|
480
503
|
|
|
481
504
|
async function doctor(args) {
|
|
@@ -2664,12 +2687,35 @@ async function appFunction(args) {
|
|
|
2664
2687
|
const [functionCode] = positional;
|
|
2665
2688
|
if (!functionCode) fail('用法: openxiangda function invoke <functionCode> [--body-json file|json]');
|
|
2666
2689
|
const body = readDirectJsonBody(flags, 'function invoke', { optional: true });
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
body
|
|
2671
|
-
|
|
2672
|
-
|
|
2690
|
+
const openXiangdaApiRequest = buildOpenXiangdaFunctionInvokeRequest(
|
|
2691
|
+
target.appType,
|
|
2692
|
+
functionCode,
|
|
2693
|
+
body
|
|
2694
|
+
);
|
|
2695
|
+
if (flags['dry-run']) {
|
|
2696
|
+
return runDirectRequest(config, target, flags, openXiangdaApiRequest);
|
|
2697
|
+
}
|
|
2698
|
+
try {
|
|
2699
|
+
const data = await requestWithAuth(
|
|
2700
|
+
config,
|
|
2701
|
+
target.profileName,
|
|
2702
|
+
openXiangdaApiRequest.path,
|
|
2703
|
+
{
|
|
2704
|
+
method: openXiangdaApiRequest.method,
|
|
2705
|
+
body: openXiangdaApiRequest.body,
|
|
2706
|
+
strictEnvelope: true,
|
|
2707
|
+
}
|
|
2708
|
+
);
|
|
2709
|
+
return outputDirectResult(data, flags);
|
|
2710
|
+
} catch (error) {
|
|
2711
|
+
if (!isHttpNotFound(error)) throw error;
|
|
2712
|
+
return runDirectRequest(
|
|
2713
|
+
config,
|
|
2714
|
+
target,
|
|
2715
|
+
flags,
|
|
2716
|
+
buildLegacyFunctionInvokeRequest(target.appType, functionCode, body)
|
|
2717
|
+
);
|
|
2718
|
+
}
|
|
2673
2719
|
}
|
|
2674
2720
|
|
|
2675
2721
|
return directResourceCrud(config, target, {
|
|
@@ -4044,7 +4090,7 @@ async function commands(args) {
|
|
|
4044
4090
|
'platform add|list|use|remove',
|
|
4045
4091
|
'auth status|refresh|logout',
|
|
4046
4092
|
'doctor [--profile name] [--app-type APP_XXX]',
|
|
4047
|
-
'design gates|template [--topic code]',
|
|
4093
|
+
'design gates|template|review [--topic code]',
|
|
4048
4094
|
'env',
|
|
4049
4095
|
'workspace init|bind|publish [--app-name] [--changed|--since|--form|--page|--only|--dry-run|--force|--resources|--skip-resources|--prune]',
|
|
4050
4096
|
'app list|create|snapshot',
|
package/lib/design-gates.js
CHANGED
|
@@ -309,7 +309,7 @@ function getDesignTopicCatalog() {
|
|
|
309
309
|
function renderDesignHelp() {
|
|
310
310
|
const catalog = getDesignTopicCatalog();
|
|
311
311
|
const lines = [
|
|
312
|
-
'用法: openxiangda design gates|template [--topic code[,code...]] [--json]',
|
|
312
|
+
'用法: openxiangda design gates|template|review [--topic code[,code...]] [--json]',
|
|
313
313
|
'',
|
|
314
314
|
catalog.hardRule,
|
|
315
315
|
'',
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const SOURCE_DIRS = [
|
|
5
|
+
path.join('src', 'app'),
|
|
6
|
+
path.join('src', 'pages'),
|
|
7
|
+
path.join('src', 'forms'),
|
|
8
|
+
path.join('src', 'runtime'),
|
|
9
|
+
];
|
|
10
|
+
const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx']);
|
|
11
|
+
const SKIP_DIRS = new Set([
|
|
12
|
+
'node_modules',
|
|
13
|
+
'.pnpm',
|
|
14
|
+
'dist',
|
|
15
|
+
'.vite',
|
|
16
|
+
'.vite-temp',
|
|
17
|
+
'.cache',
|
|
18
|
+
'coverage',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
function buildDesignReview(options = {}) {
|
|
22
|
+
const cwd = options.cwd || process.cwd();
|
|
23
|
+
const manifest = options.manifest || null;
|
|
24
|
+
const validation = options.validation || null;
|
|
25
|
+
const result = {
|
|
26
|
+
cwd,
|
|
27
|
+
checkedAt: new Date().toISOString(),
|
|
28
|
+
passed: true,
|
|
29
|
+
summary: {
|
|
30
|
+
errors: 0,
|
|
31
|
+
warnings: 0,
|
|
32
|
+
suggestions: 0,
|
|
33
|
+
},
|
|
34
|
+
errors: [],
|
|
35
|
+
warnings: [],
|
|
36
|
+
suggestions: [],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
addResourceValidationFindings(result, validation);
|
|
40
|
+
reviewPublicAccess(result, manifest);
|
|
41
|
+
reviewSourceFiles(result, cwd);
|
|
42
|
+
reviewAcceptanceArtifacts(result, cwd);
|
|
43
|
+
|
|
44
|
+
result.summary.errors = result.errors.length;
|
|
45
|
+
result.summary.warnings = result.warnings.length;
|
|
46
|
+
result.summary.suggestions = result.suggestions.length;
|
|
47
|
+
result.passed = result.errors.length === 0;
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function renderDesignReview(result) {
|
|
52
|
+
const lines = [
|
|
53
|
+
'OpenXiangda design review',
|
|
54
|
+
`cwd: ${result.cwd}`,
|
|
55
|
+
`status: ${result.passed ? 'passed' : 'needs attention'}`,
|
|
56
|
+
`summary: ${result.summary.errors} errors, ${result.summary.warnings} warnings, ${result.summary.suggestions} suggestions`,
|
|
57
|
+
];
|
|
58
|
+
appendFindings(lines, 'Errors', result.errors);
|
|
59
|
+
appendFindings(lines, 'Warnings', result.warnings);
|
|
60
|
+
appendFindings(lines, 'Suggestions', result.suggestions);
|
|
61
|
+
return `${lines.join('\n')}\n`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function appendFindings(lines, title, findings) {
|
|
65
|
+
if (!findings.length) return;
|
|
66
|
+
lines.push('', `${title}:`);
|
|
67
|
+
for (const finding of findings) {
|
|
68
|
+
lines.push(`- [${finding.code}] ${finding.message}`);
|
|
69
|
+
if (finding.file) lines.push(` file: ${finding.file}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function addResourceValidationFindings(result, validation) {
|
|
74
|
+
for (const message of validation?.errors || []) {
|
|
75
|
+
result.errors.push({
|
|
76
|
+
code: 'resource-validation-error',
|
|
77
|
+
message,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
for (const message of validation?.warnings || []) {
|
|
81
|
+
result.warnings.push({
|
|
82
|
+
code: 'resource-validation-warning',
|
|
83
|
+
message,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function reviewPublicAccess(result, manifest) {
|
|
89
|
+
if (!manifest) return;
|
|
90
|
+
const policies = manifest.publicAccessPolicies || [];
|
|
91
|
+
const policyCodes = new Set(policies.map(policy => String(policy.code || '').trim()).filter(Boolean));
|
|
92
|
+
const policyRouteCodes = new Set(policies.map(policy => String(policy.routeCode || '').trim()).filter(Boolean));
|
|
93
|
+
|
|
94
|
+
for (const route of manifest.routes || []) {
|
|
95
|
+
if (route.__invalid) continue;
|
|
96
|
+
const publicAccess = String(route.publicAccess || 'none');
|
|
97
|
+
if (publicAccess !== 'guest' && publicAccess !== 'ticket') continue;
|
|
98
|
+
const routeLabel = route.code || route.pathPattern || route.path || route.__source || 'public route';
|
|
99
|
+
const policyCode = String(route.publicPolicyCode || route.policyCode || '').trim();
|
|
100
|
+
if (policyCode && !policyCodes.has(policyCode)) {
|
|
101
|
+
result.errors.push({
|
|
102
|
+
code: 'public-route-missing-policy',
|
|
103
|
+
message: `公开路由 ${routeLabel} 引用了不存在的 public-access policy: ${policyCode}`,
|
|
104
|
+
file: route.__source,
|
|
105
|
+
});
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (!policyCode && route.code && !policyRouteCodes.has(String(route.code))) {
|
|
109
|
+
result.warnings.push({
|
|
110
|
+
code: 'public-route-unbound-policy',
|
|
111
|
+
message: `公开路由 ${routeLabel} 未声明 publicPolicyCode,也没有 policy.routeCode 绑定它`,
|
|
112
|
+
file: route.__source,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const policy of policies) {
|
|
118
|
+
if (policy.__invalid) continue;
|
|
119
|
+
const grants = policy.grants && typeof policy.grants === 'object' ? policy.grants : null;
|
|
120
|
+
const grantCount = grants
|
|
121
|
+
? ['forms', 'dataViews', 'functions', 'connectors'].reduce(
|
|
122
|
+
(count, key) => count + (Array.isArray(grants[key]) ? grants[key].length : 0),
|
|
123
|
+
0
|
|
124
|
+
)
|
|
125
|
+
: 0;
|
|
126
|
+
if (grantCount === 0) {
|
|
127
|
+
result.warnings.push({
|
|
128
|
+
code: 'public-policy-empty-grants',
|
|
129
|
+
message: `公开策略 ${policy.code || policy.__source} 没有显式 grants;公开页调用 form/dataView/function/connector 会被后端拒绝`,
|
|
130
|
+
file: policy.__source,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function reviewSourceFiles(result, cwd) {
|
|
137
|
+
for (const filePath of listSourceFiles(cwd)) {
|
|
138
|
+
const relativeFile = path.relative(cwd, filePath);
|
|
139
|
+
let source = '';
|
|
140
|
+
try {
|
|
141
|
+
source = fs.readFileSync(filePath, 'utf8');
|
|
142
|
+
} catch {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const lineCount = source.split(/\r?\n/).length;
|
|
146
|
+
if (relativeFile.startsWith(path.join('src', 'pages')) && lineCount > 600) {
|
|
147
|
+
result.warnings.push({
|
|
148
|
+
code: 'large-page-module',
|
|
149
|
+
message: `页面文件超过 600 行,建议拆分为 domain/service/hooks/components 后再交给 AI 迭代`,
|
|
150
|
+
file: relativeFile,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (/\?publicAccess=guest|publicAccess=guest|isRenderNav|workbench/i.test(source)) {
|
|
154
|
+
result.errors.push({
|
|
155
|
+
code: 'legacy-public-runtime-pattern',
|
|
156
|
+
message: '发现旧公开访问或旧 workbench/runtime 参数;React SPA 新公开页应使用 routes + public-access + PublicAccessGate',
|
|
157
|
+
file: relativeFile,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (/<(?:input|select|textarea)\b/i.test(source)) {
|
|
161
|
+
result.warnings.push({
|
|
162
|
+
code: 'native-form-control',
|
|
163
|
+
message: 'AI 编写的页面/表单代码不应直接使用原生表单控件,优先使用 OpenXiangda/antd/antd-mobile 封装',
|
|
164
|
+
file: relativeFile,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (/(pageSize|limit)\s*[:=]\s*(1000|2000|5000|9999)\b|fetchAll|getAllData|allData/i.test(source)) {
|
|
168
|
+
result.warnings.push({
|
|
169
|
+
code: 'unbounded-query-risk',
|
|
170
|
+
message: '发现疑似大页或全量拉取;列表、选择器和报表应使用服务端分页、搜索字段或 data-view',
|
|
171
|
+
file: relativeFile,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (/(localStorage|sessionStorage).{0,80}(role|permission)|(role|permission).{0,80}(localStorage|sessionStorage)/i.test(source)) {
|
|
175
|
+
result.warnings.push({
|
|
176
|
+
code: 'frontend-permission-risk',
|
|
177
|
+
message: '发现疑似前端存储权限判断;权限和数据范围必须以后端权限组、public grants 或接口返回为准',
|
|
178
|
+
file: relativeFile,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function listSourceFiles(cwd) {
|
|
185
|
+
const files = [];
|
|
186
|
+
for (const relativeDir of SOURCE_DIRS) {
|
|
187
|
+
const dir = path.join(cwd, relativeDir);
|
|
188
|
+
if (fs.existsSync(dir)) collectSourceFiles(dir, files);
|
|
189
|
+
}
|
|
190
|
+
return files;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function collectSourceFiles(dir, files) {
|
|
194
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
195
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
196
|
+
const filePath = path.join(dir, entry.name);
|
|
197
|
+
if (entry.isDirectory()) {
|
|
198
|
+
collectSourceFiles(filePath, files);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (entry.isFile() && SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
|
|
202
|
+
files.push(filePath);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function reviewAcceptanceArtifacts(result, cwd) {
|
|
208
|
+
const acceptanceDir = path.join(cwd, 'docs', 'acceptance');
|
|
209
|
+
const acceptanceFile = path.join(cwd, 'docs', 'acceptance.md');
|
|
210
|
+
if (fs.existsSync(acceptanceDir) || fs.existsSync(acceptanceFile)) return;
|
|
211
|
+
result.suggestions.push({
|
|
212
|
+
code: 'missing-acceptance-artifacts',
|
|
213
|
+
message: '建议补充 docs/acceptance/ 或 docs/acceptance.md,沉淀验收路径、账号、数据、发布 releaseId 和回滚方式',
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = {
|
|
218
|
+
buildDesignReview,
|
|
219
|
+
renderDesignReview,
|
|
220
|
+
};
|
package/lib/http.js
CHANGED
|
@@ -52,7 +52,10 @@ async function requestJson(baseUrl, apiPath, options = {}) {
|
|
|
52
52
|
|
|
53
53
|
if (!response.ok) {
|
|
54
54
|
const message = payload?.message || response.statusText || 'request failed';
|
|
55
|
-
|
|
55
|
+
const error = new Error(maskText(`HTTP ${response.status}: ${message}`));
|
|
56
|
+
error.status = response.status;
|
|
57
|
+
error.payload = payload;
|
|
58
|
+
throw error;
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
return payload;
|
package/lib/workspace-init.js
CHANGED
|
@@ -6,6 +6,16 @@ const { getProfile, loadConfig, saveProjectState } = require('./config');
|
|
|
6
6
|
const ROOT_DIR = path.join(__dirname, '..');
|
|
7
7
|
const LEGACY_TEMPLATE_DIR = path.join(ROOT_DIR, 'templates', 'sy-lowcode-app-workspace');
|
|
8
8
|
const REACT_SPA_TEMPLATE_DIR = path.join(ROOT_DIR, 'templates', 'openxiangda-react-spa');
|
|
9
|
+
const TEMPLATE_IGNORE_NAMES = new Set([
|
|
10
|
+
'node_modules',
|
|
11
|
+
'.pnpm',
|
|
12
|
+
'dist',
|
|
13
|
+
'.vite',
|
|
14
|
+
'.vite-temp',
|
|
15
|
+
'.cache',
|
|
16
|
+
'coverage',
|
|
17
|
+
]);
|
|
18
|
+
const TEMPLATE_IGNORE_PATHS = new Set(['.openxiangda/build-cache.json']);
|
|
9
19
|
|
|
10
20
|
function initWorkspace(options = {}) {
|
|
11
21
|
const targetDir = path.resolve(options.dir || process.cwd());
|
|
@@ -113,13 +123,14 @@ function normalizeRuntime(value) {
|
|
|
113
123
|
return value === 'react-spa' || value === 'spa' ? 'react-spa' : 'legacy';
|
|
114
124
|
}
|
|
115
125
|
|
|
116
|
-
function copyTemplate(sourceDir, targetDir, replacements) {
|
|
126
|
+
function copyTemplate(sourceDir, targetDir, replacements, baseDir = sourceDir) {
|
|
117
127
|
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
|
|
118
128
|
const sourcePath = path.join(sourceDir, entry.name);
|
|
129
|
+
if (shouldSkipTemplateEntry(baseDir, sourcePath, entry.name)) continue;
|
|
119
130
|
const targetPath = path.join(targetDir, entry.name);
|
|
120
131
|
if (entry.isDirectory()) {
|
|
121
132
|
fs.mkdirSync(targetPath, { recursive: true });
|
|
122
|
-
copyTemplate(sourcePath, targetPath, replacements);
|
|
133
|
+
copyTemplate(sourcePath, targetPath, replacements, baseDir);
|
|
123
134
|
continue;
|
|
124
135
|
}
|
|
125
136
|
if (!entry.isFile()) continue;
|
|
@@ -129,6 +140,12 @@ function copyTemplate(sourceDir, targetDir, replacements) {
|
|
|
129
140
|
}
|
|
130
141
|
}
|
|
131
142
|
|
|
143
|
+
function shouldSkipTemplateEntry(baseDir, sourcePath, entryName) {
|
|
144
|
+
if (TEMPLATE_IGNORE_NAMES.has(entryName)) return true;
|
|
145
|
+
const relativePath = path.relative(baseDir, sourcePath).split(path.sep).join('/');
|
|
146
|
+
return TEMPLATE_IGNORE_PATHS.has(relativePath);
|
|
147
|
+
}
|
|
148
|
+
|
|
132
149
|
function applyReplacements(content, replacements) {
|
|
133
150
|
return Object.entries(replacements).reduce(
|
|
134
151
|
(result, [key, value]) => result.split(key).join(value),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openxiangda",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.105",
|
|
4
4
|
"description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"bin": {
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"test:resource-plan": "node scripts/resource-plan-smoke.mjs",
|
|
68
68
|
"test:design-gates": "node scripts/design-gates-smoke.mjs",
|
|
69
69
|
"test:resource-cli": "node scripts/resource-cli-smoke.mjs",
|
|
70
|
+
"test:api-contract": "node scripts/openxiangda-api-contract-smoke.mjs",
|
|
70
71
|
"test:help-no-side-effects": "node scripts/help-no-side-effects-smoke.mjs",
|
|
71
72
|
"test:app-function-fallback": "node scripts/app-function-source-fallback-smoke.mjs",
|
|
72
73
|
"test:runtime-deploy": "node scripts/runtime-deploy-smoke.mjs",
|
|
@@ -607,7 +607,7 @@ var require_react_is_development = __commonJS({
|
|
|
607
607
|
var ContextProvider = REACT_PROVIDER_TYPE;
|
|
608
608
|
var Element2 = REACT_ELEMENT_TYPE;
|
|
609
609
|
var ForwardRef2 = REACT_FORWARD_REF_TYPE;
|
|
610
|
-
var
|
|
610
|
+
var Fragment19 = REACT_FRAGMENT_TYPE2;
|
|
611
611
|
var Lazy = REACT_LAZY_TYPE;
|
|
612
612
|
var Memo = REACT_MEMO_TYPE;
|
|
613
613
|
var Portal = REACT_PORTAL_TYPE;
|
|
@@ -675,7 +675,7 @@ var require_react_is_development = __commonJS({
|
|
|
675
675
|
exports.ContextProvider = ContextProvider;
|
|
676
676
|
exports.Element = Element2;
|
|
677
677
|
exports.ForwardRef = ForwardRef2;
|
|
678
|
-
exports.Fragment =
|
|
678
|
+
exports.Fragment = Fragment19;
|
|
679
679
|
exports.Lazy = Lazy;
|
|
680
680
|
exports.Memo = Memo;
|
|
681
681
|
exports.Portal = Portal;
|
|
@@ -842,6 +842,10 @@ __export(runtime_exports, {
|
|
|
842
842
|
createReactPage: () => createReactPage,
|
|
843
843
|
extractFieldsFromComponentsTree: () => extractFieldsFromComponentsTree,
|
|
844
844
|
fetchBrowserRuntimeBootstrap: () => fetchBrowserRuntimeBootstrap,
|
|
845
|
+
getAuthErrorExtra: () => getAuthErrorExtra,
|
|
846
|
+
getAuthErrorReason: () => getAuthErrorReason,
|
|
847
|
+
isAuthChallengeRequired: () => isAuthChallengeRequired,
|
|
848
|
+
isAuthClientError: () => isAuthClientError,
|
|
845
849
|
loadCustomPageModule: () => loadCustomPageModule,
|
|
846
850
|
loadRuntimeScriptModules: () => loadRuntimeScriptModules,
|
|
847
851
|
mountBrowserPageRuntime: () => mountBrowserPageRuntime,
|
|
@@ -2296,8 +2300,43 @@ var AuthClientError = class extends Error {
|
|
|
2296
2300
|
this.status = options.status;
|
|
2297
2301
|
this.code = options.code;
|
|
2298
2302
|
this.payload = options.payload;
|
|
2303
|
+
this.extra = normalizeAuthErrorExtra(
|
|
2304
|
+
options.extra || getRecordValue(options.payload, "extra")
|
|
2305
|
+
);
|
|
2306
|
+
this.reason = this.extra?.reason || this.extra?.guardCode || (typeof options.code === "string" ? options.code : void 0);
|
|
2307
|
+
this.challenge = this.extra?.challenge;
|
|
2308
|
+
this.retryAfter = readNumber(
|
|
2309
|
+
this.extra?.retryAfter ?? this.extra?.retryAfterSeconds
|
|
2310
|
+
);
|
|
2311
|
+
this.remainingAttempts = readNumber(this.extra?.remainingAttempts);
|
|
2312
|
+
this.lockUntil = this.extra?.lockUntil ?? null;
|
|
2299
2313
|
}
|
|
2300
2314
|
};
|
|
2315
|
+
var isAuthClientError = (error) => {
|
|
2316
|
+
if (error instanceof AuthClientError) return true;
|
|
2317
|
+
return Boolean(
|
|
2318
|
+
error && typeof error === "object" && error.name === "AuthClientError"
|
|
2319
|
+
);
|
|
2320
|
+
};
|
|
2321
|
+
var getAuthErrorExtra = (error) => {
|
|
2322
|
+
if (!error || typeof error !== "object") return void 0;
|
|
2323
|
+
const record2 = error;
|
|
2324
|
+
return normalizeAuthErrorExtra(record2.extra || getRecordValue(record2.payload, "extra"));
|
|
2325
|
+
};
|
|
2326
|
+
var getAuthErrorReason = (error) => {
|
|
2327
|
+
if (!error || typeof error !== "object") return void 0;
|
|
2328
|
+
const record2 = error;
|
|
2329
|
+
const extra = getAuthErrorExtra(error);
|
|
2330
|
+
return extra?.reason || extra?.guardCode || (typeof record2.reason === "string" ? record2.reason : void 0) || (typeof record2.code === "string" ? record2.code : void 0);
|
|
2331
|
+
};
|
|
2332
|
+
var isAuthChallengeRequired = (error) => {
|
|
2333
|
+
if (!error || typeof error !== "object") return false;
|
|
2334
|
+
const record2 = error;
|
|
2335
|
+
const code = record2.code;
|
|
2336
|
+
const reason = getAuthErrorReason(error);
|
|
2337
|
+
const extra = getAuthErrorExtra(error);
|
|
2338
|
+
return code === 460 || code === "460" || code === "LOGIN_CHALLENGE_REQUIRED" || reason === "LOGIN_CHALLENGE_REQUIRED" || reason === "CHALLENGE_REQUIRED" || extra?.guardCode === "LOGIN_CHALLENGE_REQUIRED";
|
|
2339
|
+
};
|
|
2301
2340
|
var createAuthClient = ({
|
|
2302
2341
|
appType,
|
|
2303
2342
|
servicePrefix = "/service",
|
|
@@ -2324,7 +2363,12 @@ var createAuthClient = ({
|
|
|
2324
2363
|
if (!response.ok || success === false || !isSuccessCode2(code)) {
|
|
2325
2364
|
throw new AuthClientError(
|
|
2326
2365
|
String(getRecordValue(payload, "message") || `Auth request failed: ${response.status}`),
|
|
2327
|
-
{
|
|
2366
|
+
{
|
|
2367
|
+
status: response.status,
|
|
2368
|
+
code,
|
|
2369
|
+
payload,
|
|
2370
|
+
extra: normalizeAuthErrorExtra(getRecordValue(payload, "extra"))
|
|
2371
|
+
}
|
|
2328
2372
|
);
|
|
2329
2373
|
}
|
|
2330
2374
|
return unwrapPayload(payload);
|
|
@@ -2374,6 +2418,27 @@ var getRecordValue = (value, key) => {
|
|
|
2374
2418
|
if (!value || typeof value !== "object") return void 0;
|
|
2375
2419
|
return value[key];
|
|
2376
2420
|
};
|
|
2421
|
+
var normalizeAuthErrorExtra = (value) => {
|
|
2422
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
2423
|
+
const record2 = value;
|
|
2424
|
+
const challenge = record2.challenge && typeof record2.challenge === "object" && !Array.isArray(record2.challenge) ? record2.challenge : void 0;
|
|
2425
|
+
const reason = readString(record2.reason);
|
|
2426
|
+
const guardCode = readString(record2.guardCode);
|
|
2427
|
+
return {
|
|
2428
|
+
...record2,
|
|
2429
|
+
...reason ? { reason } : {},
|
|
2430
|
+
...guardCode ? { guardCode } : {},
|
|
2431
|
+
...challenge ? { challenge } : {},
|
|
2432
|
+
...readNumber(record2.retryAfter) !== void 0 ? { retryAfter: readNumber(record2.retryAfter) } : {},
|
|
2433
|
+
...readNumber(record2.retryAfterSeconds) !== void 0 ? { retryAfterSeconds: readNumber(record2.retryAfterSeconds) } : {},
|
|
2434
|
+
...readNumber(record2.remainingAttempts) !== void 0 ? { remainingAttempts: readNumber(record2.remainingAttempts) } : {}
|
|
2435
|
+
};
|
|
2436
|
+
};
|
|
2437
|
+
var readString = (value) => typeof value === "string" ? value : void 0;
|
|
2438
|
+
var readNumber = (value) => {
|
|
2439
|
+
const numberValue = Number(value);
|
|
2440
|
+
return Number.isFinite(numberValue) ? numberValue : void 0;
|
|
2441
|
+
};
|
|
2377
2442
|
var isSuccessCode2 = (code) => {
|
|
2378
2443
|
if (code === void 0 || code === null || code === "") return true;
|
|
2379
2444
|
const normalized = Number(code);
|
|
@@ -4114,6 +4179,7 @@ var LoginPage = ({
|
|
|
4114
4179
|
"login"
|
|
4115
4180
|
);
|
|
4116
4181
|
const [phoneChallengeId, setPhoneChallengeId] = (0, import_react7.useState)("");
|
|
4182
|
+
const [passwordChallenge, setPasswordChallenge] = (0, import_react7.useState)(null);
|
|
4117
4183
|
const [submitting, setSubmitting] = (0, import_react7.useState)(false);
|
|
4118
4184
|
const [sendingCode, setSendingCode] = (0, import_react7.useState)(false);
|
|
4119
4185
|
const [error, setError] = (0, import_react7.useState)(null);
|
|
@@ -4136,6 +4202,7 @@ var LoginPage = ({
|
|
|
4136
4202
|
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
4137
4203
|
PasswordLoginForm,
|
|
4138
4204
|
{
|
|
4205
|
+
challenge: passwordChallenge,
|
|
4139
4206
|
form: passwordForm,
|
|
4140
4207
|
loading: submitting,
|
|
4141
4208
|
onFinish: async (values) => {
|
|
@@ -4144,12 +4211,25 @@ var LoginPage = ({
|
|
|
4144
4211
|
try {
|
|
4145
4212
|
await handleSuccess(
|
|
4146
4213
|
await auth.passwordLogin({
|
|
4214
|
+
challengeAnswer: passwordChallenge ? values.challengeAnswer : void 0,
|
|
4215
|
+
challengeId: readChallengeId(passwordChallenge),
|
|
4216
|
+
clientFingerprint: getOrCreateLoginFingerprint(auth.client),
|
|
4147
4217
|
username: values.username,
|
|
4148
4218
|
password: values.password
|
|
4149
4219
|
})
|
|
4150
4220
|
);
|
|
4221
|
+
setPasswordChallenge(null);
|
|
4151
4222
|
} catch (loginError) {
|
|
4152
|
-
|
|
4223
|
+
if (isAuthChallengeRequired(loginError)) {
|
|
4224
|
+
const nextChallenge = getAuthErrorExtra(loginError)?.challenge;
|
|
4225
|
+
if (nextChallenge) {
|
|
4226
|
+
setPasswordChallenge(nextChallenge);
|
|
4227
|
+
passwordForm.setFieldValue("challengeAnswer", "");
|
|
4228
|
+
}
|
|
4229
|
+
} else {
|
|
4230
|
+
setPasswordChallenge(null);
|
|
4231
|
+
}
|
|
4232
|
+
setError(formatAuthErrorMessage(loginError));
|
|
4153
4233
|
} finally {
|
|
4154
4234
|
setSubmitting(false);
|
|
4155
4235
|
}
|
|
@@ -4219,6 +4299,7 @@ var LoginPage = ({
|
|
|
4219
4299
|
allowRegister,
|
|
4220
4300
|
auth,
|
|
4221
4301
|
handleSuccess,
|
|
4302
|
+
passwordChallenge,
|
|
4222
4303
|
passwordForm,
|
|
4223
4304
|
passwordMethod,
|
|
4224
4305
|
phoneChallengeId,
|
|
@@ -4372,7 +4453,7 @@ var LoginPage = ({
|
|
|
4372
4453
|
}
|
|
4373
4454
|
);
|
|
4374
4455
|
};
|
|
4375
|
-
var PasswordLoginForm = ({ form, loading, onFinish }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_antd2.Form, { form, layout: "vertical", requiredMark: false, onFinish, children: [
|
|
4456
|
+
var PasswordLoginForm = ({ challenge, form, loading, onFinish }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_antd2.Form, { form, layout: "vertical", requiredMark: false, onFinish, children: [
|
|
4376
4457
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
4377
4458
|
import_antd2.Form.Item,
|
|
4378
4459
|
{
|
|
@@ -4391,6 +4472,26 @@ var PasswordLoginForm = ({ form, loading, onFinish }) => /* @__PURE__ */ (0, imp
|
|
|
4391
4472
|
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd2.Input.Password, { autoComplete: "current-password" })
|
|
4392
4473
|
}
|
|
4393
4474
|
),
|
|
4475
|
+
challenge ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
4476
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
4477
|
+
import_antd2.Alert,
|
|
4478
|
+
{
|
|
4479
|
+
showIcon: true,
|
|
4480
|
+
type: "warning",
|
|
4481
|
+
message: "\u8BF7\u5B8C\u6210\u989D\u5916\u9A8C\u8BC1",
|
|
4482
|
+
description: readChallengeQuestion(challenge) || "\u8BF7\u8F93\u5165\u9A8C\u8BC1\u7801\u540E\u7EE7\u7EED\u767B\u5F55\u3002"
|
|
4483
|
+
}
|
|
4484
|
+
),
|
|
4485
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
4486
|
+
import_antd2.Form.Item,
|
|
4487
|
+
{
|
|
4488
|
+
label: "\u9A8C\u8BC1\u7B54\u6848",
|
|
4489
|
+
name: "challengeAnswer",
|
|
4490
|
+
rules: [{ required: true, message: "\u8BF7\u8F93\u5165\u9A8C\u8BC1\u7B54\u6848" }],
|
|
4491
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd2.Input, { autoComplete: "one-time-code" })
|
|
4492
|
+
}
|
|
4493
|
+
)
|
|
4494
|
+
] }) : null,
|
|
4394
4495
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd2.Button, { block: true, htmlType: "submit", icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_icons.LoginOutlined, {}), loading, type: "primary", children: "\u767B\u5F55" })
|
|
4395
4496
|
] });
|
|
4396
4497
|
var PhoneCodeLoginForm = ({
|
|
@@ -4452,6 +4553,22 @@ var PhoneCodeLoginForm = ({
|
|
|
4452
4553
|
] });
|
|
4453
4554
|
var findMethod = (methods, type4) => methods.find((method4) => method4.type === type4);
|
|
4454
4555
|
var normalizeError = (error) => error instanceof Error ? error : new Error(String(error || "\u8BF7\u6C42\u5931\u8D25"));
|
|
4556
|
+
var formatAuthErrorMessage = (error) => {
|
|
4557
|
+
const normalized = normalizeError(error);
|
|
4558
|
+
const extra = getAuthErrorExtra(error);
|
|
4559
|
+
const reason = getAuthErrorReason(error);
|
|
4560
|
+
if (isAuthChallengeRequired(error)) {
|
|
4561
|
+
return extra?.challenge?.question ? "\u8BF7\u5B8C\u6210\u4E0B\u65B9\u9A8C\u8BC1\u540E\u518D\u767B\u5F55" : normalized.message || "\u8BF7\u5148\u5B8C\u6210\u989D\u5916\u9A8C\u8BC1\u540E\u518D\u5C1D\u8BD5\u767B\u5F55";
|
|
4562
|
+
}
|
|
4563
|
+
if (reason === "LOGIN_BLOCKED" || reason === "USERNAME_BLOCKED" || reason === "IP_BLOCKED" || reason === "ACCOUNT_LOCKED") {
|
|
4564
|
+
const retryAfter = Number(extra?.retryAfter ?? extra?.retryAfterSeconds);
|
|
4565
|
+
if (Number.isFinite(retryAfter) && retryAfter > 0) {
|
|
4566
|
+
const minutes = Math.ceil(retryAfter / 60);
|
|
4567
|
+
return `\u767B\u5F55\u53D7\u9650\uFF0C\u8BF7\u7EA6 ${minutes} \u5206\u949F\u540E\u518D\u8BD5\u6216\u8054\u7CFB\u7BA1\u7406\u5458`;
|
|
4568
|
+
}
|
|
4569
|
+
}
|
|
4570
|
+
return normalized.message || "\u767B\u5F55\u5931\u8D25";
|
|
4571
|
+
};
|
|
4455
4572
|
var getString = (value, key) => {
|
|
4456
4573
|
if (!value || typeof value !== "object") return void 0;
|
|
4457
4574
|
const result = value[key];
|
|
@@ -4485,7 +4602,21 @@ var getOrCreateGuestIdentifier = (client) => {
|
|
|
4485
4602
|
window.localStorage.setItem(key, next);
|
|
4486
4603
|
return next;
|
|
4487
4604
|
};
|
|
4605
|
+
var getOrCreateLoginFingerprint = (client) => {
|
|
4606
|
+
const key = `openxiangda:${client.appType}:login_fingerprint`;
|
|
4607
|
+
if (typeof window === "undefined") return createGuestIdentifier();
|
|
4608
|
+
const current = window.localStorage.getItem(key);
|
|
4609
|
+
const id = current || createGuestIdentifier();
|
|
4610
|
+
if (!current) window.localStorage.setItem(key, id);
|
|
4611
|
+
return `${id}:${window.navigator?.userAgent || "unknown"}`;
|
|
4612
|
+
};
|
|
4488
4613
|
var createGuestIdentifier = () => `guest_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
4614
|
+
var readChallengeId = (challenge) => {
|
|
4615
|
+
if (!challenge) return void 0;
|
|
4616
|
+
const id = challenge.id || challenge.challengeId;
|
|
4617
|
+
return typeof id === "string" && id.trim() ? id.trim() : void 0;
|
|
4618
|
+
};
|
|
4619
|
+
var readChallengeQuestion = (challenge) => typeof challenge.question === "string" ? challenge.question : void 0;
|
|
4489
4620
|
var getCurrentHref3 = () => typeof window === "undefined" ? "" : window.location.href;
|
|
4490
4621
|
var getCurrentHostname = () => typeof window === "undefined" ? "" : window.location.hostname;
|
|
4491
4622
|
var getCallbackUrl = () => {
|