Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ interface BotOutgoingMessageBuilder {
*/
fun forward(block: suspend BotForwardBlockBuilder.() -> Unit)

/**
* 添加小程序(LightApp)消息段
* @param jsonPayload JSON 格式的小程序数据
*/
fun lightApp(jsonPayload: String)

operator fun String.unaryPlus() = text(this)

}
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,18 @@ internal class MessageBuildingContext(
}
}

override fun lightApp(jsonPayload: String) = addAsync {
val buffer = Buffer()
buffer.writeByte(0x01)
buffer.write(ZLib.compress(jsonPayload.encodeToByteArray()))

Elem {
it[lightAppElem] = LightAppElem {
it[bytesData] = buffer.readByteArray()
}
}
}

suspend fun build(): List<PbObject<Elem>> = elemsList.awaitAll().flatten()

internal class Forward(
Expand Down Expand Up @@ -627,6 +639,11 @@ internal class MessageBuildingContext(
parent.forward(block)
previewBuilder.append("[聊天记录]")
}

override fun lightApp(jsonPayload: String) {
parent.lightApp(jsonPayload)
previewBuilder.append("[卡片消息]")
}
}
}
}
184 changes: 184 additions & 0 deletions build_jvm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
#!/usr/bin/env python3
"""
构建 yogurt-jvm-all.jar 的脚本

使用方法:
python build_jvm.py [--proxy HOST:PORT] [--java-home PATH]

示例:
python build_jvm.py
python build_jvm.py --proxy 192.168.31.84:7890
python build_jvm.py --proxy 127.0.0.1:7890 --java-home "D:\\SSoftwareFiles\\JDKs\\zulu21"
"""

import os
import sys
import subprocess
import argparse
from pathlib import Path


def find_java_home():
"""查找可用的 Java 21+ 路径"""
# 1. 检查环境变量
java_home = os.environ.get("JAVA_HOME")
if java_home and Path(java_home).exists():
return java_home

# 2. 常见的 JDK 安装位置
common_paths = [
# Windows
r"D:\SSoftwareFiles\JDKs\zulu21.42.19-ca-jdk21.0.7-win_x64",
r"C:\Program Files\Java\jdk-21",
r"C:\Program Files\Eclipse Adoptium\jdk-21",
r"C:\Program Files\Zulu\zulu-21",
# Linux/macOS
"/usr/lib/jvm/java-21-openjdk",
"/usr/lib/jvm/java-21",
"/opt/java/jdk-21",
]

for path in common_paths:
if Path(path).exists():
return path

return None


def run_gradle(project_root: Path, proxy_host: str = None, proxy_port: str = None, java_home: str = None):
"""运行 Gradle 构建"""

# 设置环境变量
env = os.environ.copy()

if java_home:
env["JAVA_HOME"] = java_home
print(f"✓ JAVA_HOME: {java_home}")

# 构建 Gradle 命令
if sys.platform == "win32":
gradle_cmd = str(project_root / "gradlew.bat")
else:
gradle_cmd = str(project_root / "gradlew")
# 确保有执行权限
os.chmod(gradle_cmd, 0o755)

cmd = [gradle_cmd, ":yogurt-jvm:buildFatJar", "--no-daemon"]

# 添加代理设置
if proxy_host and proxy_port:
cmd.extend([
f"-Dhttp.proxyHost={proxy_host}",
f"-Dhttp.proxyPort={proxy_port}",
f"-Dhttps.proxyHost={proxy_host}",
f"-Dhttps.proxyPort={proxy_port}",
])
print(f"✓ 代理: {proxy_host}:{proxy_port}")

print(f"✓ 工作目录: {project_root}")
print(f"✓ 执行命令: {' '.join(cmd)}")
print("-" * 60)

# 执行构建
try:
process = subprocess.run(
cmd,
cwd=project_root,
env=env,
# 不捕获输出,直接显示到终端
)
return process.returncode
except KeyboardInterrupt:
print("\n\n⚠ 构建被用户中断")
return 1
except Exception as e:
print(f"\n✗ 构建出错: {e}")
return 1


def main():
parser = argparse.ArgumentParser(
description="构建 yogurt-jvm-all.jar",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
python build_jvm.py
python build_jvm.py --proxy 192.168.31.84:7890
python build_jvm.py --proxy 127.0.0.1:7890 --java-home "D:\\JDKs\\jdk-21"
"""
)
parser.add_argument(
"--proxy",
type=str,
help="代理服务器地址 (格式: HOST:PORT,例如 192.168.31.84:7890)"
)
parser.add_argument(
"--java-home",
type=str,
help="Java 21+ 安装路径"
)

args = parser.parse_args()

print("=" * 60)
print("🍦 Yogurt JVM 构建脚本")
print("=" * 60)

# 获取项目根目录
script_dir = Path(__file__).parent.resolve()
project_root = script_dir

# 检查是否在正确的目录
if not (project_root / "gradlew.bat").exists() and not (project_root / "gradlew").exists():
print("✗ 错误: 找不到 gradlew,请确保脚本在项目根目录")
return 1

# 查找 Java
java_home = args.java_home or find_java_home()
if not java_home:
print("✗ 错误: 找不到 Java 21+,请使用 --java-home 参数指定")
print(" 示例: python build_jvm.py --java-home \"D:\\JDKs\\jdk-21\"")
return 1

# 解析代理
proxy_host = None
proxy_port = None
if args.proxy:
try:
proxy_host, proxy_port = args.proxy.split(":")
except ValueError:
print(f"✗ 错误: 代理格式不正确,应为 HOST:PORT")
return 1

print()

# 运行构建
ret = run_gradle(project_root, proxy_host, proxy_port, java_home)

print()
print("=" * 60)

if ret == 0:
jar_path = project_root / "yogurt-jvm" / "build" / "libs" / "yogurt-jvm-all.jar"
if jar_path.exists():
size_mb = jar_path.stat().st_size / (1024 * 1024)
print(f"✓ 构建成功!")
print(f"✓ JAR 文件: {jar_path}")
print(f"✓ 文件大小: {size_mb:.2f} MB")
else:
print("✓ 构建完成,但找不到 JAR 文件")
print(f" 预期位置: {jar_path}")
else:
print(f"✗ 构建失败 (退出码: {ret})")
print()
print("常见问题排查:")
print(" 1. 网络问题 - 尝试使用 --proxy 参数")
print(" 2. Java 版本 - 确保使用 Java 21+")
print(" 3. 查看上方的错误信息")

print("=" * 60)
return ret


if __name__ == "__main__":
sys.exit(main())
94 changes: 94 additions & 0 deletions pr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# PR: 添加音乐卡片消息支持

## 功能概述

本 PR 为 Yogurt 添加了音乐卡片消息发送功能,支持发送 QQ音乐、网易云音乐、酷狗音乐等平台的音乐卡片,以及自定义音乐卡片。

## 实现方式

### 1. 外部签名服务

由于音乐卡片消息需要特定格式的 Ark JSON,本实现采用了外部签名服务来生成 Ark 消息体:

- **签名服务地址**: `https://ss.xingzhige.com/music_card/card`
- **参考实现**: [NapCatQQ](https://github.com/NapNeko/NapCatQQ) 的音乐卡片功能

签名服务接收音乐类型和相关参数,返回可直接发送的 Ark JSON。

### 2. 新增文件

#### `yogurt/src/commonMain/kotlin/org/ntqqrev/yogurt/music/MusicSegment.kt`

包含以下类:

- `MusicType` - 音乐平台枚举 (qq/163/kugou/migu/kuwo/custom)
- `MusicSegment` / `MusicData` - 音乐消息段数据结构
- `MusicSignRequest` - 签名请求数据类
- `MusicSignResponse` - 签名响应数据类
- `MusicSignException` - 签名异常类
- `MusicSigner` - 签名器,负责调用外部签名服务

#### `yogurt/src/commonMain/kotlin/org/ntqqrev/yogurt/api/message/SendMusicMessage.kt`

新增两个 HTTP API 端点:

- `POST /api/send_group_music` - 发送群聊音乐卡片
- `POST /api/send_private_music` - 发送私聊音乐卡片

### 3. API 使用方式

#### 平台音乐 (qq/163/kugou/migu/kuwo)

```json
POST /api/send_group_music
{
"group_id": 123456789,
"music_type": "163",
"id": "1999253939"
}
```

#### 自定义音乐卡片

```json
POST /api/send_group_music
{
"group_id": 123456789,
"music_type": "custom",
"url": "https://music.163.com/#/song?id=1999253939",
"audio": "https://example.com/audio.mp3",
"title": "歌曲标题",
"singer": "歌手名称",
"image": "https://example.com/cover.jpg"
}
```

### 4. 为什么不使用 Milky 的 OutgoingSegment?

Milky 协议的 `OutgoingSegment` 是 sealed class,无法在外部扩展添加新的消息类型。因此选择了添加独立的 HTTP API 端点来实现音乐卡片功能,而不是作为消息段类型。

## 测试结果

| 音乐类型 | 状态 | 备注 |
|---------|------|------|
| 网易云 (163) | ✅ 正常 | |
| 自定义 (custom) | ✅ 正常 | |
| QQ音乐 (qq) | ⚠️ 部分可用 | 签名服务可能返回非 Ark 格式 |
| 酷狗 (kugou) | ⚠️ 依赖签名服务 | |

## 配置

可在 `config.json` 中配置签名服务地址(可选):

```json
{
"musicSignUrl": "https://ss.xingzhige.com/music_card/card"
}
```

如不配置,默认使用上述地址。

## 相关参考

- [NapCatQQ 音乐卡片实现](https://github.com/NapNeko/NapCatQQ)
- 签名服务: `https://ss.xingzhige.com/music_card/card`
Loading