feat: improve skill import and runtime delivery
This commit is contained in:
@@ -225,6 +225,45 @@ describe('ContinueAgentRuntime', () => {
|
||||
expect(prompt).toContain('test')
|
||||
})
|
||||
|
||||
it('keeps a full bundled Skill payload on every platform', async () => {
|
||||
const runtime = new ContinueAgentRuntime({
|
||||
binaryPath: '',
|
||||
configPath: 'C:\\safe config\\continue.yaml',
|
||||
defaultWorkspace: process.cwd(),
|
||||
hostCacheRoot: 'C:\\safe\\continue-host',
|
||||
skillInstructions: `# Skills\n${'技'.repeat(30_000)}`.slice(0, 30_000),
|
||||
createHostAdapter: () => ({
|
||||
getPreparedHost: mocks.prepareHost,
|
||||
run: mocks.runHost,
|
||||
dispose: mocks.disposeHost
|
||||
})
|
||||
})
|
||||
|
||||
await collectEvents(runtime)
|
||||
|
||||
const prompt = String(mocks.runHost.mock.calls[0]?.[0])
|
||||
expect(prompt).toContain('SYSTEM CAPABILITY INSTRUCTIONS')
|
||||
expect(prompt.length).toBeGreaterThan(24_000)
|
||||
})
|
||||
|
||||
it('reports oversized Skill payloads instead of dropping them silently', async () => {
|
||||
const runtime = new ContinueAgentRuntime({
|
||||
binaryPath: '',
|
||||
configPath: 'C:\\safe config\\continue.yaml',
|
||||
defaultWorkspace: process.cwd(),
|
||||
hostCacheRoot: 'C:\\safe\\continue-host',
|
||||
skillInstructions: '巨'.repeat(130_000),
|
||||
createHostAdapter: () => ({
|
||||
getPreparedHost: mocks.prepareHost,
|
||||
run: mocks.runHost,
|
||||
dispose: mocks.disposeHost
|
||||
})
|
||||
})
|
||||
|
||||
await expect(collectEvents(runtime)).rejects.toThrow('超过 Continue')
|
||||
expect(mocks.runHost).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('blocks anonymous platform fallback without an explicit model configuration', async () => {
|
||||
const runtime = new ContinueAgentRuntime({
|
||||
binaryPath: '',
|
||||
|
||||
@@ -43,8 +43,9 @@ export type ContinueRuntimeOptions = {
|
||||
>
|
||||
}
|
||||
|
||||
const MAX_CONTINUE_PROMPT_CHARACTERS =
|
||||
process.platform === 'win32' ? 24_000 : 128_000
|
||||
// The prompt reaches the Continue host through a local HTTP POST body, so no
|
||||
// platform command-line limit applies to it.
|
||||
const MAX_CONTINUE_PROMPT_CHARACTERS = 128_000
|
||||
|
||||
function continueToolFailureMessage(tool: ContinueHostTool): string {
|
||||
const callId = tool.callId.slice(0, 128)
|
||||
@@ -260,12 +261,19 @@ export class ContinueAgentRuntime implements AgentRuntime {
|
||||
'CURRENT CONVERSATION:'
|
||||
].join('\n')
|
||||
: ''
|
||||
const conversationContext =
|
||||
if (
|
||||
skillPrefix &&
|
||||
skillPrefix.length + prompt.length <=
|
||||
MAX_CONTINUE_PROMPT_CHARACTERS
|
||||
? `${skillPrefix}\n${prompt}`
|
||||
: prompt
|
||||
skillPrefix.length + prompt.length > MAX_CONTINUE_PROMPT_CHARACTERS
|
||||
) {
|
||||
throw new Error(
|
||||
`已启用的 Skill 说明与当前请求合计 ${(
|
||||
skillPrefix.length + prompt.length
|
||||
).toLocaleString()} 字符,超过 Continue ${MAX_CONTINUE_PROMPT_CHARACTERS.toLocaleString()} 字符上限。请在设置中减少分配给 Continue 的 Skill。`
|
||||
)
|
||||
}
|
||||
const conversationContext = skillPrefix
|
||||
? `${skillPrefix}\n${prompt}`
|
||||
: prompt
|
||||
const detection = await this.getDetection()
|
||||
signal.throwIfAborted()
|
||||
if (!detection.available || !detection.path) {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
CapabilityService,
|
||||
type CapabilityCipher
|
||||
} from './capability-service'
|
||||
import {
|
||||
BrowserProfileService,
|
||||
MemoryBrowserProfileStore
|
||||
} from './browser-profile-service'
|
||||
|
||||
const builtinSkillsRoot = join(
|
||||
process.cwd(),
|
||||
'resources',
|
||||
'skills'
|
||||
)
|
||||
|
||||
const cipher: CapabilityCipher = {
|
||||
isAvailable: () => true,
|
||||
encrypt: (value) => Buffer.from(`encrypted:${value}`),
|
||||
decrypt: (value) => value.toString().replace(/^encrypted:/u, '')
|
||||
}
|
||||
|
||||
const temporaryDirectories: string[] = []
|
||||
|
||||
async function createService(): Promise<CapabilityService> {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'goodbuddy-builtin-'))
|
||||
temporaryDirectories.push(directory)
|
||||
return new CapabilityService(
|
||||
join(directory, 'capabilities.json'),
|
||||
builtinSkillsRoot,
|
||||
join(directory, 'imported'),
|
||||
cipher,
|
||||
{
|
||||
platform: 'win32',
|
||||
architecture: 'x64',
|
||||
electronTarget: true,
|
||||
browserProfiles: new BrowserProfileService(
|
||||
new MemoryBrowserProfileStore()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
temporaryDirectories
|
||||
.splice(0)
|
||||
.map((directory) => rm(directory, { recursive: true, force: true }))
|
||||
)
|
||||
})
|
||||
|
||||
describe('bundled skills', () => {
|
||||
it('parses every bundled SKILL.md', async () => {
|
||||
const snapshot = await (await createService()).getSnapshot()
|
||||
|
||||
expect(snapshot.skills.length).toBeGreaterThan(0)
|
||||
expect(snapshot.skills.every((skill) => skill.source === 'builtin')).toBe(
|
||||
true
|
||||
)
|
||||
expect(snapshot.skills.map((skill) => skill.id)).toContain(
|
||||
'product-marketing'
|
||||
)
|
||||
})
|
||||
|
||||
it('injects every enabled bundled skill with its resolved directory', async () => {
|
||||
const service = await createService()
|
||||
const snapshot = await service.getSnapshot()
|
||||
|
||||
const instructions = await service.getSkillInstructions('continue')
|
||||
|
||||
expect(instructions).not.toContain('因超出注入上限未加载')
|
||||
for (const skill of snapshot.skills) {
|
||||
expect(instructions).toContain(join(builtinSkillsRoot, skill.id))
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -266,6 +266,77 @@ describe('CapabilityService', () => {
|
||||
).rejects.toThrow('只能删除已导入')
|
||||
})
|
||||
|
||||
it('imports a standard SKILL.md that identifies itself by name', async () => {
|
||||
const { directory, service } = await createService()
|
||||
const source = join(directory, 'standard-source', 'summarize-diff')
|
||||
await mkdir(source, { recursive: true })
|
||||
await writeFile(
|
||||
join(source, 'SKILL.md'),
|
||||
[
|
||||
'---',
|
||||
'name: summarize-diff',
|
||||
'description: |',
|
||||
' 概括暂存的改动。',
|
||||
' 当用户需要待提交变更摘要时使用。',
|
||||
'allowed-tools:',
|
||||
' - Read',
|
||||
' - Grep',
|
||||
'compatibility: droid',
|
||||
'---',
|
||||
'',
|
||||
'# Summarize Diff',
|
||||
'',
|
||||
'仅用于离线测试。'
|
||||
].join('\n'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
const imported = await service.importSkill(source)
|
||||
|
||||
expect(imported.skills).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: 'summarize-diff',
|
||||
source: 'imported',
|
||||
description: '概括暂存的改动。 当用户需要待提交变更摘要时使用。'
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('imports every Skill found under a suite directory', async () => {
|
||||
const { directory, service } = await createService()
|
||||
const suite = join(directory, 'suite', 'skills')
|
||||
await writeSkill(suite, 'alpha-skill', 'Alpha')
|
||||
await writeSkill(suite, 'beta-skill', 'Beta')
|
||||
|
||||
const imported = await service.importSkill(join(directory, 'suite'))
|
||||
|
||||
expect(imported.skills.map((skill) => skill.id)).toEqual(
|
||||
expect.arrayContaining(['alpha-skill', 'beta-skill'])
|
||||
)
|
||||
})
|
||||
|
||||
it('reports a readable error when the selected directory has no SKILL.md', async () => {
|
||||
const { directory, service } = await createService()
|
||||
const empty = join(directory, 'empty-directory')
|
||||
await mkdir(empty, { recursive: true })
|
||||
|
||||
await expect(service.importSkill(empty)).rejects.toThrow(
|
||||
'没有找到 SKILL.md'
|
||||
)
|
||||
})
|
||||
|
||||
it('exposes the skill directory and names skills dropped by the budget', async () => {
|
||||
const { builtinRoot, service } = await createService()
|
||||
await writeSkill(builtinRoot, 'oversized-skill', '超长技能')
|
||||
|
||||
const instructions = await service.getSkillInstructions('model')
|
||||
expect(instructions).toContain(join(builtinRoot, 'document-writing'))
|
||||
expect(instructions).toContain(join(builtinRoot, 'oversized-skill'))
|
||||
|
||||
const truncated = await service.getSkillInstructions('model', 200)
|
||||
expect(truncated).toContain('因超出注入上限未加载')
|
||||
})
|
||||
|
||||
it('imports a managed Skill from a ZIP package', async () => {
|
||||
const { directory, importedRoot, service } = await createService()
|
||||
const packageRoot = join(directory, 'zip-source')
|
||||
|
||||
@@ -56,16 +56,31 @@ const MAX_SKILL_FILE_BYTES = 2 * 1024 * 1024
|
||||
const MAX_SKILL_PACKAGE_BYTES = 10 * 1024 * 1024
|
||||
const MAX_SKILL_PACKAGE_FILES = 128
|
||||
const MAX_SKILL_DEPTH = 6
|
||||
const MAX_SKILL_DISCOVERY_DEPTH = 4
|
||||
const MAX_SKILL_DISCOVERY_RESULTS = 64
|
||||
const MAX_SKILL_INSTRUCTION_CHARACTERS = 262_144
|
||||
const SKILL_DISCOVERY_IGNORED_DIRECTORIES = new Set([
|
||||
'node_modules',
|
||||
'__pycache__',
|
||||
'__MACOSX'
|
||||
])
|
||||
|
||||
const skillMetadataSchema = z
|
||||
.object({
|
||||
id: skillIdSchema,
|
||||
name: z.string().trim().min(1).max(80),
|
||||
description: z.string().trim().min(1).max(500),
|
||||
version: z.string().trim().min(1).max(32).optional(),
|
||||
tags: z.array(z.string().trim().min(1).max(32)).max(12).default([])
|
||||
})
|
||||
.strict()
|
||||
// Block scalars in SKILL.md frontmatter carry newlines that would break the
|
||||
// single-line summary surfaces the renderer and runtimes rely on.
|
||||
function collapsedText(maximum: number): z.ZodType<string> {
|
||||
return z
|
||||
.string()
|
||||
.transform((value) => value.replace(/\s+/gu, ' ').trim())
|
||||
.pipe(z.string().min(1).max(maximum))
|
||||
}
|
||||
|
||||
const skillMetadataSchema = z.object({
|
||||
id: skillIdSchema.optional(),
|
||||
name: collapsedText(80),
|
||||
description: collapsedText(500),
|
||||
version: collapsedText(32).optional(),
|
||||
tags: z.array(collapsedText(32)).max(12).default([])
|
||||
})
|
||||
|
||||
const skillStateSchema = z
|
||||
.object({
|
||||
@@ -221,13 +236,22 @@ async function readSkill(
|
||||
throw new Error(`${basename(directoryPath)} 的 SKILL.md 格式无效`)
|
||||
}
|
||||
const metadata = skillMetadataSchema.parse(parseYaml(match[1]))
|
||||
if (expectedId !== null && metadata.id !== expectedId) {
|
||||
throw new Error(`Skill ID 必须与目录名一致:${metadata.id}`)
|
||||
// Standard SKILL.md files identify the skill by `name`; GoodBuddy packages
|
||||
// add an explicit `id` alongside a human-readable `name`.
|
||||
const identifier = skillIdSchema.safeParse(metadata.id ?? metadata.name)
|
||||
if (!identifier.success) {
|
||||
throw new Error(
|
||||
`${basename(directoryPath)} 的 SKILL.md 缺少可用的 Skill ID,请提供小写连字符格式的 id 或 name`
|
||||
)
|
||||
}
|
||||
if (expectedId !== null && identifier.data !== expectedId) {
|
||||
throw new Error(`Skill ID 必须与目录名一致:${identifier.data}`)
|
||||
}
|
||||
return skillSummarySchema
|
||||
.omit({ enabled: true, assignments: true })
|
||||
.parse({
|
||||
...metadata,
|
||||
id: identifier.data,
|
||||
source,
|
||||
digest: createHash('sha256').update(content).digest('hex')
|
||||
})
|
||||
@@ -261,6 +285,55 @@ async function listSkills(
|
||||
)
|
||||
}
|
||||
|
||||
async function pathExists(candidate: string): Promise<boolean> {
|
||||
return stat(candidate)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
}
|
||||
|
||||
// Mirrors the conventional layout where the first directory containing
|
||||
// SKILL.md is the skill root, so users can pick a suite directory that holds
|
||||
// many skills instead of one package at a time.
|
||||
async function discoverSkillDirectories(root: string): Promise<string[]> {
|
||||
if (await pathExists(join(root, 'SKILL.md'))) {
|
||||
return [root]
|
||||
}
|
||||
const found: string[] = []
|
||||
const walk = async (current: string, depth: number): Promise<void> => {
|
||||
if (depth > MAX_SKILL_DISCOVERY_DEPTH || found.length > MAX_SKILL_DISCOVERY_RESULTS) {
|
||||
return
|
||||
}
|
||||
let entries
|
||||
try {
|
||||
entries = await readdir(current, { withFileTypes: true })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (
|
||||
!entry.isDirectory() ||
|
||||
entry.name.startsWith('.') ||
|
||||
SKILL_DISCOVERY_IGNORED_DIRECTORIES.has(entry.name)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
const child = join(current, entry.name)
|
||||
if (await pathExists(join(child, 'SKILL.md'))) {
|
||||
found.push(child)
|
||||
continue
|
||||
}
|
||||
await walk(child, depth + 1)
|
||||
}
|
||||
}
|
||||
await walk(root, 0)
|
||||
if (found.length > MAX_SKILL_DISCOVERY_RESULTS) {
|
||||
throw new Error(
|
||||
`所选目录包含的 Skill 超过 ${MAX_SKILL_DISCOVERY_RESULTS} 个,请选择更精确的目录`
|
||||
)
|
||||
}
|
||||
return found.sort((left, right) => left.localeCompare(right))
|
||||
}
|
||||
|
||||
async function copySkillPackage(
|
||||
sourceRoot: string,
|
||||
targetRoot: string
|
||||
@@ -943,6 +1016,46 @@ export class CapabilityService {
|
||||
})
|
||||
}
|
||||
|
||||
private async importSkillDirectory(
|
||||
sourceDirectory: string,
|
||||
expectedId: string | null | undefined
|
||||
): Promise<string> {
|
||||
const temporaryPath = join(
|
||||
this.importedSkillsRoot,
|
||||
`.import-${randomUUID()}`
|
||||
)
|
||||
try {
|
||||
const skill = await readSkill(
|
||||
sourceDirectory,
|
||||
'imported',
|
||||
expectedId
|
||||
)
|
||||
const builtins = await listSkills(this.builtinSkillsRoot, 'builtin')
|
||||
if (builtins.some((item) => item.id === skill.id)) {
|
||||
throw new Error('导入的 Skill ID 与内置 Skill 冲突')
|
||||
}
|
||||
const targetPath = join(this.importedSkillsRoot, skill.id)
|
||||
if (await pathExists(targetPath)) {
|
||||
throw new Error('同名 Skill 已导入,请先删除后重试')
|
||||
}
|
||||
await copySkillPackage(sourceDirectory, temporaryPath)
|
||||
await readSkill(temporaryPath, 'imported', skill.id)
|
||||
await rename(temporaryPath, targetPath)
|
||||
const state = await this.load()
|
||||
await this.persist({
|
||||
...state,
|
||||
skills: {
|
||||
...state.skills,
|
||||
[skill.id]: defaultSkillState()
|
||||
}
|
||||
})
|
||||
return skill.id
|
||||
} catch (error) {
|
||||
await rm(temporaryPath, { recursive: true, force: true })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
importSkill(sourcePath: string): Promise<CapabilitySnapshot> {
|
||||
return this.queue(async () => {
|
||||
const canonicalSource = await realpath(sourcePath)
|
||||
@@ -955,52 +1068,61 @@ export class CapabilityService {
|
||||
throw new Error('所选 Skill 路径必须是目录或 .zip 文件')
|
||||
}
|
||||
await mkdir(this.importedSkillsRoot, { recursive: true })
|
||||
const temporaryPath = join(
|
||||
this.importedSkillsRoot,
|
||||
`.import-${randomUUID()}`
|
||||
)
|
||||
try {
|
||||
const archiveDirectoryName = isZip
|
||||
? await extractSkillZip(canonicalSource, temporaryPath)
|
||||
: undefined
|
||||
const skill = await readSkill(
|
||||
isDirectory ? canonicalSource : temporaryPath,
|
||||
'imported',
|
||||
isDirectory ? undefined : (archiveDirectoryName ?? null)
|
||||
|
||||
if (isZip) {
|
||||
const extractPath = join(
|
||||
this.importedSkillsRoot,
|
||||
`.extract-${randomUUID()}`
|
||||
)
|
||||
const builtins = await listSkills(
|
||||
this.builtinSkillsRoot,
|
||||
'builtin'
|
||||
)
|
||||
if (builtins.some((item) => item.id === skill.id)) {
|
||||
throw new Error('导入的 Skill ID 与内置 Skill 冲突')
|
||||
try {
|
||||
const archiveDirectoryName = await extractSkillZip(
|
||||
canonicalSource,
|
||||
extractPath
|
||||
)
|
||||
await this.importSkillDirectory(
|
||||
extractPath,
|
||||
archiveDirectoryName ?? null
|
||||
)
|
||||
} finally {
|
||||
await rm(extractPath, { recursive: true, force: true })
|
||||
}
|
||||
const targetPath = join(this.importedSkillsRoot, skill.id)
|
||||
if (
|
||||
await stat(targetPath)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
) {
|
||||
throw new Error('同名 Skill 已导入,请先删除后重试')
|
||||
}
|
||||
if (isDirectory) {
|
||||
await copySkillPackage(canonicalSource, temporaryPath)
|
||||
}
|
||||
await readSkill(temporaryPath, 'imported', skill.id)
|
||||
await rename(temporaryPath, targetPath)
|
||||
const state = await this.load()
|
||||
await this.persist({
|
||||
...state,
|
||||
skills: {
|
||||
...state.skills,
|
||||
[skill.id]: defaultSkillState()
|
||||
}
|
||||
})
|
||||
return this.getSnapshot()
|
||||
} catch (error) {
|
||||
await rm(temporaryPath, { recursive: true, force: true })
|
||||
throw error
|
||||
}
|
||||
|
||||
const directories = await discoverSkillDirectories(canonicalSource)
|
||||
if (directories.length === 0) {
|
||||
throw new Error(
|
||||
'所选目录及其子目录中没有找到 SKILL.md,请选择 Skill 目录或包含多个 Skill 的目录'
|
||||
)
|
||||
}
|
||||
const failures: string[] = []
|
||||
let importedCount = 0
|
||||
for (const directory of directories) {
|
||||
try {
|
||||
// A suite directory may nest skills below its own name, so the
|
||||
// directory name is only authoritative for a single-skill import.
|
||||
await this.importSkillDirectory(
|
||||
directory,
|
||||
directories.length === 1 ? undefined : null
|
||||
)
|
||||
importedCount += 1
|
||||
} catch (error) {
|
||||
failures.push(
|
||||
`${basename(directory)}:${
|
||||
error instanceof Error ? error.message : '导入失败'
|
||||
}`
|
||||
)
|
||||
}
|
||||
}
|
||||
if (importedCount === 0) {
|
||||
throw new Error(`Skill 导入失败。${failures.join(';')}`)
|
||||
}
|
||||
if (failures.length > 0) {
|
||||
throw new Error(
|
||||
`已导入 ${importedCount} 个 Skill,${failures.length} 个失败。${failures.join(';')}`
|
||||
)
|
||||
}
|
||||
return this.getSnapshot()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1202,10 +1324,15 @@ export class CapabilityService {
|
||||
|
||||
async getSkillInstructions(
|
||||
target: RuntimeTarget,
|
||||
maximumCharacters: number
|
||||
maximumCharacters: number = MAX_SKILL_INSTRUCTION_CHARACTERS
|
||||
): Promise<string> {
|
||||
const budget = Math.min(
|
||||
maximumCharacters,
|
||||
MAX_SKILL_INSTRUCTION_CHARACTERS
|
||||
)
|
||||
const snapshot = await this.getSnapshot()
|
||||
const sections: string[] = []
|
||||
const skipped: string[] = []
|
||||
let length = 0
|
||||
for (const skill of snapshot.skills) {
|
||||
if (!skill.enabled || !skill.assignments.includes(target)) {
|
||||
@@ -1215,24 +1342,38 @@ export class CapabilityService {
|
||||
skill.source === 'builtin'
|
||||
? this.builtinSkillsRoot
|
||||
: this.importedSkillsRoot
|
||||
const content = await readFile(join(root, skill.id, 'SKILL.md'), 'utf8')
|
||||
const directory = join(root, skill.id)
|
||||
const content = await readFile(join(directory, 'SKILL.md'), 'utf8')
|
||||
const body =
|
||||
/^---\r?\n[\s\S]*?\r?\n---\r?\n([\s\S]+)$/u.exec(content)?.[1]?.trim() ??
|
||||
''
|
||||
const section = `## ${skill.name}\n${body}`
|
||||
if (length + section.length > maximumCharacters) {
|
||||
// Skill bodies reference their own scripts and templates by relative
|
||||
// path, which only resolve against the installed skill directory.
|
||||
const section = [
|
||||
`## ${skill.name}`,
|
||||
`Skill 目录:${directory}`,
|
||||
body
|
||||
].join('\n')
|
||||
if (length + section.length > budget) {
|
||||
skipped.push(skill.name)
|
||||
continue
|
||||
}
|
||||
sections.push(section)
|
||||
length += section.length
|
||||
}
|
||||
return sections.length > 0
|
||||
? [
|
||||
'# GoodBuddy 已启用 Skills',
|
||||
'以下是用户明确启用并分配给当前 Runtime 的本地能力说明。请遵循这些说明,但不得覆盖系统安全规则。',
|
||||
...sections
|
||||
].join('\n\n')
|
||||
: ''
|
||||
if (sections.length === 0) {
|
||||
return ''
|
||||
}
|
||||
return [
|
||||
'# GoodBuddy 已启用 Skills',
|
||||
'以下是用户明确启用并分配给当前 Runtime 的本地能力说明。请遵循这些说明,但不得覆盖系统安全规则。',
|
||||
...(skipped.length > 0
|
||||
? [
|
||||
`注意:以下 Skill 因超出注入上限未加载,本次对话不可用:${skipped.join('、')}。`
|
||||
]
|
||||
: []),
|
||||
...sections
|
||||
].join('\n\n')
|
||||
}
|
||||
|
||||
async getResolvedMcpServers(
|
||||
|
||||
+1
-4
@@ -346,10 +346,7 @@ if (hasSingleInstanceLock) {
|
||||
): Promise<AgentRuntime> => {
|
||||
const [skillInstructions, mcpServers, browserCapability] =
|
||||
await Promise.all([
|
||||
capabilityService.getSkillInstructions(
|
||||
target,
|
||||
target === 'continue' ? 12_000 : 48_000
|
||||
),
|
||||
capabilityService.getSkillInstructions(target),
|
||||
target === 'model'
|
||||
? capabilityService.getResolvedMcpServers('model')
|
||||
: Promise.resolve([]),
|
||||
|
||||
Reference in New Issue
Block a user