一键导入
websocket-integration
通过 WebSocket 连接 Cortana AI 对话服务。编写 C# 客户端代码,实现发送消息、接收流式回复、附件传输、系统事件监听。触发关键词:WebSocket、WS 连接、远程对话、流式回复、AI 接入、实时通信。
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
通过 WebSocket 连接 Cortana AI 对话服务。编写 C# 客户端代码,实现发送消息、接收流式回复、附件传输、系统事件监听。触发关键词:WebSocket、WS 连接、远程对话、流式回复、AI 接入、实时通信。
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Madorin 插件开发入口技能。用于统一编排 Native、MCP、Process 三类通道,约束工程规范、外部包 AOT 校验、发布安装与运行时更新流程。触发关键词:插件开发、Native 插件、MCP 插件、Process 插件、进程插件、发布插件、安装插件。
Madorin Native 插件开发子技能。位置:subskills/native。用于 Native AOT 插件创建、Startup 组合、Tool 编写、JsonContext、后台服务、AOT 规则。触发关键词:Native 插件、AOT 插件、Startup.Configure、ToolAttribute、原生插件。
Madorin Process 插件开发子技能。位置:subskills/process。用于开发以独立 exe 进程运行的插件,支持 JIT self-contained 和 AOT exe 两种发布方式,通过 stdin/stdout JSON 协议与宿主通信。触发关键词:Process 插件、进程插件、exe 插件、JIT 插件、进程隔离插件。
Madorin PluginBus WebSocket 接入。用于生成 /internal 单端点、cortana.plugin-bus、conversation topic、chat.message.send、chat.generation.stop、system.notice、ping/pong 的 C# 客户端代码与样板。关键词:WebSocket、PluginBus、/internal、chat.message.send、ping、pong。
Madorin 插件工程规范子技能。位置:subskills/architecture。用于插件架构设计、职责分离、依赖注入、AOT 安全编码、配置与日志、质量门禁。触发关键词:插件架构、职责分离、依赖注入、工程规范、AOT 安全、插件设计。
Madorin MCP 通道子技能。位置:subskills/mcp。用于接入和校验外部 MCP Server 的 stdio、sse、streamable-http 配置。触发关键词:MCP、Model Context Protocol、stdio、sse、streamable-http。
| name | websocket-integration |
| description | 通过 WebSocket 连接 Cortana AI 对话服务。编写 C# 客户端代码,实现发送消息、接收流式回复、附件传输、系统事件监听。触发关键词:WebSocket、WS 连接、远程对话、流式回复、AI 接入、实时通信。 |
| user-invocable | true |
通过 WebSocket 协议连接 Cortana,实现 AI 对话交互。
| 项目 | 值 |
|---|---|
| 地址 | ws://<host>:<port>/ws/ |
| 默认端口 | 52841(可在系统设置中修改) |
| 编码 | UTF-8 JSON 文本帧 |
| 传输 | 标准 WebSocket(RFC 6455) |
连接成功后服务端立即推送 connected 消息,包含 clientId。
| type | data | 说明 |
|---|---|---|
send | 用户消息文本 | 发送对话消息,可附带 attachments |
stop | — | 中止当前 AI 回复 |
| type | 字段 | 说明 |
|---|---|---|
connected | clientId | 连接成功,分配客户端 ID |
token | data | AI 流式回复的文本片段,逐个推送 |
done | sessionId | 本轮回复完成 |
error | data | 错误信息;data="cancelled" 表示被中止 |
stt_partial | data | 语音识别中间结果 |
stt_final | data | 语音识别最终结果 |
stt_stopped | — | 语音识别已停止 |
tts_started | — | TTS 开始播放 |
tts_subtitle | data | TTS 字幕文本 |
tts_completed | — | TTS 播放完成 |
chat_completed | — | 整个对话流程结束(含 TTS) |
wakeword_detected | — | 检测到唤醒词 |
{
"type": "send",
"data": "描述一下这张图",
"attachments": [
{ "path": "C:\\Photos\\img.jpg", "name": "img.jpg", "type": "image/jpeg" }
]
}
path 必须是 Cortana 进程所在机器的本地路径所有 JSON 序列化/反序列化必须使用 Source Generator,禁止使用反射。
不要这样写(AOT 不兼容):
// ❌ 匿名类型 + 默认序列化器 → AOT 运行时崩溃
var json = JsonSerializer.Serialize(new { type = "send", data = "hello" });
必须这样写:
// ✅ 强类型 record + Source Generator → AOT 安全
var json = JsonSerializer.Serialize(msg, CortanaJsonContext.Default.WsClientMessage);
using System.Text.Json.Serialization;
/// <summary>客户端发送的消息。</summary>
public sealed record WsClientMessage
{
[JsonPropertyName("type")]
public string Type { get; init; } = string.Empty;
[JsonPropertyName("data")]
public string? Data { get; init; }
[JsonPropertyName("attachments")]
public List<WsAttachment>? Attachments { get; init; }
}
/// <summary>服务端返回的消息。</summary>
public sealed record WsServerMessage
{
[JsonPropertyName("type")]
public string Type { get; init; } = string.Empty;
[JsonPropertyName("data")]
public string? Data { get; init; }
[JsonPropertyName("clientId")]
public string? ClientId { get; init; }
[JsonPropertyName("sessionId")]
public string? SessionId { get; init; }
}
/// <summary>附件信息。</summary>
public sealed record WsAttachment
{
[JsonPropertyName("path")]
public string Path { get; init; } = string.Empty;
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("type")]
public string Type { get; init; } = string.Empty;
}
using System.Text.Json.Serialization;
[JsonSerializable(typeof(WsClientMessage))]
[JsonSerializable(typeof(WsServerMessage))]
[JsonSerializable(typeof(WsAttachment))]
[JsonSourceGenerationOptions(
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
internal partial class CortanaJsonContext : JsonSerializerContext;
关键:
JsonSerializerContext子类必须声明partial,编译器会自动生成序列化代码。 不要遗漏任何需要序列化的类型。
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
public sealed class CortanaWsClient : IAsyncDisposable
{
private readonly ClientWebSocket _ws = new();
private readonly Uri _uri;
private string? _clientId;
public CortanaWsClient(string host = "localhost", int port = 52841)
{
_uri = new Uri($"ws://{host}:{port}/ws/");
}
/// <summary>建立连接并接收 clientId。</summary>
public async Task ConnectAsync(CancellationToken ct = default)
{
await _ws.ConnectAsync(_uri, ct);
// 服务端立即推送 connected 消息
var msg = await ReceiveMessageAsync(ct);
if (msg?.Type == "connected")
_clientId = msg.ClientId;
}
/// <summary>发送文本消息,流式接收回复。</summary>
public async IAsyncEnumerable<string> SendAndStreamAsync(
string text,
List<WsAttachment>? attachments = null,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct = default)
{
var request = new WsClientMessage
{
Type = "send",
Data = text,
Attachments = attachments
};
// ✅ AOT 安全:使用 Source Generator 序列化
var json = JsonSerializer.Serialize(request, CortanaJsonContext.Default.WsClientMessage);
var bytes = Encoding.UTF8.GetBytes(json);
await _ws.SendAsync(bytes, WebSocketMessageType.Text, true, ct);
// 循环接收 token 直到 done 或 error
while (!ct.IsCancellationRequested)
{
var msg = await ReceiveMessageAsync(ct);
if (msg is null) yield break;
switch (msg.Type)
{
case "token":
if (msg.Data is not null)
yield return msg.Data;
break;
case "done":
yield break;
case "error":
throw new InvalidOperationException(msg.Data ?? "未知错误");
}
// 忽略 tts_*, stt_*, chat_completed 等广播事件
}
}
/// <summary>发送中止指令。</summary>
public async Task StopAsync(CancellationToken ct = default)
{
var request = new WsClientMessage { Type = "stop" };
var json = JsonSerializer.Serialize(request, CortanaJsonContext.Default.WsClientMessage);
await _ws.SendAsync(Encoding.UTF8.GetBytes(json), WebSocketMessageType.Text, true, ct);
}
/// <summary>接收并反序列化一条消息。</summary>
private async Task<WsServerMessage?> ReceiveMessageAsync(CancellationToken ct)
{
var buffer = new byte[8192];
using var ms = new MemoryStream();
WebSocketReceiveResult result;
do
{
result = await _ws.ReceiveAsync(buffer, ct);
if (result.MessageType == WebSocketMessageType.Close) return null;
ms.Write(buffer, 0, result.Count);
} while (!result.EndOfMessage);
// ✅ AOT 安全:使用 Source Generator 反序列化
return JsonSerializer.Deserialize(
ms.ToArray(),
CortanaJsonContext.Default.WsServerMessage);
}
public async ValueTask DisposeAsync()
{
if (_ws.State == WebSocketState.Open)
await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None);
_ws.Dispose();
}
}
await using var client = new CortanaWsClient("localhost", 52841);
await client.ConnectAsync();
// 流式打印 AI 回复
await foreach (var token in client.SendAndStreamAsync("帮我写一首七言绝句"))
{
Console.Write(token);
}
Console.WriteLine();
// 带附件的消息
var attachments = new List<WsAttachment>
{
new() { Path = @"C:\Photos\cat.jpg", Name = "cat.jpg", Type = "image/jpeg" }
};
await foreach (var token in client.SendAndStreamAsync("描述这张图片", attachments))
{
Console.Write(token);
}
// 中止生成
await client.StopAsync();
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<PublishAot>true</PublishAot>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup>
</Project>
JsonSerializerIsReflectionEnabledByDefault=false可以在编译期捕获遗漏的反射序列化调用。
| 陷阱 | 症状 | 修复 |
|---|---|---|
| 匿名类型序列化 | 运行时 NotSupportedException | 改为强类型 record + JsonSerializerContext |
遗漏 [JsonSerializable] | 序列化输出 {} 或反序列化全为 null | 在 JsonSerializerContext 上添加 [JsonSerializable(typeof(T))] |
使用 JsonSerializer.Serialize<T>(obj) | 走反射路径 | 改用 JsonSerializer.Serialize(obj, Context.Default.T) |
dynamic / ExpandoObject | 编译警告 + 运行时失败 | 改为强类型 |
| 泛型集合未注册 | List<T> 序列化为空 | 在上下文中注册 [JsonSerializable(typeof(List<T>))] |
send,AI 回复只发给该客户端;由 UI/语音发起的对话回复会广播所有 WS 客户端。stop 中止。type,保证向前兼容。path 必须是 Cortana 进程所在机器的本地路径。客户端 Cortana
│ │
│── ws://host:52841/ws/ ──────────────▶│ 握手
│◀── connected {clientId} ─────────────│
│ │
│── send {data, attachments?} ────────▶│ 发送消息
│ │
│◀── token {data} ─────────────────────│ ┐
│◀── token {data} ─────────────────────│ │ 流式回复(N 条)
│◀── token {data} ─────────────────────│ ┘
│◀── done {sessionId} ────────────────│ 回复完成
│ │
│◀── tts_started ─────────────────────│ ┐
│◀── tts_subtitle {data} ────────────│ │ TTS 广播
│◀── tts_completed ───────────────────│ ┘
│◀── chat_completed ──────────────────│ 流程结束
│ │
│── stop ─────────────────────────────▶│ 中止
│◀── error "cancelled" ───────────────│