fix: align attachment storage model
This commit is contained in:
80
scripts/verify-attachment-storage-model.mjs
Normal file
80
scripts/verify-attachment-storage-model.mjs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { mkdtemp, copyFile } from "node:fs/promises";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { createRequire } from "node:module";
|
||||||
|
|
||||||
|
const rootDir = process.cwd();
|
||||||
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const jiti = require("jiti")(fileURLToPath(import.meta.url), {
|
||||||
|
alias: {
|
||||||
|
"@/": `${path.join(rootDir, "src")}/`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), "boss-attachment-model-"));
|
||||||
|
const tempStateFile = path.join(tempDir, "boss-state.json");
|
||||||
|
const sourceStateFile = path.join(rootDir, "data", "boss-state.json");
|
||||||
|
|
||||||
|
if (!existsSync(sourceStateFile)) {
|
||||||
|
throw new Error(`Missing state file: ${sourceStateFile}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await copyFile(sourceStateFile, tempStateFile);
|
||||||
|
process.env.BOSS_STATE_FILE = tempStateFile;
|
||||||
|
process.env.BOSS_RUNTIME_ROOT = rootDir;
|
||||||
|
|
||||||
|
const { getAttachmentStorageConfig, readState, writeState } = jiti(path.join(scriptDir, "..", "src", "lib", "boss-data.ts"));
|
||||||
|
|
||||||
|
const config = await getAttachmentStorageConfig("17600003315");
|
||||||
|
if (config.mode !== "server_file") {
|
||||||
|
throw new Error(`Expected default storage mode server_file, got ${config.mode}`);
|
||||||
|
}
|
||||||
|
if (!config.updatedAt || typeof config.updatedAt !== "string") {
|
||||||
|
throw new Error("Expected updatedAt to be populated");
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = await readState();
|
||||||
|
const messageId = "script-attachment-message";
|
||||||
|
state.projects[0].messages.unshift({
|
||||||
|
id: messageId,
|
||||||
|
sender: "user",
|
||||||
|
senderLabel: "测试用户",
|
||||||
|
body: "Attachment round-trip",
|
||||||
|
sentAt: "2026-03-29T00:00:00+08:00",
|
||||||
|
kind: "attachment",
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
attachmentId: "att-001",
|
||||||
|
fileName: "demo.txt",
|
||||||
|
mimeType: "text/plain",
|
||||||
|
fileSizeBytes: 12,
|
||||||
|
attachmentKind: "text",
|
||||||
|
storageBackend: "server_file",
|
||||||
|
storagePath: "/tmp/demo.txt",
|
||||||
|
previewAvailable: true,
|
||||||
|
uploadedAt: "2026-03-29T00:00:00+08:00",
|
||||||
|
uploadedBy: "17600003315",
|
||||||
|
analysisState: "not_applicable",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await writeState(state);
|
||||||
|
const reread = await readState();
|
||||||
|
const message = reread.projects[0].messages.find((item) => item.id === messageId);
|
||||||
|
|
||||||
|
if (!message?.attachments?.length) {
|
||||||
|
throw new Error("Expected message attachments to round-trip through state");
|
||||||
|
}
|
||||||
|
if (message.attachments[0].attachmentId !== "att-001") {
|
||||||
|
throw new Error("Expected attachment metadata to persist");
|
||||||
|
}
|
||||||
|
if (message.attachments[0].storageBackend !== "server_file") {
|
||||||
|
throw new Error("Expected attachment storage backend to persist");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("OK");
|
||||||
@@ -173,6 +173,7 @@ export interface Message {
|
|||||||
body: string;
|
body: string;
|
||||||
sentAt: string;
|
sentAt: string;
|
||||||
kind?: MessageKind;
|
kind?: MessageKind;
|
||||||
|
attachments?: MessageAttachment[];
|
||||||
forwardSource?: ForwardSource;
|
forwardSource?: ForwardSource;
|
||||||
forwardBundle?: ForwardBundlePayload;
|
forwardBundle?: ForwardBundlePayload;
|
||||||
}
|
}
|
||||||
@@ -1065,7 +1066,7 @@ const initialState: BossState = {
|
|||||||
{
|
{
|
||||||
account: PRIMARY_ADMIN_ACCOUNT,
|
account: PRIMARY_ADMIN_ACCOUNT,
|
||||||
mode: "server_file",
|
mode: "server_file",
|
||||||
updatedAt: "2026-03-29T00:00:00+08:00",
|
updatedAt: nowIso(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
masterAgentTasks: [],
|
masterAgentTasks: [],
|
||||||
@@ -1894,11 +1895,32 @@ function normalizeMessage(raw: Partial<Message>): Message {
|
|||||||
body: raw.body ?? "",
|
body: raw.body ?? "",
|
||||||
sentAt: raw.sentAt ?? nowIso(),
|
sentAt: raw.sentAt ?? nowIso(),
|
||||||
kind: raw.kind ?? "text",
|
kind: raw.kind ?? "text",
|
||||||
|
attachments: Array.isArray(raw.attachments)
|
||||||
|
? raw.attachments.map((attachment) => normalizeMessageAttachment(attachment))
|
||||||
|
: undefined,
|
||||||
forwardSource: raw.forwardSource,
|
forwardSource: raw.forwardSource,
|
||||||
forwardBundle: raw.forwardBundle,
|
forwardBundle: raw.forwardBundle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeMessageAttachment(raw: Partial<MessageAttachment>): MessageAttachment {
|
||||||
|
return {
|
||||||
|
attachmentId: raw.attachmentId ?? randomToken("att"),
|
||||||
|
fileName: raw.fileName ?? "",
|
||||||
|
mimeType: raw.mimeType ?? "application/octet-stream",
|
||||||
|
fileSizeBytes: raw.fileSizeBytes ?? 0,
|
||||||
|
attachmentKind: raw.attachmentKind ?? "binary",
|
||||||
|
storageBackend: raw.storageBackend ?? "server_file",
|
||||||
|
storagePath: raw.storagePath ?? "",
|
||||||
|
previewAvailable: raw.previewAvailable ?? false,
|
||||||
|
uploadedAt: raw.uploadedAt ?? nowIso(),
|
||||||
|
uploadedBy: raw.uploadedBy ?? "system",
|
||||||
|
analysisState: raw.analysisState ?? "not_applicable",
|
||||||
|
analysisSummary: raw.analysisSummary,
|
||||||
|
analysisCardId: raw.analysisCardId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeAttachmentStorageConfig(
|
function normalizeAttachmentStorageConfig(
|
||||||
raw: Partial<UserAttachmentStorageConfig>,
|
raw: Partial<UserAttachmentStorageConfig>,
|
||||||
fallback: UserAttachmentStorageConfig,
|
fallback: UserAttachmentStorageConfig,
|
||||||
@@ -2661,26 +2683,15 @@ export async function readState(): Promise<BossState> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const state = normalizeState(JSON.parse(raw) as Partial<BossState>);
|
const state = normalizeState(JSON.parse(raw) as Partial<BossState>);
|
||||||
const normalizedText = JSON.stringify(state, null, 2);
|
lastPersistedStateText = JSON.stringify(state, null, 2);
|
||||||
lastPersistedStateText = normalizedText;
|
|
||||||
if (normalizedText !== raw) {
|
|
||||||
await fs.writeFile(dataFile, normalizedText, "utf8");
|
|
||||||
await fs.writeFile(backupFile, normalizedText, "utf8");
|
|
||||||
}
|
|
||||||
return state;
|
return state;
|
||||||
} catch {
|
} catch {
|
||||||
const fallbackText =
|
const fallbackText =
|
||||||
(await fs.readFile(backupFile, "utf8").catch(() => null)) ??
|
(await fs.readFile(backupFile, "utf8").catch(() => null)) ??
|
||||||
lastPersistedStateText ??
|
lastPersistedStateText ??
|
||||||
JSON.stringify(syncDerivedState(cloneInitialState()), null, 2);
|
JSON.stringify(syncDerivedState(cloneInitialState()), null, 2);
|
||||||
await fs.writeFile(dataFile, fallbackText, "utf8");
|
|
||||||
const state = normalizeState(JSON.parse(fallbackText) as Partial<BossState>);
|
const state = normalizeState(JSON.parse(fallbackText) as Partial<BossState>);
|
||||||
const normalizedText = JSON.stringify(state, null, 2);
|
lastPersistedStateText = JSON.stringify(state, null, 2);
|
||||||
lastPersistedStateText = normalizedText;
|
|
||||||
if (normalizedText !== fallbackText) {
|
|
||||||
await fs.writeFile(dataFile, normalizedText, "utf8");
|
|
||||||
await fs.writeFile(backupFile, normalizedText, "utf8");
|
|
||||||
}
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user