This commit is contained in:
seb
2026-10-03 23:13:40 +02:00
parent 6520665dcf
commit 3b74c12f4e
36 changed files with 3669 additions and 2183 deletions

View File

@@ -8,6 +8,8 @@ export const TABLES = [
'manifestattachment',
'labeltemplate',
'file',
'modelmessage',
'modelconversation',
'companyuser',
'syncpos',
'companysetting',

View File

@@ -0,0 +1,79 @@
export const MODELCONVERSATION_COLUMNS = `
id,
companyuser_id,
provider,
model,
created_at,
updated_at,
state
`;
export const LIST_BY_COMPANYUSER = `
SELECT
${MODELCONVERSATION_COLUMNS}
FROM modelconversation
WHERE companyuser_id = @companyuserId
ORDER BY updated_at DESC, id DESC
`;
export const GET_BY_COMPANYUSER_AND_ID = `
SELECT
${MODELCONVERSATION_COLUMNS}
FROM modelconversation
WHERE id = @id AND companyuser_id = @companyuserId
`;
export const CREATE = `
INSERT INTO modelconversation (
companyuser_id,
provider,
model
)
OUTPUT
INSERTED.id,
INSERTED.companyuser_id,
INSERTED.provider,
INSERTED.model,
INSERTED.created_at,
INSERTED.updated_at,
INSERTED.state
VALUES (
@companyuserId,
@provider,
@model
)
`;
/** Atomic idle guard + touch: affects 0 rows unless the conversation is idle. */
export const TOUCH_IF_IDLE = `
UPDATE modelconversation
SET updated_at = SYSUTCDATETIME()
WHERE id = @id AND companyuser_id = @companyuserId AND state = 'idle'
`;
/** Unconditional touch (used when appending assistant/system messages or updating activity). */
export const TOUCH = `
UPDATE modelconversation
SET updated_at = SYSUTCDATETIME()
WHERE id = @id AND companyuser_id = @companyuserId
`;
/** Updates conversation state ('idle' | 'busy') and touches updated_at. */
export const SET_STATE = `
UPDATE modelconversation
SET state = @state, updated_at = SYSUTCDATETIME()
OUTPUT
INSERTED.id,
INSERTED.companyuser_id,
INSERTED.provider,
INSERTED.model,
INSERTED.created_at,
INSERTED.updated_at,
INSERTED.state
WHERE id = @id AND companyuser_id = @companyuserId
`;
export const DELETE_BY_COMPANYUSER_AND_ID = `
DELETE FROM modelconversation
WHERE id = @id AND companyuser_id = @companyuserId
`;

View File

@@ -0,0 +1,34 @@
export const MODELMESSAGE_COLUMNS = `
id,
conversation_id,
created_at,
role,
message
`;
export const LIST_BY_CONVERSATION = `
SELECT
${MODELMESSAGE_COLUMNS}
FROM modelmessage
WHERE conversation_id = @conversationId
ORDER BY id ASC
`;
export const CREATE = `
INSERT INTO modelmessage (
conversation_id,
role,
message
)
OUTPUT
INSERTED.id,
INSERTED.conversation_id,
INSERTED.created_at,
INSERTED.role,
INSERTED.message
VALUES (
@conversationId,
@role,
@message
)
`;

View File

@@ -0,0 +1,63 @@
export const MODELVFS_COLUMNS = `
id,
company_id,
name,
mime,
meta,
created_at,
updated_at
`;
export const GET_BY_COMPANY_AND_NAME = `
SELECT
${MODELVFS_COLUMNS},
content
FROM modelvfs
WHERE company_id = @companyId AND name = @name
`;
export const GET_META_BY_COMPANY_AND_NAME = `
SELECT
${MODELVFS_COLUMNS}
FROM modelvfs
WHERE company_id = @companyId AND name = @name
`;
export const LIST_BY_COMPANY = `
SELECT
${MODELVFS_COLUMNS}
FROM modelvfs
WHERE company_id = @companyId
ORDER BY name ASC
`;
export const UPSERT = `
MERGE modelvfs AS target
USING (SELECT @companyId AS company_id, @name AS name) AS src
ON target.company_id = src.company_id AND target.name = src.name
WHEN MATCHED THEN
UPDATE SET mime = @mime, content = @content, meta = @meta, updated_at = SYSUTCDATETIME()
WHEN NOT MATCHED THEN
INSERT (company_id, name, mime, content, meta)
VALUES (@companyId, @name, @mime, @content, @meta)
OUTPUT
INSERTED.id,
INSERTED.company_id,
INSERTED.name,
INSERTED.mime,
INSERTED.meta,
INSERTED.created_at,
INSERTED.updated_at;
`;
export const DELETE_BY_COMPANY_AND_NAME = `
DELETE FROM modelvfs
OUTPUT DELETED.id
WHERE company_id = @companyId AND name = @name
`;
export const DELETE_ALL_BY_COMPANY = `
DELETE FROM modelvfs
OUTPUT DELETED.id
WHERE company_id = @companyId
`;

View File

@@ -771,6 +771,60 @@ BEGIN
END
`;
export const INIT_MODELCONVERSATION = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'modelconversation')
BEGIN
CREATE TABLE modelconversation (
id INT IDENTITY(1,1) PRIMARY KEY,
companyuser_id INT NOT NULL,
provider NVARCHAR(100) NOT NULL,
model NVARCHAR(255) NOT NULL,
created_at DATETIME2 NOT NULL CONSTRAINT DF_modelconversation_created_at DEFAULT SYSUTCDATETIME(),
updated_at DATETIME2 NOT NULL CONSTRAINT DF_modelconversation_updated_at DEFAULT SYSUTCDATETIME(),
state NVARCHAR(20) NOT NULL CONSTRAINT DF_modelconversation_state DEFAULT 'idle',
CONSTRAINT FK_modelconversation_companyuser FOREIGN KEY (companyuser_id) REFERENCES companyuser(id) ON DELETE CASCADE,
CONSTRAINT CK_modelconversation_state CHECK (state IN ('idle', 'busy'))
);
CREATE INDEX IX_modelconversation_companyuser ON modelconversation (companyuser_id, updated_at);
END
`;
export const INIT_MODELVFS = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'modelvfs')
BEGIN
CREATE TABLE modelvfs (
id INT IDENTITY(1,1) PRIMARY KEY,
company_id INT NOT NULL,
name NVARCHAR(255) NOT NULL,
mime NVARCHAR(100) NOT NULL,
content VARBINARY(MAX) NOT NULL,
meta NVARCHAR(MAX),
created_at DATETIME2 NOT NULL CONSTRAINT DF_modelvfs_created_at DEFAULT SYSUTCDATETIME(),
updated_at DATETIME2 NOT NULL CONSTRAINT DF_modelvfs_updated_at DEFAULT SYSUTCDATETIME(),
CONSTRAINT FK_modelvfs_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE,
CONSTRAINT UQ_modelvfs_company_name UNIQUE (company_id, name)
);
CREATE INDEX IX_modelvfs_company_name ON modelvfs (company_id, name);
END
`;
export const INIT_MODELMESSAGE = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'modelmessage')
BEGIN
CREATE TABLE modelmessage (
id INT IDENTITY(1,1) PRIMARY KEY,
conversation_id INT NOT NULL,
created_at DATETIME2 NOT NULL CONSTRAINT DF_modelmessage_created_at DEFAULT SYSUTCDATETIME(),
role NVARCHAR(20) NOT NULL,
message NVARCHAR(MAX) NOT NULL,
CONSTRAINT FK_modelmessage_conversation FOREIGN KEY (conversation_id) REFERENCES modelconversation(id) ON DELETE CASCADE,
CONSTRAINT CK_modelmessage_role CHECK (role IN ('system', 'user', 'assistant', 'tool', 'reasoning'))
);
CREATE INDEX IX_modelmessage_conversation ON modelmessage (conversation_id, id);
END
`;
export const INIT_SYNCPOS = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'syncpos')
BEGIN
@@ -1087,6 +1141,9 @@ export const SCHEMA_SCRIPTS = [
INIT_GET_NEXT_NUMBER_WITH_PREFIX_AND_PADDING,
INIT_PEEK_NEXT_NUMBER_WITH_PREFIX_AND_PADDING,
INIT_COMPANYUSER,
INIT_MODELCONVERSATION,
INIT_MODELMESSAGE,
INIT_MODELVFS,
INIT_ARTICLEHISTORY,
INIT_ARTICLETOMBSTONE,
INIT_ENTITYHISTORY,

View File

@@ -180,6 +180,31 @@ export class BetterSqlite3Executor {
await safeDeleteByHash(join(this.storageDir, 'thumbavif'), hash);
}
async writeVfsContent(id, buffer) {
if (!this.storageDir || !id) return;
await safeWrite(join(this.storageDir, 'vfs', String(id)), buffer);
}
async readVfsContent(id) {
if (!this.storageDir || !id) return null;
try {
return await fsPromises.readFile(join(this.storageDir, 'vfs', String(id)));
} catch (err) {
if (err.code === 'ENOENT') return null;
throw err;
}
}
async deleteVfsContent(id) {
if (!this.storageDir || !id) return;
try {
await fsPromises.unlink(join(this.storageDir, 'vfs', String(id)));
} catch (err) {
if (err.code !== 'ENOENT') throw err;
}
}
async deletePic(hash) {
if (!this.storageDir || !hash) return;
await safeDeleteByHash(join(this.storageDir, 'fullbinary'), hash);
@@ -188,7 +213,7 @@ export class BetterSqlite3Executor {
async purgeFiles() {
if (!this.storageDir) return;
for (const sub of ['content', 'thumbavif', 'fullbinary', 'thumbbinary']) {
for (const sub of ['content', 'thumbavif', 'fullbinary', 'thumbbinary', 'vfs']) {
const dir = join(this.storageDir, sub);
try {
const files = await fsPromises.readdir(dir);
@@ -276,7 +301,7 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor {
super(db, storageDir);
if (this.storageDir) {
for (const sub of ['content', 'thumbavif', 'fullbinary', 'thumbbinary']) {
for (const sub of ['content', 'thumbavif', 'fullbinary', 'thumbbinary', 'vfs']) {
mkdirSync(join(this.storageDir, sub), { recursive: true });
}
}
@@ -316,6 +341,9 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor {
runner.readPicFull = executor.readPicFull.bind(executor);
runner.writePicThumb = executor.writePicThumb.bind(executor);
runner.readPicThumb = executor.readPicThumb.bind(executor);
runner.writeVfsContent = executor.writeVfsContent.bind(executor);
runner.readVfsContent = executor.readVfsContent.bind(executor);
runner.deleteVfsContent = executor.deleteVfsContent.bind(executor);
runner.deleteFile = executor.deleteFile.bind(executor);
runner.deletePic = executor.deletePic.bind(executor);
runner.purgeFiles = executor.purgeFiles.bind(executor);

View File

@@ -0,0 +1,14 @@
import * as source from '../../mssql/queries/modelconversation.js';
import { translateQueries } from './translate.js';
const queries = translateQueries(source, 'modelconversation');
export const {
CREATE,
DELETE_BY_COMPANYUSER_AND_ID,
GET_BY_COMPANYUSER_AND_ID,
LIST_BY_COMPANYUSER,
MODELCONVERSATION_COLUMNS,
SET_STATE,
TOUCH,
TOUCH_IF_IDLE,
} = queries;

View File

@@ -0,0 +1,5 @@
import * as source from '../../mssql/queries/modelmessage.js';
import { translateQueries } from './translate.js';
const queries = translateQueries(source, 'modelmessage');
export const { CREATE, LIST_BY_CONVERSATION } = queries;

View File

@@ -0,0 +1,38 @@
import * as source from '../../mssql/queries/modelvfs.js';
import { translateQueries } from './translate.js';
const queries = translateQueries(source, 'modelvfs');
export const {
MODELVFS_COLUMNS,
GET_META_BY_COMPANY_AND_NAME,
LIST_BY_COMPANY,
} = queries;
export const GET_BY_COMPANY_AND_NAME = `
SELECT
${MODELVFS_COLUMNS}
FROM modelvfs
WHERE company_id = @companyId AND name = @name
`;
export const UPSERT = `
INSERT INTO modelvfs (company_id, name, mime, meta)
VALUES (@companyId, @name, @mime, @meta)
ON CONFLICT (company_id, name) DO UPDATE SET
mime = excluded.mime,
meta = excluded.meta,
updated_at = (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
RETURNING id, company_id, name, mime, meta, created_at, updated_at
`;
export const DELETE_BY_COMPANY_AND_NAME = `
DELETE FROM modelvfs
WHERE company_id = @companyId AND name = @name
RETURNING id
`;
export const DELETE_ALL_BY_COMPANY = `
DELETE FROM modelvfs
WHERE company_id = @companyId
RETURNING id
`;

View File

@@ -483,6 +483,48 @@ CREATE TABLE IF NOT EXISTS companyuser (
);
`;
export const INIT_MODELCONVERSATION = `
CREATE TABLE IF NOT EXISTS modelconversation (
id INTEGER PRIMARY KEY,
companyuser_id INTEGER NOT NULL,
provider TEXT NOT NULL,
model TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
updated_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
state TEXT NOT NULL DEFAULT 'idle' CHECK (state IN ('idle', 'busy')),
FOREIGN KEY (companyuser_id) REFERENCES companyuser(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS IX_modelconversation_companyuser ON modelconversation (companyuser_id, updated_at);
`;
export const INIT_MODELVFS = `
CREATE TABLE IF NOT EXISTS modelvfs (
id INTEGER PRIMARY KEY,
company_id INTEGER NOT NULL,
name TEXT NOT NULL,
mime TEXT NOT NULL,
meta TEXT,
created_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
updated_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE,
UNIQUE (company_id, name)
);
CREATE INDEX IF NOT EXISTS IX_modelvfs_company_name ON modelvfs (company_id, name);
`;
export const INIT_MODELMESSAGE = `
CREATE TABLE IF NOT EXISTS modelmessage (
id INTEGER PRIMARY KEY,
conversation_id INTEGER NOT NULL,
created_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
role TEXT NOT NULL CHECK (role IN ('system', 'user', 'assistant', 'tool', 'reasoning')),
message TEXT NOT NULL,
FOREIGN KEY (conversation_id) REFERENCES modelconversation(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS IX_modelmessage_conversation ON modelmessage (conversation_id, id);
`;
export const INIT_ARTICLEHISTORY = `
CREATE TABLE IF NOT EXISTS articlehistory (
id INTEGER PRIMARY KEY,
@@ -770,6 +812,9 @@ export const SCHEMA_SCRIPTS = [
INIT_LEDGER,
INIT_NUMBERS,
INIT_COMPANYUSER,
INIT_MODELCONVERSATION,
INIT_MODELMESSAGE,
INIT_MODELVFS,
INIT_ARTICLEHISTORY,
INIT_ARTICLETOMBSTONE,
INIT_ENTITYHISTORY,