openxiangda 1.0.115 → 1.0.117

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/cli.js CHANGED
@@ -5338,6 +5338,11 @@ const RESOURCE_TYPE_ALIASES = new Map([
5338
5338
  ['formsettings', 'formSettings'],
5339
5339
  ]);
5340
5340
 
5341
+ const RESOURCE_TYPE_ALIAS_GROUPS = new Map([
5342
+ ['permission', ['pagePermissionGroups', 'formPermissionGroups']],
5343
+ ['permissions', ['pagePermissionGroups', 'formPermissionGroups']],
5344
+ ]);
5345
+
5341
5346
  function getResourceTypeFilters(positional = [], flags = {}) {
5342
5347
  const rawValues = [
5343
5348
  ...positional,
@@ -5355,9 +5360,17 @@ function getResourceTypeFilters(positional = [], flags = {}) {
5355
5360
  const filters = new Set();
5356
5361
  for (const rawType of rawValues) {
5357
5362
  const normalized = normalizeResourceTypeAlias(rawType);
5363
+ const group = RESOURCE_TYPE_ALIAS_GROUPS.get(normalized);
5364
+ if (group) {
5365
+ for (const key of group) filters.add(key);
5366
+ continue;
5367
+ }
5358
5368
  const key = RESOURCE_TYPE_ALIASES.get(normalized);
5359
5369
  if (!key) {
5360
- fail(`未知资源类型: ${rawType}`);
5370
+ fail(
5371
+ `未知资源类型: ${rawType}。常用类型包括 workflow, notification, route, menu, role, ` +
5372
+ `page-permission-group, form-permission-group, public-access。`
5373
+ );
5361
5374
  }
5362
5375
  filters.add(key);
5363
5376
  }
@@ -9804,6 +9817,42 @@ function isUnauthorized(error) {
9804
9817
  );
9805
9818
  }
9806
9819
 
9820
+ function isTransientNetworkError(error) {
9821
+ const message = String(error?.message || error || '').toLowerCase();
9822
+ return (
9823
+ message.includes('und_err_connect_timeout') ||
9824
+ message.includes('connect_timeout') ||
9825
+ message.includes('econnreset') ||
9826
+ message.includes('socket hang up') ||
9827
+ message.includes('network socket disconnected') ||
9828
+ message.includes('fetch failed')
9829
+ );
9830
+ }
9831
+
9832
+ function requestMethodAllowsTransientRetry(options = {}) {
9833
+ const method = String(options.method || 'GET').toUpperCase();
9834
+ return method === 'GET' || method === 'HEAD';
9835
+ }
9836
+
9837
+ async function retryTransientFetch(fn, options = {}) {
9838
+ const retries = Number.isFinite(Number(options.retries))
9839
+ ? Number(options.retries)
9840
+ : 2;
9841
+ let lastError;
9842
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
9843
+ try {
9844
+ return await fn(attempt);
9845
+ } catch (error) {
9846
+ lastError = error;
9847
+ if (attempt >= retries || !isTransientNetworkError(error)) {
9848
+ throw error;
9849
+ }
9850
+ await sleep(Math.min(500 * Math.pow(2, attempt), 3000));
9851
+ }
9852
+ }
9853
+ throw lastError;
9854
+ }
9855
+
9807
9856
  async function requestWithAuth(config, profileName, apiPath, options = {}) {
9808
9857
  const resolved = getProfile(config, profileName);
9809
9858
  const profile = resolved.profile;
@@ -9811,22 +9860,25 @@ async function requestWithAuth(config, profileName, apiPath, options = {}) {
9811
9860
  fail(`profile ${resolved.profileName} 未登录,请先执行 openxiangda login --profile ${resolved.profileName}`);
9812
9861
  }
9813
9862
  const { strictEnvelope, ...requestOptions } = options;
9814
-
9815
- try {
9816
- const payload = await requestJson(profile.baseUrl, apiPath, {
9863
+ const send = () =>
9864
+ requestJson(profile.baseUrl, apiPath, {
9817
9865
  ...requestOptions,
9818
9866
  accessToken: profile.token.accessToken,
9819
9867
  });
9868
+ const sendWithRetry = () =>
9869
+ requestMethodAllowsTransientRetry(requestOptions)
9870
+ ? retryTransientFetch(send, requestOptions)
9871
+ : send();
9872
+
9873
+ try {
9874
+ const payload = await sendWithRetry();
9820
9875
  return strictEnvelope ? unwrapStrictApi(payload) : unwrapApi(payload);
9821
9876
  } catch (error) {
9822
9877
  if (!isUnauthorized(error) || !profile.token?.refreshToken) {
9823
9878
  throw error;
9824
9879
  }
9825
9880
  await refreshProfile(config, resolved.profileName);
9826
- const payload = await requestJson(profile.baseUrl, apiPath, {
9827
- ...requestOptions,
9828
- accessToken: profile.token.accessToken,
9829
- });
9881
+ const payload = await sendWithRetry();
9830
9882
  return strictEnvelope ? unwrapStrictApi(payload) : unwrapApi(payload);
9831
9883
  }
9832
9884
  }
@@ -9837,28 +9889,24 @@ async function requestFormWithAuth(config, profileName, apiPath, formDataFactory
9837
9889
  if (!profile.token?.accessToken) {
9838
9890
  fail(`profile ${resolved.profileName} 未登录,请先执行 openxiangda login --profile ${resolved.profileName}`);
9839
9891
  }
9840
-
9841
- try {
9842
- const payload = await requestForm(
9892
+ const send = () =>
9893
+ requestForm(
9843
9894
  profile.baseUrl,
9844
9895
  apiPath,
9845
9896
  formDataFactory(),
9846
9897
  profile.token.accessToken,
9847
9898
  options
9848
9899
  );
9900
+
9901
+ try {
9902
+ const payload = await retryTransientFetch(send, options);
9849
9903
  return unwrapApi(payload);
9850
9904
  } catch (error) {
9851
9905
  if (!isUnauthorized(error) || !profile.token?.refreshToken) {
9852
9906
  throw error;
9853
9907
  }
9854
9908
  await refreshProfile(config, resolved.profileName);
9855
- const payload = await requestForm(
9856
- profile.baseUrl,
9857
- apiPath,
9858
- formDataFactory(),
9859
- profile.token.accessToken,
9860
- options
9861
- );
9909
+ const payload = await retryTransientFetch(send, options);
9862
9910
  return unwrapApi(payload);
9863
9911
  }
9864
9912
  }
package/lib/http.js CHANGED
@@ -2,6 +2,8 @@ const { formatFetchError, maskText } = require('./utils');
2
2
 
3
3
  const DEFAULT_REQUEST_TIMEOUT_MS = 60000;
4
4
 
5
+ configureFetchDispatcher(DEFAULT_REQUEST_TIMEOUT_MS);
6
+
5
7
  async function requestJson(baseUrl, apiPath, options = {}) {
6
8
  if (typeof fetch !== 'function') {
7
9
  throw new Error('当前 Node.js 版本不支持 fetch,请使用 Node.js 18 或更高版本');
@@ -77,3 +79,20 @@ function isAbortError(error) {
77
79
  module.exports = {
78
80
  requestJson,
79
81
  };
82
+
83
+ function configureFetchDispatcher(timeoutMs) {
84
+ try {
85
+ // Node fetch uses undici internally. Setting the global dispatcher prevents
86
+ // large resource operations from failing on undici's 10s connect default.
87
+ const { Agent, setGlobalDispatcher } = require('undici');
88
+ setGlobalDispatcher(
89
+ new Agent({
90
+ connect: {
91
+ timeout: timeoutMs,
92
+ },
93
+ })
94
+ );
95
+ } catch {
96
+ // Keep working on runtimes where undici is not directly importable.
97
+ }
98
+ }
@@ -24,7 +24,7 @@ export default defineWorkflow({
24
24
  flow.action.transfer(),
25
25
  flow.action.return(),
26
26
  ],
27
- returnPolicy: { scopeType: "previous_all", resubmitMode: "resume_current" },
27
+ returnPolicy: { scopeType: "initiator", resubmitMode: "resume_current" },
28
28
  fieldPermissions: [{ fieldId: "amount", fieldBehavior: "READONLY" }],
29
29
  });
30
30
  const sync = flow.functionCall("sync_budget", {
@@ -108,6 +108,8 @@ Runtime pages should not guess buttons. Use `sdk.process.resolveCapabilities(...
108
108
 
109
109
  For delayed approval start, do not submit a process form and then delete/recreate records. Save the process-form instance without starting workflow via `StandardFormPage submitBehavior="save-draft"` or `sdk.form.create({ formUuid, data, saveAsDraft: true, startProcess: false })`. Later start approval on that same `formInstId` with `sdk.process.startFromExistingInstance({ formUuid, formInstId })` or `StandardFormPage submitBehavior="start-existing-process"`.
110
110
 
111
+ For return-to-initiator workflows, prefer `returnPolicy: { scopeType: "initiator", resubmitMode: "resume_current" }` or `flow.action.returnToInitiator()`. The runtime capabilities protocol exposes `resubmit` only on pending `originator_return` tasks. If a user returns to a previous approval node, continuing the flow is a normal `approve` action on that returned approval task.
112
+
111
113
  Minimum shape:
112
114
 
113
115
  ```json
@@ -25,6 +25,7 @@ Create a workflow only when the scenario has **real approval semantics**: approv
25
25
  - ✅ New AI-authored workflows: prefer **semantic DSL** `src/workflows/<code>/workflow.ts` using `defineWorkflow`; compile it to v3 designer JSON with `openxiangda workflow compile`.
26
26
  - ✅ Runtime workflow pages: use `sdk.process.resolveCapabilities(...)` or `ProcessActionBar` / `ProcessTimeline` from `openxiangda/runtime/react`; do not hard-code buttons.
27
27
  - ✅ Delayed approval start: save the process-form record first with `StandardFormPage submitBehavior="save-draft"` or `sdk.form.create({ formUuid, data, saveAsDraft: true, startProcess: false })`, then start approval in place with `sdk.process.startFromExistingInstance({ formUuid, formInstId })` or `StandardFormPage submitBehavior="start-existing-process"`.
28
+ - ✅ Return-to-initiator flows: use `returnPolicy: { scopeType: "initiator", resubmitMode: "resume_current" | "replay" }` or `flow.action.returnToInitiator()`. The runtime capabilities protocol exposes `resubmit` only for pending `originator_return` tasks; return to a previous approval node continues through the normal `approve` action.
28
29
  - ✅ Reusable backend business logic: prefer **App Function** (`src/functions/<functionCode>/index.ts` + `src/resources/functions/<functionCode>.json`), then call it from page `sdk.function.invoke` or automation/workflow `function_call`.
29
30
  - ✅ JS_CODE V2 trusted_node: source in TypeScript under `src/js-code-nodes/<scriptCode>/index.ts`, `src/automations/<resourceCode>/index.ts`, or `src/functions/<functionCode>/index.ts`. Run `pnpm build-js-code --script <code>` (validates with `tsc` first).
30
31
  - ✅ Use `trigger_v2` for new automation triggers; CLI fills root `appType` / `formUuid` from active profile when `--form-code` is provided.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.115",
3
+ "version": "1.0.117",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {
@@ -117,6 +117,7 @@
117
117
  "qrcode-terminal": "^0.12.0",
118
118
  "tailwindcss": "^3.4.17",
119
119
  "tsx": "^4.20.0",
120
+ "undici": "^6.27.0",
120
121
  "vite": "^6.0.0"
121
122
  },
122
123
  "peerDependencies": {
@@ -2560,6 +2560,33 @@ init_cjs_shims();
2560
2560
 
2561
2561
  // packages/sdk/src/runtime/core/fetch.ts
2562
2562
  init_cjs_shims();
2563
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2564
+ var isTransientFetchError = (error) => {
2565
+ const anyError = error;
2566
+ const message7 = String(anyError?.message || error || "").toLowerCase();
2567
+ const causeCode = String(anyError?.cause?.code || "").toLowerCase();
2568
+ return causeCode === "und_err_connect_timeout" || message7.includes("und_err_connect_timeout") || message7.includes("connect_timeout") || message7.includes("econnreset") || message7.includes("socket hang up") || message7.includes("network socket disconnected") || message7.includes("fetch failed");
2569
+ };
2570
+ var fetchWithTransientRetry = async (fetchImpl, input, init, options = {}) => {
2571
+ if (!options.enabled) {
2572
+ return fetchImpl(input, init);
2573
+ }
2574
+ const retries = Number.isFinite(Number(options.retries)) ? Number(options.retries) : 2;
2575
+ const baseDelayMs = Number.isFinite(Number(options.baseDelayMs)) ? Number(options.baseDelayMs) : 250;
2576
+ let lastError;
2577
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
2578
+ try {
2579
+ return await fetchImpl(input, init);
2580
+ } catch (error) {
2581
+ lastError = error;
2582
+ if (attempt >= retries || !isTransientFetchError(error)) {
2583
+ throw error;
2584
+ }
2585
+ await sleep(Math.min(baseDelayMs * Math.pow(2, attempt), 2e3));
2586
+ }
2587
+ }
2588
+ throw lastError;
2589
+ };
2563
2590
  var createBoundFetch = (fetchImpl) => {
2564
2591
  const baseFetch = fetchImpl || globalThis.fetch;
2565
2592
  return ((input, init) => baseFetch.call(globalThis, input, init));
@@ -28079,7 +28106,7 @@ var import_react162 = __toESM(require("react"));
28079
28106
 
28080
28107
  // node_modules/antd-mobile/es/utils/sleep.js
28081
28108
  init_cjs_shims();
28082
- var sleep = (time) => new Promise((resolve) => setTimeout(resolve, time));
28109
+ var sleep2 = (time) => new Promise((resolve) => setTimeout(resolve, time));
28083
28110
 
28084
28111
  // node_modules/antd-mobile/es/components/pull-to-refresh/pull-to-refresh.js
28085
28112
  var classPrefix63 = `adm-pull-to-refresh`;
@@ -28152,7 +28179,7 @@ var PullToRefresh = (p) => {
28152
28179
  throw e2;
28153
28180
  }
28154
28181
  if (props.completeDelay > 0) {
28155
- yield sleep(props.completeDelay);
28182
+ yield sleep2(props.completeDelay);
28156
28183
  }
28157
28184
  reset();
28158
28185
  });
@@ -34333,6 +34360,12 @@ var normalizeMethod2 = (method4) => {
34333
34360
  const value = String(method4 || "get").toUpperCase();
34334
34361
  return ["GET", "POST", "PUT", "DELETE", "PATCH"].includes(value) ? value : "GET";
34335
34362
  };
34363
+ var shouldRetryTransportRequest = (payload) => {
34364
+ const method4 = normalizeMethod2(payload?.method);
34365
+ if (method4 === "GET") return true;
34366
+ const path = String(payload?.path || "").split("?")[0];
34367
+ return method4 === "POST" && path === "/workflow/capabilities/resolve";
34368
+ };
34336
34369
  var normalizeCssIsolation2 = (value) => {
34337
34370
  if (value === "namespace" || value === "shadow" || value === "none") {
34338
34371
  return value;
@@ -34393,11 +34426,13 @@ var createBrowserPageBridge = (options = {}) => {
34393
34426
  body = JSON.stringify(payload.body);
34394
34427
  }
34395
34428
  }
34396
- const response = await fetchImpl(url2, {
34429
+ const response = await fetchWithTransientRetry(fetchImpl, url2, {
34397
34430
  method: normalizeMethod2(payload.method),
34398
34431
  headers,
34399
34432
  body,
34400
34433
  credentials: "include"
34434
+ }, {
34435
+ enabled: shouldRetryTransportRequest(payload)
34401
34436
  });
34402
34437
  return parseJsonResponse(response);
34403
34438
  };