///
// Toàn bộ phần nặng chạy trong worker này: nạp model, suy luận WebGPU, vòng lặp agent, tool và shell.
import type { WorkerEvent, WorkerRequest } from '../shared/protocol';
import type { WorkspaceState } from '../shared/types';
import { Agent } from './agent';
import { MiniCpmEngine } from './engine';
import { isModelCached } from './modelCache';
import { Workspace } from './workspace';
const post = (event: WorkerEvent) => (self as DedicatedWorkerGlobalScope).postMessage(event);
// ───────────────────────── Lưu trạng thái trong IndexedDB ─────────────────────────
interface Store {
load(): Promise;
save(state: WorkspaceState): Promise;
}
async function openStore(): Promise {
const db = await new Promise((resolve, reject) => {
const req = indexedDB.open('minicpm5-pi-react-workspace', 1);
req.onupgradeneeded = () => req.result.createObjectStore('state');
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
return {
load: () =>
new Promise((resolve, reject) => {
const req = db.transaction('state').objectStore('state').get('current');
req.onsuccess = () => resolve(req.result as WorkspaceState | undefined);
req.onerror = () => reject(req.error);
}),
save: (state) =>
new Promise((resolve, reject) => {
const tx = db.transaction('state', 'readwrite');
tx.objectStore('state').put(state, 'current');
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
tx.onabort = () => reject(tx.error);
}),
};
}
// ───────────────────────── Khởi tạo ─────────────────────────
const engine = new MiniCpmEngine();
let store: Store | undefined;
let workspace: Workspace;
let agent: Agent;
let busy = false;
let controller: AbortController | undefined;
async function currentState(): Promise {
return {
version: 1,
files: await workspace.snapshot(),
entries: await workspace.serialize(),
messages: agent.messages,
};
}
async function persist(): Promise {
const state = await currentState();
try {
await store?.save(state);
} catch (err) {
post({ type: 'persistence_error', error: 'Changes are in memory, but browser storage failed: ' + (err as Error).message });
}
post({ type: 'workspace', files: state.files });
return state;
}
const initialized = (async () => {
let saved: WorkspaceState | undefined;
try {
store = await openStore();
saved = await store.load();
} catch (err) {
post({ type: 'persistence_error', error: 'Workspace saving is unavailable: ' + (err as Error).message });
}
workspace = new Workspace(saved?.entries, saved?.files);
await workspace.ready;
agent = new Agent({ engine, workspace, emit: post, persist: async () => void (await persist()) }, saved?.messages ?? []);
const state = await currentState();
if (!saved) {
try {
await store?.save(state);
} catch (err) {
post({ type: 'persistence_error', error: 'Changes are in memory, but browser storage failed: ' + (err as Error).message });
}
}
post({ type: 'initialized', ...state });
})();
// ───────────────────────── Xử lý lệnh ─────────────────────────
self.onmessage = async ({ data }: MessageEvent) => {
const { id, action } = data;
const args = (data.args ?? {}) as Record;
const reply = (value?: unknown) => post({ type: 'reply', id, value });
try {
await initialized;
if (action === 'stop') {
controller?.abort();
engine.stop();
return reply(true);
}
if (action === 'snapshot') return reply(await currentState());
if (action === 'cache_status') return reply(await isModelCached());
if (busy) throw new Error('Wait for the current operation or stop it first.');
busy = true;
controller = new AbortController();
post({ type: 'busy', action, cachedOnly: args.cachedOnly === true });
try {
let value: unknown;
switch (action) {
case 'load':
value = await engine.load({ cachedOnly: args.cachedOnly === true }, controller.signal, post);
break;
case 'prompt':
await agent.prompt(args.text, controller.signal);
value = await currentState();
break;
case 'shell':
value = await workspace.exec(args.command, controller.signal);
await persist();
break;
case 'write':
await workspace.write(args.path, args.content);
value = await persist();
break;
case 'new_chat':
agent.reset();
value = await persist();
break;
case 'read':
value = await workspace.read(args.path);
break;
default:
throw new Error('Unknown action: ' + action);
}
reply(value);
} finally {
busy = false;
controller = undefined;
post({ type: 'idle' });
}
} catch (err) {
const e = err as Error & { code?: string };
post({
type: 'reply',
id,
error: e?.name === 'AbortError' ? 'Stopped.' : String(e?.message ?? e),
errorName: e?.name,
errorCode: e?.code,
});
}
};