diff --git a/BarrageGrab/App.config b/BarrageGrab/App.config
index c298622..729897a 100644
--- a/BarrageGrab/App.config
+++ b/BarrageGrab/App.config
@@ -1,107 +1,107 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
+
-
+
-
\ No newline at end of file
+
diff --git a/BarrageGrab/AppConfig.json b/BarrageGrab/AppConfig.json
index 7497daa..f7887f6 100644
--- a/BarrageGrab/AppConfig.json
+++ b/BarrageGrab/AppConfig.json
@@ -5,7 +5,9 @@
// 是否隐藏控制台
"hideConsole": false,
// 显示窗体
- "showWindow": false
+ "showWindow": true,
+ // 直播平台: "douyin" 或 "kuaishou"
+ "platform": "douyin"
},
"network": {
"proxy": {
@@ -25,7 +27,7 @@
},
"filtering": {
// 过滤Websocket数据源进程,可用','进行分隔,程序将会监听以下进程的弹幕信息
- "processFilter": "直播伴侣,douyin,chrome,msedge,QQBrowser,360se,firefox,2345explorer,iexplore",
+ "processFilter": "直播伴侣,douyin,kuaishou,kwailive,bilibili,chrome,msedge,QQBrowser,360se,firefox,2345explorer,iexplore",
// 开启内置的域名过滤,设置为false会解包所有https请求,cpu占用很高,建议在无法获取弹幕数据时调整
"hostNameEnabled": true,
// 已知的弹幕域名列表 ','分隔 用作过滤规则中,凡是webcast开头的域名程序都会自动列入白名单
diff --git a/BarrageGrab/AppSetting.cs b/BarrageGrab/AppSetting.cs
index f7bbc1e..dacce13 100644
--- a/BarrageGrab/AppSetting.cs
+++ b/BarrageGrab/AppSetting.cs
@@ -7,6 +7,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BarrageGrab.Modles.JsonEntity;
+using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using static System.Configuration.ConfigurationManager;
@@ -46,6 +47,7 @@ namespace BarrageGrab
ConfigComPort();
ConfigFilter();
+ LoadFromJson();
}
catch (Exception ex)
{
@@ -72,8 +74,11 @@ namespace BarrageGrab
// 读取JSON文件内容(保留注释)
string jsonContent = File.ReadAllText(jsonFilePath);
- // 解析JSON(Newtonsoft.Json支持JSONC格式,会自动忽略注释)
- JObject config = JObject.Parse(jsonContent);
+ // 解析JSON(忽略注释)
+ JObject config = JObject.Parse(jsonContent, new JsonLoadSettings
+ {
+ CommentHandling = CommentHandling.Ignore
+ });
// 从JSON读取应用配置
var app = config["app"];
@@ -81,7 +86,16 @@ namespace BarrageGrab
// 常规配置
var general = app["general"];
HideConsole = general["hideConsole"]?.Value() ?? false;
- ShowWindow = general["showWindow"]?.Value() ?? false;
+ ShowWindow = general["showWindow"]?.Value() ?? false;
+ string platformStr = general["platform"]?.Value() ?? "douyin";
+ if (platformStr.ToLower() == "kuaishou")
+ {
+ Platform = LivePlatform.Kuaishou;
+ }
+ else
+ {
+ Platform = LivePlatform.Douyin;
+ }
// 网络配置
var network = app["network"];
@@ -640,6 +654,11 @@ namespace BarrageGrab
///
public string LiveCompanPath { get; set; } = string.Empty;
+ ///
+ /// 直播平台 (Douyin / Kuaishou)
+ ///
+ public LivePlatform Platform { get; set; } = LivePlatform.Douyin;
+
///
/// hook直播伴侣代理开关
///
diff --git a/BarrageGrab/BUILD.bat b/BarrageGrab/BUILD.bat
new file mode 100644
index 0000000..90464e0
--- /dev/null
+++ b/BarrageGrab/BUILD.bat
@@ -0,0 +1,32 @@
+@echo off
+REM JiaKong 配置编辑器编译脚本
+REM 在 VS 中打开 WssBarrageService.csproj 编译即可
+REM 或者双击此脚本(需要安装 Visual Studio 2022)
+
+echo 正在查找 MSBuild...
+
+REM VS 2022 MSBuild 路径
+set MSBUILD="C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"
+
+if not exist %MSBUILD% (
+ set MSBUILD="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"
+)
+
+if not exist %MSBUILD% (
+ echo 未找到 MSBuild.exe,请在 Visual Studio 中打开项目并编译。
+ pause
+ exit /b 1
+)
+
+echo 正在编译 WssBarrageService...
+%MSBUILD% WssBarrageService.csproj /t:Build /p:Configuration=Debug /nologo
+
+if %ERRORLEVEL% EQU 0 (
+ echo.
+ echo 编译成功!
+ echo 输出: bin\Debug\WssBarrageServer.exe
+) else (
+ echo.
+ echo 编译失败,请检查错误信息。
+)
+pause
diff --git a/BarrageGrab/Forms/ConfigEditorForm.cs b/BarrageGrab/Forms/ConfigEditorForm.cs
new file mode 100644
index 0000000..c0f89c0
--- /dev/null
+++ b/BarrageGrab/Forms/ConfigEditorForm.cs
@@ -0,0 +1,523 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Drawing;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Windows.Forms;
+
+namespace BarrageGrab.Forms
+{
+ ///
+ /// 游戏配置编辑器 —— 可视化编辑5个配置文件
+ /// 1. effect_config.txt - 基础效果库(下拉框选项源)
+ /// 2. macro_config.txt - 组合效果(宏)
+ /// 3. gift_config.txt - 礼物/事件→效果映射
+ /// 4. command_config.txt - 弹幕指令触发词
+ /// 5. fkey_config.txt - F键→效果映射
+ ///
+ public partial class ConfigEditorForm : Form
+ {
+ private DataGridView dgvEffects, dgvMacros, dgvGifts, dgvCommands, dgvFkeys;
+ private TabControl tabMain;
+ private string ConfigDir { get { return Program.ConfigDir; } }
+ private Label lblStatus;
+
+ // 文件路径
+ private string EffectPath => Program.EffectPath;
+ private string MacroPath => Program.MacroPath;
+ private string GiftPath => Program.GiftPath;
+ private string CmdPath => Program.CommandPath;
+ private string FkeyPath => Program.FkeyPath;
+
+ private readonly string[] FKeyOptions = { "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12" };
+
+ // CommandType 列表(用于指令Tab)
+ private readonly string[] CommandTypes = {
+ "SelectState", "SpawnTroop", "Gold", "Population", "Destiny",
+ "Mobilize", "Attack", "Alliance", "ResearchTech", "SetIdeology",
+ "ApplyBuff", "Info"
+ };
+
+ public ConfigEditorForm()
+ {
+ InitializeComponent();
+ this.Text = "游戏配置编辑器 (5文件)";
+ this.ClientSize = new Size(950, 650);
+ this.StartPosition = FormStartPosition.CenterParent;
+ BuildUI();
+ LoadAll();
+ }
+
+ private void InitializeComponent()
+ {
+ this.SuspendLayout();
+ this.AutoScaleMode = AutoScaleMode.Font;
+ this.ResumeLayout(false);
+ }
+
+ // ═══════════════ UI 构建 ═══════════════
+
+ void BuildUI()
+ {
+ tabMain = new TabControl { Dock = DockStyle.Fill, Name = "tabMain" };
+ this.Controls.Add(tabMain);
+
+ // Tab1: 基础效果
+ var tabEffect = new TabPage { Text = "基础效果", Name = "tabEffect" };
+ dgvEffects = CreateEffectGrid();
+ tabEffect.Controls.Add(dgvEffects);
+ tabEffect.Controls.Add(CreateButtonPanel(() => AddRow(dgvEffects, "新效果", "出兵 3"),
+ "格式: 效果名称|游戏指令 | 原子效果,下拉框选项源"));
+ tabEffect.Controls[1].Dock = DockStyle.Bottom;
+ dgvEffects.Dock = DockStyle.Fill;
+ tabMain.TabPages.Add(tabEffect);
+
+ // Tab2: 组合宏
+ var tabMacro = new TabPage { Text = "组合宏", Name = "tabMacro" };
+ dgvMacros = CreateMacroGrid();
+ tabMacro.Controls.Add(dgvMacros);
+ tabMacro.Controls.Add(CreateButtonPanel(() => AddRow(dgvMacros, "新宏", "", "出兵 5|金币 100"),
+ "格式: 宏名称|触发词(可空)|指令1|指令2 | 指令支持$1 $2 $*"));
+ tabMacro.Controls[1].Dock = DockStyle.Bottom;
+ dgvMacros.Dock = DockStyle.Fill;
+ tabMain.TabPages.Add(tabMacro);
+
+ // Tab3: 礼物事件
+ var tabGift = new TabPage { Text = "礼物事件", Name = "tabGift" };
+ dgvGifts = CreateGiftGrid();
+ tabGift.Controls.Add(dgvGifts);
+ tabGift.Controls.Add(CreateButtonPanel(() => AddRow(dgvGifts, "新礼物", "", "{0} 送了礼物", ""),
+ "格式: 关键词|效果名称|公告|间隔 | 效果名称从下拉框选"));
+ tabGift.Controls[1].Dock = DockStyle.Bottom;
+ dgvGifts.Dock = DockStyle.Fill;
+ tabMain.TabPages.Add(tabGift);
+
+ // Tab4: 弹幕指令
+ var tabCmd = new TabPage { Text = "弹幕指令", Name = "tabCmd" };
+ dgvCommands = CreateCommandGrid();
+ tabCmd.Controls.Add(dgvCommands);
+ tabCmd.Controls.Add(CreateButtonPanel(() => AddRow(dgvCommands, "SelectState", "选择,选,select"),
+ "格式: [CommandType]|触发词1,触发词2 | 宏在'组合宏'Tab配置"));
+ tabCmd.Controls[1].Dock = DockStyle.Bottom;
+ dgvCommands.Dock = DockStyle.Fill;
+ tabMain.TabPages.Add(tabCmd);
+
+ // Tab5: F键
+ var tabFkey = new TabPage { Text = "F键", Name = "tabFkey" };
+ dgvFkeys = CreateFkeyGrid();
+ tabFkey.Controls.Add(dgvFkeys);
+ tabFkey.Controls.Add(CreateButtonPanel(() => AddRow(dgvFkeys, "F2", ""),
+ "格式: F键|效果名称 | 效果名称从下拉框选 | F1固定初始化"));
+ tabFkey.Controls[1].Dock = DockStyle.Bottom;
+ dgvFkeys.Dock = DockStyle.Fill;
+ tabMain.TabPages.Add(tabFkey);
+ }
+
+ Panel CreateButtonPanel(Action addAction, string hint)
+ {
+ var p = new Panel { Height = 40 };
+ var btnAdd = new Button { Text = "+ 新增", Left = 5, Top = 8, Width = 70 };
+ btnAdd.Click += (s, e) => addAction();
+ p.Controls.Add(btnAdd);
+
+ var btnDel = new Button { Text = "删除选中", Left = 80, Top = 8, Width = 70 };
+ btnDel.Click += (s, e) =>
+ {
+ var dgv = GetCurrentGrid();
+ if (dgv?.SelectedRows.Count > 0)
+ foreach (DataGridViewRow row in dgv.SelectedRows)
+ if (!row.IsNewRow) dgv.Rows.Remove(row);
+ };
+ p.Controls.Add(btnDel);
+
+ var btnReload = new Button { Text = "重新加载", Left = 155, Top = 8, Width = 80 };
+ btnReload.Click += (s, e) => { LoadAll(); };
+ p.Controls.Add(btnReload);
+
+ var btnSave = new Button { Text = "全部保存", Left = 240, Top = 8, Width = 90, BackColor = Color.LightGreen };
+ btnSave.Click += (s, e) => SaveAll();
+ p.Controls.Add(btnSave);
+
+ lblStatus = new Label { AutoSize = true, Left = 340, Top = 12, Text = "" };
+ p.Controls.Add(lblStatus);
+
+ var lblHint = new Label { AutoSize = true, Left = 500, Top = 12, ForeColor = Color.Gray, Text = hint };
+ p.Controls.Add(lblHint);
+ return p;
+ }
+
+ DataGridView GetCurrentGrid()
+ {
+ switch (tabMain.SelectedIndex)
+ {
+ case 0: return dgvEffects;
+ case 1: return dgvMacros;
+ case 2: return dgvGifts;
+ case 3: return dgvCommands;
+ case 4: return dgvFkeys;
+ default: return null;
+ }
+ }
+
+ void AddRow(DataGridView dgv, params object[] values) => dgv.Rows.Add(values);
+
+ // ═══════════════ 表格创建 ═══════════════
+
+ DataGridView CreateEffectGrid()
+ {
+ var dgv = CreateBaseGrid("dgvEffects");
+ dgv.Columns.Add(new DataGridViewTextBoxColumn { HeaderText = "效果名称", Name = "Name", Width = 150 });
+ dgv.Columns.Add(new DataGridViewTextBoxColumn { HeaderText = "游戏指令", Name = "Command", Width = 300 });
+ return dgv;
+ }
+
+ DataGridView CreateMacroGrid()
+ {
+ var dgv = CreateBaseGrid("dgvMacros");
+ dgv.Columns.Add(new DataGridViewTextBoxColumn { HeaderText = "宏名称", Name = "Name", Width = 120 });
+ dgv.Columns.Add(new DataGridViewTextBoxColumn { HeaderText = "触发词(逗号分隔,可空)", Name = "Triggers", Width = 150 });
+ dgv.Columns.Add(new DataGridViewTextBoxColumn { HeaderText = "指令(多条用|分隔,支持$1 $2 $*)", Name = "Commands", Width = 350 });
+ return dgv;
+ }
+
+ DataGridView CreateGiftGrid()
+ {
+ var dgv = CreateBaseGrid("dgvGifts");
+ dgv.Columns.Add(new DataGridViewTextBoxColumn { HeaderText = "关键词(礼物名/事件)", Name = "Keyword", Width = 120 });
+ var colEffect = new DataGridViewComboBoxColumn { HeaderText = "效果名称", Name = "Effect", Width = 180 };
+ colEffect.AutoComplete = true;
+ dgv.Columns.Add(colEffect);
+ dgv.DataError += (s, e) => { e.ThrowException = false; };
+ dgv.Columns.Add(new DataGridViewTextBoxColumn { HeaderText = "公告文本", Name = "Announce", Width = 200 });
+ dgv.Columns.Add(new DataGridViewTextBoxColumn { HeaderText = "间隔", Name = "Interval", Width = 50 });
+ return dgv;
+ }
+
+ DataGridView CreateCommandGrid()
+ {
+ var dgv = CreateBaseGrid("dgvCommands");
+ var colType = new DataGridViewComboBoxColumn { HeaderText = "CommandType", Name = "CmdType", Width = 120 };
+ colType.Items.AddRange(CommandTypes);
+ dgv.Columns.Add(colType);
+ dgv.DataError += (s, e) => { e.ThrowException = false; };
+ dgv.Columns.Add(new DataGridViewTextBoxColumn { HeaderText = "触发词(逗号分隔)", Name = "Triggers", Width = 400 });
+ return dgv;
+ }
+
+ DataGridView CreateFkeyGrid()
+ {
+ var dgv = CreateBaseGrid("dgvFkeys");
+ var colKey = new DataGridViewComboBoxColumn { HeaderText = "F键", Name = "FKey", Width = 80 };
+ colKey.Items.AddRange(FKeyOptions);
+ dgv.Columns.Add(colKey);
+ var colEffect = new DataGridViewComboBoxColumn { HeaderText = "效果名称", Name = "Effect", Width = 250 };
+ colEffect.AutoComplete = true;
+ dgv.Columns.Add(colEffect);
+ dgv.DataError += (s, e) => { e.ThrowException = false; };
+ return dgv;
+ }
+
+ DataGridView CreateBaseGrid(string name)
+ {
+ return new DataGridView
+ {
+ AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
+ AllowUserToAddRows = false,
+ AllowUserToDeleteRows = false,
+ RowHeadersVisible = false,
+ SelectionMode = DataGridViewSelectionMode.FullRowSelect,
+ Name = name
+ };
+ }
+
+ // ═══════════════ 效果下拉刷新 ═══════════════
+
+ /// 刷新礼物/F键Tab的效果下拉框(从effect_config + macro_config读取)
+ void RefreshEffectDropdowns()
+ {
+ var effectNames = new List();
+
+ // 从基础效果表读取
+ foreach (DataGridViewRow row in dgvEffects.Rows)
+ {
+ if (row.IsNewRow) continue;
+ var name = row.Cells["Name"].Value?.ToString();
+ if (!string.IsNullOrEmpty(name)) effectNames.Add(name);
+ }
+
+ // 从宏表读取
+ foreach (DataGridViewRow row in dgvMacros.Rows)
+ {
+ if (row.IsNewRow) continue;
+ var name = row.Cells["Name"].Value?.ToString();
+ if (!string.IsNullOrEmpty(name)) effectNames.Add(name);
+ }
+
+ // 刷新礼物Tab
+ var giftCol = dgvGifts.Columns["Effect"] as DataGridViewComboBoxColumn;
+ if (giftCol != null)
+ {
+ var oldValues = new List();
+ foreach (DataGridViewRow row in dgvGifts.Rows)
+ {
+ if (row.IsNewRow) continue;
+ var v = row.Cells["Effect"].Value?.ToString();
+ if (!string.IsNullOrEmpty(v)) oldValues.Add(v);
+ }
+ giftCol.Items.Clear();
+ foreach (var n in effectNames) giftCol.Items.Add(n);
+ foreach (var v in oldValues)
+ if (!giftCol.Items.Contains(v)) giftCol.Items.Add(v);
+ }
+
+ // 刷新F键Tab
+ var fkeyCol = dgvFkeys.Columns["Effect"] as DataGridViewComboBoxColumn;
+ if (fkeyCol != null)
+ {
+ var oldValues = new List();
+ foreach (DataGridViewRow row in dgvFkeys.Rows)
+ {
+ if (row.IsNewRow) continue;
+ var v = row.Cells["Effect"].Value?.ToString();
+ if (!string.IsNullOrEmpty(v)) oldValues.Add(v);
+ }
+ fkeyCol.Items.Clear();
+ foreach (var n in effectNames) fkeyCol.Items.Add(n);
+ foreach (var v in oldValues)
+ if (!fkeyCol.Items.Contains(v)) fkeyCol.Items.Add(v);
+ }
+ }
+
+ // ═══════════════ 加载 ═══════════════
+
+ void LoadAll()
+ {
+ if (!Directory.Exists(ConfigDir)) Directory.CreateDirectory(ConfigDir);
+ LoadEffects();
+ LoadMacros();
+ LoadGifts();
+ LoadCommands();
+ LoadFkeys();
+ RefreshEffectDropdowns();
+ lblStatus.Text = "配置已加载";
+ lblStatus.ForeColor = Color.Black;
+ }
+
+ void LoadEffects()
+ {
+ dgvEffects.Rows.Clear();
+ if (!File.Exists(EffectPath)) return;
+ foreach (var line in File.ReadAllLines(EffectPath, Encoding.UTF8))
+ {
+ var t = line.Trim();
+ if (string.IsNullOrEmpty(t) || t.StartsWith("#")) continue;
+ int pipe = t.IndexOf('|');
+ if (pipe <= 0) continue;
+ dgvEffects.Rows.Add(t.Substring(0, pipe).Trim(), t.Substring(pipe + 1).Trim());
+ }
+ }
+
+ void LoadMacros()
+ {
+ dgvMacros.Rows.Clear();
+ if (!File.Exists(MacroPath)) return;
+ foreach (var line in File.ReadAllLines(MacroPath, Encoding.UTF8))
+ {
+ var t = line.Trim();
+ if (string.IsNullOrEmpty(t) || t.StartsWith("#")) continue;
+ var parts = t.Split('|');
+ if (parts.Length < 3) continue;
+ var name = parts[0].Trim();
+ var triggers = parts[1].Trim();
+ var cmds = string.Join("|", parts.Skip(2).Select(p => p.Trim()));
+ dgvMacros.Rows.Add(name, triggers, cmds);
+ }
+ }
+
+ void LoadGifts()
+ {
+ dgvGifts.Rows.Clear();
+ if (!File.Exists(GiftPath)) return;
+ foreach (var line in File.ReadAllLines(GiftPath, Encoding.UTF8))
+ {
+ var t = line.Trim();
+ if (string.IsNullOrEmpty(t) || t.StartsWith("#")) continue;
+ var parts = t.Split('|');
+ if (parts.Length < 2) continue;
+ var keyword = parts[0].Trim();
+ var effect = parts[1].Trim();
+ var announce = parts.Length > 2 ? parts[2].Trim() : "";
+ var interval = parts.Length > 3 ? parts[3].Trim() : "";
+ dgvGifts.Rows.Add(keyword, effect, announce, interval);
+ }
+ }
+
+ void LoadCommands()
+ {
+ dgvCommands.Rows.Clear();
+ if (!File.Exists(CmdPath)) return;
+ foreach (var line in File.ReadAllLines(CmdPath, Encoding.UTF8))
+ {
+ var t = line.Trim();
+ if (string.IsNullOrEmpty(t) || t.StartsWith("#")) continue;
+ if (!t.StartsWith("[")) continue;
+ int end = t.IndexOf(']');
+ if (end <= 1) continue;
+ var typeName = t.Substring(1, end - 1).Trim();
+ var triggers = t.Substring(end + 1).TrimStart('|').Trim();
+ dgvCommands.Rows.Add(typeName, triggers);
+ }
+ }
+
+ void LoadFkeys()
+ {
+ dgvFkeys.Rows.Clear();
+ if (!File.Exists(FkeyPath))
+ {
+ // 默认值
+ var defaults = new[] {
+ ("F2","出兵5"), ("F3","金币300"), ("F4","出兵3"), ("F5","金币500"),
+ ("F6","攻击力x2_30s"), ("F7","人口1000"), ("F8","天命200"),
+ ("F9","科技全开"), ("F10","特征全开"), ("F11","动员5"), ("F12","大礼包")
+ };
+ foreach (var d in defaults) dgvFkeys.Rows.Add(d.Item1, d.Item2);
+ return;
+ }
+ foreach (var line in File.ReadAllLines(FkeyPath, Encoding.UTF8))
+ {
+ var t = line.Trim();
+ if (string.IsNullOrEmpty(t) || t.StartsWith("#")) continue;
+ int pipe = t.IndexOf('|');
+ if (pipe <= 0) continue;
+ dgvFkeys.Rows.Add(t.Substring(0, pipe).Trim(), t.Substring(pipe + 1).Trim());
+ }
+ }
+
+ // ═══════════════ 保存 ═══════════════
+
+ void SaveAll()
+ {
+ try
+ {
+ if (!Directory.Exists(ConfigDir)) Directory.CreateDirectory(ConfigDir);
+ SaveEffects();
+ SaveMacros();
+ SaveGifts();
+ SaveCommands();
+ SaveFkeys();
+ RefreshEffectDropdowns();
+ lblStatus.Text = "全部保存成功";
+ lblStatus.ForeColor = Color.Green;
+ }
+ catch (Exception ex)
+ {
+ lblStatus.Text = $"保存失败: {ex.Message}";
+ lblStatus.ForeColor = Color.Red;
+ }
+ }
+
+ void SaveEffects()
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("# 游戏基础效果配置 v1");
+ sb.AppendLine("# 格式: 效果名称|游戏指令");
+ sb.AppendLine("# 一个效果 = 一条原子指令(多指令组合用 macro_config)");
+ sb.AppendLine();
+ foreach (DataGridViewRow row in dgvEffects.Rows)
+ {
+ if (row.IsNewRow) continue;
+ var name = row.Cells["Name"].Value?.ToString() ?? "";
+ var cmd = row.Cells["Command"].Value?.ToString() ?? "";
+ if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(cmd))
+ sb.AppendLine($"{name}|{cmd}");
+ }
+ File.WriteAllText(EffectPath, sb.ToString(), Encoding.UTF8);
+ }
+
+ void SaveMacros()
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("# 游戏组合效果(宏)配置 v1");
+ sb.AppendLine("# 格式: 宏名称|触发词1,触发词2,...|指令1|指令2|...");
+ sb.AppendLine("# 触发词可选(留空则只能被礼物/F键引用)");
+ sb.AppendLine("# 指令支持 $1 $2 $* 参数替换(仅弹幕触发时)");
+ sb.AppendLine();
+ foreach (DataGridViewRow row in dgvMacros.Rows)
+ {
+ if (row.IsNewRow) continue;
+ var name = row.Cells["Name"].Value?.ToString() ?? "";
+ var triggers = row.Cells["Triggers"].Value?.ToString() ?? "";
+ var cmds = row.Cells["Commands"].Value?.ToString() ?? "";
+ if (string.IsNullOrEmpty(name)) continue;
+ // cmds 中的 | 是指令分隔符,保持原样
+ sb.AppendLine($"{name}|{triggers}|{cmds}");
+ }
+ File.WriteAllText(MacroPath, sb.ToString(), Encoding.UTF8);
+ }
+
+ void SaveGifts()
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("# 游戏礼物/事件配置 v1");
+ sb.AppendLine("# 格式: 关键词|效果名称|公告文本|触发间隔(可选)");
+ sb.AppendLine("# 效果名称引用 effect_config 或 macro_config(下拉框选择)");
+ sb.AppendLine("# {0}=昵称 {1}=数量");
+ sb.AppendLine("# 特殊关键词: 点赞/关注/进房/分享/粉丝团");
+ sb.AppendLine();
+ foreach (DataGridViewRow row in dgvGifts.Rows)
+ {
+ if (row.IsNewRow) continue;
+ var keyword = row.Cells["Keyword"].Value?.ToString() ?? "";
+ var effect = row.Cells["Effect"].Value?.ToString() ?? "";
+ var msg = row.Cells["Announce"].Value?.ToString() ?? "";
+ var interval = row.Cells["Interval"].Value?.ToString() ?? "";
+ if (string.IsNullOrEmpty(keyword) || string.IsNullOrEmpty(effect)) continue;
+ var parts = new List { keyword, effect };
+ if (!string.IsNullOrEmpty(msg)) parts.Add(msg);
+ if (!string.IsNullOrEmpty(interval)) parts.Add(interval);
+ sb.AppendLine(string.Join("|", parts));
+ }
+ File.WriteAllText(GiftPath, sb.ToString(), Encoding.UTF8);
+ }
+
+ void SaveCommands()
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("# 游戏弹幕指令触发词配置 v1");
+ sb.AppendLine("# 格式: [CommandType]|触发词1,触发词2,...");
+ sb.AppendLine("# 注: 宏指令在 macro_config.txt 配置");
+ sb.AppendLine();
+ foreach (DataGridViewRow row in dgvCommands.Rows)
+ {
+ if (row.IsNewRow) continue;
+ var type = row.Cells["CmdType"].Value?.ToString() ?? "";
+ var triggers = row.Cells["Triggers"].Value?.ToString() ?? "";
+ if (string.IsNullOrEmpty(type)) continue;
+ sb.AppendLine($"[{type}]|{triggers}");
+ }
+ File.WriteAllText(CmdPath, sb.ToString(), Encoding.UTF8);
+ }
+
+ void SaveFkeys()
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("# 游戏 F键配置 v1");
+ sb.AppendLine("# 格式: F键|效果名称");
+ sb.AppendLine("# 效果名称引用 effect_config 或 macro_config");
+ sb.AppendLine("# F1 固定为初始化,不可修改");
+ sb.AppendLine();
+ foreach (DataGridViewRow row in dgvFkeys.Rows)
+ {
+ if (row.IsNewRow) continue;
+ var key = row.Cells["FKey"].Value?.ToString() ?? "";
+ var effect = row.Cells["Effect"].Value?.ToString() ?? "";
+ if (string.IsNullOrEmpty(key)) continue;
+ sb.AppendLine($"{key}|{effect}");
+ }
+ File.WriteAllText(FkeyPath, sb.ToString(), Encoding.UTF8);
+ }
+ }
+}
diff --git a/BarrageGrab/Forms/FormView.Designer.cs b/BarrageGrab/Forms/FormView.Designer.cs
index 6006828..3a49e76 100644
--- a/BarrageGrab/Forms/FormView.Designer.cs
+++ b/BarrageGrab/Forms/FormView.Designer.cs
@@ -26,6 +26,7 @@
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
///
+
private void InitializeComponent()
{
this.rich_output = new System.Windows.Forms.RichTextBox();
@@ -36,6 +37,8 @@
this.flowLayoutPanel1 = new System.Windows.Forms.FlowLayoutPanel();
this.tabPage_Ws = new System.Windows.Forms.TabPage();
this.flowLayoutPanel2 = new System.Windows.Forms.FlowLayoutPanel();
+ this.tabPage_Log = new System.Windows.Forms.TabPage();
+ this.flowLayoutPanel3 = new System.Windows.Forms.FlowLayoutPanel();
this.label1 = new System.Windows.Forms.Label();
this.list_roomCaches = new System.Windows.Forms.ListBox();
this.cbx_enableProxy = new System.Windows.Forms.CheckBox();
@@ -43,20 +46,18 @@
this.panel2 = new System.Windows.Forms.Panel();
this.cbx_barrageLog = new System.Windows.Forms.CheckBox();
this.panel3 = new System.Windows.Forms.Panel();
+ this.txb_wsaddr = new System.Windows.Forms.TextBox();
this.btn_updateUpProxy = new System.Windows.Forms.Button();
this.txb_upstreamProxy = new System.Windows.Forms.TextBox();
this.label5 = new System.Windows.Forms.Label();
this.label4 = new System.Windows.Forms.Label();
- this.txb_wsaddr = new System.Windows.Forms.TextBox();
- this.tabPage_Log = new System.Windows.Forms.TabPage();
- this.flowLayoutPanel3 = new System.Windows.Forms.FlowLayoutPanel();
this.panel1.SuspendLayout();
this.tab_filters.SuspendLayout();
this.tabPage_Console.SuspendLayout();
this.tabPage_Ws.SuspendLayout();
+ this.tabPage_Log.SuspendLayout();
this.panel2.SuspendLayout();
this.panel3.SuspendLayout();
- this.tabPage_Log.SuspendLayout();
this.SuspendLayout();
//
// rich_output
@@ -115,7 +116,7 @@
// tabPage_Console
//
this.tabPage_Console.Controls.Add(this.flowLayoutPanel1);
- this.tabPage_Console.Font = new System.Drawing.Font("SimSun", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
+ this.tabPage_Console.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.tabPage_Console.Location = new System.Drawing.Point(22, 4);
this.tabPage_Console.Name = "tabPage_Console";
this.tabPage_Console.Padding = new System.Windows.Forms.Padding(3);
@@ -153,6 +154,27 @@
this.flowLayoutPanel2.Size = new System.Drawing.Size(57, 257);
this.flowLayoutPanel2.TabIndex = 6;
//
+ // tabPage_Log
+ //
+ this.tabPage_Log.Controls.Add(this.flowLayoutPanel3);
+ this.tabPage_Log.Location = new System.Drawing.Point(22, 4);
+ this.tabPage_Log.Name = "tabPage_Log";
+ this.tabPage_Log.Size = new System.Drawing.Size(63, 263);
+ this.tabPage_Log.TabIndex = 2;
+ this.tabPage_Log.Text = "日志 ";
+ this.tabPage_Log.UseVisualStyleBackColor = true;
+ //
+ // flowLayoutPanel3
+ //
+ this.flowLayoutPanel3.BackColor = System.Drawing.SystemColors.Control;
+ this.flowLayoutPanel3.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.flowLayoutPanel3.Location = new System.Drawing.Point(0, 0);
+ this.flowLayoutPanel3.Margin = new System.Windows.Forms.Padding(2);
+ this.flowLayoutPanel3.Name = "flowLayoutPanel3";
+ this.flowLayoutPanel3.Padding = new System.Windows.Forms.Padding(0, 8, 0, 0);
+ this.flowLayoutPanel3.Size = new System.Drawing.Size(63, 263);
+ this.flowLayoutPanel3.TabIndex = 7;
+ //
// label1
//
this.label1.AutoSize = true;
@@ -246,6 +268,16 @@
this.panel3.Size = new System.Drawing.Size(429, 159);
this.panel3.TabIndex = 13;
//
+ // txb_wsaddr
+ //
+ this.txb_wsaddr.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
+ this.txb_wsaddr.Location = new System.Drawing.Point(85, 14);
+ this.txb_wsaddr.Margin = new System.Windows.Forms.Padding(2);
+ this.txb_wsaddr.Name = "txb_wsaddr";
+ this.txb_wsaddr.ReadOnly = true;
+ this.txb_wsaddr.Size = new System.Drawing.Size(164, 21);
+ this.txb_wsaddr.TabIndex = 16;
+ //
// btn_updateUpProxy
//
this.btn_updateUpProxy.Location = new System.Drawing.Point(254, 48);
@@ -286,37 +318,6 @@
this.label4.TabIndex = 10;
this.label4.Text = "WS服务端口:";
//
- // txb_wsaddr
- //
- this.txb_wsaddr.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
- this.txb_wsaddr.Location = new System.Drawing.Point(85, 14);
- this.txb_wsaddr.Margin = new System.Windows.Forms.Padding(2);
- this.txb_wsaddr.Name = "txb_wsaddr";
- this.txb_wsaddr.ReadOnly = true;
- this.txb_wsaddr.Size = new System.Drawing.Size(164, 21);
- this.txb_wsaddr.TabIndex = 16;
- //
- // tabPage_Log
- //
- this.tabPage_Log.Controls.Add(this.flowLayoutPanel3);
- this.tabPage_Log.Location = new System.Drawing.Point(22, 4);
- this.tabPage_Log.Name = "tabPage_Log";
- this.tabPage_Log.Size = new System.Drawing.Size(63, 263);
- this.tabPage_Log.TabIndex = 2;
- this.tabPage_Log.Text = "日志 ";
- this.tabPage_Log.UseVisualStyleBackColor = true;
- //
- // flowLayoutPanel3
- //
- this.flowLayoutPanel3.BackColor = System.Drawing.SystemColors.Control;
- this.flowLayoutPanel3.Dock = System.Windows.Forms.DockStyle.Fill;
- this.flowLayoutPanel3.Location = new System.Drawing.Point(0, 0);
- this.flowLayoutPanel3.Margin = new System.Windows.Forms.Padding(2);
- this.flowLayoutPanel3.Name = "flowLayoutPanel3";
- this.flowLayoutPanel3.Padding = new System.Windows.Forms.Padding(0, 8, 0, 0);
- this.flowLayoutPanel3.Size = new System.Drawing.Size(63, 263);
- this.flowLayoutPanel3.TabIndex = 7;
- //
// FormView
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
@@ -335,11 +336,11 @@
this.tab_filters.ResumeLayout(false);
this.tabPage_Console.ResumeLayout(false);
this.tabPage_Ws.ResumeLayout(false);
+ this.tabPage_Log.ResumeLayout(false);
this.panel2.ResumeLayout(false);
this.panel2.PerformLayout();
this.panel3.ResumeLayout(false);
this.panel3.PerformLayout();
- this.tabPage_Log.ResumeLayout(false);
this.ResumeLayout(false);
this.PerformLayout();
diff --git a/BarrageGrab/Forms/FormView.cs b/BarrageGrab/Forms/FormView.cs
index 0c400a8..095e5d7 100644
--- a/BarrageGrab/Forms/FormView.cs
+++ b/BarrageGrab/Forms/FormView.cs
@@ -1,8 +1,9 @@
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
+using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
@@ -13,6 +14,7 @@ using BarrageGrab.Forms;
using BarrageGrab.Forms.Models;
using BarrageGrab.Modles.JsonEntity;
using BarrageGrab.Proxy;
+using Newtonsoft.Json;
namespace BarrageGrab
{
@@ -24,14 +26,85 @@ namespace BarrageGrab
WssBarrageGrab grab = AppRuntime.WsServer.Grab;
ISystemProxy proxy = AppRuntime.WsServer.Grab.Proxy;
+ private Panel testPanel;
+ private ComboBox cmbPlayers;
+ private ComboBox cmbEventType;
+ private PictureBox picAvatar;
+ private TextBox txtPlayerName;
+ private Label lblPlayerid;
+ private Button btnGeneratePlayers;
+ private Panel inputPanel;
+ private Label lblDynamic1, lblDynamic2;
+ private TextBox txtDynamic1, txtDynamic2;
+ private Button btnSendTest;
+ private Label lblStatus;
+
public FormView()
{
InitializeComponent();
barServer.OnPrint += WssService_OnPrint;
barServer.Grab.Proxy.OnProxyStatus += Proxy_OnProxyStatus;
AppRuntime.RoomCaches.OnCache += RoomCaches_OnCache;
+
+ // 扩大form和panel2高度
+ this.ClientSize = new Size(687, 650);
+ this.panel2.Height = 390;
+ this.panel1.Top = 392;
+ this.panel1.Height = this.ClientSize.Height - this.panel1.Top - 20;
+
+ // 固定panel3高度,不让它随panel2拉伸
+ this.panel3.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
+ this.panel3.Height = 159;
+
+ BuildTestPanel();
+
+ // 游戏配置按钮
+ var btnConfig = new Button
+ {
+ Text = "游戏配置",
+ Width = 100,
+ Left = 10,
+ Top = 90
+ };
+ btnConfig.Click += (s, e) => { using (var f = new Forms.ConfigEditorForm()) f.ShowDialog(this); };
+ this.panel3.Controls.Add(btnConfig);
+
+ // Settings button
+ var btnSettings = new Button
+ {
+ Text = "设置",
+ Width = 60,
+ Left = 120,
+ Top = 90
+ };
+ btnSettings.Click += (s, e) => { using (var f = new Forms.SettingsForm()) f.ShowDialog(this); };
+ this.panel3.Controls.Add(btnSettings);
+
+
+ // 打开游戏按钮
+ var btnLaunchGame = new Button
+ {
+ Text = "打开游戏",
+ Width = 80,
+ Left = 190,
+ Top = 90
+ };
+ btnLaunchGame.Click += (s, e) => { Program.LaunchGame(); };
+ this.panel3.Controls.Add(btnLaunchGame);
+
+ // 重新打开游戏按钮
+ var btnRestartGame = new Button
+ {
+ Text = "重启游戏",
+ Width = 80,
+ Left = 280,
+ Top = 90
+ };
+ btnRestartGame.Click += (s, e) => { Program.RestartGame(); };
+ this.panel3.Controls.Add(btnRestartGame);
}
+
private void InitTabPages()
{
var msgTypes = GetMsgTypes();
@@ -235,5 +308,351 @@ namespace BarrageGrab
AppSetting.Current.BarrageLog = checker.Checked;
AppSetting.Current.Save();
}
+
+ // ==================== 测试面板 ====================
+
+ private readonly string DefaultAvatarUrl = "https://aliyun_id_photo_bucket.oss.aliyuncs.com/default_handsome_intl.jpg";
+ private readonly List<(long Id, string Name)> _testPlayers = new List<(long Id, string Name)>();
+ private static readonly string[] SampleNames = {
+ "小明", "小红", "玩家001", "铁柱", "翠花", "老六", "大佬666",
+ "战神", "小可爱", "路人甲", "氪金玩家", "欧皇", "非酋", "主播铁粉",
+ "水友123", "弹幕刷屏王", "白嫖党", "榜一大哥", "守塔小兵", "运营总监"
+ };
+
+ private void BuildTestPanel()
+ {
+ // 测试面板标题
+ int yBase = 195;
+ var lblTitle = new Label
+ {
+ Text = "📡 WebSocket 模拟测试",
+ Location = new Point(250, yBase),
+ Size = new Size(200, 18),
+ Font = new Font("微软雅黑", 10, FontStyle.Bold),
+ ForeColor = Color.DarkBlue
+ };
+ panel2.Controls.Add(lblTitle);
+
+ // ── 生成按钮 ──
+ btnGeneratePlayers = new Button
+ {
+ Text = "随机生成玩家",
+ Location = new Point(250, yBase + 20),
+ Size = new Size(130, 24),
+ BackColor = Color.LightSteelBlue,
+ UseVisualStyleBackColor = true
+ };
+ btnGeneratePlayers.Click += BtnGeneratePlayers_Click;
+ panel2.Controls.Add(btnGeneratePlayers);
+
+ // ── 玩家下拉(含头像) ──
+ cmbPlayers = new ComboBox
+ {
+ Location = new Point(250, yBase + 46),
+ Size = new Size(310, 22),
+ DropDownStyle = ComboBoxStyle.DropDownList,
+ DropDownWidth = 400
+ };
+ cmbPlayers.SelectedIndexChanged += CmbPlayers_SelectedIndexChanged;
+ panel2.Controls.Add(cmbPlayers);
+
+ lblPlayerid = new Label
+ {
+ Text = "ID: --",
+ Location = new Point(565, yBase + 48),
+ Size = new Size(110, 18),
+ ForeColor = Color.Gray
+ };
+ panel2.Controls.Add(lblPlayerid);
+
+ // ── 事件类型下拉 ──
+ var lblEvent = new Label { Text = "事件:", Location = new Point(250, yBase + 74), Size = new Size(40, 20) };
+ panel2.Controls.Add(lblEvent);
+
+ cmbEventType = new ComboBox
+ {
+ Location = new Point(290, yBase + 72),
+ Size = new Size(130, 22),
+ DropDownStyle = ComboBoxStyle.DropDownList
+ };
+ cmbEventType.Items.AddRange(new object[] { "弹幕消息", "点赞消息", "礼物消息", "关注消息", "进直播间", "直播间分享", "粉丝团消息" });
+ cmbEventType.SelectedIndex = 0;
+ cmbEventType.SelectedIndexChanged += CmbEventType_SelectedIndexChanged;
+ panel2.Controls.Add(cmbEventType);
+
+ // ── 发送按钮 ──
+ btnSendTest = new Button
+ {
+ Text = "发送",
+ Location = new Point(430, yBase + 70),
+ Size = new Size(130, 26),
+ BackColor = Color.Orange,
+ Font = new Font("微软雅黑", 9, FontStyle.Bold),
+ UseVisualStyleBackColor = true
+ };
+ btnSendTest.Click += BtnSendTest_Click;
+ panel2.Controls.Add(btnSendTest);
+
+ // ── 动态输入面板 ──
+ inputPanel = new Panel
+ {
+ Location = new Point(248, yBase + 100),
+ Size = new Size(430, 50),
+ BorderStyle = BorderStyle.FixedSingle
+ };
+ panel2.Controls.Add(inputPanel);
+
+ lblDynamic1 = new Label { Location = new Point(8, 8), Size = new Size(50, 18), Text = "内容:" };
+ inputPanel.Controls.Add(lblDynamic1);
+ txtDynamic1 = new TextBox { Location = new Point(60, 6), Size = new Size(200, 21), Text = "测试弹幕666" };
+ inputPanel.Controls.Add(txtDynamic1);
+
+ lblDynamic2 = new Label { Location = new Point(8, 30), Size = new Size(50, 18), Text = "" };
+ inputPanel.Controls.Add(lblDynamic2);
+ txtDynamic2 = new TextBox { Location = new Point(60, 28), Size = new Size(80, 21), Text = "", Visible = false };
+ inputPanel.Controls.Add(txtDynamic2);
+
+ // ── 状态 ──
+ lblStatus = new Label
+ {
+ Location = new Point(250, yBase + 155),
+ Size = new Size(425, 18),
+ ForeColor = Color.DimGray
+ };
+ panel2.Controls.Add(lblStatus);
+
+ // 初始刷新
+ RefreshInputPanel();
+ }
+
+ private void BtnGeneratePlayers_Click(object sender, EventArgs e)
+ {
+ _testPlayers.Clear();
+ cmbPlayers.Items.Clear();
+
+ var rnd = new Random();
+ // 生成28个完全随机玩家
+ for (int i = 0; i < 28; i++)
+ {
+ long id = 10000 + rnd.Next(1, 99999);
+ string name = SampleNames[rnd.Next(SampleNames.Length)];
+ if (rnd.Next(2) == 0) name += rnd.Next(10, 999).ToString();
+ _testPlayers.Add((id, name));
+ cmbPlayers.Items.Add($"{name} (ID:{id})");
+ }
+
+ cmbPlayers.SelectedIndex = 0;
+ lblStatus.Text = $"已生成 {_testPlayers.Count} 个测试玩家";
+ }
+
+ private void CmbPlayers_SelectedIndexChanged(object sender, EventArgs e)
+ {
+ if (cmbPlayers.SelectedIndex >= 0 && cmbPlayers.SelectedIndex < _testPlayers.Count)
+ {
+ var (id, name) = _testPlayers[cmbPlayers.SelectedIndex];
+ lblPlayerid.Text = $"ID: {id}";
+ }
+ }
+
+ private void CmbEventType_SelectedIndexChanged(object sender, EventArgs e)
+ {
+ RefreshInputPanel();
+ }
+
+ private void RefreshInputPanel()
+ {
+ string evt = cmbEventType.SelectedItem?.ToString() ?? "";
+ switch (evt)
+ {
+ case "弹幕消息":
+ lblDynamic1.Text = "内容:";
+ txtDynamic1.Visible = true;
+ txtDynamic1.Text = "测试弹幕666";
+ lblDynamic2.Text = "";
+ txtDynamic2.Visible = false;
+ break;
+ case "点赞消息":
+ lblDynamic1.Text = "点赞数:";
+ txtDynamic1.Visible = true;
+ txtDynamic1.Text = "10";
+ lblDynamic2.Text = "";
+ txtDynamic2.Visible = false;
+ break;
+ case "礼物消息":
+ lblDynamic1.Text = "礼物名:";
+ txtDynamic1.Visible = true;
+ txtDynamic1.Text = "小心心";
+ lblDynamic2.Text = "数量:";
+ txtDynamic2.Visible = true;
+ txtDynamic2.Text = "1";
+ break;
+ case "直播间分享":
+ lblDynamic1.Text = "次数:";
+ txtDynamic1.Visible = true;
+ txtDynamic1.Text = "1";
+ lblDynamic2.Text = "";
+ txtDynamic2.Visible = false;
+ break;
+ default: // 关注/进房/粉丝团
+ lblDynamic1.Text = "无额外参数";
+ txtDynamic1.Visible = false;
+ lblDynamic2.Text = "";
+ txtDynamic2.Visible = false;
+ break;
+ }
+ }
+
+ private void BtnSendTest_Click(object sender, EventArgs e)
+ {
+ try
+ {
+ if (cmbPlayers.SelectedIndex < 0 || cmbPlayers.SelectedIndex >= _testPlayers.Count)
+ {
+ lblStatus.Text = "❌ 请先选择玩家";
+ return;
+ }
+
+ var (pid, pname) = _testPlayers[cmbPlayers.SelectedIndex];
+ string evt = cmbEventType.SelectedItem?.ToString() ?? "弹幕消息";
+
+ MsgUser user = new MsgUser
+ {
+ Id = pid,
+ Nickname = pname,
+ HeadImgUrl = DefaultAvatarUrl,
+ DisplayId = pid.ToString(),
+ Level = 10,
+ Gender = 1
+ };
+
+ BarrageMsgPack pack = null;
+ string desc = "";
+
+ switch (evt)
+ {
+ case "弹幕消息":
+ {
+ var msg = new Msg
+ {
+ MsgId = DateTime.Now.Ticks,
+ User = user,
+ Content = txtDynamic1.Text,
+ RoomId = "123456"
+ };
+ pack = new BarrageMsgPack(JsonConvert.SerializeObject(msg), PackMsgType.弹幕消息, "test");
+ desc = $"弹幕: {txtDynamic1.Text}";
+ }
+ break;
+
+ case "点赞消息":
+ {
+ long cnt = long.TryParse(txtDynamic1.Text, out var c) ? c : 1;
+ var msg = new LikeMsg
+ {
+ MsgId = DateTime.Now.Ticks,
+ User = user,
+ Count = cnt,
+ Total = cnt + 100,
+ RoomId = "123456"
+ };
+ pack = new BarrageMsgPack(JsonConvert.SerializeObject(msg), PackMsgType.点赞消息, "test");
+ desc = $"点赞 x{cnt}";
+ }
+ break;
+
+ case "礼物消息":
+ {
+ long cnt = long.TryParse(txtDynamic2.Text, out var gc) ? gc : 1;
+ var msg = new GiftMsg
+ {
+ MsgId = DateTime.Now.Ticks,
+ User = user,
+ GiftName = txtDynamic1.Text,
+ GiftCount = cnt,
+ GiftId = 100,
+ DiamondCount = 1,
+ RoomId = "123456"
+ };
+ pack = new BarrageMsgPack(JsonConvert.SerializeObject(msg), PackMsgType.礼物消息, "test");
+ desc = $"礼物: {txtDynamic1.Text} x{cnt}";
+ }
+ break;
+
+ case "关注消息":
+ {
+ var msg = new Msg
+ {
+ MsgId = DateTime.Now.Ticks,
+ User = user,
+ Content = "关注了主播",
+ RoomId = "123456"
+ };
+ pack = new BarrageMsgPack(JsonConvert.SerializeObject(msg), PackMsgType.关注消息, "test");
+ desc = "关注主播";
+ }
+ break;
+
+ case "进直播间":
+ {
+ var msg = new MemberMessage
+ {
+ MsgId = DateTime.Now.Ticks,
+ User = user,
+ CurrentCount = 999,
+ RoomId = "123456"
+ };
+ pack = new BarrageMsgPack(JsonConvert.SerializeObject(msg), PackMsgType.进直播间, "test");
+ desc = "进入直播间";
+ }
+ break;
+
+ case "直播间分享":
+ {
+ long cnt = long.TryParse(txtDynamic1.Text, out var sc) ? sc : 1;
+ var msg = new ShareMessage
+ {
+ MsgId = DateTime.Now.Ticks,
+ User = user,
+ ShareType = BarrageGrab.Modles.JsonEntity.ShareType.微信,
+ RoomId = "123456"
+ };
+ desc = $"分享 x{cnt}";
+ // 分享用次数送多次
+ for (int i = 0; i < cnt; i++)
+ {
+ pack = new BarrageMsgPack(JsonConvert.SerializeObject(msg), PackMsgType.直播间分享, "test");
+ barServer.Broadcast(pack);
+ }
+ lblStatus.Text = $"✅ 已发送: {pname}(ID:{pid}) → {desc}";
+ return;
+ }
+
+ case "粉丝团消息":
+ {
+ var msg = new FansclubMsg
+ {
+ MsgId = DateTime.Now.Ticks,
+ User = user,
+ Type = 2, // 加入
+ Level = 1,
+ RoomId = "123456"
+ };
+ pack = new BarrageMsgPack(JsonConvert.SerializeObject(msg), PackMsgType.粉丝团消息, "test");
+ desc = "加入粉丝团";
+ }
+ break;
+ }
+
+ if (pack != null)
+ {
+ barServer.Broadcast(pack);
+ lblStatus.Text = $"✅ 已发送: {pname}(ID:{pid}) → {desc}";
+ }
+ }
+ catch (Exception ex)
+ {
+ lblStatus.Text = $"❌ 发送失败: {ex.Message}";
+ }
+ }
}
}
diff --git a/BarrageGrab/Forms/SettingsForm.cs b/BarrageGrab/Forms/SettingsForm.cs
new file mode 100644
index 0000000..0fe394f
--- /dev/null
+++ b/BarrageGrab/Forms/SettingsForm.cs
@@ -0,0 +1,630 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Windows.Forms;
+using Newtonsoft.Json.Linq;
+
+namespace BarrageGrab.Forms
+{
+ ///
+ /// 完整设置界面 —— 可视化编辑 AppConfig.json + 游戏集成
+ /// 配置项分8个Tab: 常规/网络/弹幕过滤/轮询/进程域名/直播伴侣/串口/游戏启动项
+ /// 游戏启动项 Tab 管理启动项 + 5个配置表路径
+ ///
+ public class SettingsForm : Form
+ {
+ private TabControl tabMain;
+ private Label lblStatus;
+
+ private string ConfigPath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "appConfig.json");
+ private string GameSettingsPath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "game_settings.json");
+
+ // 常规
+ private ComboBox cmbPlatform;
+ private CheckBox cbHideConsole, cbShowWindow;
+ // 网络
+ private NumericUpDown nudWsPort, nudProxyPort;
+ private CheckBox cbListenAny, cbProxyEnabled;
+ private TextBox txtUpstreamProxy;
+ // 弹幕过滤
+ private CheckBox cbPrintBarrage, cbBarrageLog;
+ private CheckedListBox clbPrintFilter, clbPushFilter, clbLogFilter;
+ // 轮询
+ private CheckBox cbForcePolling;
+ private NumericUpDown nudPollingInterval;
+ // 进程/域名
+ private TextBox txtProcessFilter, txtHostNameList, txtWebRoomIds;
+ private CheckBox cbFilterHostName;
+ // 直播伴侣
+ private TextBox txtLiveCompanPath;
+ private CheckBox cbLiveCompanHook, cbAutoPause;
+ // 串口
+ private TextBox txtComPort, txtComBaud;
+ // 游戏启动项
+ private TextBox txtGameExePath, txtGameWorkDir;
+ private CheckBox cbAutoLaunchGame;
+ // 游戏配置表路径
+ private TextBox txtConfigDir;
+ private TextBox txtEffectFile, txtMacroFile, txtGiftFile, txtCmdFile, txtFkeyFile;
+
+ private readonly Tuple[] MsgTypeOptions = {
+ Tuple.Create(1, "1-弹幕消息"),
+ Tuple.Create(2, "2-点赞消息"),
+ Tuple.Create(3, "3-进入直播间"),
+ Tuple.Create(4, "4-关注消息"),
+ Tuple.Create(5, "5-礼物消息"),
+ Tuple.Create(6, "6-统计消息"),
+ Tuple.Create(7, "7-粉丝团消息"),
+ Tuple.Create(8, "8-直播间分享"),
+ Tuple.Create(9, "9-下播"),
+ };
+
+ // 默认值
+ private const string DefaultGameExe = @"..\Fantasy.Map.Simulator\FantasyMapSimulator.exe";
+ private const string DefaultGameDir = @"..\Fantasy.Map.Simulator";
+ private const string DefaultConfigDir = @"..\Fantasy.Map.Simulator\BepInEx\config";
+
+ public SettingsForm()
+ {
+ this.Text = "弹幕插件设置";
+ this.ClientSize = new Size(620, 620);
+ this.StartPosition = FormStartPosition.CenterParent;
+ this.FormBorderStyle = FormBorderStyle.FixedDialog;
+ this.MaximizeBox = false;
+ BuildUI();
+ LoadAll();
+ }
+
+ void BuildUI()
+ {
+ tabMain = new TabControl { Dock = DockStyle.Top, Height = 540, Name = "tabMain" };
+ this.Controls.Add(tabMain);
+
+ var pnlBottom = new Panel { Dock = DockStyle.Bottom, Height = 50 };
+ this.Controls.Add(pnlBottom);
+
+ var btnSave = new Button { Text = "保存并应用", Width = 110, Height = 30, Left = 140, Top = 10, BackColor = Color.LightGreen };
+ btnSave.Click += (s, e) => SaveAll();
+ pnlBottom.Controls.Add(btnSave);
+
+ var btnCancel = new Button { Text = "取消", Width = 80, Height = 30, Left = 260, Top = 10 };
+ btnCancel.Click += (s, e) => this.Close();
+ pnlBottom.Controls.Add(btnCancel);
+
+ var btnReload = new Button { Text = "重新加载", Width = 90, Height = 30, Left = 350, Top = 10 };
+ btnReload.Click += (s, e) => LoadAll();
+ pnlBottom.Controls.Add(btnReload);
+
+ lblStatus = new Label { AutoSize = true, Left = 450, Top = 18, ForeColor = Color.DimGray, Text = "" };
+ pnlBottom.Controls.Add(lblStatus);
+
+ BuildGeneralTab();
+ BuildNetworkTab();
+ BuildBarrageFilterTab();
+ BuildPollingTab();
+ BuildProcessDomainTab();
+ BuildLiveCompanionTab();
+ BuildComPortTab();
+ BuildGameTab();
+ }
+
+ void BuildGeneralTab()
+ {
+ var page = new TabPage("常规");
+ page.Controls.Add(new Label { Text = "直播平台:", Left = 20, Top = 20, AutoSize = true });
+ cmbPlatform = new ComboBox { Left = 120, Top = 17, Width = 150, DropDownStyle = ComboBoxStyle.DropDownList };
+ cmbPlatform.Items.AddRange(new object[] { "douyin", "kuaishou" });
+ page.Controls.Add(cmbPlatform);
+
+ cbHideConsole = new CheckBox { Text = "隐藏控制台", Left = 20, Top = 60, AutoSize = true };
+ page.Controls.Add(cbHideConsole);
+
+ cbShowWindow = new CheckBox { Text = "显示主窗体", Left = 20, Top = 90, AutoSize = true };
+ page.Controls.Add(cbShowWindow);
+
+ page.Controls.Add(new Label { Text = "提示: 平台切换需重启程序生效", Left = 20, Top = 150, AutoSize = true, ForeColor = Color.Gray });
+ tabMain.TabPages.Add(page);
+ }
+
+ void BuildNetworkTab()
+ {
+ var page = new TabPage("网络");
+ page.Controls.Add(new Label { Text = "WebSocket 端口:", Left = 20, Top = 20, AutoSize = true });
+ nudWsPort = new NumericUpDown { Left = 150, Top = 17, Width = 80, Minimum = 1, Maximum = 65535 };
+ page.Controls.Add(nudWsPort);
+
+ cbListenAny = new CheckBox { Text = "监听任意IP(0.0.0.0)", Left = 20, Top = 55, AutoSize = true };
+ page.Controls.Add(cbListenAny);
+
+ page.Controls.Add(new Label { Text = "系统代理端口:", Left = 20, Top = 90, AutoSize = true });
+ nudProxyPort = new NumericUpDown { Left = 150, Top = 87, Width = 80, Minimum = 1, Maximum = 65535 };
+ page.Controls.Add(nudProxyPort);
+
+ cbProxyEnabled = new CheckBox { Text = "启用系统代理", Left = 20, Top = 125, AutoSize = true };
+ page.Controls.Add(cbProxyEnabled);
+
+ page.Controls.Add(new Label { Text = "上游代理地址:", Left = 20, Top = 160, AutoSize = true });
+ txtUpstreamProxy = new TextBox { Left = 150, Top = 157, Width = 250 };
+ page.Controls.Add(txtUpstreamProxy);
+ page.Controls.Add(new Label { Text = "(例如: 127.0.0.1:11223, 不要带http://)", Left = 150, Top = 183, AutoSize = true, ForeColor = Color.Gray });
+ tabMain.TabPages.Add(page);
+ }
+
+ void BuildBarrageFilterTab()
+ {
+ var page = new TabPage("弹幕过滤");
+ cbPrintBarrage = new CheckBox { Text = "控制台打印弹幕", Left = 10, Top = 10, AutoSize = true };
+ page.Controls.Add(cbPrintBarrage);
+ cbBarrageLog = new CheckBox { Text = "弹幕写入日志文件", Left = 200, Top = 10, AutoSize = true };
+ page.Controls.Add(cbBarrageLog);
+
+ page.Controls.Add(new Label { Text = "打印过滤器:", Left = 10, Top = 40, AutoSize = true });
+ clbPrintFilter = CreateFilterListBox();
+ clbPrintFilter.Location = new Point(10, 60);
+ page.Controls.Add(clbPrintFilter);
+
+ page.Controls.Add(new Label { Text = "推送过滤器:", Left = 220, Top = 40, AutoSize = true });
+ clbPushFilter = CreateFilterListBox();
+ clbPushFilter.Location = new Point(220, 60);
+ page.Controls.Add(clbPushFilter);
+
+ page.Controls.Add(new Label { Text = "日志过滤器:", Left = 430, Top = 40, AutoSize = true });
+ clbLogFilter = CreateFilterListBox();
+ clbLogFilter.Location = new Point(430, 60);
+ page.Controls.Add(clbLogFilter);
+ tabMain.TabPages.Add(page);
+ }
+
+ CheckedListBox CreateFilterListBox()
+ {
+ var clb = new CheckedListBox { Width = 200, Height = 200 };
+ foreach (var opt in MsgTypeOptions) clb.Items.Add(opt.Item2, false);
+ return clb;
+ }
+
+ void BuildPollingTab()
+ {
+ var page = new TabPage("轮询");
+ cbForcePolling = new CheckBox { Text = "强制启用轮询模式获取弹幕", Left = 20, Top = 20, AutoSize = true };
+ page.Controls.Add(cbForcePolling);
+ page.Controls.Add(new Label { Text = "轮询间隔(ms):", Left = 20, Top = 60, AutoSize = true });
+ nudPollingInterval = new NumericUpDown { Left = 150, Top = 57, Width = 80, Minimum = 500, Maximum = 60000, Increment = 500 };
+ page.Controls.Add(nudPollingInterval);
+ page.Controls.Add(new Label { Text = "轮询模式更稳定但响应较慢", Left = 20, Top = 100, AutoSize = true, ForeColor = Color.Gray });
+ page.Controls.Add(new Label { Text = "不建议小于1000ms", Left = 20, Top = 120, AutoSize = true, ForeColor = Color.Gray });
+ tabMain.TabPages.Add(page);
+ }
+
+ void BuildProcessDomainTab()
+ {
+ var page = new TabPage("进程/域名");
+ page.Controls.Add(new Label { Text = "进程过滤器(逗号分隔):", Left = 20, Top = 20, AutoSize = true });
+ txtProcessFilter = new TextBox { Left = 20, Top = 40, Width = 550, Multiline = true, Height = 40 };
+ page.Controls.Add(txtProcessFilter);
+ page.Controls.Add(new Label { Text = "只监听这些进程的弹幕数据", Left = 20, Top = 85, AutoSize = true, ForeColor = Color.Gray });
+
+ cbFilterHostName = new CheckBox { Text = "启用域名过滤(降低CPU占用)", Left = 20, Top = 115, AutoSize = true };
+ page.Controls.Add(cbFilterHostName);
+
+ page.Controls.Add(new Label { Text = "域名白名单(逗号分隔):", Left = 20, Top = 145, AutoSize = true });
+ txtHostNameList = new TextBox { Left = 20, Top = 165, Width = 550 };
+ page.Controls.Add(txtHostNameList);
+
+ page.Controls.Add(new Label { Text = "Web房间ID过滤(逗号分隔):", Left = 20, Top = 195, AutoSize = true });
+ txtWebRoomIds = new TextBox { Left = 20, Top = 215, Width = 550 };
+ page.Controls.Add(txtWebRoomIds);
+ page.Controls.Add(new Label { Text = "只监听指定房间,直播伴侣不支持", Left = 20, Top = 240, AutoSize = true, ForeColor = Color.Gray });
+ tabMain.TabPages.Add(page);
+ }
+
+ void BuildLiveCompanionTab()
+ {
+ var page = new TabPage("直播伴侣");
+ page.Controls.Add(new Label { Text = "直播伴侣exe路径:", Left = 20, Top = 20, AutoSize = true });
+ txtLiveCompanPath = new TextBox { Left = 20, Top = 40, Width = 450 };
+ page.Controls.Add(txtLiveCompanPath);
+ var btnBrowse = new Button { Text = "浏览...", Left = 480, Top = 39, Width = 70 };
+ btnBrowse.Click += (s, e) =>
+ {
+ using (var dlg = new OpenFileDialog { Filter = "EXE文件|*.exe|所有文件|*.*" })
+ if (dlg.ShowDialog() == DialogResult.OK) txtLiveCompanPath.Text = dlg.FileName;
+ };
+ page.Controls.Add(btnBrowse);
+ page.Controls.Add(new Label { Text = "(留空则程序自动查找)", Left = 20, Top = 65, AutoSize = true, ForeColor = Color.Gray });
+
+ cbLiveCompanHook = new CheckBox { Text = "Hook直播伴侣代理", Left = 20, Top = 95, AutoSize = true };
+ page.Controls.Add(cbLiveCompanHook);
+ cbAutoPause = new CheckBox { Text = "进入直播间自动暂停", Left = 20, Top = 125, AutoSize = true };
+ page.Controls.Add(cbAutoPause);
+ tabMain.TabPages.Add(page);
+ }
+
+ void BuildComPortTab()
+ {
+ var page = new TabPage("串口");
+ page.Controls.Add(new Label { Text = "COM端口:", Left = 20, Top = 20, AutoSize = true });
+ txtComPort = new TextBox { Left = 120, Top = 17, Width = 100 };
+ page.Controls.Add(txtComPort);
+ page.Controls.Add(new Label { Text = "例如: COM1", Left = 230, Top = 20, AutoSize = true, ForeColor = Color.Gray });
+ page.Controls.Add(new Label { Text = "波特率:", Left = 20, Top = 55, AutoSize = true });
+ txtComBaud = new TextBox { Left = 120, Top = 52, Width = 100 };
+ page.Controls.Add(txtComBaud);
+ page.Controls.Add(new Label { Text = "例如: 9600", Left = 230, Top = 55, AutoSize = true, ForeColor = Color.Gray });
+ page.Controls.Add(new Label { Text = "留空则不启用串口抄送服务", Left = 20, Top = 95, AutoSize = true, ForeColor = Color.Gray });
+ tabMain.TabPages.Add(page);
+ }
+
+ void BuildGameTab()
+ {
+ var page = new TabPage("游戏启动项");
+ int y = 15;
+
+ // === 启动项 ===
+ page.Controls.Add(new Label { Text = "── 启动项 ──", Left = 20, Top = y, AutoSize = true, ForeColor = Color.SteelBlue, Font = new Font(Font, FontStyle.Bold) });
+ y += 25;
+
+ page.Controls.Add(new Label { Text = "游戏exe路径:", Left = 20, Top = y, AutoSize = true });
+ txtGameExePath = new TextBox { Left = 120, Top = y - 3, Width = 380 };
+ page.Controls.Add(txtGameExePath);
+ var btnBrowse1 = new Button { Text = "浏览...", Left = 510, Top = y - 4, Width = 70 };
+ btnBrowse1.Click += (s, e) =>
+ {
+ using (var dlg = new OpenFileDialog { Filter = "EXE文件|*.exe|所有文件|*.*" })
+ if (dlg.ShowDialog() == DialogResult.OK)
+ {
+ txtGameExePath.Text = ToRelativePath(dlg.FileName);
+ var dir = Path.GetDirectoryName(dlg.FileName);
+ if (!string.IsNullOrEmpty(dir)) txtGameWorkDir.Text = ToRelativePath(dir);
+ }
+ };
+ page.Controls.Add(btnBrowse1);
+ y += 27;
+
+ page.Controls.Add(new Label { Text = "工作目录:", Left = 20, Top = y, AutoSize = true });
+ txtGameWorkDir = new TextBox { Left = 120, Top = y - 3, Width = 380 };
+ page.Controls.Add(txtGameWorkDir);
+ y += 27;
+
+ cbAutoLaunchGame = new CheckBox { Text = "启动时自动打开游戏", Left = 20, Top = y, AutoSize = true };
+ page.Controls.Add(cbAutoLaunchGame);
+ y += 30;
+
+ // === 配置表路径 ===
+ page.Controls.Add(new Label { Text = "── 配置表路径 ──", Left = 20, Top = y, AutoSize = true, ForeColor = Color.SteelBlue, Font = new Font(Font, FontStyle.Bold) });
+ y += 25;
+
+ page.Controls.Add(new Label { Text = "配置表目录:", Left = 20, Top = y, AutoSize = true });
+ txtConfigDir = new TextBox { Left = 120, Top = y - 3, Width = 380 };
+ page.Controls.Add(txtConfigDir);
+ var btnBrowse2 = new Button { Text = "浏览...", Left = 510, Top = y - 4, Width = 70 };
+ btnBrowse2.Click += (s, e) =>
+ {
+ using (var dlg = new FolderBrowserDialog())
+ if (dlg.ShowDialog() == DialogResult.OK) txtConfigDir.Text = ToRelativePath(dlg.SelectedPath);
+ };
+ page.Controls.Add(btnBrowse2);
+ y += 30;
+
+ // 5个配置表文件名
+ txtEffectFile = AddConfigFileRow(page, ref y, "基础效果(effect):", "effect_config.txt");
+ txtMacroFile = AddConfigFileRow(page, ref y, "组合宏(macro):", "macro_config.txt");
+ txtGiftFile = AddConfigFileRow(page, ref y, "礼物事件(gift):", "gift_config.txt");
+ txtCmdFile = AddConfigFileRow(page, ref y, "弹幕指令(command):", "command_config.txt");
+ txtFkeyFile = AddConfigFileRow(page, ref y, "F键映射(fkey):", "fkey_config.txt");
+ y += 5;
+
+ // === 功能按钮 ===
+ var btnOpenGameConfig = new Button { Text = "打开配置编辑器", Left = 20, Top = y, Width = 130, Height = 30 };
+ btnOpenGameConfig.Click += (s, e) => { using (var f = new ConfigEditorForm()) f.ShowDialog(this); };
+ page.Controls.Add(btnOpenGameConfig);
+
+ var btnOpenConfigDir = new Button { Text = "打开配置目录", Left = 160, Top = y, Width = 110 };
+ btnOpenConfigDir.Click += (s, e) =>
+ {
+ var dir = Program.ResolveFullPath(txtConfigDir.Text.Trim());
+ if (!string.IsNullOrEmpty(dir) && Directory.Exists(dir))
+ System.Diagnostics.Process.Start("explorer.exe", dir);
+ else
+ MessageBox.Show($"配置目录不存在:\n{dir}", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
+ };
+ page.Controls.Add(btnOpenConfigDir);
+
+ var btnResetDefaults = new Button { Text = "恢复默认", Left = 280, Top = y, Width = 80 };
+ btnResetDefaults.Click += (s, e) => ResetGameDefaults();
+ page.Controls.Add(btnResetDefaults);
+
+ tabMain.TabPages.Add(page);
+ }
+
+ TextBox AddConfigFileRow(TabPage page, ref int y, string label, string defaultName)
+ {
+ page.Controls.Add(new Label { Text = label, Left = 30, Top = y, AutoSize = true });
+ var txt = new TextBox { Left = 180, Top = y - 3, Width = 200, Text = defaultName };
+ page.Controls.Add(txt);
+ y += 24;
+ return txt;
+ }
+
+ /// 将绝对路径转为相对插件目录的相对路径;若转换失败或已是相对路径则原样返回。
+ static string ToRelativePath(string absPath)
+ {
+ if (string.IsNullOrWhiteSpace(absPath)) return absPath;
+ if (!Path.IsPathRooted(absPath)) return absPath; // 已是相对路径
+ try
+ {
+ var baseDir = AppDomain.CurrentDomain.BaseDirectory;
+ var fullBase = Path.GetFullPath(baseDir.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
+ var fullAbs = Path.GetFullPath(absPath);
+ var baseUri = new Uri(fullBase + Path.DirectorySeparatorChar);
+ var absUri = new Uri(fullAbs);
+ var rel = baseUri.MakeRelativeUri(absUri).ToString();
+ // Uri 返回的是用 / 分隔的相对路径,统一为 Windows 分隔符
+ rel = Uri.UnescapeDataString(rel).Replace('/', Path.DirectorySeparatorChar);
+ return rel;
+ }
+ catch { return absPath; }
+ }
+
+ void ResetGameDefaults()
+ {
+ txtGameExePath.Text = DefaultGameExe;
+ txtGameWorkDir.Text = DefaultGameDir;
+ txtConfigDir.Text = DefaultConfigDir;
+ txtEffectFile.Text = "effect_config.txt";
+ txtMacroFile.Text = "macro_config.txt";
+ txtGiftFile.Text = "gift_config.txt";
+ txtCmdFile.Text = "command_config.txt";
+ txtFkeyFile.Text = "fkey_config.txt";
+ cbAutoLaunchGame.Checked = true;
+ }
+
+ // ═══════════════ 加载 ═══════════════
+
+ void LoadAll()
+ {
+ try
+ {
+ LoadAppConfig();
+ LoadGameSettings();
+ lblStatus.Text = "已加载";
+ lblStatus.ForeColor = Color.DimGray;
+ }
+ catch (Exception ex)
+ {
+ lblStatus.Text = $"加载失败: {ex.Message}";
+ lblStatus.ForeColor = Color.Red;
+ }
+ }
+
+ void LoadAppConfig()
+ {
+ if (!File.Exists(ConfigPath)) return;
+ var config = JObject.Parse(File.ReadAllText(ConfigPath), new JsonLoadSettings { CommentHandling = CommentHandling.Ignore });
+ var app = config["app"];
+
+ var general = app["general"];
+ cmbPlatform.SelectedItem = general["platform"]?.Value() ?? "douyin";
+ cbHideConsole.Checked = general["hideConsole"]?.Value() ?? false;
+ cbShowWindow.Checked = general["showWindow"]?.Value() ?? false;
+
+ var proxy = app["network"]["proxy"];
+ nudProxyPort.Value = proxy["port"]?.Value() ?? 8827;
+ cbProxyEnabled.Checked = proxy["enabled"]?.Value() ?? true;
+ txtUpstreamProxy.Text = proxy["upstreamAddress"]?.Value() ?? "";
+ var ws = app["network"]["websocket"];
+ nudWsPort.Value = ws["listenPort"]?.Value() ?? 8888;
+ cbListenAny.Checked = ws["listenAny"]?.Value() ?? true;
+
+ var barrage = app["barrage"];
+ cbPrintBarrage.Checked = barrage["printEnabled"]?.Value() ?? true;
+ cbBarrageLog.Checked = barrage["barrageFileLog"]?.Value() ?? false;
+ SetFilterChecks(clbPrintFilter, barrage["printFilter"]?.Value() ?? "");
+ SetFilterChecks(clbPushFilter, barrage["pushFilter"]?.Value() ?? "");
+ SetFilterChecks(clbLogFilter, barrage["logFilter"]?.Value() ?? "1,2,4,5,6,7,8");
+
+ var polling = barrage["polling"];
+ cbForcePolling.Checked = polling["enabled"]?.Value() ?? false;
+ nudPollingInterval.Value = polling["interval"]?.Value() ?? 3000;
+
+ var filtering = app["filtering"];
+ txtProcessFilter.Text = filtering["processFilter"]?.Value() ?? "";
+ cbFilterHostName.Checked = filtering["hostNameEnabled"]?.Value() ?? true;
+ txtHostNameList.Text = filtering["hostNameList"]?.Value() ?? "";
+ txtWebRoomIds.Text = filtering["webRoomIds"]?.Value() ?? "";
+
+ var liveCompanion = app["liveCompanion"];
+ txtLiveCompanPath.Text = liveCompanion["path"]?.Value() ?? "";
+ cbLiveCompanHook.Checked = liveCompanion["hookEnabled"]?.Value() ?? false;
+ cbAutoPause.Checked = liveCompanion["autoPause"]?.Value() ?? true;
+
+ var comPortStr = app["comPort"]["config"]?.Value() ?? "";
+ if (!string.IsNullOrWhiteSpace(comPortStr))
+ {
+ var parts = comPortStr.Split(':');
+ if (parts.Length >= 2) { txtComPort.Text = parts[0].Trim(); txtComBaud.Text = parts[1].Trim(); }
+ }
+ }
+
+ void LoadGameSettings()
+ {
+ // 默认值
+ txtGameExePath.Text = DefaultGameExe;
+ txtGameWorkDir.Text = DefaultGameDir;
+ cbAutoLaunchGame.Checked = true;
+ txtConfigDir.Text = DefaultConfigDir;
+ txtEffectFile.Text = "effect_config.txt";
+ txtMacroFile.Text = "macro_config.txt";
+ txtGiftFile.Text = "gift_config.txt";
+ txtCmdFile.Text = "command_config.txt";
+ txtFkeyFile.Text = "fkey_config.txt";
+
+ if (File.Exists(GameSettingsPath))
+ {
+ try
+ {
+ var json = JObject.Parse(File.ReadAllText(GameSettingsPath));
+ txtGameExePath.Text = json["gameExePath"]?.Value() ?? txtGameExePath.Text;
+ txtGameWorkDir.Text = json["gameWorkDir"]?.Value() ?? txtGameWorkDir.Text;
+ cbAutoLaunchGame.Checked = json["autoLaunchGame"]?.Value() ?? true;
+ txtConfigDir.Text = json["configDir"]?.Value() ?? txtConfigDir.Text;
+
+ var cfg = json["configFiles"];
+ if (cfg != null)
+ {
+ txtEffectFile.Text = cfg["effect"]?.Value() ?? txtEffectFile.Text;
+ txtMacroFile.Text = cfg["macro"]?.Value() ?? txtMacroFile.Text;
+ txtGiftFile.Text = cfg["gift"]?.Value() ?? txtGiftFile.Text;
+ txtCmdFile.Text = cfg["command"]?.Value() ?? txtCmdFile.Text;
+ txtFkeyFile.Text = cfg["fkey"]?.Value() ?? txtFkeyFile.Text;
+ }
+ }
+ catch { }
+ }
+ Program.AutoLaunchGame = cbAutoLaunchGame.Checked;
+ }
+
+ void SetFilterChecks(CheckedListBox clb, string filterStr)
+ {
+ for (int i = 0; i < clb.Items.Count; i++) clb.SetItemChecked(i, false);
+ if (string.IsNullOrWhiteSpace(filterStr)) return;
+ var ids = filterStr.Split(',').Select(s => s.Trim()).Where(s => !string.IsNullOrEmpty(s)).ToList();
+ for (int i = 0; i < clb.Items.Count; i++)
+ {
+ if (ids.Contains(MsgTypeOptions[i].Item1.ToString()))
+ clb.SetItemChecked(i, true);
+ }
+ }
+
+ string GetFilterString(CheckedListBox clb)
+ {
+ var ids = new List();
+ for (int i = 0; i < clb.Items.Count; i++)
+ if (clb.GetItemChecked(i)) ids.Add(MsgTypeOptions[i].Item1.ToString());
+ return string.Join(",", ids);
+ }
+
+ // ═══════════════ 保存 ═══════════════
+
+ void SaveAll()
+ {
+ try
+ {
+ SaveAppConfig();
+ SaveGameSettings();
+ AppSetting.Current.LoadFromJson(ConfigPath);
+ lblStatus.Text = "已保存(部分设置需重启生效)";
+ lblStatus.ForeColor = Color.Green;
+ }
+ catch (Exception ex)
+ {
+ lblStatus.Text = $"保存失败: {ex.Message}";
+ lblStatus.ForeColor = Color.Red;
+ }
+ }
+
+ void SaveAppConfig()
+ {
+ if (!File.Exists(ConfigPath))
+ {
+ var json = new JObject(new JProperty("app", new JObject(
+ new JProperty("general", new JObject(
+ new JProperty("hideConsole", cbHideConsole.Checked),
+ new JProperty("showWindow", cbShowWindow.Checked),
+ new JProperty("platform", cmbPlatform.SelectedItem?.ToString() ?? "douyin"))),
+ new JProperty("network", new JObject(
+ new JProperty("proxy", new JObject(
+ new JProperty("port", (int)nudProxyPort.Value),
+ new JProperty("enabled", cbProxyEnabled.Checked),
+ new JProperty("upstreamAddress", txtUpstreamProxy.Text.Trim()))),
+ new JProperty("websocket", new JObject(
+ new JProperty("listenPort", (int)nudWsPort.Value),
+ new JProperty("listenAny", cbListenAny.Checked))))),
+ new JProperty("filtering", new JObject(
+ new JProperty("processFilter", txtProcessFilter.Text.Trim()),
+ new JProperty("hostNameEnabled", cbFilterHostName.Checked),
+ new JProperty("hostNameList", txtHostNameList.Text.Trim()),
+ new JProperty("webRoomIds", txtWebRoomIds.Text.Trim()))),
+ new JProperty("barrage", new JObject(
+ new JProperty("printEnabled", cbPrintBarrage.Checked),
+ new JProperty("printFilter", GetFilterString(clbPrintFilter)),
+ new JProperty("pushFilter", GetFilterString(clbPushFilter)),
+ new JProperty("barrageFileLog", cbBarrageLog.Checked),
+ new JProperty("logFilter", GetFilterString(clbLogFilter)),
+ new JProperty("polling", new JObject(
+ new JProperty("enabled", cbForcePolling.Checked),
+ new JProperty("interval", (int)nudPollingInterval.Value))))),
+ new JProperty("liveCompanion", new JObject(
+ new JProperty("path", txtLiveCompanPath.Text.Trim()),
+ new JProperty("hookEnabled", cbLiveCompanHook.Checked),
+ new JProperty("autoPause", cbAutoPause.Checked))),
+ new JProperty("comPort", new JObject(
+ new JProperty("config", BuildComPortConfig()))))));
+ File.WriteAllText(ConfigPath, json.ToString(), Encoding.UTF8);
+ return;
+ }
+
+ var config = JObject.Parse(File.ReadAllText(ConfigPath), new JsonLoadSettings { CommentHandling = CommentHandling.Ignore });
+ var app = config["app"];
+ app["general"]["platform"] = cmbPlatform.SelectedItem?.ToString() ?? "douyin";
+ app["general"]["hideConsole"] = cbHideConsole.Checked;
+ app["general"]["showWindow"] = cbShowWindow.Checked;
+ app["network"]["proxy"]["port"] = (int)nudProxyPort.Value;
+ app["network"]["proxy"]["enabled"] = cbProxyEnabled.Checked;
+ app["network"]["proxy"]["upstreamAddress"] = txtUpstreamProxy.Text.Trim();
+ app["network"]["websocket"]["listenPort"] = (int)nudWsPort.Value;
+ app["network"]["websocket"]["listenAny"] = cbListenAny.Checked;
+ app["filtering"]["processFilter"] = txtProcessFilter.Text.Trim();
+ app["filtering"]["hostNameEnabled"] = cbFilterHostName.Checked;
+ app["filtering"]["hostNameList"] = txtHostNameList.Text.Trim();
+ app["filtering"]["webRoomIds"] = txtWebRoomIds.Text.Trim();
+ app["barrage"]["printEnabled"] = cbPrintBarrage.Checked;
+ app["barrage"]["printFilter"] = GetFilterString(clbPrintFilter);
+ app["barrage"]["pushFilter"] = GetFilterString(clbPushFilter);
+ app["barrage"]["barrageFileLog"] = cbBarrageLog.Checked;
+ app["barrage"]["logFilter"] = GetFilterString(clbLogFilter);
+ app["barrage"]["polling"]["enabled"] = cbForcePolling.Checked;
+ app["barrage"]["polling"]["interval"] = (int)nudPollingInterval.Value;
+ app["liveCompanion"]["path"] = txtLiveCompanPath.Text.Trim();
+ app["liveCompanion"]["hookEnabled"] = cbLiveCompanHook.Checked;
+ app["liveCompanion"]["autoPause"] = cbAutoPause.Checked;
+ app["comPort"]["config"] = BuildComPortConfig();
+ File.WriteAllText(ConfigPath, config.ToString(), Encoding.UTF8);
+ }
+
+ string BuildComPortConfig()
+ {
+ var port = txtComPort.Text.Trim();
+ var baud = txtComBaud.Text.Trim();
+ if (string.IsNullOrEmpty(port)) return "";
+ if (string.IsNullOrEmpty(baud)) baud = "9600";
+ return $"{port}:{baud}";
+ }
+
+ void SaveGameSettings()
+ {
+ var json = new JObject(
+ new JProperty("gameExePath", txtGameExePath.Text.Trim()),
+ new JProperty("gameWorkDir", txtGameWorkDir.Text.Trim()),
+ new JProperty("autoLaunchGame", cbAutoLaunchGame.Checked),
+ new JProperty("configDir", txtConfigDir.Text.Trim()),
+ new JProperty("configFiles", new JObject(
+ new JProperty("effect", txtEffectFile.Text.Trim()),
+ new JProperty("macro", txtMacroFile.Text.Trim()),
+ new JProperty("gift", txtGiftFile.Text.Trim()),
+ new JProperty("command", txtCmdFile.Text.Trim()),
+ new JProperty("fkey", txtFkeyFile.Text.Trim()))));
+ File.WriteAllText(GameSettingsPath, json.ToString(), Encoding.UTF8);
+
+ // 同步到 Program
+ Program.AutoLaunchGame = cbAutoLaunchGame.Checked;
+ Program.SetGamePaths(txtGameExePath.Text.Trim(), txtGameWorkDir.Text.Trim());
+ Program.SetConfigPaths(txtConfigDir.Text.Trim(),
+ txtEffectFile.Text.Trim(), txtMacroFile.Text.Trim(),
+ txtGiftFile.Text.Trim(), txtCmdFile.Text.Trim(),
+ txtFkeyFile.Text.Trim());
+ }
+ }
+}
diff --git a/BarrageGrab/Modles/BiliProtoEntity/BiliMessages.cs b/BarrageGrab/Modles/BiliProtoEntity/BiliMessages.cs
new file mode 100644
index 0000000..ca182df
--- /dev/null
+++ b/BarrageGrab/Modles/BiliProtoEntity/BiliMessages.cs
@@ -0,0 +1,192 @@
+using System.Collections.Generic;
+using Newtonsoft.Json.Linq;
+
+namespace BarrageGrab.Modles.BiliProtoEntity
+{
+ ///
+ /// B站直播弹幕WS协议操作码
+ ///
+ public enum BiliOperation
+ {
+ /// 客户端心跳
+ HEARTBEAT = 2,
+ /// 服务器心跳回复 (人气值)
+ HEARTBEAT_REPLY = 3,
+ /// 普通消息 (弹幕/礼物/进房等)
+ MESSAGE = 5,
+ /// 客户端认证请求
+ AUTH = 7,
+ /// 服务器认证回复
+ AUTH_REPLY = 8
+ }
+
+ ///
+ /// B站WS协议版本 (影响Payload压缩方式)
+ ///
+ public enum BiliProtocolVersion
+ {
+ /// JSON明文
+ PLAIN = 0,
+ /// 认证用JSON
+ PLAIN_AUTH = 1,
+ /// Zlib压缩 (内部可能含多个包)
+ ZLIB = 2,
+ /// Brotli压缩 (内部可能含多个包)
+ BROTLI = 3
+ }
+
+ ///
+ /// B站WS协议帧头 (16字节, Big-Endian)
+ ///
+ public struct BiliSocketFrame
+ {
+ /// 整个包长度 (header+body)
+ public uint PacketLength;
+ /// 头长度 (固定16)
+ public ushort HeaderLength;
+ /// 协议版本
+ public BiliProtocolVersion ProtocolVersion;
+ /// 操作码
+ public BiliOperation Operation;
+ /// 序列号 (固定1)
+ public uint SequenceId;
+
+ public const int HEADER_SIZE = 16;
+
+ /// 从字节缓冲区解析帧头
+ public static bool TryParse(byte[] buffer, int offset, int count, out BiliSocketFrame frame)
+ {
+ frame = default(BiliSocketFrame);
+ if (buffer == null || count < HEADER_SIZE) return false;
+ // Big-Endian 解析
+ frame.PacketLength = ReadUInt32BE(buffer, offset);
+ frame.HeaderLength = ReadUInt16BE(buffer, offset + 4);
+ frame.ProtocolVersion = (BiliProtocolVersion)ReadUInt16BE(buffer, offset + 6);
+ frame.Operation = (BiliOperation)ReadUInt32BE(buffer, offset + 8);
+ frame.SequenceId = ReadUInt32BE(buffer, offset + 12);
+ return frame.HeaderLength >= HEADER_SIZE && frame.PacketLength >= HEADER_SIZE;
+ }
+
+ /// 帧体数据 (从 HeaderLength 开始到 PacketLength)
+ public static byte[] GetPayload(byte[] buffer, int offset, BiliSocketFrame frame)
+ {
+ int bodyLen = (int)frame.PacketLength - frame.HeaderLength;
+ if (bodyLen <= 0) return new byte[0];
+ var data = new byte[bodyLen];
+ System.Array.Copy(buffer, offset + frame.HeaderLength, data, 0, bodyLen);
+ return data;
+ }
+
+ public static uint ReadUInt32BE(byte[] buf, int off)
+ {
+ return (uint)((buf[off] << 24) | (buf[off + 1] << 16) | (buf[off + 2] << 8) | buf[off + 3]);
+ }
+
+ public static ushort ReadUInt16BE(byte[] buf, int off)
+ {
+ return (ushort)((buf[off] << 8) | buf[off + 1]);
+ }
+ }
+
+ ///
+ /// B站弹幕消息 (cmd字段用于路由)
+ ///
+ public class BiliMessage
+ {
+ /// 消息类型 (DANMU_MSG / SEND_GIFT / INTERACT_WORD 等)
+ public string Cmd { get; set; }
+
+ /// 原始数据
+ public JObject Raw { get; set; }
+ }
+
+ ///
+ /// B站弹幕消息 cmd 列表 (用于识别已知类型)
+ ///
+ public static class BiliCmd
+ {
+ public const string DANMU_MSG = "DANMU_MSG";
+ public const string SEND_GIFT = "SEND_GIFT";
+ public const string INTERACT_WORD = "INTERACT_WORD";
+ public const string LIKE_INFO_V3_CLICK = "LIKE_INFO_V3_CLICK";
+ public const string ONLINE_RANK_COUNT = "ONLINE_RANK_COUNT";
+ public const string WATCHED_CHANGE = "WATCHED_CHANGE";
+ public const string LIVE = "LIVE";
+ public const string PREPARING = "PREPARING";
+ public const string ROOM_REAL_TIME_MESSAGE_UPDATE = "ROOM_REAL_TIME_MESSAGE_UPDATE";
+ public const string SUPER_CHAT_MESSAGE = "SUPER_CHAT_MESSAGE";
+ }
+
+ ///
+ /// B站弹幕消息(DANMU_MSG)简化结构
+ ///
+ public class BiliDanmuMsg
+ {
+ /// 弹幕内容 (info[1])
+ public string Content { get; set; }
+ /// 用户UID (info[2][0])
+ public long Uid { get; set; }
+ /// 用户昵称 (info[2][1])
+ public string Uname { get; set; }
+ /// 管理员? (info[2][2]==1)
+ public bool IsAdmin { get; set; }
+ /// 主播? (info[2][4]存在)
+ public bool IsAnchor { get; set; }
+ }
+
+ ///
+ /// B站礼物消息(SEND_GIFT.data)简化结构
+ ///
+ public class BiliGiftData
+ {
+ public long Uid { get; set; }
+ public string Uname { get; set; }
+ public string Face { get; set; }
+ public uint GiftId { get; set; }
+ public string GiftName { get; set; }
+ /// 礼物数量
+ public uint Num { get; set; }
+ /// 本批次礼物数 (连击时为本次增量)
+ public uint ComboNum { get; set; }
+ /// 礼物连击总数
+ public uint SuperBatchNum { get; set; }
+ /// 价格 (1 = 1000金瓜子)
+ public long Price { get; set; }
+ /// 金币类型 gold=付费 silver=免费
+ public string CoinType { get; set; }
+ /// 礼物图片
+ public string GifUrl { get; set; }
+ }
+
+ ///
+ /// B站进房/关注消息(INTERACT_WORD.data)简化结构
+ ///
+ public class BiliInteractData
+ {
+ public long Uid { get; set; }
+ public string Uname { get; set; }
+ /// 1=进入 2=关注 3=分享
+ public int MsgType { get; set; }
+ }
+
+ ///
+ /// B站点赞消息(LIKE_INFO_V3_CLICK.data)简化结构
+ ///
+ public class BiliLikeData
+ {
+ public long Uid { get; set; }
+ public string Uname { get; set; }
+ public string LikeText { get; set; }
+ }
+
+ ///
+ /// B站房间统计(ONLINE_RANK_COUNT.data / ROOM_REAL_TIME_MESSAGE_UPDATE.data)简化结构
+ ///
+ public class BiliRoomStats
+ {
+ /// 在线人数
+ public long Online { get; set; }
+ /// 看过人数
+ public long Watched { get; set; }
+ }
+}
diff --git a/BarrageGrab/Modles/JsonEntity/BarrageMessages.cs b/BarrageGrab/Modles/JsonEntity/BarrageMessages.cs
index 7c8b844..fb7d0ae 100644
--- a/BarrageGrab/Modles/JsonEntity/BarrageMessages.cs
+++ b/BarrageGrab/Modles/JsonEntity/BarrageMessages.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
@@ -6,6 +6,25 @@ using Org.BouncyCastle.Asn1.Crmf;
namespace BarrageGrab.Modles.JsonEntity
{
+ ///
+ /// 直播平台
+ ///
+ public enum LivePlatform
+ {
+ ///
+ /// 抖音
+ ///
+ Douyin = 0,
+ ///
+ /// 快手
+ ///
+ Kuaishou = 1,
+ ///
+ /// 哔哩哔哩
+ ///
+ Bilibili = 2
+ }
+
///
/// 弹幕消息类型
///
@@ -96,6 +115,11 @@ namespace BarrageGrab.Modles.JsonEntity
///
public string ProcessName { get; set; }
+ ///
+ /// 直播平台
+ ///
+ public LivePlatform Platform { get; set; } = LivePlatform.Douyin;
+
///
/// 消息对象
///
@@ -106,11 +130,12 @@ namespace BarrageGrab.Modles.JsonEntity
}
- public BarrageMsgPack(string data, PackMsgType type, string processName)
+ public BarrageMsgPack(string data, PackMsgType type, string processName, LivePlatform platform = LivePlatform.Douyin)
{
Data = data;
Type = type;
ProcessName = processName;
+ Platform = platform;
}
///
diff --git a/BarrageGrab/Modles/KsProtoEntity/KsMessages.cs b/BarrageGrab/Modles/KsProtoEntity/KsMessages.cs
new file mode 100644
index 0000000..4840da3
--- /dev/null
+++ b/BarrageGrab/Modles/KsProtoEntity/KsMessages.cs
@@ -0,0 +1,358 @@
+using System.Collections.Generic;
+using ProtoBuf;
+
+namespace BarrageGrab.Modles.KsProtoEntity
+{
+ [ProtoContract]
+ public class KsSocketMessage
+ {
+ [ProtoMember(1)]
+ public KsPayloadType PayloadType { get; set; }
+
+ [ProtoMember(2)]
+ public KsCompressionType CompressionType { get; set; }
+
+ [ProtoMember(3)]
+ public byte[] Payload { get; set; }
+ }
+
+ public enum KsCompressionType
+ {
+ UNKNOWN = 0,
+ NONE = 1,
+ GZIP = 2,
+ AES = 3
+ }
+
+ public enum KsPayloadType
+ {
+ UNKNOWN = 0,
+ CS_HEARTBEAT = 1,
+ CS_ERROR = 3,
+ CS_PING = 4,
+ PS_HOST_INFO = 51,
+ SC_HEARTBEAT_ACK = 101,
+ SC_ECHO = 102,
+ SC_ERROR = 103,
+ SC_PING_ACK = 104,
+ SC_INFO = 105,
+ CS_ENTER_ROOM = 200,
+ CS_USER_PAUSE = 201,
+ CS_USER_EXIT = 202,
+ CS_AUTHOR_PUSH_TRAFFIC_ZERO = 203,
+ CS_HORSE_RACING = 204,
+ CS_RACE_LOSE = 205,
+ CS_VOIP_SIGNAL = 206,
+ SC_ENTER_ROOM_ACK = 300,
+ SC_AUTHOR_PAUSE = 301,
+ SC_AUTHOR_RESUME = 302,
+ SC_AUTHOR_PUSH_TRAFFIC_ZERO = 303,
+ SC_AUTHOR_HEARTBEAT_MISS = 304,
+ SC_PIP_STARTED = 305,
+ SC_PIP_ENDED = 306,
+ SC_HORSE_RACING_ACK = 307,
+ SC_VOIP_SIGNAL = 308,
+ SC_FEED_PUSH = 310,
+ SC_ASSISTANT_STATUS = 311,
+ SC_REFRESH_WALLET = 312,
+ SC_LIVE_CHAT_CALL = 320,
+ SC_LIVE_CHAT_CALL_ACCEPTED = 321,
+ SC_LIVE_CHAT_CALL_REJECTED = 322,
+ SC_LIVE_CHAT_READY = 323,
+ SC_LIVE_CHAT_GUEST_END = 324,
+ SC_LIVE_CHAT_ENDED = 325,
+ SC_RENDERING_MAGIC_FACE_DISABLE = 326,
+ SC_RENDERING_MAGIC_FACE_ENABLE = 327,
+ SC_RED_PACK_FEED = 330,
+ SC_LIVE_WATCHING_LIST = 340,
+ SC_LIVE_QUIZ_QUESTION_ASKED = 350,
+ SC_LIVE_QUIZ_QUESTION_REVIEWED = 351,
+ SC_LIVE_QUIZ_SYNC = 352,
+ SC_LIVE_QUIZ_ENDED = 353,
+ SC_LIVE_QUIZ_WINNERS = 354,
+ SC_SUSPECTED_VIOLATION = 355,
+ SC_SHOP_OPENED = 360,
+ SC_SHOP_CLOSED = 361,
+ SC_GUESS_OPENED = 370,
+ SC_GUESS_CLOSED = 371,
+ SC_PK_INVITATION = 380,
+ SC_PK_STATISTIC = 381,
+ SC_RIDDLE_OPENED = 390,
+ SC_RIDDLE_CLOESED = 391,
+ SC_RIDE_CHANGED = 412,
+ SC_BET_CHANGED = 441,
+ SC_BET_CLOSED = 442,
+ SC_LIVE_SPECIAL_ACCOUNT_CONFIG_STATE = 645
+ }
+
+ [ProtoContract]
+ public class KsSCWebFeedPush
+ {
+ [ProtoMember(1)]
+ public string DisplayWatchingCount { get; set; }
+
+ [ProtoMember(2)]
+ public string DisplayLikeCount { get; set; }
+
+ [ProtoMember(3)]
+ public ulong PendingLikeCount { get; set; }
+
+ [ProtoMember(4)]
+ public ulong PushInterval { get; set; }
+
+ [ProtoMember(5)]
+ public List CommentFeeds { get; } = new List();
+
+ [ProtoMember(6)]
+ public string CommentCursor { get; set; }
+
+ [ProtoMember(7)]
+ public List ComboCommentFeed { get; } = new List();
+
+ [ProtoMember(8)]
+ public List LikeFeeds { get; } = new List();
+
+ [ProtoMember(9)]
+ public List GiftFeeds { get; } = new List();
+
+ [ProtoMember(10)]
+ public string GiftCursor { get; set; }
+
+ [ProtoMember(11)]
+ public List SystemNoticeFeeds { get; } = new List();
+
+ [ProtoMember(12)]
+ public List ShareFeeds { get; } = new List();
+ }
+
+ [ProtoContract]
+ public class KsWebCommentFeed
+ {
+ [ProtoMember(1)]
+ public string Id { get; set; }
+
+ [ProtoMember(2)]
+ public KsSimpleUserInfo User { get; set; }
+
+ [ProtoMember(3)]
+ public string Content { get; set; }
+
+ [ProtoMember(4)]
+ public string DeviceHash { get; set; }
+
+ [ProtoMember(5)]
+ public ulong SortRank { get; set; }
+
+ [ProtoMember(6)]
+ public string Color { get; set; }
+
+ [ProtoMember(7)]
+ public KsWebCommentFeedShowType ShowType { get; set; }
+ }
+
+ public enum KsWebCommentFeedShowType
+ {
+ FEED_SHOW_UNKNOWN = 0,
+ FEED_SHOW_NORMAL = 1,
+ FEED_HIDDEN = 2
+ }
+
+ [ProtoContract]
+ public class KsWebComboCommentFeed
+ {
+ [ProtoMember(1)]
+ public string Id { get; set; }
+
+ [ProtoMember(2)]
+ public string Content { get; set; }
+
+ [ProtoMember(3)]
+ public uint ComboCount { get; set; }
+ }
+
+ [ProtoContract]
+ public class KsWebLikeFeed
+ {
+ [ProtoMember(1)]
+ public string Id { get; set; }
+
+ [ProtoMember(2)]
+ public KsSimpleUserInfo User { get; set; }
+
+ [ProtoMember(3)]
+ public ulong SortRank { get; set; }
+
+ [ProtoMember(4)]
+ public string DeviceHash { get; set; }
+ }
+
+ [ProtoContract]
+ public class KsWebGiftFeed
+ {
+ [ProtoMember(1)]
+ public string Id { get; set; }
+
+ [ProtoMember(2)]
+ public KsSimpleUserInfo User { get; set; }
+
+ [ProtoMember(3)]
+ public ulong Time { get; set; }
+
+ [ProtoMember(4)]
+ public uint GiftId { get; set; }
+
+ [ProtoMember(5)]
+ public ulong SortRank { get; set; }
+
+ [ProtoMember(6)]
+ public string MergeKey { get; set; }
+
+ [ProtoMember(7)]
+ public uint BatchSize { get; set; }
+
+ [ProtoMember(8)]
+ public uint ComboCount { get; set; }
+
+ [ProtoMember(9)]
+ public uint Rank { get; set; }
+
+ [ProtoMember(10)]
+ public ulong ExpireDuration { get; set; }
+
+ [ProtoMember(11)]
+ public ulong ClientTimestamp { get; set; }
+
+ [ProtoMember(12)]
+ public ulong SlotDisplayDuration { get; set; }
+
+ [ProtoMember(13)]
+ public uint StarLevel { get; set; }
+
+ [ProtoMember(14)]
+ public KsGiftStyleType StyleType { get; set; }
+
+ [ProtoMember(15)]
+ public KsWebLiveAssistantType LiveAssistantType { get; set; }
+
+ [ProtoMember(16)]
+ public string DeviceHash { get; set; }
+
+ [ProtoMember(17)]
+ public bool DanmakuDisplay { get; set; }
+ }
+
+ public enum KsGiftStyleType
+ {
+ UNKNOWN_STYLE = 0,
+ BATCH_STAR_0 = 1,
+ BATCH_STAR_1 = 2,
+ BATCH_STAR_2 = 3,
+ BATCH_STAR_3 = 4,
+ BATCH_STAR_4 = 5,
+ BATCH_STAR_5 = 6,
+ BATCH_STAR_6 = 7
+ }
+
+ public enum KsWebLiveAssistantType
+ {
+ UNKNOWN_ASSISTANT_TYPE = 0,
+ SUPER = 1,
+ JUNIOR = 2
+ }
+
+ [ProtoContract]
+ public class KsWebSystemNoticeFeed
+ {
+ [ProtoMember(1)]
+ public string Id { get; set; }
+
+ [ProtoMember(2)]
+ public KsSimpleUserInfo User { get; set; }
+
+ [ProtoMember(3)]
+ public ulong Time { get; set; }
+
+ [ProtoMember(4)]
+ public string Content { get; set; }
+
+ [ProtoMember(5)]
+ public ulong DisplayDuration { get; set; }
+
+ [ProtoMember(6)]
+ public ulong SortRank { get; set; }
+
+ [ProtoMember(7)]
+ public KsDisplayType DisplayType { get; set; }
+ }
+
+ public enum KsDisplayType
+ {
+ UNKNOWN_DISPLAY_TYPE = 0,
+ COMMENT = 1,
+ ALERT = 2,
+ TOAST = 3
+ }
+
+ [ProtoContract]
+ public class KsWebShareFeed
+ {
+ [ProtoMember(1)]
+ public string Id { get; set; }
+
+ [ProtoMember(2)]
+ public KsSimpleUserInfo User { get; set; }
+
+ [ProtoMember(3)]
+ public ulong Time { get; set; }
+
+ [ProtoMember(4)]
+ public uint ThirdPartyPlatform { get; set; }
+
+ [ProtoMember(5)]
+ public ulong SortRank { get; set; }
+
+ [ProtoMember(6)]
+ public KsWebLiveAssistantType LiveAssistantType { get; set; }
+
+ [ProtoMember(7)]
+ public string DeviceHash { get; set; }
+ }
+
+ [ProtoContract]
+ public class KsSimpleUserInfo
+ {
+ [ProtoMember(1)]
+ public string PrincipalId { get; set; }
+
+ [ProtoMember(2)]
+ public string UserName { get; set; }
+
+ [ProtoMember(3)]
+ public string HeadUrl { get; set; }
+ }
+
+ [ProtoContract]
+ public class KsCSWebEnterRoom
+ {
+ [ProtoMember(1)]
+ public string Token { get; set; }
+
+ [ProtoMember(2)]
+ public string LiveStreamId { get; set; }
+
+ [ProtoMember(3)]
+ public uint ReconnectCount { get; set; }
+
+ [ProtoMember(4)]
+ public uint LastErrorCode { get; set; }
+
+ [ProtoMember(5)]
+ public string ExpTag { get; set; }
+
+ [ProtoMember(6)]
+ public string Attach { get; set; }
+
+ [ProtoMember(7)]
+ public string PageId { get; set; }
+ }
+}
diff --git a/BarrageGrab/Program.cs b/BarrageGrab/Program.cs
index a9196cc..7f85130 100644
--- a/BarrageGrab/Program.cs
+++ b/BarrageGrab/Program.cs
@@ -1,14 +1,12 @@
-using System;
-using System.Collections.Generic;
+using System;
using System.IO;
using System.Linq;
-using System.Net;
-using System.Net.Sockets;
using System.Diagnostics;
-using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Windows.Forms;
+using BarrageGrab.Modles.JsonEntity;
+using Newtonsoft.Json.Linq;
using Jint.Runtime;
namespace BarrageGrab
@@ -21,16 +19,225 @@ namespace BarrageGrab
static WinApi.ControlCtrlDelegate controlCtr = ControlCtrlHandle;
static Mutex mutex = new Mutex(false, "DyBarrageGrab");
+ // 插件所在目录(用于解析相对路径)
+ public static string BaseDir => AppDomain.CurrentDomain.BaseDirectory;
+
+ // === 启动项 (支持相对路径,相对于插件所在目录) ===
+ public static bool AutoLaunchGame = true;
+ public static string GameExePath = @"..\Fantasy.Map.Simulator\FantasyMapSimulator.exe";
+ public static string GameWorkDir = @"..\Fantasy.Map.Simulator";
+
+ // === 配置表路径 (支持相对路径) ===
+ public static string ConfigDir = @"..\Fantasy.Map.Simulator\BepInEx\config";
+ public static string EffectFile = "effect_config.txt";
+ public static string MacroFile = "macro_config.txt";
+ public static string GiftFile = "gift_config.txt";
+ public static string CommandFile = "command_config.txt";
+ public static string FkeyFile = "fkey_config.txt";
+
+ /// 将相对路径解析为绝对路径(基于插件所在目录)。已是绝对路径则原样返回。
+ public static string ResolveFullPath(string path)
+ {
+ if (string.IsNullOrWhiteSpace(path)) return path;
+ return Path.IsPathRooted(path) ? path : Path.GetFullPath(Path.Combine(BaseDir, path));
+ }
+
+ // 解析后的绝对路径(只读)
+ public static string GameExeFullPath => ResolveFullPath(GameExePath);
+ public static string GameWorkFullPath => ResolveFullPath(GameWorkDir);
+ public static string ConfigDirFullPath => ResolveFullPath(ConfigDir);
+
+ // 配置表完整路径(只读,基于 ConfigDirFullPath)
+ public static string EffectPath => Path.Combine(ConfigDirFullPath, EffectFile);
+ public static string MacroPath => Path.Combine(ConfigDirFullPath, MacroFile);
+ public static string GiftPath => Path.Combine(ConfigDirFullPath, GiftFile);
+ public static string CommandPath => Path.Combine(ConfigDirFullPath, CommandFile);
+ public static string FkeyPath => Path.Combine(ConfigDirFullPath, FkeyFile);
+
+ public static void SetGamePaths(string exePath, string workDir)
+ {
+ if (!string.IsNullOrWhiteSpace(exePath)) GameExePath = exePath;
+ if (!string.IsNullOrWhiteSpace(workDir)) GameWorkDir = workDir;
+ }
+
+ public static void SetConfigPaths(string configDir,
+ string effectFile, string macroFile, string giftFile, string cmdFile, string fkeyFile)
+ {
+ if (!string.IsNullOrWhiteSpace(configDir)) ConfigDir = configDir;
+ if (!string.IsNullOrWhiteSpace(effectFile)) EffectFile = effectFile;
+ if (!string.IsNullOrWhiteSpace(macroFile)) MacroFile = macroFile;
+ if (!string.IsNullOrWhiteSpace(giftFile)) GiftFile = giftFile;
+ if (!string.IsNullOrWhiteSpace(cmdFile)) CommandFile = cmdFile;
+ if (!string.IsNullOrWhiteSpace(fkeyFile)) FkeyFile = fkeyFile;
+ }
+
+ /// 从 GameExeFullPath 提取进程名(不带扩展名)
+ static string GameProcessName
+ {
+ get
+ {
+ try { return Path.GetFileNameWithoutExtension(GameExeFullPath); }
+ catch { return "FantasyMapSimulator"; }
+ }
+ }
+
+ /// 关闭所有同名游戏进程,返回是否已无残留
+ static bool CloseGameProcesses()
+ {
+ var procName = GameProcessName;
+ var procs = Process.GetProcessesByName(procName);
+ if (procs.Length == 0)
+ {
+ Logger.PrintColor("未发现运行中的游戏进程 (" + procName + ")", ConsoleColor.DarkGray);
+ return true;
+ }
+
+ Logger.PrintColor("发现 " + procs.Length + " 个游戏进程 (" + procName + "),正在关闭...", ConsoleColor.Yellow);
+ int killed = 0, failed = 0;
+ foreach (var p in procs)
+ {
+ try
+ {
+ if (!p.HasExited)
+ {
+ try { p.CloseMainWindow(); } catch { }
+ if (!p.WaitForExit(2000)) { p.Kill(); p.WaitForExit(3000); }
+ }
+ killed++;
+ }
+ catch { failed++; }
+ finally { try { p.Close(); } catch { } }
+ }
+
+ Thread.Sleep(500);
+ var remain = Process.GetProcessesByName(procName);
+ if (remain.Length > 0)
+ {
+ Logger.PrintColor("仍有 " + remain.Length + " 个进程未退出,强制结束...", ConsoleColor.Yellow);
+ foreach (var r in remain)
+ {
+ try { if (!r.HasExited) r.Kill(); } catch { }
+ finally { try { r.Close(); } catch { } }
+ }
+ Thread.Sleep(1000);
+ }
+
+ bool allClosed = Process.GetProcessesByName(procName).Length == 0;
+ Logger.PrintColor("关闭完成 (成功" + killed + ", 失败" + failed + ")", allClosed ? ConsoleColor.Green : ConsoleColor.Yellow);
+ return allClosed;
+ }
+
+ public static void LaunchGame()
+ {
+ try
+ {
+ if (!File.Exists(GameExeFullPath))
+ {
+ MessageBox.Show("找不到游戏文件:\n" + GameExeFullPath + "\n(配置路径: " + GameExePath + ")", "启动失败", MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ return;
+ }
+ var existing = Process.GetProcessesByName(GameProcessName);
+ if (existing.Length > 0)
+ {
+ Logger.PrintColor("游戏已在运行中,跳过启动", ConsoleColor.Yellow);
+ foreach (var p in existing) try { p.Close(); } catch { }
+ return;
+ }
+ StartGameProcess();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "启动游戏失败: " + ex.Message);
+ }
+ }
+
+ public static void RestartGame()
+ {
+ try
+ {
+ if (!File.Exists(GameExeFullPath))
+ {
+ MessageBox.Show("找不到游戏文件:\n" + GameExeFullPath + "\n(配置路径: " + GameExePath + ")", "启动失败", MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ return;
+ }
+
+ Logger.PrintColor("── 重启游戏:关闭进程 ──", ConsoleColor.Cyan);
+ bool closed = CloseGameProcesses();
+ if (!closed)
+ {
+ Logger.PrintColor("警告: 部分游戏进程未能完全关闭,仍尝试重新启动", ConsoleColor.Yellow);
+ }
+
+ Thread.Sleep(1500);
+
+ Logger.PrintColor("── 重启游戏:重新启动 ──", ConsoleColor.Cyan);
+ StartGameProcess();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "重启游戏失败: " + ex.Message);
+ }
+ }
+ private static void StartGameProcess()
+ {
+ var psi = new ProcessStartInfo(GameExeFullPath)
+ {
+ WorkingDirectory = GameWorkFullPath,
+ UseShellExecute = true
+ };
+ Process.Start(psi);
+ Logger.PrintColor("游戏已启动", ConsoleColor.Green);
+ }
+
+ /// 加载 game_settings.json (启动项 + 配置表路径)
+ private static void LoadGameSettings()
+ {
+ try
+ {
+ string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "game_settings.json");
+ if (!File.Exists(path)) return;
+ var json = JObject.Parse(File.ReadAllText(path));
+
+ // 启动项
+ string exe = json["gameExePath"]?.Value();
+ string dir = json["gameWorkDir"]?.Value();
+ bool auto = json["autoLaunchGame"]?.Value() ?? true;
+ if (!string.IsNullOrWhiteSpace(exe)) GameExePath = exe;
+ if (!string.IsNullOrWhiteSpace(dir)) GameWorkDir = dir;
+ AutoLaunchGame = auto;
+
+ // 配置表路径
+ string configDir = json["configDir"]?.Value();
+ if (!string.IsNullOrWhiteSpace(configDir)) ConfigDir = configDir;
+ var cfg = json["configFiles"];
+ if (cfg != null)
+ {
+ string eff = cfg["effect"]?.Value();
+ string mac = cfg["macro"]?.Value();
+ string gift = cfg["gift"]?.Value();
+ string cmd = cfg["command"]?.Value();
+ string fkey = cfg["fkey"]?.Value();
+ if (!string.IsNullOrWhiteSpace(eff)) EffectFile = eff;
+ if (!string.IsNullOrWhiteSpace(mac)) MacroFile = mac;
+ if (!string.IsNullOrWhiteSpace(gift)) GiftFile = gift;
+ if (!string.IsNullOrWhiteSpace(cmd)) CommandFile = cmd;
+ if (!string.IsNullOrWhiteSpace(fkey)) FkeyFile = fkey;
+ }
+
+ Logger.PrintColor($"已加载游戏设置: 自动启动={AutoLaunchGame}, 配置目录={ConfigDir}", ConsoleColor.DarkGray);
+ }
+ catch (Exception ex)
+ {
+ Logger.PrintColor($"加载游戏设置失败: {ex.Message}", ConsoleColor.Yellow);
+ }
+ }
+
private const string WatchdogArg = "--watchdog";
private const string WatchdogPidArg = "--pid";
static void Main(string[] args)
{
- if (IsWatchdogMode(args))
- {
- RunWatchdog(args);
- return;
- }
+ if (IsWatchdogMode(args)) { RunWatchdog(args); return; }
if (!mutex.WaitOne(TimeSpan.Zero, true))
{
@@ -39,14 +246,11 @@ namespace BarrageGrab
return;
}
- SetTitle("抖音弹幕监听推送");
-
+ SetTitle("弹幕监听推送");
StartWatchdog();
+ LoadGameSettings();
- try
- {
- Init();
- }
+ try { Init(); }
catch (Exception ex)
{
Logger.LogError(ex, $"程序初始化错误,{ex.Message}");
@@ -54,18 +258,11 @@ namespace BarrageGrab
exited = true;
}
- while (!exited)
- {
- Thread.Sleep(500);
- }
-
- if (!AppRuntime.WsServer.IsDisposed)
- {
- AppRuntime.WsServer.Dispose();
- }
+ while (!exited) Thread.Sleep(500);
+ if (!AppRuntime.WsServer.IsDisposed) AppRuntime.WsServer.Dispose();
Logger.PrintColor("服务器已关闭...");
- WinApi.SetConsoleCtrlHandler(controlCtr, false);//反注册捕获控制台关闭
+ WinApi.SetConsoleCtrlHandler(controlCtr, false);
}
private static void StartWatchdog()
@@ -74,21 +271,12 @@ namespace BarrageGrab
{
var exePath = Application.ExecutablePath;
if (string.IsNullOrWhiteSpace(exePath) || !File.Exists(exePath)) return;
-
var pid = Process.GetCurrentProcess().Id;
var args = string.Format("{0} {1} {2}", WatchdogArg, WatchdogPidArg, pid);
- var psi = new ProcessStartInfo(exePath, args)
- {
- UseShellExecute = false,
- CreateNoWindow = true,
- WindowStyle = ProcessWindowStyle.Hidden
- };
+ var psi = new ProcessStartInfo(exePath, args) { UseShellExecute = false, CreateNoWindow = true, WindowStyle = ProcessWindowStyle.Hidden };
Process.Start(psi);
}
- catch
- {
- // best-effort only
- }
+ catch { }
}
private static bool IsWatchdogMode(string[] args)
@@ -116,106 +304,69 @@ namespace BarrageGrab
{
var pid = TryGetWatchdogPid(args);
if (pid == null || pid <= 0) return;
-
Process target;
- try
- {
- target = Process.GetProcessById(pid.Value);
- }
- catch
- {
- SafeCloseSystemProxy();
- return;
- }
-
- for (; ; )
- {
- if (target.HasExited) break;
- Thread.Sleep(1000);
- }
-
+ try { target = Process.GetProcessById(pid.Value); }
+ catch { SafeCloseSystemProxy(); return; }
+ for (; ; ) { if (target.HasExited) break; Thread.Sleep(1000); }
SafeCloseSystemProxy();
}
- catch
- {
- try { SafeCloseSystemProxy(); } catch { }
- }
+ catch { try { SafeCloseSystemProxy(); } catch { } }
}
private static void SafeCloseSystemProxy()
{
- try
- {
- if (!AppSetting.Current.UsedProxy) return;
- WinApi.CloseSystemProxy();
- }
- catch
- {
- }
+ try { if (!AppSetting.Current.UsedProxy) return; WinApi.CloseSystemProxy(); }
+ catch { }
}
private static void Init()
{
AppRuntime.Init();
LiveCompanHelper.SwitchSetup();
- WinApi.SetConsoleCtrlHandler(controlCtr, true);//捕获控制台关闭
- WinApi.DisableQuickEditMode();//禁用控制台快速编辑模式
- AppRuntime.DisplayConsole(!AppSetting.Current.HideConsole);//控制控制台可见
- AppRuntime.WsServer.Grab.Proxy.SetUpstreamProxy(AppSetting.Current.UpstreamProxy);//设置上游代理
- AppRuntime.WsServer.OnClose += (s, e) =>
- {
- exited = true;
- };
+ WinApi.SetConsoleCtrlHandler(controlCtr, true);
+ WinApi.DisableQuickEditMode();
+ AppRuntime.DisplayConsole(!AppSetting.Current.HideConsole);
+ AppRuntime.WsServer.Grab.Proxy.SetUpstreamProxy(AppSetting.Current.UpstreamProxy);
+ AppRuntime.WsServer.OnClose += (s, e) => { exited = true; };
- //串口写入服务
if (!AppSetting.Current.ComPort.IsNullOrWhiteSpace())
- {
AppRuntime.ComPortServer.OpenStart();
- }
- //显示窗体
if (AppSetting.Current.ShowWindow)
{
var uiThread = new Thread(new ThreadStart(() =>
{
mainForm = new FormView();
- //开启消息循环
System.Windows.Forms.Application.Run(mainForm);
formExited = true;
- AppRuntime.WsServer.Dispose();//Close后自动释放资源
+ AppRuntime.WsServer.Dispose();
}));
uiThread.SetApartmentState(ApartmentState.STA);
uiThread.IsBackground = true;
uiThread.Start();
}
- AppRuntime.WsServer.StartListen();//启动WS以及代理服务
- Logger.PrintColor($"{AppRuntime.WsServer.ServerLocation} 弹幕服务已启动,其他端可通过此地址获取到弹幕流信息", ConsoleColor.Green);
+ AppRuntime.WsServer.StartListen();
+ Logger.PrintColor($"{AppRuntime.WsServer.ServerLocation} 弹幕服务已启动", ConsoleColor.Green);
+
+ if (AutoLaunchGame) LaunchGame();
Version version = System.Reflection.Assembly.GetAssembly(typeof(Program)).GetName().Version;
- SetTitle($"抖音弹幕监听推送 v{version} [{AppRuntime.WsServer.ServerLocation}]");
+ SetTitle($"弹幕监听推送 v{version} [{AppRuntime.WsServer.ServerLocation}]");
}
- //检测设置控制台标题
private static void SetTitle(string title)
{
- if (WinApi.GetConsoleWindow() != IntPtr.Zero)
- {
- Console.Title = title;
- }
+ if (WinApi.GetConsoleWindow() != IntPtr.Zero) Console.Title = title;
}
- //监听控制台消息事件
private static bool ControlCtrlHandle(int CtrlType)
{
switch (CtrlType)
{
- case 0:
- //Logger.PrintColor("0工具被强制关闭"); //Ctrl+C关闭
- //server.Close();
- break;
+ case 0: break;
case 2:
- Logger.PrintColor("2工具被强制关闭");//按控制台关闭按钮关闭
+ Logger.PrintColor("2工具被强制关闭");
AppRuntime.WsServer.Dispose();
break;
}
diff --git a/BarrageGrab/Properties/app.manifest b/BarrageGrab/Properties/app.manifest
index 5a95963..2ae9932 100644
--- a/BarrageGrab/Properties/app.manifest
+++ b/BarrageGrab/Properties/app.manifest
@@ -1,73 +1,11 @@
-
+
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/BarrageGrab/Proxy/ISystemProxy.cs b/BarrageGrab/Proxy/ISystemProxy.cs
index aa8ab64..56192e9 100644
--- a/BarrageGrab/Proxy/ISystemProxy.cs
+++ b/BarrageGrab/Proxy/ISystemProxy.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Reflection;
using BarrageGrab.Proxy.ProxyEventArgs;
@@ -9,6 +9,11 @@ namespace BarrageGrab.Proxy
{
event EventHandler OnWebSocketData;
+ ///
+ /// 客户端发送的WebSocket数据事件 (用于解析认证包等)
+ ///
+ event EventHandler OnWebSocketSentData;
+
event EventHandler OnFetchResponse;
event EventHandler OnProxyStatus;
diff --git a/BarrageGrab/Proxy/ProxyEventArgs/WsMessageEventArgs.cs b/BarrageGrab/Proxy/ProxyEventArgs/WsMessageEventArgs.cs
index a41f195..ef47f9c 100644
--- a/BarrageGrab/Proxy/ProxyEventArgs/WsMessageEventArgs.cs
+++ b/BarrageGrab/Proxy/ProxyEventArgs/WsMessageEventArgs.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
+using BarrageGrab.Modles.JsonEntity;
namespace BarrageGrab.Proxy.ProxyEventArgs
{
@@ -32,5 +33,10 @@ namespace BarrageGrab.Proxy.ProxyEventArgs
/// 是否需要解压缩
///
public bool NeedDecompress { get; set; } = true;
+
+ ///
+ /// 直播平台
+ ///
+ public LivePlatform Platform { get; set; } = LivePlatform.Douyin;
}
}
diff --git a/BarrageGrab/Proxy/SystemProxy.cs b/BarrageGrab/Proxy/SystemProxy.cs
index ce3bd77..9e9f09a 100644
--- a/BarrageGrab/Proxy/SystemProxy.cs
+++ b/BarrageGrab/Proxy/SystemProxy.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@@ -19,6 +19,11 @@ namespace BarrageGrab.Proxy
///
public event EventHandler OnWebSocketData;
+ ///
+ /// 客户端发送的WebSocket数据事件 (用于解析认证包等)
+ ///
+ public event EventHandler OnWebSocketSentData;
+
///
/// 接收到http响应事件
///
@@ -78,6 +83,15 @@ namespace BarrageGrab.Proxy
OnWebSocketData?.Invoke(this, args);
}
+ ///
+ /// 触发客户端发送的websocket数据事件
+ ///
+ ///
+ protected void FireWsSentEvent(WsMessageEventArgs args)
+ {
+ OnWebSocketSentData?.Invoke(this, args);
+ }
+
///
/// 触发弹幕http弹幕事件
///
diff --git a/BarrageGrab/Proxy/TitaniumProxy.cs b/BarrageGrab/Proxy/TitaniumProxy.cs
index 5a7133d..ec61725 100644
--- a/BarrageGrab/Proxy/TitaniumProxy.cs
+++ b/BarrageGrab/Proxy/TitaniumProxy.cs
@@ -43,7 +43,13 @@ namespace BarrageGrab.Proxy
const string BARRAGE_POOL_PATH = "/webcast/im/fetch";
const string LIVE_SCRIPT_PATH = "/obj/static/webcast/douyin_live";
const string WEBCAST_AMEMV_HOST = "webcast.amemv.com";
+ // B站直播页面域名
+ const string BILI_LIVE_HOST = "live.bilibili.com";
private readonly Regex webcastBarrageReg = new Regex(@"webcast\d+-ws-web-\w+\.(douyin|amemv)\.com");
+ // 2026-05 快手WS域名变更为 livejs-ws.kuaishou.cn
+ private readonly Regex kuaishouWsReg = new Regex(@"livejs-ws\.kuaishou\.cn|(?:livemate|pclivemate)\.pull\.yximgs\.com", RegexOptions.IgnoreCase);
+ // B站直播弹幕WS域名 (broadcast-msg.chat.bilibili.com 主域名, *-live-comet.chat.bilibili.com 新版comet服务器, live-trace.com 备用, ws.live.bilibili.com 历史域名)
+ private readonly Regex biliWsReg = new Regex(@"chat\.bilibili\.com|live-trace\.com|ws\.live\.bilibili\.com", RegexOptions.IgnoreCase);
public override string HttpUpstreamProxy { get { return proxyServer?.UpStreamHttpProxy?.ToString() ?? ""; } }
@@ -196,7 +202,7 @@ namespace BarrageGrab.Proxy
private bool CheckBrowser(string processName)
{
- return AppSetting.Current.ProcessFilter.Contains(processName) && processName != "直播伴侣" && processName != "douyin";
+ return AppSetting.Current.ProcessFilter.Contains(processName) && processName != "直播伴侣" && processName != "douyin" && processName != "kwailive" && processName != "bilibili";
}
private async Task ProxyServer_BeforeResponse(object sender, SessionEventArgs e)
@@ -221,6 +227,37 @@ namespace BarrageGrab.Proxy
//处理脚本拦截修改
await HookScriptAsync(e);
+
+ //快手礼物映射嗅探
+ await HookKsGiftSniffer(e);
+ }
+
+ // 快手礼物映射嗅探 - 拦截包含礼物数据的JSON响应
+ private async Task HookKsGiftSniffer(SessionEventArgs e)
+ {
+ try
+ {
+ string hostname = e.HttpClient.Request.RequestUri.Host;
+ if (hostname.IndexOf("kuaishou", StringComparison.OrdinalIgnoreCase) < 0 &&
+ hostname.IndexOf("yximgs", StringComparison.OrdinalIgnoreCase) < 0 &&
+ hostname.IndexOf("kskwai", StringComparison.OrdinalIgnoreCase) < 0)
+ return;
+
+ var contentType = e.HttpClient.Response.ContentType ?? "";
+
+ // 仅检查 JSON 响应
+ if (!contentType.Contains("json") && !contentType.Contains("javascript"))
+ return;
+
+ var body = await e.GetResponseBodyAsString();
+ if (string.IsNullOrWhiteSpace(body) || body.Length < 20) return;
+
+ if (KsGiftHelper.TryParseGiftData(body))
+ {
+ KsGiftHelper.SaveMapping();
+ }
+ }
+ catch { /* 嗅探失败不影响主流程 */ }
}
// Hook 直播伴侣开播信息并更新
@@ -341,6 +378,28 @@ namespace BarrageGrab.Proxy
Logger.LogInfo($"订阅到新的弹幕流地址,roomid:{roomid}");
}
+ //快手 ws 方式
+ if (
+ e.HttpClient.ConnectRequest?.TunnelType == TunnelType.Websocket &&
+ kuaishouWsReg.IsMatch(uri)
+ )
+ {
+ e.DataReceived += WebSocket_DataReceived;
+ e.DataSent += WebSocket_DataSent;
+ Logger.LogInfo($"订阅到快手弹幕流地址,url:{uri}");
+ }
+
+ //B站 ws 方式
+ if (
+ e.HttpClient.ConnectRequest?.TunnelType == TunnelType.Websocket &&
+ biliWsReg.IsMatch(uri)
+ )
+ {
+ e.DataReceived += WebSocket_DataReceived;
+ e.DataSent += WebSocket_DataSent;
+ Logger.LogInfo($"订阅到B站弹幕流地址,url:{uri}");
+ }
+
//轮询方式(当抖音ws连接断开后,客户端也会降级使用轮询模式获取弹幕)
if (uri.Contains(BARRAGE_POOL_PATH) && contentType.Contains("application/protobuffer"))
{
@@ -400,6 +459,9 @@ namespace BarrageGrab.Proxy
var processName = base.GetProcessName(processid);
var liveRoomMactch = Regex.Match(uri.Trim(), @".*:\/\/live.douyin\.com\/([0-9a-zA-Z_]+)");
var liveHomeMatch = Regex.Match(uri.Trim(), @".*:\/\/live.douyin\.com\/?$");
+ var ksLiveRoomMatch = Regex.Match(uri.Trim(), @".*:\/\/live\.kuaishou\.com\/u\/([^/?]+)");
+ // B站直播间 URL: https://live.bilibili.com/ (roomId 纯数字)
+ var biliLiveRoomMatch = Regex.Match(uri.Trim(), @".*:\/\/live\.bilibili\.com\/(\d+)");
var contentType = e.HttpClient.Response.ContentType ?? "";
//检测是否为dom页,用于脚本注入
@@ -457,9 +519,123 @@ namespace BarrageGrab.Proxy
}
e.SetResponseBodyString(html);
- }
+ }
+ else if (ksLiveRoomMatch.Success)
+ {
+ string userId = ksLiveRoomMatch.Groups[1].Value;
+ var html = await e.GetResponseBodyAsString();
+
+ var ksInfo = KsApiHelper.ParseRoomInfoFromHtml(html, userId);
+ if (ksInfo != null)
+ {
+ ksInfo.LiveUrl = url;
+ var roominfo = new RoomInfo()
+ {
+ WebRoomId = userId,
+ RoomId = ksInfo.LiveStreamId ?? userId,
+ Title = ksInfo.Nickname + "\u7684\u76f4\u64ad\u95f4",
+ IsLive = ksInfo.IsLive,
+ UserCount = ksInfo.WatchingCount,
+ LikeCount = ksInfo.LikeCount,
+ TotalUserCount = ksInfo.WatchingCountStr ?? "",
+ Cover = ksInfo.CoverUrl ?? "",
+ LiveUrl = url,
+ Owner = new RoomInfo.RoomAnchor()
+ {
+ Nickname = ksInfo.Nickname,
+ UserId = userId,
+ HeadUrl = ksInfo.HeadUrl ?? ""
+ }
+ };
+ AppRuntime.RoomCaches.AddRoomInfoCache(roominfo);
+ Logger.LogInfo("\u5feb\u624b\u76f4\u64ad\u9875" + userId + " [" + ksInfo.Nickname + "]\u7684\u76f4\u64ad\u95f4\uff0c\u623f\u95f4\u4fe1\u606f\u5df2\u91c7\u96c6\u5230\u7f13\u5b58");
+ Logger.LogInfo($"KS房间 LiveStreamId={ksInfo.LiveStreamId} Watching={ksInfo.WatchingCountStr} Liked={ksInfo.LikeCountStr} Avatar={(!string.IsNullOrEmpty(ksInfo.HeadUrl))}");
+ }
+
+ e.SetResponseBodyString(html);
+
+ // 从HTML中嗅探礼物映射(登录后__INITIAL_STATE__含allGifts等)
+ if (html.Contains("allGifts") || html.Contains("giftList") || html.Contains("giftPanel"))
+ {
+ KsGiftHelper.TryParseGiftData(html);
+ }
+ }
+ else if (biliLiveRoomMatch.Success)
+ {
+ // B站直播间页面: B站改版后页面不再注入 __INITIAL_STATE__, 改用官方API获取房间信息
+ string webRoomId = biliLiveRoomMatch.Groups[1].Value;
+ var html = await e.GetResponseBodyAsString();
+
+ // 先尝试从HTML快速解析 (兼容旧版页面)
+ var biliInfo = BiliApiHelper.ParseRoomInfoFromHtml(html, webRoomId);
+
+ if (biliInfo != null && !string.IsNullOrWhiteSpace(biliInfo.RoomId) && !string.IsNullOrWhiteSpace(biliInfo.Uid))
+ {
+ CacheBiliRoomInfo(biliInfo, webRoomId, url);
+ }
+ else if (!rinfoRequestings.Contains(webRoomId))
+ {
+ // HTML解析失败, 异步调用B站官方API获取房间信息
+ lock (rinfoRequestings) rinfoRequestings.Add(webRoomId);
+
+ var cookie = e.HttpClient.Request.Headers.GetFirstHeader("Cookie")?.Value;
+ BiliApiHelper.GetRoomInfoByApi(webRoomId, cookie).ContinueWith(t =>
+ {
+ try
+ {
+ var info = t.Result;
+ if (info != null && !string.IsNullOrWhiteSpace(info.RoomId))
+ {
+ CacheBiliRoomInfo(info, webRoomId, url);
+ }
+ else
+ {
+ Logger.LogWarn($"B站API获取房间 {webRoomId} 信息失败");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, $"B站API获取房间 {webRoomId} 信息异常: {ex.Message}");
+ }
+ finally
+ {
+ lock (rinfoRequestings) rinfoRequestings.Remove(webRoomId);
+ }
+ });
+ }
+
+ e.SetResponseBodyString(html);
+ }
}
-
+
+ /// 缓存B站房间信息
+ private void CacheBiliRoomInfo(BiliRoomInfo biliInfo, string webRoomId, string url)
+ {
+ biliInfo.LiveUrl = url;
+ var roominfo = new RoomInfo()
+ {
+ WebRoomId = webRoomId,
+ RoomId = biliInfo.RoomId,
+ Title = string.IsNullOrWhiteSpace(biliInfo.Title) ? $"{biliInfo.Nickname}的直播间" : biliInfo.Title,
+ IsLive = biliInfo.IsLive,
+ UserCount = biliInfo.Online,
+ TotalUserCount = biliInfo.Online.ToString(),
+ Cover = biliInfo.CoverUrl ?? "",
+ LiveUrl = url,
+ Owner = new RoomInfo.RoomAnchor()
+ {
+ UserId = biliInfo.Uid ?? "",
+ Nickname = biliInfo.Nickname ?? "",
+ HeadUrl = biliInfo.HeadUrl ?? ""
+ }
+ };
+ AppRuntime.RoomCaches.AddRoomInfoCache(roominfo);
+ Logger.LogInfo($"B站直播页 {webRoomId} [{biliInfo.Nickname}]的直播间,房间信息已采集到缓存 (RoomId={biliInfo.RoomId})");
+
+ // 同时把 WebRoomId 关联到 RoomId
+ AppRuntime.RoomCaches.SetRoomCache(biliInfo.RoomId, webRoomId);
+ }
+
//生成注入上下文
private string BuildContext(IDictionary constVals)
{
@@ -489,7 +665,7 @@ namespace BarrageGrab.Proxy
//https://lf-webcast-platform.bytetos.com/obj/webcast-platform-cdn/webcast/douyin_live/chunks/island_a74ce.b55095a0.js
//https://lf-webcast-platform.bytetos.com/obj/webcast-platform-cdn/webcast/douyin_live/chunks/PausePop.ec38fcf8.js 2026/04/09
//判断响应内容是否为js application/javascript
- if (processName != "直播伴侣" && processName != "douyin"
+ if (processName != "直播伴侣" && processName != "douyin" && processName != "kwailive"
&& fileName.ToLower().StartsWith("pausepop")
)
{
@@ -543,7 +719,8 @@ namespace BarrageGrab.Proxy
string url = e.HttpClient.Request.RequestUri.ToString();
string hostname = e.HttpClient.Request.RequestUri.Host;
- e.DecryptSsl = CheckHost(hostname);
+ bool decrypt = CheckHost(hostname);
+ e.DecryptSsl = decrypt;
//ws 方式
@@ -566,7 +743,12 @@ namespace BarrageGrab.Proxy
LIVE_HOST,
WEBCAST_AMEMV_HOST , //直播伴侣开播请求地址
"*-webcast-platform.bytetos.com", //新的脚本地址
- "*webcast*" //所有带webcast的域名
+ "*webcast*", //所有带webcast的域名
+ "*kuaishou.com", //快手直播页面
+ "*kuaishou.cn", //快手WS弹幕流地址 (livejs-ws.kuaishou.cn)
+ "*.yximgs.com", //快手CDN (视频流/封面等)
+ "*bilibili.com", //B站直播页面及WS弹幕流
+ "live-trace.com" //B站WS备用域名
};
if (decryptSsls.Contains(hostname))
@@ -617,6 +799,19 @@ namespace BarrageGrab.Proxy
var str = Encoding.UTF8.GetString(payload);
+ // B站: 客户端发送的数据(认证包/心跳),触发事件供解析器提取roomid
+ if (biliWsReg.IsMatch(hostname))
+ {
+ base.FireWsSentEvent(new WsMessageEventArgs()
+ {
+ ProcessID = processid,
+ HostName = hostname,
+ Payload = payload,
+ Platform = LivePlatform.Bilibili,
+ ProcessName = base.GetProcessName(processid)
+ });
+ }
+
//base.FireWsEvent(new WsMessageEventArgs()
//{
// ProcessID = processid,
@@ -629,6 +824,7 @@ namespace BarrageGrab.Proxy
}
//WebSocket 流读取
+ private static int wsDecodeErrCount = 0;
private async void WebSocket_DataReceived(object sender, DataEventArgs e)
{
var args = (SessionEventArgs)sender;
@@ -663,11 +859,15 @@ namespace BarrageGrab.Proxy
payload = frame.Data.ToArray();
}
+ var platform = biliWsReg.IsMatch(hostname) ? LivePlatform.Bilibili
+ : kuaishouWsReg.IsMatch(hostname) ? LivePlatform.Kuaishou
+ : LivePlatform.Douyin;
base.FireWsEvent(new WsMessageEventArgs()
{
ProcessID = processid,
HostName = hostname,
Payload = payload,
+ Platform = platform,
ProcessName = base.GetProcessName(processid)
});
}
@@ -675,7 +875,11 @@ namespace BarrageGrab.Proxy
}
catch (Exception ex)
{
- Logger.PrintColor("解析某个WebSocket包出错:" + ex.Message);
+ if (wsDecodeErrCount < 3)
+ {
+ Logger.PrintColor("解析某个WebSocket包出错:" + ex.Message);
+ wsDecodeErrCount++;
+ }
}
if (messageData.Count > 0)
diff --git a/BarrageGrab/Server/BiliBarrageGrab.cs b/BarrageGrab/Server/BiliBarrageGrab.cs
new file mode 100644
index 0000000..e3384e4
--- /dev/null
+++ b/BarrageGrab/Server/BiliBarrageGrab.cs
@@ -0,0 +1,405 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Text.RegularExpressions;
+using BarrageGrab.Modles.BiliProtoEntity;
+using BarrageGrab.Proxy.ProxyEventArgs;
+using BrotliSharpLib;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace BarrageGrab
+{
+ ///
+ /// 哔哩哔哩弹幕抓取器
+ /// 协议格式: 自定义16字节Big-Endian帧头 + JSON body (body可能为zlib/brotli压缩)
+ ///
+ public class BiliBarrageGrab
+ {
+ // B站直播弹幕WS域名
+ // broadcast-msg.chat.bilibili.com (主域名)
+ // *-live-comet.chat.bilibili.com (新版comet服务器, 如 zj-cn-live-comet.chat.bilibili.com)
+ // live-trace.com (备用)
+ // ws.live.bilibili.com (历史域名)
+ private readonly Regex biliWsReg = new Regex(@"chat\.bilibili\.com|live-trace\.com|ws\.live\.bilibili\.com", RegexOptions.IgnoreCase);
+ private const string BILI_LIVE_HOST = "live.bilibili.com";
+
+ /// 弹幕消息
+ public event EventHandler> OnChatMessage;
+ /// 点赞消息
+ public event EventHandler> OnLikeMessage;
+ /// 礼物消息
+ public event EventHandler> OnGiftMessage;
+ /// 进入直播间/关注消息
+ public event EventHandler> OnInteractMessage;
+ /// 房间统计消息
+ public event EventHandler> OnRoomStatsMessage;
+ /// 分享消息 (INTERACT_WORD中msg_type=3)
+ public event EventHandler> OnShareMessage;
+
+ public BiliBarrageGrab()
+ {
+ }
+
+ ///
+ /// 当前认证的房间ID (从AUTH包解析,供消息处理器读取)
+ ///
+ public string CurrentRoomId { get; private set; } = "";
+
+ public bool IsBilibiliHost(string hostname)
+ {
+ if (string.IsNullOrWhiteSpace(hostname)) return false;
+ hostname = hostname.Trim().ToLower();
+ return biliWsReg.IsMatch(hostname) || hostname == BILI_LIVE_HOST;
+ }
+
+ public bool IsBilibiliWebSocket(string uri)
+ {
+ if (string.IsNullOrWhiteSpace(uri)) return false;
+ return biliWsReg.IsMatch(uri);
+ }
+
+ ///
+ /// 处理代理收到的B站WS数据
+ ///
+ public void HandleWebSocketData(WsMessageEventArgs e)
+ {
+ var buff = e.Payload;
+ if (buff == null || buff.Length == 0) return;
+
+ try
+ {
+ // 一个WS帧可能包含多个B站数据包,需要循环解析
+ ProcessBuffer(buff, 0, buff.Length, e.ProcessName);
+ }
+ catch (Exception ex)
+ {
+ if (biliErrCount < 3)
+ {
+ Logger.LogError(ex, $"处理B站弹幕数据包时出错:{ex.Message}");
+ biliErrCount++;
+ }
+ if (biliErrCount == 3)
+ {
+ Logger.LogWarn("B站弹幕解析持续失败,已抑制后续错误日志");
+ biliErrCount++;
+ }
+ }
+ }
+
+ ///
+ /// 处理客户端发送的B站WS数据 (解析AUTH认证包提取roomid)
+ /// B站客户端连接WS后会发送AUTH包: {uid,roomid,protover,platform,type,key}
+ ///
+ public void HandleSentData(WsMessageEventArgs e)
+ {
+ var buff = e.Payload;
+ if (buff == null || buff.Length < BiliSocketFrame.HEADER_SIZE) return;
+
+ try
+ {
+ BiliSocketFrame frame;
+ if (!BiliSocketFrame.TryParse(buff, 0, buff.Length, out frame)) return;
+
+ // 只关注认证请求包 (Operation=7)
+ if (frame.Operation != BiliOperation.AUTH) return;
+
+ var payload = BiliSocketFrame.GetPayload(buff, 0, frame);
+ if (payload == null || payload.Length == 0) return;
+
+ var json = System.Text.Encoding.UTF8.GetString(payload);
+ var jobj = JObject.Parse(json);
+ var roomIdToken = jobj["roomid"];
+ if (roomIdToken == null) return;
+
+ // roomid可能是整数或字符串
+ string roomId = roomIdToken.Type == JTokenType.Integer
+ ? roomIdToken.Value().ToString()
+ : roomIdToken.Value() ?? "";
+
+ if (!string.IsNullOrWhiteSpace(roomId))
+ {
+ CurrentRoomId = roomId;
+ Logger.LogInfo($"B站认证包解析成功,当前房间ID: {roomId}");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarn($"解析B站AUTH认证包失败: {ex.Message}");
+ }
+ }
+
+ private static int biliErrCount = 0;
+
+ ///
+ /// 递归解析缓冲区中可能包含的多个B站数据包
+ ///
+ private void ProcessBuffer(byte[] buffer, int offset, int count, string processName)
+ {
+ int processed = 0;
+ while (processed + BiliSocketFrame.HEADER_SIZE <= count)
+ {
+ BiliSocketFrame frame;
+ if (!BiliSocketFrame.TryParse(buffer, offset + processed, count - processed, out frame))
+ {
+ break;
+ }
+
+ // 包长超过剩余缓冲区,说明数据不完整,等下次再处理
+ if (processed + frame.PacketLength > count)
+ {
+ break;
+ }
+
+ var payload = BiliSocketFrame.GetPayload(buffer, offset + processed, frame);
+ processed += (int)frame.PacketLength;
+
+ // 只处理服务器推送的消息和心跳回复
+ if (frame.Operation == BiliOperation.HEARTBEAT_REPLY)
+ {
+ // 心跳回复: body是4字节big-endian人气值
+ if (payload != null && payload.Length >= 4)
+ {
+ var online = (long)BiliSocketFrame.ReadUInt32BE(payload, 0);
+ var stats = new BiliRoomStats { Online = online, Watched = 0 };
+ OnRoomStatsMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, stats));
+ }
+ continue;
+ }
+
+ if (frame.Operation != BiliOperation.MESSAGE) continue;
+
+ // 根据协议版本解压
+ byte[] jsonBytes;
+ switch (frame.ProtocolVersion)
+ {
+ case BiliProtocolVersion.PLAIN:
+ case BiliProtocolVersion.PLAIN_AUTH:
+ jsonBytes = payload;
+ break;
+ case BiliProtocolVersion.ZLIB:
+ jsonBytes = ZlibDecompress(payload);
+ break;
+ case BiliProtocolVersion.BROTLI:
+ jsonBytes = BrotliDecompress(payload);
+ break;
+ default:
+ jsonBytes = payload;
+ break;
+ }
+
+ if (jsonBytes == null || jsonBytes.Length == 0) continue;
+
+ // 解压后可能仍然是多个数据包 (Zlib/Brotli压缩的通常是合并包)
+ if (frame.ProtocolVersion == BiliProtocolVersion.ZLIB ||
+ frame.ProtocolVersion == BiliProtocolVersion.BROTLI)
+ {
+ ProcessBuffer(jsonBytes, 0, jsonBytes.Length, processName);
+ continue;
+ }
+
+ // 明文JSON: 解析为消息
+ DispatchJsonMessage(jsonBytes, processName);
+ }
+ }
+
+ ///
+ /// 将JSON字节解析为B站消息并触发对应事件
+ ///
+ private void DispatchJsonMessage(byte[] jsonBytes, string processName)
+ {
+ string json;
+ try
+ {
+ json = System.Text.Encoding.UTF8.GetString(jsonBytes);
+ }
+ catch { return; }
+
+ if (string.IsNullOrWhiteSpace(json)) return;
+
+ JObject jobj;
+ try
+ {
+ jobj = JObject.Parse(json);
+ }
+ catch { return; }
+
+ var cmdToken = jobj["cmd"];
+ if (cmdToken == null) return;
+ string cmd = cmdToken.Value();
+ if (string.IsNullOrWhiteSpace(cmd)) return;
+
+ // 兼容cmd带数字后缀的版本,如 DANMU_MSG-4b1cb6f3
+ int dashIdx = cmd.IndexOf('-');
+ if (dashIdx > 0) cmd = cmd.Substring(0, dashIdx);
+
+ switch (cmd)
+ {
+ case BiliCmd.DANMU_MSG:
+ {
+ var msg = ParseDanmuMsg(jobj);
+ if (msg != null)
+ {
+ OnChatMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, msg));
+ }
+ break;
+ }
+ case BiliCmd.SEND_GIFT:
+ {
+ var msg = jobj["data"]?.ToObject();
+ if (msg != null)
+ {
+ OnGiftMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, msg));
+ }
+ break;
+ }
+ case BiliCmd.INTERACT_WORD:
+ {
+ var msg = jobj["data"]?.ToObject();
+ if (msg != null)
+ {
+ // msg_type: 1=进入 2=关注 3=分享
+ if (msg.MsgType == 3)
+ {
+ OnShareMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, msg));
+ }
+ else
+ {
+ OnInteractMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, msg));
+ }
+ }
+ break;
+ }
+ case BiliCmd.LIKE_INFO_V3_CLICK:
+ {
+ var msg = jobj["data"]?.ToObject();
+ if (msg != null)
+ {
+ OnLikeMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, msg));
+ }
+ break;
+ }
+ case BiliCmd.ONLINE_RANK_COUNT:
+ {
+ var count = jobj["data"]?["count"]?.Value() ?? 0;
+ var stats = new BiliRoomStats { Online = count, Watched = 0 };
+ OnRoomStatsMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, stats));
+ break;
+ }
+ case BiliCmd.WATCHED_CHANGE:
+ {
+ var num = jobj["data"]?["num"]?.Value() ?? 0;
+ var stats = new BiliRoomStats { Online = 0, Watched = num };
+ OnRoomStatsMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, stats));
+ break;
+ }
+ case BiliCmd.ROOM_REAL_TIME_MESSAGE_UPDATE:
+ {
+ // 同时包含在线人数和看过人数
+ var online = jobj["data"]?["online"]?.Value() ?? 0;
+ var watched = jobj["data"]?["watched_show"]?["num"]?.Value() ?? 0;
+ var stats = new BiliRoomStats { Online = online, Watched = watched };
+ OnRoomStatsMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, stats));
+ break;
+ }
+ default:
+ // 未知消息类型忽略
+ break;
+ }
+ }
+
+ ///
+ /// 解析DANMU_MSG消息: info[1]=内容, info[2]=[uid,uname,isAdmin,rank,?anchor]
+ ///
+ private BiliDanmuMsg ParseDanmuMsg(JObject jobj)
+ {
+ try
+ {
+ var info = jobj["info"] as JArray;
+ if (info == null || info.Count < 3) return null;
+
+ var content = info[1]?.Value() ?? "";
+ var userInfo = info[2] as JArray;
+ if (userInfo == null || userInfo.Count < 2) return null;
+
+ var msg = new BiliDanmuMsg
+ {
+ Content = content,
+ Uid = userInfo[0]?.Value() ?? 0,
+ Uname = userInfo[1]?.Value() ?? "",
+ IsAdmin = userInfo.Count > 2 && userInfo[2]?.Value() == 1,
+ IsAnchor = userInfo.Count > 4 && userInfo[4] != null && userInfo[4].Type != JTokenType.Null
+ };
+ return msg;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ /// Zlib解压
+ private byte[] ZlibDecompress(byte[] data)
+ {
+ if (data == null || data.Length == 0) return null;
+ try
+ {
+ using (var ms = new MemoryStream(data))
+ using (var zlib = new DeflateStream(ms, CompressionMode.Decompress))
+ using (var outMs = new MemoryStream())
+ {
+ // zlib前2字节是header(0x78 0x9c等),DeflateStream需要跳过
+ // 但 .NET Core 2.1+ 的 DeflateStream 可以处理 zlib header, .NET Framework 需要跳过
+ if (data.Length >= 2 && data[0] == 0x78)
+ {
+ ms.Position = 2;
+ }
+ zlib.CopyTo(outMs);
+ return outMs.ToArray();
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarn($"B站Zlib解压失败: {ex.Message}");
+ return null;
+ }
+ }
+
+ /// Brotli解压 (使用 BrotliSharpLib)
+ private byte[] BrotliDecompress(byte[] data)
+ {
+ if (data == null || data.Length == 0) return null;
+ try
+ {
+ using (var ms = new MemoryStream(data))
+ using (var brotli = new BrotliSharpLib.BrotliStream(ms, CompressionMode.Decompress))
+ using (var outMs = new MemoryStream())
+ {
+ brotli.CopyTo(outMs);
+ return outMs.ToArray();
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarn($"B站Brotli解压失败: {ex.Message}");
+ return null;
+ }
+ }
+ }
+
+ /// B站消息事件参数
+ public class BiliRoomMessageEventArgs : EventArgs where T : class
+ {
+ public string Process { get; set; }
+ public T Message { get; set; }
+
+ public BiliRoomMessageEventArgs() { }
+
+ public BiliRoomMessageEventArgs(string process, T data)
+ {
+ this.Process = process;
+ this.Message = data;
+ }
+ }
+}
diff --git a/BarrageGrab/Server/KsBarrageGrab.cs b/BarrageGrab/Server/KsBarrageGrab.cs
new file mode 100644
index 0000000..d587008
--- /dev/null
+++ b/BarrageGrab/Server/KsBarrageGrab.cs
@@ -0,0 +1,120 @@
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Text.RegularExpressions;
+using BarrageGrab.Modles.KsProtoEntity;
+using BarrageGrab.Proxy.ProxyEventArgs;
+using ProtoBuf;
+
+namespace BarrageGrab
+{
+ public class KsBarrageGrab
+ {
+ // 2026-05 快手WS域名变更为 livejs-ws.kuaishou.cn
+ private readonly Regex kuaishouWsReg = new Regex(@"livejs-ws\.kuaishou\.cn|(?:livemate|pclivemate)\.pull\.yximgs\.com", RegexOptions.IgnoreCase);
+ private const string KUAISHOU_LIVE_HOST = "live.kuaishou.com";
+
+ public event EventHandler> OnChatMessage;
+ public event EventHandler> OnLikeMessage;
+ public event EventHandler> OnGiftMessage;
+ public event EventHandler> OnShareMessage;
+ public event EventHandler> OnRoomStatsMessage;
+
+ public KsBarrageGrab()
+ {
+ }
+
+ public bool IsKuaishouHost(string hostname)
+ {
+ if (string.IsNullOrWhiteSpace(hostname)) return false;
+ hostname = hostname.Trim().ToLower();
+ return kuaishouWsReg.IsMatch(hostname) || hostname == KUAISHOU_LIVE_HOST;
+ }
+
+ public bool IsKuaishouWebSocket(string uri)
+ {
+ if (string.IsNullOrWhiteSpace(uri)) return false;
+ return kuaishouWsReg.IsMatch(uri);
+ }
+
+ public void HandleWebSocketData(WsMessageEventArgs e)
+ {
+ var buff = e.Payload;
+ if (buff == null || buff.Length == 0) return;
+
+ try
+ {
+ var socketMsg = Serializer.Deserialize(new ReadOnlyMemory(buff));
+ if (socketMsg == null) return;
+
+ if (socketMsg.PayloadType != KsPayloadType.SC_FEED_PUSH) return;
+
+ byte[] payload = socketMsg.Payload;
+ if (socketMsg.CompressionType == KsCompressionType.GZIP)
+ {
+ payload = Decompress(payload);
+ }
+
+ var feedPush = Serializer.Deserialize(new ReadOnlyMemory(payload));
+ if (feedPush == null) return;
+
+ foreach (var comment in feedPush.CommentFeeds)
+ {
+ OnChatMessage?.Invoke(this, new KsRoomMessageEventArgs(e.ProcessName, comment));
+ }
+
+ foreach (var like in feedPush.LikeFeeds)
+ {
+ OnLikeMessage?.Invoke(this, new KsRoomMessageEventArgs(e.ProcessName, like));
+ }
+
+ foreach (var gift in feedPush.GiftFeeds)
+ {
+ OnGiftMessage?.Invoke(this, new KsRoomMessageEventArgs(e.ProcessName, gift));
+ }
+
+ foreach (var share in feedPush.ShareFeeds)
+ {
+ OnShareMessage?.Invoke(this, new KsRoomMessageEventArgs(e.ProcessName, share));
+ }
+
+ OnRoomStatsMessage?.Invoke(this, new KsRoomMessageEventArgs(e.ProcessName, feedPush));
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, $"处理快手弹幕数据包时出错:{ex.Message}");
+ }
+ }
+
+ private byte[] Decompress(byte[] zippedData)
+ {
+ using (var ms = new MemoryStream(zippedData))
+ using (var gzip = new GZipStream(ms, CompressionMode.Decompress))
+ using (var outBuffer = new MemoryStream())
+ {
+ byte[] block = new byte[1024];
+ while (true)
+ {
+ int bytesRead = gzip.Read(block, 0, block.Length);
+ if (bytesRead <= 0) break;
+ outBuffer.Write(block, 0, bytesRead);
+ }
+ return outBuffer.ToArray();
+ }
+ }
+ }
+
+ public class KsRoomMessageEventArgs : EventArgs where T : class
+ {
+ public string Process { get; set; }
+ public T Message { get; set; }
+
+ public KsRoomMessageEventArgs() { }
+
+ public KsRoomMessageEventArgs(string process, T data)
+ {
+ this.Process = process;
+ this.Message = data;
+ }
+ }
+}
diff --git a/BarrageGrab/Server/WsBarrageServer.cs b/BarrageGrab/Server/WsBarrageServer.cs
index 12b99d2..24345b0 100644
--- a/BarrageGrab/Server/WsBarrageServer.cs
+++ b/BarrageGrab/Server/WsBarrageServer.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Drawing;
@@ -39,7 +39,9 @@ namespace BarrageGrab
ConcurrentDictionary> giftCountCache = new ConcurrentDictionary>();//礼物计数缓存
Timer dieout = new Timer(10000);//离线客户端清理计时器
Timer giftCountTimer = new Timer(10000);//礼物缓存清理计时器
- WssBarrageGrab grab = new WssBarrageGrab();//弹幕解析器核心
+ WssBarrageGrab grab = new WssBarrageGrab();//抖音弹幕解析器核心
+ KsBarrageGrab ksGrab = new KsBarrageGrab();//快手弹幕解析器核心
+ BiliBarrageGrab biliGrab = new BiliBarrageGrab();//B站弹幕解析器核心
AppSetting Appsetting = AppSetting.Current;//全局配置文件实例
static int printCount = 0; //控制台输出计数,用于判断清理控制台
@@ -92,6 +94,43 @@ namespace BarrageGrab
this.grab.OnControlMessage += Grab_OnControlMessage;
this.grab.Proxy.OnLiveCompanAction += Proxy_OnLiveCompanAction;
+ //快手事件订阅
+ this.ksGrab.OnChatMessage += KsGrab_OnChatMessage;
+ this.ksGrab.OnLikeMessage += KsGrab_OnLikeMessage;
+ this.ksGrab.OnGiftMessage += KsGrab_OnGiftMessage;
+ this.ksGrab.OnShareMessage += KsGrab_OnShareMessage;
+ this.ksGrab.OnRoomStatsMessage += KsGrab_OnRoomStatsMessage;
+
+ //B站事件订阅
+ this.biliGrab.OnChatMessage += BiliGrab_OnChatMessage;
+ this.biliGrab.OnLikeMessage += BiliGrab_OnLikeMessage;
+ this.biliGrab.OnGiftMessage += BiliGrab_OnGiftMessage;
+ this.biliGrab.OnInteractMessage += BiliGrab_OnInteractMessage;
+ this.biliGrab.OnShareMessage += BiliGrab_OnShareMessage;
+ this.biliGrab.OnRoomStatsMessage += BiliGrab_OnRoomStatsMessage;
+
+ //多平台WS数据路由 - 将代理收到的WS数据按平台分发到对应解析器
+ this.grab.Proxy.OnWebSocketData += (s, e) =>
+ {
+ if (e.Platform == LivePlatform.Kuaishou)
+ {
+ this.ksGrab.HandleWebSocketData(e);
+ }
+ else if (e.Platform == LivePlatform.Bilibili)
+ {
+ this.biliGrab.HandleWebSocketData(e);
+ }
+ };
+
+ //B站发送数据路由 - 解析AUTH认证包提取roomid
+ this.grab.Proxy.OnWebSocketSentData += (s, e) =>
+ {
+ if (e.Platform == LivePlatform.Bilibili)
+ {
+ this.biliGrab.HandleSentData(e);
+ }
+ };
+
this.socketServer = socket;
//dieout.Start();
giftCountTimer.Start();
@@ -633,6 +672,319 @@ namespace BarrageGrab
}
}
+ #region 快手消息处理
+
+ private MsgUser KsUserToMsgUser(Modles.KsProtoEntity.KsSimpleUserInfo user)
+ {
+ if (user == null) return null;
+ return new MsgUser()
+ {
+ Id = long.TryParse(user.PrincipalId, out var pid) ? pid : 0,
+ DisplayId = user.PrincipalId ?? "",
+ Nickname = user.UserName ?? "用户" + user.PrincipalId,
+ HeadImgUrl = user.HeadUrl ?? "",
+ Gender = 0,
+ Level = 0,
+ PayLevel = 0,
+ ShortId = 0,
+ };
+ }
+
+ private string KsRoomId;
+
+ private void KsGrab_OnChatMessage(object sender, KsRoomMessageEventArgs e)
+ {
+ var feed = e.Message;
+ var enty = new Msg()
+ {
+ MsgId = long.TryParse(feed.Id, out var id) ? id : DateTime.Now.Ticks,
+ User = KsUserToMsgUser(feed.User),
+ Content = feed.Content ?? "",
+ RoomId = KsRoomId ?? "",
+ WebRoomId = KsRoomId ?? "",
+ Appid = "0"
+ };
+
+ var msgType = PackMsgType.弹幕消息;
+ PrintMsg(enty, msgType);
+ var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Kuaishou);
+ Broadcast(pack);
+ }
+
+ private void KsGrab_OnLikeMessage(object sender, KsRoomMessageEventArgs e)
+ {
+ var feed = e.Message;
+ var enty = new LikeMsg()
+ {
+ MsgId = long.TryParse(feed.Id, out var id) ? id : DateTime.Now.Ticks,
+ User = KsUserToMsgUser(feed.User),
+ Content = $"{feed.User?.UserName ?? "用户"} 为主播点了赞",
+ Count = 1,
+ Total = 0,
+ RoomId = KsRoomId ?? "",
+ WebRoomId = KsRoomId ?? "",
+ Appid = "0"
+ };
+
+ var msgType = PackMsgType.点赞消息;
+ PrintMsg(enty, msgType);
+ var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Kuaishou);
+ Broadcast(pack);
+ }
+
+ private void KsGrab_OnGiftMessage(object sender, KsRoomMessageEventArgs e)
+ {
+ var feed = e.Message;
+ var enty = new GiftMsg()
+ {
+ MsgId = long.TryParse(feed.Id, out var id) ? id : DateTime.Now.Ticks,
+ User = KsUserToMsgUser(feed.User),
+ GiftId = feed.GiftId,
+ GiftName = KsGiftHelper.GetGiftName(feed.GiftId) ?? $"礼物#{feed.GiftId}",
+ GroupId = 0,
+ GiftCount = feed.ComboCount > 0 ? feed.ComboCount : 1,
+ RepeatCount = feed.ComboCount > 0 ? feed.ComboCount : 1,
+ DiamondCount = 0,
+ Combo = feed.ComboCount > 1,
+ Content = $"{feed.User?.UserName ?? "用户"} 送出 {KsGiftHelper.GetGiftName(feed.GiftId) ?? $"礼物#{feed.GiftId}"} x {feed.ComboCount}",
+ RoomId = KsRoomId ?? "",
+ WebRoomId = KsRoomId ?? "",
+ Appid = "0"
+ };
+
+ var msgType = PackMsgType.礼物消息;
+ PrintMsg(enty, msgType);
+ var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Kuaishou);
+ Broadcast(pack);
+ }
+
+ private void KsGrab_OnShareMessage(object sender, KsRoomMessageEventArgs e)
+ {
+ var feed = e.Message;
+ var enty = new ShareMessage()
+ {
+ MsgId = long.TryParse(feed.Id, out var id) ? id : DateTime.Now.Ticks,
+ User = KsUserToMsgUser(feed.User),
+ Content = $"{feed.User?.UserName ?? "用户"} 分享了直播间",
+ ShareType = ShareType.未知,
+ RoomId = KsRoomId ?? "",
+ WebRoomId = KsRoomId ?? "",
+ Appid = "0"
+ };
+
+ var msgType = PackMsgType.直播间分享;
+ PrintMsg(enty, msgType);
+ var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Kuaishou);
+ Broadcast(pack);
+ }
+
+ private void KsGrab_OnRoomStatsMessage(object sender, KsRoomMessageEventArgs e)
+ {
+ var feed = e.Message;
+ var enty = new UserSeqMsg()
+ {
+ MsgId = DateTime.Now.Ticks,
+ Content = $"当前直播间人数 {feed.DisplayWatchingCount},点赞 {feed.DisplayLikeCount}",
+ OnlineUserCount = long.TryParse(feed.DisplayWatchingCount, out var watching) ? watching : 0,
+ TotalUserCount = 0,
+ OnlineUserCountStr = feed.DisplayWatchingCount ?? "0",
+ TotalUserCountStr = "0",
+ RoomId = KsRoomId ?? "",
+ WebRoomId = KsRoomId ?? "",
+ Appid = "0"
+ };
+
+ var msgType = PackMsgType.直播间统计;
+ PrintMsg(enty, msgType);
+ var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Kuaishou);
+ Broadcast(pack);
+ }
+
+ #endregion
+
+ #region B站消息处理
+
+ private MsgUser BiliUserToMsgUser(Modles.BiliProtoEntity.BiliDanmuMsg user)
+ {
+ if (user == null) return null;
+ return new MsgUser()
+ {
+ Id = user.Uid,
+ DisplayId = user.Uid.ToString(),
+ Nickname = user.Uname ?? "用户" + user.Uid,
+ IsAdmin = user.IsAdmin,
+ IsAnchor = user.IsAnchor,
+ };
+ }
+
+ private MsgUser BiliUserToMsgUser(long uid, string uname)
+ {
+ return new MsgUser()
+ {
+ Id = uid,
+ DisplayId = uid.ToString(),
+ Nickname = string.IsNullOrWhiteSpace(uname) ? "用户" + uid : uname,
+ };
+ }
+
+ //B站弹幕
+ private void BiliGrab_OnChatMessage(object sender, BiliRoomMessageEventArgs e)
+ {
+ var msg = e.Message;
+ var enty = new Msg()
+ {
+ MsgId = DateTime.Now.Ticks,
+ User = BiliUserToMsgUser(msg),
+ Content = msg.Content ?? "",
+ RoomId = biliGrab.CurrentRoomId,
+ WebRoomId = biliGrab.CurrentRoomId,
+ Appid = "0"
+ };
+
+ var msgType = PackMsgType.弹幕消息;
+ PrintMsg(enty, msgType);
+ var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Bilibili);
+ Broadcast(pack);
+ }
+
+ //B站点赞
+ private void BiliGrab_OnLikeMessage(object sender, BiliRoomMessageEventArgs e)
+ {
+ var msg = e.Message;
+ var enty = new LikeMsg()
+ {
+ MsgId = DateTime.Now.Ticks,
+ User = BiliUserToMsgUser(msg.Uid, msg.Uname),
+ Content = $"{msg.Uname ?? "用户"} 为主播点了赞",
+ Count = 1,
+ Total = 0,
+ RoomId = biliGrab.CurrentRoomId,
+ WebRoomId = biliGrab.CurrentRoomId,
+ Appid = "0"
+ };
+
+ var msgType = PackMsgType.点赞消息;
+ PrintMsg(enty, msgType);
+ var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Bilibili);
+ Broadcast(pack);
+ }
+
+ //B站礼物
+ private void BiliGrab_OnGiftMessage(object sender, BiliRoomMessageEventArgs e)
+ {
+ var msg = e.Message;
+ var count = msg.Num > 0 ? msg.Num : 1;
+ var enty = new GiftMsg()
+ {
+ MsgId = DateTime.Now.Ticks,
+ User = BiliUserToMsgUser(msg.Uid, msg.Uname),
+ GiftId = msg.GiftId,
+ GiftName = msg.GiftName ?? $"礼物#{msg.GiftId}",
+ GroupId = 0,
+ GiftCount = count,
+ RepeatCount = msg.SuperBatchNum > 0 ? msg.SuperBatchNum : count,
+ DiamondCount = (int)(msg.Price * 1000),
+ Combo = msg.ComboNum > 1,
+ ImgUrl = msg.GifUrl ?? "",
+ Content = $"{msg.Uname ?? "用户"} 送出 {msg.GiftName ?? $"礼物#{msg.GiftId}"} x {count}",
+ RoomId = biliGrab.CurrentRoomId,
+ WebRoomId = biliGrab.CurrentRoomId,
+ Appid = "0"
+ };
+
+ var msgType = PackMsgType.礼物消息;
+ PrintMsg(enty, msgType);
+ var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Bilibili);
+ Broadcast(pack);
+ }
+
+ //B站进房/关注
+ private void BiliGrab_OnInteractMessage(object sender, BiliRoomMessageEventArgs e)
+ {
+ var msg = e.Message;
+ // msg_type: 1=进入 2=关注
+ if (msg.MsgType == 2)
+ {
+ var enty = new Msg()
+ {
+ MsgId = DateTime.Now.Ticks,
+ User = BiliUserToMsgUser(msg.Uid, msg.Uname),
+ Content = $"{msg.Uname ?? "用户"} 关注了主播",
+ RoomId = biliGrab.CurrentRoomId,
+ WebRoomId = biliGrab.CurrentRoomId,
+ Appid = "0"
+ };
+ var msgType = PackMsgType.关注消息;
+ PrintMsg(enty, msgType);
+ var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Bilibili);
+ Broadcast(pack);
+ }
+ else
+ {
+ var enty = new Modles.JsonEntity.MemberMessage()
+ {
+ MsgId = DateTime.Now.Ticks,
+ User = BiliUserToMsgUser(msg.Uid, msg.Uname),
+ Content = $"{msg.Uname ?? "用户"} 来了直播间",
+ CurrentCount = 0,
+ EnterTipType = 0,
+ RoomId = biliGrab.CurrentRoomId,
+ WebRoomId = biliGrab.CurrentRoomId,
+ Appid = "0"
+ };
+ var msgType = PackMsgType.进直播间;
+ PrintMsg(enty, msgType);
+ var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Bilibili);
+ Broadcast(pack);
+ }
+ }
+
+ //B站分享
+ private void BiliGrab_OnShareMessage(object sender, BiliRoomMessageEventArgs e)
+ {
+ var msg = e.Message;
+ var enty = new ShareMessage()
+ {
+ MsgId = DateTime.Now.Ticks,
+ User = BiliUserToMsgUser(msg.Uid, msg.Uname),
+ Content = $"{msg.Uname ?? "用户"} 分享了直播间",
+ ShareType = ShareType.未知,
+ RoomId = biliGrab.CurrentRoomId,
+ WebRoomId = biliGrab.CurrentRoomId,
+ Appid = "0"
+ };
+
+ var msgType = PackMsgType.直播间分享;
+ PrintMsg(enty, msgType);
+ var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Bilibili);
+ Broadcast(pack);
+ }
+
+ //B站房间统计
+ private void BiliGrab_OnRoomStatsMessage(object sender, BiliRoomMessageEventArgs e)
+ {
+ var msg = e.Message;
+ var enty = new UserSeqMsg()
+ {
+ MsgId = DateTime.Now.Ticks,
+ Content = $"当前直播间人数 {msg.Online},看过 {msg.Watched}",
+ OnlineUserCount = msg.Online,
+ TotalUserCount = msg.Watched,
+ OnlineUserCountStr = msg.Online.ToString(),
+ TotalUserCountStr = msg.Watched.ToString(),
+ RoomId = biliGrab.CurrentRoomId,
+ WebRoomId = biliGrab.CurrentRoomId,
+ Appid = "0"
+ };
+
+ var msgType = PackMsgType.直播间统计;
+ PrintMsg(enty, msgType);
+ var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Bilibili);
+ Broadcast(pack);
+ }
+
+ #endregion
+
//监听用户连接
private void Listen(IWebSocketConnection socket)
{
diff --git a/BarrageGrab/Server/WssBarrageGrab.cs b/BarrageGrab/Server/WssBarrageGrab.cs
index 8525b71..6e4ccf8 100644
--- a/BarrageGrab/Server/WssBarrageGrab.cs
+++ b/BarrageGrab/Server/WssBarrageGrab.cs
@@ -5,6 +5,7 @@ using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
+using BarrageGrab.Modles.JsonEntity;
using BarrageGrab.Modles.ProtoEntity;
using BarrageGrab.Proxy;
using BarrageGrab.Proxy.ProxyEventArgs;
@@ -24,7 +25,7 @@ namespace BarrageGrab
///
/// 进入直播间
///
- public event EventHandler> OnMemberMessage;
+ public event EventHandler> OnMemberMessage;
///
/// 关注
@@ -103,8 +104,10 @@ namespace BarrageGrab
}
//ws数据处理
+ private static int douyinErrCount = 0;
private void Proxy_OnWebSocketData(object sender, WsMessageEventArgs e)
{
+ if (e.Platform != LivePlatform.Douyin) return;
if (!appsetting.ProcessFilter.Contains(e.ProcessName)) return;
var buff = e.Payload;
if (buff.Length == 0) return;
@@ -129,7 +132,17 @@ namespace BarrageGrab
}
catch (Exception ex)
{
- Logger.LogError(ex, $"处理弹幕数据包时出错:{ex.Message}");
+ // 限流:同类错误最多打印3次,防止刷屏和连锁崩溃
+ if (douyinErrCount < 3)
+ {
+ Logger.LogError(ex, $"处理抖音弹幕数据包时出错:{ex.Message}");
+ douyinErrCount++;
+ }
+ if (douyinErrCount == 3)
+ {
+ Logger.LogWarn("抖音弹幕解析持续失败,已抑制后续错误日志(不影响快手弹幕)");
+ douyinErrCount++;
+ }
}
}
@@ -183,8 +196,8 @@ namespace BarrageGrab
//来了
case "WebcastMemberMessage":
{
- var arg = Serializer.Deserialize(new ReadOnlyMemory(msg.Payload));
- this.OnMemberMessage?.Invoke(this, new RoomMessageEventArgs(processName, arg));
+ var arg = Serializer.Deserialize(new ReadOnlyMemory(msg.Payload));
+ this.OnMemberMessage?.Invoke(this, new RoomMessageEventArgs(processName, arg));
break;
}
//关注
diff --git a/BarrageGrab/Utility/BiliApiHelper.cs b/BarrageGrab/Utility/BiliApiHelper.cs
new file mode 100644
index 0000000..06f874e
--- /dev/null
+++ b/BarrageGrab/Utility/BiliApiHelper.cs
@@ -0,0 +1,271 @@
+using System;
+using System.Net.Http;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using BarrageGrab.Modles;
+using Newtonsoft.Json.Linq;
+
+namespace BarrageGrab
+{
+ ///
+ /// B站直播API帮助类
+ ///
+ internal class BiliApiHelper
+ {
+ private const string UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0";
+
+ ///
+ /// 通过B站官方API获取房间信息 (推荐方式,稳定可靠)
+ /// 流程: get_info API 获取房间基本信息 -> Master/info API 获取主播昵称/头像
+ ///
+ public static async Task GetRoomInfoByApi(string webRoomId, string cookie = null)
+ {
+ if (string.IsNullOrWhiteSpace(webRoomId)) return null;
+
+ var client = CreateHttpClient(cookie);
+
+ try
+ {
+ // 1. 获取房间信息 (get_info 支持短ID/长ID自动转换)
+ string roomInfoUrl = $"https://api.live.bilibili.com/room/v1/Room/get_info?room_id={webRoomId}";
+ var roomResp = await client.GetStringAsync(roomInfoUrl);
+ var roomJson = JObject.Parse(roomResp);
+
+ if (roomJson["code"]?.Value() != 0)
+ {
+ Logger.LogWarn($"B站 get_info API 返回错误: {roomJson["message"]?.Value()}");
+ return null;
+ }
+
+ var data = roomJson["data"];
+ var info = new BiliRoomInfo
+ {
+ WebRoomId = webRoomId,
+ RoomId = data["room_id"]?.Value().ToString() ?? webRoomId,
+ ShortId = data["short_id"]?.Value().ToString() ?? "0",
+ Uid = data["uid"]?.Value().ToString() ?? "",
+ Title = data["title"]?.Value() ?? "",
+ IsLive = (data["live_status"]?.Value() ?? 0) == 1,
+ Online = data["online"]?.Value() ?? 0,
+ AreaName = data["area_name"]?.Value() ?? "",
+ ParentAreaName = data["parent_area_name"]?.Value() ?? "",
+ CoverUrl = data["user_cover"]?.Value()
+ ?? data["keyframe"]?.Value()
+ ?? ""
+ };
+
+ // 2. 获取主播昵称和头像
+ if (!string.IsNullOrWhiteSpace(info.Uid))
+ {
+ try
+ {
+ string masterUrl = $"https://api.live.bilibili.com/live_user/v1/Master/info?uid={info.Uid}";
+ var masterResp = await client.GetStringAsync(masterUrl);
+ var masterJson = JObject.Parse(masterResp);
+
+ if (masterJson["code"]?.Value() == 0)
+ {
+ var masterData = masterJson["data"];
+ info.Nickname = masterData["info"]?["uname"]?.Value() ?? "";
+ info.HeadUrl = masterData["info"]?["face"]?.Value() ?? "";
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarn($"获取B站主播信息失败: {ex.Message}");
+ }
+ }
+
+ if (string.IsNullOrWhiteSpace(info.Nickname))
+ {
+ info.Nickname = $"B站用户{info.Uid}";
+ }
+
+ Logger.LogInfo($"B站API获取房间信息成功: WebRoomId={webRoomId}, RoomId={info.RoomId}, Uname={info.Nickname}, Online={info.Online}, IsLive={info.IsLive}");
+ return info;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, $"B站API获取房间信息失败: {ex.Message}");
+ return null;
+ }
+ }
+
+ ///
+ /// 从B站直播间页面HTML中解析房间信息 (备用方案)
+ /// 注意: B站前端改版后页面可能不再注入 __INITIAL_STATE__, 此方法可能失效
+ ///
+ public static BiliRoomInfo ParseRoomInfoFromHtml(string html, string webRoomId)
+ {
+ var info = new BiliRoomInfo { WebRoomId = webRoomId };
+ if (string.IsNullOrWhiteSpace(html)) return info;
+
+ try
+ {
+ // 用括号配对算法可靠提取 __INITIAL_STATE__ 的 JSON
+ string jsonStr = ExtractJsonObject(html, "__INITIAL_STATE__");
+ if (string.IsNullOrWhiteSpace(jsonStr))
+ {
+ // B站改版后可能不再注入 __INITIAL_STATE__, 返回空对象由调用方走API
+ return info;
+ }
+
+ JObject root;
+ try
+ {
+ root = JObject.Parse(jsonStr);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarn($"B站 __INITIAL_STATE__ JSON 解析失败: {ex.Message}");
+ return info;
+ }
+
+ var roomInfoNode = root["roomInfo"]?["roomInfo"];
+ var anchorInfoNode = root["roomInfo"]?["anchorInfo"];
+
+ if (roomInfoNode != null)
+ {
+ info.RoomId = TokenToString(roomInfoNode["room_id"], webRoomId);
+ info.ShortId = TokenToString(roomInfoNode["short_id"], "0");
+ info.Uid = TokenToString(roomInfoNode["uid"], "");
+ info.Title = roomInfoNode["title"]?.Value() ?? "";
+ info.IsLive = (roomInfoNode["live_id"]?.Value() ?? 0) != 0;
+ info.Online = roomInfoNode["online"]?.Value() ?? 0;
+ info.AreaName = roomInfoNode["area_name"]?.Value() ?? "";
+ info.ParentAreaName = roomInfoNode["parent_area_name"]?.Value() ?? "";
+ info.CoverUrl = roomInfoNode["cover"]?.Value()
+ ?? roomInfoNode["keyframe"]?.Value()
+ ?? "";
+ }
+
+ if (anchorInfoNode != null)
+ {
+ var baseInfo = anchorInfoNode["base_info"];
+ info.Nickname = baseInfo?["uname"]?.Value()
+ ?? anchorInfoNode["uname"]?.Value()
+ ?? "";
+ info.HeadUrl = baseInfo?["face"]?.Value()
+ ?? anchorInfoNode["face"]?.Value()
+ ?? "";
+
+ var relationInfo = anchorInfoNode["relation_info"];
+ if (relationInfo != null)
+ {
+ info.Attention = relationInfo["attribute"]?.Value() ?? 0;
+ }
+ }
+
+ Logger.LogInfo($"B站HTML解析房间成功: WebRoomId={webRoomId}, RoomId={info.RoomId}, Uname={info.Nickname}");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarn($"解析B站页面房间信息时出错: {ex.Message}");
+ }
+
+ return info;
+ }
+
+ ///
+ /// 从HTML中提取 __INITIAL_STATE__ 后的完整JSON对象 (括号配对算法)
+ /// 不受 < 字符或 ;(function 等干扰, 比正则更可靠
+ ///
+ private static string ExtractJsonObject(string html, string marker)
+ {
+ int idx = html.IndexOf(marker);
+ if (idx < 0) return null;
+
+ // 找到 = 后面的第一个 {
+ int eqIdx = html.IndexOf('=', idx);
+ if (eqIdx < 0) return null;
+
+ int braceIdx = html.IndexOf('{', eqIdx);
+ if (braceIdx < 0) return null;
+
+ // 逐字符扫描计算 { } 嵌套层级, 同时处理字符串和转义
+ int depth = 0;
+ bool inString = false;
+ bool escape = false;
+
+ for (int i = braceIdx; i < html.Length; i++)
+ {
+ char c = html[i];
+
+ if (escape) { escape = false; continue; }
+ if (c == '\\') { escape = true; continue; }
+ if (c == '"') { inString = !inString; continue; }
+ if (inString) continue;
+
+ if (c == '{') depth++;
+ else if (c == '}')
+ {
+ depth--;
+ if (depth == 0)
+ {
+ return html.Substring(braceIdx, i - braceIdx + 1);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /// JSON token 转字符串 (兼容整数/字符串类型)
+ private static string TokenToString(JToken token, string defaultValue)
+ {
+ if (token == null) return defaultValue;
+ return token.Type == JTokenType.Integer
+ ? token.Value().ToString()
+ : token.Value() ?? defaultValue;
+ }
+
+ private static HttpClient CreateHttpClient(string cookie)
+ {
+ var client = new HttpClient();
+ client.DefaultRequestHeaders.Add("User-Agent", UA);
+ client.DefaultRequestHeaders.Add("Accept", "application/json, text/plain, */*");
+ client.DefaultRequestHeaders.Add("Accept-Language", "zh-CN,zh;q=0.9");
+ client.DefaultRequestHeaders.Add("Referer", "https://live.bilibili.com/");
+ if (!string.IsNullOrWhiteSpace(cookie))
+ {
+ client.DefaultRequestHeaders.Add("Cookie", cookie);
+ }
+ return client;
+ }
+ }
+
+ ///
+ /// B站房间信息
+ ///
+ public class BiliRoomInfo
+ {
+ /// 用户访问的房间号 (URL中的ID, 可能是短ID或真实room_id)
+ public string WebRoomId { get; set; }
+ /// 真实的房间长ID
+ public string RoomId { get; set; }
+ /// 短ID (无短ID时为"0")
+ public string ShortId { get; set; }
+ /// 主播UID
+ public string Uid { get; set; }
+ /// 直播间标题
+ public string Title { get; set; }
+ /// 主播昵称
+ public string Nickname { get; set; }
+ /// 主播头像
+ public string HeadUrl { get; set; }
+ /// 直播间封面
+ public string CoverUrl { get; set; }
+ /// 是否直播中
+ public bool IsLive { get; set; }
+ /// 在线人数
+ public long Online { get; set; }
+ /// 子分区名
+ public string AreaName { get; set; }
+ /// 父分区名
+ public string ParentAreaName { get; set; }
+ /// 关注状态 (0未关注 2已关注 6已互关)
+ public int Attention { get; set; }
+ /// 直播间地址
+ public string LiveUrl { get; set; }
+ }
+}
diff --git a/BarrageGrab/Utility/KsApiHelper.cs b/BarrageGrab/Utility/KsApiHelper.cs
new file mode 100644
index 0000000..ce09322
--- /dev/null
+++ b/BarrageGrab/Utility/KsApiHelper.cs
@@ -0,0 +1,173 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using BarrageGrab.Modles;
+using Newtonsoft.Json.Linq;
+
+namespace BarrageGrab
+{
+ internal class KsApiHelper
+ {
+ public static async Task GetRoomInfo(string userId, string cookie = null)
+ {
+ string url = $"https://live.kuaishou.com/u/{userId}";
+
+ var client = new HttpClient();
+ client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0");
+ client.DefaultRequestHeaders.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
+ client.DefaultRequestHeaders.Add("Accept-Language", "zh-CN,zh;q=0.9");
+ if (!string.IsNullOrWhiteSpace(cookie))
+ {
+ client.DefaultRequestHeaders.Add("Cookie", cookie);
+ }
+
+ try
+ {
+ var response = await client.GetStringAsync(url);
+ return ParseRoomInfoFromHtml(response, userId);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"获取快手房间信息失败: {ex.Message}");
+ return null;
+ }
+ }
+
+ public static KsRoomInfo ParseRoomInfoFromHtml(string html, string userId)
+ {
+ var info = new KsRoomInfo { UserId = userId };
+
+ try
+ {
+ // 2026-05 快手页面结构变更: __INITIAL_STATE__ JSON 中的字段名和旧版本不同
+ // liveStreamId → 嵌套在 "liveStream":{"id":"xxx"}
+ var liveStreamIdReg = new Regex(@"""liveStream""\s*:\s*\{[^}]*?""id""\s*:\s*""([^""]+)""");
+ var liveStreamMatch = liveStreamIdReg.Match(html);
+ if (liveStreamMatch.Success)
+ {
+ info.LiveStreamId = liveStreamMatch.Groups[1].Value;
+ }
+
+ // 直播封面 → liveStream.poster
+ var posterReg = new Regex(@"""liveStream""\s*:\s*\{[^}]*?""poster""\s*:\s*""([^""]+)""");
+ var posterMatch = posterReg.Match(html);
+ if (posterMatch.Success)
+ {
+ info.CoverUrl = UnescapeJson(posterMatch.Groups[1].Value);
+ }
+
+ // token
+ var tokenReg = new Regex(@"""token""\s*:\s*""([^""]*)""");
+ var tokenMatch = tokenReg.Match(html);
+ if (tokenMatch.Success && !string.IsNullOrWhiteSpace(tokenMatch.Groups[1].Value))
+ {
+ info.Token = tokenMatch.Groups[1].Value;
+ }
+
+ // 主播昵称 → 嵌套在 "author":{"...","name":"xxx"}
+ var nickReg = new Regex(@"""author""\s*:\s*\{[^}]*?""name""\s*:\s*""([^""]+)""");
+ var nickMatch = nickReg.Match(html);
+ if (nickMatch.Success)
+ {
+ info.Nickname = nickMatch.Groups[1].Value;
+ }
+
+ // 主播头像 → author.avatar
+ var avatarReg = new Regex(@"""author""\s*:\s*\{[^}]*?""avatar""\s*:\s*""([^""]+)""");
+ var avatarMatch = avatarReg.Match(html);
+ if (avatarMatch.Success)
+ {
+ info.HeadUrl = UnescapeJson(avatarMatch.Groups[1].Value);
+ }
+
+ // 观看人数 → 现在是字符串如 "1万+",不是纯数字
+ var watchingReg = new Regex(@"""watchingCount""\s*:\s*""([^""]+)""");
+ var watchingMatch = watchingReg.Match(html);
+ if (watchingMatch.Success)
+ {
+ info.WatchingCountStr = watchingMatch.Groups[1].Value;
+ info.WatchingCount = ParseCount(info.WatchingCountStr);
+ }
+
+ // 点赞数 → 字段名已变更为 liked
+ var likeReg = new Regex(@"""liked""\s*:\s*""([^""]+)""");
+ var likeMatch = likeReg.Match(html);
+ if (likeMatch.Success)
+ {
+ info.LikeCountStr = likeMatch.Groups[1].Value;
+ info.LikeCount = ParseCount(info.LikeCountStr);
+ }
+
+ // 直播状态 → 现在是 isLiving 布尔值
+ var liveReg = new Regex(@"""isLiving""\s*:\s*(true|false)");
+ var liveMatch = liveReg.Match(html);
+ if (liveMatch.Success)
+ {
+ info.IsLive = liveMatch.Groups[1].Value == "true";
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarn($"解析快手页面房间信息时出错: {ex.Message}");
+ }
+
+ return info;
+ }
+
+ // 将 "1万+" "2853" "6.7w" 等转为 long
+ private static long ParseCount(string str)
+ {
+ if (string.IsNullOrWhiteSpace(str)) return 0;
+ str = str.Trim();
+ // 含"万" → 乘以10000
+ if (str.Contains("万"))
+ {
+ var numStr = Regex.Replace(str, @"[^\d\.]", "");
+ if (double.TryParse(numStr, out var d)) return (long)(d * 10000);
+ }
+ // 含"w" → 乘以10000
+ if (str.ToLower().Contains("w"))
+ {
+ var numStr = Regex.Replace(str, @"[^\d\.]", "");
+ if (double.TryParse(numStr, out var d)) return (long)(d * 10000);
+ }
+ // 纯数字
+ if (long.TryParse(str, out var l)) return l;
+ return 0;
+ }
+
+ // 反转义 JSON Unicode 转义序列 (\\u002F → /)
+ private static string UnescapeJson(string str)
+ {
+ if (string.IsNullOrWhiteSpace(str)) return str;
+ return Regex.Replace(str, @"\\u([0-9a-fA-F]{4})", m =>
+ {
+ int code = int.Parse(m.Groups[1].Value, System.Globalization.NumberStyles.HexNumber);
+ return ((char)code).ToString();
+ });
+ }
+ }
+
+ public class KsRoomInfo
+ {
+ public string UserId { get; set; }
+ public string LiveStreamId { get; set; }
+ public string Token { get; set; }
+ public string Nickname { get; set; }
+ public long WatchingCount { get; set; }
+ public long LikeCount { get; set; }
+ public bool IsLive { get; set; }
+ public string PageId { get; set; } = Guid.NewGuid().ToString("N").Substring(0, 16);
+ public string LiveUrl { get; set; }
+ // 字符串格式的原始值(用于展示 "1万+" 等)
+ public string WatchingCountStr { get; set; }
+ public string LikeCountStr { get; set; }
+ // 头像和封面
+ public string HeadUrl { get; set; }
+ public string CoverUrl { get; set; }
+ }
+}
diff --git a/BarrageGrab/Utility/KsGiftHelper.cs b/BarrageGrab/Utility/KsGiftHelper.cs
new file mode 100644
index 0000000..d7aa434
--- /dev/null
+++ b/BarrageGrab/Utility/KsGiftHelper.cs
@@ -0,0 +1,213 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace BarrageGrab
+{
+ ///
+ /// 快手礼物ID→名称映射表,通过代理嗅探自动收集
+ ///
+ public static class KsGiftHelper
+ {
+ private static readonly ConcurrentDictionary GiftNames = new ConcurrentDictionary();
+ private static readonly string GiftCachePath;
+
+ static KsGiftHelper()
+ {
+ GiftCachePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ks_gifts.json");
+ LoadMapping();
+ }
+
+ ///
+ /// 获取礼物名称,找不到返回 null
+ ///
+ public static string GetGiftName(uint giftId)
+ {
+ GiftNames.TryGetValue(giftId, out var name);
+ return name;
+ }
+
+ ///
+ /// 尝试从任意 JSON 字符串中解析礼物映射
+ /// 支持多种快手 API 返回格式:
+ /// - giftList: [{id:1, name:"小红花"}, ...]
+ /// - giftPanelList: [{id:1, giftName:"小红花"}, ...]
+ /// - allGifts: {"1": {name:"小红花"}, ...}
+ /// 返回是否解析到了新数据
+ ///
+ public static bool TryParseGiftData(string json)
+ {
+ if (string.IsNullOrWhiteSpace(json)) return false;
+ json = json.Trim();
+ if (json.Length < 10) return false;
+
+ bool added = false;
+
+ try
+ {
+ // 如果 JSON 以数组开头,需要特殊处理
+ if (json.StartsWith("["))
+ {
+ var arr = JArray.Parse(json);
+ added = ParseGiftArray(arr) || added;
+ return added;
+ }
+
+ var obj = JObject.Parse(json);
+
+ // 方式1: giftList → [{id, name}] 或 giftList → [{id, giftName}]
+ var giftList = obj["giftList"] as JArray;
+ if (giftList != null)
+ {
+ added = ParseGiftArray(giftList) || added;
+ }
+
+ // 方式2: giftPanelList → [{id, name}]
+ var giftPanelList = obj["giftPanelList"] as JArray;
+ if (giftPanelList != null)
+ {
+ added = ParseGiftArray(giftPanelList) || added;
+ }
+
+ // 方式3: allGifts → {"1": {name:"xxx"}, "2": {...}}
+ var allGifts = obj["allGifts"] as JObject;
+ if (allGifts != null)
+ {
+ foreach (var kv in allGifts)
+ {
+ if (uint.TryParse(kv.Key, out var gid))
+ {
+ var name = kv.Value?["name"]?.Value()
+ ?? kv.Value?["giftName"]?.Value()
+ ?? kv.Value?["title"]?.Value();
+ if (!string.IsNullOrWhiteSpace(name))
+ {
+ added = UpdateMapping(gid, name) || added;
+ }
+ }
+ }
+ }
+
+ // 方式4: data.giftList / data.giftPanelList / result.giftList
+ var data = obj["data"] ?? obj["result"];
+ if (data is JObject dataObj)
+ {
+ var dataGiftList = dataObj["giftList"] as JArray;
+ if (dataGiftList != null)
+ {
+ added = ParseGiftArray(dataGiftList) || added;
+ }
+ var dataGiftPanel = dataObj["giftPanelList"] as JArray;
+ if (dataGiftPanel != null)
+ {
+ added = ParseGiftArray(dataGiftPanel) || added;
+ }
+ }
+ }
+ catch
+ {
+ // 不是有效 JSON,忽略
+ }
+
+ return added;
+ }
+
+ private static bool ParseGiftArray(JArray arr)
+ {
+ bool added = false;
+ foreach (var item in arr)
+ {
+ if (item is JObject giftObj)
+ {
+ var idToken = giftObj["id"] ?? giftObj["giftId"];
+ var nameToken = giftObj["giftName"] ?? giftObj["name"] ?? giftObj["title"] ?? giftObj["displayName"];
+
+ if (idToken != null && nameToken != null)
+ {
+ uint id = 0;
+ if (idToken.Type == JTokenType.Integer)
+ id = idToken.Value();
+ else if (idToken.Type == JTokenType.String && uint.TryParse(idToken.Value(), out var sid))
+ id = sid;
+ else
+ continue;
+
+ var name = nameToken.Value();
+ if (!string.IsNullOrWhiteSpace(name) && id > 0)
+ {
+ added = UpdateMapping(id, name) || added;
+ }
+ }
+ }
+ }
+ return added;
+ }
+
+ private static bool UpdateMapping(uint giftId, string giftName)
+ {
+ if (GiftNames.TryGetValue(giftId, out var existing) && existing == giftName)
+ return false;
+
+ GiftNames[giftId] = giftName;
+ Logger.LogInfo($"KS礼物映射: #{giftId} → {giftName}");
+ return true;
+ }
+
+ ///
+ /// 保存映射到本地文件
+ ///
+ public static void SaveMapping()
+ {
+ if (GiftNames.Count == 0) return;
+
+ try
+ {
+ var dict = new Dictionary();
+ foreach (var kv in GiftNames) dict[kv.Key] = kv.Value;
+ var json = JsonConvert.SerializeObject(dict, Formatting.Indented);
+ File.WriteAllText(GiftCachePath, json);
+ Logger.LogInfo($"KS礼物映射已保存 ({GiftNames.Count} 个) → {GiftCachePath}");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarn($"保存KS礼物映射失败: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 从本地文件加载映射
+ ///
+ private static void LoadMapping()
+ {
+ try
+ {
+ if (!File.Exists(GiftCachePath)) return;
+
+ var json = File.ReadAllText(GiftCachePath);
+ var dict = JsonConvert.DeserializeObject>(json);
+ if (dict != null)
+ {
+ foreach (var kv in dict)
+ {
+ GiftNames[kv.Key] = kv.Value;
+ }
+ Logger.LogInfo($"KS礼物映射已加载 ({GiftNames.Count} 个) → {GiftCachePath}");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarn($"加载KS礼物映射失败: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 获取当前映射数量
+ ///
+ public static int Count => GiftNames.Count;
+ }
+}
diff --git a/BarrageGrab/WssBarrageService.csproj b/BarrageGrab/WssBarrageService.csproj
index cbdd9ff..72bcdc9 100644
--- a/BarrageGrab/WssBarrageService.csproj
+++ b/BarrageGrab/WssBarrageService.csproj
@@ -1,4 +1,4 @@
-
+
@@ -11,7 +11,7 @@
Properties
BarrageGrab
WssBarrageServer
- v4.6.2
+ v4.8
512
true
false
@@ -28,9 +28,10 @@
false
false
true
- 0
+ 1
1.0.0.%2a
false
+ true
true
@@ -54,18 +55,18 @@
- LocalIntranet
-
-
- false
+ true
- Properties\app.manifest
+
logo.ico
+
+ false
+
..\packages\Acornima.1.1.0\lib\net462\Acornima.dll
@@ -233,6 +234,12 @@
FormView.cs
+
+ Form
+
+
+ Form
+
Form
@@ -259,6 +266,13 @@
+
+
+
+
+
+
+
@@ -267,9 +281,12 @@
WssBarrageService
-
+
+ PreserveNewest
+
+
@@ -327,4 +344,4 @@
rmdir /s /q "$(TargetDir)crts"
)
-
\ No newline at end of file
+
diff --git a/BarrageGrab/jiakong_settings.json b/BarrageGrab/jiakong_settings.json
new file mode 100644
index 0000000..1076135
--- /dev/null
+++ b/BarrageGrab/jiakong_settings.json
@@ -0,0 +1,13 @@
+{
+ "gameExePath": "D:\\UnityProjects\\jiakong\\Fantasy.Map.Simulator\\FantasyMapSimulator.exe",
+ "gameWorkDir": "D:\\UnityProjects\\jiakong\\Fantasy.Map.Simulator",
+ "autoLaunchGame": true,
+ "configDir": "D:\\UnityProjects\\jiakong\\Fantasy.Map.Simulator\\BepInEx\\config",
+ "configFiles": {
+ "effect": "effect_config.txt",
+ "macro": "macro_config.txt",
+ "gift": "gift_config.txt",
+ "command": "command_config.txt",
+ "fkey": "fkey_config.txt"
+ }
+}
\ No newline at end of file