@svadmin/surface 0.14.3 → 0.17.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 +25 -0
- package/compatibility.json +1 -1
- package/dist/agent-text.d.ts +27 -0
- package/dist/agent-text.js +94 -0
- package/dist/agent.d.ts +7 -1
- package/dist/agent.js +6 -2
- package/dist/builtin-schemas.d.ts +2 -2
- package/dist/business-definitions.d.ts +2 -2
- package/dist/components/ActivityFeedWidget.svelte +2 -2
- package/dist/components/BarChartWidget.svelte +2 -2
- package/dist/components/LineChartWidget.svelte +2 -2
- package/dist/components/MetricWidget.svelte +2 -2
- package/dist/components/ResourceDetailWidget.svelte +2 -2
- package/dist/components/ResourceTableWidget.svelte +2 -2
- package/dist/components/SurfaceRenderer.svelte +36 -9
- package/dist/components/SvarGridWidget.svelte +3 -3
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/json-render.js +1 -1
- package/dist/localization.d.ts +5 -0
- package/dist/localization.js +13 -0
- package/dist/runtime.d.ts +3 -1
- package/dist/runtime.js +9 -4
- package/dist/source-cache.d.ts +6 -3
- package/dist/source-cache.js +26 -3
- package/dist/workflows/catalog.d.ts +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -119,6 +119,31 @@ if (response.ok && response.value.action === 'propose') {
|
|
|
119
119
|
|
|
120
120
|
Use the same task-scoped catalog for generation, validation and rendering. JSON Schema narrows generation but does not replace cross-source binding checks, field policy or server authorization. The schema is not claimed to match every vendor's restricted structured-output subset.
|
|
121
121
|
|
|
122
|
+
### Inline chat answers
|
|
123
|
+
|
|
124
|
+
A chat host can answer with prose plus one inline card instead of a JSON-only reply. Pass `{ presentation: 'chat' }`, then split the streamed answer with `splitSurfaceAgentText()`:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import {
|
|
128
|
+
SURFACE_AGENT_FENCE_TAG,
|
|
129
|
+
buildSurfaceAgentMessages,
|
|
130
|
+
parseSurfaceAgentResponse,
|
|
131
|
+
splitSurfaceAgentText,
|
|
132
|
+
} from '@svadmin/surface';
|
|
133
|
+
|
|
134
|
+
const messages = buildSurfaceAgentMessages(request, catalog, policy, { presentation: 'chat' });
|
|
135
|
+
// 边生成边切分;未闭合的 surface 围栏返回 complete: false。
|
|
136
|
+
for (const segment of splitSurfaceAgentText(streamedText, { streaming: true })) {
|
|
137
|
+
if (segment.kind === 'text') renderMarkdown(segment.text);
|
|
138
|
+
else if (segment.complete) {
|
|
139
|
+
const parsed = parseSurfaceAgentResponse(segment.json, catalog, policy);
|
|
140
|
+
if (parsed.ok && parsed.value.action === 'propose') renderPreview(parsed.value.spec);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Only fences tagged `surface` (```` ```surface ````) are treated as proposals; every other fence stays in the prose, so ordinary JSON or shell snippets are never mistaken for a card. An unterminated fence becomes an incomplete segment only while `streaming` is true, which keeps partial JSON out of the renderer while the answer is still generating. Segmentation is presentation only: it does not validate, query data, render, or replace the host approval step.
|
|
146
|
+
|
|
122
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.
|
|
123
148
|
|
|
124
149
|
## Revisioned edits and explicit preview
|
package/compatibility.json
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** 聊天回答里标记界面提案的围栏标签,例如 ```surface。 */
|
|
2
|
+
export declare const SURFACE_AGENT_FENCE_TAG = "surface";
|
|
3
|
+
/** 聊天回答切分后的片段:普通正文,或一段待校验的界面提案 JSON。 */
|
|
4
|
+
export type SurfaceAgentTextSegment = {
|
|
5
|
+
readonly kind: 'text';
|
|
6
|
+
readonly text: string;
|
|
7
|
+
} | {
|
|
8
|
+
readonly kind: 'proposal';
|
|
9
|
+
readonly json: string;
|
|
10
|
+
readonly complete: boolean;
|
|
11
|
+
};
|
|
12
|
+
export interface SurfaceAgentTextOptions {
|
|
13
|
+
/** 生成尚未结束时,未闭合的 surface 围栏作为未完成提案返回。 */
|
|
14
|
+
readonly streaming?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 把聊天回答切成正文与界面提案,供宿主内联渲染。
|
|
18
|
+
*
|
|
19
|
+
* 只识别 `surface` 标签的围栏;其它围栏(例如 json 代码块)原样留在正文里,
|
|
20
|
+
* 避免把普通代码误当成界面提案。片段内容不做校验,宿主仍需按 catalog 和
|
|
21
|
+
* policy 调用 `parseSurfaceAgentResponse()` 或 `parseSurfaceAgentProposal()`。
|
|
22
|
+
*
|
|
23
|
+
* @param text 模型输出的完整或增量文本。
|
|
24
|
+
* @param options 生成中时传入 `streaming: true`,未闭合围栏按未完成提案返回。
|
|
25
|
+
* @returns 按出现顺序排列的片段;正文与提案互不重叠,围栏行不出现在正文里。
|
|
26
|
+
*/
|
|
27
|
+
export declare function splitSurfaceAgentText(text: string, options?: SurfaceAgentTextOptions): readonly SurfaceAgentTextSegment[];
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { SURFACE_AGENT_LIMITS } from './agent-contract.js';
|
|
2
|
+
/** 聊天回答里标记界面提案的围栏标签,例如 ```surface。 */
|
|
3
|
+
export const SURFACE_AGENT_FENCE_TAG = 'surface';
|
|
4
|
+
const OPEN_FENCE = /^ {0,3}(`{3,}|~{3,})[ \t]*([^\s`]*)[^`]*$/u;
|
|
5
|
+
const CLOSE_FENCE = /^ {0,3}(`{3,}|~{3,})[ \t]*$/u;
|
|
6
|
+
function openingFence(line) {
|
|
7
|
+
const match = OPEN_FENCE.exec(line);
|
|
8
|
+
if (!match?.[1])
|
|
9
|
+
return undefined;
|
|
10
|
+
return { marker: match[1], tag: (match[2] ?? '').toLowerCase(), line };
|
|
11
|
+
}
|
|
12
|
+
function closesFence(line, fence) {
|
|
13
|
+
const match = CLOSE_FENCE.exec(line);
|
|
14
|
+
if (!match?.[1])
|
|
15
|
+
return false;
|
|
16
|
+
return match[1][0] === fence.marker[0] && match[1].length >= fence.marker.length;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 把聊天回答切成正文与界面提案,供宿主内联渲染。
|
|
20
|
+
*
|
|
21
|
+
* 只识别 `surface` 标签的围栏;其它围栏(例如 json 代码块)原样留在正文里,
|
|
22
|
+
* 避免把普通代码误当成界面提案。片段内容不做校验,宿主仍需按 catalog 和
|
|
23
|
+
* policy 调用 `parseSurfaceAgentResponse()` 或 `parseSurfaceAgentProposal()`。
|
|
24
|
+
*
|
|
25
|
+
* @param text 模型输出的完整或增量文本。
|
|
26
|
+
* @param options 生成中时传入 `streaming: true`,未闭合围栏按未完成提案返回。
|
|
27
|
+
* @returns 按出现顺序排列的片段;正文与提案互不重叠,围栏行不出现在正文里。
|
|
28
|
+
*/
|
|
29
|
+
export function splitSurfaceAgentText(text, options = {}) {
|
|
30
|
+
if (typeof text !== 'string' || text.length === 0)
|
|
31
|
+
return [];
|
|
32
|
+
// 超长输入按上限截断扫描,其余内容作为正文交还宿主,避免无界遍历。
|
|
33
|
+
const bounded = text.slice(0, SURFACE_AGENT_LIMITS.maxInputCharacters);
|
|
34
|
+
const overflow = text.slice(bounded.length);
|
|
35
|
+
const segments = [];
|
|
36
|
+
let prose = [];
|
|
37
|
+
let fence;
|
|
38
|
+
let proposal;
|
|
39
|
+
const flushProse = () => {
|
|
40
|
+
const value = prose.join('\n');
|
|
41
|
+
prose = [];
|
|
42
|
+
if (value === '')
|
|
43
|
+
return;
|
|
44
|
+
segments.push({ kind: 'text', text: value });
|
|
45
|
+
};
|
|
46
|
+
const flushProposal = (complete) => {
|
|
47
|
+
if (proposal === undefined || fence === undefined)
|
|
48
|
+
return;
|
|
49
|
+
flushProse();
|
|
50
|
+
segments.push({ kind: 'proposal', json: proposal.join('\n').trim(), complete });
|
|
51
|
+
proposal = undefined;
|
|
52
|
+
fence = undefined;
|
|
53
|
+
};
|
|
54
|
+
for (const line of bounded.split(/\r?\n/u)) {
|
|
55
|
+
if (proposal !== undefined && fence !== undefined) {
|
|
56
|
+
if (closesFence(line, fence))
|
|
57
|
+
flushProposal(true);
|
|
58
|
+
else
|
|
59
|
+
proposal.push(line);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (fence !== undefined) {
|
|
63
|
+
// 其它围栏内的内容原样保留,围栏内的 surface 字样不是提案。
|
|
64
|
+
prose.push(line);
|
|
65
|
+
if (closesFence(line, fence))
|
|
66
|
+
fence = undefined;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const open = openingFence(line);
|
|
70
|
+
if (open === undefined) {
|
|
71
|
+
prose.push(line);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
fence = open;
|
|
75
|
+
if (open.tag === SURFACE_AGENT_FENCE_TAG)
|
|
76
|
+
proposal = [];
|
|
77
|
+
else
|
|
78
|
+
prose.push(line);
|
|
79
|
+
}
|
|
80
|
+
if (proposal !== undefined && fence !== undefined) {
|
|
81
|
+
if (options.streaming === true)
|
|
82
|
+
flushProposal(false);
|
|
83
|
+
else {
|
|
84
|
+
// 非流式输出里未闭合的围栏是残缺内容,交还正文而不是静默丢弃。
|
|
85
|
+
prose.push(fence.line, ...proposal);
|
|
86
|
+
fence = undefined;
|
|
87
|
+
proposal = undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
flushProse();
|
|
91
|
+
if (overflow.length > 0)
|
|
92
|
+
segments.push({ kind: 'text', text: overflow });
|
|
93
|
+
return segments;
|
|
94
|
+
}
|
package/dist/agent.d.ts
CHANGED
|
@@ -37,8 +37,14 @@ export declare function parseSurfaceAgentProposal(input: unknown, catalog: Surfa
|
|
|
37
37
|
export declare function parseSurfaceAgentResponse(input: unknown, catalog: SurfaceCatalog, policy: SurfacePolicy): SurfaceAgentResponseResult;
|
|
38
38
|
/** 兼容旧 v1 输出,同时补齐从组件契约派生的完整参数/结构 schema。 */
|
|
39
39
|
export declare function buildSurfaceAgentPrompt(request: string, catalog: SurfaceCatalog, policy: SurfacePolicy): string;
|
|
40
|
+
/** 模型回答的呈现方式:纯 JSON 信封,或聊天正文加一个内联界面提案。 */
|
|
41
|
+
export type SurfaceAgentPresentation = 'json' | 'chat';
|
|
42
|
+
export interface SurfaceAgentMessageOptions {
|
|
43
|
+
/** 默认 `json`,保持既有调用方的行为不变。 */
|
|
44
|
+
readonly presentation?: SurfaceAgentPresentation;
|
|
45
|
+
}
|
|
40
46
|
/** 系统契约与用户需求分开传递;不绑定某个模型厂商或 CSS 编译器。 */
|
|
41
|
-
export declare function buildSurfaceAgentMessages(request: string, catalog: SurfaceCatalog, policy: SurfacePolicy): readonly SurfaceAgentMessage[];
|
|
47
|
+
export declare function buildSurfaceAgentMessages(request: string, catalog: SurfaceCatalog, policy: SurfacePolicy, options?: SurfaceAgentMessageOptions): readonly SurfaceAgentMessage[];
|
|
42
48
|
export interface SurfaceAgentStream {
|
|
43
49
|
/** 接收文本,不解析、不渲染、不查询数据。 */
|
|
44
50
|
push(chunk: string): {
|
package/dist/agent.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Type } from 'typebox';
|
|
2
2
|
import { Compile } from 'typebox/compile';
|
|
3
3
|
import { SURFACE_AGENT_LIMITS, SURFACE_AGENT_RESPONSE_SCHEMA_VERSION, SURFACE_AGENT_SCHEMA_VERSION, createSurfaceAgentResponseSchema, createSurfaceCatalogManifest, createSurfaceGenerationSpecSchema, surfaceCannotFulfillSchema, surfaceSchemaToJson, } from './agent-contract.js';
|
|
4
|
+
import { SURFACE_AGENT_FENCE_TAG } from './agent-text.js';
|
|
4
5
|
import { validateSurfaceSpec, surfaceSchemaIssues } from './validation.js';
|
|
5
6
|
import { parseSurfaceJson, surfaceWireError } from './wire.js';
|
|
6
7
|
export { SURFACE_AGENT_SCHEMA_VERSION } from './agent-contract.js';
|
|
@@ -77,9 +78,12 @@ export function buildSurfaceAgentPrompt(request, catalog, policy) {
|
|
|
77
78
|
return boundedPrompt(`${request}\n\n[svadmin surface agent protocol]\nReturn only a human-reviewable fenced JSON proposal. ${safetyInstructions}\n${contractContext(catalog, policy)}\nOutput JSON Schema: ${JSON.stringify(schema)}\nIf the request cannot be represented safely, explain the limitation without inventing fields or capabilities. For machine-readable cannot-fulfill responses use the surface-agent/v2 message API.`);
|
|
78
79
|
}
|
|
79
80
|
/** 系统契约与用户需求分开传递;不绑定某个模型厂商或 CSS 编译器。 */
|
|
80
|
-
export function buildSurfaceAgentMessages(request, catalog, policy) {
|
|
81
|
+
export function buildSurfaceAgentMessages(request, catalog, policy, options = {}) {
|
|
81
82
|
validateRequest(request);
|
|
82
|
-
const
|
|
83
|
+
const schema = JSON.stringify(createSurfaceAgentResponseSchema(catalog, policy));
|
|
84
|
+
const content = options.presentation === 'chat'
|
|
85
|
+
? boundedPrompt(`[svadmin surface agent protocol]\nAnswer the user in short prose first, then embed exactly one interface proposal in a \`\`\`${SURFACE_AGENT_FENCE_TAG} fenced block matching the output schema. Emit at most one such block, do not add other fenced blocks, and do not repeat the JSON outside the fence. ${safetyInstructions}\n${contractContext(catalog, policy)}\nOutput JSON Schema: ${schema}\nIf the request cannot be represented, answer in prose and omit the fenced block; never invent capabilities or return an incomplete spec.`)
|
|
86
|
+
: boundedPrompt(`[svadmin surface agent protocol]\nReturn exactly one JSON object matching the output schema, without markdown or prose. ${safetyInstructions}\n${contractContext(catalog, policy)}\nOutput JSON Schema: ${schema}\nIf the request cannot be represented, return action "cannot-fulfill" with a reason and a short message. Never invent capabilities or return an incomplete spec.`);
|
|
83
87
|
return [{ role: 'system', content }, { role: 'user', content: request }];
|
|
84
88
|
}
|
|
85
89
|
/** 有界传输缓冲,不冒充 OpenUI 增量渲染;半成品不进入运行时。 */
|
|
@@ -37,13 +37,13 @@ export type ResourceTableProps = Static<typeof resourceTablePropsSchema>;
|
|
|
37
37
|
export type BarChartProps = Static<typeof barChartPropsSchema>;
|
|
38
38
|
export type LineChartProps = Static<typeof lineChartPropsSchema>;
|
|
39
39
|
export declare const styledMetricPropsSchema: Type.TUnion<[Type.TObject<{
|
|
40
|
-
tone: Type.TOptional<Type.TUnion<Type.TLiteral<"info" | "warning" | "success" | "
|
|
40
|
+
tone: Type.TOptional<Type.TUnion<Type.TLiteral<"info" | "warning" | "success" | "danger" | "neutral">[]>>;
|
|
41
41
|
label: Type.TString;
|
|
42
42
|
format: Type.TLiteral<"currency">;
|
|
43
43
|
currency: Type.TString;
|
|
44
44
|
description: Type.TOptional<Type.TString>;
|
|
45
45
|
}>, Type.TObject<{
|
|
46
|
-
tone: Type.TOptional<Type.TUnion<Type.TLiteral<"info" | "warning" | "success" | "
|
|
46
|
+
tone: Type.TOptional<Type.TUnion<Type.TLiteral<"info" | "warning" | "success" | "danger" | "neutral">[]>>;
|
|
47
47
|
label: Type.TString;
|
|
48
48
|
format: Type.TUnion<[Type.TLiteral<"number">, Type.TLiteral<"percent">]>;
|
|
49
49
|
currency: Type.TOptional<Type.TNever>;
|
|
@@ -7,7 +7,7 @@ export declare const resourceDetailPropsSchema: Type.TObject<{
|
|
|
7
7
|
format: Type.TOptional<Type.TUnion<Type.TLiteral<string>[]>>;
|
|
8
8
|
}>>;
|
|
9
9
|
title: Type.TString;
|
|
10
|
-
tone: Type.TOptional<Type.TUnion<Type.TLiteral<"info" | "warning" | "success" | "
|
|
10
|
+
tone: Type.TOptional<Type.TUnion<Type.TLiteral<"info" | "warning" | "success" | "danger" | "neutral">[]>>;
|
|
11
11
|
}>;
|
|
12
12
|
export declare const activityFeedPropsSchema: Type.TObject<{
|
|
13
13
|
idField: Type.TString;
|
|
@@ -18,7 +18,7 @@ export declare const activityFeedPropsSchema: Type.TObject<{
|
|
|
18
18
|
commentField: Type.TOptional<Type.TString>;
|
|
19
19
|
statusField: Type.TOptional<Type.TString>;
|
|
20
20
|
title: Type.TString;
|
|
21
|
-
tone: Type.TOptional<Type.TUnion<Type.TLiteral<"info" | "warning" | "success" | "
|
|
21
|
+
tone: Type.TOptional<Type.TUnion<Type.TLiteral<"info" | "warning" | "success" | "danger" | "neutral">[]>>;
|
|
22
22
|
}>;
|
|
23
23
|
export type ResourceDetailProps = Static<typeof resourceDetailPropsSchema>;
|
|
24
24
|
export type ActivityFeedProps = Static<typeof activityFeedPropsSchema>;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { asSurfaceActivities } from '../business-data.js';
|
|
6
6
|
import type { SurfaceWidgetRendererProps } from '../catalog.js';
|
|
7
7
|
import { surfaceMetric } from '../recipes.js';
|
|
8
|
-
import { formatSurfaceMessage, resolveSurfaceMessages } from '../localization.js';
|
|
8
|
+
import { formatSurfaceMessage, resolveSurfaceMessages, surfaceErrorMessage } from '../localization.js';
|
|
9
9
|
|
|
10
10
|
let { props, data, locale = 'en-US', messages }: SurfaceWidgetRendererProps = $props();
|
|
11
11
|
const baseProps = $derived(Object.fromEntries(Object.entries(props).filter(([key]) => key !== 'appearance')));
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
{:else if data.status === 'empty'}
|
|
24
24
|
<p role="status">{text.activityEmpty}</p>
|
|
25
25
|
{:else if data.status === 'error'}
|
|
26
|
-
<p role="alert">{text.activityUnavailable}</p>
|
|
26
|
+
<p role="alert">{surfaceErrorMessage(data.error, text, text.activityUnavailable, text.activityInvalidData)}</p>
|
|
27
27
|
{:else if activities?.ok}
|
|
28
28
|
<ActivityFeed activities={activities.value} allowComment={false} title={p.title} emptyLabel={text.activityEmpty}
|
|
29
29
|
countLabel={formatSurfaceMessage(text.activityCount, { count: activities.value.length })} />
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import Card from '@svadmin/ui/components/ui/card/card.svelte';
|
|
8
8
|
import { barChartPropsSchema } from '../builtin-schemas.js';
|
|
9
9
|
import type { SurfaceWidgetRendererProps } from '../catalog.js';
|
|
10
|
-
import { formatSurfaceMessage, resolveSurfaceMessages } from '../localization.js';
|
|
10
|
+
import { formatSurfaceMessage, resolveSurfaceMessages, surfaceErrorMessage } from '../localization.js';
|
|
11
11
|
import { asChartPoints, compactChartLabel } from '../widget-data.js';
|
|
12
12
|
|
|
13
13
|
let { props, data, locale = 'en-US', messages }: SurfaceWidgetRendererProps = $props();
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
{:else if data.status === 'empty'}
|
|
33
33
|
<p class="chart-state" role="status">{activeMessages.chartNoData}</p>
|
|
34
34
|
{:else if data.status === 'error'}
|
|
35
|
-
<p class="chart-state" role="alert">{data.error.
|
|
35
|
+
<p class="chart-state" role="alert">{surfaceErrorMessage(data.error, activeMessages, activeMessages.chartUnavailable, activeMessages.chartInvalidData)}</p>
|
|
36
36
|
{:else if data.status === 'ready' && points?.ok}
|
|
37
37
|
<div class="chart-frame" role="img" aria-label={chartProps.title}>
|
|
38
38
|
<BarChart data={chartData} showValues={chartProps.showValues ?? true} />
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import Card from '@svadmin/ui/components/ui/card/card.svelte';
|
|
8
8
|
import { lineChartPropsSchema } from '../builtin-schemas.js';
|
|
9
9
|
import type { SurfaceWidgetRendererProps } from '../catalog.js';
|
|
10
|
-
import { formatSurfaceMessage, resolveSurfaceMessages } from '../localization.js';
|
|
10
|
+
import { formatSurfaceMessage, resolveSurfaceMessages, surfaceErrorMessage } from '../localization.js';
|
|
11
11
|
import { asChartPoints, compactChartLabel } from '../widget-data.js';
|
|
12
12
|
|
|
13
13
|
let { props, data, locale = 'en-US', messages }: SurfaceWidgetRendererProps = $props();
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
{:else if data.status === 'empty'}
|
|
33
33
|
<p class="chart-state" role="status">{activeMessages.chartNoData}</p>
|
|
34
34
|
{:else if data.status === 'error'}
|
|
35
|
-
<p class="chart-state" role="alert">{data.error.
|
|
35
|
+
<p class="chart-state" role="alert">{surfaceErrorMessage(data.error, activeMessages, activeMessages.chartUnavailable, activeMessages.chartInvalidData)}</p>
|
|
36
36
|
{:else if data.status === 'ready' && points?.ok}
|
|
37
37
|
<div class="chart-frame" role="img" aria-label={chartProps.title}>
|
|
38
38
|
<LineChart
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import StatsCard from '@svadmin/ui/components/StatsCard.svelte';
|
|
6
6
|
import { styledMetricPropsSchema as metricPropsSchema } from '../builtin-schemas.js';
|
|
7
7
|
import type { SurfaceWidgetRendererProps } from '../catalog.js';
|
|
8
|
-
import { resolveSurfaceMessages } from '../localization.js';
|
|
8
|
+
import { resolveSurfaceMessages, surfaceErrorMessage } from '../localization.js';
|
|
9
9
|
|
|
10
10
|
let { widgetId, props, data, locale = 'en-US', messages }: SurfaceWidgetRendererProps = $props();
|
|
11
11
|
const activeMessages = $derived(resolveSurfaceMessages(locale, messages));
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
{:else}
|
|
43
43
|
<div class={"metric-state " + (metricClasses?.state ?? "")} role={data.status === 'error' ? 'alert' : 'status'}>
|
|
44
44
|
<strong>{metricProps.label}</strong>
|
|
45
|
-
<span>{data.status === 'error' ? data.error.
|
|
45
|
+
<span>{data.status === 'error' ? surfaceErrorMessage(data.error, activeMessages, activeMessages.providerUnavailable, activeMessages.metricInvalidData) : data.status === 'empty' ? activeMessages.metricNoData : activeMessages.metricInvalidData}</span>
|
|
46
46
|
</div>
|
|
47
47
|
{/if}
|
|
48
48
|
</article>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import type { SurfaceWidgetRendererProps } from '../catalog.js';
|
|
9
9
|
import type { JsonObject } from '../types.js';
|
|
10
10
|
import { surfaceMetric } from '../recipes.js';
|
|
11
|
-
import { resolveSurfaceMessages } from '../localization.js';
|
|
11
|
+
import { resolveSurfaceMessages, surfaceErrorMessage } from '../localization.js';
|
|
12
12
|
import { displayTableValue } from '../widget-data.js';
|
|
13
13
|
|
|
14
14
|
let { props, data, locale = 'en-US', messages }: SurfaceWidgetRendererProps = $props();
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
{:else if data.status === 'empty'}
|
|
31
31
|
<p role="status">{text.detailEmpty}</p>
|
|
32
32
|
{:else if data.status === 'error'}
|
|
33
|
-
<p role="alert">{text.detailUnavailable}</p>
|
|
33
|
+
<p role="alert">{surfaceErrorMessage(data.error, text, text.detailUnavailable, text.detailInvalidData)}</p>
|
|
34
34
|
{:else if record}
|
|
35
35
|
<dl aria-label={p.title}>
|
|
36
36
|
{#each p.fields as item, index (`${index}:${item.field}`)}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import Table from '@svadmin/ui/components/ui/table/table.svelte';
|
|
15
15
|
import { styledResourceTablePropsSchema as resourceTablePropsSchema } from '../builtin-schemas.js';
|
|
16
16
|
import type { SurfaceWidgetRendererProps } from '../catalog.js';
|
|
17
|
-
import { resolveSurfaceMessages } from '../localization.js';
|
|
17
|
+
import { resolveSurfaceMessages, surfaceErrorMessage } from '../localization.js';
|
|
18
18
|
import { asRecordArray, displayTableValue } from '../widget-data.js';
|
|
19
19
|
|
|
20
20
|
let { props, data, locale = 'en-US', messages }: SurfaceWidgetRendererProps = $props();
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
{:else if data.status === 'empty'}
|
|
36
36
|
<p class={"table-state " + (tableClasses?.state ?? "")} role="status">{tableProps.emptyLabel ?? activeMessages.tableNoRecords}</p>
|
|
37
37
|
{:else if data.status === 'error'}
|
|
38
|
-
<p class={"table-state " + (tableClasses?.state ?? "")} role="alert">{data.error.
|
|
38
|
+
<p class={"table-state " + (tableClasses?.state ?? "")} role="alert">{surfaceErrorMessage(data.error, activeMessages, activeMessages.tableUnavailable, activeMessages.tableInvalidData)}</p>
|
|
39
39
|
{:else if data.status === 'ready' && records?.ok}
|
|
40
40
|
<div class="surface-table">
|
|
41
41
|
<Table aria-label={tableProps.title}>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import type { SurfaceMessages } from '../localization.js';
|
|
12
12
|
import type {
|
|
13
13
|
SurfaceDataError, SurfaceDataProvider, SurfacePolicy,
|
|
14
|
-
SurfaceSourceDataState, SurfaceValidationIssue, JsonObject,
|
|
14
|
+
SurfaceSourceDataState, SurfaceValidationIssue, SurfaceWidgetDataState, JsonObject,
|
|
15
15
|
} from '../types.js';
|
|
16
16
|
import { validateSurfaceSpec } from '../validation.js';
|
|
17
17
|
import { createSurfaceSourceCache, sameSourceIdentity, snapshotSurfaceSource } from '../source-cache.js';
|
|
@@ -55,6 +55,18 @@
|
|
|
55
55
|
const i18n = useTranslation();
|
|
56
56
|
const activeLocale = $derived(locale ?? i18n.locale);
|
|
57
57
|
const activeMessages = $derived(resolveSurfaceMessages(activeLocale, messages));
|
|
58
|
+
// 统一呈现边界:错误状态只暴露本地化文案,宿主诊断仍留在 onError。
|
|
59
|
+
function presentSurfaceData(data: SurfaceWidgetDataState): SurfaceWidgetDataState {
|
|
60
|
+
if (data.status !== 'error') return data;
|
|
61
|
+
const message = data.error.code === 'access_denied'
|
|
62
|
+
? activeMessages.accessDenied
|
|
63
|
+
: data.error.code === 'provider_result_not_json' || data.error.code === 'binding_pointer_not_found'
|
|
64
|
+
? activeMessages.invalidData
|
|
65
|
+
: activeMessages.providerUnavailable;
|
|
66
|
+
return data.error.message === message
|
|
67
|
+
? data
|
|
68
|
+
: { ...data, error: { ...data.error, message } };
|
|
69
|
+
}
|
|
58
70
|
const validation = $derived(validateSurfaceSpec(spec, catalog, policy));
|
|
59
71
|
const widgetRegistrations = $derived(new Map(catalog.widgets.map((widget) => [widget.type, widget])));
|
|
60
72
|
const sourceStates = new SvelteMap<string, SurfaceSourceDataState>();
|
|
@@ -69,14 +81,16 @@
|
|
|
69
81
|
]);
|
|
70
82
|
// 复用 PR #428 的请求所有权与会话隔离实现,不维护第二套缓存。
|
|
71
83
|
const sourceCache = createSurfaceSourceCache({
|
|
84
|
+
// 缓存只保存原始 source 状态;本地化等呈现规则统一在渲染边界执行。
|
|
72
85
|
set: (id, state) => { sourceStates.set(id, state); },
|
|
73
86
|
remove: (id) => { sourceStates.delete(id); },
|
|
74
|
-
onError: (state) => { onError?.({ type: 'data', error: state.error }); },
|
|
87
|
+
onError: (state, diagnostic) => { onError?.({ type: 'data', error: diagnostic ?? state.error }); },
|
|
75
88
|
});
|
|
76
89
|
|
|
77
|
-
function providerError(sourceId: string
|
|
78
|
-
|
|
79
|
-
|
|
90
|
+
function providerError(sourceId: string): SurfaceSourceDataState {
|
|
91
|
+
return { status: 'error', sourceId, error: {
|
|
92
|
+
code: 'provider_failed', sourceId, message: activeMessages.providerUnavailable,
|
|
93
|
+
} };
|
|
80
94
|
}
|
|
81
95
|
|
|
82
96
|
const sourceRequests: SurfaceSourceRequest[] = $derived.by(() => {
|
|
@@ -97,11 +111,18 @@
|
|
|
97
111
|
request.id === snapshot.source.id && sameSourceIdentity(request.identity, identity));
|
|
98
112
|
return {
|
|
99
113
|
id: snapshot.source.id, identity, isCurrent,
|
|
100
|
-
async load(isRequestCurrent) {
|
|
101
|
-
if (!provider)
|
|
114
|
+
async load(isRequestCurrent, reportDiagnostic) {
|
|
115
|
+
if (!provider) {
|
|
116
|
+
reportDiagnostic?.({
|
|
117
|
+
code: 'provider_failed', sourceId: snapshot.source.id,
|
|
118
|
+
message: providerFailure instanceof Error ? providerFailure.message : 'Data provider request failed',
|
|
119
|
+
});
|
|
120
|
+
return providerError(snapshot.source.id);
|
|
121
|
+
}
|
|
102
122
|
try {
|
|
103
123
|
return await loadSurfaceSource({
|
|
104
124
|
source: snapshot.source, resourcePolicy: snapshot.resourcePolicy, provider,
|
|
125
|
+
...(reportDiagnostic === undefined ? {} : { reportDiagnostic }),
|
|
105
126
|
async authorize(resource, action) {
|
|
106
127
|
if (!isRequestCurrent()) return { can: false };
|
|
107
128
|
// 授权检查与缓存身份必须使用同一个所属上下文,不能回退到无关全局授权。
|
|
@@ -112,7 +133,13 @@
|
|
|
112
133
|
return isRequestCurrent() ? decision : { can: false };
|
|
113
134
|
},
|
|
114
135
|
});
|
|
115
|
-
} catch (failure) {
|
|
136
|
+
} catch (failure) {
|
|
137
|
+
reportDiagnostic?.({
|
|
138
|
+
code: 'provider_failed', sourceId: snapshot.source.id,
|
|
139
|
+
message: failure instanceof Error ? failure.message : 'Data provider request failed',
|
|
140
|
+
});
|
|
141
|
+
return providerError(snapshot.source.id);
|
|
142
|
+
}
|
|
116
143
|
},
|
|
117
144
|
};
|
|
118
145
|
});
|
|
@@ -155,7 +182,7 @@
|
|
|
155
182
|
<div class="surface-widget surface-span-{widget.placement?.columnSpan ?? 12} {semantic ? appearanceClasses(widget.props) : ''}" data-testid="surface-widget-{widget.id}">
|
|
156
183
|
{#if WidgetComponent}
|
|
157
184
|
<WidgetComponent widgetId={widget.id} props={semantic ? withoutSurfaceAppearance(widget.props) : widget.props}
|
|
158
|
-
data={resolveSurfaceSourceData(widget, widget.binding ? sourceStates.get(widget.binding.sourceId) : undefined)}
|
|
185
|
+
data={presentSurfaceData(resolveSurfaceSourceData(widget, widget.binding ? sourceStates.get(widget.binding.sourceId) : undefined))}
|
|
159
186
|
locale={activeLocale} messages={activeMessages} />
|
|
160
187
|
{/if}
|
|
161
188
|
</div>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { SVAR_SURFACE_CONTEXT, type SvarSurfaceContext } from '../svar-context.js';
|
|
6
6
|
import { decodeSvarGridProps } from '../svar-schema.js';
|
|
7
7
|
import { asRecordArray } from '../widget-data.js';
|
|
8
|
-
import { resolveSurfaceMessages } from '../localization.js';
|
|
8
|
+
import { resolveSurfaceMessages, surfaceErrorMessage } from '../localization.js';
|
|
9
9
|
|
|
10
10
|
let { widgetId, props, data, locale = 'en-US', messages }: SurfaceWidgetRendererProps = $props();
|
|
11
11
|
const host = getContext<SvarSurfaceContext | undefined>(SVAR_SURFACE_CONTEXT);
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
<section aria-labelledby="svar-surface-{widgetId}" class="surface-data-grid">
|
|
25
25
|
<h3 id="svar-surface-{widgetId}">{config.title}</h3>
|
|
26
26
|
{#if !Grid || !Theme}
|
|
27
|
-
<p role="alert">{
|
|
27
|
+
<p role="alert">{labels.tableEngineUnavailable}</p>
|
|
28
28
|
{:else if data.status === 'error'}
|
|
29
|
-
<p role="alert">{data.error.
|
|
29
|
+
<p role="alert">{surfaceErrorMessage(data.error, labels, labels.tableUnavailable, labels.tableInvalidData)}</p>
|
|
30
30
|
{:else if data.status === 'unbound' || (records && !records.ok)}
|
|
31
31
|
<p role="alert">{labels.tableInvalidData}</p>
|
|
32
32
|
{:else}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,9 @@ export type { JsonObject, JsonPrimitive, JsonValue, ResourceListDataSource, Reso
|
|
|
3
3
|
export { validateSurfaceSpec } from './validation.js';
|
|
4
4
|
export type { SurfaceMessages } from './localization.js';
|
|
5
5
|
export { SURFACE_AGENT_SCHEMA_VERSION, buildSurfaceAgentPrompt, buildSurfaceAgentMessages, parseSurfaceAgentProposal, parseSurfaceAgentResponse, createSurfaceAgentStream, } from './agent.js';
|
|
6
|
+
export { SURFACE_AGENT_FENCE_TAG, splitSurfaceAgentText, } from './agent-text.js';
|
|
7
|
+
export type { SurfaceAgentTextSegment, SurfaceAgentTextOptions, } from './agent-text.js';
|
|
8
|
+
export type { SurfaceAgentMessageOptions, SurfaceAgentPresentation, } from './agent.js';
|
|
6
9
|
export type { SurfaceAgentProposal, SurfaceAgentProposalV2, SurfaceAgentCannotFulfill, SurfaceAgentResponse, SurfaceAgentResponseResult, SurfaceAgentMessage, SurfaceAgentStream, SurfaceAgentValidationResult, } from './agent.js';
|
|
7
10
|
export { SURFACE_AGENT_LIMITS, SURFACE_AGENT_RESPONSE_SCHEMA_VERSION, SURFACE_CATALOG_SCHEMA_VERSION, createSurfaceCatalogManifest, createSurfaceAgentResponseSchema, selectSurfaceCatalog, } from './agent-contract.js';
|
|
8
11
|
export type { SurfaceCatalogManifest } from './agent-contract.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { SURFACE_LIMITS, SURFACE_SCHEMA_VERSION } from './types.js';
|
|
2
2
|
export { validateSurfaceSpec } from './validation.js';
|
|
3
3
|
export { SURFACE_AGENT_SCHEMA_VERSION, buildSurfaceAgentPrompt, buildSurfaceAgentMessages, parseSurfaceAgentProposal, parseSurfaceAgentResponse, createSurfaceAgentStream, } from './agent.js';
|
|
4
|
+
export { SURFACE_AGENT_FENCE_TAG, splitSurfaceAgentText, } from './agent-text.js';
|
|
4
5
|
export { SURFACE_AGENT_LIMITS, SURFACE_AGENT_RESPONSE_SCHEMA_VERSION, SURFACE_CATALOG_SCHEMA_VERSION, createSurfaceCatalogManifest, createSurfaceAgentResponseSchema, selectSurfaceCatalog, } from './agent-contract.js';
|
|
5
6
|
export { SURFACE_EDIT_SCHEMA_VERSION, SURFACE_EDIT_LIMITS, createSurfaceRevision, applySurfaceEditProposal, createSurfaceEditSchema, buildSurfaceEditMessages, } from './edits.js';
|
|
6
7
|
// 只导出 JSON schema,不将可选 SVAR 渲染器加载到协议入口。
|
package/dist/json-render.js
CHANGED
|
@@ -71,7 +71,7 @@ export function jsonRenderSpecToSurfaceSpec(input, catalog, policy) {
|
|
|
71
71
|
if (!element)
|
|
72
72
|
return { ok: false, issues: [issue(jsonPointer(['elements', childId]), 'Missing child element')] };
|
|
73
73
|
const widget = readWidgetElement(element, childId);
|
|
74
|
-
if (
|
|
74
|
+
if (typeof widget.error === 'string')
|
|
75
75
|
return { ok: false, issues: [issue(jsonPointer(['elements', childId]), widget.error)] };
|
|
76
76
|
widgets.push(widget);
|
|
77
77
|
}
|
package/dist/localization.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import type { SurfaceDataError } from './types.js';
|
|
1
2
|
export interface SurfaceMessages {
|
|
2
3
|
readonly renderErrorTitle: string;
|
|
3
4
|
readonly providerUnavailable: string;
|
|
5
|
+
readonly accessDenied?: string;
|
|
6
|
+
readonly invalidData?: string;
|
|
4
7
|
readonly metricNoData: string;
|
|
5
8
|
readonly metricInvalidData: string;
|
|
6
9
|
readonly chartLoading: string;
|
|
@@ -11,6 +14,7 @@ export interface SurfaceMessages {
|
|
|
11
14
|
readonly tableNoRecords: string;
|
|
12
15
|
readonly tableInvalidData: string;
|
|
13
16
|
readonly tableUnavailable: string;
|
|
17
|
+
readonly tableEngineUnavailable: string;
|
|
14
18
|
readonly detailLoading?: string;
|
|
15
19
|
readonly detailEmpty?: string;
|
|
16
20
|
readonly detailUnavailable?: string;
|
|
@@ -26,3 +30,4 @@ export interface SurfaceMessages {
|
|
|
26
30
|
}
|
|
27
31
|
export declare function resolveSurfaceMessages(locale: string, overrides?: Partial<SurfaceMessages>): Required<SurfaceMessages>;
|
|
28
32
|
export declare function formatSurfaceMessage(template: string, params: Readonly<Record<string, string | number>>): string;
|
|
33
|
+
export declare function surfaceErrorMessage(error: Pick<SurfaceDataError, 'code'>, messages: Required<SurfaceMessages>, unavailable: string, invalid: string): string;
|
package/dist/localization.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const englishMessages = {
|
|
2
2
|
renderErrorTitle: 'Surface could not be rendered',
|
|
3
3
|
providerUnavailable: 'Data provider is unavailable',
|
|
4
|
+
accessDenied: 'Access denied',
|
|
5
|
+
invalidData: 'Surface data is invalid',
|
|
4
6
|
metricNoData: 'No data',
|
|
5
7
|
metricInvalidData: 'Metric data is invalid',
|
|
6
8
|
chartLoading: 'Loading {title}',
|
|
@@ -11,6 +13,7 @@ const englishMessages = {
|
|
|
11
13
|
tableNoRecords: 'No records',
|
|
12
14
|
tableInvalidData: 'Table data is invalid',
|
|
13
15
|
tableUnavailable: 'Table data is unavailable',
|
|
16
|
+
tableEngineUnavailable: 'The host has not configured the SVAR engine',
|
|
14
17
|
detailLoading: 'Loading record',
|
|
15
18
|
detailEmpty: 'No record',
|
|
16
19
|
detailUnavailable: 'Record is unavailable',
|
|
@@ -27,6 +30,8 @@ const englishMessages = {
|
|
|
27
30
|
const simplifiedChineseMessages = {
|
|
28
31
|
renderErrorTitle: '无法渲染 Surface',
|
|
29
32
|
providerUnavailable: '数据提供器不可用',
|
|
33
|
+
accessDenied: '无权访问',
|
|
34
|
+
invalidData: 'Surface 数据无效',
|
|
30
35
|
metricNoData: '暂无数据',
|
|
31
36
|
metricInvalidData: '指标数据无效',
|
|
32
37
|
chartLoading: '正在加载{title}',
|
|
@@ -37,6 +42,7 @@ const simplifiedChineseMessages = {
|
|
|
37
42
|
tableNoRecords: '暂无记录',
|
|
38
43
|
tableInvalidData: '表格数据无效',
|
|
39
44
|
tableUnavailable: '表格数据不可用',
|
|
45
|
+
tableEngineUnavailable: '宿主尚未配置 SVAR 引擎',
|
|
40
46
|
detailLoading: '正在加载记录',
|
|
41
47
|
detailEmpty: '暂无记录',
|
|
42
48
|
detailUnavailable: '记录不可用',
|
|
@@ -63,3 +69,10 @@ export function formatSurfaceMessage(template, params) {
|
|
|
63
69
|
}
|
|
64
70
|
return formattedMessage;
|
|
65
71
|
}
|
|
72
|
+
export function surfaceErrorMessage(error, messages, unavailable, invalid) {
|
|
73
|
+
if (error.code === 'access_denied')
|
|
74
|
+
return messages.accessDenied;
|
|
75
|
+
if (error.code === 'provider_result_not_json' || error.code === 'binding_pointer_not_found')
|
|
76
|
+
return invalid;
|
|
77
|
+
return unavailable;
|
|
78
|
+
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SurfaceDataProvider, SurfaceDataSource, SurfaceResourcePolicy, SurfaceWidgetDataState } from './types.js';
|
|
1
|
+
import type { SurfaceDataError, SurfaceDataProvider, SurfaceDataSource, SurfaceResourcePolicy, SurfaceWidgetDataState } from './types.js';
|
|
2
2
|
export type SurfaceReadAction = 'list' | 'show';
|
|
3
3
|
export interface SurfaceAccessDecision {
|
|
4
4
|
readonly can: boolean;
|
|
@@ -10,6 +10,8 @@ export interface LoadSurfaceSourceRequest {
|
|
|
10
10
|
readonly resourcePolicy: SurfaceResourcePolicy;
|
|
11
11
|
readonly provider: SurfaceDataProvider;
|
|
12
12
|
readonly authorize: SurfaceAuthorizer;
|
|
13
|
+
/** Internal host hook for diagnostics; never used as user-visible state. */
|
|
14
|
+
readonly reportDiagnostic?: (diagnostic: SurfaceDataError) => void;
|
|
13
15
|
}
|
|
14
16
|
export type SurfaceLoadedSourceState = Extract<SurfaceWidgetDataState, {
|
|
15
17
|
status: 'ready' | 'error';
|
package/dist/runtime.js
CHANGED
|
@@ -46,6 +46,10 @@ function projectRecords(records, readableFields) {
|
|
|
46
46
|
function sourceError(sourceId, code, message) {
|
|
47
47
|
return { status: 'error', sourceId, error: { code, sourceId, message } };
|
|
48
48
|
}
|
|
49
|
+
function reportedError(request, code, message, diagnosticMessage) {
|
|
50
|
+
request.reportDiagnostic?.({ code, sourceId: request.source.id, message: diagnosticMessage });
|
|
51
|
+
return sourceError(request.source.id, code, message);
|
|
52
|
+
}
|
|
49
53
|
function listParams(source) {
|
|
50
54
|
const sorters = source.sorters?.map((sorter) => ({ ...sorter })) ?? [];
|
|
51
55
|
const filters = source.filters?.map((filter) => {
|
|
@@ -112,10 +116,11 @@ export async function loadSurfaceSource(request) {
|
|
|
112
116
|
}
|
|
113
117
|
catch (accessFailure) {
|
|
114
118
|
const message = accessFailure instanceof Error ? accessFailure.message : 'Access check failed';
|
|
115
|
-
return
|
|
119
|
+
return reportedError(request, 'access_check_failed', 'Access check failed', message);
|
|
120
|
+
}
|
|
121
|
+
if (!accessDecision.can) {
|
|
122
|
+
return reportedError(request, 'access_denied', 'Access denied', accessDecision.reason ?? 'Access denied');
|
|
116
123
|
}
|
|
117
|
-
if (!accessDecision.can)
|
|
118
|
-
return sourceError(source.id, 'access_denied', accessDecision.reason ?? 'Access denied');
|
|
119
124
|
try {
|
|
120
125
|
return source.type === 'resource-list'
|
|
121
126
|
? await loadListSource(source, resourcePolicy, provider)
|
|
@@ -123,6 +128,6 @@ export async function loadSurfaceSource(request) {
|
|
|
123
128
|
}
|
|
124
129
|
catch (providerFailure) {
|
|
125
130
|
const message = providerFailure instanceof Error ? providerFailure.message : 'Data provider request failed';
|
|
126
|
-
return
|
|
131
|
+
return reportedError(request, 'provider_failed', 'Data provider request failed', message);
|
|
127
132
|
}
|
|
128
133
|
}
|
package/dist/source-cache.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type { SurfaceDataSource, SurfaceResourcePolicy, SurfaceSourceDataState } from './types.js';
|
|
1
|
+
import type { SurfaceDataSource, SurfaceDataError, SurfaceResourcePolicy, SurfaceSourceDataState } from './types.js';
|
|
2
|
+
type SurfaceDataDiagnostic = Extract<SurfaceSourceDataState, {
|
|
3
|
+
status: 'error';
|
|
4
|
+
}>['error'];
|
|
2
5
|
/** Only query/projection inputs belong in this key, never widget or layout data. */
|
|
3
6
|
export declare function snapshotSurfaceSource(source: SurfaceDataSource, policy: SurfaceResourcePolicy): {
|
|
4
7
|
source: SurfaceDataSource;
|
|
@@ -9,7 +12,7 @@ export interface SurfaceSourceRequest {
|
|
|
9
12
|
readonly id: string;
|
|
10
13
|
/** Values compare by Object.is; provider and permission identities are not serialized. */
|
|
11
14
|
readonly identity: readonly unknown[];
|
|
12
|
-
readonly load: (isCurrent: () => boolean) => Promise<SurfaceSourceDataState>;
|
|
15
|
+
readonly load: (isCurrent: () => boolean, reportDiagnostic?: (diagnostic: SurfaceDataDiagnostic) => void) => Promise<SurfaceSourceDataState>;
|
|
13
16
|
/** Also guards prop changes that happen before Svelte's next effect flush. */
|
|
14
17
|
readonly isCurrent: () => boolean;
|
|
15
18
|
}
|
|
@@ -19,7 +22,7 @@ interface SourceCacheSink {
|
|
|
19
22
|
readonly remove: (sourceId: string) => void;
|
|
20
23
|
readonly onError: (state: Extract<SurfaceSourceDataState, {
|
|
21
24
|
status: 'error';
|
|
22
|
-
}
|
|
25
|
+
}>, diagnostic?: SurfaceDataError) => void;
|
|
23
26
|
}
|
|
24
27
|
/** Per-renderer reconciliation, not a cross-user or cross-component data cache. */
|
|
25
28
|
export declare function createSurfaceSourceCache(sink: SourceCacheSink): {
|
package/dist/source-cache.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
function stableSourceState(state) {
|
|
2
|
+
if (state.status !== 'error')
|
|
3
|
+
return state;
|
|
4
|
+
const message = state.error.code === 'access_denied'
|
|
5
|
+
? 'Access denied'
|
|
6
|
+
: state.error.code === 'access_check_failed'
|
|
7
|
+
? 'Access check failed'
|
|
8
|
+
: state.error.code === 'provider_failed'
|
|
9
|
+
? 'Data provider request failed'
|
|
10
|
+
: state.error.message;
|
|
11
|
+
return message === state.error.message
|
|
12
|
+
? state
|
|
13
|
+
: { ...state, error: { ...state.error, message } };
|
|
14
|
+
}
|
|
1
15
|
/** Only query/projection inputs belong in this key, never widget or layout data. */
|
|
2
16
|
export function snapshotSurfaceSource(source, policy) {
|
|
3
17
|
const snapshot = source.type === 'resource-one'
|
|
@@ -79,15 +93,23 @@ export function createSurfaceSourceCache(sink) {
|
|
|
79
93
|
return;
|
|
80
94
|
}
|
|
81
95
|
let state;
|
|
96
|
+
let diagnostic;
|
|
97
|
+
const reportDiagnostic = (next) => {
|
|
98
|
+
diagnostic ??= next;
|
|
99
|
+
};
|
|
82
100
|
try {
|
|
83
|
-
state = await request.load(current);
|
|
101
|
+
state = await request.load(current, reportDiagnostic);
|
|
84
102
|
}
|
|
85
103
|
catch (failure) {
|
|
104
|
+
const message = failure instanceof Error ? failure.message : 'Data provider request failed';
|
|
105
|
+
diagnostic = {
|
|
106
|
+
code: 'provider_failed', sourceId: request.id, message,
|
|
107
|
+
};
|
|
86
108
|
state = {
|
|
87
109
|
status: 'error', sourceId: request.id,
|
|
88
110
|
error: {
|
|
89
111
|
code: 'provider_failed', sourceId: request.id,
|
|
90
|
-
message:
|
|
112
|
+
message: 'Data provider request failed',
|
|
91
113
|
},
|
|
92
114
|
};
|
|
93
115
|
}
|
|
@@ -95,9 +117,10 @@ export function createSurfaceSourceCache(sink) {
|
|
|
95
117
|
retire();
|
|
96
118
|
return;
|
|
97
119
|
}
|
|
120
|
+
state = stableSourceState(state);
|
|
98
121
|
sink.set(request.id, state);
|
|
99
122
|
if (state.status === 'error')
|
|
100
|
-
sink.onError(state);
|
|
123
|
+
sink.onError(state, diagnostic);
|
|
101
124
|
})();
|
|
102
125
|
return entry.pending;
|
|
103
126
|
}
|
|
@@ -2,7 +2,7 @@ import { Type } from 'typebox';
|
|
|
2
2
|
import type { SurfaceCatalog, SurfaceWidgetDefinition, JsonObject } from '../types.js';
|
|
3
3
|
import type { SurfaceActionDescriptor } from './types.js';
|
|
4
4
|
export declare const surfaceAppearanceSchema: Type.TObject<{
|
|
5
|
-
tone: Type.TOptional<Type.TUnion<Type.TLiteral<"info" | "warning" | "success" | "
|
|
5
|
+
tone: Type.TOptional<Type.TUnion<Type.TLiteral<"info" | "warning" | "success" | "danger" | "neutral">[]>>;
|
|
6
6
|
}>;
|
|
7
7
|
export declare function withoutSurfaceAppearance(props: JsonObject): JsonObject;
|
|
8
8
|
/** Adds frame-level semantic appearance to every registered widget without
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/surface",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Validated declarative surfaces for svadmin dashboards",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -74,8 +74,8 @@
|
|
|
74
74
|
}
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
|
-
"@svadmin/core": ">=0.53.0 <0.
|
|
78
|
-
"@svadmin/ui": ">=0.73.0 <0.
|
|
77
|
+
"@svadmin/core": ">=0.53.0 <0.67.0",
|
|
78
|
+
"@svadmin/ui": ">=0.73.0 <0.93.0",
|
|
79
79
|
"svelte": "^5.56.10"
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"@testing-library/dom": "^10.4.1",
|
|
87
87
|
"@testing-library/svelte": "^5.4.2",
|
|
88
88
|
"@testing-library/user-event": "^14.6.6",
|
|
89
|
-
"happy-dom": "^
|
|
89
|
+
"happy-dom": "^21.0.4",
|
|
90
90
|
"postcss": "^8.5.26",
|
|
91
91
|
"vitest": "^4.1.11"
|
|
92
92
|
},
|