'commit'
This commit is contained in:
@@ -189,6 +189,7 @@ namespace WordAddIn
|
|||||||
if (login.ShowDialog() == DialogResult.OK)
|
if (login.ShowDialog() == DialogResult.OK)
|
||||||
{
|
{
|
||||||
_isLoggedIn = true;
|
_isLoggedIn = true;
|
||||||
|
AccessToken = login.Token; // 获取登录成功后的 Token
|
||||||
SaveLoginState();
|
SaveLoginState();
|
||||||
|
|
||||||
// 刷新 Ribbon 状态,使按钮变亮
|
// 刷新 Ribbon 状态,使按钮变亮
|
||||||
|
|||||||
@@ -339,6 +339,15 @@ namespace WordAddIn
|
|||||||
optimizedPrompt = await OptimizePromptForImage(prompt, style);
|
optimizedPrompt = await OptimizePromptForImage(prompt, style);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查登录状态
|
||||||
|
if (string.IsNullOrEmpty(AiRibbon.AccessToken))
|
||||||
|
{
|
||||||
|
throw new Exception("未登录,请先点击功能区的【登录】按钮获取权限。");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 指向本地 FastAPI 后端代理
|
||||||
|
string requestUrl = $"{Config.BackendBaseUrl}/api/image";
|
||||||
|
|
||||||
// 使用匿名对象构造请求体,以获得最大灵活性
|
// 使用匿名对象构造请求体,以获得最大灵活性
|
||||||
var requestBody = new
|
var requestBody = new
|
||||||
{
|
{
|
||||||
@@ -351,50 +360,63 @@ namespace WordAddIn
|
|||||||
var json = JsonConvert.SerializeObject(requestBody);
|
var json = JsonConvert.SerializeObject(requestBody);
|
||||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
var response = await _httpClient.PostAsync($"{Config.TuZiBaseUrl}/images/generations", content);
|
// 创建请求并添加 Token
|
||||||
var responseString = await response.Content.ReadAsStringAsync();
|
using (var request = new HttpRequestMessage(HttpMethod.Post, requestUrl))
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
{
|
||||||
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 格式差异
|
using (var response = await _httpClient.SendAsync(request))
|
||||||
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];
|
var responseString = await response.Content.ReadAsStringAsync();
|
||||||
// Gemini 3 image preview 可能会返回 revised_prompt 和 url
|
|
||||||
if (firstItem["url"] != null) return firstItem["url"].ToString();
|
if (!response.IsSuccessStatusCode)
|
||||||
|
|
||||||
// 有时它会返回 NO_IMAGE,这可能是模型拒绝了,或者格式不对
|
|
||||||
if (firstItem["revised_prompt"] != null && firstItem["revised_prompt"].ToString() == "NO_IMAGE")
|
|
||||||
{
|
{
|
||||||
// 尝试从其他字段找,或者这就是个错误
|
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
throw new Exception("认证失效,请重新登录。");
|
||||||
|
}
|
||||||
|
throw new Exception($"后端服务错误 ({response.StatusCode}): {responseString}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 某些 API 可能直接在数组里放 URL 字符串
|
// 使用 JObject 动态解析响应,以兼容不同厂商的 API 格式差异
|
||||||
if (firstItem.Type == JTokenType.String) return firstItem.ToString();
|
var jsonResponse = JObject.Parse(responseString);
|
||||||
}
|
|
||||||
// 2. 尝试解析 data 为直接字符串的情况
|
// 1. 尝试解析 OpenAI 标准格式: data[0].url
|
||||||
else if (data.Type == JTokenType.String)
|
var data = jsonResponse["data"];
|
||||||
{
|
if (data != null)
|
||||||
return data.ToString();
|
{
|
||||||
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -424,90 +446,78 @@ namespace WordAddIn
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 通用聊天补全请求方法。
|
/// 通用聊天补全请求方法。
|
||||||
|
/// 改为通过本地 Python FastAPI 后端代理请求。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<string> GetChatCompletion(List<Message> messages, Action<string> onProgress = null)
|
private async Task<string> GetChatCompletion(List<Message> messages, Action<string> onProgress = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// 检查登录状态
|
||||||
|
if (string.IsNullOrEmpty(AiRibbon.AccessToken))
|
||||||
|
{
|
||||||
|
throw new Exception("未登录,请先点击功能区的【登录】按钮获取权限。");
|
||||||
|
}
|
||||||
|
|
||||||
// 获取当前选择的模型
|
// 获取当前选择的模型
|
||||||
string selectedModelId = Properties.Settings.Default.SelectedModel;
|
string selectedModelId = Properties.Settings.Default.SelectedModel;
|
||||||
// 如果未设置,默认使用第一个 (qwen-plus)
|
|
||||||
if (string.IsNullOrEmpty(selectedModelId)) selectedModelId = "qwen-plus";
|
if (string.IsNullOrEmpty(selectedModelId)) selectedModelId = "qwen-plus";
|
||||||
|
|
||||||
var modelInfo = ModelConfig.GetModel(selectedModelId);
|
var modelInfo = ModelConfig.GetModel(selectedModelId);
|
||||||
|
|
||||||
string apiKey = "";
|
// 指向本地 FastAPI 后端代理
|
||||||
string requestUrl = "";
|
string requestUrl = "http://127.0.0.1:8000/api/chat";
|
||||||
|
string accessToken = AiRibbon.AccessToken;
|
||||||
string jsonContent = "";
|
string jsonContent = "";
|
||||||
|
|
||||||
// 根据模型类型配置请求
|
// 统一构造 OpenAI 兼容格式请求体发送给后端
|
||||||
if (modelInfo.ApiType == "Native") // DashScope Native (e.g., qwen-plus)
|
// 后端负责根据模型类型转发给 DashScope 或其他服务
|
||||||
|
var requestDict = new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
apiKey = Config.DashScopeApiKey;
|
{ "model", modelInfo.Id },
|
||||||
requestUrl = Config.DashScopeNativeUrl;
|
{ "messages", messages },
|
||||||
|
{ "temperature", 0.7 }
|
||||||
// 构造 DashScope Native 格式请求体
|
};
|
||||||
var requestBody = new
|
|
||||||
{
|
// 注意:目前后端 Mock 暂时只支持非流式,但协议上保留 stream 字段
|
||||||
model = modelInfo.Id,
|
if (onProgress != null)
|
||||||
input = new
|
|
||||||
{
|
|
||||||
messages = messages
|
|
||||||
},
|
|
||||||
parameters = new
|
|
||||||
{
|
|
||||||
result_format = "message",
|
|
||||||
incremental_output = (onProgress != null) // 如果有进度回调,则开启增量输出
|
|
||||||
}
|
|
||||||
};
|
|
||||||
jsonContent = JsonConvert.SerializeObject(requestBody);
|
|
||||||
}
|
|
||||||
else // Compatible (TuZi, DeepSeek, etc.)
|
|
||||||
{
|
{
|
||||||
apiKey = Config.TuZiApiKey;
|
requestDict["stream"] = true;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 针对 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");
|
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
// 创建新的 HttpRequestMessage 以设置特定的 Authorization Header
|
// 创建新的 HttpRequestMessage
|
||||||
using (var request = new HttpRequestMessage(HttpMethod.Post, requestUrl))
|
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;
|
request.Content = content;
|
||||||
|
|
||||||
// 如果是流式请求,使用 ResponseHeadersRead
|
|
||||||
var completionOption = onProgress != null ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead;
|
var completionOption = onProgress != null ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead;
|
||||||
|
|
||||||
using (var response = await _httpClient.SendAsync(request, completionOption))
|
using (var response = await _httpClient.SendAsync(request, completionOption))
|
||||||
{
|
{
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
|
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
throw new Exception("认证失效,请重新登录。");
|
||||||
|
}
|
||||||
var errorString = await response.Content.ReadAsStringAsync();
|
var errorString = await response.Content.ReadAsStringAsync();
|
||||||
throw new Exception($"API Error ({response.StatusCode}): {errorString}");
|
throw new Exception($"后端服务错误 ({response.StatusCode}): {errorString}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ... 后续处理保持不变,后端返回格式应兼容 OpenAI ...
|
||||||
|
|
||||||
|
|
||||||
if (onProgress != null)
|
if (onProgress != null)
|
||||||
{
|
{
|
||||||
// 处理流式响应
|
// 处理流式响应
|
||||||
@@ -549,38 +559,14 @@ namespace WordAddIn
|
|||||||
var json = JObject.Parse(dataStr);
|
var json = JObject.Parse(dataStr);
|
||||||
string contentDelta = "";
|
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
|
contentDelta = json["choices"]?[0]?["message"]?["content"]?.ToString();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(contentDelta))
|
if (!string.IsNullOrEmpty(contentDelta))
|
||||||
@@ -603,22 +589,15 @@ namespace WordAddIn
|
|||||||
// 非流式处理
|
// 非流式处理
|
||||||
var responseString = await response.Content.ReadAsStringAsync();
|
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);
|
var result = JsonConvert.DeserializeObject<ChatCompletionResponse>(responseString);
|
||||||
return result?.choices?.FirstOrDefault()?.message?.content;
|
return result?.choices?.FirstOrDefault()?.message?.content ?? responseString;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return responseString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,22 +150,70 @@ namespace WordAddIn
|
|||||||
/// 登录按钮点击事件。
|
/// 登录按钮点击事件。
|
||||||
/// 验证用户名和密码。
|
/// 验证用户名和密码。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void BtnLogin_Click(object sender, EventArgs e)
|
private async void BtnLogin_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
string user = txtUsername.Text.Trim();
|
string user = txtUsername.Text.Trim();
|
||||||
string pass = txtPassword.Text.Trim();
|
string pass = txtPassword.Text.Trim();
|
||||||
|
|
||||||
// 模拟验证逻辑
|
if (string.IsNullOrEmpty(user) || string.IsNullOrEmpty(pass))
|
||||||
if (user == "huanghai" && pass == "12345678")
|
|
||||||
{
|
{
|
||||||
this.DialogResult = DialogResult.OK;
|
MessageBox.Show("请输入用户名和密码!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||||
this.Close();
|
return;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
btnLogin.Enabled = false;
|
||||||
|
btnLogin.Text = "登录中...";
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
MessageBox.Show("用户名或密码错误!\n(默认账号: huanghai, 密码: 12345678)", "登录失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
using (var client = new HttpClient())
|
||||||
txtPassword.Focus();
|
{
|
||||||
txtPassword.SelectAll();
|
// 指向本地 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 = "立即登录";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user