返回 oh-my-ppt
factory.ts
根目录 / src / main / agent-runtime / agent / factory.ts
1 import log from 'electron-log/main.js'
2 import { createDeepAgent } from 'deepagents'
3 import { buildDeckAgentSystemPrompt } from '../prompt/composers/deck-system'
4 import { buildEditAgentSystemPrompt } from '../prompt/composers/edit-system'
5 import { createSessionBoundDeckTools } from '../tools/deck-tools'
6 import { getRequiredProductSkillNamesForSlideSize } from '../../product-skills'
7 import { resolveModel } from '../model/resolve'
8 import type { ModelRuntimeConfig } from '../model/usage'
9 import { attachProductSkillsBackend } from '../skills/backend'
10 import { createProductGeneralPurposeSubagent, GuardedFilesystemBackend } from './backend'
11 import type { DeepAgentStreamResult, SessionDeckGenerationContext } from './types'
12
13 export type CreateSessionEditAgentArgs = {
14 provider: string
15 apiKey: string
16 model: string
17 baseUrl?: string
18 temperature?: number
19 maxTokens?: number
20 modelRuntime?: ModelRuntimeConfig
21 styleId?: string | null
22 context: SessionDeckGenerationContext
23 }
24
25 export type CreateSessionDeckAgentArgs = CreateSessionEditAgentArgs & {
26 systemPromptAddendum?: string
27 }
28
29 function shouldBlockNativeEditFile(context: SessionDeckGenerationContext): boolean {
30 if (context.editScope === 'presentation-container') return true
31 return !Boolean(context.selectedSelector?.trim())
32 }
33
34 function shouldBlockNativeWriteFile(context: SessionDeckGenerationContext): boolean {
35 // Every edit scope has a narrower write path with scope and validation enforcement:
36 // selector -> edit_file, page -> update_single_page_file,
37 // deck -> update_page_file, container -> set_index_transition.
38 return context.mode === 'edit'
39 }
40
41 function resolveAllowedEditPaths(context: SessionDeckGenerationContext): string[] | undefined {
42 if (!context.selectedSelector?.trim()) return undefined
43 const pageId = context.selectedPageId?.trim()
44 return pageId ? [`/${pageId}.html`] : []
45 }
46
47 export function createSessionEditAgent(args: CreateSessionEditAgentArgs): DeepAgentStreamResult {
48 const model = resolveModel(
49 args.provider,
50 args.apiKey,
51 args.model,
52 args.baseUrl,
53 args.temperature,
54 args.maxTokens,
55 args.modelRuntime
56 )
57 const context: SessionDeckGenerationContext = {
58 ...args.context,
59 provider: args.provider,
60 model: args.model
61 }
62 const disableNativeEditFile = shouldBlockNativeEditFile(context)
63 const disableNativeWriteFile = shouldBlockNativeWriteFile(context)
64 const allowedEditPaths = resolveAllowedEditPaths(context)
65 const backend = new GuardedFilesystemBackend({
66 rootDir: context.projectDir,
67 virtualMode: true,
68 disableEditFile: disableNativeEditFile,
69 disableWriteFile: disableNativeWriteFile,
70 allowedEditPaths,
71 editBlockedReason: disableNativeEditFile
72 ? '当前编辑任务禁止使用 edit_file。请改用 update_single_page_file(pageId, content) 或 update_page_file(pageId, content)。'
73 : undefined,
74 writeBlockedReason:
75 '当前编辑任务禁止使用 write_file。请使用 update_single_page_file(pageId, content)、update_page_file(pageId, content) 或允许的 edit_file。'
76 })
77 const requiredSkillNames = getRequiredProductSkillNamesForSlideSize(context.slideSize)
78 const agentBackend = attachProductSkillsBackend(backend, 'session-edit', requiredSkillNames)
79 const tools = createSessionBoundDeckTools(context)
80 const systemPrompt = buildEditAgentSystemPrompt(args.styleId, context)
81 const hasSelector = Boolean(context.selectedSelector?.trim())
82 const isDeckEdit = context.mode === 'edit' && context.editScope === 'deck'
83 const isContainerEdit = context.mode === 'edit' && context.editScope === 'presentation-container'
84 const promptMode = isContainerEdit
85 ? 'container'
86 : hasSelector
87 ? 'selector'
88 : isDeckEdit
89 ? 'deck'
90 : 'single-page'
91
92 log.info('[deepagent] create session edit agent', {
93 sessionId: context.sessionId,
94 provider: args.provider,
95 model: args.model,
96 styleId: args.styleId || '',
97 projectDir: context.projectDir,
98 indexPath: context.indexPath,
99 selectedPageId: context.selectedPageId,
100 selectPageIds: context.selectPageIds,
101 disableNativeEditFile,
102 disableNativeWriteFile,
103 allowedEditPaths,
104 promptMode,
105 skillsEnabled: agentBackend.enabled,
106 requiredSkillNames
107 })
108
109 return createDeepAgent({
110 model: model as any,
111 backend: agentBackend.backend,
112 systemPrompt,
113 tools: tools as any,
114 middleware: agentBackend.middleware as any,
115 subagents: createProductGeneralPurposeSubagent({
116 model,
117 tools,
118 backend: agentBackend.backend,
119 skillSource: agentBackend.skillSource,
120 requiredSkillNames
121 })
122 })
123 }
124
125 export function createSessionDeckAgent(args: CreateSessionDeckAgentArgs): DeepAgentStreamResult {
126 const model = resolveModel(
127 args.provider,
128 args.apiKey,
129 args.model,
130 args.baseUrl,
131 args.temperature,
132 args.maxTokens,
133 args.modelRuntime
134 )
135 const context: SessionDeckGenerationContext = {
136 ...args.context,
137 provider: args.provider,
138 model: args.model
139 }
140 const backend = new GuardedFilesystemBackend({
141 rootDir: context.projectDir,
142 virtualMode: true,
143 disableEditFile: true,
144 editBlockedReason: context.templatePageReadRequired
145 ? '当前模板生成任务禁止使用 edit_file。请使用 update_template_page_file(pageId, content)。'
146 : '当前生成/全局编辑任务禁止使用 edit_file。请使用 update_single_page_file(pageId, content) 或 update_page_file(pageId, content)。'
147 })
148 const requiredSkillNames = getRequiredProductSkillNamesForSlideSize(context.slideSize)
149 const agentBackend = attachProductSkillsBackend(backend, 'session-deck', requiredSkillNames)
150 const getToolName = (tool: unknown): string => {
151 const maybe = tool as { name?: unknown; lc_kwargs?: { name?: unknown } }
152 if (typeof maybe.name === 'string') return maybe.name
153 if (typeof maybe.lc_kwargs?.name === 'string') return maybe.lc_kwargs.name
154 return ''
155 }
156 const tools = createSessionBoundDeckTools(context)
157 const systemPrompt = [
158 buildDeckAgentSystemPrompt(args.styleId, context),
159 args.systemPromptAddendum?.trim() || ''
160 ]
161 .filter(Boolean)
162 .join('\n\n')
163
164 log.info('[deepagent] create session deck agent', {
165 sessionId: context.sessionId,
166 provider: args.provider,
167 model: args.model,
168 styleId: args.styleId || '',
169 projectDir: context.projectDir,
170 indexPath: context.indexPath,
171 selectedPageId: context.selectedPageId,
172 skillsEnabled: agentBackend.enabled,
173 requiredSkillNames,
174 selectedPagePath:
175 context.selectedPageId && context.pageFileMap[context.selectedPageId]
176 ? context.pageFileMap[context.selectedPageId]
177 : '',
178 totalPages: context.outlineTitles.length,
179 toolNames: tools.map((tool) => getToolName(tool)).filter((name) => name.length > 0)
180 })
181
182 return createDeepAgent({
183 model: model as any,
184 backend: agentBackend.backend,
185 systemPrompt,
186 tools: tools as any,
187 middleware: agentBackend.middleware as any,
188 subagents: createProductGeneralPurposeSubagent({
189 model,
190 tools,
191 backend: agentBackend.backend,
192 skillSource: agentBackend.skillSource,
193 requiredSkillNames
194 })
195 })
196 }
197
197 lines TYPESCRIPT