DeepSeek Harness Remote
DeepSeek Harness Remote 可将手机或另一台电脑与你运行 DeepSeek Harness 的电脑安全连接,随时随地继续你的工作,方便随时查看进度并继续工作。
AI 翻译
尚未收集仓库信息。
尚未检测到更新。GitHub 发布版本后会显示在这里。
制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗An AI-assisted resume builder: structured resume editor, ATS keyword match scoring against a job description, AI bullet-point rewriting, AI cover letter generation, PDF export, and account/auth.
pg) for the database — production-ready, works on serverless
hosts like Vercel. Every call site only touches the functions exported from
lib/db.ts, so switching providers (Neon, Supabase, RDS, etc.) is a
connection-string change, not a code change.@anthropic-ai/sdk) — AI bullet rewriting + cover letters@fontsource, bundled at build time — no runtime dependency on Google's font CDN)# 1. Install Postgres locally, then:
createdb resumeai
# 2. Install deps and configure env
npm install
cp .env.local.example .env.local
# edit .env.local:
# DATABASE_URL=postgres://postgres:postgres@localhost:5432/resumeai
# ANTHROPIC_API_KEY=sk-ant-... (get one at console.anthropic.com)
# NEXTAUTH_SECRET=$(openssl rand -base64 32)
npm run dev
# open http://localhost:3000
Schema (users + resumes tables) is created automatically on first request — no separate migration step needed for this MVP.
Without a real ANTHROPIC_API_KEY, everything works except the two AI
endpoints (bullet rewriting and cover letter generation) — auth, resume
CRUD, ATS scoring, and PDF export are all pure-code/no-external-API and work
immediately.
Pro is sold as one-time passes (1, 3 or 12 months, prices in lib/plans.ts). No auto-renewal: buying again while on Pro adds the time after the current end date, a daily job (/api/cron/billing) moves lapsed passes back to Free and emails a reminder 3 days before the end.
RAZORPAY_KEY_ID and RAZORPAY_KEY_SECRET.https://your-domain.com/api/billing/webhook, events order.paid and payment.captured. Copy its secret into RAZORPAY_WEBHOOK_SECRET.RAZORPAY_INTERNATIONAL=on after approval.Without keys everything else works, and the Pro button says "Opens at launch".
Both are optional — the app works fine with just email/password login if you skip this. When configured, "Continue with Google" / "Continue with LinkedIn" appear on the login and signup pages automatically (no code change needed, just add the env vars).
Account linking: if someone signs up with a password using
[email protected], then later clicks "Continue with Google" using that same
Google account, they're signed into the same account — not a duplicate.
This is handled in lib/oauth-user.ts (tested against real Postgres in
tests/oauth-user.test.ts).
Reuses the same Google Cloud OAuth app as Google Drive export (GOOGLE_CLIENT_ID
/ GOOGLE_CLIENT_SECRET) — you need two redirect URIs registered on it,
not one:
https://your-domain.com/api/auth/callback/google (for login)https://your-domain.com/api/google/callback (for Drive export — see below)Add both under Google Cloud Console → Credentials → your OAuth client → Authorized redirect URIs.
https://your-domain.com/api/auth/callback/linkedinLINKEDIN_CLIENT_ID / LINKEDIN_CLIENT_SECRETLets Pro users save a resume PDF straight to their own Google Drive.
https://your-domain.com/api/google/callback (and http://localhost:3000/api/google/callback for local dev)GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRETThe app only requests the drive.file scope — it can only see/write files it creates itself, never a user's existing Drive contents.
Without real Google credentials, the rest of the app still works — the Drive export button will correctly show a "couldn't connect" state instead of crashing.
Three separate concerns, set up separately:
SENTRY_DSN and NEXT_PUBLIC_SENTRY_DSNSENTRY_ORG / SENTRY_PROJECT from your Sentry project settings/support — no account requiredsupport_messages table regardless of any other configADMIN_EMAIL to your own account's email — that unlocks /admin/support, a private inbox only you can see (returns a 404, not a login prompt, to anyone else)RESEND_API_KEY + RESEND_FROM_EMAIL (free tier at resend.com) to also get an email the moment someone submits a message. Without it, messages still save fine, you just have to check /admin/support yourself.invoice.payment_failed to Sentry as a warning (with the customer/amount) so you see it alongside other errors — it does not auto-downgrade the user, since Stripe's own retry schedule handles thatAn uptime monitor has to run independently of your app — if the app is down, code running inside it can't report that. Use a free service:
https://your-domain.vercel.app/api/health/api/health returns 200 with {"status":"ok"} when the DB is reachable, 503 otherwise — so this also catches database outages, not just "server is down"createdb resumeai_test # one-time, for the test suite
npm test # runs the Vitest suite (14 tests: ATS scoring + real Postgres DB layer)
npx tsc --noEmit # type-check
npm run build # production build
app/
page.tsx landing page
login/, signup/ auth pages
dashboard/ resume list
builder/[id]/ resume editor (form + live preview + AI tabs)
api/
auth/register user signup
auth/[...nextauth] NextAuth handlers
resumes/ CRUD for resumes
resumes/pdf PDF export
ai/generate-bullets AI bullet rewriting
ai/ats-score ATS keyword match scoring (no external API)
ai/cover-letter AI cover letter generation
lib/
db.ts database access layer (node:sqlite)
auth.ts NextAuth config
ai.ts Anthropic API calls
ats-score.ts keyword extraction + scoring engine
types.ts shared resume data types
components/
ResumeEditor.tsx the main builder UI
ResumePdfDocument.tsx PDF template
tests/
ats-score.test.ts, db.test.ts
The 5 endpoints that call Anthropic's API (bullet rewriting, summary writer,
cover letter, resignation letter, AI agent) are rate-limited per user via a
Postgres-backed sliding window (lib/rate-limit.ts) — 15-20 requests per 10
minutes depending on the endpoint. This is backed by the database (not an
in-memory counter) because Vercel runs multiple serverless instances that
don't share memory — an in-memory limiter would be trivially bypassed.
Both require RESEND_API_KEY to actually send emails:
/forgot-password sends a real reset link.Unverified users are not blocked from using the app — the banner is a nudge, not a gate. If you want verification enforced before certain actions (e.g. before Pro checkout), that's a small addition to make, not built in by default.
Real bug, found via the admin panel: accounts created via Google/LinkedIn
sign-in were originally defaulting to email_verified: false, even though
completing OAuth login already proves email ownership - there's no
separate verification step that should even exist for these accounts. If
RESEND_API_KEY was ever unset at the time (or is unset now), an affected
account has no way to self-serve clear the banner - there's no email
service to send a link to.
This was first patched with a manual admin action ("Mark email verified" on
a user's detail page in /admin/users) - functional, but required an admin
to notice and click it per affected account.
findOrCreateOAuthUser() in lib/oauth-user.ts now self-heals this
automatically, on every OAuth login, for every affected account - past or
future, no admin involvement needed. When an existing user record is
found during OAuth sign-in and it's still email_verified: false, the
successful OAuth login happening right now is treated as proof of email
ownership and the record is corrected immediately, before the session is
even created. The manual admin button still exists as a fallback (e.g. for
a credentials-based account stuck unverified for unrelated reasons), but it
should rarely be needed now.
Verified two ways, not just unit tests: a dedicated test
(tests/oauth-user.test.ts) recreates the exact stuck-account scenario end
to end, and separately, the real function was called directly against a
real Postgres database with a before/after check on the raw row -
email_verified confirmed flipping f → t at the database level, with the
pending verification token cleared too.
Technical/on-page (done):
<title> and meta description per public page (landing, pricing, templates, privacy, terms)/robots.txt and /sitemap.xml generated dynamically (app/robots.ts, app/sitemap.ts)SoftwareApplication) on the landing page — honest fields only, no fabricated ratings/reviews/templates is now publicly crawlable — it previously redirected to /login before a visitor (or Googlebot) could see it at all, which meant zero SEO value for a page that should realistically rank for "resume templates" searches. Auth is now only required when actually creating a resume, not for browsing.Requires NEXTAUTH_URL to be set to your real production domain in Vercel — the sitemap/robots/canonical URLs all derive from it. If it's still unset or pointing at localhost, fix that first or none of this resolves to real URLs in production.
Content SEO (not done, and honestly the bigger lever): the "resume examples for nurses," "how to write a resume with no experience" style pages that actually drive most of this category's organic traffic aren't built. That's a sustained content practice, not a one-time task — see the conversation this was built in for a longer discussion of what that would take.
Anyone can start building a resume at /builder/new with zero account —
manual editing, live preview, Resume Score, and Job Match all work for
anonymous visitors. Their draft is saved in the browser's localStorage
(see lib/guest-draft.ts), not on the server, since there's no account yet
to attach it to.
What requires login: AI features (bullet rewriting, cover letter, AI agent) and actually downloading a file. Clicking Export shows a modal prompting signup/login rather than calling the export API directly.
What happens after they sign up: completeGuestExport() in
lib/guest-draft.ts reads the local draft, creates a real saved resume via
the normal /api/resumes endpoint, immediately triggers the export they
originally clicked, and lands them in the real editor — their work is never
lost across the signup step.
The watermark is a CSS overlay on the live preview only ("SIGN IN TO DOWNLOAD"), not baked into any actual file — since download is gated behind login entirely, there's never a scenario where an anonymous user holds a watermarked (or any) real export. Once logged in, the file has no watermark.
Known testing limitation: the localStorage/redirect handoff itself needs a real browser to fully exercise end-to-end; what's verified here is that the server-side API sequence it depends on (create resume → export) works correctly with real data.
The admin portal uses a deliberately different color language from the user-facing app — dark crimson sidebar instead of navy, a light rose-tinted content background instead of cream, a permanent "⚠ Real user data — act carefully" label, and a warning-stripe accent bar. This isn't just styling: since admin has full read/write access to real user data (including deleting accounts), making it visually unmistakable which panel you're in is a real safety measure, not a preference.
Authenticated users can upload a PDF/DOCX/TXT resume (/api/resumes/import)
and have AI extract it into a new, editable resume — text extraction via
pdf-parse and mammoth, structuring via extractResumeFromText() in
lib/ai.ts. Subject to the same free-tier resume cap and AI rate limit as
everything else.
A real bug worth knowing about if you touch this code: pdf-parse
depends on pdfjs-dist, which dynamically loads a worker file at runtime.
Next.js's bundler doesn't handle that dynamic import correctly by default —
it works fine in an isolated script but breaks in the actual built server
with a "cannot find module .../pdf.worker.mjs" error. The fix is
serverExternalPackages: ["pdf-parse", "pdfjs-dist"] in next.config.ts,
which tells Next.js to leave this package's module resolution to Node
directly rather than bundling it. If you ever remove or "clean up" that
config line, PDF import will silently break again.
Users leave a star rating + written feedback at /dashboard/feedback. AI
analyzes it (analyzeReview() in lib/ai.ts) into sentiment, specific
likes/dislikes pulled from their actual wording (not generic categories),
and a genuine personalized reply - shown immediately in the UI and emailed
if Resend is configured.
Admin gets a dedicated "what don't they like" view at /admin/reviews,
not just a raw list of reviews to read one by one - every extracted dislike
across every review is flattened into one panel, most recent first, each
tagged with who said it and their rating. Real stats (total, average
rating, distribution) computed live, no fabricated numbers.
Real reviews can appear on the landing page - but only through two deliberate gates, since the feedback form was originally framed as private ("a real person reads every one"), not a public testimonial submission:
/admin/reviews.This is enforced server-side, not just hidden in the UI:
setReviewFeatured() in lib/db.ts refuses to feature a review without
consent, and getFeaturedReviews() independently filters on consent too -
tested directly in tests/reviews.test.ts, including a test that bypasses
the first guard at the SQL level to prove the second one still holds.
Caching behavior worth knowing: the landing page uses revalidate = 3600 (Next.js ISR), so a review you feature won't appear on the live site
instantly - it can take up to an hour. This is deliberate: querying the
database on every single landing-page visitor for content that changes
rarely would be wasteful. If you need a change to show up immediately,
redeploy (which forces a fresh build) rather than waiting.
Every template (all 9 unique designs, both live preview and PDF export) supports:
components/PhotoUpload.tsx, resized
client-side to max 400px JPEG before storage (so a multi-MB phone photo
doesn't bloat the database row). Stored as a base64 data URL directly in
the resume's customization.photoDataUrl - no blob storage needed.All three are threaded through both the live preview (ResumeEditor.tsx)
and PDF export (ResumePdfDocument.tsx) via layoutFlags() - a shared
helper so the two rendering engines read the same three fields consistently
rather than duplicating the logic per template.
Verified with a real generated test photo, not just a successful HTTP
response: confirmed via pdfimages that the PDF genuinely embeds the
uploaded image (matching pixel color, matching dimensions), that toggling
the photo off produces zero embedded images even when one was uploaded, and
that the dividers toggle produces measurably different PDF output.
Accent color wasn't actually changing anything. Root cause: Tailwind
v4's @theme inline compiles .text-seal directly to color:var(--seal),
not color:var(--color-seal). The color-override code in
ResumePreview (components/ResumeEditor.tsx) was setting
--color-seal on the wrapper div - a variable nothing actually reads.
Confirmed by inspecting the real compiled CSS output, not just reasoning
about it. Fixed by overriding --seal/--seal-soft/--seal-deep
directly. This never affected PDF export, which computes colors as plain
JS values with no CSS variables involved at all - which is exactly why the
earlier PDF-based color testing passed while the live preview was
silently broken the whole time.
Uploaded photos couldn't be repositioned or cropped. There was only
Replace-entirely or Remove-entirely - if a face wasn't centered,
object-cover's automatic crop was final with no way to fix it. Built a
real adjuster (components/PhotoAdjuster.tsx): drag to reposition, slider
to zoom, bakes the result to a canvas on Apply. The original (pre-crop)
image is kept in customization.photoOriginalDataUrl specifically so
"Adjust" can always be reopened later and re-crop from scratch, rather
than progressively degrading an already-cropped image. Also fixed a
smaller real bug found along the way: the file input never reset its
value, so re-selecting the same file silently did nothing.
Clicking "✦ AI" on an empty bullet did nothing. polish() in
ExperienceCard (components/ResumeEditor.tsx) silently returned early
if the bullet textarea was empty - no error, no feedback, just nothing.
Now shows a real message ("Write something first, then click AI to polish
it.") instead of failing silently. Also replaced the alert() popup on
API errors with the same inline message pattern.
Nav destinations fixed per explicit instruction, in TopNav.tsx and
AppSidebar.tsx:
/) everywhere, not /dashboardNewResumeButton's logic) instead of just linking to /dashboardTopNav entirelyPro is priced in 3 tiers based on World Bank income classification (see
lib/pricing-region.ts): full ($19, US/UK/EU/etc.), mid ($9,
Brazil/Mexico/China/etc.), value ($5, or ₹399 for India specifically,
since that's the planned Razorpay market — Indonesia/Philippines/etc. show
the USD figure).
Detection: Vercel's x-vercel-ip-country header, set at the edge from
real IP geolocation — free, no third-party geo service needed, and not
spoofable by a client-sent header (Vercel overwrites it). Only works when
actually deployed on Vercel; elsewhere (local dev, other hosts) it's null
and everyone gets the default "full" tier.
Captured once, at signup, not re-detected on every page load — a logged-in user's price stays stable even if they later browse from a different location, rather than shifting under them.
Billing: STRIPE_PRO_PRICE_ID_FULL / _MID / _VALUE — create 3
separate Prices in Stripe, one per tier. The old single
STRIPE_PRO_PRICE_ID still works as a fallback for the full tier if you
haven't set up regional pricing in Stripe yet. This only affects what
gets displayed and which Stripe Price gets used at checkout — it doesn't
make billing "live." Stripe/Razorpay integration itself is still pending,
per the existing pre-launch checklist.
Admin visibility: /admin/users and /admin/subscriptions show each
user's detected country/tier, and estimated MRR is computed per-subscriber
from their actual tier rather than assuming everyone pays the same amount.
Two changes to bound free-tier AI cost, which previously applied to every active user regardless of whether they ever paid:
AI Resume Agent is now Pro-only. It was the most expensive feature by
far (up to 5 Anthropic calls behind a single chat message) and was
available to free users. Gated via canUseAiAgent() in lib/limits.ts.
Bullet rewriting and summary generation are capped at 5 lifetime uses on
Free, not the existing per-10-minute rate limit (which resets forever and
doesn't bound total cost - see lib/rate-limit.ts for that separate
mechanism). This is a genuinely different kind of limit: a lifetime counter
(ai_writing_assist_count on the users table, incremented via
incrementAiWritingAssistCount() in lib/db.ts), checked via
canUseAiWritingAssist() before the AI call is ever attempted - so a
user who's hit the cap costs nothing further, ever.
Verified via an isolated boundary test (not just unit tests): set a real user's counter to 4, confirmed the next call passes the gate; set it to 5, confirmed the next call is blocked with 402 before any Anthropic API call is attempted - proving the block genuinely prevents further cost, not just returns an error after the fact.
A visible usage indicator ("3 of 5 free uses") shows in the editor for free
users - see EditForm in components/ResumeEditor.tsx.
Free plan gets Classic and Modern only; the other 8 templates require
Pro. Configured in lib/templates.ts (FREE_TEMPLATE_IDS), checked via
canUseTemplate() in lib/limits.ts.
This is enforced at every real point a template gets used, not just the gallery UI - resume creation, resume save, and PDF export all independently check it server-side. This matters specifically because PDF export takes the template as a raw request parameter rather than re-deriving it from what's saved in the database - a save-only check would've left a direct bypass. DOCX and Google Drive export don't need their own check since both are already fully Pro-gated regardless of template.
Guest builder edge case, handled deliberately: a guest can freely
preview any template while building (no restriction - it's just
client-side rendering, costs nothing). If they sign up as Free after
picking a Pro-only template, completeGuestExport() in lib/guest-draft.ts
falls back to Classic rather than silently discarding their written
content - they keep everything they wrote, just not their original visual
choice, and can see why once they're in the real editor.
Also fixed while wiring this in: save() in the resume editor
previously never checked whether the save actually succeeded - it always
displayed "Saved" regardless of the server response. Real bug, not
specific to this feature, but this is what surfaced it. Also replaced the
hardcoded "Upgrade for more PDFs →" / "DOCX is Pro →" link text with the
real server-provided message, since a generic PDF-limit message would've
been actively misleading for a template-restriction rejection.
Verified live: confirmed save/export are both rejected for a Pro template on Free (before any rendering happens), confirmed the same requests succeed for the other free template, and confirmed the exact same user succeeds once upgraded to Pro - not just a mocked assertion.
Not every AI feature needs the same model. lib/ai.ts now routes by task:
claude-sonnet-4-6): cover letter generation (writing
quality matters more here) and the AI Agent in lib/ai-agent.ts
(multi-step tool-use reasoning genuinely benefits from the stronger model)claude-haiku-4-5-20251001): bullet rewriting,
summary generation, resignation letters, resume import extraction, and
review analysis - all well-defined, mostly mechanical tasks (rewrite
this, extract that, classify this) that don't need the expensive model,
and are also the highest-volume calls, which is where routing saves the
most in aggregate.Real impact is bigger on Pro usage than Free: the free-tier lifetime cap (5 calls) already bounded free-tier cost to a small, fixed amount, so routing barely moves that number further. On an active Pro user's total AI cost, routing saves roughly 30% - the Agent and cover letter (the two priciest, least-routable features) intentionally stay on the expensive model, which is why the aggregate saving is more moderate than the ~90% saving on the routed features specifically.
Honest verification note: confirmed the routed endpoints execute correctly end-to-end with the new model (no code-level errors), but this sandbox's placeholder Anthropic API key means the actual per-call cost reduction couldn't be measured directly - Anthropic's real usage dashboard (console.anthropic.com) will show the real before/after once this is live.
DATABASE_URL at a
managed instance (Neon, Supabase, Railway, RDS all work) instead of local
Postgres. No code changes needed.NEXTAUTH_SECRET (openssl rand -base64 32)
and set it as an environment variable in your host, never commit it.ANTHROPIC_API_KEY as a server-side env var in
your host's dashboard.vercel deploy after vercel login, or connect the GitHub
repo in the Vercel dashboard). Railway/Render/Fly.io also work with a
Dockerfile if you'd rather not use Vercel.users.plan in the DB is the
standard pattern — say the word and I'll wire it in.lib/ai-agent.ts) supports 9 tools covering contact,
summary, experience, education, and skills edits. It does not yet call
the ATS scorer or cover-letter generator as tools within the same
conversation - those stay on their own tabs.The visual design (navy/gold/cream palette, Playfair Display headings, sidebar app layout) was rebuilt to match a set of reference mockups. Two things were deliberately not copied from those mockups, on principle rather than preference:
/admin queries and
displays Lettr's actual database — real user counts, real plan breakdown,
real signups-per-week chart. It'll show real (small) numbers until there
are real users, which is correct behavior, not a bug.