haixunMaster/lib/ai/refine-research-map.ts

146 lines
4.1 KiB
TypeScript
Raw Normal View History

2026-06-21 12:50:31 +00:00
import { z } from "zod";
import type { ProviderApiKeys } from "./keys";
import { generateStructuredObject } from "./generate-structured";
import { getModel } from "./provider";
import { filterSuggestedTags, preserveTagsFromPreviousMap } from "./normalize-suggested-tags";
import { buildResearchMapSystemPrompt } from "./prompts/research-map";
import { buildTopicAnchorPromptBlock } from "@/lib/topic-anchor";
import type { ResearchMap } from "@/lib/types/research";
const searchIntentSchema = z.enum([
"痛點",
"知識",
"經驗",
"對比",
"工具",
"語錄",
"需求",
"求助",
]);
const searchTypeSchema = z.enum(["短詞", "情境", "帳號", "語錄"]);
const researchMapSchema = z.object({
audienceSummary: z.string(),
contentGoal: z.string(),
questions: z.array(z.string()).min(1).max(12),
pillars: z.array(z.string()).min(1).max(8),
suggestedTags: z
.array(
z.object({
tag: z.string(),
reason: z.string(),
searchIntent: searchIntentSchema,
searchType: searchTypeSchema.optional(),
})
)
.min(1)
.max(15),
similarAccounts: z
.array(
z.object({
username: z.string(),
reason: z.string(),
postUrl: z.string().optional(),
profileUrl: z.string().optional(),
source: z.enum(["web", "threads", "scan"]).optional(),
})
)
.optional(),
exclusions: z.array(z.string()).min(1).max(12),
});
const refineResponseSchema = z.object({
reply: z.string(),
researchMap: researchMapSchema,
});
export interface RefineChatMessage {
role: "user" | "assistant";
content: string;
}
export interface RefineResearchMapInput {
label: string;
query: string;
brief?: string | null;
persona?: string | null;
currentMap: ResearchMap;
message: string;
history?: RefineChatMessage[];
aiProvider: string;
aiModel: string;
apiKeys?: ProviderApiKeys;
}
function buildRefineUserPrompt(input: RefineResearchMapInput): string {
const historyBlock =
input.history && input.history.length > 0
? `\n【先前對話】\n${input.history
.map((m) => `${m.role === "user" ? "用戶" : "助理"}${m.content}`)
.join("\n")}\n`
: "";
return `【主題】${input.label}(種子字:${input.query}
Brief${input.brief?.trim() || "(無)"}
${buildTopicAnchorPromptBlock({ label: input.label, query: input.query, brief: input.brief })}
${historyBlock}
稿
${JSON.stringify(input.currentMap, null, 2)}
調
${input.message}
調reply 24 `;
}
export async function refineResearchMap(input: RefineResearchMapInput): Promise<{
reply: string;
researchMap: ResearchMap;
}> {
const model = getModel(input.aiProvider, input.aiModel, input.apiKeys ?? {});
const system = `${buildResearchMapSystemPrompt()}
調
-
- Threads
-
- reply `;
const prompt = buildRefineUserPrompt(input);
const object = await generateStructuredObject({
model,
provider: input.aiProvider,
modelId: input.aiModel,
schema: refineResponseSchema,
system,
prompt,
});
const tagInput = {
label: input.label,
query: input.query,
brief: input.brief,
exclusions: input.currentMap.exclusions,
};
let researchMap = preserveTagsFromPreviousMap(
object.researchMap as ResearchMap,
input.currentMap,
tagInput
);
researchMap = {
...researchMap,
suggestedTags: filterSuggestedTags(researchMap.suggestedTags, tagInput),
similarAccounts: input.currentMap.similarAccounts,
};
return {
reply: object.reply,
researchMap,
};
}