Explorar o código

Fix Indeed Apply 404s by preferring stable search URLs

Apply now opens /jobs?q= title search when the stored URL is not an
Indeed search page, avoiding dead viewjob/pagead links from the model.
Tighten job-search prompts and JSON schema to favor /jobs URLs and
discourage invented jk permalinks.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 hai 2 meses
pai
achega
9785ddef31
Modificáronse 1 ficheiros con 42 adicións e 10 borrados
  1. 42 10
      App for Indeed/Views/DashboardView.swift

+ 42 - 10
App for Indeed/Views/DashboardView.swift

@@ -840,16 +840,48 @@ final class DashboardView: NSView, NSTextFieldDelegate {
 
     @objc private func didTapJobApply(_ sender: NSButton) {
         guard let job = (sender as? JobPayloadButton)?.jobPayload else { return }
-        let url: URL
-        if let rawURL = job.url, let resolved = URL(string: rawURL), !rawURL.isEmpty {
-            url = resolved
+        presentIndeedJobBrowser(url: Self.resolvedIndeedApplyURL(for: job))
+    }
+
+    /// Apply always loads a live Indeed page. Single-job URLs (`/viewjob`, `/pagead`, `/rc/clk`, …) are often expired or incorrectly synthesized by the model and show Indeed’s 404; we only trust **search** URLs whose path is `/jobs`. Otherwise we open a fresh `/jobs?q=<title>` (same regional host when the model provided one).
+    private static func resolvedIndeedApplyURL(for job: JobListing) -> URL {
+        let title = job.title.trimmingCharacters(in: .whitespacesAndNewlines)
+        let raw = job.url?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
+
+        var preferredHost: String?
+        if !raw.isEmpty, let components = URLComponents(string: raw), let host = components.host {
+            let lower = host.lowercased()
+            if isIndeedApplyHost(lower) {
+                preferredHost = host
+                let pathLower = components.path.lowercased()
+                let isJobsSearchPath = pathLower == "/jobs" || pathLower.hasSuffix("/jobs")
+                if isJobsSearchPath, let url = components.url {
+                    return url
+                }
+            }
+        }
+        return indeedJobsSearchURL(title: title, preferredHost: preferredHost)
+    }
+
+    private static func indeedJobsSearchURL(title: String, preferredHost: String?) -> URL {
+        let allowed = CharacterSet.urlQueryAllowed
+        let q = title.addingPercentEncoding(withAllowedCharacters: allowed) ?? ""
+        let host: String
+        if let preferredHost, isIndeedApplyHost(preferredHost.lowercased()) {
+            host = preferredHost
         } else {
-            let allowed = CharacterSet.urlQueryAllowed
-            let q = job.title.addingPercentEncoding(withAllowedCharacters: allowed) ?? ""
-            guard let fallback = URL(string: "https://www.indeed.com/jobs?q=\(q)") else { return }
-            url = fallback
+            host = "www.indeed.com"
+        }
+        if let url = URL(string: "https://\(host)/jobs?q=\(q)") {
+            return url
         }
-        presentIndeedJobBrowser(url: url)
+        return URL(string: "https://www.indeed.com/jobs?q=\(q)")!
+    }
+
+    private static func isIndeedApplyHost(_ host: String) -> Bool {
+        if host == "indeed.com" { return true }
+        if host.hasPrefix("indeed.") { return true }
+        return host.contains(".indeed.")
     }
 
     private func presentIndeedJobBrowser(url: URL) {
@@ -2329,7 +2361,7 @@ private final class OpenAIJobSearchService {
 
         Your final assistant message must be JSON that strictly matches the configured response schema (one object with a "jobs" array). Do not add markdown, code fences, or conversational prose outside that JSON.
 
-        Each job entry needs a title, a single-sentence description, and a "url" string. The "url" must be a direct Indeed job link (viewjob, pagead, or equivalent on an Indeed domain) when web search finds one; use an empty string for "url" only when you cannot find any Indeed URL for that role (omit the listing if it is not on Indeed).
+        Each job entry needs a title, a single-sentence description, and a "url" string. Prefer a stable Indeed **search results** URL on the correct regional domain (path `/jobs` with a `q=` query built from the listing title, company, and location—e.g. `https://www.indeed.com/jobs?q=…&l=…`). Never invent or guess job keys (`jk=`), click IDs, or viewjob URLs you did not copy exactly from live search results—wrong permalinks show 404 inside the app. Use an empty string for "url" only when you cannot construct any Indeed URL (omit the listing if it is not on Indeed).
 
         Return at most \(jobLimit) distinct listings. If the conversation context already lists jobs, do not repeat the same titles or URLs when the user asks for more—it is fine to return fewer than \(jobLimit) new results.
 
@@ -2668,7 +2700,7 @@ private struct OpenAIJobSearchAPIRequest: Encodable {
         let itemProperties = OpenAIJobSearchJobItemProperties(
             title: OpenAIJSONSchemaStringField(type: "string", description: "Job title as shown on the Indeed listing."),
             description: OpenAIJSONSchemaStringField(type: "string", description: "One concise sentence summarizing the role from the Indeed posting."),
-            url: OpenAIJSONSchemaStringField(type: "string", description: "Direct Indeed job URL (https on indeed.com or a regional Indeed domain such as indeed.co.uk); empty string only if no Indeed URL exists—never use LinkedIn, Glassdoor, or other boards.")
+            url: OpenAIJSONSchemaStringField(type: "string", description: "Indeed search URL (https, path /jobs, query q=…) on indeed.com or the correct regional Indeed host; never fabricate viewjob/jk links. Empty string only if no Indeed URL exists—never use LinkedIn, Glassdoor, or other boards.")
         )
         let itemSchema = OpenAIJobSearchJobItemSchema(
             type: "object",