IBM 계열 vLLM 연결 점검 결과 일부 CP4D 인증 엔드포인트가 username+password 대신 username+api_key JSON 본문을 요구하는 것을 확인했습니다. Cp4dTokenService가 먼저 username/password를 시도하고 실패 시 username/api_key로 자동 재시도하도록 보강했으며 README와 DEVELOPMENT 문서를 2026-04-06 16:55 (KST) 기준으로 갱신했습니다. Release 빌드 경고 0 오류 0을 확인했습니다.
This commit is contained in:
@@ -1283,3 +1283,6 @@ MIT License
|
|||||||
- AX Agent 내부 설정 공통 탭의 `운영 모드` 섹션 구분선을 아래쪽에서 위쪽으로 옮겼다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 `OverlaySectionOperationMode`를 `BorderThickness="0,1,0,0"` 기준으로 바꿔 저장 공간 섹션 아래에 선이 남지 않고, 운영 모드 시작선으로 보이도록 정리했다.
|
- AX Agent 내부 설정 공통 탭의 `운영 모드` 섹션 구분선을 아래쪽에서 위쪽으로 옮겼다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 `OverlaySectionOperationMode`를 `BorderThickness="0,1,0,0"` 기준으로 바꿔 저장 공간 섹션 아래에 선이 남지 않고, 운영 모드 시작선으로 보이도록 정리했다.
|
||||||
- 업데이트: 2026-04-06 16:49 (KST)
|
- 업데이트: 2026-04-06 16:49 (KST)
|
||||||
- AX Agent 내부 설정 공통 탭에서 `서비스와 모델`과 `등록 모델 관리` 사이에 있던 구분선을 제거했다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 `OverlaySectionService` 하단 border를 없애고 간격만 남겨, 같은 흐름의 설정이 끊기지 않고 이어 보이도록 맞췄다.
|
- AX Agent 내부 설정 공통 탭에서 `서비스와 모델`과 `등록 모델 관리` 사이에 있던 구분선을 제거했다. [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml) 의 `OverlaySectionService` 하단 border를 없애고 간격만 남겨, 같은 흐름의 설정이 끊기지 않고 이어 보이도록 맞췄다.
|
||||||
|
- 업데이트: 2026-04-06 16:55 (KST)
|
||||||
|
- IBM/CP4D 계열 연결 점검 결과, 일부 환경은 `/icp4d-api/v1/authorize` 호출 시 `username + password`가 아니라 `username + api_key` JSON 본문을 요구하는 것을 확인했다.
|
||||||
|
- [Cp4dTokenService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Cp4dTokenService.cs) 에서 CP4D 토큰 요청을 먼저 `username + password`, 실패 시 `username + api_key`로 한 번 더 시도하도록 보강해, IBM 연결형 vLLM 환경 호환성을 높였다.
|
||||||
|
|||||||
@@ -4979,3 +4979,4 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
|
|||||||
- Document update: 2026-04-06 16:39 (KST) - Replaced the plain storage-management buttons in the internal settings overlay with a new `OverlayActionBtn` style so `새로고침`, `대화 삭제`, and `저장 공간 줄이기` follow the same custom rounded action language as the rest of the AX Agent settings UI.
|
- Document update: 2026-04-06 16:39 (KST) - Replaced the plain storage-management buttons in the internal settings overlay with a new `OverlayActionBtn` style so `새로고침`, `대화 삭제`, and `저장 공간 줄이기` follow the same custom rounded action language as the rest of the AX Agent settings UI.
|
||||||
- Document update: 2026-04-06 16:44 (KST) - Moved the separator for the AX Agent internal-settings `운영 모드` block from the bottom edge to the top edge in `ChatWindow.xaml` so the line reads as the start of the operation-mode section rather than trailing under the previous storage block.
|
- Document update: 2026-04-06 16:44 (KST) - Moved the separator for the AX Agent internal-settings `운영 모드` block from the bottom edge to the top edge in `ChatWindow.xaml` so the line reads as the start of the operation-mode section rather than trailing under the previous storage block.
|
||||||
- Document update: 2026-04-06 16:49 (KST) - Removed the separator between the AX Agent internal-settings `서비스와 모델` block and the adjacent `등록 모델 관리` block in `ChatWindow.xaml` so the two model-management sections read as one continuous flow instead of two unrelated groups.
|
- Document update: 2026-04-06 16:49 (KST) - Removed the separator between the AX Agent internal-settings `서비스와 모델` block and the adjacent `등록 모델 관리` block in `ChatWindow.xaml` so the two model-management sections read as one continuous flow instead of two unrelated groups.
|
||||||
|
- Document update: 2026-04-06 16:55 (KST) - Hardened `Cp4dTokenService.cs` for IBM/CP4D-style token endpoints that expect `username + api_key` instead of `username + password`. The service now tries `username/password` first and automatically retries with `username/api_key` before failing.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Net.Http;
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
@@ -35,38 +36,53 @@ internal sealed class Cp4dTokenService
|
|||||||
|
|
||||||
var cacheKey = $"{cp4dUrl}|{username}";
|
var cacheKey = $"{cp4dUrl}|{username}";
|
||||||
|
|
||||||
// 캐시 확인 — 만료 1분 전까지 유효
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
if (_cache.TryGetValue(cacheKey, out var cached) && cached.Expiry > DateTime.UtcNow.AddMinutes(1))
|
if (_cache.TryGetValue(cacheKey, out var cached) && cached.Expiry > DateTime.UtcNow.AddMinutes(1))
|
||||||
return cached.Token;
|
return cached.Token;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 토큰 발급
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var tokenUrl = cp4dUrl.TrimEnd('/') + "/icp4d-api/v1/authorize";
|
var tokenUrl = cp4dUrl.TrimEnd('/') + "/icp4d-api/v1/authorize";
|
||||||
var body = new { username, password };
|
string? json = null;
|
||||||
|
HttpStatusCode? statusCode = null;
|
||||||
|
string? lastErrorBody = null;
|
||||||
|
|
||||||
using var req = new HttpRequestMessage(HttpMethod.Post, tokenUrl)
|
async Task<bool> TryAuthorizeAsync(object body, string bodyKind)
|
||||||
{
|
{
|
||||||
Content = JsonContent.Create(body)
|
using var req = new HttpRequestMessage(HttpMethod.Post, tokenUrl)
|
||||||
};
|
{
|
||||||
|
Content = JsonContent.Create(body)
|
||||||
|
};
|
||||||
|
|
||||||
// CP4D는 자체 서명 인증서를 사용할 수 있으므로 TLS 오류 무시 옵션 (사내 환경)
|
using var resp = await _http.SendAsync(req, ct);
|
||||||
using var resp = await _http.SendAsync(req, ct);
|
statusCode = resp.StatusCode;
|
||||||
|
var rawBody = await resp.Content.ReadAsStringAsync(ct);
|
||||||
|
if (!resp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
lastErrorBody = rawBody;
|
||||||
|
LogService.Warn($"CP4D 토큰 발급 실패({bodyKind}): {resp.StatusCode} - {rawBody}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!resp.IsSuccessStatusCode)
|
json = rawBody;
|
||||||
|
LogService.Info($"CP4D 토큰 발급 성공({bodyKind})");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var authorized = await TryAuthorizeAsync(new { username, password }, "username+password");
|
||||||
|
if (!authorized && !string.IsNullOrWhiteSpace(password))
|
||||||
|
authorized = await TryAuthorizeAsync(new { username, api_key = password }, "username+api_key");
|
||||||
|
|
||||||
|
if (!authorized || string.IsNullOrWhiteSpace(json))
|
||||||
{
|
{
|
||||||
var errBody = await resp.Content.ReadAsStringAsync(ct);
|
LogService.Warn($"CP4D 토큰 발급 최종 실패: {statusCode} - {lastErrorBody}");
|
||||||
LogService.Warn($"CP4D 토큰 발급 실패: {resp.StatusCode} - {errBody}");
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var json = await resp.Content.ReadAsStringAsync(ct);
|
|
||||||
using var doc = JsonDocument.Parse(json);
|
using var doc = JsonDocument.Parse(json);
|
||||||
|
|
||||||
// CP4D 응답 형식: {"token": "...", "_messageCode_": "...", "message": "..."}
|
|
||||||
if (!doc.RootElement.TryGetProperty("token", out var tokenProp))
|
if (!doc.RootElement.TryGetProperty("token", out var tokenProp))
|
||||||
{
|
{
|
||||||
LogService.Warn("CP4D 응답에 token 필드가 없습니다.");
|
LogService.Warn("CP4D 응답에 token 필드가 없습니다.");
|
||||||
@@ -74,12 +90,10 @@ internal sealed class Cp4dTokenService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var token = tokenProp.GetString();
|
var token = tokenProp.GetString();
|
||||||
if (string.IsNullOrEmpty(token)) return null;
|
if (string.IsNullOrEmpty(token))
|
||||||
|
return null;
|
||||||
|
|
||||||
// 토큰 만료 시간 — CP4D 기본 12시간, 안전하게 11시간으로 설정
|
|
||||||
var expiry = DateTime.UtcNow.AddHours(11);
|
var expiry = DateTime.UtcNow.AddHours(11);
|
||||||
|
|
||||||
// _messageCode_에서 만료 정보가 있으면 파싱 시도
|
|
||||||
if (doc.RootElement.TryGetProperty("accessTokenExpiry", out var expiryProp) &&
|
if (doc.RootElement.TryGetProperty("accessTokenExpiry", out var expiryProp) &&
|
||||||
expiryProp.TryGetInt64(out var expiryMs))
|
expiryProp.TryGetInt64(out var expiryMs))
|
||||||
{
|
{
|
||||||
@@ -105,12 +119,18 @@ internal sealed class Cp4dTokenService
|
|||||||
public static void InvalidateToken(string cp4dUrl, string username)
|
public static void InvalidateToken(string cp4dUrl, string username)
|
||||||
{
|
{
|
||||||
var cacheKey = $"{cp4dUrl}|{username}";
|
var cacheKey = $"{cp4dUrl}|{username}";
|
||||||
lock (_lock) { _cache.Remove(cacheKey); }
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_cache.Remove(cacheKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>모든 캐시된 토큰을 초기화합니다.</summary>
|
/// <summary>모든 캐시된 토큰을 초기화합니다.</summary>
|
||||||
public static void ClearAllTokens()
|
public static void ClearAllTokens()
|
||||||
{
|
{
|
||||||
lock (_lock) { _cache.Clear(); }
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_cache.Clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user