@vibe-forge/core 0.5.0 → 0.7.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 +1 -1
- package/src/adapter/index.ts +1 -1
- package/src/adapter/loader.ts +5 -3
- package/src/adapter/type.ts +2 -6
- package/src/channel.ts +24 -1
- package/src/controllers/task/prepare.ts +9 -1
- package/src/controllers/task/run.ts +14 -1
- package/src/env.ts +31 -3
- package/src/hooks/call.ts +74 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/runtime.ts +139 -0
- package/src/types.ts +5 -0
- package/src/utils/cache.ts +8 -1
- package/src/utils/create-logger.ts +26 -11
- package/src/utils/definition-loader.ts +88 -40
- package/src/utils/api.ts +0 -32
package/package.json
CHANGED
package/src/adapter/index.ts
CHANGED
package/src/adapter/loader.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { Adapter } from './type'
|
|
2
2
|
|
|
3
|
+
const resolveAdapterPackageName = (type: string) => (
|
|
4
|
+
type.startsWith('@') ? type : `@vibe-forge/adapter-${type}`
|
|
5
|
+
)
|
|
6
|
+
|
|
3
7
|
export const loadAdapter = async (type: string) =>
|
|
4
8
|
(
|
|
5
9
|
// eslint-disable-next-line ts/no-require-imports
|
|
6
|
-
require(
|
|
7
|
-
type.startsWith('@') ? type : `@vibe-forge/adapter-${type}`
|
|
8
|
-
)
|
|
10
|
+
require(resolveAdapterPackageName(type))
|
|
9
11
|
).default as Adapter
|
package/src/adapter/type.ts
CHANGED
|
@@ -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[]
|
|
@@ -86,10 +87,6 @@ export interface AdapterQueryOptions {
|
|
|
86
87
|
onEvent: (event: AdapterOutputEvent) => void
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
export interface AdapterInitOptions {
|
|
90
|
-
force?: boolean
|
|
91
|
-
}
|
|
92
|
-
|
|
93
90
|
export interface AdapterSession {
|
|
94
91
|
kill: () => void
|
|
95
92
|
emit: (event: AdapterEvent) => void
|
|
@@ -98,8 +95,7 @@ export interface AdapterSession {
|
|
|
98
95
|
|
|
99
96
|
export interface Adapter {
|
|
100
97
|
init?: (
|
|
101
|
-
ctx: AdapterCtx
|
|
102
|
-
options: AdapterInitOptions
|
|
98
|
+
ctx: AdapterCtx
|
|
103
99
|
) => Promise<void>
|
|
104
100
|
query: (
|
|
105
101
|
ctx: AdapterCtx,
|
package/src/channel.ts
CHANGED
|
@@ -40,6 +40,13 @@ export const channelBaseSchema = z.object({
|
|
|
40
40
|
systemPrompt: z
|
|
41
41
|
.string().optional()
|
|
42
42
|
.describe('在此频道启动会话时注入的系统提示词'),
|
|
43
|
+
// 指令
|
|
44
|
+
commandPrefix: z
|
|
45
|
+
.string().optional()
|
|
46
|
+
.describe('频道指令前缀,默认 /'),
|
|
47
|
+
language: z
|
|
48
|
+
.enum(['zh', 'en']).optional()
|
|
49
|
+
.describe('频道提示语言,默认 zh'),
|
|
43
50
|
// 访问权限控制
|
|
44
51
|
access: channelAccessSchema
|
|
45
52
|
.optional()
|
|
@@ -54,8 +61,24 @@ export type ChannelConfig = {
|
|
|
54
61
|
[K in ChannelType]: ChannelConfigByType<K>
|
|
55
62
|
}[ChannelType]
|
|
56
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
|
+
|
|
57
76
|
export interface ChannelConnection<TMessage> {
|
|
58
|
-
sendMessage: (message: TMessage) => Promise<
|
|
77
|
+
sendMessage: (message: TMessage) => Promise<ChannelSendResult | undefined>
|
|
78
|
+
pushFollowUps?: (input: {
|
|
79
|
+
messageId: string
|
|
80
|
+
followUps: readonly ChannelFollowUp[]
|
|
81
|
+
}) => Promise<void>
|
|
59
82
|
startReceiving?: (options: {
|
|
60
83
|
handlers: ChannelEventHandlers
|
|
61
84
|
}) => Promise<void>
|
|
@@ -6,6 +6,8 @@ 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'
|
|
8
8
|
|
|
9
|
+
import { resolveServerLogLevel } from '#~/env.js'
|
|
10
|
+
|
|
9
11
|
import type { RunTaskOptions } from './type'
|
|
10
12
|
|
|
11
13
|
export const prepare = async (
|
|
@@ -36,7 +38,13 @@ export const prepare = async (
|
|
|
36
38
|
// 移除 NODE_OPTIONS 环境变量,防止干扰子进程的运行环境
|
|
37
39
|
NODE_OPTIONS: undefined
|
|
38
40
|
}
|
|
39
|
-
const logger = createLogger(
|
|
41
|
+
const logger = createLogger(
|
|
42
|
+
cwd,
|
|
43
|
+
ctxId,
|
|
44
|
+
sessionId,
|
|
45
|
+
env?.LOG_PREFIX ?? '',
|
|
46
|
+
resolveServerLogLevel(env)
|
|
47
|
+
)
|
|
40
48
|
|
|
41
49
|
const jsonVariables: Record<string, string | null | undefined> = {
|
|
42
50
|
...env,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { AdapterCtx, AdapterOutputEvent, AdapterQueryOptions } from '#~/adapter/index.js'
|
|
2
2
|
import { loadAdapter } from '#~/adapter/index.js'
|
|
3
3
|
import type { ModelServiceConfig } from '#~/config.js'
|
|
4
|
+
import { callHook } from '#~/hooks/call.js'
|
|
4
5
|
import type { TaskDetail } from '#~/types.js'
|
|
5
|
-
import { callHook } from '#~/utils/api.js'
|
|
6
6
|
|
|
7
7
|
import { prepare } from './prepare'
|
|
8
8
|
import type { RunTaskOptions } from './type'
|
|
@@ -129,6 +129,17 @@ export const run = async (
|
|
|
129
129
|
|
|
130
130
|
const originalOnEvent = adapterOptions.onEvent
|
|
131
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
|
+
|
|
132
143
|
if (event.type === 'exit') {
|
|
133
144
|
const { data } = event
|
|
134
145
|
|
|
@@ -151,6 +162,8 @@ export const run = async (
|
|
|
151
162
|
}
|
|
152
163
|
|
|
153
164
|
const adapter = await loadAdapter(adapterType)
|
|
165
|
+
await adapter.init?.(ctx)
|
|
166
|
+
|
|
154
167
|
const resolvedModel = resolveQueryModel({
|
|
155
168
|
config,
|
|
156
169
|
userConfig,
|
package/src/env.ts
CHANGED
|
@@ -1,18 +1,45 @@
|
|
|
1
1
|
import { env as processEnv } from 'node:process'
|
|
2
2
|
|
|
3
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
|
4
|
+
|
|
3
5
|
export interface ServerEnv {
|
|
4
6
|
__VF_PROJECT_AI_SERVER_HOST__: string
|
|
5
7
|
__VF_PROJECT_AI_SERVER_PORT__: number
|
|
6
8
|
__VF_PROJECT_AI_SERVER_WS_PATH__: string
|
|
7
9
|
__VF_PROJECT_AI_SERVER_DATA_DIR__: string
|
|
8
10
|
__VF_PROJECT_AI_SERVER_LOG_DIR__: string
|
|
9
|
-
__VF_PROJECT_AI_SERVER_LOG_LEVEL__:
|
|
11
|
+
__VF_PROJECT_AI_SERVER_LOG_LEVEL__: LogLevel
|
|
12
|
+
__VF_PROJECT_AI_SERVER_DEBUG__: boolean
|
|
10
13
|
__VF_PROJECT_AI_SERVER_ALLOW_CORS__: boolean
|
|
11
14
|
__VF_PROJECT_AI_CLIENT_MODE__?: 'dev' | 'static'
|
|
12
15
|
__VF_PROJECT_AI_CLIENT_BASE__?: string
|
|
13
16
|
__VF_PROJECT_AI_CLIENT_DIST_PATH__?: string
|
|
14
17
|
}
|
|
15
18
|
|
|
19
|
+
const LOG_LEVELS = ['debug', 'info', 'warn', 'error'] as const satisfies readonly LogLevel[]
|
|
20
|
+
|
|
21
|
+
export function normalizeLogLevel(value: unknown): LogLevel | undefined {
|
|
22
|
+
if (typeof value !== 'string') return undefined
|
|
23
|
+
const normalized = value.trim().toLowerCase()
|
|
24
|
+
return LOG_LEVELS.includes(normalized as LogLevel)
|
|
25
|
+
? normalized as LogLevel
|
|
26
|
+
: undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveServerLogLevel(
|
|
30
|
+
env: {
|
|
31
|
+
__VF_PROJECT_AI_SERVER_LOG_LEVEL__?: unknown
|
|
32
|
+
__VF_PROJECT_AI_SERVER_DEBUG__?: unknown
|
|
33
|
+
},
|
|
34
|
+
fallback: LogLevel = 'info'
|
|
35
|
+
): LogLevel {
|
|
36
|
+
if (env.__VF_PROJECT_AI_SERVER_DEBUG__ === true || env.__VF_PROJECT_AI_SERVER_DEBUG__ === 'true') {
|
|
37
|
+
return 'debug'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return normalizeLogLevel(env.__VF_PROJECT_AI_SERVER_LOG_LEVEL__) ?? fallback
|
|
41
|
+
}
|
|
42
|
+
|
|
16
43
|
export function loadEnv(): ServerEnv {
|
|
17
44
|
const {
|
|
18
45
|
__VF_PROJECT_AI_SERVER_HOST__ = 'localhost',
|
|
@@ -21,6 +48,7 @@ export function loadEnv(): ServerEnv {
|
|
|
21
48
|
__VF_PROJECT_AI_SERVER_DATA_DIR__ = '.data',
|
|
22
49
|
__VF_PROJECT_AI_SERVER_LOG_DIR__ = '.logs',
|
|
23
50
|
__VF_PROJECT_AI_SERVER_LOG_LEVEL__ = 'info',
|
|
51
|
+
__VF_PROJECT_AI_SERVER_DEBUG__,
|
|
24
52
|
__VF_PROJECT_AI_SERVER_ALLOW_CORS__,
|
|
25
53
|
__VF_PROJECT_AI_CLIENT_MODE__ = 'static',
|
|
26
54
|
__VF_PROJECT_AI_CLIENT_BASE__,
|
|
@@ -32,8 +60,8 @@ export function loadEnv(): ServerEnv {
|
|
|
32
60
|
__VF_PROJECT_AI_SERVER_WS_PATH__,
|
|
33
61
|
__VF_PROJECT_AI_SERVER_DATA_DIR__,
|
|
34
62
|
__VF_PROJECT_AI_SERVER_LOG_DIR__,
|
|
35
|
-
__VF_PROJECT_AI_SERVER_LOG_LEVEL__:
|
|
36
|
-
|
|
63
|
+
__VF_PROJECT_AI_SERVER_LOG_LEVEL__: normalizeLogLevel(__VF_PROJECT_AI_SERVER_LOG_LEVEL__) ?? 'info',
|
|
64
|
+
__VF_PROJECT_AI_SERVER_DEBUG__: __VF_PROJECT_AI_SERVER_DEBUG__ === 'true',
|
|
37
65
|
__VF_PROJECT_AI_SERVER_ALLOW_CORS__: __VF_PROJECT_AI_SERVER_ALLOW_CORS__ != null
|
|
38
66
|
? __VF_PROJECT_AI_SERVER_ALLOW_CORS__ === 'true'
|
|
39
67
|
: true,
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import process from 'node:process'
|
|
5
|
+
|
|
6
|
+
import type { HookInputs, HookOutputs } from './type'
|
|
7
|
+
|
|
8
|
+
export type HookEventName = keyof HookInputs
|
|
9
|
+
|
|
10
|
+
type HookInputPayload<K extends HookEventName> = Omit<HookInputs[K], 'hookEventName'>
|
|
11
|
+
|
|
12
|
+
const pickHookEnv = (env: Record<string, unknown>): Record<string, string> => {
|
|
13
|
+
const result: Record<string, string> = {}
|
|
14
|
+
for (const [key, value] of Object.entries(env)) {
|
|
15
|
+
if (typeof value === 'string') {
|
|
16
|
+
result[key] = value
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return result
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const resolveHookCliJs = () => {
|
|
23
|
+
try {
|
|
24
|
+
const pkgJsonPath = require.resolve('@vibe-forge/cli/package.json')
|
|
25
|
+
return path.resolve(path.dirname(pkgJsonPath), 'call-hook.js')
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw new Error('Failed to resolve @vibe-forge/cli hook entry', { cause: error })
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const callHook = async <K extends HookEventName>(
|
|
32
|
+
hookEventName: K,
|
|
33
|
+
input: HookInputPayload<K>,
|
|
34
|
+
env: Record<string, unknown> = process.env
|
|
35
|
+
): Promise<HookOutputs[K]> => {
|
|
36
|
+
const childEnv = pickHookEnv(env)
|
|
37
|
+
const child = spawn(process.execPath, [resolveHookCliJs()], {
|
|
38
|
+
cwd: typeof input.cwd === 'string' ? input.cwd : process.cwd(),
|
|
39
|
+
env: childEnv,
|
|
40
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const stdoutChunks: Buffer[] = []
|
|
44
|
+
const stderrChunks: Buffer[] = []
|
|
45
|
+
|
|
46
|
+
child.stdout.on('data', chunk => stdoutChunks.push(chunk))
|
|
47
|
+
child.stderr.on('data', chunk => stderrChunks.push(chunk))
|
|
48
|
+
|
|
49
|
+
const exitCode = await new Promise<number>((resolve, reject) => {
|
|
50
|
+
child.once('error', reject)
|
|
51
|
+
child.once('close', code => resolve(code ?? 0))
|
|
52
|
+
child.stdin.end(JSON.stringify({
|
|
53
|
+
...input,
|
|
54
|
+
hookEventName
|
|
55
|
+
}))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const stdout = Buffer.concat(stdoutChunks).toString('utf-8').trim()
|
|
59
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf-8').trim()
|
|
60
|
+
|
|
61
|
+
if (exitCode !== 0) {
|
|
62
|
+
throw new Error(`Failed to call hook: process exited with code ${exitCode}${stderr ? ` - ${stderr}` : ''}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (stdout === '') {
|
|
66
|
+
return { continue: true } as HookOutputs[K]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(stdout) as HookOutputs[K]
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw new Error(`Failed to parse hook output: ${stdout}${stderr ? `\nstderr: ${stderr}` : ''}`, { cause: error })
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
|
|
4
|
+
import { loadConfig, resetConfigCache } from '#~/config/load.js'
|
|
5
|
+
import { resolveServerLogLevel } from '#~/env.js'
|
|
6
|
+
import { createLogger } from '#~/utils/create-logger.js'
|
|
7
|
+
import { transformCamelKey } from '#~/utils/string-transform.js'
|
|
8
|
+
|
|
9
|
+
import type { HookInput, HookInputs, HookOutputCore, HookOutputs } from './type'
|
|
10
|
+
import type { HookContext, Plugin } from './index'
|
|
11
|
+
import { resolvePlugins } from './loader'
|
|
12
|
+
|
|
13
|
+
export const callPluginHook = async <K extends keyof HookInputs>(
|
|
14
|
+
eventName: K,
|
|
15
|
+
context: HookContext,
|
|
16
|
+
input: HookInputs[K],
|
|
17
|
+
plugins: Partial<Plugin>[] = []
|
|
18
|
+
): Promise<HookOutputs[K]> => {
|
|
19
|
+
const { logger } = context
|
|
20
|
+
const filteredPlugins = plugins.filter(
|
|
21
|
+
(
|
|
22
|
+
item
|
|
23
|
+
): item is
|
|
24
|
+
& {
|
|
25
|
+
name?: string
|
|
26
|
+
}
|
|
27
|
+
& {
|
|
28
|
+
[P in K]: NonNullable<Plugin[P]>
|
|
29
|
+
} => !!item && !!item[eventName]
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
let index = 0
|
|
33
|
+
|
|
34
|
+
const next = async (): Promise<HookOutputs[K]> => {
|
|
35
|
+
if (index >= filteredPlugins.length) {
|
|
36
|
+
return { continue: true }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const currentPlugin = filteredPlugins[index]
|
|
40
|
+
const { name = '<anonymous>', [eventName]: hook } = currentPlugin
|
|
41
|
+
index++
|
|
42
|
+
|
|
43
|
+
const withNameLogger = {
|
|
44
|
+
...logger,
|
|
45
|
+
info: logger.info.bind(logger, `[plugin.${name}]`),
|
|
46
|
+
warn: logger.warn.bind(logger, `[plugin.${name}]`),
|
|
47
|
+
debug: logger.debug.bind(logger, `[plugin.${name}]`),
|
|
48
|
+
error: logger.error.bind(logger, `[plugin.${name}]`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
return await hook(
|
|
53
|
+
{
|
|
54
|
+
...context,
|
|
55
|
+
logger: withNameLogger
|
|
56
|
+
},
|
|
57
|
+
input,
|
|
58
|
+
next
|
|
59
|
+
)
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error instanceof Error && !error.name.includes('[plugin.')) {
|
|
62
|
+
error.name = `${error.name}[plugin.${name}]`
|
|
63
|
+
}
|
|
64
|
+
throw error
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return next()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const executeHookInput = async (
|
|
72
|
+
input: HookInput,
|
|
73
|
+
env: Record<string, string | null | undefined> = process.env
|
|
74
|
+
) => {
|
|
75
|
+
const workspaceFolder = env.__VF_PROJECT_WORKSPACE_FOLDER__ ?? input.cwd ?? process.env.HOME ?? '/'
|
|
76
|
+
const ctxId = env.__VF_PROJECT_AI_CTX_ID__ ?? input.sessionId ?? 'default'
|
|
77
|
+
const logPrefix = env.__VF_PROJECT_AI_LOG_PREFIX__ ?? ''
|
|
78
|
+
const loggerBase = createLogger(
|
|
79
|
+
workspaceFolder,
|
|
80
|
+
ctxId,
|
|
81
|
+
input.sessionId,
|
|
82
|
+
logPrefix,
|
|
83
|
+
resolveServerLogLevel(env)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const logger: typeof loggerBase = {
|
|
87
|
+
...loggerBase,
|
|
88
|
+
info: (...args) => loggerBase.info(`[${input.hookEventName}]`, ...args),
|
|
89
|
+
warn: (...args) => loggerBase.warn(`[${input.hookEventName}]`, ...args),
|
|
90
|
+
debug: (...args) => loggerBase.debug(`[${input.hookEventName}]`, ...args),
|
|
91
|
+
error: (...args) => loggerBase.error(`[${input.hookEventName}]`, ...args)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
resetConfigCache()
|
|
95
|
+
const jsonVariables = {
|
|
96
|
+
...env,
|
|
97
|
+
WORKSPACE_FOLDER: workspaceFolder,
|
|
98
|
+
__VF_PROJECT_WORKSPACE_FOLDER__: workspaceFolder
|
|
99
|
+
}
|
|
100
|
+
const [config, userConfig] = await loadConfig({ jsonVariables })
|
|
101
|
+
const plugins = [
|
|
102
|
+
...await resolvePlugins(config?.plugins ?? []),
|
|
103
|
+
...await resolvePlugins(userConfig?.plugins ?? [])
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
return callPluginHook(
|
|
107
|
+
input.hookEventName,
|
|
108
|
+
{ logger },
|
|
109
|
+
input as HookInputs[typeof input.hookEventName],
|
|
110
|
+
plugins
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const readHookInput = async () => {
|
|
115
|
+
const stdoutBuffer = await new Promise<Buffer>((resolve) => {
|
|
116
|
+
const chunks: Buffer[] = []
|
|
117
|
+
process.stdin.on('data', chunk => chunks.push(chunk))
|
|
118
|
+
process.stdin.once('end', () => resolve(Buffer.concat(chunks)))
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
return transformCamelKey<HookInput>(
|
|
122
|
+
JSON.parse(stdoutBuffer.toString() || '{}')
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const runHookCli = async () => {
|
|
127
|
+
try {
|
|
128
|
+
const input = await readHookInput()
|
|
129
|
+
const result = await executeHookInput(input)
|
|
130
|
+
console.log(JSON.stringify(result))
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.log(
|
|
133
|
+
JSON.stringify({
|
|
134
|
+
continue: false,
|
|
135
|
+
stopReason: `run hook error: ${String(error)}`
|
|
136
|
+
} satisfies HookOutputCore)
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
}
|
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 =
|
package/src/utils/cache.ts
CHANGED
|
@@ -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
|
-
|
|
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,8 @@
|
|
|
1
1
|
import { createWriteStream, existsSync, mkdirSync } from 'node:fs'
|
|
2
2
|
import { dirname, resolve } from 'node:path'
|
|
3
3
|
|
|
4
|
+
import type { LogLevel } from '#~/env.js'
|
|
5
|
+
|
|
4
6
|
export interface Logger {
|
|
5
7
|
stream: NodeJS.WritableStream
|
|
6
8
|
info: (...args: unknown[]) => void
|
|
@@ -9,19 +11,29 @@ export interface Logger {
|
|
|
9
11
|
error: (...args: unknown[]) => void
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
|
|
15
|
+
debug: 10,
|
|
16
|
+
info: 20,
|
|
17
|
+
warn: 30,
|
|
18
|
+
error: 40
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
export const createLogger = (
|
|
13
22
|
cwd: string,
|
|
14
23
|
taskId: string,
|
|
15
24
|
sessionId: string,
|
|
16
|
-
logPrefix = ''
|
|
25
|
+
logPrefix = '',
|
|
26
|
+
level: LogLevel = 'info'
|
|
17
27
|
) => {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
28
|
+
const normalizedSessionId = sessionId ?? 'default'
|
|
29
|
+
const taskDir = resolve(
|
|
30
|
+
cwd,
|
|
31
|
+
`.ai${logPrefix}/logs/${taskId}`
|
|
32
|
+
)
|
|
21
33
|
|
|
22
34
|
const loggerFilePath = resolve(
|
|
23
|
-
|
|
24
|
-
|
|
35
|
+
taskDir,
|
|
36
|
+
`${normalizedSessionId}.log.md`
|
|
25
37
|
)
|
|
26
38
|
// 默认日志文件不存在时,创建一个默认的日志文件
|
|
27
39
|
if (!existsSync(loggerFilePath)) {
|
|
@@ -30,7 +42,10 @@ export const createLogger = (
|
|
|
30
42
|
const loggerStream = createWriteStream(loggerFilePath, {
|
|
31
43
|
flags: 'a'
|
|
32
44
|
})
|
|
33
|
-
const createLog = (tag: string) => (...args: unknown[]) => {
|
|
45
|
+
const createLog = (tag: string, currentLevel: LogLevel) => (...args: unknown[]) => {
|
|
46
|
+
if (LOG_LEVEL_PRIORITY[currentLevel] < LOG_LEVEL_PRIORITY[level]) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
34
49
|
const msg = args
|
|
35
50
|
.map((arg) => {
|
|
36
51
|
if (typeof arg === 'string') {
|
|
@@ -66,9 +81,9 @@ export const createLogger = (
|
|
|
66
81
|
}
|
|
67
82
|
return {
|
|
68
83
|
stream: loggerStream,
|
|
69
|
-
info: createLog('I'),
|
|
70
|
-
warn: createLog('W'),
|
|
71
|
-
debug: createLog('D'),
|
|
72
|
-
error: createLog('E')
|
|
84
|
+
info: createLog('I', 'info'),
|
|
85
|
+
warn: createLog('W', 'warn'),
|
|
86
|
+
debug: createLog('D', 'debug'),
|
|
87
|
+
error: createLog('E', 'error')
|
|
73
88
|
}
|
|
74
89
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { readFile } from 'node:fs/promises'
|
|
3
|
-
import { basename, dirname,
|
|
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
|
|
117
|
-
const desc = attributes.description
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
`-
|
|
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
|
-
({
|
|
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
|
|
226
|
-
const desc = attributes.description
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
` - 标识:${
|
|
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/
|
|
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
|
-
.
|
|
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' +
|
package/src/utils/api.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import process from 'node:process'
|
|
2
|
-
|
|
3
|
-
import type { HookInputs, HookOutputs } from '#~/hooks/type.js'
|
|
4
|
-
|
|
5
|
-
export type HookEventName = keyof HookInputs
|
|
6
|
-
|
|
7
|
-
type HookInputPayload<K extends HookEventName> = Omit<HookInputs[K], 'hookEventName'>
|
|
8
|
-
|
|
9
|
-
const host = process.env.__VF_PROJECT_AI_SERVER_HOST__ ?? 'localhost'
|
|
10
|
-
const port = process.env.__VF_PROJECT_AI_SERVER_PORT__ ?? '8787'
|
|
11
|
-
const baseUrl = `http://${host}:${port}`
|
|
12
|
-
|
|
13
|
-
export const callHook = async <K extends HookEventName>(
|
|
14
|
-
hookEventName: K,
|
|
15
|
-
input: HookInputPayload<K>,
|
|
16
|
-
env: Record<string, unknown> = process.env
|
|
17
|
-
): Promise<HookOutputs[K]> => {
|
|
18
|
-
const response = await fetch(`${baseUrl}/api/hooks/call`, {
|
|
19
|
-
method: 'POST',
|
|
20
|
-
headers: { 'Content-Type': 'application/json' },
|
|
21
|
-
body: JSON.stringify({
|
|
22
|
-
hookEventName,
|
|
23
|
-
input,
|
|
24
|
-
env
|
|
25
|
-
})
|
|
26
|
-
})
|
|
27
|
-
if (!response.ok) {
|
|
28
|
-
const errorText = await response.text()
|
|
29
|
-
throw new Error(`Failed to call hook: ${response.statusText} - ${errorText}`)
|
|
30
|
-
}
|
|
31
|
-
return response.json()
|
|
32
|
-
}
|