@svadmin/surface 0.18.1 → 0.19.0

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 CHANGED
@@ -146,9 +146,9 @@ Only fences tagged `surface` (```` ```surface ````) are treated as proposals; ev
146
146
 
147
147
  `buildSurfaceAgentPrompt()` and `parseSurfaceAgentProposal()` remain compatible with the legacy proposal-only `surface-agent/v1` envelope. The new message API uses `surface-agent/v2` with a structured `cannot-fulfill` alternative. `createSurfaceAgentStream()` buffers bounded text and validates at `finish()`; it does **not** render partial JSON or implement OpenUI Lang streaming.
148
148
 
149
- ## Host-only record selection
149
+ ## Host-only interactions
150
150
 
151
- `SurfaceRenderer` stays read-only and executes nothing on its own. A host that wants a follow-up from a card registers `onInteraction`; only then does `resource-table` render a per-row select button:
151
+ `SurfaceRenderer` stays read-only and executes nothing on its own. A host that wants a follow-up from a card registers `onInteraction`; only then do interactive widgets expose their affordances:
152
152
 
153
153
  ```svelte
154
154
  <SurfaceRenderer
@@ -156,18 +156,30 @@ Only fences tagged `surface` (```` ```surface ````) are treated as proposals; ev
156
156
  {policy}
157
157
  {dataProvider}
158
158
  onInteraction={(event) => {
159
- if (event.type !== 'select-record') return;
160
- // event.record only carries JSON-safe values from the widget's declared columns.
161
- sendFollowUp(`继续分析 ${JSON.stringify(event.record)}`);
159
+ if (event.type === 'select-record') {
160
+ // event.record only carries JSON-safe values from the widget's declared columns.
161
+ sendFollowUp(`继续分析 ${JSON.stringify(event.record)}`);
162
+ } else {
163
+ // event.values are the widget's local field values, re-validated by the renderer.
164
+ sendFollowUp(`提交内容 ${JSON.stringify(event.values)}`);
165
+ }
162
166
  }}
163
167
  />
164
168
  ```
165
169
 
170
+ Two event shapes exist, both scoped with `surfaceId`, `widgetId` and optional `sourceId`:
171
+
172
+ | Event | Emitted by | Payload |
173
+ | --- | --- | --- |
174
+ | `select-record` | `resource-table` row select | `record`: detached copy projected from the widget's declared columns |
175
+ | `submit-values` | widgets that call `onSubmitValues`, such as form widgets in a custom catalog | `values`: the widget's local values, re-validated and detached by the renderer |
176
+
166
177
  An interaction event is presentation data, not an authorization record:
167
178
 
168
179
  - The library never executes the follow-up, and `SurfaceSpec` gains no action, URL, event handler, or mutation field.
169
- - `record` is a detached copy projected from the widget's declared columns; undeclared fields are dropped, and values that are not JSON-safe (for example `Date`, `NaN`, or functions) are omitted instead of converted.
170
- - When nothing survives projection, no event is emitted. `projectInteractionRecord()` is exported for hosts and custom catalog widgets that need the same rule.
180
+ - Undeclared record fields are dropped, and values that are not JSON-safe (for example `Date`, `NaN`, or functions) are omitted instead of converted.
181
+ - Submitted values fail closed: dangerous keys such as `__proto__`, non-JSON values, and payloads above `SURFACE_INTERACTION_MAX_CHARACTERS` produce no event.
182
+ - When nothing survives projection, no event is emitted. `projectInteractionRecord()` and `projectInteractionValues()` are exported for hosts and custom catalog widgets that need the same rules.
171
183
 
172
184
  ## Revisioned edits and explicit preview
173
185
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "surface": "0.18.x",
2
+ "surface": "0.19.x",
3
3
  "minimumSupported": {
4
4
  "@svadmin/core": "0.53.0",
5
5
  "@svadmin/ui": "0.73.0",
package/dist/catalog.d.ts CHANGED
@@ -10,6 +10,8 @@ export interface SurfaceWidgetRendererProps {
10
10
  readonly messages?: SurfaceMessages;
11
11
  /** 宿主注册后,组件才渲染可选择行;组件自行按声明列投影记录。 */
12
12
  readonly onSelectRecord?: (record: JsonObject) => void;
13
+ /** 宿主注册后,组件才提供提交入口;值由渲染器重新校验后再交给宿主。 */
14
+ readonly onSubmitValues?: (values: JsonObject) => void;
13
15
  }
14
16
  export interface SurfaceWidgetRegistration extends SurfaceWidgetDefinition {
15
17
  readonly presentation?: 'surface-appearance/v1';
@@ -5,7 +5,7 @@
5
5
  import { SvelteMap } from 'svelte/reactivity';
6
6
  import { resolveSurfaceSourceData } from '../binding.js';
7
7
  import { defaultSurfaceCatalog } from '../catalog.js';
8
- import type { SurfaceInteractionEvent } from '../interactions.js';
8
+ import { projectInteractionValues, type SurfaceInteractionEvent } from '../interactions.js';
9
9
  import type { SurfaceRenderCatalog } from '../catalog.js';
10
10
  import { loadSurfaceSource } from '../runtime.js';
11
11
  import { resolveSurfaceMessages } from '../localization.js';
@@ -79,6 +79,17 @@
79
79
  ...(sourceId === undefined ? {} : { sourceId }),
80
80
  });
81
81
  }
82
+
83
+ function valuesSubmitted(surfaceId: string, widgetId: string, sourceId: string | undefined, values: JsonObject): void {
84
+ if (!onInteraction) return;
85
+ // 组件值同样属于不可信输入:重新校验并深拷贝后才交给宿主。
86
+ const projected = projectInteractionValues(values);
87
+ if (projected === null) return;
88
+ onInteraction({
89
+ type: 'submit-values', surfaceId, widgetId, values: projected,
90
+ ...(sourceId === undefined ? {} : { sourceId }),
91
+ });
92
+ }
82
93
  const widgetRegistrations = $derived(new Map(catalog.widgets.map((widget) => [widget.type, widget])));
83
94
  const sourceStates = new SvelteMap<string, SurfaceSourceDataState>();
84
95
  let destroyed = false;
@@ -197,6 +208,7 @@
197
208
  locale={activeLocale} messages={activeMessages}
198
209
  {...(onInteraction === undefined ? {} : {
199
210
  onSelectRecord: (record: JsonObject) => recordSelected(validation.value.surfaceId, widget.id, widget.binding?.sourceId, record),
211
+ onSubmitValues: (values: JsonObject) => valuesSubmitted(validation.value.surfaceId, widget.id, widget.binding?.sourceId, values),
200
212
  })} />
201
213
  {/if}
202
214
  </div>
@@ -1,4 +1,4 @@
1
- import type { SurfaceInteractionEvent } from '../interactions.js';
1
+ import { type SurfaceInteractionEvent } from '../interactions.js';
2
2
  import type { SurfaceRenderCatalog } from '../catalog.js';
3
3
  import type { SurfaceMessages } from '../localization.js';
4
4
  import type { SurfaceDataError, SurfaceDataProvider, SurfacePolicy, SurfaceValidationIssue } from '../types.js';
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { SURFACE_LIMITS, SURFACE_SCHEMA_VERSION } from './types.js';
2
2
  export type { JsonObject, JsonPrimitive, JsonValue, ResourceListDataSource, ResourceListSource, ResourceOneDataSource, ResourceOneSource, SurfaceBinding, SurfaceCatalog, SurfaceCatalogDataKind, SurfaceDataError, SurfaceDataProvider, SurfaceDataSource, SurfaceFilter, SurfaceGridLayout, SurfaceGridSpan, SurfacePolicy, SurfaceResourcePolicy, SurfaceSort, SurfaceSpec, SurfaceValidationCode, SurfaceValidationIssue, SurfaceValidationResult, SurfaceWidget, SurfaceWidgetDataState, SurfaceWidgetDefinition, } from './types.js';
3
3
  export { validateSurfaceSpec } from './validation.js';
4
- export { projectInteractionRecord } from './interactions.js';
4
+ export { SURFACE_INTERACTION_MAX_CHARACTERS, projectInteractionRecord, projectInteractionValues } from './interactions.js';
5
5
  export type { SurfaceInteraction, SurfaceInteractionEvent } from './interactions.js';
6
6
  export type { SurfaceMessages } from './localization.js';
7
7
  export { SURFACE_AGENT_SCHEMA_VERSION, buildSurfaceAgentPrompt, buildSurfaceAgentMessages, parseSurfaceAgentProposal, parseSurfaceAgentResponse, createSurfaceAgentStream, } from './agent.js';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { SURFACE_LIMITS, SURFACE_SCHEMA_VERSION } from './types.js';
2
2
  export { validateSurfaceSpec } from './validation.js';
3
- export { projectInteractionRecord } from './interactions.js';
3
+ export { SURFACE_INTERACTION_MAX_CHARACTERS, projectInteractionRecord, projectInteractionValues } from './interactions.js';
4
4
  export { SURFACE_AGENT_SCHEMA_VERSION, buildSurfaceAgentPrompt, buildSurfaceAgentMessages, parseSurfaceAgentProposal, parseSurfaceAgentResponse, createSurfaceAgentStream, } from './agent.js';
5
5
  export { SURFACE_AGENT_FENCE_TAG, splitSurfaceAgentText, } from './agent-text.js';
6
6
  export { SURFACE_AGENT_LIMITS, SURFACE_AGENT_RESPONSE_SCHEMA_VERSION, SURFACE_CATALOG_SCHEMA_VERSION, createSurfaceCatalogManifest, createSurfaceAgentResponseSchema, selectSurfaceCatalog, } from './agent-contract.js';
@@ -1,17 +1,31 @@
1
1
  import type { JsonObject } from './types.js';
2
2
  /**
3
- * 用户在界面上主动触发的只读选择,不是 action、mutation 或授权凭据。
3
+ * 用户在界面上主动触发的只读交互,不是 action、mutation 或授权凭据。
4
4
  * 宿主自行决定是否把它变成后续操作(例如追加一条用户消息)。
5
5
  */
6
- export interface SurfaceInteraction {
6
+ export type SurfaceInteraction = {
7
7
  readonly type: 'select-record';
8
8
  /** 只包含声明列的 JSON 安全值;不完整,也不能当作授权记录。 */
9
9
  readonly record: JsonObject;
10
- }
10
+ } | {
11
+ readonly type: 'submit-values';
12
+ /** 组件本地收集的字段值;宿主必须重新校验,不能当作已授权输入。 */
13
+ readonly values: JsonObject;
14
+ };
11
15
  /** 渲染器补齐作用域后交给宿主的事件;宿主不得把这些字段写回渲染器。 */
12
- export interface SurfaceInteractionEvent extends SurfaceInteraction {
16
+ export type SurfaceInteractionEvent = SurfaceInteraction & {
13
17
  readonly surfaceId: string;
14
18
  readonly widgetId: string;
15
19
  readonly sourceId?: string;
16
- }
20
+ };
21
+ /** 组件一次回传的最大字符数;超过时失败关闭而不是截断。 */
22
+ export declare const SURFACE_INTERACTION_MAX_CHARACTERS = 16384;
17
23
  export declare function projectInteractionRecord(record: JsonObject, fields: readonly string[]): JsonObject | null;
24
+ /**
25
+ * 校验并深拷贝组件提交的字段值。只接受 JSON 安全值;危险键(如 `__proto__`)、
26
+ * 非 JSON 值和超过字符上限的内容一律失败关闭,调用方不应发出事件。
27
+ *
28
+ * @param values 组件本地状态,例如表单字段。
29
+ * @returns 独立副本,或 `null`。
30
+ */
31
+ export declare function projectInteractionValues(values: JsonObject): JsonObject | null;
@@ -1,4 +1,6 @@
1
1
  import { isJsonValue, jsonValueIssue } from './json.js';
2
+ /** 组件一次回传的最大字符数;超过时失败关闭而不是截断。 */
3
+ export const SURFACE_INTERACTION_MAX_CHARACTERS = 16_384;
2
4
  /**
3
5
  * 只投影声明过的列,并跳过无法安全序列化的值(如 Date、函数、循环引用)。
4
6
  * 嵌套对象和数组会深拷贝,宿主后续修改不会写回渲染器缓存。
@@ -42,3 +44,25 @@ export function projectInteractionRecord(record, fields) {
42
44
  }
43
45
  return included === 0 ? null : projected;
44
46
  }
47
+ /**
48
+ * 校验并深拷贝组件提交的字段值。只接受 JSON 安全值;危险键(如 `__proto__`)、
49
+ * 非 JSON 值和超过字符上限的内容一律失败关闭,调用方不应发出事件。
50
+ *
51
+ * @param values 组件本地状态,例如表单字段。
52
+ * @returns 独立副本,或 `null`。
53
+ */
54
+ export function projectInteractionValues(values) {
55
+ if (jsonValueIssue(values) !== null)
56
+ return null;
57
+ const detached = detachJsonValue(values);
58
+ if (detached === null || typeof detached !== 'object' || Array.isArray(detached))
59
+ return null;
60
+ let serialized;
61
+ try {
62
+ serialized = JSON.stringify(detached);
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ return serialized.length > SURFACE_INTERACTION_MAX_CHARACTERS ? null : detached;
68
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/surface",
3
- "version": "0.18.1",
3
+ "version": "0.19.0",
4
4
  "description": "Validated declarative surfaces for svadmin dashboards",
5
5
  "type": "module",
6
6
  "sideEffects": [