Skip to content
Open
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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@ API_BASE_URL=
API_PROXY_TARGET=http://backend:8888

# Spring Boot 运行配置
SERVER_ADDRESS=127.0.0.1
SERVER_PORT=8888
APP_AUTO_OPEN_BROWSER=false
APP_BROWSER_INITIALIZE_ON_STARTUP=false
APP_STATIC_SERVER_ENABLED=true

# 原生前端生产服务默认仅监听本机;只有明确需要局域网访问时才修改。
FRONTEND_HOST=127.0.0.1
TZ=Asia/Shanghai
JAVA_OPTS=-Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8

Expand Down
13 changes: 8 additions & 5 deletions doc/API接口.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ http://localhost:8888

| 方法 | 路径 | 说明 |
| --- | --- | --- |
| `GET` | `/api/config` | 获取全部全局配置 |
| `GET` | `/api/config/{key}` | 获取指定配置 |
| `POST` | `/api/config` | 新增配置 |
| `PUT` | `/api/config/{key}` | 更新指定配置 |
| `GET` | `/api/config` | 获取 UI 白名单内的全局配置;敏感值只返回是否已配置 |
| `GET` | `/api/config/{key}` | 获取指定白名单配置;`API_KEY`、`HOOK_URL` 不返回原值 |
| `POST` | `/api/config` | 批量新增或更新白名单配置;敏感值为空时保留原值 |
| `PUT` | `/api/config/{key}` | 更新指定白名单配置 |
| `DELETE` | `/api/config/{key}` | 显式清除 `API_KEY` 或 `HOOK_URL` 的数据库值 |
| `GET` | `/api/config/health` | 配置模块健康检查 |

## AI 配置
Expand Down Expand Up @@ -54,9 +55,11 @@ Boss 投递分析页“岗位数据”区域使用 `GET/POST /api/ai/thresholds`

| 方法 | 路径 | 说明 |
| --- | --- | --- |
| `GET` | `/api/cookie` | 获取 Cookie |
| `GET` | `/api/cookie` | 获取 Cookie 是否已配置及非敏感元数据,不返回 Cookie 原文 |
| `POST` | `/api/cookie/save` | 保存 Cookie |

> Cookie 原文只供内部浏览器会话恢复使用。各平台兼容查询接口同样只返回 `configured` 状态和非敏感元数据。

## Profile 档案

Profile 用于区分不同候选人或简历上下文。当前档案会影响 AI 配置、简历、平台配置、岗位数据和 AI 分析结果的读取与写入。
Expand Down
5 changes: 3 additions & 2 deletions doc/Dsign.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,9 @@ SSE 推送前端 ▶ 前端刷新任务状态

### 6.3 敏感信息与加密

- AI Key、Cookie 等敏感字段使用对称加密存储(进程启动时解密)。
- 数据库文件与快照目录加入 Git 忽略;生产环境可选 SQLCipher。
- 当前 V1 的 AI Key、Webhook 和 Cookie 保存在本机 SQLite 中,**尚未做字段级加密**。
- HTTP 查询接口不回传敏感原文,配置页面采用只写输入并只显示“是否已配置”;内部 AI、通知和浏览器会话按需读取原值。
- 数据库文件与快照目录必须加入 Git 忽略;对外部署或多人共享前,需要再引入系统凭据库、字段级加密或 SQLCipher,并补齐身份认证与用户隔离。

### 6.4 资源与路径规范

Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ services:
- "127.0.0.1:${BACKEND_PORT:-8888}:8888"
environment:
TZ: ${TZ:-Asia/Shanghai}
SERVER_ADDRESS: 0.0.0.0
SERVER_PORT: ${SERVER_PORT:-8888}
APP_AUTO_OPEN_BROWSER: ${APP_AUTO_OPEN_BROWSER:-false}
SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL:-jdbc:sqlite:/workspace/db/getjobs.db}
Expand Down
107 changes: 88 additions & 19 deletions front/app/env-config/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export default function EnvConfig() {
})

const [showApiKey, setShowApiKey] = useState(false)
const [sensitiveConfigured, setSensitiveConfigured] = useState({ hookUrl: false, apiKey: false })
const [sensitiveDirty, setSensitiveDirty] = useState({ hookUrl: false, apiKey: false })
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [showSaveDialog, setShowSaveDialog] = useState(false)
Expand All @@ -47,20 +49,25 @@ export default function EnvConfig() {

if (result.success && result.data) {
setEnvConfig({
hookUrl: result.data.HOOK_URL || '',
hookUrl: '',
aiProvider: result.data.AI_PROVIDER === 'api' || result.data.AI_PROVIDER === 'remote' ? 'api' : 'codex',
codexPath: result.data.CODEX_PATH || 'codex',
codexModel: result.data.CODEX_MODEL || 'gpt-5.6-sol',
codexTimeoutSeconds: result.data.CODEX_TIMEOUT_SECONDS || '300',
baseUrl: result.data.BASE_URL || '',
apiKey: result.data.API_KEY || '',
apiKey: '',
model: result.data.MODEL || '',
botIsSend: (() => {
const raw = result.data.BOT_IS_SEND
const val = String(raw ?? '').trim().toLowerCase()
return val === '1' || val === 'true' ? 1 : 0
})(),
})
setSensitiveConfigured({
hookUrl: result.sensitive?.HOOK_URL === true,
apiKey: result.sensitive?.API_KEY === true,
})
setSensitiveDirty({ hookUrl: false, apiKey: false })
}
} catch (error) {
console.error('获取配置失败:', error)
Expand All @@ -78,17 +85,21 @@ export default function EnvConfig() {
try {
setSaving(true)

const configMap = {
HOOK_URL: envConfig.hookUrl,
const configMap: Record<string, string> = {
AI_PROVIDER: envConfig.aiProvider,
CODEX_PATH: envConfig.codexPath,
CODEX_MODEL: envConfig.codexModel,
CODEX_TIMEOUT_SECONDS: envConfig.codexTimeoutSeconds,
BASE_URL: envConfig.baseUrl,
API_KEY: envConfig.apiKey,
MODEL: envConfig.model,
BOT_IS_SEND: String(envConfig.botIsSend ?? 0),
}
if (sensitiveDirty.hookUrl && envConfig.hookUrl.trim()) {
configMap.HOOK_URL = envConfig.hookUrl.trim()
}
if (sensitiveDirty.apiKey && envConfig.apiKey.trim()) {
configMap.API_KEY = envConfig.apiKey.trim()
}

const response = await fetch(`${API_BASE}/api/config`, {
method: 'POST',
Expand All @@ -105,6 +116,12 @@ export default function EnvConfig() {
const result = await response.json()

if (result.success) {
setSensitiveConfigured((current) => ({
hookUrl: sensitiveDirty.hookUrl && envConfig.hookUrl.trim() ? true : current.hookUrl,
apiKey: sensitiveDirty.apiKey && envConfig.apiKey.trim() ? true : current.apiKey,
}))
setSensitiveDirty({ hookUrl: false, apiKey: false })
setEnvConfig((current) => ({ ...current, hookUrl: '', apiKey: '' }))
if (!silent) {
setSaveResult({ success: true, message: '保存成功' })
setShowSaveDialog(true)
Expand All @@ -123,6 +140,39 @@ export default function EnvConfig() {
}
}

const clearSensitiveConfig = async (key: 'HOOK_URL' | 'API_KEY') => {
const label = key === 'HOOK_URL' ? 'Webhook URL' : 'API Key'
if (!window.confirm(`确定清除已保存的 ${label} 吗?清除后相关功能将无法使用,直到重新填写。`)) {
return
}

try {
setSaving(true)
const response = await fetch(`${API_BASE}/api/config/${key}`, { method: 'DELETE' })
const result = await response.json()
if (!response.ok || !result.success) {
throw new Error(result.message || '清除失败')
}
if (key === 'HOOK_URL') {
setSensitiveConfigured((current) => ({ ...current, hookUrl: result.configured === true }))
setSensitiveDirty((current) => ({ ...current, hookUrl: false }))
setEnvConfig((current) => ({ ...current, hookUrl: '' }))
} else {
setSensitiveConfigured((current) => ({ ...current, apiKey: result.configured === true }))
setSensitiveDirty((current) => ({ ...current, apiKey: false }))
setEnvConfig((current) => ({ ...current, apiKey: '' }))
}
setSaveResult({ success: true, message: result.message || `${label} 已清除` })
setShowSaveDialog(true)
} catch (error) {
console.error('清除敏感配置失败:', error)
setSaveResult({ success: false, message: `${label} 清除失败,请检查后端服务。` })
setShowSaveDialog(true)
} finally {
setSaving(false)
}
}

return (
<div className="space-y-6">
<PageHeader
Expand Down Expand Up @@ -177,14 +227,24 @@ export default function EnvConfig() {
<Label htmlFor="hookUrl">Webhook URL</Label>
<Input
id="hookUrl"
type="text"
type="password"
value={envConfig.hookUrl}
onChange={(e) => setEnvConfig({ ...envConfig, hookUrl: e.target.value })}
placeholder="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your_key"
onChange={(e) => {
setEnvConfig({ ...envConfig, hookUrl: e.target.value })
setSensitiveDirty({ ...sensitiveDirty, hookUrl: true })
}}
placeholder={sensitiveConfigured.hookUrl ? '已配置;输入新值可替换' : 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your_key'}
/>
<p className="text-xs text-muted-foreground">
企业微信群机器人webhook地址,用于接收通知消息
</p>
<div className="flex items-center justify-between gap-3">
<p className="text-xs text-muted-foreground">
{sensitiveConfigured.hookUrl ? '已配置,页面不会读取或显示原值。' : '尚未配置企业微信 Webhook。'}
</p>
{sensitiveConfigured.hookUrl && (
<Button type="button" variant="outline" size="sm" disabled={saving} onClick={() => clearSensitiveConfig('HOOK_URL')}>
清除已保存值
</Button>
)}
</div>
</div>
</CardContent>
</Card>
Expand Down Expand Up @@ -273,8 +333,11 @@ export default function EnvConfig() {
id="apiKey"
type={showApiKey ? 'text' : 'password'}
value={envConfig.apiKey}
onChange={(e) => setEnvConfig({ ...envConfig, apiKey: e.target.value })}
placeholder="sk-xxxxxxxxxxxxxxxxx"
onChange={(e) => {
setEnvConfig({ ...envConfig, apiKey: e.target.value })
setSensitiveDirty({ ...sensitiveDirty, apiKey: true })
}}
placeholder={sensitiveConfigured.apiKey ? '已配置;输入新值可替换' : 'sk-xxxxxxxxxxxxxxxxx'}
/>
<Button
onClick={() => setShowApiKey(!showApiKey)}
Expand All @@ -286,9 +349,16 @@ export default function EnvConfig() {
{showApiKey ? '隐藏' : '显示'}
</Button>
</div>
<p className="text-xs text-muted-foreground">
🔐 API密钥将被安全存储,请妥善保管
</p>
<div className="flex items-center justify-between gap-3">
<p className="text-xs text-muted-foreground">
{sensitiveConfigured.apiKey ? '已配置,页面不会读取或显示原值。' : '尚未配置远程 API Key。'}
</p>
{sensitiveConfigured.apiKey && (
<Button type="button" variant="outline" size="sm" disabled={saving} onClick={() => clearSensitiveConfig('API_KEY')}>
清除已保存值
</Button>
)}
</div>
</div>
</CardContent>
</Card>
Expand All @@ -300,9 +370,8 @@ export default function EnvConfig() {
<BiInfoCircle className="h-5 w-5 text-primary flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm text-foreground">
<strong className="font-semibold">提示:</strong> 这些环境变量将保存到{' '}
<code className="bg-primary/10 px-2 py-0.5 rounded text-primary font-mono text-xs">.env</code>{' '}
文件中。请勿将包含敏感信息的 .env 文件提交到版本控制系统。
<strong className="font-semibold">提示:</strong> 配置保存在本机项目数据库中。API Key 和 Webhook
只允许写入,页面只显示“是否已配置”,不会读取或回显原值。
</p>
</div>
</div>
Expand Down
3 changes: 2 additions & 1 deletion front/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"build": "next build",
"build:prod": "next build && node scripts/copy-dist.mjs",
"start": "node start-prod.mjs",
"lint": "eslint"
"lint": "eslint",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@radix-ui/react-label": "^2.1.7",
Expand Down
2 changes: 1 addition & 1 deletion front/server.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ module.exports = {
// 生产环境端口
port: 6866,
// 主机名
hostname: '0.0.0.0',
hostname: process.env.FRONTEND_HOST || '127.0.0.1',
},

// API 配置(如果需要在构建时使用)
Expand Down
2 changes: 1 addition & 1 deletion front/start-prod.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ delete require.cache[require.resolve(configPath)];
const config = require(configPath);

const port = config.production?.port || config.port || 6866;
const hostname = config.production?.hostname || '0.0.0.0';
const hostname = process.env.FRONTEND_HOST || config.production?.hostname || '127.0.0.1';
const outDir = path.resolve(__dirname, 'out');

const contentTypes = new Map([
Expand Down
3 changes: 3 additions & 0 deletions scripts/run_backend.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ foreach ($dir in @($DbDir, $DataDir, $OutputDir, $CacheDir, $LogDir, $ChromeProf
if (-not $env:SPRING_DATASOURCE_URL) {
$env:SPRING_DATASOURCE_URL = "jdbc:sqlite:$(Join-Path $DbDir 'getjobs.db')"
}
if (-not $env:SERVER_ADDRESS) {
$env:SERVER_ADDRESS = "127.0.0.1"
}
if (-not $env:APP_DATA_DIR) {
$env:APP_DATA_DIR = $DataDir
}
Expand Down
Loading
Loading