fix(catalog): 处理 Codex 对 Duo 模型缓存与远程端口比较的两项审阅意见
- 动态模型缓存键改为镜像 discoverGitLabDuoWorkflowNamespace 真实解析输入: 内置发现只传 apiKey/baseUrl/fetch,原键里的 namespaceId/projectId/cwd 恒为 空,退化为 apiKey+baseUrl,导致同 token 下两个不同 group 的工作区互相复用 权威模型缓存。改为按 (凭据, baseUrl, 命名空间/项目 config 或同名环境变量, 有效 cwd) 指纹分区。 - 远程 host 比较改用 host(含端口)而非 hostname:同主机不同端口的自托管 GitLab 不再被误判为同实例,避免从错误远程推导项目路径。SCP 远程无端口概念,仍按裸 host 比较。新增跨端口远程不被当作工作区项目的回归测试。
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user