@vibe-forge/core 0.7.0 → 0.7.3
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/package.json
CHANGED
package/src/adapter/type.ts
CHANGED
|
@@ -6,10 +6,18 @@ import type { ChatMessage, ChatMessageContent } from '../types'
|
|
|
6
6
|
|
|
7
7
|
export type AdapterMessageContent = ChatMessageContent
|
|
8
8
|
|
|
9
|
+
export interface AdapterErrorData {
|
|
10
|
+
message: string
|
|
11
|
+
code?: string
|
|
12
|
+
details?: unknown
|
|
13
|
+
fatal?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
9
16
|
export type AdapterOutputEvent =
|
|
10
17
|
| { type: 'init'; data: SessionInitInfo }
|
|
11
18
|
| { type: 'summary'; data: SessionSummaryInfo }
|
|
12
19
|
| { type: 'message'; data: ChatMessage }
|
|
20
|
+
| { type: 'error'; data: AdapterErrorData }
|
|
13
21
|
| { type: 'exit'; data: { exitCode?: number; stderr?: string } }
|
|
14
22
|
| { type: 'stop'; data?: ChatMessage }
|
|
15
23
|
|
package/src/config/types.ts
CHANGED
|
@@ -34,6 +34,20 @@ export interface ModelServiceConfig {
|
|
|
34
34
|
* 模型服务支持的模型别名
|
|
35
35
|
*/
|
|
36
36
|
modelsAlias?: Record<string, string[]>
|
|
37
|
+
/**
|
|
38
|
+
* 模型服务超时(毫秒)。
|
|
39
|
+
* - Codex: 映射为 `stream_idle_timeout_ms`
|
|
40
|
+
* - Claude Code Router: 映射为全局 `API_TIMEOUT_MS`
|
|
41
|
+
* - OpenCode: 映射为 provider `timeout` / `chunkTimeout`
|
|
42
|
+
*/
|
|
43
|
+
timeoutMs?: number
|
|
44
|
+
/**
|
|
45
|
+
* 模型服务默认最大输出 token。
|
|
46
|
+
* - Codex: 映射为 `turn/start.maxOutputTokens`
|
|
47
|
+
* - Claude Code Router: 映射为 `maxtoken` transformer
|
|
48
|
+
* - OpenCode: 映射为 model `options.maxOutputTokens` / `limit.output`
|
|
49
|
+
*/
|
|
50
|
+
maxOutputTokens?: number
|
|
37
51
|
/**
|
|
38
52
|
* 拓展配置,由下游自行消费
|
|
39
53
|
*/
|
|
@@ -178,6 +192,12 @@ export interface Config {
|
|
|
178
192
|
* 自定义对话风格。通过指定提示词约束对话风格。
|
|
179
193
|
*/
|
|
180
194
|
customInstructions?: string
|
|
195
|
+
/**
|
|
196
|
+
* 是否注入 Vibe Forge 自动生成的默认系统提示词
|
|
197
|
+
* (例如 rules / skills / entities / specs 生成的提示词)。
|
|
198
|
+
* 默认为 true。
|
|
199
|
+
*/
|
|
200
|
+
injectDefaultSystemPrompt?: boolean
|
|
181
201
|
}
|
|
182
202
|
/**
|
|
183
203
|
* 插件配置
|
|
@@ -1,24 +1,122 @@
|
|
|
1
1
|
import process from 'node:process'
|
|
2
|
+
import { basename, dirname } from 'node:path'
|
|
2
3
|
|
|
3
4
|
import type { AdapterQueryOptions } from '#~/adapter/type.js'
|
|
4
5
|
import { DefinitionLoader } from '#~/utils/definition-loader.js'
|
|
5
|
-
import type { Definition, Filter, Skill } from '#~/utils/definition-loader.js'
|
|
6
|
+
import type { Definition, Entity, Filter, Skill, Spec } from '#~/utils/definition-loader.js'
|
|
7
|
+
|
|
8
|
+
const filterSkills = (
|
|
9
|
+
skills: Definition<Skill>[],
|
|
10
|
+
selection?: AdapterQueryOptions['skills']
|
|
11
|
+
) => {
|
|
12
|
+
if (selection == null) return skills
|
|
13
|
+
|
|
14
|
+
const include = selection.include != null && selection.include.length > 0
|
|
15
|
+
? new Set(selection.include)
|
|
16
|
+
: undefined
|
|
17
|
+
const exclude = new Set(selection.exclude ?? [])
|
|
18
|
+
|
|
19
|
+
return skills.filter((skill) => {
|
|
20
|
+
const name = basename(dirname(skill.path))
|
|
21
|
+
return (include == null || include.has(name)) && !exclude.has(name)
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const dedupeSkills = (skills: Definition<Skill>[]) => {
|
|
26
|
+
const seen = new Set<string>()
|
|
27
|
+
return skills.filter((skill) => {
|
|
28
|
+
if (seen.has(skill.path)) return false
|
|
29
|
+
seen.add(skill.path)
|
|
30
|
+
return true
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type SkillSelectionInput =
|
|
35
|
+
| AdapterQueryOptions['skills']
|
|
36
|
+
| Entity['skills']
|
|
37
|
+
| Spec['skills']
|
|
38
|
+
|
|
39
|
+
const toNormalizedSkillSelection = (
|
|
40
|
+
selection?: SkillSelectionInput
|
|
41
|
+
): AdapterQueryOptions['skills'] | undefined => {
|
|
42
|
+
if (selection == null) return undefined
|
|
43
|
+
|
|
44
|
+
if (Array.isArray(selection)) {
|
|
45
|
+
return selection.length > 0
|
|
46
|
+
? {
|
|
47
|
+
include: selection
|
|
48
|
+
}
|
|
49
|
+
: undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if ('type' in selection && Array.isArray(selection.list)) {
|
|
53
|
+
const list = selection.list.filter((item): item is string => typeof item === 'string')
|
|
54
|
+
return selection.type === 'include'
|
|
55
|
+
? {
|
|
56
|
+
include: list
|
|
57
|
+
}
|
|
58
|
+
: {
|
|
59
|
+
exclude: list
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return selection
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const mergeSkillSelections = (
|
|
67
|
+
...selections: Array<SkillSelectionInput | undefined>
|
|
68
|
+
): AdapterQueryOptions['skills'] | undefined => {
|
|
69
|
+
let include: Set<string> | undefined
|
|
70
|
+
const exclude = new Set<string>()
|
|
71
|
+
|
|
72
|
+
for (const selection of selections) {
|
|
73
|
+
const normalized = toNormalizedSkillSelection(selection)
|
|
74
|
+
if (normalized == null) continue
|
|
75
|
+
|
|
76
|
+
if (normalized.include != null && normalized.include.length > 0) {
|
|
77
|
+
const current = new Set(normalized.include)
|
|
78
|
+
include = include == null
|
|
79
|
+
? current
|
|
80
|
+
: new Set([...include].filter(item => current.has(item)))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const item of normalized.exclude ?? []) {
|
|
84
|
+
exclude.add(item)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (include == null && exclude.size === 0) return undefined
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
include: include == null ? undefined : [...include],
|
|
92
|
+
exclude: exclude.size === 0 ? undefined : [...exclude]
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const getIncludedSkillNames = (selection?: SkillSelectionInput): string[] => {
|
|
97
|
+
const normalized = toNormalizedSkillSelection(selection)
|
|
98
|
+
return normalized?.include ?? []
|
|
99
|
+
}
|
|
6
100
|
|
|
7
101
|
export async function generateAdapterQueryOptions(
|
|
8
102
|
type: 'spec' | 'entity' | undefined,
|
|
9
103
|
name?: string,
|
|
10
|
-
cwd: string = process.cwd()
|
|
104
|
+
cwd: string = process.cwd(),
|
|
105
|
+
input?: {
|
|
106
|
+
skills?: AdapterQueryOptions['skills']
|
|
107
|
+
}
|
|
11
108
|
) {
|
|
12
109
|
const loader = new DefinitionLoader(cwd)
|
|
13
110
|
const options: Partial<AdapterQueryOptions> = {}
|
|
14
111
|
const systemPromptParts: string[] = []
|
|
112
|
+
let effectiveSkillSelection = toNormalizedSkillSelection(input?.skills)
|
|
15
113
|
|
|
16
114
|
// 1. 获取数据
|
|
17
115
|
// 1.1 获取默认数据
|
|
18
116
|
const entities = type !== 'entity'
|
|
19
117
|
? await loader.loadDefaultEntities()
|
|
20
118
|
: []
|
|
21
|
-
const
|
|
119
|
+
const defaultSkills = await loader.loadDefaultSkills()
|
|
22
120
|
const rules = await loader.loadDefaultRules()
|
|
23
121
|
const specs = await loader.loadDefaultSpecs()
|
|
24
122
|
|
|
@@ -42,7 +140,9 @@ export async function generateAdapterQueryOptions(
|
|
|
42
140
|
// always load spec or entity tagged rules
|
|
43
141
|
rules.push(
|
|
44
142
|
...(
|
|
45
|
-
await loader.loadRules(attributes.rules
|
|
143
|
+
await loader.loadRules(attributes.rules, {
|
|
144
|
+
baseDir: dirname(data.path)
|
|
145
|
+
})
|
|
46
146
|
).map((rule) => ({
|
|
47
147
|
...rule,
|
|
48
148
|
attributes: {
|
|
@@ -56,18 +156,40 @@ export async function generateAdapterQueryOptions(
|
|
|
56
156
|
if (
|
|
57
157
|
attributes.skills
|
|
58
158
|
) {
|
|
59
|
-
|
|
159
|
+
effectiveSkillSelection = mergeSkillSelections(
|
|
160
|
+
input?.skills,
|
|
161
|
+
attributes.skills
|
|
162
|
+
)
|
|
163
|
+
targetSkills.push(
|
|
164
|
+
...filterSkills(
|
|
165
|
+
await loader.loadSkills(getIncludedSkillNames(attributes.skills)),
|
|
166
|
+
effectiveSkillSelection
|
|
167
|
+
)
|
|
168
|
+
)
|
|
60
169
|
}
|
|
61
170
|
|
|
62
171
|
targetBody = body
|
|
63
172
|
targetToolsFilter = attributes.tools
|
|
64
173
|
targetMcpServersFilter = attributes.mcpServers
|
|
65
174
|
}
|
|
175
|
+
const skills = filterSkills(defaultSkills, effectiveSkillSelection)
|
|
176
|
+
let selectedSkillsPrompt: Definition<Skill>[] = []
|
|
177
|
+
if (input?.skills?.include != null && input.skills.include.length > 0) {
|
|
178
|
+
selectedSkillsPrompt = dedupeSkills(
|
|
179
|
+
filterSkills(
|
|
180
|
+
await loader.loadSkills(input.skills.include),
|
|
181
|
+
effectiveSkillSelection
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
}
|
|
66
185
|
|
|
67
186
|
// 2. 基于数据生成上下文
|
|
68
187
|
// 2.1 加载关联上下文
|
|
69
188
|
systemPromptParts.push(loader.generateRulesPrompt(rules))
|
|
70
|
-
systemPromptParts.push(loader.generateSkillsPrompt(targetSkills))
|
|
189
|
+
systemPromptParts.push(loader.generateSkillsPrompt(dedupeSkills(targetSkills)))
|
|
190
|
+
systemPromptParts.push(loader.generateSkillsPrompt(
|
|
191
|
+
selectedSkillsPrompt.filter(skill => !targetSkills.some(target => target.path === skill.path))
|
|
192
|
+
))
|
|
71
193
|
// 2.2 加载上下文路由
|
|
72
194
|
systemPromptParts.push(loader.generateEntitiesRoutePrompt(entities))
|
|
73
195
|
systemPromptParts.push(loader.generateSkillsRoutePrompt(skills))
|
|
@@ -36,6 +36,25 @@ export interface Spec {
|
|
|
36
36
|
tools?: Filter
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export interface LocalRuleReference {
|
|
40
|
+
type?: 'local'
|
|
41
|
+
path: string
|
|
42
|
+
desc?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RemoteRuleReference {
|
|
46
|
+
type: 'remote'
|
|
47
|
+
tags?: string[]
|
|
48
|
+
desc?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type RuleReference = string | LocalRuleReference | RemoteRuleReference
|
|
52
|
+
|
|
53
|
+
export interface SkillSelection {
|
|
54
|
+
type: 'include' | 'exclude'
|
|
55
|
+
list: string[]
|
|
56
|
+
}
|
|
57
|
+
|
|
39
58
|
export interface Entity {
|
|
40
59
|
name?: string
|
|
41
60
|
always?: boolean
|
|
@@ -43,8 +62,8 @@ export interface Entity {
|
|
|
43
62
|
tags?: string[]
|
|
44
63
|
prompt?: string
|
|
45
64
|
promptPath?: string
|
|
46
|
-
rules?:
|
|
47
|
-
skills?: string[]
|
|
65
|
+
rules?: RuleReference[]
|
|
66
|
+
skills?: string[] | SkillSelection
|
|
48
67
|
mcpServers?: Filter
|
|
49
68
|
tools?: Filter
|
|
50
69
|
}
|
|
@@ -123,6 +142,66 @@ const resolveSpecIdentifier = (path: string, explicitName?: string) => {
|
|
|
123
142
|
return resolveDocumentName(path, explicitName, ['index.md'])
|
|
124
143
|
}
|
|
125
144
|
|
|
145
|
+
const toNonEmptyStringArray = (values: unknown): string[] => {
|
|
146
|
+
if (!Array.isArray(values)) return []
|
|
147
|
+
return values
|
|
148
|
+
.filter((value): value is string => typeof value === 'string')
|
|
149
|
+
.map(value => value.trim())
|
|
150
|
+
.filter(Boolean)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const isLocalRuleReference = (value: RuleReference): value is LocalRuleReference => {
|
|
154
|
+
return (
|
|
155
|
+
value != null &&
|
|
156
|
+
typeof value === 'object' &&
|
|
157
|
+
typeof value.path === 'string' &&
|
|
158
|
+
(value.type == null || value.type === 'local')
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const isRemoteRuleReference = (value: RuleReference): value is RemoteRuleReference => {
|
|
163
|
+
return (
|
|
164
|
+
value != null &&
|
|
165
|
+
typeof value === 'object' &&
|
|
166
|
+
value.type === 'remote'
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const resolveRulePattern = (pattern: string, baseDir: string) => {
|
|
171
|
+
const trimmed = pattern.trim()
|
|
172
|
+
if (!trimmed) return undefined
|
|
173
|
+
if (trimmed.startsWith('./') || trimmed.startsWith('../')) {
|
|
174
|
+
return normalizePath(resolve(baseDir, trimmed))
|
|
175
|
+
}
|
|
176
|
+
return trimmed
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const createRemoteRuleDefinition = (
|
|
180
|
+
rule: RemoteRuleReference,
|
|
181
|
+
index: number
|
|
182
|
+
): Definition<Rule> => {
|
|
183
|
+
const tags = toNonEmptyStringArray(rule.tags)
|
|
184
|
+
const desc = rule.desc?.trim() || (
|
|
185
|
+
tags.length > 0
|
|
186
|
+
? `远程知识库标签:${tags.join(', ')}`
|
|
187
|
+
: '远程知识库规则引用'
|
|
188
|
+
)
|
|
189
|
+
const bodyParts = [
|
|
190
|
+
desc,
|
|
191
|
+
tags.length > 0 ? `知识库标签:${tags.join(', ')}` : undefined,
|
|
192
|
+
'该规则来自远程知识库引用,不对应本地文件。'
|
|
193
|
+
].filter((value): value is string => Boolean(value))
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
path: `remote-rule-${index + 1}.md`,
|
|
197
|
+
body: bodyParts.join('\n'),
|
|
198
|
+
attributes: {
|
|
199
|
+
name: tags.length > 0 ? `remote:${tags.join(',')}` : `remote-rule-${index + 1}`,
|
|
200
|
+
description: desc
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
126
205
|
export class DefinitionLoader {
|
|
127
206
|
private readonly cwd: string
|
|
128
207
|
|
|
@@ -142,10 +221,53 @@ export class DefinitionLoader {
|
|
|
142
221
|
})
|
|
143
222
|
}
|
|
144
223
|
|
|
145
|
-
async loadRules(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
224
|
+
async loadRules(
|
|
225
|
+
rules: RuleReference[],
|
|
226
|
+
options?: {
|
|
227
|
+
baseDir?: string
|
|
228
|
+
}
|
|
229
|
+
) {
|
|
230
|
+
const baseDir = options?.baseDir ?? this.cwd
|
|
231
|
+
const definitions: Definition<Rule>[] = []
|
|
232
|
+
|
|
233
|
+
for (const [index, rule] of rules.entries()) {
|
|
234
|
+
if (typeof rule === 'string') {
|
|
235
|
+
const pattern = resolveRulePattern(rule, baseDir)
|
|
236
|
+
if (!pattern) continue
|
|
237
|
+
definitions.push(
|
|
238
|
+
...await loadLocalDocuments<Rule>(
|
|
239
|
+
await this.scan([pattern])
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (isRemoteRuleReference(rule)) {
|
|
246
|
+
definitions.push(createRemoteRuleDefinition(rule, index))
|
|
247
|
+
continue
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!isLocalRuleReference(rule)) continue
|
|
251
|
+
|
|
252
|
+
const pattern = resolveRulePattern(rule.path, baseDir)
|
|
253
|
+
if (!pattern) continue
|
|
254
|
+
|
|
255
|
+
const docs = await loadLocalDocuments<Rule>(
|
|
256
|
+
await this.scan([pattern])
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
definitions.push(
|
|
260
|
+
...docs.map((doc) => ({
|
|
261
|
+
...doc,
|
|
262
|
+
attributes: {
|
|
263
|
+
...doc.attributes,
|
|
264
|
+
description: rule.desc?.trim() || doc.attributes.description
|
|
265
|
+
}
|
|
266
|
+
}))
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return definitions
|
|
149
271
|
}
|
|
150
272
|
async loadDefaultRules(): Promise<Definition<Rule>[]> {
|
|
151
273
|
return this.loadRules([
|
package/src/ws.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { SessionInfo } from './adapter/index.js'
|
|
1
|
+
import type { AdapterErrorData, SessionInfo } from './adapter/index.js'
|
|
2
2
|
import type { AskUserQuestionParams, ChatMessage } from './types.js'
|
|
3
3
|
|
|
4
4
|
export type WSEvent =
|
|
5
|
-
| { type: 'error';
|
|
5
|
+
| { type: 'error'; data: AdapterErrorData; message?: string }
|
|
6
6
|
| { type: 'message'; message: ChatMessage }
|
|
7
7
|
| { type: 'session_info'; info: SessionInfo }
|
|
8
8
|
| { type: 'tool_result'; toolCallId: string; output: any; isError: boolean }
|