openxiangda 1.0.90 → 1.0.92
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 +12 -2
- package/lib/cli.js +2 -1
- package/lib/http.js +2 -2
- package/lib/utils.js +13 -0
- package/openxiangda-skills/SKILL.md +2 -2
- package/openxiangda-skills/references/architecture-design.md +1 -1
- package/openxiangda-skills/references/pages/page-sdk.md +8 -1
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +12 -2
- package/package.json +1 -1
- package/packages/sdk/dist/runtime/index.cjs +10 -6
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.mjs +263 -258
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/react.cjs +10 -6
- package/packages/sdk/dist/runtime/react.cjs.map +1 -1
- package/packages/sdk/dist/runtime/react.d.mts +3 -0
- package/packages/sdk/dist/runtime/react.d.ts +3 -0
- package/packages/sdk/dist/runtime/react.mjs +11 -6
- package/packages/sdk/dist/runtime/react.mjs.map +1 -1
- package/templates/openxiangda-react-spa/src/app/router.tsx +23 -1
package/README.md
CHANGED
|
@@ -205,16 +205,26 @@ import {
|
|
|
205
205
|
PublicAccessGate,
|
|
206
206
|
} from "openxiangda/runtime/react"
|
|
207
207
|
|
|
208
|
+
const PublicLoading = () => <div role="status">正在进入公开页面</div>
|
|
209
|
+
const PublicAccessError = ({ error }: { error: { message?: string } }) => (
|
|
210
|
+
<div role="alert">{error.message || "公开链接不可用或已过期"}</div>
|
|
211
|
+
)
|
|
212
|
+
|
|
208
213
|
<OpenXiangdaProvider appType={appType} servicePrefix="/service">
|
|
209
214
|
<OpenXiangdaPageProvider>
|
|
210
|
-
<PublicAccessGate
|
|
215
|
+
<PublicAccessGate
|
|
216
|
+
errorFallback={error => <PublicAccessError error={error} />}
|
|
217
|
+
fallback={<PublicLoading />}
|
|
218
|
+
policyCode="public_register"
|
|
219
|
+
routeCode="public.register"
|
|
220
|
+
>
|
|
211
221
|
<PublicRegisterPage />
|
|
212
222
|
</PublicAccessGate>
|
|
213
223
|
</OpenXiangdaPageProvider>
|
|
214
224
|
</OpenXiangdaProvider>
|
|
215
225
|
```
|
|
216
226
|
|
|
217
|
-
不要把 `PublicAccessGate` 单独放在没有 `OpenXiangdaProvider` / `OpenXiangdaPageProvider` 的页面树下;否则 public session 不能注入后续 SDK 请求,`usePageSdk()` 也会抛出 `usePageSdkStore 必须在 PageProvider
|
|
227
|
+
不要把 `PublicAccessGate` 单独放在没有 `OpenXiangdaProvider` / `OpenXiangdaPageProvider` 的页面树下;否则 public session 不能注入后续 SDK 请求,`usePageSdk()` 也会抛出 `usePageSdkStore 必须在 PageProvider 内使用`。公开页面必须给 `PublicAccessGate` 配 `fallback` 和 `errorFallback`,避免慢网、缺 ticket、ticket 过期时出现空白页。
|
|
218
228
|
|
|
219
229
|
多表只读查询和固定口径统计优先声明 `src/resources/data-views/*.json` 数据视图,而不是在页面里手写多次单表查询再拼数据。默认 `storageMode: "materialized"` 会创建 PostgreSQL materialized view,适合读多写少和可接受刷新延迟的列表/报表;`storageMode: "live"` 每次查询实时编译逻辑视图,适合强实时但数据量可控的复杂查询。`viewType: "aggregate"` 是统计聚合视图,适合按客户、状态、月份等维度聚合 count/sum/avg/min/max。发布时 CLI 会把 `formCode` 解析为当前 profile 的 `formUuid`;页面通过 `sdk.dataView.query(code, params)` 查询行级视图,通过 `sdk.dataView.stats(code, params)` 查询聚合视图,也可以用 `sdk.dataSource.run()` 路由 `dataView.query` / `dataView.stats`。materialized 模式应为常用筛选、排序、统计维度和时间桶声明 `indexes`,并确认用户能接受的刷新延迟;live 模式忽略 `indexes`,不需要刷新。
|
|
220
230
|
|
package/lib/cli.js
CHANGED
|
@@ -24,6 +24,7 @@ const { assertCanInitializeWorkspace, initWorkspace } = require('./workspace-ini
|
|
|
24
24
|
const { bootstrapWorkspace } = require('./workspace-bootstrap');
|
|
25
25
|
const {
|
|
26
26
|
fail,
|
|
27
|
+
formatFetchError,
|
|
27
28
|
maskText,
|
|
28
29
|
openBrowser,
|
|
29
30
|
parseArgs,
|
|
@@ -7972,7 +7973,7 @@ async function requestForm(baseUrl, apiPath, formData, accessToken, options = {}
|
|
|
7972
7973
|
if (isAbortError(error)) {
|
|
7973
7974
|
throw new Error(maskText(`HTTP form request timed out after ${timeoutMs}ms: ${apiPath}`));
|
|
7974
7975
|
}
|
|
7975
|
-
throw error;
|
|
7976
|
+
throw new Error(formatFetchError(error, apiPath));
|
|
7976
7977
|
} finally {
|
|
7977
7978
|
clearTimeout(timer);
|
|
7978
7979
|
}
|
package/lib/http.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { maskText } = require('./utils');
|
|
1
|
+
const { formatFetchError, maskText } = require('./utils');
|
|
2
2
|
|
|
3
3
|
const DEFAULT_REQUEST_TIMEOUT_MS = 60000;
|
|
4
4
|
|
|
@@ -35,7 +35,7 @@ async function requestJson(baseUrl, apiPath, options = {}) {
|
|
|
35
35
|
if (isAbortError(error)) {
|
|
36
36
|
throw new Error(maskText(`HTTP request timed out after ${timeoutMs}ms: ${apiPath}`));
|
|
37
37
|
}
|
|
38
|
-
throw error;
|
|
38
|
+
throw new Error(formatFetchError(error, apiPath));
|
|
39
39
|
} finally {
|
|
40
40
|
clearTimeout(timer);
|
|
41
41
|
}
|
package/lib/utils.js
CHANGED
|
@@ -11,6 +11,18 @@ function maskText(value) {
|
|
|
11
11
|
.replace(/\b1[3-9]\d{9}\b/g, match => `${match.slice(0, 3)}****${match.slice(-4)}`);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function formatFetchError(error, context) {
|
|
15
|
+
const message = error && error.message ? String(error.message) : String(error || 'fetch failed');
|
|
16
|
+
const cause = error && error.cause;
|
|
17
|
+
const causeParts = [];
|
|
18
|
+
if (cause?.code) causeParts.push(String(cause.code));
|
|
19
|
+
if (cause?.name) causeParts.push(String(cause.name));
|
|
20
|
+
if (cause?.message) causeParts.push(String(cause.message));
|
|
21
|
+
const suffix = causeParts.length ? `; cause=${causeParts.join(': ')}` : '';
|
|
22
|
+
const contextSuffix = context ? `; request=${context}` : '';
|
|
23
|
+
return maskText(`${message}${suffix}${contextSuffix}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
function print(value) {
|
|
15
27
|
console.log(maskText(value));
|
|
16
28
|
}
|
|
@@ -81,6 +93,7 @@ function writeJson(value) {
|
|
|
81
93
|
|
|
82
94
|
module.exports = {
|
|
83
95
|
fail,
|
|
96
|
+
formatFetchError,
|
|
84
97
|
maskText,
|
|
85
98
|
openBrowser,
|
|
86
99
|
parseArgs,
|
|
@@ -45,7 +45,7 @@ OpenXiangda supports two workspace modes. Classic `sy-lowcode-app-workspace` pub
|
|
|
45
45
|
- ✅ Run `openxiangda update check --json` at the start of substantial work; if `updateAvailable`, run `openxiangda update install` and `openxiangda skill install --force`.
|
|
46
46
|
- ✅ For non-trivial app requirements, run the architecture design gate first: forms, fields, pages, permissions, data views, status/workflow, automation, notifications, and development tasks must be decided before coding.
|
|
47
47
|
- ✅ For app login/auth requirements, run the auth security gate before coding: enabled methods, registration policy, identity matching keys, provider boundary, default roles, rate limits, audit fields, and third-party ownership must be confirmed.
|
|
48
|
-
- ✅ For public/no-login access requirements, run the public access design gate before coding: public routes, external role codes, guest vs ticket, form/dataView/function/connector grants,
|
|
48
|
+
- ✅ For public/no-login access requirements, run the public access design gate before coding: public routes, external role codes, guest vs ticket, form/dataView/function/connector grants, backend permission groups, and visible loading/error fallback states must be confirmed. New React SPA apps use `/view/:appType/public/*`, `src/resources/routes/`, `src/resources/public-access/`, and `PublicAccessGate`; route children that use Page SDK hooks must be inside `OpenXiangdaProvider` + `OpenXiangdaPageProvider`.
|
|
49
49
|
- ✅ Public/no-login validation must inspect the JSON envelope, not only HTTP status. HTTP 200 with `code: "PUBLIC_GRANT_DENIED"` is a denied request; success requires `code` `200`, `"200"`, or compatible `0`, and no `success: false`.
|
|
50
50
|
- ✅ For form-entry UX, pick components in this order: OpenXiangda platform form components → `antd` / `antd-mobile` wrappers → custom component only when neither exists.
|
|
51
51
|
- ✅ List pages, linked options, remote selectors, and report drill-downs must use paginated server-side queries with explicit searchable fields. Do not fetch a large page and filter in local state.
|
|
@@ -149,7 +149,7 @@ When the user provides a root domain such as `https://yida.wisejob.cn/`, use it
|
|
|
149
149
|
- For external APIs, create a connector manifest in `src/resources/connectors/` and call it from pages through `sdk.connector`; never put third-party API keys in page source.
|
|
150
150
|
- For reusable backend logic shared by pages, automations, and workflows, create an App Function in `src/resources/functions/<functionCode>.json` plus `src/functions/<functionCode>/index.ts`. Call it from pages with `sdk.function.invoke`, from automation/workflow graphs with `function_call`, or from the runtime endpoint when the caller has app automation management permission. Current App Function MVP exposes controlled helpers such as `ctx.resources`, `ctx.form`, `ctx.dataView`, `ctx.connector`, `ctx.notification`, and `ctx.platform.api`; it does not expose raw SQL or Redis.
|
|
151
151
|
- For application login, declare auth resources in `src/resources/auth/<code>.json` and publish them with `openxiangda resource publish`. App Function auth providers may validate external credentials and return only an identity assertion; they must never issue tokens, set cookies, or write platform user/binding tables directly. The platform auth service decides create/bind/reject and issues tokens.
|
|
152
|
-
- For external/no-login pages in React SPA apps, declare `src/resources/routes/<code>.json` and `src/resources/public-access/<code>.json`, put the page under `/view/:appType/public/*`, and use `PublicAccessGate` or `createPublicAccessClient`. The route tree must also include `OpenXiangdaPageProvider` inside `OpenXiangdaProvider` before any `usePageSdk()` / `usePageContext()` / `useDataSource()` call; otherwise pages throw `usePageSdkStore 必须在 PageProvider 内使用`. Public guest access to forms, dataViews, functions, and connectors is denied unless policy `grants` explicitly names the resource; backend permission groups still apply.
|
|
152
|
+
- For external/no-login pages in React SPA apps, declare `src/resources/routes/<code>.json` and `src/resources/public-access/<code>.json`, put the page under `/view/:appType/public/*`, and use `PublicAccessGate` or `createPublicAccessClient`. The route tree must also include `OpenXiangdaPageProvider` inside `OpenXiangdaProvider` before any `usePageSdk()` / `usePageContext()` / `useDataSource()` call; otherwise pages throw `usePageSdkStore 必须在 PageProvider 内使用`. Always provide `fallback` and `errorFallback` for public routes so slow networks, missing tickets, or expired tickets do not render blank pages. Public guest access to forms, dataViews, functions, and connectors is denied unless policy `grants` explicitly names the resource; backend permission groups still apply.
|
|
153
153
|
- When verifying public access, parse the response body and reject string business errors such as `PUBLIC_GRANT_DENIED` even if the HTTP status is 200. Do not use `response.ok` alone as the success condition.
|
|
154
154
|
- Before scaffolding a real app, complex page, data management page, portal shell, status lifecycle, role governance, or automation, read `references/best-practices.md` and pick the architecture first. Prefer copying the relevant pattern from `examples/best-practices/` into `src/` instead of generating a single large page file.
|
|
155
155
|
- Before publishing to another platform, verify the workspace is bound for that profile. Resource IDs from one profile must not be reused for another profile.
|
|
@@ -264,7 +264,7 @@ For simple CRUD/admin lists, design can proceed with the appropriate OpenXiangda
|
|
|
264
264
|
- Public policies live under `src/resources/public-access/`.
|
|
265
265
|
- Policy `externalRoleCodes` are virtual role codes carried by the scoped public token; use the same codes in page/form/dataView permission groups.
|
|
266
266
|
- Policy `grants` must list every form/dataView/function/connector the public page can access. Do not rely on broad guest permissions.
|
|
267
|
-
- Use `PublicAccessGate` in React routes or `createPublicAccessClient` for custom bootstrapping. If the page uses Page SDK hooks, the route tree must have `OpenXiangdaProvider` plus `OpenXiangdaPageProvider`; `PublicAccessGate` alone is not enough.
|
|
267
|
+
- Use `PublicAccessGate` in React routes or `createPublicAccessClient` for custom bootstrapping. If the page uses Page SDK hooks, the route tree must have `OpenXiangdaProvider` plus `OpenXiangdaPageProvider`; `PublicAccessGate` alone is not enough. Public routes must define visible loading and error fallback states so slow networks, missing tickets, or expired tickets do not become blank pages.
|
|
268
268
|
- In validation scripts, success requires the JSON envelope success code (`200`, `"200"`, or compatible `0`). HTTP 200 with `code: "PUBLIC_GRANT_DENIED"` is a denied request, not a success.
|
|
269
269
|
- Validation must include at least one real browser render check for each public and private route, because API-only E2E can miss provider/runtime crashes such as `usePageSdkStore 必须在 PageProvider 内使用`.
|
|
270
270
|
- Old `?publicAccess=guest` and form `publicAccess` settings are legacy compatibility and should not appear in new-app designs.
|
|
@@ -152,11 +152,18 @@ import {
|
|
|
152
152
|
PublicAccessGate,
|
|
153
153
|
} from "openxiangda/runtime/react";
|
|
154
154
|
|
|
155
|
+
const PublicLoading = () => <div role="status">正在进入公开页面</div>;
|
|
156
|
+
const PublicAccessError = ({ error }: { error: { message?: string } }) => (
|
|
157
|
+
<div role="alert">{error.message || "公开链接不可用或已过期"}</div>
|
|
158
|
+
);
|
|
159
|
+
|
|
155
160
|
export function PublicRegisterRoute() {
|
|
156
161
|
return (
|
|
157
162
|
<OpenXiangdaProvider appType={appType} servicePrefix="/service">
|
|
158
163
|
<OpenXiangdaPageProvider>
|
|
159
164
|
<PublicAccessGate
|
|
165
|
+
errorFallback={error => <PublicAccessError error={error} />}
|
|
166
|
+
fallback={<PublicLoading />}
|
|
160
167
|
policyCode="public_register"
|
|
161
168
|
routeCode="public.register"
|
|
162
169
|
>
|
|
@@ -183,7 +190,7 @@ await publicAccess.startSession({
|
|
|
183
190
|
});
|
|
184
191
|
```
|
|
185
192
|
|
|
186
|
-
`PublicAccessGate` stores the returned bearer token in the runtime provider and injects it into follow-up SDK requests. It does not replace `OpenXiangdaPageProvider`. If you call `createPublicAccessClient` directly outside the provider, pass the returned `accessToken` as `Authorization: Bearer <token>` for follow-up APIs. The matching resources must exist under `src/resources/routes/` and `src/resources/public-access/`. Public guest access to forms, data views, functions, and connectors is denied unless the policy `grants` explicitly names that resource.
|
|
193
|
+
`PublicAccessGate` stores the returned bearer token in the runtime provider and injects it into follow-up SDK requests. It does not replace `OpenXiangdaPageProvider`. Always provide `fallback` and `errorFallback` so slow networks, missing tickets, or expired tickets do not render a blank public page. If you call `createPublicAccessClient` directly outside the provider, pass the returned `accessToken` as `Authorization: Bearer <token>` for follow-up APIs. The matching resources must exist under `src/resources/routes/` and `src/resources/public-access/`. Public guest access to forms, data views, functions, and connectors is denied unless the policy `grants` explicitly names that resource.
|
|
187
194
|
|
|
188
195
|
When testing public pages or writing direct fetch wrappers, check the JSON envelope, not only `response.ok`. Platform runtime APIs can return HTTP 200 with `code: "PUBLIC_GRANT_DENIED"`; this is a failed request. Treat only `code === 200`, `code === "200"`, or compatible success code `0` as success, and fail on `success: false`.
|
|
189
196
|
|
|
@@ -125,16 +125,26 @@ import {
|
|
|
125
125
|
PublicAccessGate,
|
|
126
126
|
} from "openxiangda/runtime/react";
|
|
127
127
|
|
|
128
|
+
const PublicLoading = () => <div role="status">正在进入公开页面</div>;
|
|
129
|
+
const PublicAccessError = ({ error }: { error: { message?: string } }) => (
|
|
130
|
+
<div role="alert">{error.message || "公开链接不可用或已过期"}</div>
|
|
131
|
+
);
|
|
132
|
+
|
|
128
133
|
<OpenXiangdaProvider appType={appType} servicePrefix="/service">
|
|
129
134
|
<OpenXiangdaPageProvider>
|
|
130
|
-
<PublicAccessGate
|
|
135
|
+
<PublicAccessGate
|
|
136
|
+
errorFallback={error => <PublicAccessError error={error} />}
|
|
137
|
+
fallback={<PublicLoading />}
|
|
138
|
+
policyCode="public_register"
|
|
139
|
+
routeCode="public.register"
|
|
140
|
+
>
|
|
131
141
|
<PublicRegisterPage />
|
|
132
142
|
</PublicAccessGate>
|
|
133
143
|
</OpenXiangdaPageProvider>
|
|
134
144
|
</OpenXiangdaProvider>
|
|
135
145
|
```
|
|
136
146
|
|
|
137
|
-
React SPA 中 `OpenXiangdaProvider` 负责 runtime/bootstrap 和 public token 注入,`OpenXiangdaPageProvider` 负责 `usePageSdk()` / `usePageContext()` 的 Page SDK 上下文。缺少 `OpenXiangdaPageProvider` 会抛出 `usePageSdkStore 必须在 PageProvider
|
|
147
|
+
React SPA 中 `OpenXiangdaProvider` 负责 runtime/bootstrap 和 public token 注入,`OpenXiangdaPageProvider` 负责 `usePageSdk()` / `usePageContext()` 的 Page SDK 上下文。缺少 `OpenXiangdaPageProvider` 会抛出 `usePageSdkStore 必须在 PageProvider 内使用`。公开页面必须给 `PublicAccessGate` 配 `fallback` 和 `errorFallback`,避免慢网、缺 ticket、ticket 过期时出现空白页。
|
|
138
148
|
|
|
139
149
|
公开访问验证必须检查 JSON envelope。HTTP 200 但 `code: "PUBLIC_GRANT_DENIED"` 代表后端已拒绝;只有 `code` 为 `200`、`"200"` 或兼容成功码 `0`,且没有 `success: false` 时才算成功。
|
|
140
150
|
|
package/package.json
CHANGED
|
@@ -4519,9 +4519,12 @@ var OpenXiangdaProvider = ({
|
|
|
4519
4519
|
[appType]
|
|
4520
4520
|
);
|
|
4521
4521
|
const [accessToken, setAccessTokenState] = (0, import_react8.useState)(null);
|
|
4522
|
+
const accessTokenRef = (0, import_react8.useRef)(null);
|
|
4522
4523
|
const setAccessToken = (0, import_react8.useCallback)(
|
|
4523
4524
|
(nextAccessToken) => {
|
|
4524
|
-
|
|
4525
|
+
const normalizedAccessToken = nextAccessToken || null;
|
|
4526
|
+
accessTokenRef.current = normalizedAccessToken;
|
|
4527
|
+
setAccessTokenState(normalizedAccessToken);
|
|
4525
4528
|
},
|
|
4526
4529
|
[]
|
|
4527
4530
|
);
|
|
@@ -4547,7 +4550,7 @@ var OpenXiangdaProvider = ({
|
|
|
4547
4550
|
return;
|
|
4548
4551
|
}
|
|
4549
4552
|
setState((prev) => ({ ...prev, loading: true, error: null }));
|
|
4550
|
-
const requestFetch = options.accessToken !== void 0 ? createAuthorizedFetch(resolvedFetch, options.accessToken || null) :
|
|
4553
|
+
const requestFetch = options.accessToken !== void 0 ? createAuthorizedFetch(resolvedFetch, options.accessToken || null) : createAuthorizedFetch(resolvedFetch, accessTokenRef.current);
|
|
4551
4554
|
try {
|
|
4552
4555
|
const response = await requestFetch(
|
|
4553
4556
|
buildServiceUrl3(
|
|
@@ -4581,7 +4584,7 @@ var OpenXiangdaProvider = ({
|
|
|
4581
4584
|
error: normalizeRuntimeError(error)
|
|
4582
4585
|
});
|
|
4583
4586
|
}
|
|
4584
|
-
}, [
|
|
4587
|
+
}, [resolvedAppType, resolvedFetch, servicePrefix]);
|
|
4585
4588
|
(0, import_react8.useEffect)(() => {
|
|
4586
4589
|
void reload();
|
|
4587
4590
|
}, [reload]);
|
|
@@ -4591,10 +4594,11 @@ var OpenXiangdaProvider = ({
|
|
|
4591
4594
|
appType: resolvedAppType,
|
|
4592
4595
|
servicePrefix,
|
|
4593
4596
|
fetchImpl: authorizedFetch,
|
|
4597
|
+
baseFetchImpl: resolvedFetch,
|
|
4594
4598
|
reload,
|
|
4595
4599
|
setAccessToken
|
|
4596
4600
|
}),
|
|
4597
|
-
[authorizedFetch, reload, resolvedAppType, servicePrefix, state]
|
|
4601
|
+
[authorizedFetch, reload, resolvedAppType, resolvedFetch, servicePrefix, state]
|
|
4598
4602
|
);
|
|
4599
4603
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(OpenXiangdaRuntimeContext.Provider, { value, children });
|
|
4600
4604
|
};
|
|
@@ -5142,14 +5146,14 @@ var usePublicAccess = (options = {}) => {
|
|
|
5142
5146
|
const {
|
|
5143
5147
|
appType: runtimeAppType,
|
|
5144
5148
|
servicePrefix: runtimeServicePrefix,
|
|
5145
|
-
|
|
5149
|
+
baseFetchImpl: runtimeBaseFetchImpl,
|
|
5146
5150
|
reload: reloadRuntime,
|
|
5147
5151
|
setAccessToken
|
|
5148
5152
|
} = runtime;
|
|
5149
5153
|
const {
|
|
5150
5154
|
appType = runtimeAppType,
|
|
5151
5155
|
servicePrefix = runtimeServicePrefix,
|
|
5152
|
-
fetchImpl =
|
|
5156
|
+
fetchImpl = runtimeBaseFetchImpl,
|
|
5153
5157
|
autoStart = true,
|
|
5154
5158
|
...sessionInput
|
|
5155
5159
|
} = options;
|