@fiodos/web-core 0.1.12 → 0.1.14
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/dist/cjs/adapters/webCredentialAutofillAdapter.d.ts +16 -0
- package/dist/cjs/adapters/webCredentialAutofillAdapter.js +47 -0
- package/dist/cjs/api/backendClient.js +14 -0
- package/dist/cjs/config/types.d.ts +15 -2
- package/dist/cjs/controller/AgentController.d.ts +14 -0
- package/dist/cjs/controller/AgentController.js +63 -0
- package/dist/cjs/dropin/createFiodosAgent.d.ts +27 -1
- package/dist/cjs/dropin/createFiodosAgent.js +32 -1
- package/dist/cjs/embed/dynamics.d.ts +61 -0
- package/dist/cjs/embed/dynamics.js +173 -0
- package/dist/cjs/embed/global.d.ts +45 -0
- package/dist/cjs/embed/global.js +48 -0
- package/dist/cjs/index.d.ts +3 -0
- package/dist/cjs/index.js +8 -1
- package/dist/cjs/orb/mountOrb.js +111 -18
- package/dist/cjs/orb/orbView.js +40 -7
- package/dist/cjs/orb/publishedConfig.d.ts +4 -2
- package/dist/cjs/orb/publishedConfig.js +56 -7
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/embed/fiodos-embed.js +66 -0
- package/dist/esm/adapters/webCredentialAutofillAdapter.d.ts +16 -0
- package/dist/esm/adapters/webCredentialAutofillAdapter.js +44 -0
- package/dist/esm/api/backendClient.js +14 -0
- package/dist/esm/config/types.d.ts +15 -2
- package/dist/esm/controller/AgentController.d.ts +14 -0
- package/dist/esm/controller/AgentController.js +63 -0
- package/dist/esm/dropin/createFiodosAgent.d.ts +27 -1
- package/dist/esm/dropin/createFiodosAgent.js +32 -1
- package/dist/esm/embed/dynamics.d.ts +61 -0
- package/dist/esm/embed/dynamics.js +168 -0
- package/dist/esm/embed/global.d.ts +45 -0
- package/dist/esm/embed/global.js +46 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/orb/mountOrb.js +112 -19
- package/dist/esm/orb/orbView.js +41 -8
- package/dist/esm/orb/publishedConfig.d.ts +4 -2
- package/dist/esm/orb/publishedConfig.js +56 -7
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +5 -3
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web implementation of @fiodos/core's CredentialAutofillAdapter, on top of
|
|
3
|
+
* the Credential Management API (`navigator.credentials.get({ password: true,
|
|
4
|
+
* mediation: 'required' })`). The BROWSER owns the account-picker / user
|
|
5
|
+
* verification UI; this module only receives the outcome:
|
|
6
|
+
*
|
|
7
|
+
* - A `PasswordCredential` → 'success' with the saved username + password.
|
|
8
|
+
* - `null` (user dismissed the picker, or no saved credential and the browser
|
|
9
|
+
* chose not to prompt) → 'dismissed' (graceful fallback, never an error).
|
|
10
|
+
* - No API / SecurityError (insecure context, iframe policy) → 'unavailable'.
|
|
11
|
+
*
|
|
12
|
+
* The credentials never touch storage, logs or the network from here: they go
|
|
13
|
+
* straight into the app's own login handler parameters, one call, then gone.
|
|
14
|
+
*/
|
|
15
|
+
import type { CredentialAutofillAdapter } from '@fiodos/core';
|
|
16
|
+
export declare function createWebCredentialAutofillAdapter(): CredentialAutofillAdapter;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createWebCredentialAutofillAdapter = createWebCredentialAutofillAdapter;
|
|
4
|
+
function credentialsContainer() {
|
|
5
|
+
if (typeof navigator === 'undefined')
|
|
6
|
+
return null;
|
|
7
|
+
const nav = navigator;
|
|
8
|
+
return nav.credentials && typeof nav.credentials.get === 'function' ? nav.credentials : null;
|
|
9
|
+
}
|
|
10
|
+
function passwordCredentialSupported() {
|
|
11
|
+
if (typeof window === 'undefined')
|
|
12
|
+
return false;
|
|
13
|
+
const w = window;
|
|
14
|
+
// The Credential Management password store needs both the constructor and a
|
|
15
|
+
// secure context; feature-detect instead of sniffing the browser.
|
|
16
|
+
return typeof w.PasswordCredential === 'function' && w.isSecureContext !== false;
|
|
17
|
+
}
|
|
18
|
+
function createWebCredentialAutofillAdapter() {
|
|
19
|
+
return {
|
|
20
|
+
isSupported() {
|
|
21
|
+
return credentialsContainer() !== null && passwordCredentialSupported();
|
|
22
|
+
},
|
|
23
|
+
async requestCredentials() {
|
|
24
|
+
const container = credentialsContainer();
|
|
25
|
+
if (!container || !passwordCredentialSupported())
|
|
26
|
+
return { status: 'unavailable' };
|
|
27
|
+
let raw;
|
|
28
|
+
try {
|
|
29
|
+
// mediation 'required': ALWAYS show the browser's account picker — the
|
|
30
|
+
// user explicitly asked to sign in, so a visible, cancellable choice is
|
|
31
|
+
// the honest UX (and the only one that lets them pick among accounts).
|
|
32
|
+
raw = await container.get({ password: true, mediation: 'required' });
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// SecurityError (permissions policy / insecure frame), NotSupportedError…
|
|
36
|
+
return { status: 'unavailable' };
|
|
37
|
+
}
|
|
38
|
+
const cred = raw;
|
|
39
|
+
if (!cred)
|
|
40
|
+
return { status: 'dismissed' };
|
|
41
|
+
if (cred.type === 'password' && cred.id && typeof cred.password === 'string' && cred.password) {
|
|
42
|
+
return { status: 'success', username: cred.id, password: cred.password };
|
|
43
|
+
}
|
|
44
|
+
return { status: 'unavailable' };
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -119,6 +119,20 @@ function createFiodosBackendClient(options) {
|
|
|
119
119
|
if (request.sessionId?.trim()) {
|
|
120
120
|
body.session_id = request.sessionId.trim();
|
|
121
121
|
}
|
|
122
|
+
// Device capability flags (retrocompatible: old backends ignore them).
|
|
123
|
+
if (request.platformCapabilities?.credentialAutofill) {
|
|
124
|
+
body.platform_capabilities = { credential_autofill: true };
|
|
125
|
+
}
|
|
126
|
+
// Live session verdict for audience filtering (retrocompatible: absent
|
|
127
|
+
// when the host wired no session source; old backends ignore it).
|
|
128
|
+
if (request.sessionContext) {
|
|
129
|
+
body.session_context = {
|
|
130
|
+
authenticated: request.sessionContext.authenticated,
|
|
131
|
+
...(request.sessionContext.capabilities
|
|
132
|
+
? { capabilities: request.sessionContext.capabilities }
|
|
133
|
+
: {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
122
136
|
// Autonomous-chain continuation metadata (backward compatible: absent on a
|
|
123
137
|
// normal turn; an old backend simply ignores these fields).
|
|
124
138
|
if (request.isContinuation) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Mirrors @fiodos/react's FiodosWebAgentConfig (same adapters, same lifecycle).
|
|
4
4
|
* Shared by every framework binding through the AgentController.
|
|
5
5
|
*/
|
|
6
|
-
import type { ActionRegistries, AgentBackendClient, AgentMessages, AppManifest, ConfirmationLexicon, MessageCatalog, NavigationAdapter, Sanitizer, StorageAdapter, TelemetryAdapter, VoiceAdapter } from '@fiodos/core';
|
|
6
|
+
import type { ActionRegistries, AgentBackendClient, AgentMessages, AppManifest, ConfirmationLexicon, CredentialAutofillAdapter, MessageCatalog, NavigationAdapter, Sanitizer, StorageAdapter, TelemetryAdapter, VoiceAdapter } from '@fiodos/core';
|
|
7
7
|
import type { ResolveUiMessagesOptions, WebUiMessages } from '../ui/messages';
|
|
8
8
|
export type AgentPhase = 'idle' | 'listening' | 'thinking' | 'speaking' | 'confirming';
|
|
9
9
|
export interface AgentTimings {
|
|
@@ -55,7 +55,20 @@ export interface FiodosWebAgentConfig {
|
|
|
55
55
|
storage: StorageAdapter;
|
|
56
56
|
telemetry?: TelemetryAdapter;
|
|
57
57
|
/** Host auth check, required by manifest actions declaring requiresAuth. */
|
|
58
|
-
isAuthenticated?: () => boolean;
|
|
58
|
+
isAuthenticated?: () => boolean | null | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Fine-grained session capability flags from the wired session bridge
|
|
61
|
+
* (audience map: e.g. { has_payment_method: true }). Sent with each turn so
|
|
62
|
+
* the backend shows this user only THEIR face of the orb. Boolean flags
|
|
63
|
+
* only — never user data. null/undefined = unknown (nothing is filtered).
|
|
64
|
+
*/
|
|
65
|
+
getSessionCapabilities?: () => Record<string, boolean> | null | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Platform saved-credential service powering the built-in SMART SIGN-IN
|
|
68
|
+
* flow (browser password manager). Defaults to the web Credential
|
|
69
|
+
* Management adapter; pass `null` to disable the feature entirely.
|
|
70
|
+
*/
|
|
71
|
+
credentialAutofill?: CredentialAutofillAdapter | null;
|
|
59
72
|
/**
|
|
60
73
|
* Host end-user identity. Conversation memory is namespaced per identity
|
|
61
74
|
* (hashed LOCALLY, never sent by this module): logout/account switches swap
|
|
@@ -74,6 +74,7 @@ export declare class AgentController {
|
|
|
74
74
|
private readonly telemetry;
|
|
75
75
|
private readonly consent;
|
|
76
76
|
private readonly executor;
|
|
77
|
+
private readonly credentialAutofill;
|
|
77
78
|
private readonly timings;
|
|
78
79
|
private readonly chaining;
|
|
79
80
|
private readonly ttsLocale;
|
|
@@ -191,6 +192,19 @@ export declare class AgentController {
|
|
|
191
192
|
confirmPendingAction(): Promise<void>;
|
|
192
193
|
cancelPendingAction(): void;
|
|
193
194
|
private handleConfirmationTranscript;
|
|
195
|
+
/**
|
|
196
|
+
* Device capability flags advertised on each turn (never user data). Fresh
|
|
197
|
+
* per turn: cheap, and honest if the environment changes mid-session.
|
|
198
|
+
*/
|
|
199
|
+
private platformCapabilities;
|
|
200
|
+
/**
|
|
201
|
+
* Live session verdict advertised on each turn (audience map — never user
|
|
202
|
+
* data): whether the user is signed in plus the app's boolean capability
|
|
203
|
+
* flags. Fresh per turn so login/logout mid-session is honest. Undefined
|
|
204
|
+
* when the host wired neither source (older installs — backend filters
|
|
205
|
+
* nothing, exactly today's behavior).
|
|
206
|
+
*/
|
|
207
|
+
private sessionContext;
|
|
194
208
|
private routeLabel;
|
|
195
209
|
private actionLabel;
|
|
196
210
|
/**
|
|
@@ -19,6 +19,7 @@ exports.AgentController = void 0;
|
|
|
19
19
|
* - The agent only ever executes actions declared in the manifest.
|
|
20
20
|
*/
|
|
21
21
|
const core_1 = require("@fiodos/core");
|
|
22
|
+
const webCredentialAutofillAdapter_1 = require("../adapters/webCredentialAutofillAdapter");
|
|
22
23
|
const errors_1 = require("../api/errors");
|
|
23
24
|
const formatTurnError_1 = require("../api/formatTurnError");
|
|
24
25
|
const backendClient_1 = require("../api/backendClient");
|
|
@@ -94,12 +95,19 @@ class AgentController {
|
|
|
94
95
|
appId: config.manifest.appId,
|
|
95
96
|
version: config.consentVersion,
|
|
96
97
|
});
|
|
98
|
+
// SMART SIGN-IN ships by default on web (browser password manager);
|
|
99
|
+
// hosts can override the adapter or pass null to opt out entirely.
|
|
100
|
+
this.credentialAutofill =
|
|
101
|
+
config.credentialAutofill === undefined
|
|
102
|
+
? (0, webCredentialAutofillAdapter_1.createWebCredentialAutofillAdapter)()
|
|
103
|
+
: config.credentialAutofill;
|
|
97
104
|
this.executor = (0, core_1.createActionExecutor)({
|
|
98
105
|
manifest: config.manifest,
|
|
99
106
|
registries: config.registries,
|
|
100
107
|
navigation: config.navigation,
|
|
101
108
|
messages: this.messages,
|
|
102
109
|
isAuthenticated: config.isAuthenticated,
|
|
110
|
+
credentialAutofill: this.credentialAutofill,
|
|
103
111
|
});
|
|
104
112
|
this.timings = { ...types_1.DEFAULT_AGENT_TIMINGS, ...config.timings };
|
|
105
113
|
// Chaining OFF by default; clamp to the same safe bounds as react/react-native.
|
|
@@ -584,6 +592,57 @@ class AgentController {
|
|
|
584
592
|
this.beginConfirmationVoiceWindow();
|
|
585
593
|
}
|
|
586
594
|
}
|
|
595
|
+
/**
|
|
596
|
+
* Device capability flags advertised on each turn (never user data). Fresh
|
|
597
|
+
* per turn: cheap, and honest if the environment changes mid-session.
|
|
598
|
+
*/
|
|
599
|
+
platformCapabilities() {
|
|
600
|
+
let autofill = false;
|
|
601
|
+
try {
|
|
602
|
+
autofill = this.credentialAutofill?.isSupported() === true;
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
autofill = false;
|
|
606
|
+
}
|
|
607
|
+
return autofill ? { credentialAutofill: true } : undefined;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Live session verdict advertised on each turn (audience map — never user
|
|
611
|
+
* data): whether the user is signed in plus the app's boolean capability
|
|
612
|
+
* flags. Fresh per turn so login/logout mid-session is honest. Undefined
|
|
613
|
+
* when the host wired neither source (older installs — backend filters
|
|
614
|
+
* nothing, exactly today's behavior).
|
|
615
|
+
*/
|
|
616
|
+
sessionContext() {
|
|
617
|
+
const { isAuthenticated, getSessionCapabilities } = this.config;
|
|
618
|
+
if (!isAuthenticated && !getSessionCapabilities)
|
|
619
|
+
return undefined;
|
|
620
|
+
let authenticated = null;
|
|
621
|
+
try {
|
|
622
|
+
const v = isAuthenticated?.();
|
|
623
|
+
authenticated = v == null ? null : Boolean(v);
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
authenticated = null;
|
|
627
|
+
}
|
|
628
|
+
let capabilities;
|
|
629
|
+
try {
|
|
630
|
+
const raw = getSessionCapabilities?.();
|
|
631
|
+
if (raw && typeof raw === 'object') {
|
|
632
|
+
const clean = {};
|
|
633
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
634
|
+
if (typeof v === 'boolean')
|
|
635
|
+
clean[k] = v;
|
|
636
|
+
}
|
|
637
|
+
if (Object.keys(clean).length > 0)
|
|
638
|
+
capabilities = clean;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
catch {
|
|
642
|
+
capabilities = undefined;
|
|
643
|
+
}
|
|
644
|
+
return { authenticated, ...(capabilities ? { capabilities } : {}) };
|
|
645
|
+
}
|
|
587
646
|
routeLabel(intent) {
|
|
588
647
|
return this.config.manifest.routes.find((r) => r.intent === intent)?.label;
|
|
589
648
|
}
|
|
@@ -769,6 +828,8 @@ class AgentController {
|
|
|
769
828
|
isContinuation: true,
|
|
770
829
|
chainStep: step,
|
|
771
830
|
lastStep,
|
|
831
|
+
platformCapabilities: this.platformCapabilities(),
|
|
832
|
+
sessionContext: this.sessionContext(),
|
|
772
833
|
...(0, backendClient_1.buildManifestPayload)(this.config.manifest),
|
|
773
834
|
}, { signal: controller.signal });
|
|
774
835
|
oc = await this.applyOutcome(turn, routeNow, snap?.context ?? {}, state.userMessage);
|
|
@@ -926,6 +987,8 @@ class AgentController {
|
|
|
926
987
|
conversationHistory: conv.history,
|
|
927
988
|
conversationSummary: conv.summary ?? undefined,
|
|
928
989
|
sessionId: this.sessionId,
|
|
990
|
+
platformCapabilities: this.platformCapabilities(),
|
|
991
|
+
sessionContext: this.sessionContext(),
|
|
929
992
|
...(0, backendClient_1.buildManifestPayload)(this.config.manifest),
|
|
930
993
|
}, { signal: controller.signal });
|
|
931
994
|
outcome = await this.applyOutcome(turn, currentRoute, snapshot?.context ?? {}, text);
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { type ActionRegistries, type AppManifest } from '@fiodos/core';
|
|
15
15
|
import { AgentController } from '../controller/AgentController';
|
|
16
|
+
import type { AgentScreenSnapshot } from '../context/screenContextStore';
|
|
16
17
|
import { type MountOrbOptions, type MountedOrb } from '../orb/mountOrb';
|
|
17
18
|
import type { FiodosWebAgentConfig } from '../config/types';
|
|
18
19
|
export interface CreateFiodosAgentOptions {
|
|
@@ -37,8 +38,22 @@ export interface CreateFiodosAgentOptions {
|
|
|
37
38
|
sttLocale?: string;
|
|
38
39
|
/** Host router navigate delegate. Defaults to location.assign. */
|
|
39
40
|
navigate?: (route: string) => void;
|
|
41
|
+
/**
|
|
42
|
+
* Current route/screen resolver for the agent's screen awareness. Defaults
|
|
43
|
+
* to location.pathname + search. Apps whose screens are CLIENT STATE (no URL
|
|
44
|
+
* change) pass the generated `fyodosGetCurrentRoute` here so the orb knows
|
|
45
|
+
* which screen the user is on (see screen-truth wiring).
|
|
46
|
+
*/
|
|
47
|
+
getCurrentRoute?: () => string | null;
|
|
40
48
|
/** Host auth check for actions requiring auth. */
|
|
41
|
-
isAuthenticated?: () => boolean;
|
|
49
|
+
isAuthenticated?: () => boolean | null | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Fine-grained session capability flags (audience map, e.g.
|
|
52
|
+
* { has_payment_method: true }) from the generated session wiring. Boolean
|
|
53
|
+
* flags only; sent with each turn so the orb shows this user only THEIR
|
|
54
|
+
* available actions.
|
|
55
|
+
*/
|
|
56
|
+
getSessionCapabilities?: () => Record<string, boolean> | null | undefined;
|
|
42
57
|
/** Voice id forwarded to the backend TTS. */
|
|
43
58
|
ttsVoice?: string | (() => string | undefined);
|
|
44
59
|
/** End-user id for rate limiting / telemetry correlation. */
|
|
@@ -64,6 +79,17 @@ export interface FiodosAgentHandle {
|
|
|
64
79
|
* Call it from the app's logout for a hard wipe; no-op until ready.
|
|
65
80
|
*/
|
|
66
81
|
resetConversation(): void;
|
|
82
|
+
/**
|
|
83
|
+
* Publishes WHAT THE USER IS SEEING so 'execute' actions can target it —
|
|
84
|
+
* the embed-host equivalent of the React `useAgentScreenContext` hook.
|
|
85
|
+
* Hosts without our SDK on screens (Dynamics web resources, WinCC CWCs)
|
|
86
|
+
* call this whenever their form/record/view changes. On the auto-fetch
|
|
87
|
+
* path, calls made before the controller is ready are kept and applied on
|
|
88
|
+
* ready, so hosts don't need to await `ready` themselves.
|
|
89
|
+
*/
|
|
90
|
+
setScreenContext(snapshot: AgentScreenSnapshot): void;
|
|
91
|
+
/** Clears a previously published snapshot (e.g. the record was closed). */
|
|
92
|
+
clearScreenContext(): void;
|
|
67
93
|
destroy(): void;
|
|
68
94
|
}
|
|
69
95
|
export declare function createFiodosAgent(options: CreateFiodosAgentOptions): FiodosAgentHandle;
|
|
@@ -68,13 +68,17 @@ function assemble(options, manifest, baseUrl) {
|
|
|
68
68
|
manifest,
|
|
69
69
|
locale,
|
|
70
70
|
sttLocale,
|
|
71
|
-
navigation: (0, webNavigationAdapter_1.createWebNavigationAdapter)({
|
|
71
|
+
navigation: (0, webNavigationAdapter_1.createWebNavigationAdapter)({
|
|
72
|
+
navigate: options.navigate ?? defaultNavigate,
|
|
73
|
+
getCurrentRoute: options.getCurrentRoute,
|
|
74
|
+
}),
|
|
72
75
|
registries: options.registries ?? { handlers: {}, idempotencyCheckers: {} },
|
|
73
76
|
backend,
|
|
74
77
|
voice: (0, webVoiceAdapter_1.createWebVoiceAdapter)(),
|
|
75
78
|
storage: (0, webStorageAdapter_1.createWebStorageAdapter)(),
|
|
76
79
|
telemetry,
|
|
77
80
|
isAuthenticated: options.isAuthenticated,
|
|
81
|
+
getSessionCapabilities: options.getSessionCapabilities,
|
|
78
82
|
getUserId: options.getUserId,
|
|
79
83
|
ttsVoice: options.ttsVoice,
|
|
80
84
|
...options.configOverrides,
|
|
@@ -86,6 +90,9 @@ function assemble(options, manifest, baseUrl) {
|
|
|
86
90
|
function createFiodosAgent(options) {
|
|
87
91
|
const baseUrl = (options.baseUrl ?? clientBootstrap_1.DEFAULT_WEB_API_URL).replace(/\/$/, '');
|
|
88
92
|
const shouldMount = options.mount !== false;
|
|
93
|
+
// One stable owner id per handle: the host is "one screen" from the store's
|
|
94
|
+
// point of view, so consecutive setScreenContext calls replace each other.
|
|
95
|
+
const contextOwner = Symbol('fiodos-embed-host');
|
|
89
96
|
// ── Manual path: manifest provided → fully synchronous (back-compat). ───────
|
|
90
97
|
if (options.manifest) {
|
|
91
98
|
const controller = assemble(options, options.manifest, baseUrl);
|
|
@@ -101,6 +108,12 @@ function createFiodosAgent(options) {
|
|
|
101
108
|
resetConversation() {
|
|
102
109
|
controller.resetConversation();
|
|
103
110
|
},
|
|
111
|
+
setScreenContext(snapshot) {
|
|
112
|
+
controller.screenStore.set(contextOwner, snapshot);
|
|
113
|
+
},
|
|
114
|
+
clearScreenContext() {
|
|
115
|
+
controller.screenStore.clear(contextOwner);
|
|
116
|
+
},
|
|
104
117
|
destroy() {
|
|
105
118
|
orb?.unmount();
|
|
106
119
|
controller.dispose();
|
|
@@ -109,6 +122,9 @@ function createFiodosAgent(options) {
|
|
|
109
122
|
return handle;
|
|
110
123
|
}
|
|
111
124
|
// ── Auto path (React parity): fetch manifest by apiKey, then assemble. ──────
|
|
125
|
+
// Context published before the controller resolves is buffered (last one
|
|
126
|
+
// wins) and applied on ready — hosts fire form events without awaiting us.
|
|
127
|
+
let pendingSnapshot;
|
|
112
128
|
const handle = {
|
|
113
129
|
controller: null,
|
|
114
130
|
orb: null,
|
|
@@ -116,6 +132,18 @@ function createFiodosAgent(options) {
|
|
|
116
132
|
resetConversation() {
|
|
117
133
|
handle.controller?.resetConversation();
|
|
118
134
|
},
|
|
135
|
+
setScreenContext(snapshot) {
|
|
136
|
+
if (handle.controller)
|
|
137
|
+
handle.controller.screenStore.set(contextOwner, snapshot);
|
|
138
|
+
else
|
|
139
|
+
pendingSnapshot = snapshot;
|
|
140
|
+
},
|
|
141
|
+
clearScreenContext() {
|
|
142
|
+
if (handle.controller)
|
|
143
|
+
handle.controller.screenStore.clear(contextOwner);
|
|
144
|
+
else
|
|
145
|
+
pendingSnapshot = null;
|
|
146
|
+
},
|
|
119
147
|
destroy() {
|
|
120
148
|
cancelled = true;
|
|
121
149
|
handle.orb?.unmount();
|
|
@@ -151,6 +179,9 @@ function createFiodosAgent(options) {
|
|
|
151
179
|
return null;
|
|
152
180
|
}
|
|
153
181
|
handle.controller = controller;
|
|
182
|
+
if (pendingSnapshot)
|
|
183
|
+
controller.screenStore.set(contextOwner, pendingSnapshot);
|
|
184
|
+
pendingSnapshot = undefined;
|
|
154
185
|
handle.orb = shouldMount ? (0, mountOrb_1.mountOrb)(controller, options.orb) : null;
|
|
155
186
|
if (shouldMount && options.apiKey) {
|
|
156
187
|
void (0, clientBootstrap_1.sendOrbSeen)({ baseUrl, apiKey: options.apiKey });
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { AgentScreenSnapshot } from '../context/screenContextStore';
|
|
2
|
+
interface XrmAttributeLike {
|
|
3
|
+
getName(): string;
|
|
4
|
+
getValue(): unknown;
|
|
5
|
+
/** Present on optionset/lookup attributes; preferred over raw values. */
|
|
6
|
+
getText?(): string | string[] | null;
|
|
7
|
+
}
|
|
8
|
+
interface XrmEntityLike {
|
|
9
|
+
getEntityName(): string;
|
|
10
|
+
/** Record GUID, usually brace-wrapped: "{1D2C...}". Empty on create forms. */
|
|
11
|
+
getId(): string;
|
|
12
|
+
getPrimaryAttributeValue?(): string | null;
|
|
13
|
+
attributes?: {
|
|
14
|
+
forEach(cb: (attribute: XrmAttributeLike) => void): void;
|
|
15
|
+
};
|
|
16
|
+
/** Model-driven apps expose save events here (Xrm ≥ 9.x). */
|
|
17
|
+
addOnPostSave?(handler: () => void): void;
|
|
18
|
+
removeOnPostSave?(handler: () => void): void;
|
|
19
|
+
}
|
|
20
|
+
export interface DynamicsFormContextLike {
|
|
21
|
+
data: {
|
|
22
|
+
entity: XrmEntityLike;
|
|
23
|
+
};
|
|
24
|
+
ui?: {
|
|
25
|
+
getFormType?(): number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** The subset of FiodosAgentHandle the bridge needs (keeps this module cheap to test). */
|
|
29
|
+
export interface ScreenContextSink {
|
|
30
|
+
setScreenContext(snapshot: AgentScreenSnapshot): void;
|
|
31
|
+
clearScreenContext(): void;
|
|
32
|
+
}
|
|
33
|
+
export interface DynamicsSnapshotOptions {
|
|
34
|
+
/**
|
|
35
|
+
* Logical names of the attributes to expose to the agent. When omitted, the
|
|
36
|
+
* first `maxAttributes` non-empty scalar attributes are included.
|
|
37
|
+
*/
|
|
38
|
+
attributes?: string[];
|
|
39
|
+
/** Cap when no allowlist is given. Default 12. */
|
|
40
|
+
maxAttributes?: number;
|
|
41
|
+
}
|
|
42
|
+
/** "{1D2C…}" → "1d2c…" (Web API URLs want bare lowercase GUIDs). */
|
|
43
|
+
export declare function normalizeDynamicsId(rawId: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* Builds the AgentScreenSnapshot for the current form state, or null when
|
|
46
|
+
* there is nothing referenceable yet (create form: no record id).
|
|
47
|
+
*/
|
|
48
|
+
export declare function dynamicsFormSnapshot(formContext: DynamicsFormContextLike, options?: DynamicsSnapshotOptions): AgentScreenSnapshot | null;
|
|
49
|
+
export interface DynamicsContextBridge {
|
|
50
|
+
/** Re-reads the form and republishes the snapshot (wire to OnChange/OnSave). */
|
|
51
|
+
refresh(): void;
|
|
52
|
+
/** Stops publishing and clears the agent's screen context. */
|
|
53
|
+
disconnect(): void;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Publishes the form snapshot now, republishes after every save (when the
|
|
57
|
+
* form exposes OnPostSave), and clears the context on disconnect. Call it from
|
|
58
|
+
* the form's OnLoad handler.
|
|
59
|
+
*/
|
|
60
|
+
export declare function connectDynamicsFormContext(agent: ScreenContextSink, formContext: DynamicsFormContextLike, options?: DynamicsSnapshotOptions): DynamicsContextBridge;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeDynamicsId = normalizeDynamicsId;
|
|
4
|
+
exports.dynamicsFormSnapshot = dynamicsFormSnapshot;
|
|
5
|
+
exports.connectDynamicsFormContext = connectDynamicsFormContext;
|
|
6
|
+
// ── Value normalization ──────────────────────────────────────────────────────
|
|
7
|
+
const MAX_VALUE_CHARS = 120;
|
|
8
|
+
function clip(text) {
|
|
9
|
+
const t = text.trim();
|
|
10
|
+
return t.length > MAX_VALUE_CHARS ? `${t.slice(0, MAX_VALUE_CHARS - 1)}…` : t;
|
|
11
|
+
}
|
|
12
|
+
/** Lookup values arrive as [{ id, name, entityType }] — show the names. */
|
|
13
|
+
function lookupNames(value) {
|
|
14
|
+
const names = value
|
|
15
|
+
.map((v) => v && typeof v === 'object' && typeof v.name === 'string'
|
|
16
|
+
? (v.name)
|
|
17
|
+
: null)
|
|
18
|
+
.filter((n) => !!n);
|
|
19
|
+
return names.length ? names.join(', ') : null;
|
|
20
|
+
}
|
|
21
|
+
function normalizeAttributeValue(attribute) {
|
|
22
|
+
// Optionset / lookup / boolean attributes carry a human label — prefer it.
|
|
23
|
+
try {
|
|
24
|
+
const text = attribute.getText?.();
|
|
25
|
+
if (typeof text === 'string' && text.trim())
|
|
26
|
+
return clip(text);
|
|
27
|
+
if (Array.isArray(text) && text.length)
|
|
28
|
+
return clip(text.join(', '));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
/* some attribute types throw on getText — fall through to raw value */
|
|
32
|
+
}
|
|
33
|
+
let value;
|
|
34
|
+
try {
|
|
35
|
+
value = attribute.getValue();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (value == null)
|
|
41
|
+
return null;
|
|
42
|
+
if (typeof value === 'string')
|
|
43
|
+
return value.trim() ? clip(value) : null;
|
|
44
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
45
|
+
return value;
|
|
46
|
+
if (value instanceof Date)
|
|
47
|
+
return value.toISOString();
|
|
48
|
+
if (Array.isArray(value)) {
|
|
49
|
+
const names = lookupNames(value);
|
|
50
|
+
return names ? clip(names) : null;
|
|
51
|
+
}
|
|
52
|
+
return null; // collections / unknown object shapes are skipped, never guessed
|
|
53
|
+
}
|
|
54
|
+
/** "{1D2C…}" → "1d2c…" (Web API URLs want bare lowercase GUIDs). */
|
|
55
|
+
function normalizeDynamicsId(rawId) {
|
|
56
|
+
return rawId.replace(/[{}]/g, '').trim().toLowerCase();
|
|
57
|
+
}
|
|
58
|
+
// ── Snapshot builder ─────────────────────────────────────────────────────────
|
|
59
|
+
/**
|
|
60
|
+
* Builds the AgentScreenSnapshot for the current form state, or null when
|
|
61
|
+
* there is nothing referenceable yet (create form: no record id).
|
|
62
|
+
*/
|
|
63
|
+
function dynamicsFormSnapshot(formContext, options = {}) {
|
|
64
|
+
const entity = formContext.data?.entity;
|
|
65
|
+
if (!entity)
|
|
66
|
+
return null;
|
|
67
|
+
const entityName = entity.getEntityName();
|
|
68
|
+
const recordId = normalizeDynamicsId(entity.getId() ?? '');
|
|
69
|
+
const allowlist = options.attributes?.length
|
|
70
|
+
? new Set(options.attributes.map((a) => a.trim().toLowerCase()).filter(Boolean))
|
|
71
|
+
: null;
|
|
72
|
+
const maxAttributes = options.maxAttributes ?? 12;
|
|
73
|
+
const fields = {};
|
|
74
|
+
let count = 0;
|
|
75
|
+
entity.attributes?.forEach((attribute) => {
|
|
76
|
+
let name;
|
|
77
|
+
try {
|
|
78
|
+
name = attribute.getName();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!name)
|
|
84
|
+
return;
|
|
85
|
+
if (allowlist) {
|
|
86
|
+
if (!allowlist.has(name.toLowerCase()))
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
else if (count >= maxAttributes) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const value = normalizeAttributeValue(attribute);
|
|
93
|
+
if (value == null)
|
|
94
|
+
return;
|
|
95
|
+
fields[name] = value;
|
|
96
|
+
count += 1;
|
|
97
|
+
});
|
|
98
|
+
let title;
|
|
99
|
+
try {
|
|
100
|
+
title = entity.getPrimaryAttributeValue?.() ?? undefined;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
title = undefined;
|
|
104
|
+
}
|
|
105
|
+
// Create forms have no id yet: the agent can still SEE the fields, but there
|
|
106
|
+
// is no target for execute actions, so we publish text-only context.
|
|
107
|
+
const item = recordId
|
|
108
|
+
? { id: recordId, kind: entityName, title, metadata: fields }
|
|
109
|
+
: null;
|
|
110
|
+
const fieldSummary = Object.entries(fields)
|
|
111
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
112
|
+
.join('; ');
|
|
113
|
+
const textParts = [
|
|
114
|
+
`Dynamics 365 form — entity "${entityName}"`,
|
|
115
|
+
recordId ? `record "${title ?? recordId}" (id ${recordId})` : 'new record (not saved yet)',
|
|
116
|
+
];
|
|
117
|
+
if (fieldSummary)
|
|
118
|
+
textParts.push(`Fields: ${fieldSummary}`);
|
|
119
|
+
const context = {
|
|
120
|
+
...(item ? { visibleContent: { items: [item], focusedIndex: 0 } } : {}),
|
|
121
|
+
custom: {
|
|
122
|
+
platform: 'dynamics365',
|
|
123
|
+
entityName,
|
|
124
|
+
...(recordId ? { recordId } : {}),
|
|
125
|
+
...(typeof formContext.ui?.getFormType === 'function'
|
|
126
|
+
? { formType: formContext.ui.getFormType() }
|
|
127
|
+
: {}),
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
return { kind: `dynamics-form:${entityName}`, text: textParts.join('. '), context };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Publishes the form snapshot now, republishes after every save (when the
|
|
134
|
+
* form exposes OnPostSave), and clears the context on disconnect. Call it from
|
|
135
|
+
* the form's OnLoad handler.
|
|
136
|
+
*/
|
|
137
|
+
function connectDynamicsFormContext(agent, formContext, options = {}) {
|
|
138
|
+
let disconnected = false;
|
|
139
|
+
const publish = () => {
|
|
140
|
+
if (disconnected)
|
|
141
|
+
return;
|
|
142
|
+
const snapshot = dynamicsFormSnapshot(formContext, options);
|
|
143
|
+
if (snapshot)
|
|
144
|
+
agent.setScreenContext(snapshot);
|
|
145
|
+
else
|
|
146
|
+
agent.clearScreenContext();
|
|
147
|
+
};
|
|
148
|
+
publish();
|
|
149
|
+
const entity = formContext.data?.entity;
|
|
150
|
+
if (entity?.addOnPostSave) {
|
|
151
|
+
try {
|
|
152
|
+
entity.addOnPostSave(publish);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
/* older clients — the host can still call refresh() manually */
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
refresh: publish,
|
|
160
|
+
disconnect() {
|
|
161
|
+
if (disconnected)
|
|
162
|
+
return;
|
|
163
|
+
disconnected = true;
|
|
164
|
+
try {
|
|
165
|
+
entity?.removeOnPostSave?.(publish);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
/* best-effort unsubscribe */
|
|
169
|
+
}
|
|
170
|
+
agent.clearScreenContext();
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embed entrypoint — single-file IIFE bundle for CLOSED third-party hosts
|
|
3
|
+
* where npm/ESM is not available: Dynamics 365 web resources, WinCC Unified
|
|
4
|
+
* Custom Web Controls, Salesforce LWC iframes, plain <script> tags.
|
|
5
|
+
*
|
|
6
|
+
* Built by `npm run build:embed` into dist/embed/fiodos-embed.js and exposed
|
|
7
|
+
* as the `Fiodos` global:
|
|
8
|
+
*
|
|
9
|
+
* <script src="fiodos-embed.js"></script>
|
|
10
|
+
* <script>
|
|
11
|
+
* const agent = Fiodos.createFiodosAgent({
|
|
12
|
+
* manifest, // e.g. from `fiodos from-odata`
|
|
13
|
+
* registries: Fiodos.buildApiActionRegistries(manifest, {
|
|
14
|
+
* baseUrl: 'https://org.crm.dynamics.com/api/data/v9.2',
|
|
15
|
+
* authProviders: { dataverse: () => ({ Authorization: `Bearer ${token}` }) },
|
|
16
|
+
* }),
|
|
17
|
+
* });
|
|
18
|
+
* </script>
|
|
19
|
+
*
|
|
20
|
+
* The bundle is self-contained (core + web-core inlined) and side-effect-free
|
|
21
|
+
* beyond assigning the global — it never auto-mounts; the host decides when.
|
|
22
|
+
*/
|
|
23
|
+
import { createFiodosAgent } from '../dropin/createFiodosAgent';
|
|
24
|
+
import { mountOrb } from '../orb/mountOrb';
|
|
25
|
+
import { AgentController } from '../controller/AgentController';
|
|
26
|
+
import { createScreenContextStore } from '../context/screenContextStore';
|
|
27
|
+
import { connectDynamicsFormContext, dynamicsFormSnapshot, normalizeDynamicsId } from './dynamics';
|
|
28
|
+
import { buildApiActionRegistries, validateManifest } from '@fiodos/core';
|
|
29
|
+
declare const api: {
|
|
30
|
+
readonly version: "0.1.14";
|
|
31
|
+
readonly createFiodosAgent: typeof createFiodosAgent;
|
|
32
|
+
readonly mountOrb: typeof mountOrb;
|
|
33
|
+
readonly AgentController: typeof AgentController;
|
|
34
|
+
readonly createScreenContextStore: typeof createScreenContextStore;
|
|
35
|
+
readonly buildApiActionRegistries: typeof buildApiActionRegistries;
|
|
36
|
+
readonly validateManifest: typeof validateManifest;
|
|
37
|
+
readonly connectDynamicsFormContext: typeof connectDynamicsFormContext;
|
|
38
|
+
readonly dynamicsFormSnapshot: typeof dynamicsFormSnapshot;
|
|
39
|
+
readonly normalizeDynamicsId: typeof normalizeDynamicsId;
|
|
40
|
+
};
|
|
41
|
+
declare global {
|
|
42
|
+
var Fiodos: typeof api | undefined;
|
|
43
|
+
}
|
|
44
|
+
export type FiodosEmbedApi = typeof api;
|
|
45
|
+
export {};
|