Implement attachment analysis task flow
This commit is contained in:
141
scripts/validate-attachment-analysis.mjs
Normal file
141
scripts/validate-attachment-analysis.mjs
Normal file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const runtimeDir = await fs.mkdtemp(path.join(os.tmpdir(), "boss-attachment-analysis-"));
|
||||
const stateFile = path.join(runtimeDir, "data", "boss-state.json");
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
process.env.BOSS_RUNTIME_ROOT = runtimeDir;
|
||||
process.env.BOSS_STATE_FILE = stateFile;
|
||||
process.env.BOSS_AUTH_AUTO_LOGIN = "0";
|
||||
|
||||
const { NextRequest } = require("next/server");
|
||||
const authLoginRoute = require(path.join(rootDir, ".next/standalone/.next/server/app/api/auth/login/route.js"));
|
||||
const attachmentsRoute = require(
|
||||
path.join(rootDir, ".next/standalone/.next/server/app/api/v1/projects/[projectId]/attachments/route.js"),
|
||||
);
|
||||
const analyzeRoute = require(
|
||||
path.join(
|
||||
rootDir,
|
||||
".next/standalone/.next/server/app/api/v1/projects/[projectId]/attachments/[attachmentId]/analyze/route.js",
|
||||
),
|
||||
);
|
||||
|
||||
const loginHandler = authLoginRoute.routeModule.userland.POST;
|
||||
const uploadHandler = attachmentsRoute.routeModule.userland.POST;
|
||||
const analyzeHandler = analyzeRoute.routeModule.userland.POST;
|
||||
|
||||
async function invokeRoute(handler, url, init = {}, context) {
|
||||
const request = new NextRequest(url, {
|
||||
method: init.method ?? "GET",
|
||||
headers: init.headers,
|
||||
body: init.body,
|
||||
});
|
||||
return handler(request, context);
|
||||
}
|
||||
|
||||
function parseCookieValue(setCookieHeader, cookieName) {
|
||||
assert.ok(setCookieHeader, "set-cookie header is missing");
|
||||
const match = setCookieHeader.match(new RegExp(`${cookieName}=([^;]+)`));
|
||||
assert.ok(match, `${cookieName} cookie is missing`);
|
||||
return match[1];
|
||||
}
|
||||
|
||||
async function loginAsAdmin() {
|
||||
const response = await invokeRoute(
|
||||
loginHandler,
|
||||
"http://localhost/api/auth/login",
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
account: "17600003315",
|
||||
password: "boss123456",
|
||||
method: "password",
|
||||
}),
|
||||
},
|
||||
);
|
||||
assert.equal(response.status, 200, "login should succeed");
|
||||
const payload = await response.json();
|
||||
assert.equal(payload.ok, true, "login payload should be ok");
|
||||
const cookie = parseCookieValue(response.headers.get("set-cookie"), "boss_session");
|
||||
return { cookie, payload };
|
||||
}
|
||||
|
||||
async function uploadAttachment(cookie, projectId, fileName, type, bytes) {
|
||||
const form = new FormData();
|
||||
form.set("file", new File([bytes], fileName, { type }));
|
||||
|
||||
const response = await invokeRoute(
|
||||
uploadHandler,
|
||||
`http://localhost/api/v1/projects/${projectId}/attachments`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { cookie: `boss_session=${cookie}` },
|
||||
body: form,
|
||||
},
|
||||
{ params: Promise.resolve({ projectId }) },
|
||||
);
|
||||
assert.equal(response.status, 200, `upload ${fileName} should succeed`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
const { cookie } = await loginAsAdmin();
|
||||
|
||||
const textUpload = await uploadAttachment(
|
||||
cookie,
|
||||
"master-agent",
|
||||
"analysis-note.txt",
|
||||
"text/plain",
|
||||
Buffer.from("text attachment for automatic analysis"),
|
||||
);
|
||||
assert.equal(textUpload.attachment.analysisState, "queued_auto", "text attachment should queue automatically");
|
||||
assert.ok(textUpload.analysisTask, "queued auto attachment should create a master agent task");
|
||||
assert.equal(textUpload.analysisTask.taskType, "attachment_analysis", "queued task type should be attachment_analysis");
|
||||
assert.equal(
|
||||
textUpload.analysisTask.attachmentFileName,
|
||||
"analysis-note.txt",
|
||||
"queued task should carry attachment file name",
|
||||
);
|
||||
|
||||
const manualUpload = await uploadAttachment(
|
||||
cookie,
|
||||
"master-agent",
|
||||
"manual-binary.bin",
|
||||
"application/octet-stream",
|
||||
Buffer.from([0, 1, 2, 3]),
|
||||
);
|
||||
assert.equal(manualUpload.attachment.analysisState, "ready_manual", "binary attachment should be manually analyzable");
|
||||
|
||||
const analyzeResponse = await invokeRoute(
|
||||
analyzeHandler,
|
||||
`http://localhost/api/v1/projects/master-agent/attachments/${manualUpload.attachment.attachmentId}/analyze`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { cookie: `boss_session=${cookie}` },
|
||||
},
|
||||
{
|
||||
params: Promise.resolve({
|
||||
projectId: "master-agent",
|
||||
attachmentId: manualUpload.attachment.attachmentId,
|
||||
}),
|
||||
},
|
||||
);
|
||||
assert.equal(analyzeResponse.status, 200, "manual analyze should succeed");
|
||||
const analyzePayload = await analyzeResponse.json();
|
||||
assert.ok(analyzePayload.taskId, "manual analyze should return a taskId");
|
||||
assert.ok(analyzePayload.task, "manual analyze should return a task payload");
|
||||
assert.equal(analyzePayload.task.taskType, "attachment_analysis", "manual analyze task should be attachment_analysis");
|
||||
assert.equal(
|
||||
analyzePayload.task.attachmentId,
|
||||
manualUpload.attachment.attachmentId,
|
||||
"manual task should link the attachment",
|
||||
);
|
||||
|
||||
console.log("attachment analysis validation passed");
|
||||
@@ -0,0 +1,56 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { canSessionAccessAttachmentProject } from "@/lib/boss-attachment-access";
|
||||
import { getProjectAttachment, readState } from "@/lib/boss-data";
|
||||
import { queueAttachmentAnalysisTask } from "@/lib/boss-master-agent";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ projectId: string; attachmentId: string }> },
|
||||
) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { projectId, attachmentId } = await context.params;
|
||||
const record = await getProjectAttachment(projectId, attachmentId);
|
||||
if (!record) {
|
||||
return NextResponse.json({ ok: false, message: "ATTACHMENT_NOT_FOUND" }, { status: 404 });
|
||||
}
|
||||
|
||||
const state = await readState();
|
||||
if (!canSessionAccessAttachmentProject(state, session, record.project)) {
|
||||
return NextResponse.json({ ok: false, message: "FORBIDDEN" }, { status: 403 });
|
||||
}
|
||||
|
||||
if (record.attachment.analysisState !== "ready_manual" && record.attachment.analysisState !== "failed") {
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: false,
|
||||
message: "ATTACHMENT_NOT_READY_FOR_MANUAL_ANALYSIS",
|
||||
analysisState: record.attachment.analysisState,
|
||||
},
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const task = await queueAttachmentAnalysisTask({
|
||||
projectId,
|
||||
attachmentId,
|
||||
requestMessageId: record.message.id,
|
||||
requestedBy: session.displayName || "你",
|
||||
requestedByAccount: session.account,
|
||||
markProcessing: true,
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true, taskId: task.taskId, task });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "UNKNOWN_ERROR";
|
||||
const status = message === "ATTACHMENT_NOT_FOUND" ? 404 : 500;
|
||||
return NextResponse.json({ ok: false, message }, { status });
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
readState,
|
||||
type MessageAttachment,
|
||||
} from "@/lib/boss-data";
|
||||
import { queueAttachmentAnalysisTask } from "@/lib/boss-master-agent";
|
||||
import { detectAttachmentKind, resolveAttachmentAnalysisState } from "@/lib/boss-attachments";
|
||||
import { getAttachmentStorageProvider } from "@/lib/boss-storage";
|
||||
|
||||
@@ -81,10 +82,22 @@ export async function POST(
|
||||
attachment,
|
||||
});
|
||||
|
||||
let analysisTask = null;
|
||||
if (attachment.analysisState === "queued_auto") {
|
||||
analysisTask = await queueAttachmentAnalysisTask({
|
||||
projectId,
|
||||
attachmentId,
|
||||
requestMessageId: message.id,
|
||||
requestedBy: session.displayName || "你",
|
||||
requestedByAccount: session.account,
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
attachment,
|
||||
message,
|
||||
analysisTask,
|
||||
downloadUrl: `/api/v1/attachments/${attachmentId}/download`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -121,6 +121,7 @@ export type AiProvider = "master_codex_node" | "openai_api";
|
||||
export type AiAccountRole = "primary" | "backup" | "api_fallback";
|
||||
export type AiAccountStatus = "ready" | "needs_login" | "needs_api_key" | "degraded" | "disabled";
|
||||
export type MasterAgentTaskStatus = "queued" | "running" | "completed" | "failed";
|
||||
export type MasterAgentTaskType = "conversation_reply" | "attachment_analysis";
|
||||
|
||||
export interface UserSettings {
|
||||
liveUpdates: boolean;
|
||||
@@ -393,6 +394,7 @@ export interface MasterIdentitySummary {
|
||||
export interface MasterAgentTask {
|
||||
taskId: string;
|
||||
projectId: string;
|
||||
taskType: MasterAgentTaskType;
|
||||
requestMessageId: string;
|
||||
requestText: string;
|
||||
executionPrompt: string;
|
||||
@@ -401,6 +403,8 @@ export interface MasterAgentTask {
|
||||
deviceId: string;
|
||||
accountId?: string;
|
||||
accountLabel?: string;
|
||||
attachmentId?: string;
|
||||
attachmentFileName?: string;
|
||||
status: MasterAgentTaskStatus;
|
||||
requestedAt: string;
|
||||
claimedAt?: string;
|
||||
@@ -2104,6 +2108,7 @@ function normalizeState(raw: Partial<BossState> | undefined): BossState {
|
||||
masterAgentTasks: ensureArray(raw.masterAgentTasks, base.masterAgentTasks).map((task) => ({
|
||||
taskId: task.taskId ?? randomToken("mastertask"),
|
||||
projectId: task.projectId ?? "master-agent",
|
||||
taskType: task.taskType ?? "conversation_reply",
|
||||
requestMessageId: task.requestMessageId ?? "",
|
||||
requestText: task.requestText ?? "",
|
||||
executionPrompt: task.executionPrompt ?? task.requestText ?? "",
|
||||
@@ -2112,6 +2117,8 @@ function normalizeState(raw: Partial<BossState> | undefined): BossState {
|
||||
deviceId: task.deviceId ?? PRIMARY_CODEX_NODE_ID,
|
||||
accountId: task.accountId,
|
||||
accountLabel: task.accountLabel,
|
||||
attachmentId: task.attachmentId,
|
||||
attachmentFileName: task.attachmentFileName,
|
||||
status: task.status ?? "queued",
|
||||
requestedAt: task.requestedAt ?? nowIso(),
|
||||
claimedAt: task.claimedAt,
|
||||
@@ -3429,6 +3436,8 @@ export async function getMasterAgentRuntimeAccount() {
|
||||
}
|
||||
|
||||
export async function queueMasterAgentTask(payload: {
|
||||
projectId?: string;
|
||||
taskType?: MasterAgentTaskType;
|
||||
requestMessageId: string;
|
||||
requestText: string;
|
||||
executionPrompt: string;
|
||||
@@ -3437,11 +3446,14 @@ export async function queueMasterAgentTask(payload: {
|
||||
deviceId: string;
|
||||
accountId?: string;
|
||||
accountLabel?: string;
|
||||
attachmentId?: string;
|
||||
attachmentFileName?: string;
|
||||
}) {
|
||||
const task = await mutateState((state) => {
|
||||
const task: MasterAgentTask = {
|
||||
taskId: randomToken("mastertask"),
|
||||
projectId: "master-agent",
|
||||
projectId: payload.projectId ?? "master-agent",
|
||||
taskType: payload.taskType ?? "conversation_reply",
|
||||
requestMessageId: payload.requestMessageId,
|
||||
requestText: payload.requestText,
|
||||
executionPrompt: payload.executionPrompt,
|
||||
@@ -3450,6 +3462,8 @@ export async function queueMasterAgentTask(payload: {
|
||||
deviceId: payload.deviceId,
|
||||
accountId: payload.accountId,
|
||||
accountLabel: payload.accountLabel,
|
||||
attachmentId: payload.attachmentId,
|
||||
attachmentFileName: payload.attachmentFileName,
|
||||
status: "queued",
|
||||
requestedAt: nowIso(),
|
||||
};
|
||||
@@ -3470,6 +3484,7 @@ export async function getMasterAgentTask(taskId: string) {
|
||||
}
|
||||
|
||||
export async function claimNextMasterAgentTask(deviceId: string) {
|
||||
let attachmentProjectId: string | undefined;
|
||||
const task = await mutateState((state) => {
|
||||
const next = state.masterAgentTasks.find(
|
||||
(item) => item.deviceId === deviceId && item.status === "queued",
|
||||
@@ -3477,6 +3492,16 @@ export async function claimNextMasterAgentTask(deviceId: string) {
|
||||
if (!next) return null;
|
||||
next.status = "running";
|
||||
next.claimedAt = nowIso();
|
||||
if (next.taskType === "attachment_analysis" && next.attachmentId) {
|
||||
const project = state.projects.find((item) => item.id === next.projectId);
|
||||
const match = project ? findProjectAttachment(project, next.attachmentId) : null;
|
||||
if (match) {
|
||||
match.attachment.analysisState = "processing";
|
||||
match.attachment.analysisSummary = undefined;
|
||||
match.attachment.analysisCardId = undefined;
|
||||
attachmentProjectId = next.projectId;
|
||||
}
|
||||
}
|
||||
return { ...next };
|
||||
});
|
||||
if (task) {
|
||||
@@ -3485,6 +3510,10 @@ export async function claimNextMasterAgentTask(deviceId: string) {
|
||||
deviceId: task.deviceId,
|
||||
status: task.status,
|
||||
});
|
||||
if (attachmentProjectId) {
|
||||
publishBossEvent("project.messages.updated", { projectId: attachmentProjectId });
|
||||
publishBossEvent("conversation.updated", { projectId: attachmentProjectId });
|
||||
}
|
||||
}
|
||||
return task;
|
||||
}
|
||||
@@ -3531,15 +3560,56 @@ export async function completeMasterAgentTask(payload: {
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.status === "completed" && task.replyBody) {
|
||||
pushProjectLedgerMessage(state, "master-agent", {
|
||||
let attachmentProjectId: string | undefined;
|
||||
if (task.taskType === "attachment_analysis" && task.attachmentId) {
|
||||
const project = state.projects.find((item) => item.id === task.projectId);
|
||||
const match = project ? findProjectAttachment(project, task.attachmentId) : null;
|
||||
if (match) {
|
||||
attachmentProjectId = project?.id;
|
||||
if (payload.status === "completed") {
|
||||
const summary = summarizeAttachmentAnalysis(task.replyBody ?? "");
|
||||
match.attachment.analysisState = "completed";
|
||||
match.attachment.analysisSummary = summary;
|
||||
pushProjectLedgerMessage(state, task.projectId, {
|
||||
sender: "master",
|
||||
senderLabel: task.accountLabel ? `主 Agent · ${task.accountLabel}` : "主 Agent",
|
||||
body: summary,
|
||||
kind: "text",
|
||||
});
|
||||
if (task.replyBody) {
|
||||
const card = pushProjectLedgerMessage(state, task.projectId, {
|
||||
sender: "master",
|
||||
senderLabel: task.accountLabel ? `主 Agent · ${task.accountLabel}` : "主 Agent",
|
||||
body: task.replyBody,
|
||||
kind: "analysis_card",
|
||||
});
|
||||
match.attachment.analysisCardId = card?.id;
|
||||
} else {
|
||||
match.attachment.analysisCardId = undefined;
|
||||
}
|
||||
} else if (payload.status === "failed") {
|
||||
match.attachment.analysisState = "failed";
|
||||
match.attachment.analysisSummary = task.errorMessage ?? "附件分析失败,请稍后重试。";
|
||||
match.attachment.analysisCardId = undefined;
|
||||
pushProjectLedgerMessage(state, task.projectId, {
|
||||
sender: "ops",
|
||||
senderLabel: task.accountLabel ? `主 Agent Relay · ${task.accountLabel}` : "主 Agent Relay",
|
||||
body: `附件分析失败:${task.errorMessage ?? "UNKNOWN_ERROR"}`,
|
||||
kind: "text",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!attachmentProjectId && payload.status === "completed" && task.replyBody) {
|
||||
pushProjectLedgerMessage(state, task.projectId, {
|
||||
sender: "master",
|
||||
senderLabel: task.accountLabel ? `主 Agent · ${task.accountLabel}` : "主 Agent",
|
||||
body: task.replyBody,
|
||||
kind: "text",
|
||||
});
|
||||
} else if (payload.status === "failed") {
|
||||
pushProjectLedgerMessage(state, "master-agent", {
|
||||
} else if (!attachmentProjectId && payload.status === "failed") {
|
||||
pushProjectLedgerMessage(state, task.projectId, {
|
||||
sender: "ops",
|
||||
senderLabel: task.accountLabel ? `主 Agent Relay · ${task.accountLabel}` : "主 Agent Relay",
|
||||
body: `Master Codex Node 执行失败:${task.errorMessage ?? "UNKNOWN_ERROR"}`,
|
||||
@@ -4482,6 +4552,23 @@ export function findProjectAttachment(
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getProjectAttachment(projectId: string, attachmentId: string) {
|
||||
const state = await readState();
|
||||
const project = state.projects.find((item) => item.id === projectId);
|
||||
if (!project) {
|
||||
return null;
|
||||
}
|
||||
const match = findProjectAttachment(project, attachmentId);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
project,
|
||||
message: match.message,
|
||||
attachment: match.attachment,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAttachmentById(attachmentId: string) {
|
||||
const state = await readState();
|
||||
for (const project of state.projects) {
|
||||
@@ -4497,6 +4584,70 @@ export async function getAttachmentById(attachmentId: string) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function summarizeAttachmentAnalysis(body: string) {
|
||||
const compact = body.replace(/\s+/g, " ").trim();
|
||||
if (!compact) {
|
||||
return "附件分析已完成。";
|
||||
}
|
||||
return compact.length <= 120 ? compact : `${compact.slice(0, 117)}...`;
|
||||
}
|
||||
|
||||
export async function updateAttachmentAnalysisResult(payload: {
|
||||
projectId: string;
|
||||
attachmentId: string;
|
||||
status: Exclude<AttachmentAnalysisState, "not_applicable" | "queued_auto" | "ready_manual">;
|
||||
summary?: string;
|
||||
cardBody?: string;
|
||||
}) {
|
||||
return mutateState((state) => {
|
||||
const project = state.projects.find((item) => item.id === payload.projectId);
|
||||
if (!project) {
|
||||
throw new Error("PROJECT_NOT_FOUND");
|
||||
}
|
||||
const match = findProjectAttachment(project, payload.attachmentId);
|
||||
if (!match) {
|
||||
throw new Error("ATTACHMENT_NOT_FOUND");
|
||||
}
|
||||
|
||||
match.attachment.analysisState = payload.status;
|
||||
match.attachment.analysisSummary =
|
||||
payload.status === "completed"
|
||||
? payload.summary?.trim() || summarizeAttachmentAnalysis(payload.cardBody ?? "")
|
||||
: payload.summary;
|
||||
match.attachment.analysisCardId = undefined;
|
||||
|
||||
if (payload.status === "completed" && payload.cardBody?.trim()) {
|
||||
const summary = payload.summary?.trim() || summarizeAttachmentAnalysis(payload.cardBody);
|
||||
pushProjectLedgerMessage(state, payload.projectId, {
|
||||
sender: "master",
|
||||
senderLabel: "主 Agent",
|
||||
body: summary,
|
||||
kind: "text",
|
||||
});
|
||||
const card = pushProjectLedgerMessage(state, payload.projectId, {
|
||||
sender: "master",
|
||||
senderLabel: "主 Agent",
|
||||
body: payload.cardBody.trim(),
|
||||
kind: "analysis_card",
|
||||
});
|
||||
match.attachment.analysisCardId = card?.id;
|
||||
match.attachment.analysisSummary = summary;
|
||||
}
|
||||
|
||||
return {
|
||||
projectId: payload.projectId,
|
||||
attachmentId: payload.attachmentId,
|
||||
analysisState: match.attachment.analysisState,
|
||||
analysisSummary: match.attachment.analysisSummary,
|
||||
analysisCardId: match.attachment.analysisCardId,
|
||||
};
|
||||
}).then((result) => {
|
||||
publishBossEvent("project.messages.updated", { projectId: result.projectId });
|
||||
publishBossEvent("conversation.updated", { projectId: result.projectId });
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
function requiresForwardApproval(source: Project, target: Project) {
|
||||
return source.collaborationMode === "approval_required" && target.id !== "master-agent";
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@ import {
|
||||
AUTH_SESSION_TTL_MS,
|
||||
aiProviderLabel,
|
||||
appendProjectMessage,
|
||||
getProjectAttachment,
|
||||
getRuntimeAiAccountById,
|
||||
getMasterAgentRuntimeAccount,
|
||||
getMasterAgentTask,
|
||||
queueMasterAgentTask,
|
||||
readState,
|
||||
updateAttachmentAnalysisResult,
|
||||
updateAiAccountHealth,
|
||||
} from "@/lib/boss-data";
|
||||
|
||||
@@ -218,6 +220,91 @@ async function waitForMasterAgentTaskCompletion(taskId: string, timeoutMs = 55_0
|
||||
return getMasterAgentTask(taskId);
|
||||
}
|
||||
|
||||
function buildAttachmentAnalysisPrompt(params: {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
attachment: NonNullable<Awaited<ReturnType<typeof getProjectAttachment>>>["attachment"];
|
||||
messageBody: string;
|
||||
requestedBy: string;
|
||||
requestedByAccount: string;
|
||||
}) {
|
||||
const attachment = params.attachment;
|
||||
return [
|
||||
"你是 Boss 控制台的附件分析主 Agent。",
|
||||
"请只根据下面的附件元数据和你能实际读取到的附件内容进行分析。",
|
||||
"如果你无法直接读取原始内容,不要假装已经看过内容,必须明确说明限制,并只基于元数据给出判断。",
|
||||
"输出要求:",
|
||||
"1. 一句话结论",
|
||||
"2. 内容摘要或可见特征",
|
||||
"3. 风险或异常",
|
||||
"4. 建议动作",
|
||||
"",
|
||||
`projectId: ${params.projectId}`,
|
||||
`projectName: ${params.projectName}`,
|
||||
`requestedBy: ${params.requestedBy}`,
|
||||
`requestedByAccount: ${params.requestedByAccount}`,
|
||||
`attachmentId: ${attachment.attachmentId}`,
|
||||
`fileName: ${attachment.fileName}`,
|
||||
`mimeType: ${attachment.mimeType}`,
|
||||
`fileSizeBytes: ${attachment.fileSizeBytes}`,
|
||||
`attachmentKind: ${attachment.attachmentKind}`,
|
||||
`storageBackend: ${attachment.storageBackend}`,
|
||||
`storagePath: ${attachment.storagePath}`,
|
||||
`previewAvailable: ${attachment.previewAvailable ? "yes" : "no"}`,
|
||||
`uploadedAt: ${attachment.uploadedAt}`,
|
||||
`uploadedBy: ${attachment.uploadedBy}`,
|
||||
`analysisState: ${attachment.analysisState}`,
|
||||
"",
|
||||
"原始消息:",
|
||||
params.messageBody || "无",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export async function queueAttachmentAnalysisTask(params: {
|
||||
projectId: string;
|
||||
attachmentId: string;
|
||||
requestMessageId: string;
|
||||
requestedBy: string;
|
||||
requestedByAccount: string;
|
||||
markProcessing?: boolean;
|
||||
}) {
|
||||
const record = await getProjectAttachment(params.projectId, params.attachmentId);
|
||||
if (!record) {
|
||||
throw new Error("ATTACHMENT_NOT_FOUND");
|
||||
}
|
||||
|
||||
const state = await readState();
|
||||
const task = await queueMasterAgentTask({
|
||||
projectId: record.project.id,
|
||||
taskType: "attachment_analysis",
|
||||
requestMessageId: params.requestMessageId,
|
||||
requestText: `分析附件《${record.attachment.fileName}》`,
|
||||
executionPrompt: buildAttachmentAnalysisPrompt({
|
||||
projectId: record.project.id,
|
||||
projectName: record.project.name,
|
||||
attachment: record.attachment,
|
||||
messageBody: record.message.body,
|
||||
requestedBy: params.requestedBy,
|
||||
requestedByAccount: params.requestedByAccount,
|
||||
}),
|
||||
requestedBy: params.requestedBy,
|
||||
requestedByAccount: params.requestedByAccount,
|
||||
deviceId: state.user.boundDeviceId || "mac-studio",
|
||||
attachmentId: record.attachment.attachmentId,
|
||||
attachmentFileName: record.attachment.fileName,
|
||||
});
|
||||
|
||||
if (params.markProcessing) {
|
||||
await updateAttachmentAnalysisResult({
|
||||
projectId: params.projectId,
|
||||
attachmentId: params.attachmentId,
|
||||
status: "processing",
|
||||
});
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
export async function validateAiAccountConnection(accountId: string) {
|
||||
const account = await getRuntimeAiAccountById(accountId);
|
||||
if (!account) {
|
||||
|
||||
Reference in New Issue
Block a user