fix: resolve architecture disconnects and codebase weaknesses
- Standardize MessageRecord types — single source of truth from @bete/shared - Clean up config: remove unused GUILD_ID/TEXT_GUILD_ID/TEXT_CHANNEL_ID, fix WEBSERVER_PORT default (3001), remove default admin password - Move mascot_chat_messages table to Drizzle schema with proper migration - Remove runtime DDL (CREATE TABLE IF NOT EXISTS) from mascot-chat repository - Remove phantom analytics/ module from documentation - Add better-sqlite3 dependency to root devDependencies - Replace 'as any' casts with proper type assertions across AI moderation - Add error logging to silent catch blocks in LLM client - Apply Biome formatting and import organization Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
67d66bb5dd
commit
3614d32701
@@ -111,7 +111,6 @@ Express 5 + Helmet HTTP server with WebSocket (ws) on port 3001 (default).
|
|||||||
- `recordings/` — Voice recording listing
|
- `recordings/` — Voice recording listing
|
||||||
- `ui-state/` — Persistent UI state for dashboard
|
- `ui-state/` — Persistent UI state for dashboard
|
||||||
- `config/` — App config endpoint
|
- `config/` — App config endpoint
|
||||||
- `analytics/` — Analytics (schema defined)
|
|
||||||
|
|
||||||
**WebSocket events (outbound to frontend):**
|
**WebSocket events (outbound to frontend):**
|
||||||
- `message_created`, `message_updated`, `message_deleted`, `message_analyzed`
|
- `message_created`, `message_updated`, `message_deleted`, `message_analyzed`
|
||||||
@@ -222,7 +221,6 @@ React 19 + Vite 8 + Tailwind CSS 4 + TypeScript dashboard.
|
|||||||
- Hooks: `useVoiceControl`, `useMediaControl`
|
- Hooks: `useVoiceControl`, `useMediaControl`
|
||||||
- `messages/` — Message list with filters
|
- `messages/` — Message list with filters
|
||||||
- Hooks: `useMessages`
|
- Hooks: `useMessages`
|
||||||
- `analytics/` — Analytics (hook scaffolded)
|
|
||||||
- `shared/`
|
- `shared/`
|
||||||
- `api/client.ts` — All HTTP API calls + types
|
- `api/client.ts` — All HTTP API calls + types
|
||||||
- `ws/socket.ts` — WebSocket singleton with `useDashboardSocket` hook
|
- `ws/socket.ts` — WebSocket singleton with `useDashboardSocket` hook
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "latest",
|
"@biomejs/biome": "latest",
|
||||||
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
|
"better-sqlite3": "^11.9.1",
|
||||||
"drizzle-kit": "^0.31.10",
|
"drizzle-kit": "^0.31.10",
|
||||||
"tsx": "^4.22.2",
|
"tsx": "^4.22.2",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
* Individual services re-export from here; they do NOT define their own schemas.
|
* Individual services re-export from here; they do NOT define their own schemas.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ConfigError } from "../errors/index.js";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { ConfigError } from "../errors/index.js";
|
||||||
|
|
||||||
export const configSchema = z
|
export const configSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -18,14 +18,9 @@ export const configSchema = z
|
|||||||
MONITOR_GUILD_ID: z.string().min(1).optional(),
|
MONITOR_GUILD_ID: z.string().min(1).optional(),
|
||||||
|
|
||||||
// ── Legacy voice ─────────────────────────────────────────────────────
|
// ── Legacy voice ─────────────────────────────────────────────────────
|
||||||
GUILD_ID: z.string().min(1).optional(),
|
|
||||||
VOICE_GUILD_ID: z.string().min(1).optional(),
|
VOICE_GUILD_ID: z.string().min(1).optional(),
|
||||||
VOICE_CHANNEL_ID: z.string().min(1).optional(),
|
VOICE_CHANNEL_ID: z.string().min(1).optional(),
|
||||||
|
|
||||||
// ── Text capture legacy ──────────────────────────────────────────────
|
|
||||||
TEXT_GUILD_ID: z.string().min(1).optional(),
|
|
||||||
TEXT_CHANNEL_ID: z.string().min(1).optional(),
|
|
||||||
|
|
||||||
// ── Recording ────────────────────────────────────────────────────────
|
// ── Recording ────────────────────────────────────────────────────────
|
||||||
RECORDINGS_DIR: z.string().default("./recordings"),
|
RECORDINGS_DIR: z.string().default("./recordings"),
|
||||||
RECORDING_SEGMENT_MS: z.coerce.number().positive().default(5000),
|
RECORDING_SEGMENT_MS: z.coerce.number().positive().default(5000),
|
||||||
@@ -35,7 +30,10 @@ export const configSchema = z
|
|||||||
DECODER_COOLDOWN_MS: z.coerce.number().positive().default(30000),
|
DECODER_COOLDOWN_MS: z.coerce.number().positive().default(30000),
|
||||||
|
|
||||||
// ── Audio ────────────────────────────────────────────────────────────
|
// ── Audio ────────────────────────────────────────────────────────────
|
||||||
AUDIO_STREAM_SILENCE_DURATION_MS: z.coerce.number().positive().default(3000),
|
AUDIO_STREAM_SILENCE_DURATION_MS: z.coerce
|
||||||
|
.number()
|
||||||
|
.positive()
|
||||||
|
.default(3000),
|
||||||
PACKET_FILTER_MIN_SIZE: z.coerce.number().positive().default(8),
|
PACKET_FILTER_MIN_SIZE: z.coerce.number().positive().default(8),
|
||||||
OPUS_FRAME_SIZE: z.coerce.number().positive().default(960),
|
OPUS_FRAME_SIZE: z.coerce.number().positive().default(960),
|
||||||
AUDIO_SAMPLE_RATE: z.coerce.number().positive().default(48000),
|
AUDIO_SAMPLE_RATE: z.coerce.number().positive().default(48000),
|
||||||
@@ -43,7 +41,7 @@ export const configSchema = z
|
|||||||
AVATAR_SIZE: z.coerce.number().positive().default(64),
|
AVATAR_SIZE: z.coerce.number().positive().default(64),
|
||||||
|
|
||||||
// ── Server ───────────────────────────────────────────────────────────
|
// ── Server ───────────────────────────────────────────────────────────
|
||||||
WEBSERVER_PORT: z.coerce.number().positive().default(3000),
|
WEBSERVER_PORT: z.coerce.number().positive().default(3001),
|
||||||
NODE_ENV: z
|
NODE_ENV: z
|
||||||
.enum(["development", "production", "test"])
|
.enum(["development", "production", "test"])
|
||||||
.default("development"),
|
.default("development"),
|
||||||
@@ -55,7 +53,7 @@ export const configSchema = z
|
|||||||
.optional()
|
.optional()
|
||||||
.transform((v) => v === "true")
|
.transform((v) => v === "true")
|
||||||
.default(false),
|
.default(false),
|
||||||
ADMIN_PASSWORD: z.string().default("admin123"),
|
ADMIN_PASSWORD: z.string(),
|
||||||
|
|
||||||
// ── Database (PostgreSQL) ────────────────────────────────────────────
|
// ── Database (PostgreSQL) ────────────────────────────────────────────
|
||||||
DATABASE_URL: z.string().optional(),
|
DATABASE_URL: z.string().optional(),
|
||||||
@@ -104,7 +102,11 @@ export const configSchema = z
|
|||||||
AI_LLM_MODEL: z.string().default("text"),
|
AI_LLM_MODEL: z.string().default("text"),
|
||||||
AI_LLM_VISION_MODEL: z.string().optional(),
|
AI_LLM_VISION_MODEL: z.string().optional(),
|
||||||
AI_LLM_MAX_CONCURRENT: z.coerce.number().int().positive().default(5),
|
AI_LLM_MAX_CONCURRENT: z.coerce.number().int().positive().default(5),
|
||||||
AI_LLM_IMAGE_MAX_DIMENSION: z.coerce.number().int().positive().default(1024),
|
AI_LLM_IMAGE_MAX_DIMENSION: z.coerce
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.default(1024),
|
||||||
AI_LLM_TEXT_BATCH_SIZE: z.coerce.number().int().positive().default(20),
|
AI_LLM_TEXT_BATCH_SIZE: z.coerce.number().int().positive().default(20),
|
||||||
AI_LLM_MEDIA_ANALYSIS_TIMEOUT_MS: z.coerce
|
AI_LLM_MEDIA_ANALYSIS_TIMEOUT_MS: z.coerce
|
||||||
.number()
|
.number()
|
||||||
@@ -114,14 +116,21 @@ export const configSchema = z
|
|||||||
|
|
||||||
// ── AI Analysis Timing ──────────────────────────────────────────────
|
// ── AI Analysis Timing ──────────────────────────────────────────────
|
||||||
AI_ANALYSIS_DEBOUNCE_MS: z.coerce.number().positive().default(500),
|
AI_ANALYSIS_DEBOUNCE_MS: z.coerce.number().positive().default(500),
|
||||||
AI_ANALYSIS_RECOVERY_INTERVAL_MS: z.coerce.number().positive().default(15000),
|
AI_ANALYSIS_RECOVERY_INTERVAL_MS: z.coerce
|
||||||
|
.number()
|
||||||
|
.positive()
|
||||||
|
.default(15000),
|
||||||
AI_ANALYSIS_ERROR_COOLDOWN_MS: z.coerce.number().positive().default(30000),
|
AI_ANALYSIS_ERROR_COOLDOWN_MS: z.coerce.number().positive().default(30000),
|
||||||
|
|
||||||
// ── AI Analysis Batch ───────────────────────────────────────────────
|
// ── AI Analysis Batch ───────────────────────────────────────────────
|
||||||
AI_ANALYSIS_MAX_BATCH_SIZE: z.coerce.number().int().positive().default(200),
|
AI_ANALYSIS_MAX_BATCH_SIZE: z.coerce.number().int().positive().default(200),
|
||||||
AI_ANALYSIS_MAX_CONTEXT_TOKENS: z.coerce.number().positive().default(8000),
|
AI_ANALYSIS_MAX_CONTEXT_TOKENS: z.coerce.number().positive().default(8000),
|
||||||
AI_ANALYSIS_MAX_TARGET_TOKENS: z.coerce.number().positive().default(4000),
|
AI_ANALYSIS_MAX_TARGET_TOKENS: z.coerce.number().positive().default(4000),
|
||||||
AI_ANALYSIS_CONTEXT_MESSAGE_LIMIT: z.coerce.number().int().positive().default(20),
|
AI_ANALYSIS_CONTEXT_MESSAGE_LIMIT: z.coerce
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.default(20),
|
||||||
AI_ANALYSIS_PROCESSING_TIMEOUT_MS: z.coerce
|
AI_ANALYSIS_PROCESSING_TIMEOUT_MS: z.coerce
|
||||||
.number()
|
.number()
|
||||||
.positive()
|
.positive()
|
||||||
@@ -159,7 +168,9 @@ export const configSchema = z
|
|||||||
.default(false),
|
.default(false),
|
||||||
AUTO_DELETE_FLAGGED_DELAY_MS: z.coerce.number().min(0).default(0),
|
AUTO_DELETE_FLAGGED_DELAY_MS: z.coerce.number().min(0).default(0),
|
||||||
AUTO_DELETE_MIN_CONFIDENCE: z.coerce.number().min(0).max(1).default(0.5),
|
AUTO_DELETE_MIN_CONFIDENCE: z.coerce.number().min(0).max(1).default(0.5),
|
||||||
AUTO_DELETE_ALLOWED_SEVERITIES: z.string().default("critical,high,medium,low"),
|
AUTO_DELETE_ALLOWED_SEVERITIES: z
|
||||||
|
.string()
|
||||||
|
.default("critical,high,medium,low"),
|
||||||
AUTO_DELETE_ALLOWED_CATEGORIES: z.string().default(""),
|
AUTO_DELETE_ALLOWED_CATEGORIES: z.string().default(""),
|
||||||
AUTO_DELETE_EXCLUDED_CHANNEL_IDS: z.string().default(""),
|
AUTO_DELETE_EXCLUDED_CHANNEL_IDS: z.string().default(""),
|
||||||
AUTO_DELETE_EXCLUDED_USER_IDS: z.string().default(""),
|
AUTO_DELETE_EXCLUDED_USER_IDS: z.string().default(""),
|
||||||
@@ -215,15 +226,13 @@ export type AppConfig = z.infer<typeof configSchema> & {
|
|||||||
EFFECTIVE_VOICE_GUILD_ID?: string;
|
EFFECTIVE_VOICE_GUILD_ID?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function loadConfig(
|
export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
|
||||||
env: NodeJS.ProcessEnv = process.env,
|
|
||||||
): AppConfig {
|
|
||||||
try {
|
try {
|
||||||
const parsed = configSchema.parse(env);
|
const parsed = configSchema.parse(env);
|
||||||
return {
|
return {
|
||||||
...parsed,
|
...parsed,
|
||||||
EFFECTIVE_TEXT_GUILD_ID: parsed.MONITOR_GUILD_ID,
|
EFFECTIVE_TEXT_GUILD_ID: parsed.MONITOR_GUILD_ID,
|
||||||
EFFECTIVE_VOICE_GUILD_ID: parsed.VOICE_GUILD_ID ?? parsed.GUILD_ID,
|
EFFECTIVE_VOICE_GUILD_ID: parsed.VOICE_GUILD_ID,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
export * from "./config/index.js";
|
||||||
export * from "./errors/index.js";
|
export * from "./errors/index.js";
|
||||||
export * from "./logger/index.js";
|
export * from "./logger/index.js";
|
||||||
export * from "./utils/index.js";
|
|
||||||
export * from "./moderation-types.js";
|
export * from "./moderation-types.js";
|
||||||
export * from "./config/index.js";
|
export * from "./utils/index.js";
|
||||||
|
|||||||
Generated
+41
-3
@@ -15,6 +15,12 @@ importers:
|
|||||||
'@biomejs/biome':
|
'@biomejs/biome':
|
||||||
specifier: latest
|
specifier: latest
|
||||||
version: 2.4.16
|
version: 2.4.16
|
||||||
|
'@types/better-sqlite3':
|
||||||
|
specifier: ^7.6.13
|
||||||
|
version: 7.6.13
|
||||||
|
better-sqlite3:
|
||||||
|
specifier: ^11.9.1
|
||||||
|
version: 11.10.0
|
||||||
drizzle-kit:
|
drizzle-kit:
|
||||||
specifier: ^0.31.10
|
specifier: ^0.31.10
|
||||||
version: 0.31.10
|
version: 0.31.10
|
||||||
@@ -60,7 +66,7 @@ importers:
|
|||||||
version: 17.4.2
|
version: 17.4.2
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.45.2
|
specifier: ^0.45.2
|
||||||
version: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)
|
version: 0.45.2(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@11.10.0)(pg@8.21.0)
|
||||||
express:
|
express:
|
||||||
specifier: ^5.2.1
|
specifier: ^5.2.1
|
||||||
version: 5.2.1
|
version: 5.2.1
|
||||||
@@ -136,7 +142,7 @@ importers:
|
|||||||
version: 17.4.2
|
version: 17.4.2
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.45.2
|
specifier: ^0.45.2
|
||||||
version: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)
|
version: 0.45.2(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@11.10.0)(pg@8.21.0)
|
||||||
imghash:
|
imghash:
|
||||||
specifier: ^1.1.4
|
specifier: ^1.1.4
|
||||||
version: 1.1.4
|
version: 1.1.4
|
||||||
@@ -216,6 +222,9 @@ importers:
|
|||||||
|
|
||||||
services/frontend:
|
services/frontend:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@bete/shared':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/shared
|
||||||
'@radix-ui/react-scroll-area':
|
'@radix-ui/react-scroll-area':
|
||||||
specifier: ^1.2.10
|
specifier: ^1.2.10
|
||||||
version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||||
@@ -2180,6 +2189,9 @@ packages:
|
|||||||
'@tybys/wasm-util@0.10.2':
|
'@tybys/wasm-util@0.10.2':
|
||||||
resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
|
resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
|
||||||
|
|
||||||
|
'@types/better-sqlite3@7.6.13':
|
||||||
|
resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==}
|
||||||
|
|
||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||||
|
|
||||||
@@ -2487,9 +2499,15 @@ packages:
|
|||||||
before-after-hook@2.2.3:
|
before-after-hook@2.2.3:
|
||||||
resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
|
resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
|
||||||
|
|
||||||
|
better-sqlite3@11.10.0:
|
||||||
|
resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==}
|
||||||
|
|
||||||
bidi-js@1.0.3:
|
bidi-js@1.0.3:
|
||||||
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
|
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
|
||||||
|
|
||||||
|
bindings@1.5.0:
|
||||||
|
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
||||||
|
|
||||||
bintrees@1.0.2:
|
bintrees@1.0.2:
|
||||||
resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==}
|
resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==}
|
||||||
|
|
||||||
@@ -3191,6 +3209,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw==}
|
resolution: {integrity: sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
file-uri-to-path@1.0.0:
|
||||||
|
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
||||||
|
|
||||||
fill-range@7.1.1:
|
fill-range@7.1.1:
|
||||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -6879,6 +6900,10 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@types/better-sqlite3@7.6.13':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 25.9.0
|
||||||
|
|
||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
@@ -7185,10 +7210,19 @@ snapshots:
|
|||||||
|
|
||||||
before-after-hook@2.2.3: {}
|
before-after-hook@2.2.3: {}
|
||||||
|
|
||||||
|
better-sqlite3@11.10.0:
|
||||||
|
dependencies:
|
||||||
|
bindings: 1.5.0
|
||||||
|
prebuild-install: 7.1.3
|
||||||
|
|
||||||
bidi-js@1.0.3:
|
bidi-js@1.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
require-from-string: 2.0.2
|
require-from-string: 2.0.2
|
||||||
|
|
||||||
|
bindings@1.5.0:
|
||||||
|
dependencies:
|
||||||
|
file-uri-to-path: 1.0.0
|
||||||
|
|
||||||
bintrees@1.0.2: {}
|
bintrees@1.0.2: {}
|
||||||
|
|
||||||
bl@4.1.0:
|
bl@4.1.0:
|
||||||
@@ -7555,10 +7589,12 @@ snapshots:
|
|||||||
esbuild: 0.25.12
|
esbuild: 0.25.12
|
||||||
tsx: 4.22.1
|
tsx: 4.22.1
|
||||||
|
|
||||||
drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0):
|
drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@11.10.0)(pg@8.21.0):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@opentelemetry/api': 1.9.1
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@types/better-sqlite3': 7.6.13
|
||||||
'@types/pg': 8.20.0
|
'@types/pg': 8.20.0
|
||||||
|
better-sqlite3: 11.10.0
|
||||||
pg: 8.21.0
|
pg: 8.21.0
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
@@ -7923,6 +7959,8 @@ snapshots:
|
|||||||
|
|
||||||
file-type@10.11.0: {}
|
file-type@10.11.0: {}
|
||||||
|
|
||||||
|
file-uri-to-path@1.0.0: {}
|
||||||
|
|
||||||
fill-range@7.1.1:
|
fill-range@7.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
to-regex-range: 5.0.1
|
to-regex-range: 5.0.1
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { mascotChatService } from "./mascot-chat.service.js";
|
|||||||
|
|
||||||
const logger = createChildLogger("mascot-chat.controller");
|
const logger = createChildLogger("mascot-chat.controller");
|
||||||
|
|
||||||
|
interface AuthenticatedRequest extends Request {
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export async function handleMascotChat(req: Request, res: Response) {
|
export async function handleMascotChat(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const { message, context } = req.body;
|
const { message, context } = req.body;
|
||||||
@@ -16,7 +20,7 @@ export async function handleMascotChat(req: Request, res: Response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get user ID from auth middleware (if available)
|
// Get user ID from auth middleware (if available)
|
||||||
const userId = (req as any).userId || "anonymous";
|
const userId = (req as AuthenticatedRequest).userId || "anonymous";
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
{ userId, messageLength: message.length, context },
|
{ userId, messageLength: message.length, context },
|
||||||
@@ -56,7 +60,7 @@ export async function handleMascotChat(req: Request, res: Response) {
|
|||||||
|
|
||||||
export async function getMascotChatHistory(req: Request, res: Response) {
|
export async function getMascotChatHistory(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const userId = (req as any).userId || "anonymous";
|
const userId = (req as AuthenticatedRequest).userId || "anonymous";
|
||||||
const limit = Math.min(parseInt(req.query.limit as string) || 50, 100);
|
const limit = Math.min(parseInt(req.query.limit as string) || 50, 100);
|
||||||
|
|
||||||
const history = await mascotChatService.getChatHistory(userId, limit);
|
const history = await mascotChatService.getChatHistory(userId, limit);
|
||||||
@@ -76,7 +80,7 @@ export async function getMascotChatHistory(req: Request, res: Response) {
|
|||||||
|
|
||||||
export async function clearMascotChatHistory(req: Request, res: Response) {
|
export async function clearMascotChatHistory(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const userId = (req as any).userId || "anonymous";
|
const userId = (req as AuthenticatedRequest).userId || "anonymous";
|
||||||
|
|
||||||
await mascotChatService.clearChatHistory(userId);
|
await mascotChatService.clearChatHistory(userId);
|
||||||
|
|
||||||
|
|||||||
@@ -37,33 +37,7 @@ export interface ServerInsights {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MascotChatRepository {
|
export class MascotChatRepository {
|
||||||
private initialized = false;
|
|
||||||
|
|
||||||
async ensureSchema(): Promise<void> {
|
|
||||||
if (this.initialized) return;
|
|
||||||
|
|
||||||
const pool = getPool();
|
|
||||||
await pool.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS mascot_chat_messages (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
user_id TEXT NOT NULL,
|
|
||||||
user_message TEXT NOT NULL,
|
|
||||||
mascot_response TEXT NOT NULL,
|
|
||||||
context JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
await pool.query(`
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_mascot_chat_messages_user_created
|
|
||||||
ON mascot_chat_messages (user_id, created_at DESC)
|
|
||||||
`);
|
|
||||||
|
|
||||||
this.initialized = true;
|
|
||||||
logger.info("Mascot chat schema ready");
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveConversation(input: SaveConversationInput): Promise<void> {
|
async saveConversation(input: SaveConversationInput): Promise<void> {
|
||||||
await this.ensureSchema();
|
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
|
|
||||||
await pool.query(
|
await pool.query(
|
||||||
@@ -88,7 +62,6 @@ export class MascotChatRepository {
|
|||||||
userId: string,
|
userId: string,
|
||||||
limit: number,
|
limit: number,
|
||||||
): Promise<MascotChatHistoryRow[]> {
|
): Promise<MascotChatHistoryRow[]> {
|
||||||
await this.ensureSchema();
|
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
|
|
||||||
const { rows } = await pool.query<MascotChatHistoryRow>(
|
const { rows } = await pool.query<MascotChatHistoryRow>(
|
||||||
@@ -107,7 +80,6 @@ export class MascotChatRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async clearChatHistory(userId: string): Promise<void> {
|
async clearChatHistory(userId: string): Promise<void> {
|
||||||
await this.ensureSchema();
|
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
|
|
||||||
const { rowCount } = await pool.query(
|
const { rowCount } = await pool.query(
|
||||||
@@ -122,7 +94,6 @@ export class MascotChatRepository {
|
|||||||
guildId?: string,
|
guildId?: string,
|
||||||
channelId?: string,
|
channelId?: string,
|
||||||
): Promise<ServerInsights> {
|
): Promise<ServerInsights> {
|
||||||
await this.ensureSchema();
|
|
||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ let publisherClient: Redis | null = null;
|
|||||||
let subscriberClient: Redis | null = null;
|
let subscriberClient: Redis | null = null;
|
||||||
|
|
||||||
function ensureRedisConfig(): boolean {
|
function ensureRedisConfig(): boolean {
|
||||||
return !!(config.REDIS_URL);
|
return !!config.REDIS_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createClient(): Redis {
|
function createClient(): Redis {
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- Move mascot_chat_messages from runtime CREATE TABLE to Drizzle migration
|
||||||
|
-- Previously created at runtime by mascot-chat.repository.ts ensureSchema()
|
||||||
|
CREATE TABLE IF NOT EXISTS "mascot_chat_messages" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"user_message" text NOT NULL,
|
||||||
|
"mascot_response" text NOT NULL,
|
||||||
|
"context" jsonb DEFAULT '{}' NOT NULL,
|
||||||
|
"created_at" timestamptz DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_mascot_chat_messages_user_created" ON "mascot_chat_messages" USING btree ("user_id", "created_at" DESC);
|
||||||
@@ -50,6 +50,13 @@
|
|||||||
"when": 1780900000000,
|
"when": 1780900000000,
|
||||||
"tag": "0006_replace_base64_with_image_url",
|
"tag": "0006_replace_base64_with_image_url",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 7,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1781174400000,
|
||||||
|
"tag": "0007_mascot_chat_table",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -413,7 +413,7 @@ async function processIndividualFallback(
|
|||||||
type: "individual",
|
type: "individual",
|
||||||
message,
|
message,
|
||||||
skipNormalAnalysis: false,
|
skipNormalAnalysis: false,
|
||||||
} as any)) as
|
} as unknown)) as
|
||||||
| { ok: true; results: AnalysisResult[] }
|
| { ok: true; results: AnalysisResult[] }
|
||||||
| { ok: false; results: AnalysisResult[]; error: string };
|
| { ok: false; results: AnalysisResult[]; error: string };
|
||||||
|
|
||||||
@@ -443,7 +443,7 @@ async function processIndividualFallback(
|
|||||||
type: "individual",
|
type: "individual",
|
||||||
message,
|
message,
|
||||||
skipNormalAnalysis: true,
|
skipNormalAnalysis: true,
|
||||||
} as any)) as
|
} as unknown)) as
|
||||||
| { ok: true; results: AnalysisResult[] }
|
| { ok: true; results: AnalysisResult[] }
|
||||||
| { ok: false; results: AnalysisResult[]; error: string };
|
| { ok: false; results: AnalysisResult[]; error: string };
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { config } from "../../shared/config/config.js";
|
|||||||
import { createModerationAction } from "../message-capture/messageStore.js";
|
import { createModerationAction } from "../message-capture/messageStore.js";
|
||||||
import type { MessageRecord } from "../message-capture/types.js";
|
import type { MessageRecord } from "../message-capture/types.js";
|
||||||
|
|
||||||
|
interface ChannelWithSend {
|
||||||
|
send: (content: string | object, options?: unknown) => Promise<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
const logger = createChildLogger("auto-delete-manager");
|
const logger = createChildLogger("auto-delete-manager");
|
||||||
|
|
||||||
const parseStringList = (value?: string | null): string[] => {
|
const parseStringList = (value?: string | null): string[] => {
|
||||||
@@ -353,7 +357,7 @@ export async function attemptAutoDeleteFlaggedMessage(
|
|||||||
if (
|
if (
|
||||||
logChannel &&
|
logChannel &&
|
||||||
"send" in logChannel &&
|
"send" in logChannel &&
|
||||||
typeof (logChannel as any).send === "function"
|
typeof (logChannel as ChannelWithSend).send === "function"
|
||||||
) {
|
) {
|
||||||
const severity = message.ai_severity ?? "none";
|
const severity = message.ai_severity ?? "none";
|
||||||
const categories =
|
const categories =
|
||||||
@@ -362,7 +366,7 @@ export async function attemptAutoDeleteFlaggedMessage(
|
|||||||
0,
|
0,
|
||||||
200,
|
200,
|
||||||
);
|
);
|
||||||
await (logChannel as any).send(
|
await (logChannel as ChannelWithSend).send(
|
||||||
`**🧹 Auto-Delete** — Pesan dari <@${message.user_id}> di <#${channelId}>\n` +
|
`**🧹 Auto-Delete** — Pesan dari <@${message.user_id}> di <#${channelId}>\n` +
|
||||||
`**Status:** ${message.ai_status}\n` +
|
`**Status:** ${message.ai_status}\n` +
|
||||||
`**Severitas:** ${severity}\n` +
|
`**Severitas:** ${severity}\n` +
|
||||||
|
|||||||
@@ -14,6 +14,24 @@ import { withLlmConcurrency } from "./concurrencyLimiter.js";
|
|||||||
|
|
||||||
const log = createChildLogger("llm-client");
|
const log = createChildLogger("llm-client");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Covers all LLM response chunk shapes the streaming handler supports.
|
||||||
|
* Different providers (OpenAI, Anthropic-compatible, local LLMs) may return
|
||||||
|
* content in different fields — we try them all via optional chaining.
|
||||||
|
*/
|
||||||
|
type LLMResponseChunk = {
|
||||||
|
choices?: Array<{
|
||||||
|
delta?: { content?: string | null };
|
||||||
|
message?: { content?: string | null };
|
||||||
|
finish_reason?: string | null;
|
||||||
|
text?: string;
|
||||||
|
}>;
|
||||||
|
message?: { content?: string | null };
|
||||||
|
content?: string;
|
||||||
|
response?: string;
|
||||||
|
finish_reason?: string;
|
||||||
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Lazy singleton — created on first use so that config is always resolved.
|
// Lazy singleton — created on first use so that config is always resolved.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -112,7 +130,7 @@ export async function llmChat(
|
|||||||
if (currentParams.stream) {
|
if (currentParams.stream) {
|
||||||
let content = "";
|
let content = "";
|
||||||
let finishReason = "stop";
|
let finishReason = "stop";
|
||||||
for await (const chunk of response as any) {
|
for await (const chunk of response as unknown as AsyncIterable<LLMResponseChunk>) {
|
||||||
const choice = chunk?.choices?.[0];
|
const choice = chunk?.choices?.[0];
|
||||||
const textChunk =
|
const textChunk =
|
||||||
choice?.delta?.content ||
|
choice?.delta?.content ||
|
||||||
|
|||||||
@@ -179,7 +179,12 @@ export function extractJson(content: string): unknown {
|
|||||||
if (parsed && typeof parsed === "object") {
|
if (parsed && typeof parsed === "object") {
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (err) {
|
||||||
|
log.debug(
|
||||||
|
{ err: err instanceof Error ? err.message : String(err) },
|
||||||
|
"Failed to parse JSON from code block — trying next block",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let start = 0; start < content.length; start++) {
|
for (let start = 0; start < content.length; start++) {
|
||||||
@@ -224,7 +229,12 @@ export function extractJson(content: string): unknown {
|
|||||||
if (parsed && typeof parsed === "object") {
|
if (parsed && typeof parsed === "object") {
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (err) {
|
||||||
|
log.debug(
|
||||||
|
{ err: err instanceof Error ? err.message : String(err) },
|
||||||
|
"Failed to parse JSON candidate — trying next position",
|
||||||
|
);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,7 +276,7 @@ export function parseModerationResponse(
|
|||||||
parsed = { results: [parsed] };
|
parsed = { results: [parsed] };
|
||||||
} else {
|
} else {
|
||||||
const arrayKey = Object.keys(parsed).find((key) => {
|
const arrayKey = Object.keys(parsed).find((key) => {
|
||||||
const val = (parsed as any)[key];
|
const val = parsed[key];
|
||||||
return (
|
return (
|
||||||
Array.isArray(val) &&
|
Array.isArray(val) &&
|
||||||
val.length > 0 &&
|
val.length > 0 &&
|
||||||
@@ -274,12 +284,12 @@ export function parseModerationResponse(
|
|||||||
(item: unknown) =>
|
(item: unknown) =>
|
||||||
typeof item === "object" &&
|
typeof item === "object" &&
|
||||||
item !== null &&
|
item !== null &&
|
||||||
"message_id" in (item as any),
|
"message_id" in (item as Record<string, unknown>),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
if (arrayKey) {
|
if (arrayKey) {
|
||||||
parsed.results = (parsed as any)[arrayKey];
|
parsed.results = parsed[arrayKey];
|
||||||
} else {
|
} else {
|
||||||
parsed = { results: [parsed] };
|
parsed = { results: [parsed] };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export async function pruneExpiredTexts(): Promise<number> {
|
|||||||
`DELETE FROM text_analysis_cache WHERE expires_at < $1`,
|
`DELETE FROM text_analysis_cache WHERE expires_at < $1`,
|
||||||
[Date.now()],
|
[Date.now()],
|
||||||
);
|
);
|
||||||
return (result as any).rowCount ?? 0;
|
return (result as unknown as { rowCount?: number }).rowCount ?? 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
{ error: error instanceof Error ? error.message : String(error) },
|
{ error: error instanceof Error ? error.message : String(error) },
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
import type fs from "node:fs";
|
import type fs from "node:fs";
|
||||||
import type prism from "prism-media";
|
|
||||||
import type {
|
import type {
|
||||||
AIStatus,
|
|
||||||
AISeverity,
|
|
||||||
AIRecommendedAction,
|
AIRecommendedAction,
|
||||||
|
AISeverity,
|
||||||
|
AIStatus,
|
||||||
|
AnalysisQueueStatus,
|
||||||
|
AttachmentRecord,
|
||||||
BroadcasterClient,
|
BroadcasterClient,
|
||||||
|
MessageRecord,
|
||||||
ModerationBroadcaster,
|
ModerationBroadcaster,
|
||||||
RoleMetadata,
|
RoleMetadata,
|
||||||
UserMetadata,
|
UserMetadata,
|
||||||
MessageRecord,
|
|
||||||
AttachmentRecord,
|
|
||||||
VoiceRecordingUploadData,
|
VoiceRecordingUploadData,
|
||||||
AnalysisQueueStatus,
|
|
||||||
} from "@bete/shared";
|
} from "@bete/shared";
|
||||||
|
import type prism from "prism-media";
|
||||||
|
|
||||||
// Re-export all shared types for backward compatibility
|
// Re-export all shared types for backward compatibility
|
||||||
export type {
|
export type {
|
||||||
AIStatus,
|
|
||||||
AISeverity,
|
|
||||||
AIRecommendedAction,
|
AIRecommendedAction,
|
||||||
BroadcasterClient,
|
AISeverity,
|
||||||
ModerationBroadcaster,
|
AIStatus,
|
||||||
RoleMetadata,
|
AnalysisQueueStatus,
|
||||||
UserMetadata,
|
AnalysisResult,
|
||||||
MessageRecord,
|
|
||||||
AttachmentRecord,
|
AttachmentRecord,
|
||||||
VoiceSegmentRecord,
|
BroadcasterClient,
|
||||||
DashboardMessage,
|
DashboardMessage,
|
||||||
MessageQuery,
|
MessageQuery,
|
||||||
PageResult,
|
MessageRecord,
|
||||||
AnalysisResult,
|
|
||||||
VoiceRecordingUploadData,
|
|
||||||
AnalysisQueueStatus,
|
|
||||||
MessageReview,
|
MessageReview,
|
||||||
ModerationAction,
|
ModerationAction,
|
||||||
|
ModerationActionType,
|
||||||
|
ModerationBroadcaster,
|
||||||
|
PageResult,
|
||||||
RetentionPolicy,
|
RetentionPolicy,
|
||||||
ReviewStatus,
|
ReviewStatus,
|
||||||
ModerationActionType,
|
RoleMetadata,
|
||||||
|
UserMetadata,
|
||||||
|
VoiceRecordingUploadData,
|
||||||
|
VoiceSegmentRecord,
|
||||||
} from "@bete/shared";
|
} from "@bete/shared";
|
||||||
|
|
||||||
// Types that are LOCAL ONLY (not in shared) — keep here
|
// Types that are LOCAL ONLY (not in shared) — keep here
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import type { AppConfig as SharedAppConfig } from "@bete/shared/config";
|
import type { AppConfig as SharedAppConfig } from "@bete/shared/config";
|
||||||
import { config as sharedConfig, loadConfig as sharedLoadConfig } from "@bete/shared/config";
|
import {
|
||||||
|
config as sharedConfig,
|
||||||
|
loadConfig as sharedLoadConfig,
|
||||||
|
} from "@bete/shared/config";
|
||||||
|
|
||||||
// Re-export the unified config with EFFECTIVE_* fields added
|
// Re-export the unified config with EFFECTIVE_* fields added
|
||||||
export type AppConfig = SharedAppConfig & {
|
export type AppConfig = SharedAppConfig & {
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import {
|
|||||||
foreignKey as pgForeignKey,
|
foreignKey as pgForeignKey,
|
||||||
index as pgIndex,
|
index as pgIndex,
|
||||||
integer as pgInteger,
|
integer as pgInteger,
|
||||||
|
jsonb as pgJsonb,
|
||||||
real as pgReal,
|
real as pgReal,
|
||||||
pgTable,
|
pgTable,
|
||||||
text as pgText,
|
text as pgText,
|
||||||
|
timestamp as pgTimestamp,
|
||||||
|
uuid as pgUuid,
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
// PostgreSQL Schema
|
// PostgreSQL Schema
|
||||||
@@ -492,6 +495,30 @@ export const pgCorrectedModerationsTable = pgTable(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mascot Chat Messages Table (PostgreSQL)
|
||||||
|
* Stores AI mascot chat conversation history
|
||||||
|
*/
|
||||||
|
export const pgMascotChatMessagesTable = pgTable(
|
||||||
|
"mascot_chat_messages",
|
||||||
|
{
|
||||||
|
id: pgUuid("id").defaultRandom().primaryKey(),
|
||||||
|
user_id: pgText("user_id").notNull(),
|
||||||
|
user_message: pgText("user_message").notNull(),
|
||||||
|
mascot_response: pgText("mascot_response").notNull(),
|
||||||
|
context: pgJsonb("context").notNull().default("{}"),
|
||||||
|
created_at: pgTimestamp("created_at", { withTimezone: true, mode: "date" })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
userCreatedIdx: pgIndex("idx_mascot_chat_messages_user_created").on(
|
||||||
|
table.user_id,
|
||||||
|
table.created_at.desc(),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Runtime table exports
|
// Runtime table exports
|
||||||
// =====================
|
// =====================
|
||||||
|
|
||||||
@@ -509,6 +536,7 @@ export const stickerCacheTable = pgStickerCacheTable;
|
|||||||
export const correctedModerationsTable = pgCorrectedModerationsTable;
|
export const correctedModerationsTable = pgCorrectedModerationsTable;
|
||||||
export const userReputationsTable = pgUserReputationsTable;
|
export const userReputationsTable = pgUserReputationsTable;
|
||||||
export const channelCulturesTable = pgChannelCulturesTable;
|
export const channelCulturesTable = pgChannelCulturesTable;
|
||||||
|
export const mascotChatMessagesTable = pgMascotChatMessagesTable;
|
||||||
|
|
||||||
// Export table types for use in queries
|
// Export table types for use in queries
|
||||||
export type MuxerJob = typeof muxerJobsTable.$inferSelect;
|
export type MuxerJob = typeof muxerJobsTable.$inferSelect;
|
||||||
@@ -550,3 +578,7 @@ export type UserReputationInsert = typeof userReputationsTable.$inferInsert;
|
|||||||
|
|
||||||
export type ChannelCulture = typeof channelCulturesTable.$inferSelect;
|
export type ChannelCulture = typeof channelCulturesTable.$inferSelect;
|
||||||
export type ChannelCultureInsert = typeof channelCulturesTable.$inferInsert;
|
export type ChannelCultureInsert = typeof channelCulturesTable.$inferInsert;
|
||||||
|
|
||||||
|
export type MascotChatMessage = typeof mascotChatMessagesTable.$inferSelect;
|
||||||
|
export type MascotChatMessageInsert =
|
||||||
|
typeof mascotChatMessagesTable.$inferInsert;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"format": "biome format --write src/"
|
"format": "biome format --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@bete/shared": "workspace:*",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
export type AIStatus = "pending" | "clean" | "warn" | "flagged" | "error";
|
export type {
|
||||||
export type AISeverity = "none" | "low" | "medium" | "high" | "critical";
|
AIRecommendedAction,
|
||||||
export type AIRecommendedAction =
|
AISeverity,
|
||||||
| "none"
|
AIStatus,
|
||||||
| "monitor"
|
MessageRecord,
|
||||||
| "warn"
|
} from "@bete/shared";
|
||||||
| "review"
|
|
||||||
| "delete"
|
|
||||||
| "escalate";
|
|
||||||
|
|
||||||
export interface MessageMetadata {
|
export interface MessageMetadata {
|
||||||
stickers?: Array<{ name?: string; url?: string }>;
|
stickers?: Array<{ name?: string; url?: string }>;
|
||||||
@@ -30,33 +27,6 @@ export function parseMetadata(value: string | null): MessageMetadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageRecord {
|
|
||||||
id: string;
|
|
||||||
guild_id: string;
|
|
||||||
channel_id: string;
|
|
||||||
thread_id: string | null;
|
|
||||||
user_id: string;
|
|
||||||
username: string;
|
|
||||||
avatar_url: string | null;
|
|
||||||
content: string;
|
|
||||||
edited_content: string | null;
|
|
||||||
created_at: number;
|
|
||||||
edited_at: number | null;
|
|
||||||
deleted_at: number | null;
|
|
||||||
type: "text" | "edited" | "deleted";
|
|
||||||
metadata: string | null;
|
|
||||||
ai_status?: AIStatus | null;
|
|
||||||
ai_moderation_flags?: string | null;
|
|
||||||
ai_moderation_score?: number | null;
|
|
||||||
ai_analysis?: string | null;
|
|
||||||
ai_categories?: string | null;
|
|
||||||
ai_severity?: AISeverity | null;
|
|
||||||
ai_confidence?: number | null;
|
|
||||||
ai_recommended_action?: AIRecommendedAction | null;
|
|
||||||
ai_analyzed_at?: number | null;
|
|
||||||
ai_error?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PageResult<T> {
|
export interface PageResult<T> {
|
||||||
data: T[];
|
data: T[];
|
||||||
nextCursor: string | null;
|
nextCursor: string | null;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// ─── Shared HTTP client — all API endpoints in one file ──────────────────────
|
// ─── Shared HTTP client — all API endpoints in one file ──────────────────────
|
||||||
|
|
||||||
|
import type { MessageRecord } from "@bete/shared";
|
||||||
|
|
||||||
const BE_API_URL = import.meta.env.VITE_BE_API_URL || "http://localhost:3001";
|
const BE_API_URL = import.meta.env.VITE_BE_API_URL || "http://localhost:3001";
|
||||||
const BE_WS_URL = import.meta.env.VITE_BE_WS_URL || "ws://localhost:3001";
|
const BE_WS_URL = import.meta.env.VITE_BE_WS_URL || "ws://localhost:3001";
|
||||||
|
|
||||||
@@ -57,32 +59,7 @@ export interface PageResult<T> {
|
|||||||
nextCursor: string | null;
|
nextCursor: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageRecord {
|
export type { MessageRecord };
|
||||||
id: string;
|
|
||||||
guild_id: string;
|
|
||||||
channel_id: string;
|
|
||||||
thread_id: string | null;
|
|
||||||
user_id: string;
|
|
||||||
username: string;
|
|
||||||
avatar_url: string | null;
|
|
||||||
content: string;
|
|
||||||
edited_content: string | null;
|
|
||||||
created_at: number;
|
|
||||||
edited_at: number | null;
|
|
||||||
deleted_at: number | null;
|
|
||||||
type: "text" | "edited" | "deleted";
|
|
||||||
metadata: string | null;
|
|
||||||
ai_status?: string | null;
|
|
||||||
ai_moderation_flags?: string | null;
|
|
||||||
ai_moderation_score?: number | null;
|
|
||||||
ai_analysis?: string | null;
|
|
||||||
ai_categories?: string | null;
|
|
||||||
ai_severity?: string | null;
|
|
||||||
ai_confidence?: number | null;
|
|
||||||
ai_recommended_action?: string | null;
|
|
||||||
ai_analyzed_at?: number | null;
|
|
||||||
ai_error?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Guild {
|
export interface Guild {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user