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 = "立即登录";
+ }
}
}
}