Lyyyi 2 дней назад
Родитель
Сommit
e705e7b83c

+ 45 - 0
OASystem/OASystem.Api/Controllers/AITestController.cs

@@ -7,6 +7,7 @@ using OASystem.API.OAMethodLib;
 using OASystem.API.OAMethodLib.DeepSeekAPI;
 using OASystem.API.OAMethodLib.DoubaoAPI;
 using OASystem.API.OAMethodLib.Hotmail;
+using OASystem.API.OAMethodLib.KiMiApi;
 using OASystem.API.OAMethodLib.MicrosoftGraphMailbox;
 using OASystem.API.OAMethodLib.QiYeWeChatAPI;
 using OASystem.API.OAMethodLib.Quartz.Business;
@@ -277,6 +278,50 @@ namespace OASystem.API.Controllers
 
         #endregion
 
+        #region Kimi AI
+
+        /// <summary>
+        /// Kimi (Moonshot) 通用对话。默认用于"中文→英文"翻译(如中文国家名转英文国家名)。
+        /// </summary>
+        /// <param name="request">对话请求</param>
+        [HttpPost("kimi-chat")]
+        public async Task<IActionResult> KimiChat([FromBody] KimiChatRequest request)
+        {
+            if (request == null || string.IsNullOrWhiteSpace(request.Text))
+                return BadRequest(new { success = false, error = "Text 不能为空" });
+
+            try
+            {
+                var messages = new List<SeedMessages>
+                {
+                    new SeedMessages { Role = KimiRole.system, Content = "你是一个专业翻译,请将输入的中文国家/地区名称翻译成对应的英文国家/地区名称。只输出翻译结果本身,不要输出任何解释、引号或多余字符。" },
+                    new SeedMessages { Role = KimiRole.user, Content = request.Text.Trim() }
+                };
+
+                var result = await new KiMiApiClient().SeedMessageByFullConterObject(messages);
+                var content = result?.Choices?.FirstOrDefault()?.Message?.Content;
+
+                return Ok(new
+                {
+                    success = true,
+                    data = content,
+                    model = result?.Model
+                });
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "调用 Kimi API 失败。");
+                return StatusCode(500, new { success = false, error = "调用 Kimi API 失败", detail = ex.Message });
+            }
+        }
+
+        public class KimiChatRequest
+        {
+            public string Text { get; set; } = string.Empty;
+        }
+
+        #endregion
+
         #region 腾讯 TokenHub AI
         /// <summary>
         /// 简单对话

Разница между файлами не показана из-за своего большого размера
+ 680 - 223
OASystem/OASystem.Api/Controllers/ResourceController.cs


+ 42 - 0
OASystem/OASystem.Api/OAMethodLib/SnovioAPI/SnovioIndustryMapper.cs

@@ -0,0 +1,42 @@
+namespace OASystem.API.OAMethodLib.SnovioAPI;
+
+/// <summary>
+/// 词条中文行业 → Snovio 英文行业映射。
+/// Snovio 对 industries 取值做严格校验(未知值直接 422),中文原词不可直透;
+/// 下表英文值已逐个经 Snovio 实测建任务通过(202),未知中文行业直接丢弃以保召回。
+/// </summary>
+public static class SnovioIndustryMapper
+{
+    private static readonly Dictionary<string, List<string>> _map = new(StringComparer.OrdinalIgnoreCase)
+    {
+        ["工业与制造业领域"] = new() { "Machinery", "Industrial Automation" },
+        ["能源与资源领域"] = new() { "Oil & Energy" },
+        ["交通运输与物流领域"] = new() { "Logistics & Supply Chain" },
+        ["城乡建设与规划领域"] = new() { "Construction" },
+        ["生态环境与水务领域"] = new() { "Environmental Services" },
+        ["农业与食品领域"] = new() { "Farming" },
+        ["金融与商贸领域"] = new() { "Financial Services" },
+        ["医药健康与康养领域"] = new() { "Hospital & Health Care" },
+        ["文化旅游与体育领域"] = new() { "Leisure, Travel & Tourism" },
+        ["信息科技与数字经济领域"] = new() { "Computer Software" },
+        ["教育与科技领域"] = new() { "Higher Education" },
+        ["社会服务与公共管理领域"] = new() { "Government Administration" },
+        // “其他重点领域”无对应,丢弃(不传行业条件)。
+    };
+
+    /// <summary>
+    /// 将词条中文行业集合转为 Snovio 英文行业集合(去重去空,未映射的丢弃)。
+    /// </summary>
+    public static List<string> MapToSnovio(IEnumerable<string>? industries)
+    {
+        var result = new List<string>();
+        if (industries == null) return result;
+        foreach (var industry in industries)
+        {
+            if (string.IsNullOrWhiteSpace(industry)) continue;
+            if (_map.TryGetValue(industry.Trim(), out var mapped))
+                result.AddRange(mapped);
+        }
+        return result.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
+    }
+}

+ 83 - 2
OASystem/OASystem.Api/OAMethodLib/SnovioAPI/SnovioModels.cs

@@ -111,10 +111,11 @@ public sealed class SnovioCompanyFilter
     public SnovioStringListFilter? Industries { get; set; }
 
     /// <summary>
-    /// 公司规模,例如 1-10、51-200、10001+。
+    /// 公司规模数组,例如 ["1-10", "51-200", "10001+", "Self"]。
+    /// Snovio 要求数组格式,单值也请传单元素数组。
     /// </summary>
     [JsonPropertyName("size")]
-    public string? Size { get; set; }
+    public List<string>? Size { get; set; }
 
     /// <summary>
     /// 公司营收最小值和最大值,单位由 Snov.io 数据源定义。
@@ -305,7 +306,9 @@ public sealed class SnovioCompanySearchResultResponse
 
 /// <summary>
 /// 公司搜索结果数据。
+/// 兼容 Snov.io 任务处理中时 data 为空数组的响应。
 /// </summary>
+[System.Text.Json.Serialization.JsonConverter(typeof(SnovioCompanySearchResultDataConverter))]
 public sealed class SnovioCompanySearchResultData
 {
     /// <summary>
@@ -333,6 +336,84 @@ public sealed class SnovioCompanySearchResultData
     public List<SnovioCompanySearchCompany> Companies { get; set; } = new();
 }
 
+/// <summary>
+/// 兼容 Snov.io 公司搜索结果中 data 为对象、空数组或 null 的响应转换器。
+/// 任务处理中(in_progress)时 Snovio 返回 "data": [],此时按空结果处理,由轮询继续等待。
+/// </summary>
+internal sealed class SnovioCompanySearchResultDataConverter
+    : System.Text.Json.Serialization.JsonConverter<SnovioCompanySearchResultData>
+{
+    public override SnovioCompanySearchResultData Read(
+        ref Utf8JsonReader reader,
+        Type typeToConvert,
+        JsonSerializerOptions options)
+    {
+        if (reader.TokenType == JsonTokenType.Null)
+            return new SnovioCompanySearchResultData();
+
+        if (reader.TokenType == JsonTokenType.StartArray)
+        {
+            // 处理中返回空数组,直接跳过。
+            using var _ = JsonDocument.ParseValue(ref reader);
+            return new SnovioCompanySearchResultData();
+        }
+
+        if (reader.TokenType == JsonTokenType.StartObject)
+        {
+            using var document = JsonDocument.ParseValue(ref reader);
+            var root = document.RootElement;
+            var result = new SnovioCompanySearchResultData();
+
+            if (root.TryGetProperty("total", out var totalElement) &&
+                totalElement.ValueKind == JsonValueKind.Number &&
+                totalElement.TryGetInt64(out var total))
+            {
+                result.Total = total;
+            }
+
+            if (root.TryGetProperty("page", out var pageElement) &&
+                pageElement.ValueKind == JsonValueKind.Number &&
+                pageElement.TryGetInt32(out var page))
+            {
+                result.Page = page;
+            }
+
+            if (root.TryGetProperty("total_pages", out var totalPagesElement) &&
+                totalPagesElement.ValueKind == JsonValueKind.Number &&
+                totalPagesElement.TryGetInt32(out var totalPages))
+            {
+                result.TotalPages = totalPages;
+            }
+
+            if (root.TryGetProperty("companies", out var companiesElement) &&
+                companiesElement.ValueKind == JsonValueKind.Array)
+            {
+                result.Companies = companiesElement.Deserialize<List<SnovioCompanySearchCompany>>(options)
+                                   ?? new List<SnovioCompanySearchCompany>();
+            }
+
+            return result;
+        }
+
+        throw new System.Text.Json.JsonException(
+            "Snov.io 公司搜索结果响应中的 data 必须是对象、数组或 null。");
+    }
+
+    public override void Write(
+        Utf8JsonWriter writer,
+        SnovioCompanySearchResultData value,
+        JsonSerializerOptions options)
+    {
+        writer.WriteStartObject();
+        writer.WriteNumber("total", value.Total);
+        writer.WriteNumber("page", value.Page);
+        writer.WriteNumber("total_pages", value.TotalPages);
+        writer.WritePropertyName("companies");
+        System.Text.Json.JsonSerializer.Serialize(writer, value.Companies, options);
+        writer.WriteEndObject();
+    }
+}
+
 /// <summary>
 /// Snov.io 公司搜索结果中的公司信息。
 /// </summary>

+ 2 - 3
OASystem/OASystem.Api/OAMethodLib/SnovioAPI/SnovioService.cs

@@ -672,9 +672,8 @@ public sealed class SnovioService : ISnovioService
         AddStringList(formFields, "filters[company][industries][include]", company.Industries?.Include);
         AddStringList(formFields, "filters[company][industries][exclude]", company.Industries?.Exclude);
         AddStringList(formFields, "filters[company][specialities]", company.Specialities);
-
-        if (!string.IsNullOrWhiteSpace(company.Size))
-            formFields["filters[company][size]"] = company.Size.Trim();
+        // Snovio 要求 size 为数组,以 filters[company][size][i] 形式发送。
+        AddStringList(formFields, "filters[company][size]", company.Size);
 
         if (company.Revenue?.Min is { } revenueMin)
             formFields["filters[company][revenue][min]"] = revenueMin.ToString(CultureInfo.InvariantCulture);

+ 7 - 2
OASystem/OASystem.Domain/Entities/Resource/Res_InvitationAI.cs

@@ -271,11 +271,16 @@ namespace OASystem.Domain.Entities.Resource
 
         public static List<OrgScale> BuildInitialData() => new()
         {
+            // 与 Snovio 公司规模档位对齐(见 Snovio API 文档 DatabaseSearch filters.company.size),Self 保持原样
+            new() { Name = "Self", MinStaff = 1, MaxStaff = 1 },
             new() { Name = "微型 (1-10人)", MinStaff = 1, MaxStaff = 10 },
             new() { Name = "小型 (11-50人)", MinStaff = 11, MaxStaff = 50 },
             new() { Name = "中型 (51-200人)", MinStaff = 51, MaxStaff = 200 },
-            new() { Name = "大型 (201-1000人)", MinStaff = 201, MaxStaff = 1000 },
-            new() { Name = "超大型 (1000人以上)", MinStaff = 1001, MaxStaff = int.MaxValue }
+            new() { Name = "中大型 (201-500人)", MinStaff = 201, MaxStaff = 500 },
+            new() { Name = "大型 (501-1000人)", MinStaff = 501, MaxStaff = 1000 },
+            new() { Name = "超大型 (1001-5000人)", MinStaff = 1001, MaxStaff = 5000 },
+            new() { Name = "特大型 (5001-10000人)", MinStaff = 5001, MaxStaff = 10000 },
+            new() { Name = "巨型 (10001人以上)", MinStaff = 10001, MaxStaff = int.MaxValue }
         };
 
         /// <summary>

Некоторые файлы не были показаны из-за большого количества измененных файлов