Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=interview_guide
POSTGRES_USER=postgres
POSTGRES_PASSWORD=password
POSTGRES_PASSWORD=123456

# Redis(compose 映射 6379)
REDIS_HOST=localhost
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,17 @@ public AgentUtilsConfiguration(
this.agentUtilsProperties = agentUtilsProperties;
}

/**
* 创建 SkillsTool,并注册为 Spring Bean。
*/
@Bean("interviewSkillsToolCallback")
public ToolCallback interviewSkillsToolCallback() {
String configuredSkillsRoot = agentUtilsProperties.getSkillsRoot();
// 规范化路径:处理 null/空白、\\ 分隔符、/SKILL.md 后缀、通配符、末尾斜杠等边界情况
String normalizedSkillsRoot = normalizeSkillsRoot(configuredSkillsRoot);
Resource skillsRootResource = resourceLoader.getResource(normalizedSkillsRoot);

// 技能目录不存在时直接拦停启动,避免 Agent 调用时静默报错难以排查
if (!skillsRootResource.exists()) {
throw new IllegalStateException("未找到 skills 根目录,请检查配置: " + normalizedSkillsRoot);
}
Expand All @@ -43,23 +48,33 @@ public ToolCallback interviewSkillsToolCallback() {
.build();
}

/**
* 规范化 skills 路径:兼容用户配置可能携带的各种后缀和格式。
* 用户可能配置 classpath:skills、classpath:skills/*、classpath:skills/SKILL.md 等变体,
* 统一归约到目录形式后再交给 ResourceLoader。
*/
private String normalizeSkillsRoot(String raw) {
// 未配置时使用默认值,确保 SkillsTool 能找到内置技能
if (raw == null || raw.isBlank()) {
return "classpath:skills";
}

String normalized = raw.trim();
normalized = normalized.replace('\\', '/');

// 用户可能直接指向某个 SKILL.md 文件而非目录
if (normalized.endsWith("/SKILL.md")) {
// 去掉 SKILL.md
normalized = normalized.substring(0, normalized.length() - "/SKILL.md".length());
}

// 去掉 classpath:skills/* 中的通配符,SkillsTool 会扫描子目录
int wildcardIndex = normalized.indexOf('*');
if (wildcardIndex >= 0) {
normalized = normalized.substring(0, wildcardIndex);
}

// 去掉末尾的 /
while (normalized.endsWith("/")) {
normalized = normalized.substring(0, normalized.length() - 1);
}
Expand Down
22 changes: 17 additions & 5 deletions app/src/main/java/interview/guide/common/ai/ApiPathResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,21 @@
import java.time.Duration;
import java.util.regex.Pattern;

/**
* OpenAI API 客户端构造器。
* 不同 LLM Provider 的 baseUrl 结构不同:
* - OpenAI 系:{@code https://api.openai.com/v1} → API 路径为 /v1/chat/completions
* - Ollama 等:{@code http://localhost:11434} → 路径就是 /chat/completions(无版本前缀)
* 此类自动识别 baseUrl 中是否已包含版本段,决定是否需要补全 API 路径。
*/
public final class ApiPathResolver {

private static final int DEFAULT_CONNECT_TIMEOUT = 10000;
private static final int DEFAULT_READ_TIMEOUT = 300000;
// 连接超时 10s:网络握手不应等待过久
private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10);
// 读取超时 5min:LLM 流式响应可能持续较长时间
private static final Duration DEFAULT_READ_TIMEOUT = Duration.ofMinutes(5);

// 匹配 baseUrl 末尾的 /v1、/v1beta 等版本段
private static final Pattern TRAILING_VERSION = Pattern.compile("/v\\d+[a-zA-Z0-9]*$");

private ApiPathResolver() {}
Expand All @@ -24,10 +34,10 @@ public static OpenAIClient buildOpenAiClient(String baseUrl, String apiKey) {
}

public static OpenAIClient buildOpenAiClient(String baseUrl, String apiKey,
int connectTimeout, int readTimeout) {
Duration connectTimeout, Duration readTimeout) {
Timeout timeout = Timeout.builder()
.connect(Duration.ofMillis(connectTimeout))
.read(Duration.ofMillis(readTimeout))
.connect(connectTimeout)
.read(readTimeout)
.build();
ClientOptions options = ClientOptions.Companion.builder()
.apiKey(apiKey)
Expand All @@ -47,6 +57,7 @@ public static String resolveVersionedBaseUrl(String baseUrl) {
return stripped + "/v1";
}

/** 判断 baseUrl 末尾是否包含 /v1 之类的版本段。 */
public static boolean baseUrlContainsVersion(String baseUrl) {
if (baseUrl == null || baseUrl.isBlank()) {
return false;
Expand All @@ -55,6 +66,7 @@ public static boolean baseUrlContainsVersion(String baseUrl) {
return TRAILING_VERSION.matcher(stripped).find();
}

/** 去除字符串末尾的斜杠,便于路径拼接和正则匹配。 */
public static String stripTrailingSlashes(String value) {
if (value == null) {
return "";
Expand Down
Loading