首次提交:初始化项目代码
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"resume-platform/pkg/config"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DeepSeekProvider DeepSeek AI 供应商实现
|
||||
type DeepSeekProvider struct {
|
||||
cfg *config.DeepSeekConfig // DeepSeek 配置
|
||||
httpClient *http.Client // HTTP 客户端
|
||||
}
|
||||
|
||||
// NewDeepSeekProvider 创建 DeepSeek 供应商实例
|
||||
// @param cfg DeepSeek 配置
|
||||
// @return *DeepSeekProvider 供应商实例
|
||||
// @author sunct
|
||||
func NewDeepSeekProvider(cfg *config.DeepSeekConfig) *DeepSeekProvider {
|
||||
return &DeepSeekProvider{
|
||||
cfg: cfg,
|
||||
httpClient: &http.Client{
|
||||
Timeout: time.Duration(cfg.Timeout) * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 获取供应商名称
|
||||
// @return string 供应商标识
|
||||
// @author sunct
|
||||
func (p *DeepSeekProvider) GetName() string {
|
||||
return "deepseek"
|
||||
}
|
||||
|
||||
// Generate 调用 DeepSeek API 生成文本
|
||||
// @param ctx 上下文
|
||||
// @param prompt 提示词
|
||||
// @param opts 可选参数
|
||||
// @return string 生成结果
|
||||
// @return error 调用错误
|
||||
// @author sunct
|
||||
func (p *DeepSeekProvider) Generate(ctx context.Context, prompt string, opts ...Option) (string, error) {
|
||||
options := &Options{
|
||||
MaxTokens: p.cfg.MaxTokens,
|
||||
Temperature: 0.7,
|
||||
TopP: 0.9,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"model": p.getModelName(),
|
||||
"messages": []map[string]interface{}{
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt,
|
||||
},
|
||||
},
|
||||
"max_tokens": options.MaxTokens,
|
||||
"temperature": options.Temperature,
|
||||
"top_p": options.TopP,
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
req, err := http.NewRequest("POST", p.cfg.BaseURL+"/chat/completions", bytes.NewBuffer(jsonBody))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+p.cfg.APIKey)
|
||||
|
||||
resp, err := p.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return string(respBody), nil
|
||||
}
|
||||
|
||||
if choices, ok := result["choices"].([]interface{}); ok && len(choices) > 0 {
|
||||
if choice, ok := choices[0].(map[string]interface{}); ok {
|
||||
if message, ok := choice["message"].(map[string]interface{}); ok {
|
||||
if content, ok := message["content"].(string); ok {
|
||||
return content, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return string(respBody), nil
|
||||
}
|
||||
|
||||
// getModelName 获取模型名称,未配置时使用默认 deepseek-chat
|
||||
// @return string 模型名
|
||||
// @author sunct
|
||||
func (p *DeepSeekProvider) getModelName() string {
|
||||
if p.cfg.Model != "" {
|
||||
return p.cfg.Model
|
||||
}
|
||||
return "deepseek-chat"
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"resume-platform/pkg/config"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DoubaoProvider 豆包 AI 供应商实现
|
||||
type DoubaoProvider struct {
|
||||
cfg *config.DoubaoConfig // 豆包配置
|
||||
httpClient *http.Client // HTTP 客户端
|
||||
}
|
||||
|
||||
// NewDoubaoProvider 创建豆包供应商实例
|
||||
// @param cfg 豆包配置
|
||||
// @return *DoubaoProvider 供应商实例
|
||||
// @author sunct
|
||||
func NewDoubaoProvider(cfg *config.DoubaoConfig) *DoubaoProvider {
|
||||
return &DoubaoProvider{
|
||||
cfg: cfg,
|
||||
httpClient: &http.Client{
|
||||
Timeout: time.Duration(cfg.Timeout) * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 获取供应商名称
|
||||
// @return string 供应商标识
|
||||
// @author sunct
|
||||
func (p *DoubaoProvider) GetName() string {
|
||||
return "doubao"
|
||||
}
|
||||
|
||||
// Generate 调用豆包 API 生成文本
|
||||
// @param ctx 上下文
|
||||
// @param prompt 提示词
|
||||
// @param opts 可选参数
|
||||
// @return string 生成结果
|
||||
// @return error 调用错误
|
||||
// @author sunct
|
||||
func (p *DoubaoProvider) Generate(ctx context.Context, prompt string, opts ...Option) (string, error) {
|
||||
options := &Options{
|
||||
MaxTokens: p.cfg.MaxTokens,
|
||||
Temperature: 0.7,
|
||||
TopP: 0.9,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"model": p.getModelName(),
|
||||
"messages": []map[string]interface{}{
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt,
|
||||
},
|
||||
},
|
||||
"max_tokens": options.MaxTokens,
|
||||
"temperature": options.Temperature,
|
||||
"top_p": options.TopP,
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
req, err := http.NewRequest("POST", p.cfg.BaseURL+"/chat/completions", bytes.NewBuffer(jsonBody))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+p.cfg.APIKey)
|
||||
|
||||
resp, err := p.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return string(respBody), nil
|
||||
}
|
||||
|
||||
if choices, ok := result["choices"].([]interface{}); ok && len(choices) > 0 {
|
||||
if choice, ok := choices[0].(map[string]interface{}); ok {
|
||||
if message, ok := choice["message"].(map[string]interface{}); ok {
|
||||
if content, ok := message["content"].(string); ok {
|
||||
return content, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return string(respBody), nil
|
||||
}
|
||||
|
||||
// getModelName 获取模型名称,未配置时使用默认 Doubao
|
||||
// @return string 模型名
|
||||
// @author sunct
|
||||
func (p *DoubaoProvider) getModelName() string {
|
||||
if p.cfg.Model != "" {
|
||||
return p.cfg.Model
|
||||
}
|
||||
return "Doubao"
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Package ai AI 供应商适配层,定义统一的 LLM 调用接口
|
||||
// 支持通义千问、DeepSeek、豆包、讯飞星火等多家大模型
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"resume-platform/pkg/config"
|
||||
)
|
||||
|
||||
// Provider AI 服务提供者接口,所有大模型供应商需实现此接口
|
||||
type Provider interface {
|
||||
// Generate 调用大模型生成文本
|
||||
Generate(ctx context.Context, prompt string, opts ...Option) (string, error)
|
||||
// GetName 获取供应商名称
|
||||
GetName() string
|
||||
}
|
||||
|
||||
// Option 调用选项函数类型,用于可选参数配置
|
||||
type Option func(*Options)
|
||||
|
||||
// Options AI 调用可选参数
|
||||
type Options struct {
|
||||
MaxTokens int // 最大生成token数
|
||||
Temperature float64 // 温度(随机性)
|
||||
TopP float64 // 核采样参数
|
||||
}
|
||||
|
||||
// WithMaxTokens 设置最大生成 token 数
|
||||
// @param maxTokens 最大token数
|
||||
// @return Option 选项函数
|
||||
// @author sunct
|
||||
func WithMaxTokens(maxTokens int) Option {
|
||||
return func(o *Options) {
|
||||
o.MaxTokens = maxTokens
|
||||
}
|
||||
}
|
||||
|
||||
// WithTemperature 设置生成温度
|
||||
// @param temp 温度值(0~1)
|
||||
// @return Option 选项函数
|
||||
// @author sunct
|
||||
func WithTemperature(temp float64) Option {
|
||||
return func(o *Options) {
|
||||
o.Temperature = temp
|
||||
}
|
||||
}
|
||||
|
||||
// WithTopP 设置核采样参数
|
||||
// @param topP 核采样值
|
||||
// @return Option 选项函数
|
||||
// @author sunct
|
||||
func WithTopP(topP float64) Option {
|
||||
return func(o *Options) {
|
||||
o.TopP = topP
|
||||
}
|
||||
}
|
||||
|
||||
// NewProvider AI 供应商工厂函数,根据配置创建对应供应商实例
|
||||
// @param cfg AI 配置
|
||||
// @return Provider 供应商实例
|
||||
// @return error 创建错误
|
||||
// @author sunct
|
||||
func NewProvider(cfg *config.AIConfig) (Provider, error) {
|
||||
switch cfg.Provider {
|
||||
case "spark":
|
||||
return NewSparkProvider(&cfg.Spark), nil
|
||||
case "tongyi":
|
||||
return NewTongyiProvider(&cfg.Tongyi), nil
|
||||
case "doubao":
|
||||
return NewDoubaoProvider(&cfg.Doubao), nil
|
||||
case "deepseek":
|
||||
return NewDeepSeekProvider(&cfg.DeepSeek), nil
|
||||
default:
|
||||
return NewSparkProvider(&cfg.Spark), nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"resume-platform/pkg/config"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type SparkProvider struct {
|
||||
cfg *config.SparkConfig
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewSparkProvider(cfg *config.SparkConfig) *SparkProvider {
|
||||
return &SparkProvider{
|
||||
cfg: cfg,
|
||||
httpClient: &http.Client{
|
||||
Timeout: time.Duration(cfg.Timeout) * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SparkProvider) GetName() string {
|
||||
return "spark"
|
||||
}
|
||||
|
||||
func (p *SparkProvider) Generate(ctx context.Context, prompt string, opts ...Option) (string, error) {
|
||||
options := &Options{
|
||||
MaxTokens: p.cfg.MaxTokens,
|
||||
Temperature: 0.7,
|
||||
TopP: 0.9,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
}
|
||||
|
||||
wsURL, err := p.buildAuthURL()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return p.sendWebSocketRequest(ctx, wsURL, prompt, options)
|
||||
}
|
||||
|
||||
func (p *SparkProvider) buildAuthURL() (string, error) {
|
||||
parsedURL, err := url.Parse(p.cfg.BaseURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
host := parsedURL.Host
|
||||
path := parsedURL.Path
|
||||
now := time.Now()
|
||||
date := now.UTC().Format(time.RFC1123)
|
||||
|
||||
signatureOrigin := fmt.Sprintf("host: %s\ndate: %s\nGET %s HTTP/1.1", host, date, path)
|
||||
|
||||
mac := hmac.New(sha256.New, []byte(p.cfg.APISecret))
|
||||
mac.Write([]byte(signatureOrigin))
|
||||
signatureSha := base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
||||
|
||||
authorizationOrigin := fmt.Sprintf(
|
||||
`api_key="%s", algorithm="%s", headers="%s", signature="%s"`,
|
||||
p.cfg.APIKey, "hmac-sha256", "host date request-line", signatureSha,
|
||||
)
|
||||
|
||||
authorization := base64.StdEncoding.EncodeToString([]byte(authorizationOrigin))
|
||||
|
||||
query := url.Values{}
|
||||
query.Set("authorization", authorization)
|
||||
query.Set("date", date)
|
||||
query.Set("host", host)
|
||||
|
||||
return p.cfg.BaseURL + "?" + query.Encode(), nil
|
||||
}
|
||||
|
||||
func (p *SparkProvider) sendWebSocketRequest(ctx context.Context, wsURL string, prompt string, options *Options) (string, error) {
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to dial websocket: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
done := make(chan struct{})
|
||||
var result strings.Builder
|
||||
var lastErr error
|
||||
|
||||
go func() {
|
||||
defer close(done)
|
||||
for {
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsCloseError(err, websocket.CloseNormalClosure) {
|
||||
return
|
||||
}
|
||||
lastErr = fmt.Errorf("failed to read message: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal(message, &resp); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if header, ok := resp["header"].(map[string]interface{}); ok {
|
||||
if code, ok := header["code"].(float64); ok && code != 0 {
|
||||
if msg, ok := header["message"].(string); ok {
|
||||
lastErr = fmt.Errorf("API error: %s (code: %d)", msg, int(code))
|
||||
} else {
|
||||
lastErr = fmt.Errorf("API error with code: %d", int(code))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if payload, ok := resp["payload"].(map[string]interface{}); ok {
|
||||
if choices, ok := payload["choices"].(map[string]interface{}); ok {
|
||||
if text, ok := choices["text"].([]interface{}); ok && len(text) > 0 {
|
||||
if item, ok := text[0].(map[string]interface{}); ok {
|
||||
if content, ok := item["content"].(string); ok {
|
||||
result.WriteString(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
if status, ok := choices["status"].(float64); ok && status == 2 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
body := map[string]interface{}{
|
||||
"header": map[string]interface{}{
|
||||
"app_id": p.cfg.APPID,
|
||||
"uid": "resume-platform",
|
||||
},
|
||||
"parameter": map[string]interface{}{
|
||||
"chat": map[string]interface{}{
|
||||
"domain": p.getDomainName(),
|
||||
"temperature": options.Temperature,
|
||||
"max_tokens": options.MaxTokens,
|
||||
"top_k": 4,
|
||||
"web_search": p.cfg.WebSearch,
|
||||
"show_ref_label": p.cfg.ShowRefLabel,
|
||||
},
|
||||
},
|
||||
"payload": map[string]interface{}{
|
||||
"message": map[string]interface{}{
|
||||
"text": []map[string]interface{}{
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
if err := conn.WriteMessage(websocket.TextMessage, jsonBody); err != nil {
|
||||
return "", fmt.Errorf("failed to write message: %w", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
if lastErr != nil {
|
||||
return "", lastErr
|
||||
}
|
||||
return result.String(), nil
|
||||
case <-ctx.Done():
|
||||
conn.Close()
|
||||
return "", ctx.Err()
|
||||
case <-time.After(time.Duration(p.cfg.Timeout) * time.Second):
|
||||
conn.Close()
|
||||
return "", fmt.Errorf("request timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SparkProvider) getDomainName() string {
|
||||
switch p.cfg.Model {
|
||||
case "lite":
|
||||
return "lite"
|
||||
case "pro":
|
||||
return "generalv3"
|
||||
case "pro-128k":
|
||||
return "pro-128k"
|
||||
case "max":
|
||||
return "generalv3.5"
|
||||
case "max-32k":
|
||||
return "max-32k"
|
||||
case "ultra":
|
||||
return "4.0Ultra"
|
||||
case "kjwx":
|
||||
return "kjwx"
|
||||
default:
|
||||
return "lite"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"resume-platform/pkg/config"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TongyiProvider 通义千问 AI 供应商实现
|
||||
type TongyiProvider struct {
|
||||
cfg *config.TongyiConfig // 通义千问配置
|
||||
httpClient *http.Client // HTTP 客户端
|
||||
}
|
||||
|
||||
// NewTongyiProvider 创建通义千问供应商实例
|
||||
// @param cfg 通义千问配置
|
||||
// @return *TongyiProvider 供应商实例
|
||||
// @author sunct
|
||||
func NewTongyiProvider(cfg *config.TongyiConfig) *TongyiProvider {
|
||||
return &TongyiProvider{
|
||||
cfg: cfg,
|
||||
httpClient: &http.Client{
|
||||
Timeout: time.Duration(cfg.Timeout) * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 获取供应商名称
|
||||
// @return string 供应商标识
|
||||
// @author sunct
|
||||
func (p *TongyiProvider) GetName() string {
|
||||
return "tongyi"
|
||||
}
|
||||
|
||||
// Generate 调用通义千问 API 生成文本
|
||||
// @param ctx 上下文
|
||||
// @param prompt 提示词
|
||||
// @param opts 可选参数
|
||||
// @return string 生成结果
|
||||
// @return error 调用错误
|
||||
// @author sunct
|
||||
func (p *TongyiProvider) Generate(ctx context.Context, prompt string, opts ...Option) (string, error) {
|
||||
options := &Options{
|
||||
MaxTokens: p.cfg.MaxTokens,
|
||||
Temperature: 0.7,
|
||||
TopP: 0.9,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"model": p.getModelName(),
|
||||
"messages": []map[string]interface{}{
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt,
|
||||
},
|
||||
},
|
||||
"max_tokens": options.MaxTokens,
|
||||
"temperature": options.Temperature,
|
||||
"top_p": options.TopP,
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
req, err := http.NewRequest("POST", p.cfg.BaseURL+"/chat/completions", bytes.NewBuffer(jsonBody))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+p.cfg.APIKey)
|
||||
|
||||
resp, err := p.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return string(respBody), nil
|
||||
}
|
||||
|
||||
if choices, ok := result["choices"].([]interface{}); ok && len(choices) > 0 {
|
||||
if choice, ok := choices[0].(map[string]interface{}); ok {
|
||||
if message, ok := choice["message"].(map[string]interface{}); ok {
|
||||
if content, ok := message["content"].(string); ok {
|
||||
return content, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return string(respBody), nil
|
||||
}
|
||||
|
||||
// getModelName 获取模型名称,未配置时使用默认 qwen-turbo
|
||||
// @return string 模型名
|
||||
// @author sunct
|
||||
func (p *TongyiProvider) getModelName() string {
|
||||
if p.cfg.Model != "" {
|
||||
return p.cfg.Model
|
||||
}
|
||||
return "qwen-turbo"
|
||||
}
|
||||
Reference in New Issue
Block a user