@77-zhike/app-sdk 0.2.0-d7b1.2 → 0.2.0-d7b1.20

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 (96) hide show
  1. package/README.md +281 -0
  2. package/package.json +17 -3
  3. package/src/cli/api-client.js +4 -0
  4. package/src/cli/browser-auth.js +64 -18
  5. package/src/cli/credential-store.js +21 -0
  6. package/src/cli/development-logs.js +5 -2
  7. package/src/cli/index.js +59 -42
  8. package/src/cli/local-commands.js +6 -3
  9. package/src/cli/logs.js +56 -0
  10. package/src/cli/project-init.js +111 -179
  11. package/src/cli/windows-credential-bridge.ps1 +131 -0
  12. package/src/cli/windows-credential-store.js +112 -0
  13. package/src/client/analysis.d.ts +4 -0
  14. package/src/client/analysis.js +12 -0
  15. package/src/client/destinations.d.ts +25 -0
  16. package/src/client/destinations.js +70 -0
  17. package/src/client/documents.d.ts +21 -0
  18. package/src/client/documents.js +39 -0
  19. package/src/client/external-url.js +9 -0
  20. package/src/client/index.d.ts +225 -3
  21. package/src/client/index.js +75 -3
  22. package/src/client/interactions.d.ts +23 -3
  23. package/src/client/interactions.js +53 -3
  24. package/src/client/points.generated.js +56 -0
  25. package/src/config.js +11 -7
  26. package/src/forms/array-contract.d.ts +32 -0
  27. package/src/forms/array-contract.js +138 -0
  28. package/src/forms/components.generated.js +2 -2
  29. package/src/forms/factory.js +21 -7
  30. package/src/forms/field-array.d.ts +16 -0
  31. package/src/forms/field-array.js +52 -0
  32. package/src/forms/index.d.ts +74 -3
  33. package/src/forms/index.js +2 -0
  34. package/src/forms/options.d.ts +7 -2
  35. package/src/forms/options.js +8 -1
  36. package/src/forms/record-options.d.ts +28 -0
  37. package/src/forms/record-options.js +92 -0
  38. package/src/forms/schema.d.ts +73 -9
  39. package/src/forms/schema.js +128 -0
  40. package/src/forms/state.d.ts +30 -0
  41. package/src/forms/state.js +82 -0
  42. package/src/forms/validation.d.ts +9 -0
  43. package/src/forms/validation.js +81 -0
  44. package/src/forms/values.d.ts +5 -0
  45. package/src/forms/values.js +38 -0
  46. package/src/jsx-dev-runtime.d.ts +2 -0
  47. package/src/jsx-dev-runtime.js +1 -0
  48. package/src/jsx-runtime.d.ts +2 -0
  49. package/src/jsx-runtime.js +1 -0
  50. package/src/server/agents/index.d.ts +62 -0
  51. package/src/server/agents/index.js +40 -0
  52. package/src/server/index.d.ts +60 -5
  53. package/src/server/index.js +43 -0
  54. package/src/server/steps/index.d.ts +71 -0
  55. package/src/server/steps/index.js +15 -0
  56. package/src/server/triggers/index.d.ts +68 -0
  57. package/src/server/triggers/index.js +27 -0
  58. package/src/server/workflow-blocks/index.d.ts +72 -0
  59. package/src/server/workflow-blocks/index.js +32 -0
  60. package/src/toolchain/agents/discovery.js +126 -0
  61. package/src/toolchain/agents/exposure.js +31 -0
  62. package/src/toolchain/agents/manifest.js +84 -0
  63. package/src/toolchain/ai-capabilities.js +69 -0
  64. package/src/toolchain/allowed-packages.json +7 -0
  65. package/src/toolchain/dependency-policy.js +181 -0
  66. package/src/toolchain/index.js +0 -88
  67. package/src/toolchain/real-build.js +135 -27
  68. package/src/toolchain/source-discovery.js +330 -81
  69. package/src/toolchain/workflow-blocks/discovery.js +133 -0
  70. package/src/toolchain/workflow-steps/manifest.js +99 -0
  71. package/src/toolchain/workflow-triggers/manifest.js +47 -0
  72. package/src/ui/components.generated.js +4 -1
  73. package/src/ui/conversation.d.ts +81 -0
  74. package/src/ui/conversation.js +82 -0
  75. package/src/ui/data-table-model.d.ts +14 -0
  76. package/src/ui/data-table-model.js +98 -0
  77. package/src/ui/data-table.d.ts +70 -0
  78. package/src/ui/data-table.js +74 -0
  79. package/src/ui/events.d.ts +10 -0
  80. package/src/ui/factory.js +87 -3
  81. package/src/ui/index.d.ts +351 -8
  82. package/src/ui/index.js +1 -1
  83. package/src/ui/jsx.generated.d.ts +359 -0
  84. package/src/ui/native-dom.d.ts +49 -0
  85. package/src/ui/native-dom.js +44 -0
  86. package/src/ui/scroll-viewport.d.ts +22 -0
  87. package/src/ui/scroll-viewport.js +23 -0
  88. package/src/ui/slider.d.ts +27 -0
  89. package/src/ui/slider.js +26 -0
  90. package/src/ui/style.d.ts +5 -0
  91. package/src/ui/style.generated.js +2 -0
  92. package/src/ui/style.js +15 -0
  93. package/src/ui/text-selection.d.ts +22 -0
  94. package/src/ui/text-selection.js +23 -0
  95. package/src/ui/validation.js +86 -0
  96. package/src/cli/validate-bundle.js +0 -36
@@ -0,0 +1,138 @@
1
+ import { assertJsonValue } from '../ui/validation.js';
2
+ import { isDateTime, isPlainDate, parseNumberDraft } from './values.js';
3
+ import { assertRecordRef, recordObjectsForField } from './record-options.js';
4
+
5
+ export const ARRAY_LIMITS = Object.freeze({ arrays: 3, rowFields: 12, rows: 50, instances: 300, valueBytes: 65536, messageBytes: 196608 });
6
+ const fail = () => { throw new Error('runtime_form_array_invalid'); };
7
+ export const validFieldKey = key => typeof key === 'string' && /^[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(key) && !['constructor', 'prototype'].includes(key);
8
+ export const formBytes = value => new TextEncoder().encode(JSON.stringify(value)).length;
9
+ export function formObject(value, keys) {
10
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !keys.includes(key))) fail();
11
+ }
12
+ export function parseFieldPath(path) {
13
+ if (validFieldKey(path)) return { field: path };
14
+ const match = typeof path === 'string' && /^([a-zA-Z][a-zA-Z0-9_]{0,63})\[(0|[1-9][0-9]*)\]\.([a-zA-Z][a-zA-Z0-9_]{0,63})$/.exec(path);
15
+ if (!match || !validFieldKey(match[1]) || !validFieldKey(match[3]) || !Number.isSafeInteger(Number(match[2])) || Number(match[2]) >= ARRAY_LIMITS.rows) fail();
16
+ return { array: match[1], index: Number(match[2]), field: match[3] };
17
+ }
18
+ export function fieldAtPath(fields, path) {
19
+ const address = parseFieldPath(path);
20
+ const field = address.array ? fields[address.array]?.item?.fields[address.field] : fields[address.field];
21
+ if (!field || address.array && fields[address.array].type !== 'array') fail();
22
+ return field;
23
+ }
24
+ export function valueAtPath(values, path) {
25
+ const address = parseFieldPath(path);
26
+ return address.array ? values[address.array]?.[address.index]?.[address.field] : values[address.field];
27
+ }
28
+ export function setPathValue(values, path, value) {
29
+ const address = parseFieldPath(path);
30
+ if (address.array) values[address.array][address.index][address.field] = value;
31
+ else values[address.field] = value;
32
+ }
33
+ export function rulePath(path) {
34
+ const address = parseFieldPath(path);
35
+ return address.array ? `${address.array}[*].${address.field}` : address.field;
36
+ }
37
+ export function leafPaths(fields, values) {
38
+ return Object.entries(fields).flatMap(([name, field]) => field.type === 'array'
39
+ ? (values[name] ?? []).flatMap((_row, index) => Object.keys(field.item.fields).map(key => `${name}[${index}].${key}`)) : [name]);
40
+ }
41
+ export const rhfPath = path => path.replace(/\[(\d+)\]/g, '.$1');
42
+ export const publicPath = path => path.replace(/\.(\d+)\./g, '[$1].');
43
+ export function assertFieldValue(value, field, draft = false) {
44
+ if (field.type === 'array') {
45
+ if (!Array.isArray(value) || value.length > field.maxItems || Object.keys(value).length !== value.length) fail();
46
+ for (const row of value) {
47
+ formObject(row, Object.keys(field.item.fields));
48
+ if (Object.keys(row).length !== Object.keys(field.item.fields).length) fail();
49
+ for (const [key, leaf] of Object.entries(field.item.fields)) assertFieldValue(row[key], leaf, draft);
50
+ }
51
+ } else if (field.type === 'boolean') { if (typeof value !== 'boolean') fail(); }
52
+ else if (field.type === 'number') { if (value !== null && !(draft && typeof value === 'string') && (typeof value !== 'number' || !Number.isFinite(value))) fail(); }
53
+ else if (field.type === 'record') { if (value !== null) assertRecordRef(value, recordObjectsForField(field)); }
54
+ else if (field.type === 'plainDate' || field.type === 'dateTime') { if (value !== null && !(draft && typeof value === 'string') && !(field.type === 'plainDate' ? isPlainDate(value) : isDateTime(value))) fail(); }
55
+ else if (typeof value !== 'string' || value.length > 10000) fail();
56
+ }
57
+ export function assertArrayCapacity(fields, values, draft = false) {
58
+ if (!Object.values(fields).some(field => field.type === 'array')) return;
59
+ formObject(values, Object.keys(fields));
60
+ if (Object.keys(fields).length !== Object.keys(values).length) fail();
61
+ for (const [name, field] of Object.entries(fields)) assertFieldValue(values[name], field, draft);
62
+ if (leafPaths(fields, values).length > ARRAY_LIMITS.instances || formBytes(values) > ARRAY_LIMITS.valueBytes) throw new Error('runtime_form_limit_exceeded');
63
+ }
64
+ export function normalizeFormValues(fields, raw) {
65
+ return Object.fromEntries(Object.entries(fields).map(([name, field]) => {
66
+ const value = raw[name];
67
+ return [name, field.type === 'array' ? value.map(row => normalizeFormValues(field.item.fields, row))
68
+ : field.type === 'number' ? parseNumberDraft(value).value
69
+ : field.type === 'dateTime' ? isDateTime(value) ? new Date(value).toISOString() : null
70
+ : field.type === 'plainDate' ? isPlainDate(value) ? value : null : structuredClone(value)];
71
+ }));
72
+ }
73
+ /** errors / validatingFields 的索引只对应这次快照;无错误行必须是显式 null。 */
74
+ export function assertErrorTree(tree, fields, values, boolean = false, maxText = 4096) {
75
+ formObject(tree, Object.keys(fields));
76
+ for (const [name, error] of Object.entries(tree)) {
77
+ const field = fields[name];
78
+ if (field.type === 'array') {
79
+ if (!Array.isArray(error) || error.length > (values?.[name]?.length ?? field.maxItems) || Object.keys(error).length !== error.length) fail();
80
+ for (const row of error) if (row !== null) assertErrorTree(row, field.item.fields, undefined, boolean, maxText);
81
+ } else if (boolean ? typeof error !== 'boolean' : typeof error !== 'string' || !error.trim() || error.length > maxText) fail();
82
+ }
83
+ }
84
+ export function assertArrayErrors(errors, fields, maxText = 4096) {
85
+ formObject(errors, Object.keys(fields).filter(name => fields[name].type === 'array'));
86
+ if (Object.values(errors).some(value => typeof value !== 'string' || !value.trim() || value.length > maxText)) fail();
87
+ }
88
+ export function flattenErrors(tree = {}) {
89
+ return Object.fromEntries(Object.entries(tree).flatMap(([name, value]) => Array.isArray(value)
90
+ ? value.flatMap((row, index) => Object.entries(row ?? {}).map(([key, error]) => [`${name}[${index}].${key}`, error])) : [[name, value]]));
91
+ }
92
+ export function nestErrors(flat) {
93
+ const tree = {};
94
+ for (const [path, error] of Object.entries(flat)) {
95
+ const address = parseFieldPath(path);
96
+ if (!address.array) tree[path] = error;
97
+ else {
98
+ const rows = tree[address.array] ??= [];
99
+ while (rows.length <= address.index) rows.push(null);
100
+ (rows[address.index] ??= {})[address.field] = error;
101
+ }
102
+ }
103
+ return tree;
104
+ }
105
+ export function assertArrayBinding(binding) {
106
+ formObject(binding, ['name', 'rowKey', 'formInstanceId', 'structureRevision']);
107
+ if (!validFieldKey(binding.name) || !opaque(binding.rowKey) || !opaque(binding.formInstanceId) || !revision(binding.structureRevision)) fail();
108
+ return binding;
109
+ }
110
+ const opaque = value => typeof value === 'string' && value.length > 0 && value.length <= 200;
111
+ const revision = value => Number.isSafeInteger(value) && value >= 0;
112
+ export function assertArrayOperation(input) {
113
+ formObject(input, ['formId', 'formInstanceId', 'arrayName', 'baseStructureRevision', 'action']);
114
+ if (!opaque(input.formId) || !opaque(input.formInstanceId) || !validFieldKey(input.arrayName) || !revision(input.baseStructureRevision)) fail();
115
+ const action = input.action;
116
+ const keys = { append: ['kind', 'value'], insertBefore: ['kind', 'beforeKey', 'value'], remove: ['kind', 'rowKey'], move: ['kind', 'rowKey', 'before'] }[action?.kind];
117
+ if (!keys) fail();
118
+ formObject(action, keys);
119
+ if (Object.keys(action).length !== keys.length || keys.includes('rowKey') && !opaque(action.rowKey) || keys.includes('beforeKey') && !opaque(action.beforeKey) || keys.includes('before') && action.before !== null && !opaque(action.before)) fail();
120
+ return input;
121
+ }
122
+ export function assertArraySnapshot(snapshot, formId, name, fields) {
123
+ assertJsonValue(snapshot, { maxBytes: ARRAY_LIMITS.messageBytes, maxEntries: 4096 });
124
+ formObject(snapshot, ['formId', 'name', 'formInstanceId', 'revision', 'structureRevision', 'rows', 'submitting', 'canAppend', 'error']);
125
+ const field = fields[name];
126
+ if (snapshot.formId !== formId || snapshot.name !== name || !opaque(snapshot.formInstanceId) || !revision(snapshot.revision) || !revision(snapshot.structureRevision) || field?.type !== 'array' || !Array.isArray(snapshot.rows) || snapshot.rows.length > field.maxItems || typeof snapshot.submitting !== 'boolean' || typeof snapshot.canAppend !== 'boolean') fail();
127
+ const keys = new Set();
128
+ for (const [index, row] of snapshot.rows.entries()) {
129
+ formObject(row, ['key', 'index', 'value', 'errors', 'validatingFields']);
130
+ if (!opaque(row.key) || keys.has(row.key) || row.index !== index) fail();
131
+ keys.add(row.key);
132
+ assertFieldValue([row.value], { ...field, maxItems: 1 });
133
+ assertErrorTree(row.errors, field.item.fields);
134
+ assertErrorTree(row.validatingFields, field.item.fields, undefined, true);
135
+ }
136
+ if (snapshot.error !== null && (typeof snapshot.error !== 'string' || !snapshot.error || snapshot.error.length > 4096) || formBytes(snapshot) > ARRAY_LIMITS.messageBytes) fail();
137
+ return snapshot;
138
+ }
@@ -1,4 +1,4 @@
1
1
  // 此文件由 scripts/generate-components.mjs 生成,请修改产品能力声明。
2
- export const fieldComponents = Object.freeze(["TextInput","Checkbox","Select","Combobox","MemberCombobox","TeamCombobox"]);
3
- export const boundComponents = Object.freeze(["TextInput","Checkbox","Select","Combobox","MemberCombobox","TeamCombobox","SubmitButton"]);
2
+ export const fieldComponents = Object.freeze(["TextInput","Checkbox","Select","Combobox","MemberCombobox","TeamCombobox","TextArea","NumberInput","PlainDateInput","DateTimeInput","Toggle","RadioGroup","RecordCombobox"]);
3
+ export const boundComponents = Object.freeze(["TextInput","Checkbox","Select","Combobox","MemberCombobox","TeamCombobox","SubmitButton","TextArea","NumberInput","PlainDateInput","DateTimeInput","Toggle","RadioGroup","RecordCombobox","InputGroup","Fieldset"]);
4
4
  export const formComponent = "Form";
@@ -1,23 +1,37 @@
1
+ import { createFieldArrays } from './field-array.js';
2
+ import { compileForm } from './schema.js';
3
+ import { createWithState } from './state.js';
4
+ import { useNormalizedRecordOptions, recordObjectsForField } from './record-options.js';
1
5
  import { boundComponents, formComponent } from './components.generated.js';
2
6
  import { useNormalizedOptions } from './normalize-options.js';
3
- export function createFormHook({ createElement, useId, useMemo, useRef }) {
7
+ export function createFormHook(React, { useBindCallback = () => callback => callback, useInvoke = () => () => Promise.reject(new Error('runtime_host_interaction_unavailable')) } = {}) {
8
+ const { createElement, useId, useMemo, useRef } = React;
4
9
 
5
10
  return function useForm(options) {
6
11
  const formId = useId();
12
+ const bindCallback = useBindCallback();
13
+ const invoke = useInvoke();
7
14
  const initial = useRef(options);
8
15
  return useMemo(() => {
9
- const { schema, initialValues } = initial.current;
16
+ const { initialValues } = initial.current;
17
+ const { schema, ...validation } = compileForm(initial.current, formId, bindCallback);
18
+ const arrays = createFieldArrays(React, formId, schema, invoke);
10
19
  const field = type => function Field({ children, ...props }) {
11
- if (type === 'Combobox') {
20
+ const binding = arrays.useBinding(props.name);
21
+ if (type === 'Combobox' || type === 'RecordCombobox') {
12
22
  const { options, ...rest } = props;
13
- const normalized = useNormalizedOptions({ useRef, useMemo }, options);
14
- return createElement(type, { ...rest, ...normalized, formId });
23
+ const normalized = type === 'RecordCombobox'
24
+ ? useNormalizedRecordOptions({ useRef, useMemo }, options, recordObjectsForField(binding.arrayBinding ? schema.fields[binding.arrayBinding.name].item.fields[binding.name] : schema.fields[props.name]))
25
+ : useNormalizedOptions({ useRef, useMemo }, options);
26
+ return createElement(type, { ...rest, ...normalized, ...binding, formId });
15
27
  }
16
- return createElement(type, { ...props, formId }, children);
28
+ return createElement(type, { ...props, ...binding, formId }, children);
17
29
  };
18
30
  return Object.freeze({
31
+ FieldArray: arrays.FieldArray,
32
+ WithState: createWithState(React, formId, schema, initialValues),
19
33
  Form: function Form({ children, onSubmit }) {
20
- return createElement(formComponent, { formId, schema, initialValues, onSubmit }, children);
34
+ return createElement(formComponent, { formId, schema, initialValues, onSubmit, ...validation }, children);
21
35
  },
22
36
  ...Object.fromEntries(boundComponents.map(type => [type, field(type)])),
23
37
  });
@@ -0,0 +1,16 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { ArrayName, FormSchema, FormValues } from './schema.js';
3
+ type ReadonlyValue<T> = T extends object ? { readonly [K in keyof T]: ReadonlyValue<T[K]> } : T;
4
+ export type ArrayOperationResult = { ok: true; rowKey?: string } | { ok: false; code: string; message: string };
5
+ type Row<S extends FormSchema, N extends ArrayName<S>> = FormValues<S>[N] extends (infer R)[] ? R : never;
6
+ export interface FieldArrayHandle<R> {
7
+ readonly ready: boolean;
8
+ readonly rows: readonly { readonly key: string; readonly index: number; readonly value: ReadonlyValue<R>; readonly errors: Readonly<Partial<Record<keyof R, string>>>; readonly validatingFields: Readonly<Partial<Record<keyof R, boolean>>> }[];
9
+ readonly pending: boolean; readonly submitting: boolean; readonly canAppend: boolean;
10
+ readonly error: string | null; readonly operationError: string | null;
11
+ append(value: R): Promise<ArrayOperationResult>;
12
+ insertBefore(beforeKey: string, value: R): Promise<ArrayOperationResult>;
13
+ remove(rowKey: string): Promise<ArrayOperationResult>;
14
+ move(rowKey: string, position: { before: string | null }): Promise<ArrayOperationResult>;
15
+ }
16
+ export type FieldArrayComponent<S extends FormSchema> = <N extends ArrayName<S>>(props: { name: N; children: (array: FieldArrayHandle<Row<S, N>>) => ReactNode }) => ReactNode;
@@ -0,0 +1,52 @@
1
+ import { assertArraySnapshot, parseFieldPath } from './array-contract.js';
2
+ import { freezeFormState } from './state.js';
3
+
4
+ export function createFieldArrays(React, formId, schema, invoke) {
5
+ const { createContext, createElement, Fragment, useContext, useEffect, useRef, useState, useMemo } = React;
6
+ const Context = createContext(null);
7
+ // 只串行化结构操作;行值仍由 Host 持有,不能乐观修改第二份草稿。
8
+ const gate = { pending: false };
9
+ function Observer({ name, children }) {
10
+ if (schema.fields[name]?.type !== 'array' || typeof children !== 'function') throw new Error('runtime_form_array_invalid');
11
+ const latest = useRef(null), alive = useRef(true);
12
+ const [snapshot, setSnapshot] = useState(null), [pending, setPending] = useState(false), [operationError, setError] = useState(null);
13
+ useEffect(() => { alive.current = true; return () => { alive.current = false; }; }, []);
14
+ const onState = useMemo(() => next => {
15
+ assertArraySnapshot(next, formId, name, schema.fields);
16
+ if (!alive.current || latest.current && latest.current.formInstanceId === next.formInstanceId && latest.current.revision >= next.revision) return;
17
+ latest.current = freezeFormState(next); setSnapshot(latest.current);
18
+ }, [name]);
19
+ async function operate(action) {
20
+ const state = latest.current;
21
+ const reject = (code, message) => { if (alive.current) setError(message); return { ok: false, code, message }; };
22
+ if (!alive.current || !state) return reject('owner_closed', '表单尚未就绪');
23
+ if (gate.pending || state.submitting) return reject('busy', '请等待当前操作完成');
24
+ gate.pending = true; setPending(true); setError(null);
25
+ try {
26
+ const result = await invoke('formArray', { formId, formInstanceId: state.formInstanceId, arrayName: name, baseStructureRevision: state.structureRevision, action });
27
+ if (result?.snapshot) onState(result.snapshot);
28
+ if (!result || typeof result.ok !== 'boolean') throw new Error('runtime_form_array_invalid');
29
+ if (!result.ok) return reject(result.code, result.message);
30
+ return { ok: true, ...(result.rowKey ? { rowKey: result.rowKey } : {}) };
31
+ } catch { return reject('owner_closed', '操作未确认,请检查当前明细后再操作'); }
32
+ finally { gate.pending = false; if (alive.current) setPending(false); }
33
+ }
34
+ const handle = Object.freeze({ ready: Boolean(snapshot), rows: snapshot?.rows ?? [], pending, submitting: snapshot?.submitting ?? false,
35
+ canAppend: Boolean(snapshot?.canAppend && !pending), error: snapshot?.error ?? null, operationError,
36
+ append: value => operate({ kind: 'append', value }), insertBefore: (beforeKey, value) => operate({ kind: 'insertBefore', beforeKey, value }),
37
+ remove: rowKey => operate({ kind: 'remove', rowKey }), move: (rowKey, { before }) => operate({ kind: 'move', rowKey, before }),
38
+ });
39
+ return createElement(Fragment, null, createElement('FormArrayObserver', { formId, name, onState }),
40
+ createElement(Context.Provider, { value: snapshot }, children(handle)));
41
+ }
42
+ function useBinding(name) {
43
+ const snapshot = useContext(Context);
44
+ if (name === undefined) return {};
45
+ const path = parseFieldPath(name);
46
+ if (!path.array) return { name };
47
+ if (!snapshot || snapshot.name !== path.array || !snapshot.rows[path.index] || !schema.fields[path.array].item.fields[path.field]) throw new Error('runtime_form_array_binding_missing');
48
+ return { name: path.field, arrayBinding: { name: path.array, rowKey: snapshot.rows[path.index].key, formInstanceId: snapshot.formInstanceId, structureRevision: snapshot.structureRevision } };
49
+ }
50
+ function FieldArray(props) { return createElement(Observer, { ...props, key: props.name }); }
51
+ return { FieldArray, useBinding };
52
+ }
@@ -1,10 +1,19 @@
1
1
  // 此文件由 scripts/generate-components.mjs 生成,请修改产品能力声明。
2
2
  import type { ComponentType, ReactNode } from "react";
3
- import type { FormSchema, FormValues, FieldName, FormSubmitResult } from "./schema.js";
3
+ import type { FormSchema, FormValues, FieldName, FormSubmitResult, FormOptions } from "./schema.js";
4
4
  import type { Option, OptionsPage, OptionsProvider } from "./options.js";
5
+ import type { RecordOptionsProvider } from "./record-options.js";
6
+ import type { FieldArrayComponent } from "./field-array.js";
7
+ export type { FieldArrayHandle, ArrayOperationResult } from "./field-array.js";
8
+ import type { WithStateComponent } from "./state.js";
9
+ export type { FormStateSelection, SelectedFormState } from "./state.js";
10
+ export type { RecordRef, RecordObjectOption, RecordOption, RecordOptionsPage, RecordOptionsProvider } from "./record-options.js";
5
11
  export type { Option, OptionsPage, OptionsProvider } from "./options.js";
6
- export type { FormField, FormSchema, FormValues, FieldName, FormSubmitResult } from "./schema.js";
12
+ export { Forms } from "./schema.js";
13
+ export type { FormField, FormSchema, FormValues, FieldName, FormSubmitResult, FormOptions, FormValidationResult, FormFieldErrors, FormArrayErrors, ArrayName } from "./schema.js";
7
14
  export interface FormHandle<S extends FormSchema> {
15
+ WithState: WithStateComponent<S>;
16
+ FieldArray: FieldArrayComponent<S>;
8
17
  Form: ComponentType<{
9
18
  children?: ReactNode;
10
19
  onSubmit: (values: FormValues<S>) => FormSubmitResult<S> | Promise<FormSubmitResult<S>>;
@@ -14,11 +23,15 @@ export interface FormHandle<S extends FormSchema> {
14
23
  description?: string;
15
24
  placeholder?: string;
16
25
  disabled?: boolean;
26
+ labelHidden?: boolean;
27
+ inputMode?: "numeric" | "decimal";
28
+ type?: "text" | "email" | "password" | "tel" | "url";
17
29
  }>;
18
30
  Checkbox: ComponentType<{
19
31
  name: FieldName<S, "boolean">;
20
32
  description?: string;
21
33
  disabled?: boolean;
34
+ labelHidden?: boolean;
22
35
  }>;
23
36
  Select: ComponentType<{
24
37
  name: FieldName<S, "enum">;
@@ -48,7 +61,65 @@ export interface FormHandle<S extends FormSchema> {
48
61
  SubmitButton: ComponentType<{
49
62
  children?: string;
50
63
  disabled?: boolean;
64
+ icon?: import("../ui/index.js").AppIconName;
65
+ iconOnly?: boolean;
66
+ label?: string;
67
+ }>;
68
+ TextArea: ComponentType<{
69
+ name: FieldName<S, "string">;
70
+ description?: string;
71
+ placeholder?: string;
72
+ disabled?: boolean;
73
+ resizable?: boolean;
74
+ labelHidden?: boolean;
75
+ appearance?: "default" | "revision";
76
+ }>;
77
+ NumberInput: ComponentType<{
78
+ name: FieldName<S, "number">;
79
+ description?: string;
80
+ placeholder?: string;
81
+ disabled?: boolean;
82
+ step?: number;
83
+ }>;
84
+ PlainDateInput: ComponentType<{
85
+ name: FieldName<S, "plainDate">;
86
+ description?: string;
87
+ placeholder?: string;
88
+ disabled?: boolean;
89
+ }>;
90
+ DateTimeInput: ComponentType<{
91
+ name: FieldName<S, "dateTime">;
92
+ description?: string;
93
+ placeholder?: string;
94
+ disabled?: boolean;
95
+ }>;
96
+ Toggle: ComponentType<{
97
+ name: FieldName<S, "boolean">;
98
+ description?: string;
99
+ placeholder?: string;
100
+ disabled?: boolean;
101
+ }>;
102
+ RadioGroup: ComponentType<{
103
+ name: FieldName<S, "enum">;
104
+ description?: string;
105
+ placeholder?: string;
106
+ disabled?: boolean;
107
+ variant?: "default" | "cards";
108
+ }>;
109
+ RecordCombobox: ComponentType<{
110
+ name: FieldName<S, "record">;
111
+ description?: string;
112
+ placeholder?: string;
113
+ disabled?: boolean;
114
+ options?: RecordOptionsProvider;
115
+ }>;
116
+ InputGroup: ComponentType<{
117
+ children?: ReactNode;
118
+ }>;
119
+ Fieldset: ComponentType<{
120
+ legend: string;
121
+ children?: ReactNode;
51
122
  }>;
52
123
  }
53
124
  /** schema 和 initialValues 仅在首次挂载使用;重新渲染不会重置输入。 */
54
- export function useForm<const S extends FormSchema>(options: { schema: S; initialValues: FormValues<S> }): FormHandle<S>;
125
+ export function useForm<const S extends FormSchema>(options: FormOptions<S>): FormHandle<S>;
@@ -1,3 +1,5 @@
1
1
  import * as React from "react";
2
2
  import { createFormHook } from "./factory.js";
3
3
  export const useForm = createFormHook(React);
4
+
5
+ export { Forms } from './schema.js';
@@ -1,12 +1,17 @@
1
+ import type { AppIconName, BadgeProps } from '../ui/index.js';
1
2
  /** value 是持久化标识,label 只负责显示。无法回显不能清除原 value。 */
2
- export interface Option {
3
+ export type Option = {
3
4
  value: string;
4
5
  label: string;
5
6
  description?: string;
6
7
  disabled?: boolean;
7
8
  disabledReason?: string;
9
+ categoryLabel?: string;
10
+ avatarUrl?: string;
11
+ icon?: AppIconName;
12
+ color?: NonNullable<BadgeProps['color']>;
8
13
  }
9
- export interface OptionsPage {
14
+ export type OptionsPage = {
10
15
  items: Option[];
11
16
  /** null 表示末页;游标只能在当前搜索下使用。 */
12
17
  nextCursor: string | null;
@@ -1,16 +1,23 @@
1
+ import { sdkIconNames } from '../ui/icons.generated.js';
2
+ import { badgeColors } from '../ui/components.generated.js';
3
+ import { assertExternalUrl } from '../client/external-url.js';
1
4
  // SDK 回调与 Host 共用有界 DTO 验证;函数留在 Guest,不传递 Provider 对象。
2
5
  const fail = () => { throw new Error('runtime_options_invalid'); };
3
6
  const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
4
7
  const only = (value, keys) => { if (!object(value) || Object.keys(value).some(key => !keys.includes(key))) fail(); };
5
8
  export const OPTIONS_PAGE_LIMIT = 100;
6
9
  export function assertOption(option) {
7
- only(option, ['value', 'label', 'description', 'disabled', 'disabledReason']);
10
+ only(option, ['value', 'label', 'description', 'disabled', 'disabledReason', 'categoryLabel', 'avatarUrl', 'icon', 'color']);
8
11
  assertOptionValue(option.value);
9
12
  if (typeof option.label !== 'string' || !option.label || option.label.length > 200) fail();
10
13
  for (const key of ['description', 'disabledReason']) {
11
14
  if (option[key] !== undefined && (typeof option[key] !== 'string' || option[key].length > 4096)) fail();
12
15
  }
13
16
  if (option.disabled !== undefined && typeof option.disabled !== 'boolean') fail();
17
+ if (option.categoryLabel !== undefined && (typeof option.categoryLabel !== 'string' || !option.categoryLabel.trim() || option.categoryLabel.length > 200)) fail();
18
+ if (option.avatarUrl !== undefined) { try { assertExternalUrl(option.avatarUrl); } catch { fail(); } }
19
+ if (option.icon !== undefined && !sdkIconNames.includes(option.icon)) fail();
20
+ if (option.color !== undefined && !badgeColors.includes(option.color)) fail();
14
21
  return option;
15
22
  }
16
23
  export function assertOptionValue(value) {
@@ -0,0 +1,28 @@
1
+ export type RecordRef = {
2
+ /** 数据源范围内稳定的公开对象键,不是内部模型名或授权证明。 */
3
+ objectSlug: string;
4
+ recordId: string;
5
+ };
6
+ export type RecordObjectOption = { objectSlug: string; label: string; }
7
+ export type RecordOption = {
8
+ value: RecordRef;
9
+ label: string;
10
+ description?: string;
11
+ disabled?: boolean;
12
+ disabledReason?: string;
13
+ }
14
+ export type RecordOptionsPage = { items: RecordOption[]; nextCursor: string | null; }
15
+ export interface RecordOptionsProvider {
16
+ search(input: { objectSlug: string; query: string; cursor?: string }): Promise<RecordOptionsPage>;
17
+ /** null 表示未解析;查询错误必须拒绝 Promise,不能静默清空引用。 */
18
+ getOption(value: RecordRef): Promise<RecordOption | null>;
19
+ }
20
+ export function recordKey(value: RecordRef): string;
21
+ export function assertRecordRef(value: unknown, objects?: readonly RecordObjectOption[]): RecordRef;
22
+ export function assertRecordObjects(value: unknown): RecordObjectOption[];
23
+ export function assertRecordSearch(value: unknown, objects?: readonly RecordObjectOption[]): { objectSlug: string; query: string; cursor?: string };
24
+ export function assertRecordOption(value: unknown): RecordOption;
25
+ export function assertRecordPage(value: unknown, objectSlug: string): RecordOptionsPage;
26
+ export function assertSelectedRecordOption(value: unknown, ref: RecordRef): RecordOption | null;
27
+
28
+ export function recordObjectsForField(field: { objectSlug?: string; objects?: readonly RecordObjectOption[]; label: string }): readonly RecordObjectOption[];
@@ -0,0 +1,92 @@
1
+ import { assertOptionValue, assertOptionsSearch } from './options.js';
2
+ const fail = () => { throw new Error('runtime_record_options_invalid'); };
3
+ const only = (value, keys) => {
4
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !keys.includes(key))) fail();
5
+ };
6
+ /** 仅用于 Host 选择器 identity,不作为公开保存值或传输编码。 */
7
+ export const recordKey = value => JSON.stringify([value.objectSlug, value.recordId]);
8
+ export function assertRecordRef(value, objects) {
9
+ only(value, ['objectSlug', 'recordId']);
10
+ assertOptionValue(value.objectSlug); assertOptionValue(value.recordId);
11
+ if (objects && !objects.some(object => object.objectSlug === value.objectSlug)) fail();
12
+ return value;
13
+ }
14
+ export function assertRecordObjects(objects) {
15
+ if (!Array.isArray(objects) || objects.length !== 1) fail();
16
+ const seen = new Set();
17
+ for (const object of objects) {
18
+ only(object, ['objectSlug', 'label']);
19
+ assertOptionValue(object.objectSlug);
20
+ if (seen.has(object.objectSlug) || typeof object.label !== 'string' || !object.label.trim() || object.label.length > 200) fail();
21
+ seen.add(object.objectSlug);
22
+ }
23
+ return objects;
24
+ }
25
+ /** 新配置绑定唯一对象;旧数组仅允许恰好一项。 */
26
+ export function recordObjectsForField(field) {
27
+ if (field.objectSlug !== undefined) {
28
+ if (field.objects !== undefined) fail();
29
+ assertOptionValue(field.objectSlug);
30
+ return [{ objectSlug: field.objectSlug, label: field.label }];
31
+ }
32
+ return assertRecordObjects(field.objects);
33
+ }
34
+ export function assertRecordSearch(input, objects) {
35
+ only(input, ['objectSlug', 'query', 'cursor']);
36
+ assertOptionValue(input.objectSlug);
37
+ if (objects && !objects.some(object => object.objectSlug === input.objectSlug)) fail();
38
+ assertOptionsSearch({ query: input.query, ...(input.cursor === undefined ? {} : { cursor: input.cursor }) });
39
+ return input;
40
+ }
41
+ export function assertRecordOption(option) {
42
+ only(option, ['value', 'label', 'description', 'disabled', 'disabledReason']);
43
+ assertRecordRef(option.value);
44
+ if (typeof option.label !== 'string' || !option.label || option.label.length > 200) fail();
45
+ for (const key of ['description', 'disabledReason']) if (option[key] !== undefined && (typeof option[key] !== 'string' || option[key].length > 4096)) fail();
46
+ if (option.disabled !== undefined && typeof option.disabled !== 'boolean') fail();
47
+ return option;
48
+ }
49
+ export function assertRecordPage(page, objectSlug) {
50
+ only(page, ['items', 'nextCursor']);
51
+ if (!Array.isArray(page.items) || page.items.length > 100) fail();
52
+ if (page.nextCursor !== null && (typeof page.nextCursor !== 'string' || !page.nextCursor || page.nextCursor.length > 4096)) fail();
53
+ const seen = new Set();
54
+ for (const item of page.items) {
55
+ assertRecordOption(item);
56
+ const key = recordKey(item.value);
57
+ if (item.value.objectSlug !== objectSlug || seen.has(key)) fail();
58
+ seen.add(key);
59
+ }
60
+ return page;
61
+ }
62
+ export function assertSelectedRecordOption(option, ref) {
63
+ assertRecordRef(ref);
64
+ if (option === null) return null;
65
+ assertRecordOption(option);
66
+ if (recordKey(option.value) !== recordKey(ref)) fail();
67
+ return option;
68
+ }
69
+ export function useNormalizedRecordOptions({ useRef, useMemo }, options, objects) {
70
+ assertRecordObjects(objects);
71
+ const source = useRef({ options: null, search: null, getOption: null, revision: 0 });
72
+ const search = options?.search, getOption = options?.getOption;
73
+ if (source.current.options !== options || source.current.search !== search || source.current.getOption !== getOption) {
74
+ source.current = { options, search, getOption, revision: source.current.revision + 1 };
75
+ }
76
+ const revision = source.current.revision;
77
+ return useMemo(() => {
78
+ if (options === undefined) return {};
79
+ if (!options || typeof search !== 'function' || typeof getOption !== 'function') fail();
80
+ return {
81
+ sourceRevision: String(revision),
82
+ onSearch: input => {
83
+ assertRecordSearch(input, objects);
84
+ return Promise.resolve(search.call(options, input)).then(page => assertRecordPage(page, input.objectSlug));
85
+ },
86
+ onGetOption: value => {
87
+ assertRecordRef(value, objects);
88
+ return Promise.resolve(getOption.call(options, value)).then(option => assertSelectedRecordOption(option, value));
89
+ },
90
+ };
91
+ }, [revision]);
92
+ }
@@ -1,18 +1,82 @@
1
- export type FormField =
2
- | { type: "string"; label: string; required?: boolean; maxLength?: number }
1
+ import type { RecordRef, RecordObjectOption } from "./record-options.js";
2
+ export interface RuleMessages { required?: string; min?: string; max?: string; format?: string; }
3
+ export type FormField = (
4
+ | { type: "string"; label: string; required?: boolean; minLength?: number; maxLength?: number; format?: "email" | "url" }
3
5
  | { type: "boolean"; label: string; required?: boolean }
4
- | { type: "enum"; label: string; required?: boolean; options: readonly { value: string; label: string }[] };
5
- export interface FormSchema { fields: Readonly<Record<string, FormField>>; }
6
- export type FormValues<S extends FormSchema> = {
7
- [K in keyof S["fields"]]: S["fields"][K] extends { type: "boolean" } ? boolean : string;
6
+ | { type: "enum"; label: string; required?: boolean; options: readonly { value: string; label: string; description?: string }[] }
7
+ | { type: "number"; label: string; required?: boolean; min?: number; max?: number }
8
+ | { type: "plainDate" | "dateTime"; label: string; required?: boolean; min?: string; max?: string }
9
+ | ({ type: "record"; label: string; required?: boolean } & ({ objectSlug: string; objects?: never } | { objectSlug?: never; objects: readonly [RecordObjectOption] }))) & { messages?: RuleMessages };
10
+ export type LeafDefinition = FormField | { type: FormField['type']; label: (text: string) => unknown };
11
+ export interface ArrayField<F extends Record<string, LeafDefinition> = Record<string, LeafDefinition>> {
12
+ type: 'array'; label: string; minItems: number; maxItems: number; item: { type: 'object'; fields: F };
13
+ messages?: { min?: string; max?: string };
14
+ }
15
+ export type ArrayBuilder<F extends Record<string, LeafDefinition>> = Omit<ArrayField<F>, 'label'> & {
16
+ label(text: string): ArrayBuilder<F>;
17
+ min(count: number, options?: MessageOption): ArrayBuilder<F>;
18
+ max(count: number, options?: MessageOption): ArrayBuilder<F>;
19
+ custom(validator: (rows: ReadonlyValue<RowValues<F>[]>) => void | string | Promise<void | string>): ArrayBuilder<F>;
8
20
  };
21
+ export interface FormSchema { fields: Readonly<Record<string, LeafDefinition | ArrayField | ArrayBuilder<Record<string, LeafDefinition>>>>; }
22
+ export type ValueOf<F> = F extends { type: 'array'; item: { fields: infer R extends Record<string, LeafDefinition> } } ? RowValues<R>[]
23
+ : F extends { type: 'boolean' } ? boolean : F extends { type: 'record' } ? (F extends { objectSlug: infer O extends string } ? RecordRef & { objectSlug: O } : RecordRef) | null
24
+ : F extends { type: 'number' } ? number | null : F extends { type: 'plainDate' | 'dateTime' } ? string | null : string;
25
+ export type RowValues<F extends Record<string, LeafDefinition>> = { [K in keyof F]: ValueOf<F[K]> };
26
+ export type FormValues<S extends FormSchema> = { [K in keyof S['fields']]: ValueOf<S['fields'][K]> };
27
+ export type ArrayName<S extends FormSchema> = { [K in keyof S['fields']]: S['fields'][K]['type'] extends 'array' ? K : never }[keyof S['fields']] & string;
9
28
  export type FieldName<S extends FormSchema, T extends FormField['type']> = {
10
- [K in keyof S['fields']]: S['fields'][K]['type'] extends T ? K : never;
11
- }[keyof S['fields']] & string;
29
+ [K in keyof S['fields'] & string]: S['fields'][K] extends { type: 'array'; item: { fields: infer F extends Record<string, LeafDefinition> } }
30
+ ? `${K}[${number}].${{ [P in keyof F & string]: F[P]['type'] extends T ? P : never }[keyof F & string]}`
31
+ : S['fields'][K]['type'] extends T ? K : never;
32
+ }[keyof S['fields'] & string];
33
+ export type FormFieldErrors<S extends FormSchema, E = string> = { [K in keyof S['fields']]?: S['fields'][K] extends { type: 'array'; item: { fields: infer F } }
34
+ ? (Partial<Record<keyof F, E>> | null)[] : E };
35
+ export type FormArrayErrors<S extends FormSchema> = Partial<Record<ArrayName<S>, string>>;
12
36
  /** void 兼容既有提交;失败保留输入,字段名必须属于当前表单。 */
13
37
  export type FormSubmitResult<S extends FormSchema> = void | { ok: true } | {
14
38
  ok: false;
15
39
  code: string;
16
40
  message: string;
17
- fieldErrors?: Partial<Record<Extract<keyof S['fields'], string>, string>>;
41
+ fieldErrors?: FormFieldErrors<S>;
42
+ arrayErrors?: FormArrayErrors<S>;
43
+ };
44
+
45
+ export type FormValidationResult<S extends FormSchema> = void | {
46
+ fieldErrors?: FormFieldErrors<S>;
47
+ arrayErrors?: FormArrayErrors<S>;
48
+ formError?: string;
49
+ };
50
+ type FieldValue<F extends FormField> = FormValues<{ fields: { value: F } }>['value'];
51
+ type ReadonlyValue<T> = T extends object ? { readonly [K in keyof T]: ReadonlyValue<T[K]> } : T;
52
+ export type FormOptions<S extends FormSchema> = {
53
+ schema: S;
54
+ initialValues: FormValues<S>;
55
+ validationMode?: 'onBlur' | 'onChange' | 'onSubmit';
56
+ validate?: (values: ReadonlyValue<FormValues<S>>) => FormValidationResult<S> | Promise<FormValidationResult<S>>;
57
+ };
58
+ type MessageOption = { message: string };
59
+ export type FieldBuilder<F extends FormField> = Omit<F, 'label'> & {
60
+ label(text: string): FieldBuilder<F>;
61
+ required(message?: string): FieldBuilder<F>;
62
+ optional(): FieldBuilder<F>;
63
+ custom(validator: (value: ReadonlyValue<FieldValue<F>>) => void | string | Promise<void | string>): FieldBuilder<F>;
64
+ } & (F['type'] extends 'string' | 'number' | 'plainDate' | 'dateTime' ? {
65
+ min(value: F['type'] extends 'string' | 'number' ? number : string, options?: MessageOption): FieldBuilder<F>;
66
+ max(value: F['type'] extends 'string' | 'number' ? number : string, options?: MessageOption): FieldBuilder<F>;
67
+ } : {}) & (F['type'] extends 'string' ? {
68
+ email(options?: MessageOption): FieldBuilder<F>;
69
+ url(options?: MessageOption): FieldBuilder<F>;
70
+ } : {});
71
+ // builder 的 label 是方法,wire 的 label 是字符串;只有编译后才跨 Worker 边界。
72
+ export const Forms: {
73
+ schema<const F extends Record<string, LeafDefinition | ArrayBuilder<Record<string, LeafDefinition>>>>(fields: F): { readonly fields: F };
74
+ array<const F extends Record<string, LeafDefinition>>(fields: F): ArrayBuilder<F>;
75
+ string(): FieldBuilder<Extract<FormField, { type: 'string' }>>;
76
+ number(): FieldBuilder<Extract<FormField, { type: 'number' }>>;
77
+ boolean(): FieldBuilder<Extract<FormField, { type: 'boolean' }>>;
78
+ enum(options: readonly { value: string; label: string; description?: string }[]): FieldBuilder<Extract<FormField, { type: 'enum' }>>;
79
+ plainDate(): FieldBuilder<{ type: 'plainDate'; label: string }>;
80
+ dateTime(): FieldBuilder<{ type: 'dateTime'; label: string }>;
81
+ record<const O extends string>(objectSlug: O): FieldBuilder<{ type: 'record'; label: string; objectSlug: O }>;
18
82
  };