From 1c7da206ce7e0c19b39c66dc1b4aaf2a0449eafe Mon Sep 17 00:00:00 2001 From: HuangHai <10402852@qq.com> Date: Thu, 29 Jan 2026 09:00:56 +0800 Subject: [PATCH] 'commit' --- WordAddIn/WordAddIn/AiRibbon.cs | 1 + WordAddIn/WordAddIn/AiService.cs | 245 ++++++++++++++----------------- WordAddIn/WordAddIn/LoginForm.cs | 66 +++++++-- 3 files changed, 170 insertions(+), 142 deletions(-) diff --git a/WordAddIn/WordAddIn/AiRibbon.cs b/WordAddIn/WordAddIn/AiRibbon.cs index dc956bb..aec2a76 100644 --- a/WordAddIn/WordAddIn/AiRibbon.cs +++ b/WordAddIn/WordAddIn/AiRibbon.cs @@ -189,6 +189,7 @@ namespace WordAddIn if (login.ShowDialog() == DialogResult.OK) { _isLoggedIn = true; + AccessToken = login.Token; // 获取登录成功后的 Token SaveLoginState(); // 刷新 Ribbon 状态,使按钮变亮 diff --git a/WordAddIn/WordAddIn/AiService.cs b/WordAddIn/WordAddIn/AiService.cs index 4872a24..25f3805 100644 --- a/WordAddIn/WordAddIn/AiService.cs +++ b/WordAddIn/WordAddIn/AiService.cs @@ -339,6 +339,15 @@ namespace WordAddIn optimizedPrompt = await OptimizePromptForImage(prompt, style); } + // 检查登录状态 + if (string.IsNullOrEmpty(AiRibbon.AccessToken)) + { + throw new Exception("未登录,请先点击功能区的【登录】按钮获取权限。"); + } + + // 指向本地 FastAPI 后端代理 + string requestUrl = $"{Config.BackendBaseUrl}/api/image"; + // 使用匿名对象构造请求体,以获得最大灵活性 var requestBody = new { @@ -351,50 +360,63 @@ namespace WordAddIn var json = JsonConvert.SerializeObject(requestBody); var content = new StringContent(json, Encoding.UTF8, "application/json"); - var response = await _httpClient.PostAsync($"{Config.TuZiBaseUrl}/images/generations", content); - var responseString = await response.Content.ReadAsStringAsync(); - - if (!response.IsSuccessStatusCode) + // 创建请求并添加 Token + using (var request = new HttpRequestMessage(HttpMethod.Post, requestUrl)) { - throw new Exception($"API Error ({response.StatusCode}): {responseString}"); - } + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", AiRibbon.AccessToken); + request.Content = content; - // 使用 JObject 动态解析响应,以兼容不同厂商的 API 格式差异 - var jsonResponse = JObject.Parse(responseString); - - // 1. 尝试解析 OpenAI 标准格式: data[0].url - var data = jsonResponse["data"]; - if (data != null) - { - if (data.Type == JTokenType.Array && data.HasValues) + using (var response = await _httpClient.SendAsync(request)) { - var firstItem = data[0]; - // Gemini 3 image preview 可能会返回 revised_prompt 和 url - if (firstItem["url"] != null) return firstItem["url"].ToString(); - - // 有时它会返回 NO_IMAGE,这可能是模型拒绝了,或者格式不对 - if (firstItem["revised_prompt"] != null && firstItem["revised_prompt"].ToString() == "NO_IMAGE") + var responseString = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) { - // 尝试从其他字段找,或者这就是个错误 + if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + throw new Exception("认证失效,请重新登录。"); + } + throw new Exception($"后端服务错误 ({response.StatusCode}): {responseString}"); } - // 某些 API 可能直接在数组里放 URL 字符串 - if (firstItem.Type == JTokenType.String) return firstItem.ToString(); - } - // 2. 尝试解析 data 为直接字符串的情况 - else if (data.Type == JTokenType.String) - { - return data.ToString(); + // 使用 JObject 动态解析响应,以兼容不同厂商的 API 格式差异 + var jsonResponse = JObject.Parse(responseString); + + // 1. 尝试解析 OpenAI 标准格式: data[0].url + var data = jsonResponse["data"]; + if (data != null) + { + if (data.Type == JTokenType.Array && data.HasValues) + { + var firstItem = data[0]; + // Gemini 3 image preview 可能会返回 revised_prompt 和 url + if (firstItem["url"] != null) return firstItem["url"].ToString(); + + // 有时它会返回 NO_IMAGE,这可能是模型拒绝了,或者格式不对 + if (firstItem["revised_prompt"] != null && firstItem["revised_prompt"].ToString() == "NO_IMAGE") + { + // 尝试从其他字段找,或者这就是个错误 + } + + // 某些 API 可能直接在数组里放 URL 字符串 + if (firstItem.Type == JTokenType.String) return firstItem.ToString(); + } + // 2. 尝试解析 data 为直接字符串的情况 + else if (data.Type == JTokenType.String) + { + return data.ToString(); + } + } + + // 3. 尝试解析根节点 url (部分非标准 API) + if (jsonResponse["url"] != null) + { + return jsonResponse["url"].ToString(); + } + + throw new Exception($"无法从响应中解析图片 URL。原始响应: {responseString}"); } } - - // 3. 尝试解析根节点 url (部分非标准 API) - if (jsonResponse["url"] != null) - { - return jsonResponse["url"].ToString(); - } - - throw new Exception($"无法从响应中解析图片 URL。原始响应: {responseString}"); } catch (Exception ex) { @@ -424,90 +446,78 @@ namespace WordAddIn /// /// 通用聊天补全请求方法。 + /// 改为通过本地 Python FastAPI 后端代理请求。 /// private async Task GetChatCompletion(List messages, Action onProgress = null) { try { + // 检查登录状态 + if (string.IsNullOrEmpty(AiRibbon.AccessToken)) + { + throw new Exception("未登录,请先点击功能区的【登录】按钮获取权限。"); + } + // 获取当前选择的模型 string selectedModelId = Properties.Settings.Default.SelectedModel; - // 如果未设置,默认使用第一个 (qwen-plus) if (string.IsNullOrEmpty(selectedModelId)) selectedModelId = "qwen-plus"; var modelInfo = ModelConfig.GetModel(selectedModelId); - string apiKey = ""; - string requestUrl = ""; + // 指向本地 FastAPI 后端代理 + string requestUrl = "http://127.0.0.1:8000/api/chat"; + string accessToken = AiRibbon.AccessToken; string jsonContent = ""; - // 根据模型类型配置请求 - if (modelInfo.ApiType == "Native") // DashScope Native (e.g., qwen-plus) + // 统一构造 OpenAI 兼容格式请求体发送给后端 + // 后端负责根据模型类型转发给 DashScope 或其他服务 + var requestDict = new Dictionary { - apiKey = Config.DashScopeApiKey; - requestUrl = Config.DashScopeNativeUrl; - - // 构造 DashScope Native 格式请求体 - var requestBody = new - { - model = modelInfo.Id, - input = new - { - messages = messages - }, - parameters = new - { - result_format = "message", - incremental_output = (onProgress != null) // 如果有进度回调,则开启增量输出 - } - }; - jsonContent = JsonConvert.SerializeObject(requestBody); - } - else // Compatible (TuZi, DeepSeek, etc.) + { "model", modelInfo.Id }, + { "messages", messages }, + { "temperature", 0.7 } + }; + + // 注意:目前后端 Mock 暂时只支持非流式,但协议上保留 stream 字段 + if (onProgress != null) { - apiKey = Config.TuZiApiKey; - requestUrl = $"{Config.TuZiBaseUrl}/chat/completions"; - - // 构造 OpenAI 兼容格式请求体 - var requestDict = new Dictionary - { - { "model", modelInfo.Id }, - { "messages", messages }, - { "temperature", 0.7 } - }; - - if (onProgress != null) - { - requestDict["stream"] = true; - } - - // 针对 glm-4.7 开启思考模式 - if (modelInfo.Id == "glm-4.7") - { - requestDict["enable_thinking"] = true; - } - - jsonContent = JsonConvert.SerializeObject(requestDict); + requestDict["stream"] = true; } + // 针对 glm-4.7 开启思考模式 (透传给后端) + if (modelInfo.Id == "glm-4.7") + { + requestDict["enable_thinking"] = true; + } + + jsonContent = JsonConvert.SerializeObject(requestDict); + var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); - // 创建新的 HttpRequestMessage 以设置特定的 Authorization Header + // 创建新的 HttpRequestMessage using (var request = new HttpRequestMessage(HttpMethod.Post, requestUrl)) { - request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", apiKey); + // 使用登录获取的 Access Token + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); request.Content = content; - // 如果是流式请求,使用 ResponseHeadersRead var completionOption = onProgress != null ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead; using (var response = await _httpClient.SendAsync(request, completionOption)) { if (!response.IsSuccessStatusCode) { + if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + throw new Exception("认证失效,请重新登录。"); + } var errorString = await response.Content.ReadAsStringAsync(); - throw new Exception($"API Error ({response.StatusCode}): {errorString}"); + throw new Exception($"后端服务错误 ({response.StatusCode}): {errorString}"); } + // ... 后续处理保持不变,后端返回格式应兼容 OpenAI ... + + if (onProgress != null) { // 处理流式响应 @@ -549,38 +559,14 @@ namespace WordAddIn var json = JObject.Parse(dataStr); string contentDelta = ""; - if (modelInfo.ApiType == "Native") + // 统一处理:假设后端代理返回 OpenAI 兼容格式 + // 优先尝试获取 delta (流式) + contentDelta = json["choices"]?[0]?["delta"]?["content"]?.ToString(); + + // 如果没有 delta,尝试获取 message (非流式/完整包) + if (string.IsNullOrEmpty(contentDelta)) { - // Check for DashScope API Error in stream - if (json["code"] != null) - { - string errorCode = json["code"].ToString(); - string errorMsg = json["message"]?.ToString() ?? "Unknown error"; - try { System.IO.File.AppendAllText(@"C:\Users\Public\WordAddInLog.txt", $"{DateTime.Now} DashScope Stream Error: {errorCode} - {errorMsg}\n"); } catch { } - throw new Exception($"DashScope Error: {errorCode} - {errorMsg}"); - } - - // DashScope Native incremental output - // Priority 1: output.choices[0].message.content (Standard for result_format='message') - if (json["output"]?["choices"]?[0]?["message"]?["content"] != null) - { - contentDelta = json["output"]["choices"][0]["message"]["content"].ToString(); - } - // Priority 2: output.text (Standard for result_format='text' or older models) - else if (json["output"]?["text"] != null) - { - contentDelta = json["output"]["text"].ToString(); - } - // Priority 3: output.choices[0].delta.content (OpenAI compatible style) - else if (json["output"]?["choices"]?[0]?["delta"]?["content"] != null) - { - contentDelta = json["output"]["choices"][0]["delta"]["content"].ToString(); - } - } - else - { - // OpenAI Compatible - contentDelta = json["choices"]?[0]?["delta"]?["content"]?.ToString(); + contentDelta = json["choices"]?[0]?["message"]?["content"]?.ToString(); } if (!string.IsNullOrEmpty(contentDelta)) @@ -603,22 +589,15 @@ namespace WordAddIn // 非流式处理 var responseString = await response.Content.ReadAsStringAsync(); - if (modelInfo.ApiType == "Native") + // 统一按照 OpenAI 兼容格式解析 + try { - // 解析 DashScope Native 响应 - var json = JObject.Parse(responseString); - var contentText = json["output"]?["text"]?.ToString(); - if (contentText == null) - { - contentText = json["output"]?["choices"]?[0]?["message"]?["content"]?.ToString(); - } - return contentText ?? responseString; - } - else - { - // 解析 OpenAI 兼容响应 var result = JsonConvert.DeserializeObject(responseString); - return result?.choices?.FirstOrDefault()?.message?.content; + return result?.choices?.FirstOrDefault()?.message?.content ?? responseString; + } + catch + { + return responseString; } } } diff --git a/WordAddIn/WordAddIn/LoginForm.cs b/WordAddIn/WordAddIn/LoginForm.cs index 520d0f1..e772916 100644 --- a/WordAddIn/WordAddIn/LoginForm.cs +++ b/WordAddIn/WordAddIn/LoginForm.cs @@ -150,22 +150,70 @@ namespace WordAddIn /// 登录按钮点击事件。 /// 验证用户名和密码。 /// - private void BtnLogin_Click(object sender, EventArgs e) + private async void BtnLogin_Click(object sender, EventArgs e) { string user = txtUsername.Text.Trim(); string pass = txtPassword.Text.Trim(); - // 模拟验证逻辑 - if (user == "huanghai" && pass == "12345678") + if (string.IsNullOrEmpty(user) || string.IsNullOrEmpty(pass)) { - this.DialogResult = DialogResult.OK; - this.Close(); + MessageBox.Show("请输入用户名和密码!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; } - else + + btnLogin.Enabled = false; + btnLogin.Text = "登录中..."; + + try { - MessageBox.Show("用户名或密码错误!\n(默认账号: huanghai, 密码: 12345678)", "登录失败", MessageBoxButtons.OK, MessageBoxIcon.Error); - txtPassword.Focus(); - txtPassword.SelectAll(); + using (var client = new HttpClient()) + { + // 指向本地 FastAPI 后端 + string url = $"{Config.BackendBaseUrl}/token"; + + var content = new FormUrlEncodedContent(new[] + { + new KeyValuePair("username", user), + new KeyValuePair("password", pass), + new KeyValuePair("grant_type", "password") + }); + + var response = await client.PostAsync(url, content); + + if (response.IsSuccessStatusCode) + { + string jsonResponse = await response.Content.ReadAsStringAsync(); + var json = JObject.Parse(jsonResponse); + this.Token = json["access_token"]?.ToString(); + + if (!string.IsNullOrEmpty(this.Token)) + { + this.DialogResult = DialogResult.OK; + this.Close(); + } + else + { + MessageBox.Show("登录失败:未能获取有效的 Token。", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + else + { + string errorDetail = await response.Content.ReadAsStringAsync(); + MessageBox.Show($"登录失败:{response.ReasonPhrase}\n{errorDetail}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } + catch (Exception ex) + { + MessageBox.Show($"连接服务器失败:{ex.Message}\n请确保后端服务已启动 (http://127.0.0.1:8000)", "网络错误", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + if (!this.IsDisposed) + { + btnLogin.Enabled = true; + btnLogin.Text = "立即登录"; + } } } }