feat: AI岗位专区 — 5个AI岗位置顶 + 首页分组展示

- schema: HotPosition 新增 category 字段 (ai/traditional)
- positions: 5 AI岗位 (AI算法/大模型应用/Prompt/AI产品/AI运维) + 7传统岗位
- frontend: 首页拆分 "🔥 AI热门岗位" 置顶高亮 + "更多岗位" 折叠
- ai服务: 新增 primaryFallbackModel (sensenova-6.7-flash-lite) 降级链路
This commit is contained in:
wlt
2026-06-17 13:57:18 +08:00
parent a5c4bcb821
commit 103dbd3b34
4 changed files with 116 additions and 30 deletions
+26 -17
View File
@@ -1,6 +1,6 @@
import { Injectable, Logger } from '@nestjs/common'
import axios from 'axios'
import https from 'https'
import { Injectable, Logger } from "@nestjs/common"
import axios from "axios"
import https from "https"
interface AiCallOptions {
systemPrompt: string
@@ -15,26 +15,35 @@ const httpAgent = new https.Agent({ rejectUnauthorized: true, keepAlive: true })
export class AiService {
private readonly logger = new Logger(AiService.name)
private readonly primaryUrl = process.env.AI_PRIMARY_URL || 'https://token.sensenova.cn/v1'
private readonly primaryKey = process.env.AI_PRIMARY_KEY || ''
private readonly primaryModel = process.env.AI_PRIMARY_MODEL || 'deepseek-v4-flash'
private readonly primaryUrl = process.env.AI_PRIMARY_URL || "https://token.sensenova.cn/v1"
private readonly primaryKey = process.env.AI_PRIMARY_KEY || ""
private readonly primaryModel = process.env.AI_PRIMARY_MODEL || "deepseek-v4-flash"
private readonly primaryFallbackModel = process.env.AI_PRIMARY_FALLBACK_MODEL || "sensenova-6.7-flash-lite"
private readonly backupUrl = process.env.AI_BACKUP_URL || 'https://integrate.api.nvidia.com/v1'
private readonly backupKey = process.env.AI_BACKUP_KEY || ''
private readonly backupModel = process.env.AI_BACKUP_MODEL || 'stepfun-ai/step-3.5-flash'
private readonly backupUrl = process.env.AI_BACKUP_URL || "https://integrate.api.nvidia.com/v1"
private readonly backupKey = process.env.AI_BACKUP_KEY || ""
private readonly backupModel = process.env.AI_BACKUP_MODEL || "stepfun-ai/step-3.5-flash"
async call(options: AiCallOptions): Promise<string> {
const { systemPrompt, userMessage, temperature = 0.7, maxTokens = 2048 } = options
// Try primary AI
// Try primary AI (deepseek-v4-flash on sensenova)
try {
const result = await this.callApi(this.primaryUrl, this.primaryKey, this.primaryModel, systemPrompt, userMessage, temperature, maxTokens)
if (result) return result
} catch (e) {
this.logger.warn(`Primary AI failed: ${(e as Error).message}, trying backup...`)
this.logger.warn(`Primary AI failed: ${(e as Error).message}, trying primary fallback...`)
}
// Try backup AI
// Try primary fallback model (sensenova-6.7-flash-lite, same provider)
try {
const result = await this.callApi(this.primaryUrl, this.primaryKey, this.primaryFallbackModel, systemPrompt, userMessage, temperature, maxTokens)
if (result) return result
} catch (e) {
this.logger.warn(`Primary fallback AI also failed: ${(e as Error).message}, trying backup...`)
}
// Try backup AI (NVIDIA)
try {
const result = await this.callApi(this.backupUrl, this.backupKey, this.backupModel, systemPrompt, userMessage, temperature, maxTokens)
if (result) return result
@@ -43,7 +52,7 @@ export class AiService {
}
// Final fallback
throw new Error('AI 服务暂时不可用,请稍后重试')
throw new Error("AI \u670d\u52a1\u6682\u65f6\u4e0d\u53ef\u7528\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5")
}
private async callApi(
@@ -56,16 +65,16 @@ export class AiService {
{
model,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userMessage },
{ role: "system", content: systemPrompt },
{ role: "user", content: userMessage },
],
temperature,
max_tokens: maxTokens,
},
{
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
timeout: 60000,
httpsAgent: httpAgent,
@@ -1,20 +1,22 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
import { Document } from 'mongoose'
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"
import { Document } from "mongoose"
export type HotPositionDocument = HotPosition & Document
export type PositionCategory = "ai" | "traditional"
@Schema({ timestamps: true })
export class HotPosition {
@Prop({ required: true })
name: string
@Prop({ default: '' })
@Prop({ default: "" })
salary?: string
@Prop({ default: '' })
@Prop({ default: "" })
company?: string
@Prop({ default: '' })
@Prop({ default: "" })
icon?: string
@Prop({ default: 0 })
@@ -22,7 +24,11 @@ export class HotPosition {
@Prop({ default: true })
active: boolean
@Prop({ type: String, enum: ["ai", "traditional"], default: "traditional" })
category: PositionCategory
}
export const HotPositionSchema = SchemaFactory.createForClass(HotPosition)
HotPositionSchema.index({ sort: 1 })
HotPositionSchema.index({ category: 1, active: 1 })