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.
Files changed (28) hide show
  1. package/lib/app-function-invoke.js +28 -0
  2. package/lib/cli.js +55 -9
  3. package/lib/design-gates.js +1 -1
  4. package/lib/design-review.js +220 -0
  5. package/lib/http.js +4 -1
  6. package/lib/workspace-init.js +19 -2
  7. package/package.json +2 -1
  8. package/packages/sdk/dist/runtime/index.cjs +136 -5
  9. package/packages/sdk/dist/runtime/index.cjs.map +1 -1
  10. package/packages/sdk/dist/runtime/index.d.mts +1 -1
  11. package/packages/sdk/dist/runtime/index.d.ts +1 -1
  12. package/packages/sdk/dist/runtime/index.mjs +169 -38
  13. package/packages/sdk/dist/runtime/index.mjs.map +1 -1
  14. package/packages/sdk/dist/runtime/react.cjs +134 -3
  15. package/packages/sdk/dist/runtime/react.cjs.map +1 -1
  16. package/packages/sdk/dist/runtime/react.d.mts +39 -7
  17. package/packages/sdk/dist/runtime/react.d.ts +39 -7
  18. package/packages/sdk/dist/runtime/react.mjs +143 -12
  19. package/packages/sdk/dist/runtime/react.mjs.map +1 -1
  20. package/packages/sdk/src/build-source/scripts/build-forms.runtime-entry.test.ts +1 -1
  21. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +25 -0
  22. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +33 -0
  23. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +25 -0
  24. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +33 -0
  25. package/templates/openxiangda-react-spa/AGENTS.md +3 -0
  26. package/templates/openxiangda-react-spa/package.json +4 -1
  27. package/templates/openxiangda-react-spa/scripts/deploy.mjs +42 -0
  28. 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
- fail('用法: openxiangda design gates|template [--topic code] [--json]');
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
- return runDirectRequest(config, target, flags, {
2668
- method: 'POST',
2669
- path: `/${encodeURIComponent(target.appType)}/v1/functions/${encodeURIComponent(functionCode)}/invoke.json`,
2670
- body,
2671
- strictEnvelope: true,
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',
@@ -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
- throw new Error(maskText(`HTTP ${response.status}: ${message}`));
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;
@@ -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.103",
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 Fragment18 = REACT_FRAGMENT_TYPE2;
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 = Fragment18;
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
- { status: response.status, code, payload }
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
- setError(normalizeError(loginError).message);
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 = () => {