@rimori/client 2.5.19 → 2.5.20-next.1
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/controller/TranslationController.js +2 -0
- package/dist/fromRimori/EventBus.d.ts +4 -2
- package/dist/fromRimori/EventBus.js +9 -8
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/plugin/CommunicationHandler.js +2 -2
- package/dist/plugin/TTS/MessageSender.d.ts +5 -1
- package/dist/plugin/TTS/MessageSender.js +8 -1
- package/dist/plugin/module/AIModule.d.ts +15 -12
- package/dist/plugin/module/AIModule.js +62 -45
- package/dist/plugin/module/DbModule.d.ts +28 -3
- package/dist/plugin/module/DbModule.js +6 -5
- package/dist/plugin/module/EventModule.js +39 -6
- package/dist/plugin/module/PluginModule.d.ts +17 -0
- package/dist/plugin/module/PluginModule.js +4 -0
- package/package.json +4 -4
|
@@ -49,6 +49,8 @@ export class Translator {
|
|
|
49
49
|
},
|
|
50
50
|
debug: false,
|
|
51
51
|
parseMissingKeyHandler: (key, defaultValue) => {
|
|
52
|
+
if (!key.trim())
|
|
53
|
+
return '';
|
|
52
54
|
if (this.isTranslationKey(key)) {
|
|
53
55
|
console.warn(`Translation key not found: ${key}`);
|
|
54
56
|
return defaultValue !== null && defaultValue !== void 0 ? defaultValue : '';
|
|
@@ -13,6 +13,8 @@ export interface EventBusMessage<T = EventPayload> {
|
|
|
13
13
|
topic: string;
|
|
14
14
|
data: T;
|
|
15
15
|
debug: boolean;
|
|
16
|
+
/** Session token inherited from the calling plugin, used to attribute AI calls to the originating exercise session. */
|
|
17
|
+
ai_session_token?: string;
|
|
16
18
|
}
|
|
17
19
|
export type EventHandler<T = EventPayload> = (event: EventBusMessage<T>) => void | Promise<void>;
|
|
18
20
|
export interface EventListener {
|
|
@@ -56,7 +58,7 @@ export declare class EventBusHandler {
|
|
|
56
58
|
* - pl1234.card.delete
|
|
57
59
|
* - pl1234.card.triggerBackup
|
|
58
60
|
*/
|
|
59
|
-
emit<T = EventPayload>(sender: string, topic: string, data?: T, eventId?: number): void;
|
|
61
|
+
emit<T = EventPayload>(sender: string, topic: string, data?: T, eventId?: number, aiSessionToken?: string): void;
|
|
60
62
|
private emitInternal;
|
|
61
63
|
/**
|
|
62
64
|
* Subscribes to an event on the event bus.
|
|
@@ -93,7 +95,7 @@ export declare class EventBusHandler {
|
|
|
93
95
|
* @param data - The data of the event.
|
|
94
96
|
* @returns A promise that resolves to the event.
|
|
95
97
|
*/
|
|
96
|
-
request<T = EventPayload>(sender: string, topic: string, data?: EventPayload): Promise<EventBusMessage<T>>;
|
|
98
|
+
request<T = EventPayload>(sender: string, topic: string, data?: EventPayload, aiSessionToken?: string): Promise<EventBusMessage<T>>;
|
|
97
99
|
/**
|
|
98
100
|
* Gets the matching handlers for an event.
|
|
99
101
|
* @param topic - The topic of the event.
|
|
@@ -63,7 +63,7 @@ export class EventBusHandler {
|
|
|
63
63
|
this.generatedIds.set(id, now);
|
|
64
64
|
return id;
|
|
65
65
|
}
|
|
66
|
-
createEvent(sender, topic, data, eventId) {
|
|
66
|
+
createEvent(sender, topic, data, eventId, aiSessionToken) {
|
|
67
67
|
const generatedEventId = eventId || this.generateUniqueId();
|
|
68
68
|
return {
|
|
69
69
|
eventId: generatedEventId,
|
|
@@ -72,6 +72,7 @@ export class EventBusHandler {
|
|
|
72
72
|
topic,
|
|
73
73
|
data,
|
|
74
74
|
debug: this.debugEnabled,
|
|
75
|
+
ai_session_token: aiSessionToken,
|
|
75
76
|
};
|
|
76
77
|
}
|
|
77
78
|
/**
|
|
@@ -92,15 +93,15 @@ export class EventBusHandler {
|
|
|
92
93
|
* - pl1234.card.delete
|
|
93
94
|
* - pl1234.card.triggerBackup
|
|
94
95
|
*/
|
|
95
|
-
emit(sender, topic, data, eventId) {
|
|
96
|
-
this.emitInternal(sender, topic, data || {}, eventId);
|
|
96
|
+
emit(sender, topic, data, eventId, aiSessionToken) {
|
|
97
|
+
this.emitInternal(sender, topic, data || {}, eventId, false, aiSessionToken);
|
|
97
98
|
}
|
|
98
|
-
emitInternal(sender, topic, data, eventId, skipResponseTrigger = false) {
|
|
99
|
+
emitInternal(sender, topic, data, eventId, skipResponseTrigger = false, aiSessionToken) {
|
|
99
100
|
if (!this.validateTopic(topic)) {
|
|
100
101
|
this.logAndThrowError(false, `Invalid topic: ` + topic);
|
|
101
102
|
return;
|
|
102
103
|
}
|
|
103
|
-
const event = this.createEvent(sender, topic, data, eventId);
|
|
104
|
+
const event = this.createEvent(sender, topic, data, eventId, aiSessionToken);
|
|
104
105
|
const handlers = this.getMatchingHandlers(event.topic);
|
|
105
106
|
handlers.forEach((handler) => {
|
|
106
107
|
if (handler.ignoreSender && handler.ignoreSender.includes(sender)) {
|
|
@@ -245,16 +246,16 @@ export class EventBusHandler {
|
|
|
245
246
|
* @param data - The data of the event.
|
|
246
247
|
* @returns A promise that resolves to the event.
|
|
247
248
|
*/
|
|
248
|
-
request(sender, topic, data) {
|
|
249
|
+
request(sender, topic, data, aiSessionToken) {
|
|
249
250
|
return __awaiter(this, void 0, void 0, function* () {
|
|
250
251
|
if (!this.validateTopic(topic)) {
|
|
251
252
|
this.logAndThrowError(true, `Invalid topic: ` + topic);
|
|
252
253
|
}
|
|
253
|
-
const event = this.createEvent(sender, topic, data || {});
|
|
254
|
+
const event = this.createEvent(sender, topic, data || {}, undefined, aiSessionToken);
|
|
254
255
|
this.logIfDebug(`Requesting data from ` + topic, { event });
|
|
255
256
|
return new Promise((resolve) => {
|
|
256
257
|
this.responseResolvers.set(event.eventId, (value) => resolve(value));
|
|
257
|
-
this.emitInternal(sender, topic, data || {}, event.eventId, true);
|
|
258
|
+
this.emitInternal(sender, topic, data || {}, event.eventId, true, aiSessionToken);
|
|
258
259
|
});
|
|
259
260
|
});
|
|
260
261
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -15,7 +15,8 @@ export { Translator } from './controller/TranslationController';
|
|
|
15
15
|
export type { TriggerAction } from './plugin/module/ExerciseModule';
|
|
16
16
|
export type { Message, ToolInvocation } from './plugin/module/AIModule';
|
|
17
17
|
export type { Theme, ApplicationMode } from './plugin/module/PluginModule';
|
|
18
|
-
export type { UserInfo, Language, UserRole, ExplicitUndefined, BasePluginSettings } from './plugin/module/PluginModule';
|
|
18
|
+
export type { UserInfo, Language, UserRole, SubscriptionTier, ExplicitUndefined, BasePluginSettings } from './plugin/module/PluginModule';
|
|
19
|
+
export { TIER_ORDER, ROLE_ORDER } from './plugin/module/PluginModule';
|
|
19
20
|
export type { SharedContent, BasicSharedContent, ContentStatus } from './plugin/module/SharedContentController';
|
|
20
21
|
export type { MacroAccomplishmentPayload, MicroAccomplishmentPayload } from './controller/AccomplishmentController';
|
|
21
22
|
export { StorageModule } from './plugin/module/StorageModule';
|
package/dist/index.js
CHANGED
|
@@ -10,4 +10,5 @@ export * from './plugin/CommunicationHandler';
|
|
|
10
10
|
export { setupWorker } from './worker/WorkerSetup';
|
|
11
11
|
export { AudioController } from './controller/AudioController';
|
|
12
12
|
export { Translator } from './controller/TranslationController';
|
|
13
|
+
export { TIER_ORDER, ROLE_ORDER } from './plugin/module/PluginModule';
|
|
13
14
|
export { StorageModule } from './plugin/module/StorageModule';
|
|
@@ -58,9 +58,9 @@ export class RimoriCommunicationHandler {
|
|
|
58
58
|
EventBus.emit(this.pluginId, 'error', { error }, eventId);
|
|
59
59
|
}
|
|
60
60
|
else if (event) {
|
|
61
|
-
const { topic, sender, data: eventData, eventId } = event;
|
|
61
|
+
const { topic, sender, data: eventData, eventId, ai_session_token } = event;
|
|
62
62
|
if (sender !== this.pluginId) {
|
|
63
|
-
EventBus.emit(sender, topic, eventData, eventId);
|
|
63
|
+
EventBus.emit(sender, topic, eventData, eventId, ai_session_token);
|
|
64
64
|
}
|
|
65
65
|
else {
|
|
66
66
|
console.log('[PluginController] event from self', event);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type VoiceBackend = (text: string, voice?: string, speed?: number, language?: string, cache?: boolean) => Promise<Blob>;
|
|
1
|
+
type VoiceBackend = (text: string, voice?: string, speed?: number, language?: string, cache?: boolean, instructions?: string) => Promise<Blob>;
|
|
2
2
|
export declare class MessageSender {
|
|
3
3
|
private player;
|
|
4
4
|
private fetchedSentences;
|
|
@@ -6,9 +6,12 @@ export declare class MessageSender {
|
|
|
6
6
|
private voice;
|
|
7
7
|
private voiceBackend;
|
|
8
8
|
private cache;
|
|
9
|
+
private voiceSpeed;
|
|
10
|
+
private instructions;
|
|
9
11
|
constructor(voiceBackend: VoiceBackend, voice: string, cache?: boolean);
|
|
10
12
|
private getCompletedSentences;
|
|
11
13
|
handleNewText(currentText: string | undefined, isLoading: boolean): Promise<void>;
|
|
14
|
+
setInstructions(instructions: string | undefined): void;
|
|
12
15
|
private generateSpeech;
|
|
13
16
|
play(): void;
|
|
14
17
|
stop(): void;
|
|
@@ -17,5 +20,6 @@ export declare class MessageSender {
|
|
|
17
20
|
setVolume(volume: number): void;
|
|
18
21
|
setOnLoudnessChange(callback: (value: number) => void): void;
|
|
19
22
|
setOnEndOfSpeech(callback: () => void): void;
|
|
23
|
+
setVoiceSpeed(speed: number): void;
|
|
20
24
|
}
|
|
21
25
|
export {};
|
|
@@ -13,6 +13,7 @@ export class MessageSender {
|
|
|
13
13
|
this.player = new ChunkedAudioPlayer();
|
|
14
14
|
this.fetchedSentences = new Set();
|
|
15
15
|
this.lastLoading = false;
|
|
16
|
+
this.voiceSpeed = 1;
|
|
16
17
|
if ((voice === null || voice === void 0 ? void 0 : voice.split('_').length) !== 2) {
|
|
17
18
|
throw new Error("Invalid voice id format '" + voice + "'. Voice id needs to look like <provider>_<voice_id>");
|
|
18
19
|
}
|
|
@@ -61,9 +62,12 @@ export class MessageSender {
|
|
|
61
62
|
}
|
|
62
63
|
});
|
|
63
64
|
}
|
|
65
|
+
setInstructions(instructions) {
|
|
66
|
+
this.instructions = instructions;
|
|
67
|
+
}
|
|
64
68
|
generateSpeech(sentence) {
|
|
65
69
|
return __awaiter(this, void 0, void 0, function* () {
|
|
66
|
-
const blob = yield this.voiceBackend(sentence, this.voice,
|
|
70
|
+
const blob = yield this.voiceBackend(sentence, this.voice, this.voiceSpeed, undefined, this.cache, this.instructions);
|
|
67
71
|
return yield blob.arrayBuffer();
|
|
68
72
|
});
|
|
69
73
|
}
|
|
@@ -92,4 +96,7 @@ export class MessageSender {
|
|
|
92
96
|
setOnEndOfSpeech(callback) {
|
|
93
97
|
this.player.setOnEndOfSpeech(callback);
|
|
94
98
|
}
|
|
99
|
+
setVoiceSpeed(speed) {
|
|
100
|
+
this.voiceSpeed = speed;
|
|
101
|
+
}
|
|
95
102
|
}
|
|
@@ -61,17 +61,20 @@ export declare class AIModule {
|
|
|
61
61
|
private sessionTokenId;
|
|
62
62
|
private onRateLimitedCb?;
|
|
63
63
|
constructor(backendUrl: string, getToken: () => string);
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
64
|
+
/** Exercise session management. */
|
|
65
|
+
readonly session: {
|
|
66
|
+
/** Returns the current exercise session token ID (null if no active session). */
|
|
67
|
+
get: () => string | null;
|
|
68
|
+
/** Sets the session token ID. */
|
|
69
|
+
set: (id: string) => void;
|
|
70
|
+
/** Clears the stored session token. */
|
|
71
|
+
clear: () => void;
|
|
72
|
+
/**
|
|
73
|
+
* Ensures a session token exists, creating one from the backend if needed.
|
|
74
|
+
* Mirrors the lazy-issuance pattern used by the AI/LLM endpoint.
|
|
75
|
+
*/
|
|
76
|
+
ensure: () => Promise<void>;
|
|
77
|
+
};
|
|
75
78
|
/** Registers a callback invoked whenever a 429 rate-limit response is received. */
|
|
76
79
|
setOnRateLimited(cb: (exercisesRemaining: number) => void): void;
|
|
77
80
|
/**
|
|
@@ -101,7 +104,7 @@ export declare class AIModule {
|
|
|
101
104
|
* @param cache Whether to cache the result (default: false).
|
|
102
105
|
* @returns The generated audio as a Blob.
|
|
103
106
|
*/
|
|
104
|
-
getVoice(text: string, voice?: string, speed?: number, language?: string, cache?: boolean): Promise<Blob>;
|
|
107
|
+
getVoice(text: string, voice?: string, speed?: number, language?: string, cache?: boolean, instructions?: string): Promise<Blob>;
|
|
105
108
|
/**
|
|
106
109
|
* Convert voice audio to text using AI.
|
|
107
110
|
* @param file The audio file to convert.
|
|
@@ -14,47 +14,46 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
14
14
|
export class AIModule {
|
|
15
15
|
constructor(backendUrl, getToken) {
|
|
16
16
|
this.sessionTokenId = null;
|
|
17
|
+
/** Exercise session management. */
|
|
18
|
+
this.session = {
|
|
19
|
+
/** Returns the current exercise session token ID (null if no active session). */
|
|
20
|
+
get: () => this.sessionTokenId,
|
|
21
|
+
/** Sets the session token ID. */
|
|
22
|
+
set: (id) => {
|
|
23
|
+
this.sessionTokenId = id;
|
|
24
|
+
},
|
|
25
|
+
/** Clears the stored session token. */
|
|
26
|
+
clear: () => {
|
|
27
|
+
this.sessionTokenId = null;
|
|
28
|
+
},
|
|
29
|
+
/**
|
|
30
|
+
* Ensures a session token exists, creating one from the backend if needed.
|
|
31
|
+
* Mirrors the lazy-issuance pattern used by the AI/LLM endpoint.
|
|
32
|
+
*/
|
|
33
|
+
ensure: () => __awaiter(this, void 0, void 0, function* () {
|
|
34
|
+
var _a, _b, _c;
|
|
35
|
+
if (this.sessionTokenId)
|
|
36
|
+
return;
|
|
37
|
+
const response = yield fetch(`${this.backendUrl}/ai/session`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { Authorization: `Bearer ${this.getToken()}` },
|
|
40
|
+
});
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
if (response.status === 429) {
|
|
43
|
+
const body = yield response.json().catch(() => ({}));
|
|
44
|
+
const remaining = (_a = body.exercises_remaining) !== null && _a !== void 0 ? _a : 0;
|
|
45
|
+
(_b = this.onRateLimitedCb) === null || _b === void 0 ? void 0 : _b.call(this, remaining);
|
|
46
|
+
throw new Error(`Rate limit exceeded: ${(_c = body.error) !== null && _c !== void 0 ? _c : 'Daily exercise limit reached'}. exercises_remaining: ${remaining}`);
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`Failed to create session: ${response.status} ${response.statusText}`);
|
|
49
|
+
}
|
|
50
|
+
const { session_token_id } = yield response.json();
|
|
51
|
+
this.sessionTokenId = session_token_id;
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
17
54
|
this.backendUrl = backendUrl;
|
|
18
55
|
this.getToken = getToken;
|
|
19
56
|
}
|
|
20
|
-
/** Returns the current exercise session token ID (null if no active session). */
|
|
21
|
-
getSessionTokenId() {
|
|
22
|
-
return this.sessionTokenId;
|
|
23
|
-
}
|
|
24
|
-
/** Sets the session token ID (used by workers inheriting a session from the plugin). */
|
|
25
|
-
setSessionToken(id) {
|
|
26
|
-
this.sessionTokenId = id;
|
|
27
|
-
}
|
|
28
|
-
/** Clears the stored session token (called after macro accomplishment). */
|
|
29
|
-
clearSessionToken() {
|
|
30
|
-
this.sessionTokenId = null;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Ensures a session token exists, requesting one from the backend if needed.
|
|
34
|
-
* Mirrors the lazy-issuance pattern used by the AI/LLM endpoint.
|
|
35
|
-
*/
|
|
36
|
-
ensureSessionToken() {
|
|
37
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
38
|
-
var _a, _b, _c;
|
|
39
|
-
if (this.sessionTokenId)
|
|
40
|
-
return;
|
|
41
|
-
const response = yield fetch(`${this.backendUrl}/ai/session`, {
|
|
42
|
-
method: 'POST',
|
|
43
|
-
headers: { Authorization: `Bearer ${this.getToken()}` },
|
|
44
|
-
});
|
|
45
|
-
if (!response.ok) {
|
|
46
|
-
if (response.status === 429) {
|
|
47
|
-
const body = yield response.json().catch(() => ({}));
|
|
48
|
-
const remaining = (_a = body.exercises_remaining) !== null && _a !== void 0 ? _a : 0;
|
|
49
|
-
(_b = this.onRateLimitedCb) === null || _b === void 0 ? void 0 : _b.call(this, remaining);
|
|
50
|
-
throw new Error(`Rate limit exceeded: ${(_c = body.error) !== null && _c !== void 0 ? _c : 'Daily exercise limit reached'}. exercises_remaining: ${remaining}`);
|
|
51
|
-
}
|
|
52
|
-
throw new Error(`Failed to create session: ${response.status} ${response.statusText}`);
|
|
53
|
-
}
|
|
54
|
-
const { session_token_id } = yield response.json();
|
|
55
|
-
this.sessionTokenId = session_token_id;
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
57
|
/** Registers a callback invoked whenever a 429 rate-limit response is received. */
|
|
59
58
|
setOnRateLimited(cb) {
|
|
60
59
|
this.onRateLimitedCb = cb;
|
|
@@ -121,16 +120,16 @@ export class AIModule {
|
|
|
121
120
|
* @returns The generated audio as a Blob.
|
|
122
121
|
*/
|
|
123
122
|
getVoice(text_1) {
|
|
124
|
-
return __awaiter(this, arguments, void 0, function* (text, voice = 'alloy', speed = 1, language, cache = false) {
|
|
123
|
+
return __awaiter(this, arguments, void 0, function* (text, voice = 'alloy', speed = 1, language, cache = false, instructions) {
|
|
125
124
|
var _a;
|
|
126
|
-
yield this.
|
|
125
|
+
yield this.session.ensure();
|
|
127
126
|
return yield fetch(`${this.backendUrl}/voice/tts`, {
|
|
128
127
|
method: 'POST',
|
|
129
128
|
headers: {
|
|
130
129
|
'Content-Type': 'application/json',
|
|
131
130
|
Authorization: `Bearer ${this.getToken()}`,
|
|
132
131
|
},
|
|
133
|
-
body: JSON.stringify({ input: text, voice, speed, language, cache, session_token_id: (_a = this.sessionTokenId) !== null && _a !== void 0 ? _a : undefined }),
|
|
132
|
+
body: JSON.stringify({ input: text, voice, speed, language, cache, instructions, session_token_id: (_a = this.sessionTokenId) !== null && _a !== void 0 ? _a : undefined }),
|
|
134
133
|
}).then((r) => r.blob());
|
|
135
134
|
});
|
|
136
135
|
}
|
|
@@ -142,7 +141,7 @@ export class AIModule {
|
|
|
142
141
|
*/
|
|
143
142
|
getTextFromVoice(file, language) {
|
|
144
143
|
return __awaiter(this, void 0, void 0, function* () {
|
|
145
|
-
yield this.
|
|
144
|
+
yield this.session.ensure();
|
|
146
145
|
const formData = new FormData();
|
|
147
146
|
formData.append('file', file);
|
|
148
147
|
if (language) {
|
|
@@ -222,7 +221,7 @@ export class AIModule {
|
|
|
222
221
|
}
|
|
223
222
|
streamObject(params) {
|
|
224
223
|
return __awaiter(this, void 0, void 0, function* () {
|
|
225
|
-
var _a, _b, _c, _d;
|
|
224
|
+
var _a, _b, _c, _d, _e;
|
|
226
225
|
const { messages, responseSchema, onResult = () => null, cache = false, tools = [], model = undefined, knowledgeId, } = params;
|
|
227
226
|
const chatMessages = messages.map((message, index) => (Object.assign(Object.assign({}, message), { id: `${index + 1}` })));
|
|
228
227
|
const response = yield fetch(`${this.backendUrl}/ai/llm`, {
|
|
@@ -254,6 +253,18 @@ export class AIModule {
|
|
|
254
253
|
const reader = response.body.getReader();
|
|
255
254
|
const decoder = new TextDecoder('utf-8');
|
|
256
255
|
let currentObject = {};
|
|
256
|
+
// Buffer for SSE lines that are split across network chunks.
|
|
257
|
+
// TCP/IP does not guarantee that each `read()` call delivers a complete
|
|
258
|
+
// logical line. For example, the `token:` line carrying the session token
|
|
259
|
+
// may arrive as two separate chunks:
|
|
260
|
+
// chunk 1 → `token: {"token_id":`
|
|
261
|
+
// chunk 2 → `"abc123"}\n`
|
|
262
|
+
// Without buffering, `JSON.parse` would throw on the partial line and the
|
|
263
|
+
// session token would be silently discarded, causing the next LLM call to
|
|
264
|
+
// start without a session (triggering an unnecessary extra round-trip via
|
|
265
|
+
// `session.ensure()`). By keeping the incomplete tail in `lineBuffer` and
|
|
266
|
+
// prepending it to the next chunk we always process whole lines.
|
|
267
|
+
let lineBuffer = '';
|
|
257
268
|
let isLoading = true;
|
|
258
269
|
while (isLoading) {
|
|
259
270
|
//wait 50ms to not overload the CPU
|
|
@@ -268,7 +279,13 @@ export class AIModule {
|
|
|
268
279
|
if (!value)
|
|
269
280
|
continue;
|
|
270
281
|
const chunk = decoder.decode(value, { stream: true });
|
|
271
|
-
|
|
282
|
+
// Prepend any incomplete line left over from the previous chunk, then
|
|
283
|
+
// split on newlines. `parts.pop()` removes (and saves) the last element
|
|
284
|
+
// which may be an incomplete line if the chunk did not end with '\n'.
|
|
285
|
+
const combined = lineBuffer + chunk;
|
|
286
|
+
const parts = combined.split('\n');
|
|
287
|
+
lineBuffer = (_e = parts.pop()) !== null && _e !== void 0 ? _e : '';
|
|
288
|
+
const lines = parts.filter((line) => line.trim());
|
|
272
289
|
for (const line of lines) {
|
|
273
290
|
// Handle token: line (session token issued by backend on first AI call)
|
|
274
291
|
if (line.startsWith('token:')) {
|
|
@@ -278,7 +295,7 @@ export class AIModule {
|
|
|
278
295
|
this.sessionTokenId = tokenData.token_id;
|
|
279
296
|
}
|
|
280
297
|
}
|
|
281
|
-
catch (
|
|
298
|
+
catch (_f) {
|
|
282
299
|
console.error('Failed to parse token: line', line);
|
|
283
300
|
}
|
|
284
301
|
continue;
|
|
@@ -1,7 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PostgrestFilterBuilder, PostgrestQueryBuilder } from '@supabase/postgrest-js';
|
|
2
2
|
import { SupabaseClient } from '../CommunicationHandler';
|
|
3
3
|
import { RimoriCommunicationHandler, RimoriInfo } from '../CommunicationHandler';
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Wraps PostgrestQueryBuilder and overrides select() to always return Row[].
|
|
6
|
+
*
|
|
7
|
+
* postgrest-js's GetResult type can only infer row types from string literals.
|
|
8
|
+
* Dynamic select strings (e.g. `'col:' + getTableName('other') + '(id)'`) produce
|
|
9
|
+
* GenericStringError. This wrapper forces select() to return Row[] regardless of
|
|
10
|
+
* the query string, so callers using from<Row>() always get typed results.
|
|
11
|
+
*/
|
|
12
|
+
type DbQueryBuilder<Row extends Record<string, unknown>> = Omit<PostgrestQueryBuilder<any, any, {
|
|
13
|
+
Row: Row;
|
|
14
|
+
Insert: Partial<Row>;
|
|
15
|
+
Update: Partial<Row>;
|
|
16
|
+
Relationships: [];
|
|
17
|
+
}, string>, 'select'> & {
|
|
18
|
+
select(columns?: string, options?: {
|
|
19
|
+
head?: boolean;
|
|
20
|
+
count?: 'exact' | 'planned' | 'estimated';
|
|
21
|
+
}): PostgrestFilterBuilder<any, any, Row, Row[], string, any, 'GET'>;
|
|
22
|
+
};
|
|
5
23
|
/**
|
|
6
24
|
* Database module for plugin database operations.
|
|
7
25
|
* Provides access to plugin tables with automatic prefixing and schema management.
|
|
@@ -16,10 +34,16 @@ export declare class DbModule {
|
|
|
16
34
|
* Query a database table.
|
|
17
35
|
* Global tables (starting with 'global_') remain in public schema.
|
|
18
36
|
* Plugin tables use the schema provided by rimori-main (plugins or plugins_alpha).
|
|
37
|
+
*
|
|
38
|
+
* The generic parameter `Row` lets callers opt-in to typed row access:
|
|
39
|
+
* client.db.from<{ id: string; name: string }>('decks')
|
|
40
|
+
* When omitted, row fields are inferred from the select() string (each field typed as `any`).
|
|
41
|
+
* Works with both literal and dynamic select strings.
|
|
42
|
+
*
|
|
19
43
|
* @param relation The table name (without prefix for plugin tables, with 'global_' for global tables).
|
|
20
44
|
* @returns A Postgrest query builder for the table.
|
|
21
45
|
*/
|
|
22
|
-
from<
|
|
46
|
+
from<Row extends Record<string, unknown> = any>(relation: string): DbQueryBuilder<Row>;
|
|
23
47
|
/**
|
|
24
48
|
* Get the table name for a given plugin table.
|
|
25
49
|
* Internally all tables are prefixed with the plugin id. This function is used to get the correct table name for a given public table.
|
|
@@ -28,3 +52,4 @@ export declare class DbModule {
|
|
|
28
52
|
*/
|
|
29
53
|
getTableName(table: string): string;
|
|
30
54
|
}
|
|
55
|
+
export {};
|
|
@@ -18,19 +18,20 @@ export class DbModule {
|
|
|
18
18
|
* Query a database table.
|
|
19
19
|
* Global tables (starting with 'global_') remain in public schema.
|
|
20
20
|
* Plugin tables use the schema provided by rimori-main (plugins or plugins_alpha).
|
|
21
|
+
*
|
|
22
|
+
* The generic parameter `Row` lets callers opt-in to typed row access:
|
|
23
|
+
* client.db.from<{ id: string; name: string }>('decks')
|
|
24
|
+
* When omitted, row fields are inferred from the select() string (each field typed as `any`).
|
|
25
|
+
* Works with both literal and dynamic select strings.
|
|
26
|
+
*
|
|
21
27
|
* @param relation The table name (without prefix for plugin tables, with 'global_' for global tables).
|
|
22
28
|
* @returns A Postgrest query builder for the table.
|
|
23
29
|
*/
|
|
24
30
|
from(relation) {
|
|
25
31
|
const tableName = this.getTableName(relation);
|
|
26
|
-
// Use the schema determined by rimori-main based on release channel
|
|
27
|
-
// Global tables (starting with 'global_') remain in public schema
|
|
28
|
-
// Plugin tables use the schema provided by rimori-main (plugins or plugins_alpha)
|
|
29
32
|
if (relation.startsWith('global_')) {
|
|
30
|
-
// Global tables stay in public schema
|
|
31
33
|
return this.supabase.schema('public').from(tableName);
|
|
32
34
|
}
|
|
33
|
-
// Plugin tables go to the schema provided by rimori-main
|
|
34
35
|
return this.supabase.schema(this.rimoriInfo.dbSchema).from(tableName);
|
|
35
36
|
}
|
|
36
37
|
/**
|
|
@@ -63,8 +63,13 @@ export class EventModule {
|
|
|
63
63
|
* @returns The response from the event.
|
|
64
64
|
*/
|
|
65
65
|
request(topic, data) {
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
67
|
+
var _a;
|
|
68
|
+
const globalTopic = this.getGlobalEventTopic(topic);
|
|
69
|
+
yield this.aiModule.session.ensure();
|
|
70
|
+
const sessionToken = (_a = this.aiModule.session.get()) !== null && _a !== void 0 ? _a : undefined;
|
|
71
|
+
return EventBus.request(this.pluginId, globalTopic, data, sessionToken);
|
|
72
|
+
});
|
|
68
73
|
}
|
|
69
74
|
/**
|
|
70
75
|
* Subscribe to an event.
|
|
@@ -74,7 +79,12 @@ export class EventModule {
|
|
|
74
79
|
*/
|
|
75
80
|
on(topic, callback) {
|
|
76
81
|
const topics = Array.isArray(topic) ? topic : [topic];
|
|
77
|
-
return EventBus.on(topics.map((t) => this.getGlobalEventTopic(t)),
|
|
82
|
+
return EventBus.on(topics.map((t) => this.getGlobalEventTopic(t)), (event) => {
|
|
83
|
+
if (event.ai_session_token && !this.aiModule.session.get()) {
|
|
84
|
+
this.aiModule.session.set(event.ai_session_token);
|
|
85
|
+
}
|
|
86
|
+
callback(event);
|
|
87
|
+
});
|
|
78
88
|
}
|
|
79
89
|
/**
|
|
80
90
|
* Subscribe to an event once.
|
|
@@ -91,7 +101,30 @@ export class EventModule {
|
|
|
91
101
|
*/
|
|
92
102
|
respond(topic, data) {
|
|
93
103
|
const topics = Array.isArray(topic) ? topic : [topic];
|
|
94
|
-
|
|
104
|
+
let wrappedData = data;
|
|
105
|
+
if (typeof data === 'function') {
|
|
106
|
+
wrappedData = (event) => __awaiter(this, void 0, void 0, function* () {
|
|
107
|
+
const previousToken = this.aiModule.session.get();
|
|
108
|
+
if (event.ai_session_token) {
|
|
109
|
+
this.aiModule.session.set(event.ai_session_token);
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
console.log('responding to event', event);
|
|
113
|
+
return yield data(event);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
if (event.ai_session_token) {
|
|
117
|
+
if (previousToken) {
|
|
118
|
+
this.aiModule.session.set(previousToken);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
this.aiModule.session.clear();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
EventBus.respond(this.pluginId, topics.map((t) => this.getGlobalEventTopic(t)), wrappedData);
|
|
95
128
|
}
|
|
96
129
|
/**
|
|
97
130
|
* Emit an accomplishment.
|
|
@@ -100,7 +133,7 @@ export class EventModule {
|
|
|
100
133
|
emitAccomplishment(payload) {
|
|
101
134
|
return __awaiter(this, void 0, void 0, function* () {
|
|
102
135
|
if (payload.type === 'macro') {
|
|
103
|
-
const sessionId = this.aiModule.
|
|
136
|
+
const sessionId = this.aiModule.session.get();
|
|
104
137
|
if (sessionId) {
|
|
105
138
|
try {
|
|
106
139
|
yield fetch(`${this.backendUrl}/ai/session/${sessionId}/complete`, {
|
|
@@ -111,7 +144,7 @@ export class EventModule {
|
|
|
111
144
|
catch (_a) {
|
|
112
145
|
// non-fatal — session will expire naturally
|
|
113
146
|
}
|
|
114
|
-
this.aiModule.
|
|
147
|
+
this.aiModule.session.clear();
|
|
115
148
|
}
|
|
116
149
|
}
|
|
117
150
|
this.accomplishmentController.emitAccomplishment(payload);
|
|
@@ -105,6 +105,11 @@ export interface Language {
|
|
|
105
105
|
uppercase: string;
|
|
106
106
|
}
|
|
107
107
|
export type UserRole = 'user' | 'plugin_moderator' | 'lang_moderator' | 'admin';
|
|
108
|
+
export type SubscriptionTier = 'anonymous' | 'free' | 'standard' | 'premium' | 'early_access';
|
|
109
|
+
/** Ordered tiers from lowest to highest access level */
|
|
110
|
+
export declare const TIER_ORDER: SubscriptionTier[];
|
|
111
|
+
/** Ordered roles from lowest to highest access level */
|
|
112
|
+
export declare const ROLE_ORDER: UserRole[];
|
|
108
113
|
export declare const LEARNING_REASONS: readonly ["work", "partner", "friends", "study", "living", "culture", "growth", "citizenship", "other"];
|
|
109
114
|
export type LearningReason = (typeof LEARNING_REASONS)[number];
|
|
110
115
|
export type ExplicitUndefined<T> = {
|
|
@@ -161,4 +166,16 @@ export interface UserInfo {
|
|
|
161
166
|
* The user's role: 'user', 'plugin_moderator', 'lang_moderator', or 'admin'
|
|
162
167
|
*/
|
|
163
168
|
user_role: UserRole;
|
|
169
|
+
/**
|
|
170
|
+
* The user's subscription tier. Higher tiers include all privileges of lower tiers.
|
|
171
|
+
* Order (ascending): anonymous < free < standard < premium < early_access
|
|
172
|
+
*/
|
|
173
|
+
subscription_tier: SubscriptionTier;
|
|
174
|
+
/**
|
|
175
|
+
* Regional dialect name for the user's target city (e.g. "Skånska", "Göteborgska").
|
|
176
|
+
* Only set when: subscription_tier >= premium AND skill_level_listening >= B1
|
|
177
|
+
* AND dialect_enabled = true AND city has a dialect in DB.
|
|
178
|
+
* Undefined otherwise. Plugins decide how to use this on a per-feature basis.
|
|
179
|
+
*/
|
|
180
|
+
dialect?: string;
|
|
164
181
|
}
|
|
@@ -178,6 +178,10 @@ export class PluginModule {
|
|
|
178
178
|
});
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
|
+
/** Ordered tiers from lowest to highest access level */
|
|
182
|
+
export const TIER_ORDER = ['anonymous', 'free', 'standard', 'premium', 'early_access'];
|
|
183
|
+
/** Ordered roles from lowest to highest access level */
|
|
184
|
+
export const ROLE_ORDER = ['user', 'plugin_moderator', 'lang_moderator', 'admin'];
|
|
181
185
|
export const LEARNING_REASONS = [
|
|
182
186
|
'work',
|
|
183
187
|
'partner',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rimori/client",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.20-next.1",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"repository": {
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"format": "prettier --write ."
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@supabase/postgrest-js": "^2.
|
|
40
|
-
"dotenv": "16.5.0",
|
|
41
|
-
"i18next": "^25.
|
|
39
|
+
"@supabase/postgrest-js": "^2.98.0",
|
|
40
|
+
"dotenv": "^16.5.0",
|
|
41
|
+
"i18next": "^25.8.14"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@eslint/js": "^9.37.0",
|