openxiangda 1.0.130 → 1.0.131

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.
@@ -107,6 +107,10 @@ const result = await sdk.function.invoke("reservation_reminder_summary", {
107
107
  })
108
108
  ```
109
109
 
110
+ App Function source should prefer `export default async function(ctx, input) {}`.
111
+ The second argument is the `input` passed by `sdk.function.invoke`, and the
112
+ same value is also available as `ctx.input` for compatibility.
113
+
110
114
  Use App Functions when the logic must run server-side and be reusable by pages, automations, or workflows. Runtime invocation requires app automation management permission by default. To let ordinary app users call a function from a page, declare the allowed current app roles in `definitionJson.runtimeInvoke.roleCodes`; keep sensitive admin-only functions without broad grants. Inside the function, authorize sensitive actions with trusted runtime context such as `ctx.operator.roleCodes`, `ctx.operator.currentRoleCode`, `ctx.operator.hasFullAccess`, `ctx.currentUser`, or `ctx.permissions`; do not trust role codes sent in the page input. When the function writes forms with `ctx.form.createOne/updateOne/updateById`, treat the write as a trusted backend operation over declared `resources.forms`; do not open direct submit permission on internal forms just for that page action.
111
115
 
112
116
  Work center:
@@ -103,7 +103,7 @@ Use `workflow pull` to inspect the live definition. Use `workflow list --json` a
103
103
 
104
104
  JS_CODE is the backend execution escape hatch for workflow and automation. Use it when the logic must run on the server after a backend trigger, such as a fixed cron schedule, a form date-field schedule, a form submit/update/delete/field-change event, or a workflow approval/process event. It is appropriate for cross-form data queries, create/update/batch update operations, process termination, platform API calls, external HTTP calls, and complex orchestration that the frontend cannot handle reliably.
105
105
 
106
- App Function is the reusable backend execution model. Use it when the logic should be called by custom pages, multiple automations, workflows, or the runtime API. Source lives in `src/functions/<functionCode>/index.ts`; manifest lives in `src/resources/functions/<functionCode>.json`. Call it from pages with `sdk.function.invoke(code, { input })`, from graph definitions with `function_call`, or from the runtime endpoint `/:appType/v1/functions/:code/invoke.json` when the caller has app automation management permission. Current MVP exposes controlled helpers only and does not expose raw SQL or Redis. `ctx.form.createOne/updateOne/updateById` are trusted backend writes governed by declared `resources.forms` and function invocation authorization; keep internal forms closed to direct user submit unless the business explicitly needs raw form submission.
106
+ App Function is the reusable backend execution model. Use it when the logic should be called by custom pages, multiple automations, workflows, or the runtime API. Source lives in `src/functions/<functionCode>/index.ts`; manifest lives in `src/resources/functions/<functionCode>.json`. Call it from pages with `sdk.function.invoke(code, { input })`, from graph definitions with `function_call`, or from the runtime endpoint `/:appType/v1/functions/:code/invoke.json` when the caller has app automation management permission. Prefer `export default async function(ctx, input) {}` for App Function source; the second argument is the invoke input and the same value is available as `ctx.input`. Current MVP exposes controlled helpers only and does not expose raw SQL or Redis. `ctx.form.createOne/updateOne/updateById` are trusted backend writes governed by declared `resources.forms` and function invocation authorization; keep internal forms closed to direct user submit unless the business explicitly needs raw form submission.
107
107
 
108
108
  For new AI-authored automations, prefer code-first `automation_code_ts` resources instead of visual v3 graph definitions. Put the source in `src/automations/<resourceCode>/index.ts`, define `definition.code.json` with `kind: "automation_code_ts"`, and provide `preview.json` for read-only frontend display. Use `ctx.logger.debug/info/warn/error(message, data?)` at every important step; OpenXiangda can inspect logs with `automation executions`, `automation logs`, and `automation diagnose`.
109
109
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.130",
3
+ "version": "1.0.131",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {
@@ -7026,7 +7026,42 @@ var modeDepth = (mode) => {
7026
7026
  return 3;
7027
7027
  };
7028
7028
  var activeLevels = (mode) => LEVELS.slice(0, modeDepth(mode));
7029
- var valueToPath = (value) => LEVELS.map((level) => value?.[level]?.value).filter(Boolean);
7029
+ var stringifyValue = (value) => {
7030
+ if (value === void 0 || value === null || value === "") return void 0;
7031
+ return String(value);
7032
+ };
7033
+ var normalizeAddressPart = (part) => {
7034
+ if (part === void 0 || part === null || part === "") return void 0;
7035
+ if (typeof part === "object") {
7036
+ const value2 = stringifyValue(part.value ?? part.adcode ?? part.code ?? part.id ?? part.label);
7037
+ if (!value2) return void 0;
7038
+ const label = stringifyValue(part.label ?? part.name ?? part.title) ?? "";
7039
+ return { label, value: value2 };
7040
+ }
7041
+ const value = stringifyValue(part);
7042
+ return value ? { label: "", value } : void 0;
7043
+ };
7044
+ var normalizeAddressValue = (raw) => {
7045
+ if (!raw || typeof raw !== "object") return void 0;
7046
+ const next = {};
7047
+ LEVELS.forEach((level) => {
7048
+ const part = normalizeAddressPart(raw[level]);
7049
+ if (part) next[level] = part;
7050
+ });
7051
+ if (raw.detail !== void 0 && raw.detail !== null) {
7052
+ next.detail = String(raw.detail);
7053
+ }
7054
+ if (typeof raw.fullAddress === "string" && raw.fullAddress) {
7055
+ next.fullAddress = raw.fullAddress;
7056
+ }
7057
+ return Object.keys(next).length > 0 ? next : void 0;
7058
+ };
7059
+ var getDisplayLabel = (part) => {
7060
+ const label = part?.label?.trim();
7061
+ if (!label) return void 0;
7062
+ return label === part?.value ? void 0 : label;
7063
+ };
7064
+ var valueToPath = (value, levels) => levels.map((level) => value?.[level]?.value).filter(Boolean);
7030
7065
  var normalizeDivision = (item, level, depth, index) => ({
7031
7066
  label: String(item.name || item.label || item.adcode || item.value),
7032
7067
  value: String(item.adcode || item.value || item.id),
@@ -7035,7 +7070,66 @@ var normalizeDivision = (item, level, depth, index) => ({
7035
7070
  });
7036
7071
  var composeFullAddress = (value, levels = LEVELS) => {
7037
7072
  if (!value) return "";
7038
- return [...levels.map((level) => value[level]?.label), value.detail].filter(Boolean).join("");
7073
+ return [...levels.map((level) => getDisplayLabel(value[level])), value.detail].filter(Boolean).join("");
7074
+ };
7075
+ var mergeValuePathIntoOptions = (source, value, levels, depth) => {
7076
+ if (!value) return source;
7077
+ const mergeAt = (items, index) => {
7078
+ const level = levels[index];
7079
+ const part = level ? value[level] : void 0;
7080
+ if (!level || !part?.value) return items;
7081
+ let matched = false;
7082
+ const nextItems = items.map((item) => {
7083
+ if (String(item.value) !== part.value) return item;
7084
+ matched = true;
7085
+ const label = getDisplayLabel(part) ?? getDisplayLabel(item) ?? "\u5730\u5740\u52A0\u8F7D\u4E2D";
7086
+ const hasNextPath = Boolean(levels[index + 1] && value[levels[index + 1]]?.value);
7087
+ return {
7088
+ ...item,
7089
+ label,
7090
+ level: item.level ?? level,
7091
+ isLeaf: index + 1 >= depth || !hasNextPath && item.isLeaf === true,
7092
+ children: hasNextPath ? mergeAt(item.children ?? [], index + 1) : item.children
7093
+ };
7094
+ });
7095
+ if (!matched) {
7096
+ const hasNextPath = Boolean(levels[index + 1] && value[levels[index + 1]]?.value);
7097
+ const option = {
7098
+ label: getDisplayLabel(part) ?? "\u5730\u5740\u52A0\u8F7D\u4E2D",
7099
+ value: part.value,
7100
+ level,
7101
+ isLeaf: index + 1 >= depth
7102
+ };
7103
+ if (hasNextPath) option.children = mergeAt([], index + 1);
7104
+ nextItems.push(option);
7105
+ }
7106
+ return nextItems;
7107
+ };
7108
+ return mergeAt(source, 0);
7109
+ };
7110
+ var setNestedChildren = (source, selectedOptions, children) => {
7111
+ const [current, ...rest] = selectedOptions;
7112
+ if (!current) return source;
7113
+ const currentValue = String(current.value);
7114
+ let matched = false;
7115
+ const next = source.map((item) => {
7116
+ if (String(item.value) !== currentValue) return item;
7117
+ matched = true;
7118
+ return {
7119
+ ...item,
7120
+ children: rest.length ? setNestedChildren(item.children ?? [], rest, children) : children
7121
+ };
7122
+ });
7123
+ if (!matched) {
7124
+ next.push({
7125
+ label: String(current.label || current.value),
7126
+ value: currentValue,
7127
+ level: current.level,
7128
+ isLeaf: false,
7129
+ children: rest.length ? setNestedChildren([], rest, children) : children
7130
+ });
7131
+ }
7132
+ return next;
7039
7133
  };
7040
7134
  function AddressField(props) {
7041
7135
  const {
@@ -7057,7 +7151,8 @@ function AddressField(props) {
7057
7151
  const { formData, fieldBehaviors, setFieldValue, registerField, unregisterField, api } = useFormContext();
7058
7152
  const { isMobile } = useDeviceDetect();
7059
7153
  const behavior = propBehavior ?? fieldBehaviors[fieldId] ?? "NORMAL";
7060
- const value = formData[fieldId];
7154
+ const rawValue = formData[fieldId];
7155
+ const value = (0, import_react42.useMemo)(() => normalizeAddressValue(rawValue), [rawValue]);
7061
7156
  const [options, setOptions] = (0, import_react42.useState)([]);
7062
7157
  const [loadingRoots, setLoadingRoots] = (0, import_react42.useState)(false);
7063
7158
  const [mobileOpen, setMobileOpen] = (0, import_react42.useState)(false);
@@ -7067,24 +7162,78 @@ function AddressField(props) {
7067
7162
  const [tempValue, setTempValue] = (0, import_react42.useState)();
7068
7163
  const depth = modeDepth(mode);
7069
7164
  const levels = (0, import_react42.useMemo)(() => activeLevels(mode), [mode]);
7165
+ const valuePath = (0, import_react42.useMemo)(() => valueToPath(value, levels), [levels, value]);
7166
+ const displayOptions = (0, import_react42.useMemo)(
7167
+ () => mergeValuePathIntoOptions(options, value, levels, depth),
7168
+ [depth, levels, options, value]
7169
+ );
7070
7170
  const detailEnabled = mode === "province-city-district-street-detail";
7071
7171
  const disabled = behavior === "DISABLED";
7072
7172
  const MobilePopup = getMobileComponent3("Popup");
7073
7173
  (0, import_react42.useEffect)(() => {
7074
7174
  registerField(fieldId);
7075
- if (defaultValue !== void 0 && formData[fieldId] === void 0) {
7175
+ return () => unregisterField(fieldId);
7176
+ }, [fieldId, registerField, unregisterField]);
7177
+ (0, import_react42.useEffect)(() => {
7178
+ if (defaultValue !== void 0 && rawValue === void 0) {
7076
7179
  setFieldValue(fieldId, defaultValue);
7077
7180
  }
7078
- return () => unregisterField(fieldId);
7079
- }, [fieldId]);
7080
- const loadDivisions = async (parentAdcode, level, index) => {
7081
- const list = await api.getChinaDivisions(parentAdcode);
7082
- return list.map((item) => normalizeDivision(item, level, depth, index));
7083
- };
7181
+ }, [defaultValue, fieldId, rawValue, setFieldValue]);
7182
+ const loadDivisions = (0, import_react42.useCallback)(
7183
+ async (parentAdcode, level, index) => {
7184
+ const list = await api.getChinaDivisions(parentAdcode);
7185
+ return list.map((item) => normalizeDivision(item, level, depth, index));
7186
+ },
7187
+ [api, depth]
7188
+ );
7084
7189
  (0, import_react42.useEffect)(() => {
7190
+ let mounted = true;
7085
7191
  setLoadingRoots(true);
7086
- loadDivisions(void 0, "province", 0).then(setOptions).finally(() => setLoadingRoots(false));
7087
- }, [api, depth]);
7192
+ loadDivisions(void 0, "province", 0).then((nextOptions) => {
7193
+ if (mounted) setOptions(nextOptions);
7194
+ }).finally(() => {
7195
+ if (mounted) setLoadingRoots(false);
7196
+ });
7197
+ return () => {
7198
+ mounted = false;
7199
+ };
7200
+ }, [loadDivisions]);
7201
+ (0, import_react42.useEffect)(() => {
7202
+ if (!value) return;
7203
+ let cancelled = false;
7204
+ const hydrateMissingLabels = async () => {
7205
+ let nextValue = { ...value };
7206
+ let changed = false;
7207
+ let parentAdcode;
7208
+ for (let index = 0; index < levels.length; index += 1) {
7209
+ const level = levels[index];
7210
+ const part = nextValue[level];
7211
+ if (!part?.value) break;
7212
+ if (!getDisplayLabel(part)) {
7213
+ const divisions = await loadDivisions(parentAdcode, level, index);
7214
+ const matched = divisions.find((option) => option.value === part.value);
7215
+ if (matched) {
7216
+ nextValue = { ...nextValue, [level]: { label: matched.label, value: matched.value } };
7217
+ changed = true;
7218
+ }
7219
+ }
7220
+ parentAdcode = part.value;
7221
+ }
7222
+ const fullAddress = composeFullAddress(nextValue, levels);
7223
+ if (fullAddress && nextValue.fullAddress !== fullAddress) {
7224
+ nextValue = { ...nextValue, fullAddress };
7225
+ changed = true;
7226
+ }
7227
+ if (!cancelled && changed) {
7228
+ setFieldValue(fieldId, nextValue);
7229
+ }
7230
+ };
7231
+ hydrateMissingLabels().catch(() => {
7232
+ });
7233
+ return () => {
7234
+ cancelled = true;
7235
+ };
7236
+ }, [fieldId, levels, loadDivisions, setFieldValue, value]);
7088
7237
  (0, import_react42.useEffect)(() => {
7089
7238
  if (!mobileOpen) return;
7090
7239
  const level = levels[mobileLevelIndex] || "province";
@@ -7092,7 +7241,7 @@ function AddressField(props) {
7092
7241
  const parentAdcode = parentLevel ? tempValue?.[parentLevel]?.value : void 0;
7093
7242
  setMobileLoading(true);
7094
7243
  loadDivisions(parentAdcode, level, mobileLevelIndex).then(setMobileOptions).finally(() => setMobileLoading(false));
7095
- }, [mobileOpen, mobileLevelIndex, tempValue, levels]);
7244
+ }, [loadDivisions, mobileOpen, mobileLevelIndex, tempValue, levels]);
7096
7245
  if (behavior === "HIDDEN") return null;
7097
7246
  const setAddressValue = (next) => {
7098
7247
  const normalized = next ? { ...next, fullAddress: composeFullAddress(next, levels) } : void 0;
@@ -7228,18 +7377,20 @@ function AddressField(props) {
7228
7377
  import_antd18.Cascader,
7229
7378
  {
7230
7379
  style: { width: "100%" },
7231
- options,
7380
+ options: displayOptions,
7232
7381
  allowClear,
7233
7382
  disabled,
7234
- value: valueToPath(value),
7383
+ value: valuePath,
7235
7384
  placeholder: props.placeholder || "\u8BF7\u9009\u62E9\u5730\u5740",
7236
7385
  loading: loadingRoots,
7386
+ displayRender: (labels) => labels.filter(Boolean).join(" / ") || (valuePath.length ? "\u5730\u5740\u52A0\u8F7D\u4E2D" : ""),
7237
7387
  loadData: async (selectedOptions) => {
7238
7388
  const target = selectedOptions[selectedOptions.length - 1];
7239
7389
  const nextLevel = levels[selectedOptions.length] ?? "street";
7240
7390
  const children = await loadDivisions(target.value, nextLevel, selectedOptions.length);
7241
- target.children = children;
7242
- setOptions([...options]);
7391
+ setOptions(
7392
+ (currentOptions) => setNestedChildren(currentOptions, selectedOptions, children)
7393
+ );
7243
7394
  },
7244
7395
  onChange: (_path, selectedOptions) => {
7245
7396
  if (!selectedOptions?.length) {