import { getAiSemaphore } from '../ai-shared/AiConcurrency'; import { AiApiError } from '../ai-shared/AiError'; import { resolveAiRuntimeConfig, type AiRuntimeConfig } from '../ai-shared/AiRuntimeConfig'; import { AiStructuredOutputClient, type JsonSchemaObject } from '../ai-shared/AiStructuredOutputClient'; import { createAiTimeoutBudget } from '../ai-shared/AiTimeout'; import { collectKnowledgeSamples, uniqueTitles, type KnowledgeTopicSample, } from './KnowledgeCatalog'; import { KnowledgeRepository } from './KnowledgeRepository'; const SUGGESTED_PROMPT_COUNT = 3; const AI_TIMEOUT_MS = 12_000; const AI_MAX_OUTPUT_TOKENS = 512; const PROMPT_SCHEMA: JsonSchemaObject = { type: 'object', additionalProperties: false, required: ['prompts'], properties: { prompts: { type: 'array', minItems: SUGGESTED_PROMPT_COUNT, maxItems: SUGGESTED_PROMPT_COUNT, items: { type: 'string', minLength: 4, maxLength: 48, }, }, }, }; export async function generateSuggestedPrompts( signal?: AbortSignal, ): Promise<{ prompts: string[]; source: 'ai' | 'knowledge' | 'empty' }> { const config = resolveAiRuntimeConfig(); const repository = new KnowledgeRepository(config); const samples = collectKnowledgeSamples(repository, signal); if (samples.length === 0) { return { prompts: [], source: 'empty' }; } const fallback = buildHeuristicPrompts(samples); try { const aiPrompts = await generateWithAi(config, samples, signal); if (aiPrompts) { return { prompts: aiPrompts, source: 'ai' }; } } catch (error) { if (error instanceof AiApiError || signal?.aborted) { // 欢迎区推荐提问允许降级,不因 AI 故障阻断挂件。 } else { throw error; } } return { prompts: fallback, source: 'knowledge' }; } async function generateWithAi( config: AiRuntimeConfig, samples: KnowledgeTopicSample[], signal?: AbortSignal, ): Promise { const budget = createAiTimeoutBudget( Math.min(config.timeoutMs, AI_TIMEOUT_MS), signal, ); try { const client = new AiStructuredOutputClient(config); const model = config.assistant.model ?? config.model; const catalog = samples.map((sample, index) => ( `${index + 1}. 主题:${sample.title}\n摘要:${sample.excerpt}` )).join('\n\n'); const response = await getAiSemaphore(config.maxConcurrency).run( () => client.generateObject({ system: [ '你是企业管理后台的全局工作助手。', '根据给定工作资料摘要,生成恰好 3 个中文推荐提问。', '要求:问题必须能被资料内容回答;具体、可点击、互不重复;不要编号、不要解释。', '问题中禁止出现文件名、路径、目录,以及“知识库/资料库/文档文件”等实现字样。', ].join('\n'), user: `工作资料摘要:\n${catalog}`, schema: PROMPT_SCHEMA, model, maxOutputTokens: AI_MAX_OUTPUT_TOKENS, timeoutMs: budget.remainingMs(), signal: budget.signal, }), budget.signal, ); return normalizePrompts(response.object.prompts) ?? null; } finally { budget.dispose(); } } function buildHeuristicPrompts(samples: KnowledgeTopicSample[]): string[] { const titles = uniqueTitles(samples.map((sample) => sample.title)); const prompts: string[] = []; for (const title of titles) { if (prompts.length >= SUGGESTED_PROMPT_COUNT) { break; } prompts.push(`${title}的核心要点是什么?`); } if (prompts.length < SUGGESTED_PROMPT_COUNT && titles[0]) { const primary = titles[0]; const extras = [ `如何开展与${primary}相关的工作?`, `${primary}有哪些需要注意的约束?`, `${primary}适用于哪些场景?`, ]; for (const prompt of extras) { if (prompts.length >= SUGGESTED_PROMPT_COUNT) { break; } if (!prompts.includes(prompt)) { prompts.push(prompt); } } } while (prompts.length < SUGGESTED_PROMPT_COUNT) { prompts.push('有哪些我可以先了解的内容?'); } return prompts.slice(0, SUGGESTED_PROMPT_COUNT); } function normalizePrompts(value: unknown): string[] | null { if (!Array.isArray(value)) { return null; } const prompts = value .filter((item): item is string => typeof item === 'string') .map((item) => item.replace(/\s+/g, ' ').trim()) .filter((item) => item.length >= 4 && item.length <= 64) .filter((item) => !/(知识库|资料库|\.md\b|\.txt\b|\/knowledge\/)/i.test(item)); const unique: string[] = []; for (const prompt of prompts) { if (!unique.includes(prompt)) { unique.push(prompt); } } return unique.length >= SUGGESTED_PROMPT_COUNT ? unique.slice(0, SUGGESTED_PROMPT_COUNT) : null; }