From 3298c35eecebbfb25a4837310bb924a59b9e6b34 Mon Sep 17 00:00:00 2001 From: jiwangyihao Date: Tue, 23 Jun 2026 02:59:19 +0800 Subject: [PATCH] =?UTF-8?q?fix(catalog):=20=E5=A4=84=E7=90=86=20Codex=20?= =?UTF-8?q?=E5=AF=B9=20Duo=20=E6=A8=A1=E5=9E=8B=E7=BC=93=E5=AD=98=E4=B8=8E?= =?UTF-8?q?=E8=BF=9C=E7=A8=8B=E7=AB=AF=E5=8F=A3=E6=AF=94=E8=BE=83=E7=9A=84?= =?UTF-8?q?=E4=B8=A4=E9=A1=B9=E5=AE=A1=E9=98=85=E6=84=8F=E8=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 动态模型缓存键改为镜像 discoverGitLabDuoWorkflowNamespace 真实解析输入: 内置发现只传 apiKey/baseUrl/fetch,原键里的 namespaceId/projectId/cwd 恒为 空,退化为 apiKey+baseUrl,导致同 token 下两个不同 group 的工作区互相复用 权威模型缓存。改为按 (凭据, baseUrl, 命名空间/项目 config 或同名环境变量, 有效 cwd) 指纹分区。 - 远程 host 比较改用 host(含端口)而非 hostname:同主机不同端口的自托管 GitLab 不再被误判为同实例,避免从错误远程推导项目路径。SCP 远程无端口概念,仍按裸 host 比较。新增跨端口远程不被当作工作区项目的回归测试。 --- .../src/discovery/gitlab-duo-workflow.ts | 8 +++- .../catalog/src/provider-models/special.ts | 19 +++++++--- .../gitlab-duo-workflow-discovery.test.ts | 38 +++++++++++++++++++ 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/packages/catalog/src/discovery/gitlab-duo-workflow.ts b/packages/catalog/src/discovery/gitlab-duo-workflow.ts index ccb3b7e7c..8c63c86bf 100644 --- a/packages/catalog/src/discovery/gitlab-duo-workflow.ts +++ b/packages/catalog/src/discovery/gitlab-duo-workflow.ts @@ -770,8 +770,11 @@ function parseGitLabRemoteProjectPath(remoteUrl: string, expectedHost: string | function parseRemoteUrl(remoteUrl: string): { host: string; projectPath: string } | null { try { const url = new URL(remoteUrl); - return { host: url.hostname, projectPath: url.pathname }; + // `host` (not `hostname`) keeps any explicit port so a self-managed GitLab on a + // non-default port is not confused with another service on the same hostname. + return { host: url.host, projectPath: url.pathname }; } catch { + // SCP-style `git@host:path` has no port concept; bare host is the only key. const scpMatch = remoteUrl.match(/^(?:[^@]+@)?([^:]+):(.+)$/); if (scpMatch?.[1] && scpMatch[2]) { return { host: scpMatch[1], projectPath: scpMatch[2] }; @@ -782,7 +785,8 @@ function parseRemoteUrl(remoteUrl: string): { host: string; projectPath: string function parseUrlHost(url: string): string | null { try { - return new URL(url).hostname; + // Match `parseRemoteUrl`: include the port so host comparison is port-aware. + return new URL(url).host; } catch { return null; } diff --git a/packages/catalog/src/provider-models/special.ts b/packages/catalog/src/provider-models/special.ts index 90d9fc853..be8329bb7 100644 --- a/packages/catalog/src/provider-models/special.ts +++ b/packages/catalog/src/provider-models/special.ts @@ -83,8 +83,12 @@ export function gitLabDuoWorkflowModelManagerOptions( // models), so the default provider-id cache namespace would let a second // account/namespace load the first one's authoritative model list at startup // and skip refetching. Partition the cache by a non-reversible fingerprint of - // the credential + base URL + namespace/project/cwd scope. Falls back to the - // bare provider id when no credential is present (static-only fallback model). + // the exact inputs `fetchGitLabDuoWorkflowModels` resolves the namespace from + // (credential + base URL + namespace/project config + the same env vars + the + // effective workspace cwd whose git remote drives auto-discovery). Built-in + // discovery only passes apiKey/baseUrl/fetch, so the cwd/env terms — not the + // empty config fields — are what actually separate workspace A from B here. + // Falls back to the bare provider id when no credential is present. ...(apiKey ? { cacheProviderId: gitLabDuoWorkflowModelCacheProviderId(apiKey, config) } : undefined), dynamicModelsAuthoritative: true, staticModels: [ @@ -107,9 +111,14 @@ export function gitLabDuoWorkflowModelManagerOptions( } function gitLabDuoWorkflowModelCacheProviderId(apiKey: string, config: GitLabDuoWorkflowModelManagerConfig): string { - const scope = [config.baseUrl ?? "", config.namespaceId ?? "", config.projectId ?? "", config.cwd ?? ""].join( - "\u0000", - ); + // Mirror the exact inputs `discoverGitLabDuoWorkflowNamespace` keys off: explicit + // namespace/project config OR the same env vars, then the git remote at the + // effective cwd. Built-in discovery leaves the config fields empty, so the env + + // resolved cwd terms are what actually distinguish two workspaces sharing a token. + const namespaceId = config.namespaceId ?? Bun.env.GITLAB_DUO_NAMESPACE_ID ?? ""; + const projectId = config.projectId ?? Bun.env.GITLAB_DUO_PROJECT_ID ?? Bun.env.GITLAB_DUO_PROJECT_PATH ?? ""; + const cwd = config.cwd ?? process.cwd(); + const scope = [config.baseUrl ?? "", namespaceId, projectId, cwd].join("\u0000"); return `gitlab-duo-agent:${Bun.hash(`${apiKey}\u0000${scope}`).toString(36)}`; } diff --git a/packages/catalog/test/gitlab-duo-workflow-discovery.test.ts b/packages/catalog/test/gitlab-duo-workflow-discovery.test.ts index d5bd3cbb5..e7be27797 100644 --- a/packages/catalog/test/gitlab-duo-workflow-discovery.test.ts +++ b/packages/catalog/test/gitlab-duo-workflow-discovery.test.ts @@ -655,4 +655,42 @@ describe("GitLab Duo Workflow discovery", () => { await fs.rm(tmpDir, { recursive: true, force: true }); } }); + + it("does not treat a same-host different-port remote as the workspace project", async () => { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-gitlab-duo-workflow-")); + try { + await fs.mkdir(path.join(tmpDir, ".git")); + // The configured GitLab is on :8443; the remote points at the same hostname + // on :9443 — a different GitLab service. It must NOT be accepted as this + // instance's project, so discovery falls through to the group candidate + // instead of querying :8443 for a project path that lives elsewhere. + await fs.writeFile( + path.join(tmpDir, ".git", "config"), + `[remote "origin"]\n\turl = https://gitlab.example.com:9443/group/project.git\n`, + ); + const { fetch, calls } = createMockFetch({ + projects: { + "group/project": { id: 7, namespace: { rootAncestor: { id: "remote-root" } } }, + }, + groups: [{ id: "group-root" }], + models: { + "remote-root": availableModels("remote_model"), + "group-root": availableModels("group_model"), + }, + }); + + const selection = await discoverGitLabDuoWorkflowNamespace({ + apiKey: TEST_TOKEN, + baseUrl: "https://gitlab.example.com:8443", + cwd: tmpDir, + fetch, + }); + + // Falls through to the group candidate, never queries the cross-port project. + expect(selection.rootNamespaceId).toBe("group-root"); + expect(calls.some(call => call.url.includes("/api/v4/projects/group%2Fproject"))).toBe(false); + } finally { + await fs.rm(tmpDir, { recursive: true, force: true }); + } + }); });