@endora-commerce/mod-search 0.100.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/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/backend/cli/reindex.d.ts +4 -0
- package/dist/backend/cli/reindex.d.ts.map +1 -0
- package/dist/backend/cli/reindex.js +32 -0
- package/dist/backend/cli/reindex.js.map +1 -0
- package/dist/backend/entities/search-phrase-record.entity.d.ts +40 -0
- package/dist/backend/entities/search-phrase-record.entity.d.ts.map +1 -0
- package/dist/backend/entities/search-phrase-record.entity.js +85 -0
- package/dist/backend/entities/search-phrase-record.entity.js.map +1 -0
- package/dist/backend/index.d.ts +87 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +153 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/plugin.d.ts +133 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +250 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +28 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +33 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.public.d.ts +22 -0
- package/dist/backend/routes.public.d.ts.map +1 -0
- package/dist/backend/routes.public.js +192 -0
- package/dist/backend/routes.public.js.map +1 -0
- package/dist/backend/services/embedder-config-resolver.d.ts +25 -0
- package/dist/backend/services/embedder-config-resolver.d.ts.map +1 -0
- package/dist/backend/services/embedder-config-resolver.js +47 -0
- package/dist/backend/services/embedder-config-resolver.js.map +1 -0
- package/dist/backend/services/llm-toggle.service.d.ts +50 -0
- package/dist/backend/services/llm-toggle.service.d.ts.map +1 -0
- package/dist/backend/services/llm-toggle.service.js +78 -0
- package/dist/backend/services/llm-toggle.service.js.map +1 -0
- package/dist/backend/services/search-event-subscriber.d.ts +125 -0
- package/dist/backend/services/search-event-subscriber.d.ts.map +1 -0
- package/dist/backend/services/search-event-subscriber.js +113 -0
- package/dist/backend/services/search-event-subscriber.js.map +1 -0
- package/dist/backend/services/search-indexer.d.ts +346 -0
- package/dist/backend/services/search-indexer.d.ts.map +1 -0
- package/dist/backend/services/search-indexer.js +670 -0
- package/dist/backend/services/search-indexer.js.map +1 -0
- package/dist/backend/services/search-phrase-recorder.service.d.ts +25 -0
- package/dist/backend/services/search-phrase-recorder.service.d.ts.map +1 -0
- package/dist/backend/services/search-phrase-recorder.service.js +89 -0
- package/dist/backend/services/search-phrase-recorder.service.js.map +1 -0
- package/dist/backend/services/search-query-port.d.ts +29 -0
- package/dist/backend/services/search-query-port.d.ts.map +1 -0
- package/dist/backend/services/search-query-port.js +20 -0
- package/dist/backend/services/search-query-port.js.map +1 -0
- package/dist/backend/services/search-query.service.d.ts +205 -0
- package/dist/backend/services/search-query.service.d.ts.map +1 -0
- package/dist/backend/services/search-query.service.js +346 -0
- package/dist/backend/services/search-query.service.js.map +1 -0
- package/dist/backend/services/search-reindex-worker.d.ts +27 -0
- package/dist/backend/services/search-reindex-worker.d.ts.map +1 -0
- package/dist/backend/services/search-reindex-worker.js +13 -0
- package/dist/backend/services/search-reindex-worker.js.map +1 -0
- package/dist/backend/services/search-suggest.service.d.ts +53 -0
- package/dist/backend/services/search-suggest.service.d.ts.map +1 -0
- package/dist/backend/services/search-suggest.service.js +64 -0
- package/dist/backend/services/search-suggest.service.js.map +1 -0
- package/dist/backend/services/suggestion-pricing-enricher.d.ts +62 -0
- package/dist/backend/services/suggestion-pricing-enricher.d.ts.map +1 -0
- package/dist/backend/services/suggestion-pricing-enricher.js +41 -0
- package/dist/backend/services/suggestion-pricing-enricher.js.map +1 -0
- package/dist/manifest.d.ts +263 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +300 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260501T123145_search_phrase_records_init.d.ts +13 -0
- package/dist/migrations/20260501T123145_search_phrase_records_init.d.ts.map +1 -0
- package/dist/migrations/20260501T123145_search_phrase_records_init.js +40 -0
- package/dist/migrations/20260501T123145_search_phrase_records_init.js.map +1 -0
- package/dist/migrations/index.d.ts +27 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +29 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/search.md +284 -0
- package/i18n/en.json +10 -0
- package/i18n/pl.json +10 -0
- package/package.json +72 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { rethrowIfModuleDisabled } from '@endora-commerce/platform/kernel';
|
|
3
|
+
import { SEARCH_SETTING_CODES } from '../../manifest.js';
|
|
4
|
+
const stringSchema = z.string();
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the Meilisearch embedder config for a channel (feature 058, T060).
|
|
7
|
+
*
|
|
8
|
+
* The `search.llm.embedder_credentials` reference (a reusable `llm` credential
|
|
9
|
+
* configuration) is the SINGLE source: its `baseUrl` → embedder URL, `apiKey` →
|
|
10
|
+
* embedder API key, `model` → embedder model. When the reference is unset or
|
|
11
|
+
* unresolvable, every field comes back empty and the caller treats LLM search as
|
|
12
|
+
* not configured (fail closed). There are no separate embedder settings.
|
|
13
|
+
*
|
|
14
|
+
* The secret `apiKey` is decrypted in-memory by `resolve`; it never leaves the
|
|
15
|
+
* process.
|
|
16
|
+
*/
|
|
17
|
+
export async function resolveEmbedderConfig(settings, channelId, credentials) {
|
|
18
|
+
let refCode = '';
|
|
19
|
+
try {
|
|
20
|
+
refCode = await settings.get(SEARCH_SETTING_CODES.LLM_EMBEDDER_CREDENTIALS, channelId, stringSchema);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Not seeded / out of scope — treat as unset.
|
|
24
|
+
refCode = '';
|
|
25
|
+
}
|
|
26
|
+
if (!credentials || !refCode) {
|
|
27
|
+
return { url: '', apiKey: '', model: '' };
|
|
28
|
+
}
|
|
29
|
+
// Tolerated: an unset or unresolvable reference means LLM search is not
|
|
30
|
+
// configured, and the caller is right to serve without it. **A presence
|
|
31
|
+
// answer is not that**: `credentials` being switched off would otherwise read
|
|
32
|
+
// as "the operator never configured this", and the screen would offer to fix
|
|
33
|
+
// a setting that is already correct.
|
|
34
|
+
const resolved = await credentials.resolve(refCode).catch((error) => {
|
|
35
|
+
rethrowIfModuleDisabled(error);
|
|
36
|
+
return null;
|
|
37
|
+
});
|
|
38
|
+
if (!resolved || resolved.status !== 'ok') {
|
|
39
|
+
return { url: '', apiKey: '', model: '' };
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
url: typeof resolved.values['baseUrl'] === 'string' ? resolved.values['baseUrl'] : '',
|
|
43
|
+
apiKey: typeof resolved.values['apiKey'] === 'string' ? resolved.values['apiKey'] : '',
|
|
44
|
+
model: typeof resolved.values['model'] === 'string' ? resolved.values['model'] : '',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=embedder-config-resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embedder-config-resolver.js","sourceRoot":"","sources":["../../../src/backend/services/embedder-config-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,uBAAuB,EAAyB,MAAM,kCAAkC,CAAC;AAClG,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAazD,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC;AAEhC;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,QAA0B,EAC1B,SAAiB,EACjB,WAAmC;IAEnC,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,QAAQ,CAAC,GAAG,CAC1B,oBAAoB,CAAC,wBAAwB,EAC7C,SAAS,EACT,YAAY,CACb,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,8CAA8C;QAC9C,OAAO,GAAG,EAAE,CAAC;IACf,CAAC;IAED,IAAI,CAAC,WAAW,IAAI,CAAC,OAAO,EAAE,CAAC;QAC7B,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IAC5C,CAAC;IAED,wEAAwE;IACxE,wEAAwE;IACxE,8EAA8E;IAC9E,6EAA6E;IAC7E,qCAAqC;IACrC,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;QAC3E,uBAAuB,CAAC,KAAK,CAAC,CAAC;QAC/B,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;QAC1C,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IAC5C,CAAC;IAED,OAAO;QACL,GAAG,EAAE,OAAO,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE;QACrF,MAAM,EAAE,OAAO,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE;QACtF,KAAK,EAAE,OAAO,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE;KACpF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type SettingsAdminAuditContext, type SettingsAdminPort } from '@endora-commerce/contracts';
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
3
|
+
import type { SettingsReadPort } from '@endora-commerce/platform/kernel';
|
|
4
|
+
import { type CredentialResolvePort } from './embedder-config-resolver.js';
|
|
5
|
+
/**
|
|
6
|
+
* LlmToggleService — feature 006 / US2 / T024.
|
|
7
|
+
*
|
|
8
|
+
* Cross-setting validating wrapper around `search.llm.enabled`. The four
|
|
9
|
+
* `search.llm.*` settings (toggle + url + api_key + model) live in the
|
|
10
|
+
* Settings module's `setting_values` table; the three embedder.* fields
|
|
11
|
+
* are written through the generic Settings admin route (per-field audit
|
|
12
|
+
* trail). This wrapper only writes the `enabled` toggle, but refuses to
|
|
13
|
+
* flip it on for any channel whose embedder.* triplet is incomplete
|
|
14
|
+
* (FR-011).
|
|
15
|
+
*
|
|
16
|
+
* Disabling never refuses — turning off LLM-augmented search must always
|
|
17
|
+
* be possible regardless of the embedder fields' state.
|
|
18
|
+
*/
|
|
19
|
+
export interface LlmToggleParams {
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
/** When omitted, applies to every channel in the setting's scope. */
|
|
22
|
+
salesChannelCodes?: string[];
|
|
23
|
+
expectedVersion: string | null;
|
|
24
|
+
actor: SettingsAdminAuditContext;
|
|
25
|
+
}
|
|
26
|
+
export interface LlmToggleResult {
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
newVersion: string;
|
|
29
|
+
appliedChannelIds: string[];
|
|
30
|
+
}
|
|
31
|
+
export declare class LlmToggleService {
|
|
32
|
+
private readonly emFactory;
|
|
33
|
+
private readonly settingsService;
|
|
34
|
+
private readonly settingsAdminService;
|
|
35
|
+
/** Feature 058 — resolves `search.llm.embedder_credentials` (optional). */
|
|
36
|
+
private readonly credentials?;
|
|
37
|
+
constructor(emFactory: () => EntityManager, settingsService: SettingsReadPort, settingsAdminService: SettingsAdminPort,
|
|
38
|
+
/** Feature 058 — resolves `search.llm.embedder_credentials` (optional). */
|
|
39
|
+
credentials?: CredentialResolvePort | undefined);
|
|
40
|
+
toggle(params: LlmToggleParams): Promise<LlmToggleResult>;
|
|
41
|
+
/**
|
|
42
|
+
* Reads the three embedder.* settings for every targeted channel and
|
|
43
|
+
* refuses with `LLM_CONFIG_INCOMPLETE` (HTTP 400) when any value is
|
|
44
|
+
* empty. The error's `details[]` lists each missing
|
|
45
|
+
* (channelCode, settingCode) pair so the admin UI can pinpoint what
|
|
46
|
+
* needs filling.
|
|
47
|
+
*/
|
|
48
|
+
private assertEmbedderConfigComplete;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=llm-toggle.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llm-toggle.service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/llm-toggle.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,yBAAyB,EAC9B,KAAK,iBAAiB,EACvB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAGzE,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,+BAA+B,CAAC;AAEvC;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,qEAAqE;IACrE,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,KAAK,EAAE,yBAAyB,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,qBAAa,gBAAgB;IAEzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,oBAAoB;IACrC,2EAA2E;IAC3E,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;gBAJZ,SAAS,EAAE,MAAM,aAAa,EAC9B,eAAe,EAAE,gBAAgB,EACjC,oBAAoB,EAAE,iBAAiB;IACxD,2EAA2E;IAC1D,WAAW,CAAC,EAAE,qBAAqB,YAAA;IAGhD,MAAM,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;IA2B/D;;;;;;OAMG;YACW,4BAA4B;CAwD3C"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ERROR_CODES, } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
import { SalesChannel } from '@endora-commerce/platform/kernel';
|
|
4
|
+
import { SEARCH_SETTING_CODES } from '../../manifest.js';
|
|
5
|
+
import { resolveEmbedderConfig, } from './embedder-config-resolver.js';
|
|
6
|
+
export class LlmToggleService {
|
|
7
|
+
emFactory;
|
|
8
|
+
settingsService;
|
|
9
|
+
settingsAdminService;
|
|
10
|
+
credentials;
|
|
11
|
+
constructor(emFactory, settingsService, settingsAdminService,
|
|
12
|
+
/** Feature 058 — resolves `search.llm.embedder_credentials` (optional). */
|
|
13
|
+
credentials) {
|
|
14
|
+
this.emFactory = emFactory;
|
|
15
|
+
this.settingsService = settingsService;
|
|
16
|
+
this.settingsAdminService = settingsAdminService;
|
|
17
|
+
this.credentials = credentials;
|
|
18
|
+
}
|
|
19
|
+
async toggle(params) {
|
|
20
|
+
if (params.enabled) {
|
|
21
|
+
await this.assertEmbedderConfigComplete(params.salesChannelCodes);
|
|
22
|
+
}
|
|
23
|
+
const result = params.salesChannelCodes
|
|
24
|
+
? await this.settingsAdminService.setValueForSubset(SEARCH_SETTING_CODES.LLM_ENABLED, params.salesChannelCodes, params.enabled, params.expectedVersion, params.actor)
|
|
25
|
+
: await this.settingsAdminService.setValueForAllChannels(SEARCH_SETTING_CODES.LLM_ENABLED, params.enabled, params.expectedVersion, params.actor);
|
|
26
|
+
return {
|
|
27
|
+
enabled: params.enabled,
|
|
28
|
+
newVersion: result.newVersion,
|
|
29
|
+
appliedChannelIds: result.affectedChannelIds,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Reads the three embedder.* settings for every targeted channel and
|
|
34
|
+
* refuses with `LLM_CONFIG_INCOMPLETE` (HTTP 400) when any value is
|
|
35
|
+
* empty. The error's `details[]` lists each missing
|
|
36
|
+
* (channelCode, settingCode) pair so the admin UI can pinpoint what
|
|
37
|
+
* needs filling.
|
|
38
|
+
*/
|
|
39
|
+
async assertEmbedderConfigComplete(salesChannelCodes) {
|
|
40
|
+
const em = this.emFactory();
|
|
41
|
+
const channels = salesChannelCodes
|
|
42
|
+
? await em.find(SalesChannel, { code: { $in: salesChannelCodes } })
|
|
43
|
+
: await em.find(SalesChannel, {});
|
|
44
|
+
if (salesChannelCodes && channels.length !== salesChannelCodes.length) {
|
|
45
|
+
const found = new Set(channels.map((c) => c.code));
|
|
46
|
+
const missing = salesChannelCodes.filter((c) => !found.has(c));
|
|
47
|
+
throw new HttpError(400, ERROR_CODES.SETTING_OUT_OF_SCOPE_FOR_CHANNEL, `Unknown sales channel(s): ${missing.join(', ')}.`, missing.map((c) => ({
|
|
48
|
+
path: `salesChannelCodes.${c}`,
|
|
49
|
+
issue: 'unknown sales channel',
|
|
50
|
+
})));
|
|
51
|
+
}
|
|
52
|
+
const missing = [];
|
|
53
|
+
for (const channel of channels) {
|
|
54
|
+
// Feature 058 — the embedder config comes solely from the
|
|
55
|
+
// `search.llm.embedder_credentials` reference (Base URL + API key + model).
|
|
56
|
+
// A field is "missing" when the referenced configuration does not supply
|
|
57
|
+
// it (or no reference is set); the error points at the credential setting.
|
|
58
|
+
const cfg = await resolveEmbedderConfig(this.settingsService, channel.id, this.credentials);
|
|
59
|
+
const fields = [
|
|
60
|
+
['url', cfg.url],
|
|
61
|
+
['apiKey', cfg.apiKey],
|
|
62
|
+
['model', cfg.model],
|
|
63
|
+
];
|
|
64
|
+
for (const [field, value] of fields) {
|
|
65
|
+
if (!value || value.length === 0) {
|
|
66
|
+
missing.push({ channelCode: channel.code, field });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (missing.length > 0) {
|
|
71
|
+
throw new HttpError(400, ERROR_CODES.LLM_CONFIG_INCOMPLETE, 'LLM-augmented search cannot be enabled until the referenced embedder credential supplies a Base URL, API key and model on every targeted channel.', missing.map((m) => ({
|
|
72
|
+
path: `${m.channelCode}.${SEARCH_SETTING_CODES.LLM_EMBEDDER_CREDENTIALS}`,
|
|
73
|
+
issue: `embedder ${m.field} is not provided by the credential`,
|
|
74
|
+
})));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=llm-toggle.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llm-toggle.service.js","sourceRoot":"","sources":["../../../src/backend/services/llm-toggle.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { EventBase } from '@endora-commerce/platform/events';
|
|
3
|
+
import type { SearchIndexer } from './search-indexer.js';
|
|
4
|
+
import type { SettingsReadPort } from '@endora-commerce/platform/kernel';
|
|
5
|
+
import { type CredentialResolvePort } from './embedder-config-resolver.js';
|
|
6
|
+
/**
|
|
7
|
+
* SearchEventSubscriber (T067 — incremental upsert path).
|
|
8
|
+
*
|
|
9
|
+
* The handlers that keep the per-channel Meilisearch indexes in sync with
|
|
10
|
+
* Postgres, so a catalog mutation costs an incremental upsert rather than an
|
|
11
|
+
* offline reindex. **The class no longer subscribes to anything** — the
|
|
12
|
+
* registrations live in this module's `backend.ts`, where `ctx.subscribe` wraps
|
|
13
|
+
* each one in `subscribeForModule` and the module's effective state decides
|
|
14
|
+
* whether the handler runs (issue #107). What used to happen here was a bare
|
|
15
|
+
* `eventBus.on`, so a deployment with `search` switched off went on rewriting
|
|
16
|
+
* Meilisearch documents on every product write.
|
|
17
|
+
*
|
|
18
|
+
* The events `backend.ts` maps onto these methods:
|
|
19
|
+
*
|
|
20
|
+
* - `product.created.v1` → upsertProduct
|
|
21
|
+
* - `product.updated.v1` → upsertProduct
|
|
22
|
+
* - `product.archived.v1` → deleteProduct
|
|
23
|
+
* - `product.deleted.v1` → deleteProduct
|
|
24
|
+
* - `attribute.updated.v1` → refreshAttributeSettings
|
|
25
|
+
* - `category.updated.v1` → reindexCategorySubtree (feature 068: the
|
|
26
|
+
* category's slug and activation state are projected onto every product
|
|
27
|
+
* document under it, so a rename or a deactivation has to reach the index)
|
|
28
|
+
*
|
|
29
|
+
* Feature 006 / T027 also attaches the LLM-augmented-search reactor:
|
|
30
|
+
*
|
|
31
|
+
* - `settings.value_changed` (settingCode = `search.llm.enabled`)
|
|
32
|
+
* → attach or detach a Meilisearch embedder per affected channel,
|
|
33
|
+
* depending on the new boolean value. When `enabled=true` and any
|
|
34
|
+
* of the embedder.* config values are empty, the handler logs and
|
|
35
|
+
* no-ops — the toggle's pre-write validator (LlmToggleService) is
|
|
36
|
+
* the user-visible refusal path; this handler is the
|
|
37
|
+
* belt-and-braces guard for direct generic-Settings writes.
|
|
38
|
+
*
|
|
39
|
+
* Handlers swallow their own errors so a transient Meilisearch outage
|
|
40
|
+
* doesn't break the catalog write path. The reserved-fallback in the read
|
|
41
|
+
* path (catalog/routes.public.ts) keeps search functional even with a
|
|
42
|
+
* stale index until the next offline `search:reindex`.
|
|
43
|
+
*/
|
|
44
|
+
interface CatalogEvents extends Record<string, EventBase> {
|
|
45
|
+
'product.created.v1': EventBase & {
|
|
46
|
+
productId: string;
|
|
47
|
+
sku: string;
|
|
48
|
+
};
|
|
49
|
+
'product.updated.v1': EventBase & {
|
|
50
|
+
productId: string;
|
|
51
|
+
changedFields: string[];
|
|
52
|
+
};
|
|
53
|
+
'product.archived.v1': EventBase & {
|
|
54
|
+
productId: string;
|
|
55
|
+
};
|
|
56
|
+
'product.deleted.v1': EventBase & {
|
|
57
|
+
productId: string;
|
|
58
|
+
};
|
|
59
|
+
'attribute.updated.v1': EventBase & {
|
|
60
|
+
key: string;
|
|
61
|
+
isSearchable: boolean;
|
|
62
|
+
isFilterable: boolean;
|
|
63
|
+
};
|
|
64
|
+
'category.updated.v1': EventBase & {
|
|
65
|
+
categoryId: string;
|
|
66
|
+
};
|
|
67
|
+
'settings.value_changed': EventBase & {
|
|
68
|
+
settingCode: string;
|
|
69
|
+
salesChannelIds: string[];
|
|
70
|
+
/**
|
|
71
|
+
* Set when the platform-wide global override (settings.global_value)
|
|
72
|
+
* changed (feature 042). Channels without a per-channel override
|
|
73
|
+
* resolve from the global; this flag tells the reactor to re-evaluate
|
|
74
|
+
* every in-scope channel, not only the codes listed in `salesChannelIds`.
|
|
75
|
+
*/
|
|
76
|
+
globalValueUpdated?: boolean;
|
|
77
|
+
valueType: string;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** The `settings.value_changed` payload this module reacts to. */
|
|
81
|
+
export type SettingChangedPayload = CatalogEvents['settings.value_changed'];
|
|
82
|
+
export interface SearchEventSubscriberDeps {
|
|
83
|
+
emFactory: () => EntityManager;
|
|
84
|
+
indexer: SearchIndexer;
|
|
85
|
+
/**
|
|
86
|
+
* Universal-getter for Settings — required when the LLM-toggle reactor
|
|
87
|
+
* is enabled (feature 006). When undefined, the `settings.value_changed`
|
|
88
|
+
* handler is not attached; foundation tests that don't have settings
|
|
89
|
+
* wired stay green.
|
|
90
|
+
*/
|
|
91
|
+
settingsService?: SettingsReadPort;
|
|
92
|
+
/**
|
|
93
|
+
* Feature 058 — resolves `search.llm.embedder_credentials` into the embedder
|
|
94
|
+
* config, falling back per field to the legacy embedder settings. Optional.
|
|
95
|
+
*/
|
|
96
|
+
credentials?: CredentialResolvePort;
|
|
97
|
+
/**
|
|
98
|
+
* Logger hook for failures. Defaults to console.warn so production logs
|
|
99
|
+
* still surface them; tests pass a vi.fn() to assert.
|
|
100
|
+
*/
|
|
101
|
+
onError?: (err: unknown, eventName: string) => void;
|
|
102
|
+
}
|
|
103
|
+
export declare class SearchEventSubscriber {
|
|
104
|
+
private readonly deps;
|
|
105
|
+
constructor(deps: SearchEventSubscriberDeps);
|
|
106
|
+
private get log();
|
|
107
|
+
/** `product.created.v1` / `product.updated.v1`. */
|
|
108
|
+
onProductUpserted(productId: string, eventName: string): Promise<void>;
|
|
109
|
+
/** `product.archived.v1` / `product.deleted.v1`. */
|
|
110
|
+
onProductRemoved(productId: string, eventName: string): Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* `category.updated.v1` — feature 068: a category's slug and activation state
|
|
113
|
+
* are projected onto every product document beneath it.
|
|
114
|
+
*/
|
|
115
|
+
onCategoryUpdated(categoryId: string): Promise<void>;
|
|
116
|
+
/** `attribute.updated.v1`. */
|
|
117
|
+
onAttributeUpdated(): Promise<void>;
|
|
118
|
+
/**
|
|
119
|
+
* `settings.value_changed` — the LLM-augmented-search reactor (feature 006 /
|
|
120
|
+
* T027). A no-op unless a settings service was supplied.
|
|
121
|
+
*/
|
|
122
|
+
onSettingChanged(payload: SettingChangedPayload): Promise<void>;
|
|
123
|
+
}
|
|
124
|
+
export {};
|
|
125
|
+
//# sourceMappingURL=search-event-subscriber.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-event-subscriber.d.ts","sourceRoot":"","sources":["../../../src/backend/services/search-event-subscriber.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAIzE,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,+BAA+B,CAAC;AAEvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,UAAU,aAAc,SAAQ,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC;IACvD,oBAAoB,EAAE,SAAS,GAAG;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACrE,oBAAoB,EAAE,SAAS,GAAG;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACjF,qBAAqB,EAAE,SAAS,GAAG;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IACzD,oBAAoB,EAAE,SAAS,GAAG;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IACxD,sBAAsB,EAAE,SAAS,GAAG;QAClC,GAAG,EAAE,MAAM,CAAC;QACZ,YAAY,EAAE,OAAO,CAAC;QACtB,YAAY,EAAE,OAAO,CAAC;KACvB,CAAC;IACF,qBAAqB,EAAE,SAAS,GAAG;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1D,wBAAwB,EAAE,SAAS,GAAG;QACpC,WAAW,EAAE,MAAM,CAAC;QACpB,eAAe,EAAE,MAAM,EAAE,CAAC;QAC1B;;;;;WAKG;QACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;QAC7B,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,kEAAkE;AAClE,MAAM,MAAM,qBAAqB,GAAG,aAAa,CAAC,wBAAwB,CAAC,CAAC;AAE5E,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,aAAa,CAAC;IAC/B,OAAO,EAAE,aAAa,CAAC;IACvB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,gBAAgB,CAAC;IACnC;;;OAGG;IACH,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CACrD;AAID,qBAAa,qBAAqB;IACpB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,yBAAyB;IAE5D,OAAO,KAAK,GAAG,GAEd;IAED,mDAAmD;IAC7C,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ5E,oDAAoD;IAC9C,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ3E;;;OAGG;IACG,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ1D,8BAA8B;IACxB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAQzC;;;OAGG;IACG,gBAAgB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;CAuDtE"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { SalesChannel } from '@endora-commerce/platform/kernel';
|
|
3
|
+
import { SEARCH_SETTING_CODES } from '../../manifest.js';
|
|
4
|
+
import { resolveEmbedderConfig, } from './embedder-config-resolver.js';
|
|
5
|
+
const booleanSchema = z.boolean();
|
|
6
|
+
export class SearchEventSubscriber {
|
|
7
|
+
deps;
|
|
8
|
+
constructor(deps) {
|
|
9
|
+
this.deps = deps;
|
|
10
|
+
}
|
|
11
|
+
get log() {
|
|
12
|
+
return this.deps.onError ?? defaultLogger;
|
|
13
|
+
}
|
|
14
|
+
/** `product.created.v1` / `product.updated.v1`. */
|
|
15
|
+
async onProductUpserted(productId, eventName) {
|
|
16
|
+
try {
|
|
17
|
+
await this.deps.indexer.upsertProduct(this.deps.emFactory(), productId);
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
this.log(err, eventName);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** `product.archived.v1` / `product.deleted.v1`. */
|
|
24
|
+
async onProductRemoved(productId, eventName) {
|
|
25
|
+
try {
|
|
26
|
+
await this.deps.indexer.deleteProduct(this.deps.emFactory(), productId);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
this.log(err, eventName);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* `category.updated.v1` — feature 068: a category's slug and activation state
|
|
34
|
+
* are projected onto every product document beneath it.
|
|
35
|
+
*/
|
|
36
|
+
async onCategoryUpdated(categoryId) {
|
|
37
|
+
try {
|
|
38
|
+
await this.deps.indexer.reindexCategorySubtree(this.deps.emFactory(), categoryId);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
this.log(err, 'category.updated.v1');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** `attribute.updated.v1`. */
|
|
45
|
+
async onAttributeUpdated() {
|
|
46
|
+
try {
|
|
47
|
+
await this.deps.indexer.refreshAttributeSettings(this.deps.emFactory());
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
this.log(err, 'attribute.updated.v1');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* `settings.value_changed` — the LLM-augmented-search reactor (feature 006 /
|
|
55
|
+
* T027). A no-op unless a settings service was supplied.
|
|
56
|
+
*/
|
|
57
|
+
async onSettingChanged(payload) {
|
|
58
|
+
const settingsService = this.deps.settingsService;
|
|
59
|
+
if (!settingsService)
|
|
60
|
+
return;
|
|
61
|
+
if (payload.settingCode !== SEARCH_SETTING_CODES.LLM_ENABLED)
|
|
62
|
+
return;
|
|
63
|
+
const { emFactory, indexer } = this.deps;
|
|
64
|
+
const log = this.log;
|
|
65
|
+
try {
|
|
66
|
+
const em = emFactory();
|
|
67
|
+
// Feature 042: a global-override change touches every channel
|
|
68
|
+
// that doesn't carry its own per-channel value — re-evaluate
|
|
69
|
+
// the lot. Per-channel writes still target only the listed ids.
|
|
70
|
+
const channels = payload.globalValueUpdated
|
|
71
|
+
? await em.find(SalesChannel, {})
|
|
72
|
+
: await em.find(SalesChannel, {
|
|
73
|
+
id: { $in: payload.salesChannelIds },
|
|
74
|
+
});
|
|
75
|
+
for (const channel of channels) {
|
|
76
|
+
const enabled = await settingsService.get(SEARCH_SETTING_CODES.LLM_ENABLED, channel.id, booleanSchema);
|
|
77
|
+
if (enabled) {
|
|
78
|
+
// Feature 058 — prefer the credential reference, fall back per
|
|
79
|
+
// field to the legacy embedder settings.
|
|
80
|
+
const { url, apiKey, model } = await resolveEmbedderConfig(settingsService, channel.id, this.deps.credentials);
|
|
81
|
+
if (url && apiKey && model) {
|
|
82
|
+
await indexer.attachEmbedderForChannel(channel.code, {
|
|
83
|
+
url,
|
|
84
|
+
apiKey,
|
|
85
|
+
model,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// Direct generic-Settings write bypassed the toggle
|
|
90
|
+
// validator; surface for ops, leave the index alone so
|
|
91
|
+
// we don't poison Meilisearch with empty creds.
|
|
92
|
+
log(new Error(`LLM enabled on channel ${channel.code} without complete embedder config; embedder NOT attached`), 'settings.value_changed');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
await indexer.detachEmbedderForChannel(channel.code);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
log(err, 'settings.value_changed');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function defaultLogger(err, eventName) {
|
|
106
|
+
console.warn(JSON.stringify({
|
|
107
|
+
level: 'warn',
|
|
108
|
+
msg: 'search subscriber handler failed',
|
|
109
|
+
eventName,
|
|
110
|
+
error: err instanceof Error ? err.message : String(err),
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=search-event-subscriber.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-event-subscriber.js","sourceRoot":"","sources":["../../../src/backend/services/search-event-subscriber.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EACL,qBAAqB,GAEtB,MAAM,+BAA+B,CAAC;AA2FvC,MAAM,aAAa,GAAG,CAAC,CAAC,OAAO,EAAE,CAAC;AAElC,MAAM,OAAO,qBAAqB;IACH;IAA7B,YAA6B,IAA+B;QAA/B,SAAI,GAAJ,IAAI,CAA2B;IAAG,CAAC;IAEhE,IAAY,GAAG;QACb,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,aAAa,CAAC;IAC5C,CAAC;IAED,mDAAmD;IACnD,KAAK,CAAC,iBAAiB,CAAC,SAAiB,EAAE,SAAiB;QAC1D,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,SAAS,CAAC,CAAC;QAC1E,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;IAED,oDAAoD;IACpD,KAAK,CAAC,gBAAgB,CAAC,SAAiB,EAAE,SAAiB;QACzD,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,SAAS,CAAC,CAAC;QAC1E,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,iBAAiB,CAAC,UAAkB;QACxC,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,UAAU,CAAC,CAAC;QACpF,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IAED,8BAA8B;IAC9B,KAAK,CAAC,kBAAkB;QACtB,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,wBAAwB,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;QAC1E,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,sBAAsB,CAAC,CAAC;QACxC,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,gBAAgB,CAAC,OAA8B;QACnD,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC;QAClD,IAAI,CAAC,eAAe;YAAE,OAAO;QAC7B,IAAI,OAAO,CAAC,WAAW,KAAK,oBAAoB,CAAC,WAAW;YAAE,OAAO;QACrE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC;QACzC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC;QACrB,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,SAAS,EAAE,CAAC;YACvB,8DAA8D;YAC9D,6DAA6D;YAC7D,gEAAgE;YAChE,MAAM,QAAQ,GAAG,OAAO,CAAC,kBAAkB;gBACzC,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,CAAC;gBACjC,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,YAAY,EAAE;oBAC1B,EAAE,EAAE,EAAE,GAAG,EAAE,OAAO,CAAC,eAAe,EAAE;iBACrC,CAAC,CAAC;YACP,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;gBAC/B,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,GAAG,CACvC,oBAAoB,CAAC,WAAW,EAChC,OAAO,CAAC,EAAE,EACV,aAAa,CACd,CAAC;gBACF,IAAI,OAAO,EAAE,CAAC;oBACZ,+DAA+D;oBAC/D,yCAAyC;oBACzC,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,qBAAqB,CACxD,eAAe,EACf,OAAO,CAAC,EAAE,EACV,IAAI,CAAC,IAAI,CAAC,WAAW,CACtB,CAAC;oBACF,IAAI,GAAG,IAAI,MAAM,IAAI,KAAK,EAAE,CAAC;wBAC3B,MAAM,OAAO,CAAC,wBAAwB,CAAC,OAAO,CAAC,IAAI,EAAE;4BACnD,GAAG;4BACH,MAAM;4BACN,KAAK;yBACN,CAAC,CAAC;oBACL,CAAC;yBAAM,CAAC;wBACN,oDAAoD;wBACpD,uDAAuD;wBACvD,gDAAgD;wBAChD,GAAG,CACD,IAAI,KAAK,CACP,0BAA0B,OAAO,CAAC,IAAI,0DAA0D,CACjG,EACD,wBAAwB,CACzB,CAAC;oBACJ,CAAC;gBACH,CAAC;qBAAM,CAAC;oBACN,MAAM,OAAO,CAAC,wBAAwB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;gBACvD,CAAC;YACH,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,GAAG,EAAE,wBAAwB,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;CACF;AAED,SAAS,aAAa,CAAC,GAAY,EAAE,SAAiB;IAEpD,OAAO,CAAC,IAAI,CACV,IAAI,CAAC,SAAS,CAAC;QACb,KAAK,EAAE,MAAM;QACb,GAAG,EAAE,kCAAkC;QACvC,SAAS;QACT,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;KACxD,CAAC,CACH,CAAC;AACJ,CAAC"}
|