chore: 保存剩余工作区改动并清理到干净状态
- 补 OnlyOffice callback 附件写回的 media asset bridge 适配层,并继续扩展 documents bridge/测试覆盖 - 提交当前 OMX hooks 删除与 Harness 运行状态文件更新,保留本轮任务执行轨迹 - 一并提交仓库中已跟踪的 Rust target 构建产物与相关协议桥接变更,确保工作区清零
This commit is contained in:
@@ -3,6 +3,14 @@ import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
import { getConvexHttpClient } from "@/lib/convex/server";
|
||||
import { api } from "@/lib/convex/api";
|
||||
import { resolveOnlyOfficeInternalUrl } from "@/lib/onlyoffice/internal-url";
|
||||
import {
|
||||
buildDocumentBridgeContextWithActor,
|
||||
buildDocumentCommandEnvelope,
|
||||
} from "@/lib/documents/bridge";
|
||||
import {
|
||||
executeMediaAssetWritebackBridgeCommand,
|
||||
type MediaAssetReplaceStoragePayload,
|
||||
} from "@/lib/documents/media-asset-command-adapter";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -137,10 +145,42 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 1 });
|
||||
}
|
||||
|
||||
await client.mutation(api.mediaAssets.replaceStorageFromUpload, {
|
||||
userId,
|
||||
id: assetId,
|
||||
storageId: storageId as any,
|
||||
const bridgeContext = buildDocumentBridgeContextWithActor({
|
||||
request,
|
||||
workspaceId: String((asset as any).workspace_id || "").trim() || null,
|
||||
actor: {
|
||||
actorType: "service",
|
||||
actorId: userId,
|
||||
sessionId: "onlyoffice-callback",
|
||||
},
|
||||
source: {
|
||||
channel: "onlyoffice-callback",
|
||||
client: "onlyoffice-document-server",
|
||||
},
|
||||
idempotencyKey: String(body.key || assetId || "").trim() || null,
|
||||
});
|
||||
const envelope = buildDocumentCommandEnvelope({
|
||||
name: "media.assets.replace_storage",
|
||||
payload: {
|
||||
assetId,
|
||||
documentId: String((asset as any).document_id || "").trim(),
|
||||
workspaceId: String((asset as any).workspace_id || "").trim() || null,
|
||||
storageId,
|
||||
userId,
|
||||
} satisfies MediaAssetReplaceStoragePayload,
|
||||
context: bridgeContext,
|
||||
target: {
|
||||
workspaceId: String((asset as any).workspace_id || "").trim() || null,
|
||||
pageId: String((asset as any).document_id || "").trim() || null,
|
||||
},
|
||||
reason: "ONLYOFFICE 回调写回附件存储",
|
||||
refs: [`onlyoffice:asset:${assetId}`],
|
||||
});
|
||||
|
||||
await executeMediaAssetWritebackBridgeCommand({
|
||||
client,
|
||||
context: bridgeContext,
|
||||
envelope,
|
||||
});
|
||||
|
||||
return NextResponse.json({ error: 0 });
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { api } from "@/lib/convex/api";
|
||||
import type { BridgeContext, BridgeTarget, CommandEnvelope } from "@/lib/documents/bridge";
|
||||
import { getAuthedConvexClient } from "@/lib/convex/route";
|
||||
|
||||
const bridgeLogsApi = api as any;
|
||||
import type { ConvexHttpClient } from "convex/browser";
|
||||
|
||||
function buildPayloadSummary(commandName: string, context: BridgeContext): string {
|
||||
return `command=${commandName};request_id=${context.requestId};trace_id=${context.traceId}`;
|
||||
@@ -15,17 +14,18 @@ function normalizeWorkspaceId(context: BridgeContext, target?: BridgeTarget | nu
|
||||
export async function recordBridgeCommandArtifacts<T>(input: {
|
||||
context: BridgeContext;
|
||||
envelope: CommandEnvelope<T>;
|
||||
client?: ConvexHttpClient;
|
||||
}): Promise<void> {
|
||||
const workspaceId = normalizeWorkspaceId(input.context, input.envelope.target);
|
||||
if (!workspaceId) return;
|
||||
|
||||
const { client } = await getAuthedConvexClient();
|
||||
const client = input.client ?? (await getAuthedConvexClient()).client;
|
||||
const commandLogId = `clog_${input.envelope.commandId}`;
|
||||
const eventId = `evt_${input.envelope.commandId}`;
|
||||
const now = new Date().toISOString();
|
||||
const payload = input.envelope.payload as Record<string, unknown>;
|
||||
|
||||
await client.mutation(bridgeLogsApi.bridgeLogs.recordCommandLog, {
|
||||
await client.mutation(api.bridgeLogs.recordCommandLog, {
|
||||
workspaceId,
|
||||
id: commandLogId,
|
||||
requestId: input.context.requestId,
|
||||
@@ -48,7 +48,7 @@ export async function recordBridgeCommandArtifacts<T>(input: {
|
||||
finishedAt: now,
|
||||
});
|
||||
|
||||
await client.mutation(bridgeLogsApi.bridgeLogs.recordDomainEvent, {
|
||||
await client.mutation(api.bridgeLogs.recordDomainEvent, {
|
||||
workspaceId,
|
||||
id: eventId,
|
||||
requestId: input.context.requestId,
|
||||
|
||||
@@ -30,10 +30,12 @@ import {
|
||||
assertStats,
|
||||
assertTitle,
|
||||
buildDocumentBridgeMutationRequest,
|
||||
buildDocumentBridgeContextWithActor,
|
||||
buildDocumentCommandEnvelope,
|
||||
buildDocumentQueryEnvelope,
|
||||
type BridgeContext,
|
||||
} from "@/lib/documents/bridge";
|
||||
import { executeMediaAssetWritebackBridgeCommand } from "@/lib/documents/media-asset-command-adapter";
|
||||
import { executeMetadataBridgeCommand } from "@/lib/documents/metadata-command-adapter";
|
||||
import { executeSaveBridgeCommand } from "@/lib/documents/save-command-adapter";
|
||||
import { buildDocumentSavePayload } from "@/lib/documents/save-contract";
|
||||
@@ -228,6 +230,83 @@ describe("documents bridge helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("buildDocumentBridgeMutationRequest builds media asset writeback runtime request", () => {
|
||||
const envelope = buildDocumentCommandEnvelope({
|
||||
name: "media.assets.replace_storage",
|
||||
payload: {
|
||||
assetId: "asset_1",
|
||||
documentId: "doc_1",
|
||||
workspaceId: "ws_1",
|
||||
storageId: "storage_1",
|
||||
userId: "user_1",
|
||||
},
|
||||
context: mockContext,
|
||||
target: { workspaceId: "ws_1", pageId: "doc_1" },
|
||||
});
|
||||
|
||||
const request = buildDocumentBridgeMutationRequest({
|
||||
context: mockContext,
|
||||
envelope,
|
||||
mapConvexArgs: (payload) => ({
|
||||
userId: payload.userId,
|
||||
id: payload.assetId,
|
||||
storageId: payload.storageId,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(request.functionName).toBe("mediaAssets:replaceStorageFromUpload");
|
||||
expect(request.workspaceId).toBe("ws_1");
|
||||
expect(request.args).toEqual({
|
||||
userId: "user_1",
|
||||
id: "asset_1",
|
||||
storageId: "storage_1",
|
||||
});
|
||||
expect(JSON.parse(request.payloadJson)).toMatchObject({
|
||||
kind: "command",
|
||||
name: "media.assets.replace_storage",
|
||||
workspace_id: "ws_1",
|
||||
request_id: "req_1",
|
||||
trace_id: "trace_1",
|
||||
});
|
||||
});
|
||||
|
||||
it("buildDocumentBridgeContextWithActor keeps explicit actor and source", () => {
|
||||
const request = new Request("http://127.0.0.1:3001/api/onlyoffice/callback", {
|
||||
headers: {
|
||||
"x-request-id": "req_oo_1",
|
||||
"x-trace-id": "trace_oo_1",
|
||||
},
|
||||
});
|
||||
|
||||
const context = buildDocumentBridgeContextWithActor({
|
||||
request,
|
||||
workspaceId: "ws_1",
|
||||
actor: {
|
||||
actorType: "service",
|
||||
actorId: "user_1",
|
||||
sessionId: "onlyoffice-callback",
|
||||
},
|
||||
source: {
|
||||
channel: "onlyoffice-callback",
|
||||
client: "onlyoffice-document-server",
|
||||
},
|
||||
idempotencyKey: "asset_1",
|
||||
});
|
||||
|
||||
expect(context.requestId).toBe("req_oo_1");
|
||||
expect(context.traceId).toBe("trace_oo_1");
|
||||
expect(context.actor).toEqual({
|
||||
actorType: "service",
|
||||
actorId: "user_1",
|
||||
sessionId: "onlyoffice-callback",
|
||||
});
|
||||
expect(context.source).toEqual({
|
||||
channel: "onlyoffice-callback",
|
||||
client: "onlyoffice-document-server",
|
||||
});
|
||||
expect(context.idempotencyKey).toBe("asset_1");
|
||||
});
|
||||
|
||||
it("executeMetadataBridgeCommand routes title update through adapter", async () => {
|
||||
const mutation = vi.fn().mockResolvedValue({ ok: true });
|
||||
const { getAuthedConvexClient } = await import("@/lib/convex/route");
|
||||
@@ -400,4 +479,43 @@ describe("documents bridge helpers", () => {
|
||||
code: "REJECTED",
|
||||
});
|
||||
});
|
||||
|
||||
it("executeMediaAssetWritebackBridgeCommand routes callback writeback through adapter", async () => {
|
||||
const mutation = vi.fn().mockResolvedValue({ ok: true, fileUrl: "https://example.com/file.docx" });
|
||||
const { recordBridgeCommandArtifacts } = await import("@/lib/documents/bridge-log");
|
||||
vi.mocked(recordBridgeCommandArtifacts).mockResolvedValue(undefined);
|
||||
|
||||
await executeMediaAssetWritebackBridgeCommand({
|
||||
client: {
|
||||
mutation,
|
||||
} as unknown as ConvexHttpClient,
|
||||
context: mockContext,
|
||||
envelope: buildDocumentCommandEnvelope({
|
||||
name: "media.assets.replace_storage",
|
||||
payload: {
|
||||
assetId: "asset_1",
|
||||
documentId: "doc_1",
|
||||
workspaceId: "ws_1",
|
||||
storageId: "storage_1",
|
||||
userId: "user_1",
|
||||
},
|
||||
context: mockContext,
|
||||
target: { workspaceId: "ws_1", pageId: "doc_1" },
|
||||
}),
|
||||
});
|
||||
|
||||
expect(mutation).toHaveBeenCalledTimes(1);
|
||||
expect(mutation.mock.calls[0]?.[1]).toEqual({
|
||||
userId: "user_1",
|
||||
id: "asset_1",
|
||||
storageId: "storage_1",
|
||||
});
|
||||
expect(recordBridgeCommandArtifacts).toHaveBeenCalledWith({
|
||||
client: expect.any(Object),
|
||||
context: mockContext,
|
||||
envelope: expect.objectContaining({
|
||||
name: "media.assets.replace_storage",
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -101,6 +101,7 @@ const DOCUMENT_BRIDGE_MUTATION_FUNCTIONS = {
|
||||
"documents.stats.update": "documents:updateStats",
|
||||
"documents.options.update": "documents:updateOptions",
|
||||
"documents.save": "documents:updateContent",
|
||||
"media.assets.replace_storage": "mediaAssets:replaceStorageFromUpload",
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
function readHeaderValue(headerList: Headers, ...candidates: string[]): string | null {
|
||||
@@ -180,6 +181,55 @@ export async function buildDocumentBridgeContext(input: {
|
||||
};
|
||||
}
|
||||
|
||||
export function buildDocumentBridgeContextWithActor(input: {
|
||||
request: Request;
|
||||
actor: BridgeActor;
|
||||
workspaceId?: string | null;
|
||||
idempotencyKey?: string | null;
|
||||
validateOnly?: boolean;
|
||||
dryRun?: boolean;
|
||||
source?: Partial<BridgeSource>;
|
||||
authToken?: string | null;
|
||||
}): BridgeContext {
|
||||
const headerList = input.request.headers;
|
||||
const requestId =
|
||||
readHeaderValue(headerList, "x-request-id", "x-mnote-request-id") ?? makeFallbackId("req");
|
||||
const traceId =
|
||||
readHeaderValue(headerList, "x-trace-id", "x-mnote-trace-id", "x-request-id") ?? makeFallbackId("trace");
|
||||
const sourceChannel = input.source?.channel?.trim() || readHeaderValue(headerList, "x-source-channel") || "next-route";
|
||||
const sourceClient =
|
||||
input.source?.client?.trim() ||
|
||||
readHeaderValue(headerList, "x-source-client", "user-agent") ||
|
||||
"wolai-frontend";
|
||||
const deploymentId =
|
||||
readHeaderValue(headerList, "x-deployment-id") ?? process.env.VERCEL_DEPLOYMENT_ID ?? null;
|
||||
const projectId = readHeaderValue(headerList, "x-project-id") ?? process.env.VERCEL_PROJECT_ID ?? null;
|
||||
const tenantId = readHeaderValue(headerList, "x-tenant-id");
|
||||
const authToken = input.authToken ?? readHeaderValue(headerList, "authorization");
|
||||
const idempotencyKey =
|
||||
input.idempotencyKey ?? readHeaderValue(headerList, "idempotency-key", "x-idempotency-key");
|
||||
const validateOnly = input.validateOnly ?? toBooleanFlag(readHeaderValue(headerList, "x-validate-only"));
|
||||
const dryRun = input.dryRun ?? toBooleanFlag(readHeaderValue(headerList, "x-dry-run"));
|
||||
|
||||
return {
|
||||
deploymentId,
|
||||
projectId,
|
||||
workspaceId: input.workspaceId ?? null,
|
||||
requestId,
|
||||
traceId,
|
||||
actor: input.actor,
|
||||
source: {
|
||||
channel: sourceChannel,
|
||||
client: sourceClient,
|
||||
},
|
||||
tenantId,
|
||||
authToken,
|
||||
idempotencyKey,
|
||||
validateOnly,
|
||||
dryRun,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildDocumentCommandEnvelope<T>(input: {
|
||||
name: string;
|
||||
payload: T;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { ConvexHttpClient } from "convex/browser";
|
||||
import { api } from "@/lib/convex/api";
|
||||
import {
|
||||
buildDocumentBridgeMutationRequest,
|
||||
executeDocumentBridgeMutationRequest,
|
||||
type CommandEnvelope,
|
||||
type BridgeContext,
|
||||
} from "@/lib/documents/bridge";
|
||||
import { recordBridgeCommandArtifacts } from "@/lib/documents/bridge-log";
|
||||
|
||||
export type MediaAssetReplaceStoragePayload = {
|
||||
assetId: string;
|
||||
documentId: string;
|
||||
workspaceId: string | null;
|
||||
storageId: string;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export type MediaAssetWritebackExecutionResult = {
|
||||
requestId: string;
|
||||
traceId: string;
|
||||
commandId: string;
|
||||
commandName: string;
|
||||
};
|
||||
|
||||
export async function executeMediaAssetWritebackBridgeCommand(input: {
|
||||
client: ConvexHttpClient;
|
||||
context: BridgeContext;
|
||||
envelope: CommandEnvelope<MediaAssetReplaceStoragePayload>;
|
||||
}): Promise<MediaAssetWritebackExecutionResult> {
|
||||
const mutationRequest = buildDocumentBridgeMutationRequest({
|
||||
context: input.context,
|
||||
envelope: input.envelope,
|
||||
mapConvexArgs: (payload) => ({
|
||||
userId: payload.userId,
|
||||
id: payload.assetId,
|
||||
storageId: payload.storageId as any,
|
||||
}),
|
||||
});
|
||||
|
||||
await executeDocumentBridgeMutationRequest({
|
||||
client: input.client,
|
||||
mutation: api.mediaAssets.replaceStorageFromUpload,
|
||||
request: mutationRequest,
|
||||
});
|
||||
|
||||
try {
|
||||
await recordBridgeCommandArtifacts({
|
||||
client: input.client,
|
||||
context: input.context,
|
||||
envelope: input.envelope,
|
||||
});
|
||||
} catch (error) {
|
||||
// 说明:OnlyOffice callback 写回的主事实是附件存储替换;日志落账失败不应反向导致保存失败。
|
||||
console.warn("[onlyoffice/callback] bridge log write skipped:", error);
|
||||
}
|
||||
|
||||
return {
|
||||
requestId: input.context.requestId,
|
||||
traceId: input.context.traceId,
|
||||
commandId: input.envelope.commandId,
|
||||
commandName: input.envelope.name,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user