This commit is contained in:
HuangHai
2026-01-29 09:00:56 +08:00
parent 8101a3eafc
commit 1c7da206ce
3 changed files with 170 additions and 142 deletions

View File

@@ -189,6 +189,7 @@ namespace WordAddIn
if (login.ShowDialog() == DialogResult.OK)
{
_isLoggedIn = true;
AccessToken = login.Token; // 获取登录成功后的 Token
SaveLoginState();
// 刷新 Ribbon 状态,使按钮变亮

View File

@@ -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
/// <summary>
/// 通用聊天补全请求方法。
/// 改为通过本地 Python FastAPI 后端代理请求。
/// </summary>
private async Task<string> GetChatCompletion(List<Message> messages, Action<string> 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<string, object>
{
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<string, object>
{
{ "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<ChatCompletionResponse>(responseString);
return result?.choices?.FirstOrDefault()?.message?.content;
return result?.choices?.FirstOrDefault()?.message?.content ?? responseString;
}
catch
{
return responseString;
}
}
}

View File

@@ -150,22 +150,70 @@ namespace WordAddIn
/// 登录按钮点击事件。
/// 验证用户名和密码。
/// </summary>
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<string, string>("username", user),
new KeyValuePair<string, string>("password", pass),
new KeyValuePair<string, string>("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 = "立即登录";
}
}
}
}