Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ API_PROXY_TARGET=http://backend:8888
# Spring Boot 运行配置
SERVER_PORT=8888
APP_AUTO_OPEN_BROWSER=false
APP_BROWSER_INITIALIZE_ON_STARTUP=false
APP_STATIC_SERVER_ENABLED=true
TZ=Asia/Shanghai
JAVA_OPTS=-Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8

Expand Down
43 changes: 37 additions & 6 deletions WINDOWS_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ chrome-extension
你当前项目目录是:

```text
C:\Users\10578\Documents\New project 3\AI-JobPilot
C:\Users\10578\Documents\New project 3
```

后面的命令如果需要手动执行,都在这个目录或它的 `front` 子目录执行。
Expand Down Expand Up @@ -93,9 +93,9 @@ start_windows.bat
3. 检查 Node.js 和 pnpm。
4. 检查 `front\node_modules` 是否存在。
5. 准备 `db`、`data`、`output`、`logs`、`target\cache` 等目录。
6. 启动后端
7. 启动前端
8. 简单检查 8888 和 6866 端口
6. 先启动前端,再启动后端,避免 6866 端口冲突
7. 分别检查前端页面和后端健康接口,而不只是检查端口是否被占用
8. 默认不自动打开管理页或招聘网站

启动成功后打开:

Expand All @@ -115,6 +115,37 @@ http://localhost:8888/api/health
{"status":"UP","service":"GetJobs"}
```

## 交给 Alter 启动

不要让 Alter 启动根目录的 `start_windows.bat`。该文件用于人工双击,会在后台拉起两个服务;Alter 应分别托管下面两个前台脚本。

### Frontend 组件

```text
脚本:C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
参数:-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "C:\Users\10578\Documents\New project 3\scripts\run_frontend.ps1"
工作目录:C:\Users\10578\Documents\New project 3
健康地址:http://127.0.0.1:6866
```

### Backend 组件

```text
脚本:C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
参数:-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "C:\Users\10578\Documents\New project 3\scripts\run_backend.ps1"
工作目录:C:\Users\10578\Documents\New project 3
健康地址:http://127.0.0.1:8888/api/health
```

如果你只想手动运行脚本,也可以在 PowerShell 中分别执行:

```powershell
.\scripts\run_frontend.ps1
.\scripts\run_backend.ps1
```

两个组件可以同时交给 Alter 启动。Backend 脚本会关闭后端对 6866 的额外占用,因此前后端不再抢端口。

## 第一次启动前安装前端依赖

如果脚本提示 `front 目录还没有安装依赖`,请在项目根目录执行:
Expand Down Expand Up @@ -156,7 +187,7 @@ http://localhost:8888/api/health
另开一个 PowerShell 窗口,进入前端目录:

```powershell
cd C:\Users\10578\Documents\New project 3\AI-JobPilot\front
cd "C:\Users\10578\Documents\New project 3\front"
pnpm dev
```

Expand All @@ -183,7 +214,7 @@ Boss 和智联建议使用 Chrome Bridge。
5. 选择:

```text
C:\Users\10578\Documents\New project 3\AI-JobPilot\chrome-extension
C:\Users\10578\Documents\New project 3\chrome-extension
```

6. 回到 `http://localhost:6866` 并刷新页面。
Expand Down
91 changes: 52 additions & 39 deletions front/app/ai-config/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
'use client'

import { useEffect, useState } from 'react'
import { BiSave, BiBrain, BiInfoCircle, BiUpload } from 'react-icons/bi'
import { BiSave, BiBrain, BiInfoCircle, BiRefresh, BiUpload } from 'react-icons/bi'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import PageHeader from '@/app/components/PageHeader'
import ProfileSwitcher, { type Profile } from '@/app/components/ProfileSwitcher'
import { API_BASE } from '@/lib/api'
import { API_BASE, type ApiEnvelope, friendlyApiError, readApiResponse } from '@/lib/api'

const MAX_RESUME_FILE_SIZE = 30 * 1024 * 1024

Expand Down Expand Up @@ -40,6 +40,24 @@ type SavedResume = {
parseMessage?: string
}

type ProfileAwareResponse<T> = ApiEnvelope<T> & {
currentProfile?: Profile | null
hasProfile?: boolean
}

type BossConfigResponse = ProfileAwareResponse<never> & {
config?: {
enableAi?: unknown
sayHi?: string
}
}

type GeneratedAiConfig = {
introduce?: string
prompt?: string
sayHi?: string
}

type SaveOptions = {
nextAiConfig?: AiConfig
nextResumeText?: string
Expand All @@ -65,6 +83,7 @@ export default function AiConfigPage() {
const [generating, setGenerating] = useState(false)
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
const [statusMessage, setStatusMessage] = useState('')
const [loadError, setLoadError] = useState('')
const [enableAi, setEnableAi] = useState<number>(0)
const [currentProfile, setCurrentProfile] = useState<Profile | null>(null)
const [hasProfile, setHasProfile] = useState(false)
Expand All @@ -90,6 +109,7 @@ export default function AiConfigPage() {
}

const reloadCurrentData = async () => {
setLoadError('')
await Promise.all([fetchAiConfig(), fetchBossConfig(), fetchResume(), fetchPriorityCompanies()])
setHasUnsavedChanges(false)
setResumeDirty(false)
Expand All @@ -102,8 +122,7 @@ export default function AiConfigPage() {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
})
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
const result = await response.json()
const result = await readApiResponse<AiConfig>(response, 'AI配置加载失败') as ProfileAwareResponse<AiConfig>
if (result.success && result.data) {
setAiConfig({
introduce: result.data.introduce || '',
Expand All @@ -115,15 +134,15 @@ export default function AiConfigPage() {
setCurrentProfile(result.currentProfile || null)
setHasProfile(Boolean(result.hasProfile || result.currentProfile))
} catch (error) {
console.error('加载AI配置失败:', error)
setLoadError((current) => current || friendlyApiError(error, 'AI配置加载失败'))
setAiConfig({ introduce: '', prompt: '' })
}
}

const fetchResume = async () => {
try {
const response = await fetch(`${API_BASE}/api/ai/resume`)
const result = await response.json()
const result = await readApiResponse<SavedResume>(response, '简历加载失败') as ProfileAwareResponse<SavedResume>
if (result.success && result.data) {
setResumeText(result.data.resumeText || '')
setResumeMeta({
Expand All @@ -138,7 +157,7 @@ export default function AiConfigPage() {
setCurrentProfile(result.currentProfile || null)
setHasProfile(Boolean(result.hasProfile || result.currentProfile))
} catch (error) {
console.error('加载简历失败:', error)
setLoadError((current) => current || friendlyApiError(error, '简历加载失败'))
setResumeText('')
setResumeMeta(null)
}
Expand All @@ -147,7 +166,7 @@ export default function AiConfigPage() {
const fetchPriorityCompanies = async () => {
try {
const response = await fetch(`${API_BASE}/api/ai/companies/priority`)
const result = await response.json()
const result = await readApiResponse<PriorityCompany[]>(response, '优先公司加载失败') as ProfileAwareResponse<PriorityCompany[]>
if (result.success && Array.isArray(result.data)) {
setPriorityCompanies(result.data.map((it: PriorityCompany) => it.companyName).filter(Boolean).join('\n'))
} else {
Expand All @@ -156,7 +175,7 @@ export default function AiConfigPage() {
setCurrentProfile(result.currentProfile || null)
setHasProfile(Boolean(result.hasProfile || result.currentProfile))
} catch (error) {
console.error('加载优先公司失败:', error)
setLoadError((current) => current || friendlyApiError(error, '优先公司加载失败'))
setPriorityCompanies('')
}
}
Expand All @@ -167,30 +186,19 @@ export default function AiConfigPage() {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
})
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
const result = await response.json()
const result = await readApiResponse<never>(response, 'Boss配置加载失败') as BossConfigResponse
setEnableAi(parseEnableAi(result?.config?.enableAi))
setSayHi(result?.config?.sayHi || '')
setCurrentProfile(result?.currentProfile || null)
setHasProfile(Boolean(result?.hasProfile || result?.currentProfile))
} catch (e) {
console.error('加载Boss AI配置失败:', e)
}
}

const parseJsonResponse = async (response: Response, fallback: string) => {
try {
const result = await response.json()
if (!response.ok || result?.success === false) {
throw new Error(result?.message || fallback)
}
return result
} catch (error) {
if (error instanceof Error) throw error
throw new Error(fallback)
setLoadError((current) => current || friendlyApiError(error, 'Boss配置加载失败'))
}
}

const parseJsonResponse = async <T,>(response: Response, fallback: string) =>
readApiResponse<T>(response, fallback)

const toggleEnableAi = async () => {
if (!hasProfile) {
alert('请先新建档案')
Expand All @@ -204,12 +212,11 @@ export default function AiConfigPage() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enableAi: next }),
})
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
await readApiResponse<unknown>(response, 'AI开关保存失败')
setStatusMessage('AI开关已保存')
} catch (e) {
console.error('更新enable_ai失败:', e)
} catch (error) {
setEnableAi((prev) => (prev ? 0 : 1))
alert('切换失败,请检查后端服务连接')
alert(friendlyApiError(error, '切换失败,请检查后端服务连接'))
}
}

Expand All @@ -219,7 +226,7 @@ export default function AiConfigPage() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(configToSave),
})
const result = await parseJsonResponse(response, 'AI配置保存失败')
const result = await parseJsonResponse<AiConfig>(response, 'AI配置保存失败')
return result.data
}

Expand All @@ -239,7 +246,7 @@ export default function AiConfigPage() {
method: 'POST',
body: resumeForm,
})
const result = await parseJsonResponse(response, '简历保存失败')
const result = await parseJsonResponse<SavedResume>(response, '简历保存失败')
if (result.data) {
setResumeText(result.data.resumeText || textToSave)
setResumeMeta({
Expand All @@ -265,7 +272,7 @@ export default function AiConfigPage() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(companies),
})
const result = await parseJsonResponse(response, '优先公司保存失败')
const result = await parseJsonResponse<PriorityCompany[]>(response, '优先公司保存失败')
return result.data
}

Expand All @@ -275,8 +282,7 @@ export default function AiConfigPage() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sayHi: nextSayHi, enableAi }),
})
if (!response.ok) throw new Error('Boss默认打招呼语保存失败')
return response.json()
await readApiResponse<unknown>(response, 'Boss默认打招呼语保存失败')
}

const saveEverything = async ({
Expand Down Expand Up @@ -309,8 +315,7 @@ export default function AiConfigPage() {
try {
await saveEverything()
} catch (error) {
console.error('保存AI配置失败:', error)
alert(error instanceof Error ? error.message : '保存失败,请检查服务器连接!')
alert(friendlyApiError(error, '保存失败,请检查服务器连接!'))
} finally {
setLoading(false)
}
Expand All @@ -336,7 +341,7 @@ export default function AiConfigPage() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ resumeText: latestResumeText }),
})
const result = await parseJsonResponse(response, 'AI配置生成失败')
const result = await parseJsonResponse<GeneratedAiConfig>(response, 'AI配置生成失败')

const nextSayHi = result.data?.sayHi || ''
const nextAiConfig = {
Expand All @@ -358,8 +363,7 @@ export default function AiConfigPage() {
setStatusMessage('已提交简历并生成AI配置')
alert('已提交简历,并生成打招呼话术和AI配置!')
} catch (error) {
console.error('提交简历并生成AI配置失败:', error)
alert(error instanceof Error ? error.message : '提交简历并生成AI配置失败')
alert(friendlyApiError(error, '提交简历并生成AI配置失败'))
} finally {
setGenerating(false)
}
Expand Down Expand Up @@ -423,6 +427,15 @@ export default function AiConfigPage() {
}}
/>

{loadError ? (
<div role="status" aria-live="polite" className="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
<span>{loadError}</span>
<Button type="button" size="sm" variant="ghost" onClick={reloadCurrentData}>
<BiRefresh className="mr-1" /> 重试加载
</Button>
</div>
) : null}

{!hasProfile ? (
<div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
请先在上方新建档案;没有档案时,下方简历、AI配置、平台参数和投递分析都不会写入。
Expand Down
Loading
Loading