博客 (228)

方法一:打开 事件查看器-> Windows 日志-> 应用程序。

QQ20251223-081955.png


方法二:启用ASP.NET Core Module Stdout 日志。

修改网站根目录下的 web.config 文件,在 <aspNetCore> 节点中启用标准输出日志。你需要先手动创建 logs 文件夹并确保有写入权限。

<aspNetCore processPath="dotnet" 
            arguments=".\YourAppName.dll" 
            stdoutLogEnabled="true" 
            stdoutLogFile=".\logs\stdout" 
            hostingModel="inprocess" />

重现错误后,即可在 logs 目录下查看生成的日志文件。注意:出于性能考虑,问题解决后建议将 stdoutLogEnabled 设为 false。

xoyozo 半个月前
346

今天发现在 .NET Framework 和 .NET 9 中使用 System.Uri.EscapeDataString() 方法对字符串进行编码,会产生不同的结果。

譬如“(”符号,前者视其为非保留字符,不进行转义,后者视为保留字符,转义为“%28”。

原因是 .NET Framework 4.8 主要遵循 RFC 2396,而 .NET 9 遵循 RFC 3986。


在跨平台签名验证场景中,对 URL 编码的一致性要求极高,任何细微差别都会导致签名校验失败。

以下是以 RFC 3986 标准为核心、优先使用各平台内置的高一致性方案。


对于 .NET 9,直接使用 Uri.EscapeDataString()。

string encodedData = System.Uri.EscapeDataString(dataToEncode);


对于 .NET Framework,以下是一个遵循 RFC 3986 严格标准的自定义编码方法示例。

static string Rfc3986EscapeDataString(string input)
{
    // 定义 RFC 3986 中明确的未保留字符集(不编码)
    var unreservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";

    var result = new StringBuilder();
    byte[] data = Encoding.UTF8.GetBytes(input); // 统一转换为 UTF-8 字节

    foreach (byte b in data)
    {
        char currentChar = (char)b;
        // 如果是未保留字符,直接输出
        if (unreservedChars.IndexOf(currentChar) != -1)
        {
            result.Append(currentChar);
        }
        else
        {
            // 否则,进行百分号编码(%XX,大写)
            result.Append('%').Append(b.ToString("X2"));
        }
    }
    return result.ToString();
}


对于 PHP,直接使用内置的 rawurlencode() 函数。这个函数的设计初衷就是严格遵循 RFC 3986 标准。

$encoded_data = rawurlencode($data_to_encode);


对于 JavaScript,encodeURIComponent 函数严格遵循 RFC 3986 标准。

let encodedData = encodeURIComponent(dataToEncode);


重要提示:无论使用哪种语言,务必在编码前明确指定字符串使用 UTF-8 编码。编码不一致是导致乱码和签名失败最常见的原因之一 。

xoyozo 1 个月前
443

一般地,应选择尺寸小于显存的大模型版本,且适当冗余。

譬如,显存 8GB,选择尺寸为 5.2GB 的 deepseek-r1:8b。

这样,整个大模型都能被完整地读取到显存中。

若选择 9.0GB 的 deepseek-r1:14b 则显存不足,Ollama 会自动调用系统内存和 CPU 来协同工作,导致推理速度显著下降。

显卡的算力影响生成的速度,模型的参数决定生成的质量。

另外经实测,在 /api/generate 接口的 format 参数中设置返回的 JSON 格式,会缩短生成时间,降低生成质量,可能的原因是强制格式限制了词汇选择空间。

xoyozo 2 个月前
451

今天遇到一个向基于 .NET Framework 框架开发的 Web 网站 POST 数据响应 405 Method Not Allowed 的问题。

服务端接口地址是 https://***/api/abc/Default.aspx,

若请求 https://***/api/abc/Default.aspx 或 https://***/api/abc/ 则正常 ,

若请求 https://***/api/abc 就会出现这个问题。

原因可能是服务器可能返回 301 重定向到 https://***/api/abc/Default.aspx,

导致最终接口接受到的请求方法不是 POST 。

xoyozo 2 个月前
511

个人整理,仅供参考。具体规格请以官方发布为准。


名称万兆路由器BE10000BE7000BE6500 ProBE6500全屋 BE3600 Pro 套装全屋 BE3600 Pro 网线版BE10000 Pro
型号



主:RN04

子:RN01

主:RP01

子:RP03


上市时间2022.102023.52023.102024.82024.102025.52025.9
处理器Qualcomm 四核 A73 2.2GHzQualcomm 四核 A73 1.5GHzQualcomm 四核 A53 1.5GHzQualcomm 四核 A53 1.1GHz

主/子:

高通 IPQ5312 四核 1.1GHz

主/子:

Qualcomm Dragonwing N7

Qualcomm A7 四核 1.8GHz
内存2GB1GB1GB512MB

主/子:512MB

一说子是 128MB

主:512MB

子:256MB

2GB
频段2.4GHz、5.2GHz、5.8GHz2.4GHz、5GHz2.4GHz、5GHz2.4GHz、5GHz2.4GHz、5GHz2.4GHz、5GHz2.4GHz、5.2GHz、5.8GHz
组网混合 Mesh混合 Mesh混合 Mesh混合 Mesh混合 MeshAC + APAI Mesh
天线12根高增益天线 + 12路信号放大器 + NFC内置天线7根外置高增益WiFi天线 + 1根内置高增益WiFi天线 + NFC内置天线6根高增益WiFi内置天线 + 1根蓝牙内置天线 + 1根NFC内置天线6根外置高增益Wi-Fi天线主/子:4根内置天线

主:无

子:2根内置双频天线

12根高增益天线 + 12路信号放大器
中枢网关不支持不支持支持不支持

主:支持

子:不支持

主:支持

子:不支持

支持
蓝牙网关不支持不支持

蓝牙 Mesh 1.0 100 台 + 蓝牙 100 台

升级固件后支持蓝牙 Mesh 2.0

蓝牙 Mesh 1.0

蓝牙 Mesh 1.0 200 台 + 蓝牙 100 台

主:不支持

子:蓝牙 Mesh 2.0

蓝牙 Mesh 2.0 200 台 + 蓝牙 100 台

散热主动散热自然散热自然散热自然散热自然散热

主:自然散热

子:主动散热

主动散热
接口

4×2.5G

1×10G

1×10G SFP+

1×USB 3.0

4×2.5G

1×USB 3.0

4×2.5G4×2.5G

主/子:

1×2.5G

3×1G

主:5×2.5G

子:2×2.5G

4×2.5G

2×10G

1×M.2

1×USB 3.0

Wi-FiWi-Fi 7Wi-Fi 7Wi-Fi 7Wi-Fi 7Wi-Fi 7

主:无

子:Wi-Fi 7

Wi-Fi 7
价格
最新价格最新价格最新价格最新价格最新价格最新价格最新价格
  • 表格于 2025 年 10 月整理更新。

  • 如果只考虑支持蓝牙 Mesh 2.0,那么有 BE3600 Pro 网线版 和 BE10000 Pro 可选,搭配其它 Mesh 路由器实现全屋 Wi-Fi 7 覆盖,搭配其它中枢网关或从网关设备实现全屋蓝牙 Mesh 2.0 覆盖。

  • 如果考虑用 Xiaomi 中枢网关 来部署独立的中枢架构,那么选择路由器就没有限制了。

  • 名称中带有“全屋”字样的通常以子母套装形式出售,子母路由配置通常不同。购买两台一模一样的普通 BE 路由器,就相当于组建了一套“不分子母”的 Mesh 套装。

  • 名称中带有“Pro”字样的通常具备中枢网关功能。

xoyozo 3 个月前
1,998
using System.Runtime.InteropServices;

/// <summary>
/// Windows 资源管理器的文件名排序规则(允许在非 Windows 平台使用)
/// <para>调用 Windows API(StrCmpLogicalW 函数)实现,仅在 Windows 平台环境中使用</para>
/// <para>基本规则如下:</para>
/// <para>将文件名中的数字作为数值来处理。</para>
/// <para>不区分大小写。</para>
/// <para>字符类型的大致优先级顺序为:特殊符号 → 数字 → 字母。</para>
/// <para>对于中文文件名,排序方式取决于系统的区域设置:拼音(默认)、笔画。</para>
/// </summary>
public class FileLogicalComparer : IComparer<string>
{
    [DllImport("Shlwapi.dll", CharSet = CharSet.Unicode)]
    private static extern int StrCmpLogicalW(string psz1, string psz2);
    private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);

    public int Compare(string? x, string? y)
    {
        // 处理 null 值情况
        if (x == null && y == null) return 0;
        if (x == null) return -1;
        if (y == null) return 1;

        try
        {
            if (IsWindows)
            {
                // Windows 平台:使用原生 API
                return StrCmpLogicalW(x, y);
            }
            else
            {
                // 非 Windows 平台:使用 C# 实现的回退方案
                return NaturalCompareFallback(x, y);
            }
        }
        catch (Exception) // 捕获 DllNotFound、EntryPointNotFoundException 等异常
        {
            // 异常时使用默认的字符串比较器
            return string.Compare(x, y, StringComparison.Ordinal);
        }
    }

    /// <summary>
    /// C# 实现的自然排序回退方案(代码来自 AI,未测试!)
    /// </summary>
    private static int NaturalCompareFallback(string x, string y)
    {
        if (x == y) return 0;

        int i = 0, j = 0;

        while (i < x.Length && j < y.Length)
        {
            if (char.IsDigit(x[i]) && char.IsDigit(y[j]))
            {
                // 提取连续数字并进行数值比较
                string num1 = ExtractNumber(x, ref i);
                string num2 = ExtractNumber(y, ref j);

                if (long.TryParse(num1, out long n1) && long.TryParse(num2, out long n2))
                {
                    if (n1 != n2)
                        return n1.CompareTo(n2);
                }
                else
                {
                    // 解析失败时按字符串比较
                    int strCompare = string.Compare(num1, num2, StringComparison.Ordinal);
                    if (strCompare != 0)
                        return strCompare;
                }
            }
            else
            {
                // 非数字字符直接比较
                if (x[i] != y[j])
                    return x[i].CompareTo(y[j]);

                i++;
                j++;
            }
        }

        return x.Length.CompareTo(y.Length);
    }

    /// <summary>
    /// 从字符串中提取连续的数字序列
    /// </summary>
    private static string ExtractNumber(string str, ref int index)
    {
        int start = index;
        while (index < str.Length && char.IsDigit(str[index]))
        {
            index++;
        }
        return str.Substring(start, index - start);
    }
}
xoyozo 3 个月前
1,966

在 Linux 上运行 .NET 网站,通过

HttpContext.Connection.RemoteIpAddress

获取客户端的 IP 地址,结果是

::ffff:127.0.0.1

解决方法:

打开 Program.cs 文件,在 var app = builder.Build(); 之前(尽量往前)添加以下代码:

if (OperatingSystem.IsLinux())
{
    builder.Services.Configure<ForwardedHeadersOptions>(options =>
    {
        options.ForwardedHeaders = ForwardedHeaders.XForwardedFor
                                    | ForwardedHeaders.XForwardedProto
                                    | ForwardedHeaders.XForwardedHost;

        // 清除 KnownNetworks 和 KnownProxies,表示信任来自本机的代理(如 Nginx)
        options.KnownNetworks.Clear();
        options.KnownProxies.Clear();
    });

    Console.WriteLine("ForwardedHeaders enabled (Running on Linux)");
}

然后在 app.UseRouting(); 之前添加以下代码:

if (OperatingSystem.IsLinux())
{
    app.UseForwardedHeaders();
    Console.WriteLine("UseForwardedHeaders() applied.");
}

其中,OperatingSystem.IsLinux() 用于判断只在 Linux 环境中生效,你可以视自身情况作判断。

xoyozo 4 个月前
3,994

早前记录过在 CentOS 中部署 ASP.NET Core 网站,现在宝塔面板已经直接支持创建 .NET 网站了,再次记录一下。


本文环境:VS2022、.NET 9、宝塔面板v11。


一、在 VS 中创建一个 .NET 9 网站,项目名称例:WebApplication1

发布选项:

image.png

关于“生成单个文件”选项,若勾选,发布后的项目启动文件不带后缀名(.dll),尝试在宝塔面板 v11.0.0 中无法顺利启动。


二、在 Linux 服务器上创建目录:/www/wwwroot/WebApplication1/

将发布后的文件上传到这个目录上,最终的文件结构如下:

image.png


三、进入宝塔面板,在 网站-Net项目 中安装 .Net环境管理,这里以 9.0.201 为例:

image.png


四、添加项目

项目名称:随意

运行路径:项目所在目录,本例中为:

/www/wwwroot/WebApplication1

启动命令:使用 dotnet 命令,dotnet 命令可以不使用全路径(/www/server/dotnet/9.0.201/dotnet),第一个参数是项目启动文件,--urls 是反向代理的服务器和端口。服务器名可以是*也可以是localhost。例:

dotnet WebApplication1.dll --urls=http://*:5000

项目端口:与启动命令中的端口号一致

开机启动:一般会勾选

启动用户:尽量用 www,最小权限原则。

image.png

正常情况下,网站已启动,如果未启动,检查配置,根据报错内容排查问题。


五、配置网站

添加域名、SSL 证书等。


友情提示:

  • 网站版本更新重新发布后,需要手动重启网站才能生效,习惯于发布到 IIS(会自动生效)的同学要特别注意。

  • 新手尽量以空项目或默认项目来部署,避免因特殊原因导致一系列其它问题。

  • nginx 反向代理并不会转发所有 Header,需要手动配置。

xoyozo 4 个月前
6,847

AutoUpdater.NET 是一个开源库,专为 .NET 桌面应用程序设计,支持 Windows Forms 和 WPF 应用。它通过从服务器获取 XML 文件来检测新版本信息,当发现新版本时向用户显示更新对话框。

相对于 ClickOnce,AutoUpdater.NET 的配置更简单一些。

首先通过 NuGet 包管理器安装 Autoupdater.NET.Official,然后在应用程序入口点添加以下代码:

AutoUpdater.Start("http://yourserver.com/path/to/update.xml");

XML 文件结构如下:

<?xml version="1.0" encoding="UTF-8"?>
<item>
  <version>2.0.0.0</version> <!--必填:最新版本号-->
  <url>http://yourserver.com/path/to/updatefile.zip</url> <!--必填:更新文件下载地址-->
  <changelog>http://yourserver.com/path/to/changelog.txt</changelog> <!--可选:更新日志-->
  <mandatory>False</mandatory> <!--可选:是否强制更新-->
</item>

Windows Forms 在 Program.cs 文件的 Main() 方法中,WPF 在 App.xaml.cs 的 OnStartup() 中添加:

AutoUpdater.Start("http://yourserver.com/path/to/update.xml");
AutoUpdater.CheckForUpdateEvent += (e) =>
{
    if (e.Error != null)
    {
        MessageBox.Show($"检查版本更新失败:{e.Error.Message}");
    }
    if (e.IsUpdateAvailable)
    {
        if (e.Mandatory.Value)
        {
            // 强制更新
            AutoUpdater.DownloadUpdate(e);
        }
        else
        {
            // 可选更新
            if (MessageBox.Show("发现新版本,是否立即更新?", "更新提示", MessageBoxButtons.YesNo, MessageBoxIcon.Information) == DialogResult.Yes)
            {
                AutoUpdater.DownloadUpdate(e);
            }
        }
    }
};

这样就能简单实现打开应用时判断是否有更新。具体用法参:GitHub

对于控制台应用程序,AutoUpdater.NET 并不直接支持。可以使用 GeneralUpdate 等轻量级自动更新库。

其它:

  • 若希望通过标准安装流程(如添加到开始菜单),优先选择 ClickOnce,适合长期维护的内部工具。

  • 若追求快速集成和无感更新,AutoUpdater.NET 更灵活。

  • 无论哪种方案,务必对程序进行代码签名(如购买企业证书或生成测试证书),否则系统可能拦截安装。(你要允许来自未知发布者的此应用对你的设备进行更改吗)

  • 可以购买“OV 代码签名”证书或“EV 代码签名”证书,注意:“代码签名证书”与“域名证书”互不通用。

xoyozo 5 个月前
6,655

一、安装 Ollama

官网下载安装 Ollama。

你可以更改大模型存放目录,也可以开放远程访问

查看版本号:

ollama --version


二、在 shell 中安装和运行模型

Models 中选择一个你想部署的模型,复制安装命令,并在终端中执行。

官方建议:应该至少有 8 GB 的 RAM 来运行 7b 版本,16 GB 的 RAM 来运行 13b 版本,32 GB 的 RAM 来运行 33b 版本

本文以 deepseek-r1:7b 为例。

下载模型

ollama pull deepseek-r1:7b

Tip: 下载即将完成时速度可能会变得非常慢,只要按 Ctrl+C,再重新执行一次命令,就会继续正常下载。

显示模型信息

ollama show deepseek-r1:7b

运行模型(一次性响应)

ollama run deepseek-r1:7b "写一首诗"

运行模型(进入聊天模式)

ollama run deepseek-r1:7b

结束当前会话

/bye

列出所有模型

ollama list

列出当前加载的模型

ollama ps

停止当前正在运行的模型

ollama stop deepseek-r1:7b

删除一个模型

ollama rm deepseek-r1:7b


三、使用 REST API 调用模型

修改端口

ollama serve --port 11434

/api/generate 接口:生成一次性响应

curl http://localhost:11434/api/generate -d '{
  "model": "deepseek-r1:7b",
  "prompt":"为什么天空是蓝色的?"
}'

/api/chat 接口:与模型聊天

curl http://localhost:11434/api/chat -d '{
  "model": "deepseek-r1:7b",
  "messages": [
    { "role": "user", "content": "你好呀!" }
  ]
}'


四、在 .NET 中调用

1、直接 HTTP 调用(基础方案)

    创建 HttpClient,使用 PostAsJsonAsync 请求,使用 ReadFromJsonAsync 读取结果。

2、使用 OllamaSharp 库(推荐方案)

    创建 OllamaApiClient,使用 SelectedModel 设置模型,使用 GenerateAsync 获得结果。或创建对话 ollama.Chat(),并 Send 内容。

3、.NET Aspire 集成(企业级方案)

    适合微服务架构,结合容器化部署。


OllamaSharp 库”和“.NET Aspire 集成”两种方案怎么选?

OllamaSharp 库:定位轻量级模型交互 SDK,适用于独立应用、微服务中的 AI 组件等场景,技术复度低,支持模型对话/生成/管理、流式响应、多模态支持,需自行实现监控、熔断。

.NET Aspire 集成:定位企业级云原生 AI 服务编排框架,适用于多服务协同的分布式系统,技术复度高,支持服务编排、健康检查、弹性伸缩、混合云部署,内置可观测性仪表盘、自动故障转移。

决策建议:初创项目用 OllamaSharp 快速试错,用户量破千后通过 Aspire 重构。两者并非互斥,可在 Aspire 中封装 OllamaSharp 客户端,兼顾灵活性与运维能力。

xoyozo 6 个月前
4,519