@vibe-forge/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/package.json +73 -0
- package/src/adapter/index.ts +6 -0
- package/src/adapter/loader.ts +9 -0
- package/src/adapter/type.ts +99 -0
- package/src/config.ts +375 -0
- package/src/controllers/config/index.ts +207 -0
- package/src/controllers/system/assets/completed.mp3 +0 -0
- package/src/controllers/system/assets/mcp.png +0 -0
- package/src/controllers/system/index.ts +102 -0
- package/src/controllers/task/generate-adapter-query-options.ts +86 -0
- package/src/controllers/task/index.ts +2 -0
- package/src/controllers/task/prepare.ts +60 -0
- package/src/controllers/task/run.ts +98 -0
- package/src/controllers/task/schema.ts +131 -0
- package/src/controllers/task/type.ts +6 -0
- package/src/env.ts +43 -0
- package/src/hooks/index.ts +37 -0
- package/src/hooks/loader.ts +75 -0
- package/src/hooks/type.ts +173 -0
- package/src/index.ts +9 -0
- package/src/schema.ts +13 -0
- package/src/types.ts +66 -0
- package/src/utils/cache.ts +51 -0
- package/src/utils/create-logger.ts +74 -0
- package/src/utils/definition-loader.ts +354 -0
- package/src/utils/filter.ts +26 -0
- package/src/utils/string-transform.ts +37 -0
- package/src/utils/uuid.ts +6 -0
- package/src/ws.ts +13 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { dirname, extname, resolve } from 'node:path'
|
|
4
|
+
import process from 'node:process'
|
|
5
|
+
|
|
6
|
+
import { dump, load } from 'js-yaml'
|
|
7
|
+
|
|
8
|
+
import type { Config } from '../../config'
|
|
9
|
+
import { resetConfigCache } from '../../config'
|
|
10
|
+
|
|
11
|
+
export type ConfigSource = 'project' | 'user'
|
|
12
|
+
|
|
13
|
+
export interface UpdateConfigFileOptions {
|
|
14
|
+
workspaceFolder?: string
|
|
15
|
+
source: ConfigSource
|
|
16
|
+
section: string
|
|
17
|
+
value: unknown
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const shouldMaskKey = (key: string) => /key|token|secret|password/i.test(key)
|
|
21
|
+
|
|
22
|
+
const projectConfigPaths = [
|
|
23
|
+
'./.ai.config.json',
|
|
24
|
+
'./infra/.ai.config.json',
|
|
25
|
+
'./.ai.config.yaml',
|
|
26
|
+
'./.ai.config.yml',
|
|
27
|
+
'./infra/.ai.config.yaml',
|
|
28
|
+
'./infra/.ai.config.yml'
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
const userConfigPaths = [
|
|
32
|
+
'./.ai.dev.config.json',
|
|
33
|
+
'./infra/.ai.dev.config.json',
|
|
34
|
+
'./.ai.dev.config.yaml',
|
|
35
|
+
'./.ai.dev.config.yml',
|
|
36
|
+
'./infra/.ai.dev.config.yaml',
|
|
37
|
+
'./infra/.ai.dev.config.yml'
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
const resolveConfigPath = (workspaceFolder: string, source: ConfigSource) => {
|
|
41
|
+
const paths = source === 'project' ? projectConfigPaths : userConfigPaths
|
|
42
|
+
for (const path of paths) {
|
|
43
|
+
const resolved = resolve(workspaceFolder, path)
|
|
44
|
+
if (existsSync(resolved)) {
|
|
45
|
+
return resolved
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return resolve(workspaceFolder, paths[0])
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const parseConfigContent = (format: string, content: string) => {
|
|
52
|
+
if (format === '.yaml' || format === '.yml') {
|
|
53
|
+
return (load(content) ?? {}) as Record<string, unknown>
|
|
54
|
+
}
|
|
55
|
+
return JSON.parse(content) as Record<string, unknown>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const serializeConfigContent = (format: string, value: Record<string, unknown>) => {
|
|
59
|
+
if (format === '.yaml' || format === '.yml') {
|
|
60
|
+
return `${dump(value, { noRefs: true, lineWidth: 120 })}\n`
|
|
61
|
+
}
|
|
62
|
+
return `${JSON.stringify(value, null, 2)}\n`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const mergeMaskedValues = (incoming: unknown, existing: unknown): unknown => {
|
|
66
|
+
if (Array.isArray(incoming)) return incoming
|
|
67
|
+
if (incoming != null && typeof incoming === 'object') {
|
|
68
|
+
const incomingRecord = incoming as Record<string, unknown>
|
|
69
|
+
const existingRecord = (existing != null && typeof existing === 'object')
|
|
70
|
+
? (existing as Record<string, unknown>)
|
|
71
|
+
: {}
|
|
72
|
+
return Object.entries(incomingRecord).reduce<Record<string, unknown>>((acc, [key, val]) => {
|
|
73
|
+
if (shouldMaskKey(key) && val === '******') {
|
|
74
|
+
acc[key] = existingRecord[key]
|
|
75
|
+
} else {
|
|
76
|
+
acc[key] = mergeMaskedValues(val, existingRecord[key])
|
|
77
|
+
}
|
|
78
|
+
return acc
|
|
79
|
+
}, {})
|
|
80
|
+
}
|
|
81
|
+
return incoming
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const updateConfigSection = (config: Config, section: string, value: unknown): Config => {
|
|
85
|
+
const nextConfig: Config = { ...config }
|
|
86
|
+
const sectionValue = (value != null && typeof value === 'object')
|
|
87
|
+
? (value as Record<string, unknown>)
|
|
88
|
+
: {}
|
|
89
|
+
|
|
90
|
+
const updateField = <T extends keyof Config>(key: T, nextValue: Config[T] | undefined) => {
|
|
91
|
+
if (nextValue === undefined) {
|
|
92
|
+
delete nextConfig[key]
|
|
93
|
+
} else {
|
|
94
|
+
nextConfig[key] = nextValue
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
switch (section) {
|
|
99
|
+
case 'general': {
|
|
100
|
+
updateField('baseDir', sectionValue.baseDir as Config['baseDir'])
|
|
101
|
+
updateField('defaultAdapter', sectionValue.defaultAdapter as Config['defaultAdapter'])
|
|
102
|
+
updateField('defaultModelService', sectionValue.defaultModelService as Config['defaultModelService'])
|
|
103
|
+
updateField('defaultModel', sectionValue.defaultModel as Config['defaultModel'])
|
|
104
|
+
updateField('recommendedModels', sectionValue.recommendedModels as Config['recommendedModels'])
|
|
105
|
+
updateField('interfaceLanguage', sectionValue.interfaceLanguage as Config['interfaceLanguage'])
|
|
106
|
+
updateField('modelLanguage', sectionValue.modelLanguage as Config['modelLanguage'])
|
|
107
|
+
updateField('announcements', sectionValue.announcements as Config['announcements'])
|
|
108
|
+
updateField(
|
|
109
|
+
'permissions',
|
|
110
|
+
mergeMaskedValues(sectionValue.permissions, config.permissions) as Config['permissions']
|
|
111
|
+
)
|
|
112
|
+
updateField(
|
|
113
|
+
'env',
|
|
114
|
+
mergeMaskedValues(sectionValue.env, config.env) as Config['env']
|
|
115
|
+
)
|
|
116
|
+
updateField(
|
|
117
|
+
'notifications',
|
|
118
|
+
mergeMaskedValues(sectionValue.notifications, config.notifications) as Config['notifications']
|
|
119
|
+
)
|
|
120
|
+
updateField(
|
|
121
|
+
'shortcuts',
|
|
122
|
+
mergeMaskedValues(sectionValue.shortcuts, config.shortcuts) as Config['shortcuts']
|
|
123
|
+
)
|
|
124
|
+
return nextConfig
|
|
125
|
+
}
|
|
126
|
+
case 'conversation': {
|
|
127
|
+
updateField('conversation', mergeMaskedValues(sectionValue, config.conversation) as Config['conversation'])
|
|
128
|
+
return nextConfig
|
|
129
|
+
}
|
|
130
|
+
case 'modelServices': {
|
|
131
|
+
updateField(
|
|
132
|
+
'modelServices',
|
|
133
|
+
mergeMaskedValues(sectionValue, config.modelServices) as Config['modelServices']
|
|
134
|
+
)
|
|
135
|
+
return nextConfig
|
|
136
|
+
}
|
|
137
|
+
case 'adapters': {
|
|
138
|
+
updateField('adapters', mergeMaskedValues(sectionValue, config.adapters) as Config['adapters'])
|
|
139
|
+
return nextConfig
|
|
140
|
+
}
|
|
141
|
+
case 'plugins': {
|
|
142
|
+
updateField('plugins', sectionValue.plugins as Config['plugins'])
|
|
143
|
+
updateField(
|
|
144
|
+
'enabledPlugins',
|
|
145
|
+
mergeMaskedValues(sectionValue.enabledPlugins, config.enabledPlugins) as Config['enabledPlugins']
|
|
146
|
+
)
|
|
147
|
+
updateField(
|
|
148
|
+
'extraKnownMarketplaces',
|
|
149
|
+
mergeMaskedValues(
|
|
150
|
+
sectionValue.extraKnownMarketplaces,
|
|
151
|
+
config.extraKnownMarketplaces
|
|
152
|
+
) as Config['extraKnownMarketplaces']
|
|
153
|
+
)
|
|
154
|
+
return nextConfig
|
|
155
|
+
}
|
|
156
|
+
case 'mcp': {
|
|
157
|
+
updateField(
|
|
158
|
+
'mcpServers',
|
|
159
|
+
mergeMaskedValues(sectionValue.mcpServers, config.mcpServers) as Config['mcpServers']
|
|
160
|
+
)
|
|
161
|
+
updateField(
|
|
162
|
+
'defaultIncludeMcpServers',
|
|
163
|
+
sectionValue.defaultIncludeMcpServers as Config['defaultIncludeMcpServers']
|
|
164
|
+
)
|
|
165
|
+
updateField(
|
|
166
|
+
'defaultExcludeMcpServers',
|
|
167
|
+
sectionValue.defaultExcludeMcpServers as Config['defaultExcludeMcpServers']
|
|
168
|
+
)
|
|
169
|
+
updateField(
|
|
170
|
+
'noDefaultVibeForgeMcpServer',
|
|
171
|
+
sectionValue.noDefaultVibeForgeMcpServer as Config['noDefaultVibeForgeMcpServer']
|
|
172
|
+
)
|
|
173
|
+
return nextConfig
|
|
174
|
+
}
|
|
175
|
+
case 'shortcuts': {
|
|
176
|
+
updateField(
|
|
177
|
+
'shortcuts',
|
|
178
|
+
mergeMaskedValues(sectionValue, config.shortcuts) as Config['shortcuts']
|
|
179
|
+
)
|
|
180
|
+
return nextConfig
|
|
181
|
+
}
|
|
182
|
+
default:
|
|
183
|
+
return nextConfig
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export const updateConfigFile = async (options: UpdateConfigFileOptions) => {
|
|
188
|
+
const workspaceFolder = options.workspaceFolder ?? process.cwd()
|
|
189
|
+
const configPath = resolveConfigPath(workspaceFolder, options.source)
|
|
190
|
+
const format = extname(configPath).toLowerCase()
|
|
191
|
+
const hasExisting = existsSync(configPath)
|
|
192
|
+
const existingContent = hasExisting ? await readFile(configPath, 'utf-8') : ''
|
|
193
|
+
const existingConfig = hasExisting ? parseConfigContent(format, existingContent) : {}
|
|
194
|
+
const updatedConfig = updateConfigSection(existingConfig as Config, options.section, options.value)
|
|
195
|
+
await mkdir(dirname(configPath), { recursive: true })
|
|
196
|
+
await writeFile(
|
|
197
|
+
configPath,
|
|
198
|
+
serializeConfigContent(format, updatedConfig as Record<string, unknown>),
|
|
199
|
+
'utf-8'
|
|
200
|
+
)
|
|
201
|
+
resetConfigCache()
|
|
202
|
+
return { configPath, updatedConfig }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export const configController = {
|
|
206
|
+
updateConfigFile
|
|
207
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
|
|
5
|
+
import type { NotificationMetadata } from 'node-notifier'
|
|
6
|
+
import notifier from 'node-notifier'
|
|
7
|
+
import z from 'zod'
|
|
8
|
+
|
|
9
|
+
const notifyOptionsSchema = z.object({
|
|
10
|
+
title: z.string().optional(),
|
|
11
|
+
description: z.string(),
|
|
12
|
+
icon: z.string().optional().describe('自定义图标路径'),
|
|
13
|
+
sound: z
|
|
14
|
+
.union([z.boolean(), z.string()])
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('是否播放音效或指定音效文件路径'),
|
|
17
|
+
volume: z.number().optional().describe('音量,0-1 或 0-100'),
|
|
18
|
+
timeout: z
|
|
19
|
+
.union([z.number(), z.literal(false)])
|
|
20
|
+
.optional()
|
|
21
|
+
.describe('通知超时时间'),
|
|
22
|
+
actions: z.array(z.string()).optional().describe('通知操作按钮'),
|
|
23
|
+
needConfirm: z.boolean().optional().describe('是否需要用户确认')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
type NotifyOptions = z.infer<typeof notifyOptionsSchema>
|
|
27
|
+
|
|
28
|
+
export const notify = async (options: NotifyOptions) => {
|
|
29
|
+
const {
|
|
30
|
+
title,
|
|
31
|
+
description,
|
|
32
|
+
icon,
|
|
33
|
+
sound = true,
|
|
34
|
+
volume,
|
|
35
|
+
timeout = 10 * 60 * 1000,
|
|
36
|
+
needConfirm
|
|
37
|
+
} = options
|
|
38
|
+
|
|
39
|
+
// 默认图标
|
|
40
|
+
const defaultIcon = path.resolve(__dirname, './assets/mcp.png')
|
|
41
|
+
// 默认音效
|
|
42
|
+
const defaultSound = path.resolve(__dirname, './assets/completed.mp3')
|
|
43
|
+
|
|
44
|
+
const resolvedSound = typeof sound === 'string'
|
|
45
|
+
? sound
|
|
46
|
+
: (sound ? defaultSound : undefined)
|
|
47
|
+
const resolvedVolume = typeof volume === 'number'
|
|
48
|
+
? (volume > 1 ? Math.min(volume, 100) / 100 : Math.max(volume, 0))
|
|
49
|
+
: undefined
|
|
50
|
+
const shouldPlaySound = resolvedSound != null && resolvedVolume !== 0
|
|
51
|
+
const shouldUseNotifierSound = !(resolvedVolume != null && resolvedSound != null && process.platform === 'darwin')
|
|
52
|
+
if (shouldPlaySound && !shouldUseNotifierSound && resolvedSound != null) {
|
|
53
|
+
try {
|
|
54
|
+
const args = ['-v', `${resolvedVolume ?? 1}`, resolvedSound]
|
|
55
|
+
const proc = spawn('afplay', args, { stdio: 'ignore', detached: true })
|
|
56
|
+
proc.unref()
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const [response, metadata] = await new Promise<
|
|
62
|
+
[string, NotificationMetadata | undefined]
|
|
63
|
+
>((ok, no) => {
|
|
64
|
+
notifier.notify(
|
|
65
|
+
{
|
|
66
|
+
icon: icon || defaultIcon,
|
|
67
|
+
title,
|
|
68
|
+
sound: shouldUseNotifierSound ? resolvedSound : undefined,
|
|
69
|
+
message: description,
|
|
70
|
+
wait: true,
|
|
71
|
+
reply: true,
|
|
72
|
+
timeout
|
|
73
|
+
},
|
|
74
|
+
(err, response, metadata) => {
|
|
75
|
+
if (err) {
|
|
76
|
+
no(err)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
if (!needConfirm) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
ok([response, metadata])
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
if (!needConfirm) {
|
|
86
|
+
ok([
|
|
87
|
+
'default',
|
|
88
|
+
{
|
|
89
|
+
activationType: 'default',
|
|
90
|
+
activationAt: Date.now().toLocaleString(),
|
|
91
|
+
deliveredAt: Date.now().toLocaleString()
|
|
92
|
+
}
|
|
93
|
+
])
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
return { response, metadata }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const systemController = {
|
|
100
|
+
notify,
|
|
101
|
+
notifyOptionsSchema
|
|
102
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
|
|
3
|
+
import type { AdapterQueryOptions } from '#~/adapter/type.js'
|
|
4
|
+
import { DefinitionLoader } from '#~/utils/definition-loader.js'
|
|
5
|
+
import type { Definition, Filter, Skill } from '#~/utils/definition-loader.js'
|
|
6
|
+
|
|
7
|
+
export async function generateAdapterQueryOptions(
|
|
8
|
+
type: 'spec' | 'entity' | undefined,
|
|
9
|
+
name?: string,
|
|
10
|
+
cwd: string = process.cwd()
|
|
11
|
+
): Promise<Partial<AdapterQueryOptions>> {
|
|
12
|
+
const loader = new DefinitionLoader(cwd)
|
|
13
|
+
const options: Partial<AdapterQueryOptions> = {}
|
|
14
|
+
const systemPromptParts: string[] = []
|
|
15
|
+
|
|
16
|
+
// 1. 获取数据
|
|
17
|
+
// 1.1 获取默认数据
|
|
18
|
+
const entities = type !== 'entity'
|
|
19
|
+
? await loader.loadDefaultEntities()
|
|
20
|
+
: []
|
|
21
|
+
const skills = await loader.loadDefaultSkills()
|
|
22
|
+
const rules = await loader.loadDefaultRules()
|
|
23
|
+
const specs = await loader.loadDefaultSpecs()
|
|
24
|
+
|
|
25
|
+
// 1.2 获取指定数据
|
|
26
|
+
const targetSkills: Definition<Skill>[] = []
|
|
27
|
+
let targetBody = ''
|
|
28
|
+
let targetToolsFilter: Filter | undefined
|
|
29
|
+
let targetMcpServersFilter: Filter | undefined
|
|
30
|
+
if (type && name) {
|
|
31
|
+
const data = {
|
|
32
|
+
spec: await loader.loadSpec(name),
|
|
33
|
+
entity: await loader.loadEntity(name)
|
|
34
|
+
}[type]
|
|
35
|
+
if (!data) {
|
|
36
|
+
throw new Error(`Failed to load ${type} ${name}`)
|
|
37
|
+
}
|
|
38
|
+
const { attributes, body } = data
|
|
39
|
+
if (
|
|
40
|
+
attributes.rules
|
|
41
|
+
) {
|
|
42
|
+
// always load spec or entity tagged rules
|
|
43
|
+
rules.push(
|
|
44
|
+
...(
|
|
45
|
+
await loader.loadRules(attributes.rules)
|
|
46
|
+
).map((rule) => ({
|
|
47
|
+
...rule,
|
|
48
|
+
attributes: {
|
|
49
|
+
...rule.attributes,
|
|
50
|
+
// 实体或流程中的规则为默认加载
|
|
51
|
+
always: true
|
|
52
|
+
}
|
|
53
|
+
}))
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
if (
|
|
57
|
+
attributes.skills
|
|
58
|
+
) {
|
|
59
|
+
targetSkills.push(...await loader.loadSkills(attributes.skills))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
targetBody = body
|
|
63
|
+
targetToolsFilter = attributes.tools
|
|
64
|
+
targetMcpServersFilter = attributes.mcpServers
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 2. 基于数据生成上下文
|
|
68
|
+
// 2.1 加载关联上下文
|
|
69
|
+
systemPromptParts.push(loader.generateRulesPrompt(rules))
|
|
70
|
+
systemPromptParts.push(loader.generateSkillsPrompt(targetSkills))
|
|
71
|
+
// 2.2 加载上下文路由
|
|
72
|
+
systemPromptParts.push(loader.generateEntitiesRoutePrompt(entities))
|
|
73
|
+
systemPromptParts.push(loader.generateSkillsRoutePrompt(skills))
|
|
74
|
+
systemPromptParts.push(loader.generateSpecRoutePrompt(specs))
|
|
75
|
+
// 2.3 加载目标上下文与配置
|
|
76
|
+
systemPromptParts.push(targetBody)
|
|
77
|
+
targetToolsFilter && (
|
|
78
|
+
options.tools = targetToolsFilter
|
|
79
|
+
)
|
|
80
|
+
targetMcpServersFilter && (
|
|
81
|
+
options.mcpServers = targetMcpServersFilter
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
options.systemPrompt = systemPromptParts.join('\n\n')
|
|
85
|
+
return options
|
|
86
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
|
|
3
|
+
import type { AdapterCtx, AdapterQueryOptions } from '@vibe-forge/core'
|
|
4
|
+
import { loadConfig } from '@vibe-forge/core'
|
|
5
|
+
import { getCache, setCache } from '@vibe-forge/core/utils/cache'
|
|
6
|
+
import { createLogger } from '@vibe-forge/core/utils/create-logger'
|
|
7
|
+
import { uuid } from '@vibe-forge/core/utils/uuid'
|
|
8
|
+
|
|
9
|
+
import type { RunTaskOptions } from './type'
|
|
10
|
+
|
|
11
|
+
export const prepare = async (
|
|
12
|
+
options: RunTaskOptions,
|
|
13
|
+
adapterOptions: AdapterQueryOptions
|
|
14
|
+
) => {
|
|
15
|
+
const cwd = options.cwd ?? process.env.__VF_PROJECT_WORKSPACE_FOLDER__ ?? process.cwd()
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
sessionId = uuid()
|
|
19
|
+
} = adapterOptions
|
|
20
|
+
const {
|
|
21
|
+
ctxId = process.env.__VF_PROJECT_AI_CTX_ID__ ?? sessionId,
|
|
22
|
+
env: envFromOptions
|
|
23
|
+
} = options
|
|
24
|
+
const {
|
|
25
|
+
__IS_LOADER_CLI__: _0,
|
|
26
|
+
...prevEnv
|
|
27
|
+
} = {
|
|
28
|
+
...process.env,
|
|
29
|
+
...envFromOptions
|
|
30
|
+
}
|
|
31
|
+
const env: Record<string, string | null | undefined> = {
|
|
32
|
+
...prevEnv,
|
|
33
|
+
__VF_PROJECT_AI_CTX_ID__: ctxId,
|
|
34
|
+
__VF_PROJECT_AI_SESSION_ID__: sessionId,
|
|
35
|
+
__VF_PROJECT_AI_RUN_TYPE__: adapterOptions.runtime,
|
|
36
|
+
// 移除 NODE_OPTIONS 环境变量,防止干扰子进程的运行环境
|
|
37
|
+
NODE_OPTIONS: undefined
|
|
38
|
+
}
|
|
39
|
+
const logger = createLogger(cwd, ctxId, sessionId, env?.LOG_PREFIX ?? '')
|
|
40
|
+
|
|
41
|
+
const jsonVariables: Record<string, string | null | undefined> = {
|
|
42
|
+
...env,
|
|
43
|
+
WORKSPACE_FOLDER: cwd,
|
|
44
|
+
__VF_PROJECT_WORKSPACE_FOLDER__: cwd
|
|
45
|
+
}
|
|
46
|
+
const [config, userConfig] = await loadConfig({ jsonVariables })
|
|
47
|
+
return [
|
|
48
|
+
{
|
|
49
|
+
ctxId,
|
|
50
|
+
cwd,
|
|
51
|
+
env,
|
|
52
|
+
cache: {
|
|
53
|
+
set: (key, value) => setCache(cwd, ctxId, sessionId, key, value),
|
|
54
|
+
get: (key) => getCache(cwd, ctxId, sessionId, key)
|
|
55
|
+
},
|
|
56
|
+
logger,
|
|
57
|
+
configs: [config, userConfig]
|
|
58
|
+
} satisfies AdapterCtx
|
|
59
|
+
] as const
|
|
60
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { AdapterCtx, AdapterOutputEvent, AdapterQueryOptions, TaskDetail } from '@vibe-forge/core'
|
|
2
|
+
import { loadAdapter } from '@vibe-forge/core'
|
|
3
|
+
import { setCache } from '@vibe-forge/core/utils/cache'
|
|
4
|
+
|
|
5
|
+
import { prepare } from './prepare'
|
|
6
|
+
import type { RunTaskOptions } from './type'
|
|
7
|
+
|
|
8
|
+
declare module '@vibe-forge/core' {
|
|
9
|
+
interface Cache {
|
|
10
|
+
base: Omit<AdapterCtx, 'logger' | 'cache'>
|
|
11
|
+
detail: TaskDetail
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const run = async (
|
|
16
|
+
options: RunTaskOptions,
|
|
17
|
+
adapterOptions: AdapterQueryOptions
|
|
18
|
+
) => {
|
|
19
|
+
const [ctx] = await prepare(options, adapterOptions)
|
|
20
|
+
const {
|
|
21
|
+
configs: [config, userConfig]
|
|
22
|
+
} = ctx
|
|
23
|
+
|
|
24
|
+
const { logger, cache, ...base } = ctx
|
|
25
|
+
|
|
26
|
+
await cache.set('base', base)
|
|
27
|
+
|
|
28
|
+
const startTime = Date.now()
|
|
29
|
+
logger.info('[Framework] Process start', {
|
|
30
|
+
...base,
|
|
31
|
+
adapterOptions,
|
|
32
|
+
startDateTime: new Date(startTime).toLocaleString()
|
|
33
|
+
})
|
|
34
|
+
const adapters = {
|
|
35
|
+
...config?.adapters,
|
|
36
|
+
...userConfig?.adapters
|
|
37
|
+
}
|
|
38
|
+
// dprint-ignore
|
|
39
|
+
const adapterType =
|
|
40
|
+
// 0. adapter from options
|
|
41
|
+
options.adapter ??
|
|
42
|
+
// 1. config default adapter
|
|
43
|
+
config?.defaultAdapter ??
|
|
44
|
+
// 2. user config default adapter
|
|
45
|
+
userConfig?.defaultAdapter ??
|
|
46
|
+
// 3. first adapter in config
|
|
47
|
+
(() => {
|
|
48
|
+
const adapterNames = Object.keys(adapters)
|
|
49
|
+
if (adapterNames.length === 0) {
|
|
50
|
+
throw new Error('No adapter found in config, please set adapters in config file')
|
|
51
|
+
}
|
|
52
|
+
return adapterNames[0]
|
|
53
|
+
})()
|
|
54
|
+
|
|
55
|
+
const detail: TaskDetail = {
|
|
56
|
+
ctxId: ctx.ctxId,
|
|
57
|
+
sessionId: adapterOptions.sessionId,
|
|
58
|
+
status: 'running',
|
|
59
|
+
startTime,
|
|
60
|
+
description: adapterOptions.description,
|
|
61
|
+
adapter: adapterType,
|
|
62
|
+
model: adapterOptions.model
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const saveDetail = async (d: TaskDetail) => {
|
|
66
|
+
// Save to caches/ctxId/detail.json (ignoring sessionId)
|
|
67
|
+
await setCache(ctx.cwd, ctx.ctxId, undefined, 'detail', d)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await saveDetail(detail)
|
|
71
|
+
|
|
72
|
+
const originalOnEvent = adapterOptions.onEvent
|
|
73
|
+
const wrappedOnEvent = (event: AdapterOutputEvent) => {
|
|
74
|
+
if (event.type === 'exit') {
|
|
75
|
+
detail.status = event.data.exitCode === 0 ? 'completed' : 'failed'
|
|
76
|
+
detail.endTime = Date.now()
|
|
77
|
+
detail.exitCode = event.data.exitCode ?? undefined
|
|
78
|
+
void saveDetail(detail).catch(console.error)
|
|
79
|
+
}
|
|
80
|
+
originalOnEvent(event)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const adapter = await loadAdapter(adapterType)
|
|
84
|
+
const session = await adapter.query(
|
|
85
|
+
ctx,
|
|
86
|
+
{
|
|
87
|
+
...adapterOptions,
|
|
88
|
+
onEvent: wrappedOnEvent
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if (session.pid) {
|
|
93
|
+
detail.pid = session.pid
|
|
94
|
+
await saveDetail(detail)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { session, ctx }
|
|
98
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import z from 'zod'
|
|
2
|
+
|
|
3
|
+
export const TaskOptions = z.object({
|
|
4
|
+
type: z
|
|
5
|
+
.union([
|
|
6
|
+
z.literal('entity'),
|
|
7
|
+
// 基础库 API 能力开发
|
|
8
|
+
z.literal('spec')
|
|
9
|
+
])
|
|
10
|
+
.describe('任务模式'),
|
|
11
|
+
name: z.string().describe('垂类知识库基准目标'),
|
|
12
|
+
specParams: z.record(z.string()).describe('SPEC 执行时的参数').optional(),
|
|
13
|
+
runtime: z
|
|
14
|
+
.object({
|
|
15
|
+
type: z
|
|
16
|
+
.string()
|
|
17
|
+
.describe('选择特定的 AI CLI 类型'),
|
|
18
|
+
noDefaultSystemPrompt: z
|
|
19
|
+
.boolean()
|
|
20
|
+
.describe('是否禁用默认的系统提示')
|
|
21
|
+
.optional()
|
|
22
|
+
}),
|
|
23
|
+
description: z
|
|
24
|
+
.string()
|
|
25
|
+
.describe('本次任务的描述,介绍关于本次任务需要进行的工作')
|
|
26
|
+
.optional(),
|
|
27
|
+
sessionId: z
|
|
28
|
+
.string()
|
|
29
|
+
.describe(
|
|
30
|
+
'复用上次会话的历史消息作为本次任务的上下文。\n' +
|
|
31
|
+
'- 通常如果某个任务在执行的时候出现了非预期的错误,那么你可以通过传入相同的 sessionID 来继续这个会话\n' +
|
|
32
|
+
'- 如果有一个步骤需要间隔执行,比如说先执行任务 A 的 A-1,然后完成后执行 B 的 B-1,等 B-1 这个完成后回到 A-1 继续执行 A-2 时也可以使用'
|
|
33
|
+
)
|
|
34
|
+
.optional(),
|
|
35
|
+
frontendTimeout: z
|
|
36
|
+
.number()
|
|
37
|
+
.describe(
|
|
38
|
+
'前台等待时间的上限,单位秒,默认值为 8 分钟。' +
|
|
39
|
+
'在超过这个时间后该任务会被转化为一个后台任务,并直接返回当前的输出以及 sessionId,你可以通过 sessionId 来继续这个任务,或者查询任务的状态'
|
|
40
|
+
)
|
|
41
|
+
.default(8 * 60)
|
|
42
|
+
.optional(),
|
|
43
|
+
defaultModel: z.string().describe('默认的模型').optional()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
export type TaskOptions = z.infer<typeof TaskOptions>
|
|
47
|
+
|
|
48
|
+
export const MCPRunTasksOptions = z.object({
|
|
49
|
+
tasks: z
|
|
50
|
+
.array(TaskOptions)
|
|
51
|
+
.describe(
|
|
52
|
+
'子任务列表。传递多个子任务时会并发执行多个,可用于优化整体任务效率。'
|
|
53
|
+
),
|
|
54
|
+
|
|
55
|
+
bashDefaultTimeoutMs: z.number().optional(),
|
|
56
|
+
bashMaxTimeoutMs: z.number().optional(),
|
|
57
|
+
maxMcpOutputTokens: z.number().optional(),
|
|
58
|
+
mcpTimeout: z.number().optional(),
|
|
59
|
+
mcpToolTimeout: z.number().optional()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
export type MCPRunTasksOptions = z.infer<typeof MCPRunTasksOptions>
|
|
63
|
+
|
|
64
|
+
export const Options = z
|
|
65
|
+
.object({
|
|
66
|
+
taskId: z
|
|
67
|
+
.string()
|
|
68
|
+
.describe(
|
|
69
|
+
'唯一 id,会用于关联多个任务相关信息。\n' +
|
|
70
|
+
'如果是第一次执行,则不需要传入,会在返回值中自动生成一个,在下次创建或复用的时候必须传入该值以供记录相关上下文。\n' +
|
|
71
|
+
'用户指定了 taskId,则以用户指定的 taskId 为准,在创建任务时则必须要指定对应的 taskId 参数。'
|
|
72
|
+
)
|
|
73
|
+
.optional()
|
|
74
|
+
})
|
|
75
|
+
.extend(MCPRunTasksOptions.shape)
|
|
76
|
+
|
|
77
|
+
export type Options = z.infer<typeof Options>
|
|
78
|
+
|
|
79
|
+
export const Entity = z.object({
|
|
80
|
+
prompt: z
|
|
81
|
+
.string()
|
|
82
|
+
.describe('实体的描述,简单介绍一下当前实体的作用。')
|
|
83
|
+
.optional(),
|
|
84
|
+
promptPath: z
|
|
85
|
+
.string()
|
|
86
|
+
.describe(
|
|
87
|
+
'实体的描述文件路径,文件内容为实体的描述。默认为当前目录下的 AGENTS.md 文件。'
|
|
88
|
+
)
|
|
89
|
+
.optional(),
|
|
90
|
+
rules: z
|
|
91
|
+
.array(
|
|
92
|
+
z.union([
|
|
93
|
+
z.string(),
|
|
94
|
+
z.discriminatedUnion('type', [
|
|
95
|
+
z.object({
|
|
96
|
+
type: z.literal('local').optional(),
|
|
97
|
+
path: z.string(),
|
|
98
|
+
desc: z.string().optional()
|
|
99
|
+
}),
|
|
100
|
+
z.object({
|
|
101
|
+
type: z.literal('remote'),
|
|
102
|
+
tags: z.array(z.string()).describe('关键 tag').optional(),
|
|
103
|
+
desc: z.string().describe('知识库的描述').optional()
|
|
104
|
+
})
|
|
105
|
+
])
|
|
106
|
+
])
|
|
107
|
+
)
|
|
108
|
+
.optional()
|
|
109
|
+
.describe('垂类 agent 的规则集合'),
|
|
110
|
+
skills: z
|
|
111
|
+
.object({
|
|
112
|
+
type: z.union([z.literal('include'), z.literal('exclude')]),
|
|
113
|
+
list: z.array(z.string()).describe('技能列表')
|
|
114
|
+
})
|
|
115
|
+
.optional(),
|
|
116
|
+
mcpServers: z
|
|
117
|
+
.object({
|
|
118
|
+
include: z.array(z.string()).describe('包含的服务名称列表'),
|
|
119
|
+
exclude: z.array(z.string()).describe('排除的服务名称列表')
|
|
120
|
+
})
|
|
121
|
+
.optional(),
|
|
122
|
+
tools: z
|
|
123
|
+
.object({
|
|
124
|
+
include: z.array(z.string()).describe('包含的工具名称列表'),
|
|
125
|
+
exclude: z.array(z.string()).describe('排除的工具名称列表')
|
|
126
|
+
})
|
|
127
|
+
.optional(),
|
|
128
|
+
defaultModel: z.string().optional()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
export type Entity = z.infer<typeof Entity>
|