@vibe-forge/core 0.4.0 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-forge/core",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "imports": {
5
5
  "#~/*.js": {
6
6
  "__vibe-forge__": {
@@ -22,60 +22,61 @@
22
22
  "require": "./dist/index.js"
23
23
  }
24
24
  },
25
- "./channel": {
25
+ "./adapter": {
26
26
  "__vibe-forge__": {
27
- "default": "./src/channel.ts"
27
+ "default": "./src/adapter/index.ts"
28
28
  },
29
29
  "default": {
30
- "import": "./dist/channel.mjs",
31
- "require": "./dist/channel.js"
30
+ "import": "./dist/adapter.mjs",
31
+ "require": "./dist/adapter.js"
32
32
  }
33
33
  },
34
- "./utils/*": {
34
+ "./channel": {
35
35
  "__vibe-forge__": {
36
- "default": "./src/utils/*.ts"
36
+ "default": "./src/channel.ts"
37
37
  },
38
38
  "default": {
39
- "import": "./dist/utils/*.mjs",
40
- "require": "./dist/utils/*.js"
39
+ "import": "./dist/channel.mjs",
40
+ "require": "./dist/channel.js"
41
41
  }
42
42
  },
43
- "./controllers/task": {
43
+ "./schema": {
44
44
  "__vibe-forge__": {
45
- "default": "./src/controllers/task/index.ts"
45
+ "default": "./src/schema.ts"
46
46
  },
47
47
  "default": {
48
- "import": "./dist/controllers/task/index.mjs",
49
- "require": "./dist/controllers/task/index.js"
48
+ "import": "./dist/schema.mjs",
49
+ "require": "./dist/schema.js"
50
50
  }
51
51
  },
52
- "./controllers/benchmark": {
52
+ "./hooks": {
53
53
  "__vibe-forge__": {
54
- "default": "./src/controllers/benchmark/index.ts"
54
+ "default": "./src/hooks/index.ts"
55
55
  },
56
56
  "default": {
57
- "import": "./dist/controllers/benchmark/index.mjs",
58
- "require": "./dist/controllers/benchmark/index.js"
57
+ "import": "./dist/hooks/index.mjs",
58
+ "require": "./dist/hooks/index.js"
59
59
  }
60
60
  },
61
- "./schema": {
61
+ "./utils/*": {
62
62
  "__vibe-forge__": {
63
- "default": "./src/schema.ts"
63
+ "default": "./src/utils/*.ts"
64
64
  },
65
65
  "default": {
66
- "import": "./dist/schema.mjs",
67
- "require": "./dist/schema.js"
66
+ "import": "./dist/utils/*.mjs",
67
+ "require": "./dist/utils/*.js"
68
68
  }
69
69
  },
70
- "./hooks": {
70
+ "./controllers/*": {
71
71
  "__vibe-forge__": {
72
- "default": "./src/hooks/index.ts"
72
+ "default": "./src/controllers/*/index.ts"
73
73
  },
74
74
  "default": {
75
- "import": "./dist/hooks/index.mjs",
76
- "require": "./dist/hooks/index.js"
75
+ "import": "./dist/controllers/*/index.mjs",
76
+ "require": "./dist/controllers/*/index.js"
77
77
  }
78
- }
78
+ },
79
+ "./package.json": "./package.json"
79
80
  },
80
81
  "dependencies": {
81
82
  "fast-glob": "^3.3.3",
@@ -20,6 +20,7 @@ export type SessionInfo =
20
20
  export interface SessionInitInfo {
21
21
  uuid: string
22
22
  model: string
23
+ adapter?: string
23
24
  version: string
24
25
  tools: string[]
25
26
  slashCommands: string[]
package/src/channel.ts CHANGED
@@ -4,12 +4,53 @@ export interface ChannelMap {}
4
4
 
5
5
  export type ChannelType = keyof ChannelMap
6
6
 
7
+ export const channelAccessSchema = z.object({
8
+ // 管理员
9
+ admins: z.array(z.string()).optional().describe(
10
+ '频道管理员账号(sender ID),管理员拥有管理操作权限且不受以下访问控制限制'
11
+ ),
12
+ // 会话类型控制
13
+ allowPrivateChat: z.boolean().optional().describe('是否允许私聊消息,默认 true'),
14
+ allowGroupChat: z.boolean().optional().describe('是否允许群聊消息,默认 true'),
15
+ // 群组访问控制
16
+ allowedGroups: z.array(z.string()).optional().describe('群组白名单(channel ID),设置后仅在指定群中响应'),
17
+ blockedGroups: z.array(z.string()).optional().describe('群组黑名单(channel ID),在指定群中不响应'),
18
+ // 用户访问控制
19
+ allowedSenders: z.array(z.string()).optional().describe('发送者白名单(sender ID),设置后仅白名单内的用户可交互'),
20
+ blockedSenders: z.array(z.string()).optional().describe('发送者黑名单(sender ID),黑名单内的用户消息将被忽略')
21
+ })
22
+
23
+ export type ChannelAccessConfig = z.infer<typeof channelAccessSchema>
24
+
7
25
  export const channelBaseSchema = z.object({
8
- type: z.string().min(1).describe('频道类型'),
9
- title: z.string().optional().describe('频道标题'),
10
- description: z.string().optional().describe('频道说明'),
11
- enabled: z.boolean().optional().describe('是否启用'),
12
- admins: z.array(z.string()).optional().describe('频道管理员账号')
26
+ // 基础配置
27
+ type: z
28
+ .string().min(1)
29
+ .describe('频道类型'),
30
+ title: z
31
+ .string().optional()
32
+ .describe('频道标题'),
33
+ description: z
34
+ .string().optional()
35
+ .describe('频道说明'),
36
+ enabled: z
37
+ .boolean().optional()
38
+ .describe('是否启用'),
39
+ // 会话行为
40
+ systemPrompt: z
41
+ .string().optional()
42
+ .describe('在此频道启动会话时注入的系统提示词'),
43
+ // 指令
44
+ commandPrefix: z
45
+ .string().optional()
46
+ .describe('频道指令前缀,默认 /'),
47
+ language: z
48
+ .enum(['zh', 'en']).optional()
49
+ .describe('频道提示语言,默认 zh'),
50
+ // 访问权限控制
51
+ access: channelAccessSchema
52
+ .optional()
53
+ .describe('频道访问权限配置')
13
54
  })
14
55
 
15
56
  export type ChannelBaseConfig = z.infer<typeof channelBaseSchema>
@@ -20,14 +61,48 @@ export type ChannelConfig = {
20
61
  [K in ChannelType]: ChannelConfigByType<K>
21
62
  }[ChannelType]
22
63
 
64
+ export interface ChannelSendResult {
65
+ messageId?: string
66
+ }
67
+
68
+ export interface ChannelFollowUp {
69
+ content: string
70
+ i18nContents?: Array<{
71
+ content: string
72
+ language: string
73
+ }>
74
+ }
75
+
23
76
  export interface ChannelConnection<TMessage> {
24
- sendMessage: (message: TMessage) => Promise<void>
77
+ sendMessage: (message: TMessage) => Promise<ChannelSendResult | undefined>
78
+ pushFollowUps?: (input: {
79
+ messageId: string
80
+ followUps: readonly ChannelFollowUp[]
81
+ }) => Promise<void>
25
82
  startReceiving?: (options: {
26
83
  handlers: ChannelEventHandlers
27
84
  }) => Promise<void>
85
+ /**
86
+ * Called when a new session is being created for this channel.
87
+ * The channel implementation can use this to inject channel-specific context
88
+ * (e.g. bot profile fetched from platform API) into the system prompt.
89
+ */
90
+ generateSystemPrompt?: (inbound: ChannelInboundEvent) => Promise<string | undefined>
28
91
  close?: () => Promise<void>
29
92
  }
30
93
 
94
+ export interface ChannelLogger {
95
+ error: (...msg: unknown[]) => void | Promise<void>
96
+ warn: (...msg: unknown[]) => void | Promise<void>
97
+ info: (...msg: unknown[]) => void | Promise<void>
98
+ debug: (...msg: unknown[]) => void | Promise<void>
99
+ trace: (...msg: unknown[]) => void | Promise<void>
100
+ }
101
+
102
+ export interface ChannelConnectionOptions {
103
+ logger?: ChannelLogger
104
+ }
105
+
31
106
  export interface ChannelInboundEvent {
32
107
  channelType: string
33
108
  sessionType: string
@@ -70,6 +145,11 @@ export const defineChannel = <TConfigSchema extends z.ZodTypeAny, TMessageSchema
70
145
  descriptor: ChannelDescriptor<TConfigSchema, TMessageSchema>
71
146
  ) => descriptor
72
147
 
73
- export const defineChannelConnection = <TConfigSchema extends z.ZodTypeAny, TMessageSchema extends z.ZodTypeAny>(
74
- connect: (config: z.infer<TConfigSchema>) => Promise<ChannelConnection<z.infer<TMessageSchema>>>
75
- ) => connect
148
+ export type ChannelCreateFn<TConfig = unknown, TMessage = unknown> = (
149
+ config: TConfig,
150
+ options?: ChannelConnectionOptions
151
+ ) => Promise<ChannelConnection<TMessage>>
152
+
153
+ export const defineCreateChannelConnection = <TConfigSchema extends z.ZodTypeAny, TMessageSchema extends z.ZodTypeAny>(
154
+ connect: ChannelCreateFn<z.infer<TConfigSchema>, z.infer<TMessageSchema>>
155
+ ): ChannelCreateFn<z.infer<TConfigSchema>, z.infer<TMessageSchema>> => connect
@@ -1,8 +1,14 @@
1
- import type { ChannelConfig } from '../channels'
1
+ import type { ChannelConfig } from '../channel'
2
2
  import type { PluginConfig } from '../hooks'
3
3
 
4
4
  export interface AdapterMap {}
5
5
 
6
+ export interface AdapterBuiltinModel {
7
+ value: string
8
+ title: string
9
+ description: string
10
+ }
11
+
6
12
  export interface ModelServiceConfig {
7
13
  /**
8
14
  * 模型服务展示标题
@@ -228,6 +234,7 @@ export interface ConfigSection {
228
234
  modelServices?: Config['modelServices']
229
235
  channels?: Config['channels']
230
236
  adapters?: Config['adapters']
237
+ adapterBuiltinModels?: Record<string, AdapterBuiltinModel[]>
231
238
  plugins?: {
232
239
  plugins?: Config['plugins']
233
240
  enabledPlugins?: Config['enabledPlugins']
@@ -1,7 +1,7 @@
1
1
  import process from 'node:process'
2
2
 
3
- import type { AdapterCtx, AdapterQueryOptions } from '@vibe-forge/core'
4
3
  import { loadConfig } from '@vibe-forge/core'
4
+ import type { AdapterCtx, AdapterQueryOptions } from '@vibe-forge/core/adapter'
5
5
  import { getCache, setCache } from '@vibe-forge/core/utils/cache'
6
6
  import { createLogger } from '@vibe-forge/core/utils/create-logger'
7
7
  import { uuid } from '@vibe-forge/core/utils/uuid'
@@ -1,12 +1,8 @@
1
- import type {
2
- AdapterCtx,
3
- AdapterOutputEvent,
4
- AdapterQueryOptions,
5
- ModelServiceConfig,
6
- TaskDetail
7
- } from '@vibe-forge/core'
8
- import { loadAdapter } from '@vibe-forge/core'
9
- import { callHook } from '@vibe-forge/core/utils/api'
1
+ import type { AdapterCtx, AdapterOutputEvent, AdapterQueryOptions } from '#~/adapter/index.js'
2
+ import { loadAdapter } from '#~/adapter/index.js'
3
+ import type { ModelServiceConfig } from '#~/config.js'
4
+ import type { TaskDetail } from '#~/types.js'
5
+ import { callHook } from '#~/utils/api.js'
10
6
 
11
7
  import { prepare } from './prepare'
12
8
  import type { RunTaskOptions } from './type'
@@ -26,17 +22,29 @@ const resolveQueryModel = (params: {
26
22
  inputModel?: string
27
23
  }) => {
28
24
  const inputModel = normalizeNonEmptyString(params.inputModel)
29
- if (inputModel?.includes(',')) return inputModel
25
+ // User explicitly provided a model → pass through as-is.
26
+ // The adapter decides CCR vs native based on whether it contains ",".
27
+ if (inputModel != null) return inputModel
30
28
 
29
+ // No explicit model → auto-resolve from modelServices config.
30
+ // Produces "service,model" format when services are configured,
31
+ // which signals the adapter to route through CCR.
31
32
  const mergedModelServices = {
32
33
  ...(params.config?.modelServices ?? {}),
33
34
  ...(params.userConfig?.modelServices ?? {})
34
35
  }
35
- const mergedDefaultModel = pickFirstNonEmptyString([params.userConfig?.defaultModel, params.config?.defaultModel])
36
- const mergedDefaultModelService = pickFirstNonEmptyString([
37
- params.userConfig?.defaultModelService,
38
- params.config?.defaultModelService
39
- ])
36
+ const mergedDefaultModel = pickFirstNonEmptyString(
37
+ [
38
+ params.userConfig?.defaultModel,
39
+ params.config?.defaultModel
40
+ ]
41
+ )
42
+ const mergedDefaultModelService = pickFirstNonEmptyString(
43
+ [
44
+ params.userConfig?.defaultModelService,
45
+ params.config?.defaultModelService
46
+ ]
47
+ )
40
48
 
41
49
  const serviceEntries = Object.entries(mergedModelServices)
42
50
  const modelToService = new Map<string, string>()
@@ -54,20 +62,21 @@ const resolveQueryModel = (params: {
54
62
  }
55
63
  }
56
64
 
65
+ if (availableModels.length === 0) return undefined
66
+
57
67
  const resolveDefaultModel = () => {
58
- if (availableModels.length === 0) return undefined
59
68
  if (mergedDefaultModel && modelToService.has(mergedDefaultModel)) return mergedDefaultModel
60
69
  if (mergedDefaultModelService && mergedModelServices[mergedDefaultModelService]) {
61
- const service = mergedModelServices[mergedDefaultModelService]
70
+ const service = mergedModelServices[mergedDefaultModelService] as ModelServiceConfig | undefined
62
71
  const models = Array.isArray(service?.models)
63
- ? service?.models.filter(item => typeof item === 'string' && item.trim() !== '')
72
+ ? service?.models.filter((item: unknown) => typeof item === 'string' && (item as string).trim() !== '')
64
73
  : []
65
74
  if (models.length > 0) return models[0]
66
75
  }
67
76
  return availableModels[0]
68
77
  }
69
78
 
70
- const resolvedModel = inputModel ?? resolveDefaultModel()
79
+ const resolvedModel = resolveDefaultModel()
71
80
  if (!resolvedModel) return undefined
72
81
 
73
82
  const resolvedService = modelToService.get(resolvedModel) ??
@@ -120,6 +129,17 @@ export const run = async (
120
129
 
121
130
  const originalOnEvent = adapterOptions.onEvent
122
131
  const wrappedOnEvent = (event: AdapterOutputEvent) => {
132
+ if (event.type === 'init') {
133
+ originalOnEvent({
134
+ ...event,
135
+ data: {
136
+ ...event.data,
137
+ adapter: adapterType
138
+ }
139
+ })
140
+ return
141
+ }
142
+
123
143
  if (event.type === 'exit') {
124
144
  const { data } = event
125
145
 
package/src/env.ts CHANGED
@@ -11,8 +11,6 @@ export interface ServerEnv {
11
11
  __VF_PROJECT_AI_CLIENT_MODE__?: 'dev' | 'static'
12
12
  __VF_PROJECT_AI_CLIENT_BASE__?: string
13
13
  __VF_PROJECT_AI_CLIENT_DIST_PATH__?: string
14
- __VF_PROJECT_AI_ADAPTER_CLAUDE_CODE_CLI_PATH__?: string
15
- __VF_PROJECT_AI_ADAPTER_CLAUDE_CODE_CLI_ARGS__?: string
16
14
  }
17
15
 
18
16
  export function loadEnv(): ServerEnv {
@@ -26,10 +24,7 @@ export function loadEnv(): ServerEnv {
26
24
  __VF_PROJECT_AI_SERVER_ALLOW_CORS__,
27
25
  __VF_PROJECT_AI_CLIENT_MODE__ = 'static',
28
26
  __VF_PROJECT_AI_CLIENT_BASE__,
29
- __VF_PROJECT_AI_CLIENT_DIST_PATH__,
30
-
31
- __VF_PROJECT_AI_ADAPTER_CLAUDE_CODE_CLI_PATH__,
32
- __VF_PROJECT_AI_ADAPTER_CLAUDE_CODE_CLI_ARGS__
27
+ __VF_PROJECT_AI_CLIENT_DIST_PATH__
33
28
  } = processEnv || {}
34
29
  return {
35
30
  __VF_PROJECT_AI_SERVER_HOST__,
@@ -42,12 +37,8 @@ export function loadEnv(): ServerEnv {
42
37
  __VF_PROJECT_AI_SERVER_ALLOW_CORS__: __VF_PROJECT_AI_SERVER_ALLOW_CORS__ != null
43
38
  ? __VF_PROJECT_AI_SERVER_ALLOW_CORS__ === 'true'
44
39
  : true,
45
- __VF_PROJECT_AI_CLIENT_MODE__:
46
- __VF_PROJECT_AI_CLIENT_MODE__ as ServerEnv['__VF_PROJECT_AI_CLIENT_MODE__'],
40
+ __VF_PROJECT_AI_CLIENT_MODE__: __VF_PROJECT_AI_CLIENT_MODE__ as ServerEnv['__VF_PROJECT_AI_CLIENT_MODE__'],
47
41
  __VF_PROJECT_AI_CLIENT_BASE__,
48
- __VF_PROJECT_AI_CLIENT_DIST_PATH__,
49
-
50
- __VF_PROJECT_AI_ADAPTER_CLAUDE_CODE_CLI_PATH__,
51
- __VF_PROJECT_AI_ADAPTER_CLAUDE_CODE_CLI_ARGS__
42
+ __VF_PROJECT_AI_CLIENT_DIST_PATH__
52
43
  }
53
44
  }
package/src/index.ts CHANGED
@@ -1,4 +1,3 @@
1
- export * from './adapter'
2
1
  export * from './config'
3
2
  export * from './controllers/benchmark'
4
3
  export * from './controllers/config'
package/src/types.ts CHANGED
@@ -9,6 +9,8 @@ export interface Project {
9
9
 
10
10
  export type SessionStatus = 'running' | 'completed' | 'failed' | 'terminated' | 'waiting_input'
11
11
 
12
+ export type SessionPermissionMode = 'default' | 'acceptEdits' | 'plan' | 'dontAsk' | 'bypassPermissions'
13
+
12
14
  export interface Session {
13
15
  id: string
14
16
  parentSessionId?: string
@@ -21,6 +23,9 @@ export interface Session {
21
23
  isArchived?: boolean
22
24
  tags?: string[]
23
25
  status?: SessionStatus
26
+ model?: string
27
+ adapter?: string
28
+ permissionMode?: SessionPermissionMode
24
29
  }
25
30
 
26
31
  export type ChatMessageContent =
@@ -46,6 +46,13 @@ export const getCache = async <K extends keyof Cache>(
46
46
  key: K
47
47
  ): Promise<Cache[K] | undefined> => {
48
48
  const cachePath = getCachePath(cwd, taskId, sessionId, key)
49
- await fs.access(cachePath)
49
+ try {
50
+ await fs.access(cachePath)
51
+ } catch (error) {
52
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
53
+ return undefined
54
+ }
55
+ throw error
56
+ }
50
57
  return JSON.parse(await fs.readFile(cachePath, 'utf-8'))
51
58
  }
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { readFile } from 'node:fs/promises'
3
- import { basename, dirname, join, relative, resolve } from 'node:path'
3
+ import { basename, dirname, relative, resolve } from 'node:path'
4
4
  import process from 'node:process'
5
5
 
6
6
  import { glob } from 'fast-glob'
@@ -79,6 +79,50 @@ export const loadLocalDocuments = async <Attrs extends object>(
79
79
  return Promise.all(promises)
80
80
  }
81
81
 
82
+ const normalizePath = (path: string) => path.split('\\').join('/')
83
+
84
+ const stripExtension = (fileName: string) => fileName.replace(/\.[^/.]+$/, '')
85
+
86
+ const getFirstNonEmptyLine = (text: string) =>
87
+ text
88
+ .split('\n')
89
+ .map(line => line.trim())
90
+ .find(Boolean)
91
+
92
+ const toPromptPath = (cwd: string, path: string) => {
93
+ const relPath = normalizePath(relative(cwd, path))
94
+ return relPath.startsWith('..') ? normalizePath(path) : relPath
95
+ }
96
+
97
+ const resolveDocumentName = (
98
+ path: string,
99
+ explicitName?: string,
100
+ indexFileNames: string[] = []
101
+ ) => {
102
+ const trimmedName = explicitName?.trim()
103
+ if (trimmedName) return trimmedName
104
+
105
+ const fileName = basename(path).toLowerCase()
106
+ if (indexFileNames.includes(fileName)) {
107
+ return basename(dirname(path))
108
+ }
109
+
110
+ return stripExtension(basename(path))
111
+ }
112
+
113
+ const resolveDocumentDescription = (
114
+ body: string,
115
+ explicitDescription?: string,
116
+ fallbackName?: string
117
+ ) => {
118
+ const trimmedDescription = explicitDescription?.trim()
119
+ return trimmedDescription || getFirstNonEmptyLine(body) || fallbackName || ''
120
+ }
121
+
122
+ const resolveSpecIdentifier = (path: string, explicitName?: string) => {
123
+ return resolveDocumentName(path, explicitName, ['index.md'])
124
+ }
125
+
82
126
  export class DefinitionLoader {
83
127
  private readonly cwd: string
84
128
 
@@ -113,13 +157,12 @@ export class DefinitionLoader {
113
157
  const rulesPrompt = rules
114
158
  .map((rule) => {
115
159
  const { path, body, attributes } = rule
116
- const name = attributes.name ?? basename(path)
117
- const desc = attributes.description ?? name
118
- return (
119
- ` - ${name}:${desc}\n` +
120
- `${attributes.always ? body : ''}\n` +
121
- '--------------------\n'
122
- )
160
+ const name = resolveDocumentName(path, attributes.name)
161
+ const desc = resolveDocumentDescription(body, attributes.description, name)
162
+ const content = attributes.always && body.trim()
163
+ ? `<rule-content>\n${body.trim()}\n</rule-content>\n`
164
+ : ''
165
+ return ` - ${name}:${desc}\n${content}--------------------\n`
123
166
  })
124
167
  .filter(Boolean)
125
168
  .join('\n')
@@ -150,9 +193,7 @@ export class DefinitionLoader {
150
193
  // Filter by directory name (skill name)
151
194
  if (skills) {
152
195
  paths = paths.filter(path => {
153
- const parts = path.split('/')
154
- // .../skills/{name}/SKILL.md
155
- return skills.includes(parts[parts.length - 2])
196
+ return skills.includes(basename(dirname(path)))
156
197
  })
157
198
  }
158
199
 
@@ -164,13 +205,17 @@ export class DefinitionLoader {
164
205
  generateSkillsPrompt(skills: Definition<Skill>[]): string {
165
206
  return skills
166
207
  .map((skill) => {
167
- const { path, body } = skill
208
+ const { path, body, attributes } = skill
209
+ const name = resolveDocumentName(path, attributes.name, ['skill.md'])
210
+ const desc = resolveDocumentDescription(body, attributes.description, name)
168
211
  return (
169
212
  '技能相关信息如下,通过阅读以下内容了解技能的详细信息:\n' +
170
- `- 技能文件资源路径:${dirname(path)}\n` +
213
+ `- 技能名称:${name}\n` +
214
+ `- 技能介绍:${desc}\n` +
215
+ `- 技能文件资源路径:${toPromptPath(this.cwd, dirname(path))}\n` +
171
216
  '- 资源内容:\n' +
172
217
  '<skill-content>\n' +
173
- `${body}\n` +
218
+ `${body.trim()}\n` +
174
219
  '</skill-content>\n' +
175
220
  '资源内容中的文件路径相对「技能文件资源路径」路径,通过读取相关工具按照实际需要进行阅读。\n'
176
221
  )
@@ -185,7 +230,11 @@ export class DefinitionLoader {
185
230
  skills
186
231
  .filter(({ attributes: { always } }) => always !== false)
187
232
  .map(
188
- ({ attributes: { name, description } }) => ` - ${name}:${description}\n`
233
+ ({ path, body, attributes }) => {
234
+ const name = resolveDocumentName(path, attributes.name, ['skill.md'])
235
+ const desc = resolveDocumentDescription(body, attributes.description, name)
236
+ return ` - ${name}:${desc}\n`
237
+ }
189
238
  )
190
239
  .join('')
191
240
  }\n` +
@@ -197,7 +246,8 @@ export class DefinitionLoader {
197
246
  const patterns = [
198
247
  `.ai/specs/${name}.md`,
199
248
  `.ai/specs/${name}/index.md`,
200
- `.ai/plugins/*/specs/${name}.md`
249
+ `.ai/plugins/*/specs/${name}.md`,
250
+ `.ai/plugins/*/specs/${name}/index.md`
201
251
  ]
202
252
  const paths = await this.scan(patterns)
203
253
  if (paths.length === 0) return undefined
@@ -221,34 +271,23 @@ export class DefinitionLoader {
221
271
  generateSpecRoutePrompt(specsDocuments: Definition<Spec>[]): string {
222
272
  const specsRouteStr = specsDocuments
223
273
  .filter(({ attributes }) => attributes.always !== false)
224
- .map(({ path, attributes }) => {
225
- const name = attributes.name ?? basename(dirname(path))
226
- const desc = attributes.description ?? name
274
+ .map(({ path, body, attributes }) => {
275
+ const name = resolveDocumentName(path, attributes.name, ['index.md'])
276
+ const desc = resolveDocumentDescription(body, attributes.description, name)
277
+ const identifier = resolveSpecIdentifier(path, attributes.name)
227
278
  const params = attributes.params ?? []
228
- // Calculate relative path for display/ID
229
- // User code used relative('.ai/specs', path), but here path is absolute.
230
- // We can try to make it relative to cwd/.ai/specs if possible, or just relative to cwd.
231
- // The user code seems to assume specs are in .ai/specs.
232
- // Let's use relative(join(this.cwd, '.ai/specs'), path) if it's in there, otherwise...
233
- // Actually, just providing a relative path from project root is probably fine or the name.
234
- // User code: relative('.ai/specs', path)
235
-
236
- let relPath = relative(join(this.cwd, '.ai/specs'), path)
237
- if (relPath.startsWith('..')) {
238
- // Maybe in a plugin?
239
- relPath = relative(this.cwd, path)
240
- }
279
+ const paramsPrompt = params.length > 0
280
+ ? params
281
+ .map(({ name, description }) => ` - ${name}:${description ?? '无'}\n`)
282
+ .join('')
283
+ : ' - 无\n'
241
284
 
242
285
  return (
243
286
  `- 流程名称:${name}\n` +
244
287
  ` - 介绍:${desc}\n` +
245
- ` - 标识:${relPath}\n` +
288
+ ` - 标识:${identifier}\n` +
246
289
  ' - 参数:\n' +
247
- `${
248
- params
249
- .map(({ name, description }) => ` - ${name}:${description}\n`)
250
- .join('')
251
- }\n`
290
+ `${paramsPrompt}`
252
291
  )
253
292
  })
254
293
  .join('\n')
@@ -285,7 +324,9 @@ export class DefinitionLoader {
285
324
 
286
325
  // 2. Fallback to Markdown file
287
326
  const patterns = [
327
+ `.ai/entities/${name}.md`,
288
328
  `.ai/entities/${name}/README.md`,
329
+ `.ai/plugins/*/entities/${name}.md`,
289
330
  `.ai/plugins/*/entities/${name}/README.md`
290
331
  ]
291
332
  const paths = await this.scan(patterns)
@@ -327,7 +368,9 @@ export class DefinitionLoader {
327
368
  // List both .md and index.json entities
328
369
  const mdPatterns = [
329
370
  '.ai/entities/*.md',
330
- '.ai/plugins/*/entities/*.md'
371
+ '.ai/entities/*/README.md',
372
+ '.ai/plugins/*/entities/*.md',
373
+ '.ai/plugins/*/entities/*/README.md'
331
374
  ]
332
375
  const jsonPatterns = [
333
376
  '.ai/entities/*/index.json',
@@ -350,7 +393,12 @@ export class DefinitionLoader {
350
393
  '项目存在如下实体:\n' +
351
394
  `${
352
395
  entities
353
- .map(({ attributes: { name, prompt: _p }, body }) => ` - ${name}:${body}\n`)
396
+ .filter(({ attributes }) => attributes.always !== false)
397
+ .map(({ path, attributes, body }) => {
398
+ const name = resolveDocumentName(path, attributes.name, ['readme.md', 'index.json'])
399
+ const desc = resolveDocumentDescription(body, attributes.description, name)
400
+ return ` - ${name}:${desc}\n`
401
+ })
354
402
  .join('')
355
403
  }\n` +
356
404
  '解决用户问题时,需根据用户需求可以通过 run-tasks 工具指定为实体后,自行调度多个不同类型的实体来完成工作。\n' +