新增: Agent 大纲协议(outline/gen_page 动作)与设置弹窗统一(UnifiedSettingsModal 归并 Settings/OSS/Agent 三弹窗)
This commit is contained in:
+52
-15
@@ -566,13 +566,18 @@ export function parseChatReply(text: string): { reply: string; op: AiOp | null }
|
||||
* 组装 Agent 请求 prompt:用户指令 + chat 协议要求 + deck 上下文。
|
||||
* 大 deck(序列化 >700KB)降级为「当前页完整 JSON + 全deck大纲摘要」,防超 1MiB 帧上限。
|
||||
*/
|
||||
export function buildAgentPrompt(input: string): string {
|
||||
export function buildAgentPrompt(input: string, selectedElement?: SlideElement | null, history?: { role: string; content: string }[]): string {
|
||||
const SYS_AGENT =
|
||||
SYS_BASE +
|
||||
'\n任务:你是通过中继接入的远程 Agent。根据用户指令编辑当前演示。\n' +
|
||||
'回复格式:先用中文说明你将做什么,如需修改 PPT,在回复最后另起一行输出分隔标记 ' + SEP + ',紧随其后输出 JSON 操作。\n' +
|
||||
'JSON 操作格式:{"action":"add_page|update_page|create_all|answer","slides":[...],"target":页码(从1开始,可选)}\n' +
|
||||
'JSON 操作格式:{"action":"add_page|update_page|create_all|answer|outline|gen_page","slides":[...],"target":页码(从1开始,可选)}\n' +
|
||||
'- add_page:在 target 页后插入新页;- update_page:替换 target 页;- create_all:整体替换;- answer:仅回答不改稿。\n' +
|
||||
'- outline:用户要求「拟定大纲/先出大纲」时返回,格式 {"action":"outline","title":"整份标题","items":[...]};\n' +
|
||||
' items 结构:{ "kind":"cover|toc|content|quote|end", "title":"页标题", "points":["要点 1","要点 2"], "hint":"可选补充指令" };\n' +
|
||||
' kind 取值:cover(封面)/toc(目录)/content(内容)/quote(金句)/end(结尾),封面与结尾各 1 页,目录可选;\n' +
|
||||
' 页数遵循用户指令(指定具体页数照办;未指定/「自动」则按主题信息量在 5-12 页裁量:概念介绍 5-7、常规 7-9、复杂多维度 9-12);hint 给后续生成幻灯片的补充指令。\n' +
|
||||
'- gen_page:用户给出大纲条目要求生成该页时返回,格式 {"action":"gen_page","slides":[单个slide对象]}。\n' +
|
||||
'没有改动时不要输出分隔标记。不要使用 markdown 代码块。'
|
||||
const deck = store.getDeck()
|
||||
let body: string
|
||||
@@ -583,7 +588,34 @@ export function buildAgentPrompt(input: string): string {
|
||||
} else {
|
||||
body = '完整 deck JSON:\n' + full + '\n当前页码:第 ' + (store.getCurrentIndex() + 1) + ' 页'
|
||||
}
|
||||
return SYS_AGENT + '\n\n' + body + '\n\n用户指令:' + input
|
||||
// 选中元素上下文:与 direct 通道对话行为对齐
|
||||
if (selectedElement) {
|
||||
const typeLabel = selectedElement.type
|
||||
const preview = (selectedElement.content || '').replace(/\n/g, ' ').slice(0, 100)
|
||||
body += '\n\n【用户当前选中的元素】(第' + (store.getCurrentIndex() + 1) + '页)'
|
||||
body += '\n类型: ' + typeLabel + ',内容预览: "' + preview + '"'
|
||||
body += '\n用户接下来的指令默认针对此元素,除非明确说整页/整套。'
|
||||
}
|
||||
// 多轮对话历史(不含本次输入);单条截断防长回复撑爆 1MiB 帧上限
|
||||
let hist = ''
|
||||
if (history && history.length) {
|
||||
hist = '\n\n对话历史:\n' + history
|
||||
.map(m => (m.role === 'user' ? '用户: ' : '助手: ') + m.content.slice(0, 1500))
|
||||
.join('\n')
|
||||
}
|
||||
return SYS_AGENT + '\n\n' + hist + '\n\n' + body + '\n\n用户指令:' + input
|
||||
}
|
||||
|
||||
/** 大纲条目规范化(outline() 与 normalizeOp 的 outline 分支共用) */
|
||||
function normOutlineItems(arr: any[]): OutlineItem[] {
|
||||
return (Array.isArray(arr) ? arr : []).map((it: any, i: number) => ({
|
||||
id: 'ol-' + Date.now() + '-' + i,
|
||||
kind: ['cover', 'toc', 'content', 'quote', 'end'].includes(it.kind) ? it.kind : 'content',
|
||||
title: String(it.title || '未命名').slice(0, 80),
|
||||
points: Array.isArray(it.points) ? it.points.map((p: any) => String(p).slice(0, 200)).filter(Boolean).slice(0, 6) : [],
|
||||
hint: it.hint ? String(it.hint).slice(0, 120) : undefined,
|
||||
done: false
|
||||
}))
|
||||
}
|
||||
|
||||
function normalizeOp(json: any): AiOp | null {
|
||||
@@ -591,6 +623,14 @@ function normalizeOp(json: any): AiOp | null {
|
||||
let action: AiOp['action'] = json.action || 'answer'
|
||||
const slides = normSlides(json.slides)
|
||||
const target = json.target != null ? (Number(json.target) - 1) : null
|
||||
// outline:agent 返回整份大纲,条目规范化(复用 outline() 的逻辑)
|
||||
if (action === 'outline') {
|
||||
const items = normOutlineItems(json.items)
|
||||
if (!items.length) action = 'answer'
|
||||
else return { action, slides, target: null, note: json.note || '', outline: { title: String(json.title || '').slice(0, 80) || '未命名大纲', topic: '', items } }
|
||||
}
|
||||
// gen_page:按大纲条目生成的单页,空则降级 answer
|
||||
if (action === 'gen_page' && !slides.length) action = 'answer'
|
||||
if (action === 'update_page' && slides.length) action = 'update_page'
|
||||
if (action === 'add_page' && slides.length) action = 'add_page'
|
||||
if ((action === 'update_page' || action === 'add_page') && !slides.length) action = 'answer'
|
||||
@@ -610,7 +650,8 @@ import type { Outline, OutlineItem, ThemeSuggestion, ImageGenResult } from './ty
|
||||
const SYS_OUTLINE =
|
||||
'你是「u-ppt」的演示策划助手。用户给你一个主题,你先制定大纲,不要直接写完整幻灯片。\n\n' +
|
||||
'大纲要素:先拟定一个具体、有信息量的整份标题(不要「关于 X 的分享」这类空泛标题);\n' +
|
||||
'然后拆成 5-8 页,每页给出:页类型 kind、标题 title、3-5 条要点 points、可选 hint。\n' +
|
||||
'然后拆成页(页数遵循用户指令:指定了具体页数就照办;「自动」则按主题的信息量在 5-12 页间裁量,' +
|
||||
'概念介绍 5-7 页、常规主题 7-9 页、多维度复杂主题 9-12 页),每页给出:页类型 kind、标题 title、3-5 条要点 points、可选 hint。\n' +
|
||||
'kind 取值:cover(封面) / toc(目录) / content(内容) / quote(金句) / end(结尾)。\n' +
|
||||
'封面与结尾各 1 页,目录可选。\n' +
|
||||
'hint 用来给后续生成幻灯片的 AI 补充指令,例如「数据页:用 stat+chart」「对比页:双 card 并置」「引言页:深色背景金句」。\n\n' +
|
||||
@@ -651,22 +692,18 @@ const SYS_BEAUTIFY =
|
||||
|
||||
/* ---------- 1. 大纲生成 ---------- */
|
||||
|
||||
export async function outline(opts: { topic: string; count?: number; signal?: AbortSignal }): Promise<Outline> {
|
||||
const count = opts.count || 7
|
||||
export async function outline(opts: { topic: string; count?: number | 'auto'; signal?: AbortSignal }): Promise<Outline> {
|
||||
const count = opts.count || 'auto'
|
||||
const countHint = count === 'auto'
|
||||
? '页数自动裁量(结合主题信息量在 5-12 页之间取舍)'
|
||||
: '请拟定约 ' + count + ' 页'
|
||||
const messages: Message[] = [
|
||||
{ role: 'system', content: SYS_OUTLINE },
|
||||
{ role: 'user', content: '主题:' + opts.topic + '\n请拟定约 ' + count + ' 页的大纲(含封面与结尾),中文。' }
|
||||
{ role: 'user', content: '主题:' + opts.topic + '\n' + countHint + '的大纲(含封面与结尾),中文。' }
|
||||
]
|
||||
const r = await streamChat(messages, { jsonMode: true, signal: opts.signal })
|
||||
if (!r.json) throw new Error('AI 未返回有效大纲,请重试。')
|
||||
const items: OutlineItem[] = (Array.isArray(r.json.items) ? r.json.items : []).map((it: any, i: number) => ({
|
||||
id: 'ol-' + Date.now() + '-' + i,
|
||||
kind: ['cover', 'toc', 'content', 'quote', 'end'].includes(it.kind) ? it.kind : 'content',
|
||||
title: String(it.title || '未命名').slice(0, 80),
|
||||
points: Array.isArray(it.points) ? it.points.map((p: any) => String(p).slice(0, 200)).filter(Boolean).slice(0, 6) : [],
|
||||
hint: it.hint ? String(it.hint).slice(0, 120) : undefined,
|
||||
done: false
|
||||
}))
|
||||
const items = normOutlineItems(r.json.items)
|
||||
if (!items.length) throw new Error('大纲为空,请重试或换一个主题。')
|
||||
return { title: String(r.json.title || opts.topic).slice(0, 80), topic: opts.topic, items }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user