mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-12 16:00:27 +00:00
Compare commits
123 Commits
feat/suppo
...
v4.14.13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5769b6a62 | ||
|
|
d9297c1e10 | ||
|
|
94f07ab98b | ||
|
|
01a6594707 | ||
|
|
82a7154b92 | ||
|
|
9b385ac9c9 | ||
|
|
e3d4cee416 | ||
|
|
f6c79370cb | ||
|
|
39460e4acb | ||
|
|
f971c312b9 | ||
|
|
2c3a304440 | ||
|
|
286b0e03f7 | ||
|
|
447f86e2b5 | ||
|
|
0592f1a99a | ||
|
|
90e3936204 | ||
|
|
1239f622d2 | ||
|
|
d511e2bb3f | ||
|
|
ff93aa3dc7 | ||
|
|
cc8891b6a1 | ||
|
|
7c65b1eaf1 | ||
|
|
ebe3e9c63c | ||
|
|
d33a872c42 | ||
|
|
9377dc3d52 | ||
|
|
17322bb5a4 | ||
|
|
c0bcced5fb | ||
|
|
805c1d5ea2 | ||
|
|
b3399b07ad | ||
|
|
71f8504849 | ||
|
|
3b7ca1a08f | ||
|
|
57f3c4dd31 | ||
|
|
5b20ebb7b0 | ||
|
|
3a3eaeec7c | ||
|
|
b0cc7b6ee5 | ||
|
|
e5108c0427 | ||
|
|
927797f3d5 | ||
|
|
72e01f8c84 | ||
|
|
c38b98a0c4 | ||
|
|
05d27e86ce | ||
|
|
40409a3841 | ||
|
|
65bae6b57a | ||
|
|
0b6afb66d9 | ||
|
|
52be000fdd | ||
|
|
55ce5bcfd3 | ||
|
|
29888cb38b | ||
|
|
6ea4c9ec65 | ||
|
|
4bec3aa597 | ||
|
|
38c320d2c9 | ||
|
|
76cbd8a1c1 | ||
|
|
0779628be5 | ||
|
|
34ca919c4d | ||
|
|
b1b357347b | ||
|
|
129d63f66e | ||
|
|
699b46acbd | ||
|
|
7f05aee11d | ||
|
|
542036f46e | ||
|
|
b958e9e803 | ||
|
|
73fcfb5900 | ||
|
|
adabc4da46 | ||
|
|
bf073b544b | ||
|
|
a71219062a | ||
|
|
001fe01ace | ||
|
|
0aa0c44634 | ||
|
|
93126e514e | ||
|
|
1ae10ae0c6 | ||
|
|
4b693bf6e2 | ||
|
|
574c257591 | ||
|
|
d680328762 | ||
|
|
d711cdecaf | ||
|
|
c5f1792009 | ||
|
|
a5e705e6a4 | ||
|
|
007f1db339 | ||
|
|
008fb39f8f | ||
|
|
6b8cc6756d | ||
|
|
24623f18d8 | ||
|
|
8b676ed693 | ||
|
|
ab2dfcfd8f | ||
|
|
2998e04435 | ||
|
|
613690f5af | ||
|
|
a60d8d109c | ||
|
|
28c9761e3d | ||
|
|
805cc32d7f | ||
|
|
de9d5180fe | ||
|
|
791e359199 | ||
|
|
63a9d571f3 | ||
|
|
59d4b08982 | ||
|
|
81e4e54f25 | ||
|
|
b75b733bb0 | ||
|
|
246269b519 | ||
|
|
3c24d6b700 | ||
|
|
679c980683 | ||
|
|
19766002ae | ||
|
|
c2d3a8034d | ||
|
|
58220d3fbc | ||
|
|
2daddbb030 | ||
|
|
6ec5bbeddf | ||
|
|
75236dd50c | ||
|
|
01958d47a4 | ||
|
|
772f07c58b | ||
|
|
0f9647bf64 | ||
|
|
8197ebcbcf | ||
|
|
d0519feb4f | ||
|
|
d43c6b10a3 | ||
|
|
857be5ee49 | ||
|
|
af8005dd6f | ||
|
|
6e8adad7ca | ||
|
|
0f8584b8e1 | ||
|
|
37f40a2635 | ||
|
|
1b4d604e32 | ||
|
|
81a0c07922 | ||
|
|
a8cb6b5865 | ||
|
|
d25bd65b2d | ||
|
|
e510a75f0c | ||
|
|
e3c6048a7f | ||
|
|
789c72d4cf | ||
|
|
711a060dd9 | ||
|
|
6268923f01 | ||
|
|
f6b9017429 | ||
|
|
178e51bbb8 | ||
|
|
8a232d8c68 | ||
|
|
7216755430 | ||
|
|
0c91f9c66b | ||
|
|
e8855a59b0 | ||
|
|
5de2664af4 |
2
.github/prompt/default.md
vendored
2
.github/prompt/default.md
vendored
@@ -2,7 +2,7 @@
|
|||||||
[使用文档](https://napneko.github.io/)
|
[使用文档](https://napneko.github.io/)
|
||||||
|
|
||||||
## Windows 一键包
|
## Windows 一键包
|
||||||
我们为提供了的轻量化一键部署方案
|
我们提供了轻量化的一键部署方案
|
||||||
相对于普通需要安装QQ的方案,下面已内置QQ和Napcat 阅读使用文档参考
|
相对于普通需要安装QQ的方案,下面已内置QQ和Napcat 阅读使用文档参考
|
||||||
|
|
||||||
你可以下载
|
你可以下载
|
||||||
|
|||||||
2
.github/prompt/release_note_prompt.txt
vendored
2
.github/prompt/release_note_prompt.txt
vendored
@@ -31,7 +31,7 @@
|
|||||||
[使用文档](https://napneko.github.io/)
|
[使用文档](https://napneko.github.io/)
|
||||||
|
|
||||||
## Windows 一键包
|
## Windows 一键包
|
||||||
我们为提供了的轻量化一键部署方案
|
我们提供了轻量化的一键部署方案
|
||||||
相对于普通需要安装QQ的方案,下面已内置QQ和Napcat 阅读使用文档参考
|
相对于普通需要安装QQ的方案,下面已内置QQ和Napcat 阅读使用文档参考
|
||||||
|
|
||||||
你可以下载
|
你可以下载
|
||||||
|
|||||||
70
.github/workflows/auto-release.yml
vendored
70
.github/workflows/auto-release.yml
vendored
@@ -5,6 +5,63 @@ on:
|
|||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
publish-schema:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
|
- name: Get Version
|
||||||
|
id: get_version
|
||||||
|
run: |
|
||||||
|
latest_tag=$(git describe --tags $(git rev-list --tags --max-count=1))
|
||||||
|
version=${latest_tag#v}
|
||||||
|
echo "version=${version}" >> $GITHUB_ENV
|
||||||
|
echo "latest_tag=${latest_tag}" >> $GITHUB_ENV
|
||||||
|
echo "Debug: Version is ${version}"
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build napcat-schema
|
||||||
|
run: |
|
||||||
|
cd packages/napcat-schema
|
||||||
|
pnpm run build:openapi
|
||||||
|
|
||||||
|
- name: Checkout NapCatDocs
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: NapNeko/NapCatDocs
|
||||||
|
token: ${{ secrets.NAPCAT_BUILD }}
|
||||||
|
path: napcat-docs
|
||||||
|
|
||||||
|
- name: Copy OpenAPI Schema
|
||||||
|
run: |
|
||||||
|
mkdir -p napcat-docs/src/api/${{ env.version }}
|
||||||
|
cp packages/napcat-schema/dist/openapi.json napcat-docs/src/api/${{ env.version }}/openapi.json
|
||||||
|
echo "OpenAPI schema copied to napcat-docs/src/api/${{ env.version }}/openapi.json"
|
||||||
|
|
||||||
|
- name: Commit and Push
|
||||||
|
run: |
|
||||||
|
cd napcat-docs
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add src/api/${{ env.version }}/openapi.json
|
||||||
|
git commit -m "chore: update OpenAPI schema for version ${{ env.version }}" || echo "No changes to commit"
|
||||||
|
git push
|
||||||
|
|
||||||
shell-docker:
|
shell-docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -80,4 +137,17 @@ jobs:
|
|||||||
-H "Accept: application/vnd.github+json" \
|
-H "Accept: application/vnd.github+json" \
|
||||||
-H "Authorization: Bearer $GH_TOKEN" \
|
-H "Authorization: Bearer $GH_TOKEN" \
|
||||||
https://api.github.com/repos/NapNeko/NapCatLinuxNodeLoader/actions/workflows/release.yml/dispatches \
|
https://api.github.com/repos/NapNeko/NapCatLinuxNodeLoader/actions/workflows/release.yml/dispatches \
|
||||||
|
-d "{\"ref\":\"main\",\"inputs\":{\"napcat_version\":\"${NAPCAT_VERSION}\",\"qq_url_amd64\":\"${QQ_VERSION_X86_64}\",\"qq_url_arm64\":\"${QQ_VERSION_ARM64}\"}}"
|
||||||
|
- name: Trigger Release NapCat AppImage Workflow
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.NAPCAT_BUILD }}
|
||||||
|
NAPCAT_VERSION: ${{ env.latest_tag }}
|
||||||
|
QQ_VERSION_X86_64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_x86_64.AppImage' # 写死 QQ 版本
|
||||||
|
QQ_VERSION_ARM64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_arm64.AppImage' # 写死 QQ 版本
|
||||||
|
run: |
|
||||||
|
echo "Debug: Triggering Release NapCat AppImage with napcat_version=${NAPCAT_VERSION}, qq_url_amd64=${QQ_VERSION_X86_64}, qq_url_arm64=${QQ_VERSION_ARM64}"
|
||||||
|
curl -X POST \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
-H "Authorization: Bearer $GH_TOKEN" \
|
||||||
|
https://api.github.com/repos/NapNeko/NapCatLinuxNodeLoader/actions/workflows/docker-publish.yml/dispatches \
|
||||||
-d "{\"ref\":\"main\",\"inputs\":{\"napcat_version\":\"${NAPCAT_VERSION}\",\"qq_url_amd64\":\"${QQ_VERSION_X86_64}\",\"qq_url_arm64\":\"${QQ_VERSION_ARM64}\"}}"
|
-d "{\"ref\":\"main\",\"inputs\":{\"napcat_version\":\"${NAPCAT_VERSION}\",\"qq_url_amd64\":\"${QQ_VERSION_X86_64}\",\"qq_url_arm64\":\"${QQ_VERSION_ARM64}\"}}"
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -16,4 +16,5 @@ checkVersion.sh
|
|||||||
bun.lockb
|
bun.lockb
|
||||||
tests/run/
|
tests/run/
|
||||||
guild1.db-wal
|
guild1.db-wal
|
||||||
guild1.db-shm
|
guild1.db-shm
|
||||||
|
packages/napcat-develop/config/.env
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ _Modern protocol-side framework implemented based on NTQQ._
|
|||||||
| Docs | [](https://napneko.github.io/) | [](https://doc.napneko.icu/) | [](https://napcat.napneko.icu/) |
|
| Docs | [](https://napneko.github.io/) | [](https://doc.napneko.icu/) | [](https://napcat.napneko.icu/) |
|
||||||
|:-:|:-:|:-:|:-:|
|
|:-:|:-:|:-:|:-:|
|
||||||
|
|
||||||
| Docs | [](https://napneko.pages.dev/) | [](https://napcat.cyou/) | [](https://www.napcat.wiki) |
|
| Docs | [](https://napneko.pages.dev/) | [](https://napcat.top/) | [](https://napcat.top/) |
|
||||||
|:-:|:-:|:-:|:-:|
|
|:-:|:-:|:-:|:-:|
|
||||||
|
|
||||||
| QQ Group | [](https://qm.qq.com/q/CMmPbGw0jA) | [](https://qm.qq.com/q/8zJMLjqy2Y) | [](https://qm.qq.com/q/CMmPbGw0jA) | [](https://qm.qq.com/q/I6LU87a0Yq) |
|
| QQ Group | [](https://qm.qq.com/q/CMmPbGw0jA) | [](https://qm.qq.com/q/8zJMLjqy2Y) | [](https://qm.qq.com/q/CMmPbGw0jA) | [](https://qm.qq.com/q/I6LU87a0Yq) |
|
||||||
|
|||||||
@@ -6,8 +6,10 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build:shell": "pnpm --filter napcat-shell run build || exit 1",
|
"build:shell": "pnpm --filter napcat-shell run build || exit 1",
|
||||||
"build:shell:dev": "pnpm --filter napcat-shell run build:dev || exit 1",
|
"build:shell:dev": "pnpm --filter napcat-shell run build:dev || exit 1",
|
||||||
|
"build:shell:config": "pnpm --filter napcat-shell run build && pnpm --filter napcat-develop run copy-env",
|
||||||
"build:framework": "pnpm --filter napcat-framework run build || exit 1",
|
"build:framework": "pnpm --filter napcat-framework run build || exit 1",
|
||||||
"build:webui": "pnpm --filter napcat-webui-frontend run build || exit 1",
|
"build:webui": "pnpm --filter napcat-webui-frontend run build || exit 1",
|
||||||
|
"build:plugin-builtin": "pnpm --filter napcat-plugin-builtin run build || exit 1",
|
||||||
"dev:shell": "pnpm --filter napcat-develop run dev || exit 1",
|
"dev:shell": "pnpm --filter napcat-develop run dev || exit 1",
|
||||||
"typecheck": "pnpm -r --if-present run typecheck",
|
"typecheck": "pnpm -r --if-present run typecheck",
|
||||||
"test": "pnpm --filter napcat-test run test",
|
"test": "pnpm --filter napcat-test run test",
|
||||||
|
|||||||
176
packages/napcat-adapter/index.ts
Normal file
176
packages/napcat-adapter/index.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import { InstanceContext, NapCatCore } from 'napcat-core';
|
||||||
|
import { NapCatPathWrapper } from 'napcat-common/src/path';
|
||||||
|
import { NapCatOneBot11Adapter } from 'napcat-onebot';
|
||||||
|
import { NapCatProtocolAdapter } from 'napcat-protocol';
|
||||||
|
|
||||||
|
// 协议适配器类型
|
||||||
|
export type ProtocolAdapterType = 'onebot11' | 'napcat-protocol';
|
||||||
|
|
||||||
|
// 协议适配器接口
|
||||||
|
export interface IProtocolAdapter {
|
||||||
|
readonly name: string;
|
||||||
|
readonly enabled: boolean;
|
||||||
|
init (): Promise<void>;
|
||||||
|
close (): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 协议适配器包装器
|
||||||
|
class OneBotAdapterWrapper implements IProtocolAdapter {
|
||||||
|
readonly name = 'onebot11';
|
||||||
|
private adapter: NapCatOneBot11Adapter;
|
||||||
|
|
||||||
|
constructor (adapter: NapCatOneBot11Adapter) {
|
||||||
|
this.adapter = adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
get enabled (): boolean {
|
||||||
|
return true; // OneBot11 默认启用
|
||||||
|
}
|
||||||
|
|
||||||
|
async init (): Promise<void> {
|
||||||
|
await this.adapter.InitOneBot();
|
||||||
|
}
|
||||||
|
|
||||||
|
async close (): Promise<void> {
|
||||||
|
await this.adapter.networkManager.closeAllAdapters();
|
||||||
|
}
|
||||||
|
|
||||||
|
getAdapter (): NapCatOneBot11Adapter {
|
||||||
|
return this.adapter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NapCat Protocol 适配器包装器
|
||||||
|
class NapCatProtocolAdapterWrapper implements IProtocolAdapter {
|
||||||
|
readonly name = 'napcat-protocol';
|
||||||
|
private adapter: NapCatProtocolAdapter;
|
||||||
|
|
||||||
|
constructor (adapter: NapCatProtocolAdapter) {
|
||||||
|
this.adapter = adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
get enabled (): boolean {
|
||||||
|
return this.adapter.isEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init (): Promise<void> {
|
||||||
|
await this.adapter.initProtocol();
|
||||||
|
}
|
||||||
|
|
||||||
|
async close (): Promise<void> {
|
||||||
|
await this.adapter.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
getAdapter (): NapCatProtocolAdapter {
|
||||||
|
return this.adapter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 协议适配器管理器
|
||||||
|
export class NapCatAdapterManager {
|
||||||
|
private core: NapCatCore;
|
||||||
|
private context: InstanceContext;
|
||||||
|
private pathWrapper: NapCatPathWrapper;
|
||||||
|
|
||||||
|
// 协议适配器实例
|
||||||
|
private onebotAdapter: OneBotAdapterWrapper | null = null;
|
||||||
|
private napcatProtocolAdapter: NapCatProtocolAdapterWrapper | null = null;
|
||||||
|
|
||||||
|
// 所有已注册的适配器
|
||||||
|
private adapters: Map<string, IProtocolAdapter> = new Map();
|
||||||
|
|
||||||
|
constructor (core: NapCatCore, context: InstanceContext, pathWrapper: NapCatPathWrapper) {
|
||||||
|
this.core = core;
|
||||||
|
this.context = context;
|
||||||
|
this.pathWrapper = pathWrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化所有协议适配器
|
||||||
|
async initAdapters (): Promise<void> {
|
||||||
|
this.context.logger.log('[AdapterManager] 开始初始化协议适配器...');
|
||||||
|
|
||||||
|
// 初始化 OneBot11 适配器 (默认启用)
|
||||||
|
try {
|
||||||
|
const onebot = new NapCatOneBot11Adapter(this.core, this.context, this.pathWrapper);
|
||||||
|
this.onebotAdapter = new OneBotAdapterWrapper(onebot);
|
||||||
|
this.adapters.set('onebot11', this.onebotAdapter);
|
||||||
|
await this.onebotAdapter.init();
|
||||||
|
this.context.logger.log('[AdapterManager] OneBot11 适配器初始化完成');
|
||||||
|
} catch (e) {
|
||||||
|
this.context.logger.logError('[AdapterManager] OneBot11 适配器初始化失败:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化 NapCat Protocol 适配器 (默认关闭,需要配置启用)
|
||||||
|
try {
|
||||||
|
const napcatProtocol = new NapCatProtocolAdapter(this.core, this.context, this.pathWrapper);
|
||||||
|
this.napcatProtocolAdapter = new NapCatProtocolAdapterWrapper(napcatProtocol);
|
||||||
|
this.adapters.set('napcat-protocol', this.napcatProtocolAdapter);
|
||||||
|
|
||||||
|
if (this.napcatProtocolAdapter.enabled) {
|
||||||
|
await this.napcatProtocolAdapter.init();
|
||||||
|
this.context.logger.log('[AdapterManager] NapCat Protocol 适配器初始化完成');
|
||||||
|
} else {
|
||||||
|
this.context.logger.log('[AdapterManager] NapCat Protocol 适配器未启用,跳过初始化');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.context.logger.logError('[AdapterManager] NapCat Protocol 适配器初始化失败:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context.logger.log(`[AdapterManager] 协议适配器初始化完成,已加载 ${this.adapters.size} 个适配器`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 OneBot11 适配器
|
||||||
|
getOneBotAdapter (): NapCatOneBot11Adapter | null {
|
||||||
|
return this.onebotAdapter?.getAdapter() ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 NapCat Protocol 适配器
|
||||||
|
getNapCatProtocolAdapter (): NapCatProtocolAdapter | null {
|
||||||
|
return this.napcatProtocolAdapter?.getAdapter() ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取指定适配器
|
||||||
|
getAdapter (name: ProtocolAdapterType): IProtocolAdapter | undefined {
|
||||||
|
return this.adapters.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有已启用的适配器
|
||||||
|
getEnabledAdapters (): IProtocolAdapter[] {
|
||||||
|
return Array.from(this.adapters.values()).filter(adapter => adapter.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有适配器
|
||||||
|
getAllAdapters (): IProtocolAdapter[] {
|
||||||
|
return Array.from(this.adapters.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭所有适配器
|
||||||
|
async closeAllAdapters (): Promise<void> {
|
||||||
|
this.context.logger.log('[AdapterManager] 开始关闭所有协议适配器...');
|
||||||
|
|
||||||
|
for (const [name, adapter] of this.adapters) {
|
||||||
|
try {
|
||||||
|
await adapter.close();
|
||||||
|
this.context.logger.log(`[AdapterManager] ${name} 适配器已关闭`);
|
||||||
|
} catch (e) {
|
||||||
|
this.context.logger.logError(`[AdapterManager] 关闭 ${name} 适配器失败:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.adapters.clear();
|
||||||
|
this.context.logger.log('[AdapterManager] 所有协议适配器已关闭');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新加载指定适配器
|
||||||
|
async reloadAdapter (name: ProtocolAdapterType): Promise<void> {
|
||||||
|
const adapter = this.adapters.get(name);
|
||||||
|
if (adapter) {
|
||||||
|
await adapter.close();
|
||||||
|
await adapter.init();
|
||||||
|
this.context.logger.log(`[AdapterManager] ${name} 适配器已重新加载`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { NapCatOneBot11Adapter } from 'napcat-onebot';
|
||||||
|
export { NapCatProtocolAdapter } from 'napcat-protocol';
|
||||||
30
packages/napcat-adapter/package.json
Normal file
30
packages/napcat-adapter/package.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "napcat-adapter",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./index.ts"
|
||||||
|
},
|
||||||
|
"./*": {
|
||||||
|
"import": "./*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"napcat-core": "workspace:*",
|
||||||
|
"napcat-common": "workspace:*",
|
||||||
|
"napcat-onebot": "workspace:*",
|
||||||
|
"napcat-protocol": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,28 @@
|
|||||||
{
|
{
|
||||||
"name": "napcat-common",
|
"name": "napcat-common",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
|
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./src/index.ts"
|
||||||
},
|
},
|
||||||
"exports": {
|
"./src/*": {
|
||||||
".": {
|
"import": "./src/*"
|
||||||
"import": "./src/index.ts"
|
|
||||||
},
|
|
||||||
"./src/*": {
|
|
||||||
"import": "./src/*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"ajv": "^8.13.0",
|
|
||||||
"file-type": "^21.0.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^22.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": "^8.13.0",
|
||||||
|
"file-type": "^21.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,11 +2,14 @@ import fs from 'fs';
|
|||||||
import { stat } from 'fs/promises';
|
import { stat } from 'fs/promises';
|
||||||
import crypto, { randomUUID } from 'crypto';
|
import crypto, { randomUUID } from 'crypto';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
import http from 'node:http';
|
||||||
|
import tls from 'node:tls';
|
||||||
import { solveProblem } from '@/napcat-common/src/helper';
|
import { solveProblem } from '@/napcat-common/src/helper';
|
||||||
|
|
||||||
export interface HttpDownloadOptions {
|
export interface HttpDownloadOptions {
|
||||||
url: string;
|
url: string;
|
||||||
headers?: Record<string, string> | string;
|
headers?: Record<string, string> | string;
|
||||||
|
proxy?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Uri2LocalRes = {
|
type Uri2LocalRes = {
|
||||||
@@ -96,6 +99,7 @@ export function calculateFileMD5 (filePath: string): Promise<string> {
|
|||||||
|
|
||||||
async function tryDownload (options: string | HttpDownloadOptions, useReferer: boolean = false): Promise<Response> {
|
async function tryDownload (options: string | HttpDownloadOptions, useReferer: boolean = false): Promise<Response> {
|
||||||
let url: string;
|
let url: string;
|
||||||
|
let proxy: string | undefined;
|
||||||
let headers: Record<string, string> = {
|
let headers: Record<string, string> = {
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36',
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36',
|
||||||
};
|
};
|
||||||
@@ -104,6 +108,7 @@ async function tryDownload (options: string | HttpDownloadOptions, useReferer: b
|
|||||||
headers['Host'] = new URL(url).hostname;
|
headers['Host'] = new URL(url).hostname;
|
||||||
} else {
|
} else {
|
||||||
url = options.url;
|
url = options.url;
|
||||||
|
proxy = options.proxy;
|
||||||
if (options.headers) {
|
if (options.headers) {
|
||||||
if (typeof options.headers === 'string') {
|
if (typeof options.headers === 'string') {
|
||||||
headers = JSON.parse(options.headers);
|
headers = JSON.parse(options.headers);
|
||||||
@@ -115,6 +120,18 @@ async function tryDownload (options: string | HttpDownloadOptions, useReferer: b
|
|||||||
if (useReferer && !headers['Referer']) {
|
if (useReferer && !headers['Referer']) {
|
||||||
headers['Referer'] = url;
|
headers['Referer'] = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果配置了代理,使用代理下载
|
||||||
|
if (proxy) {
|
||||||
|
try {
|
||||||
|
const response = await httpDownloadWithProxy(url, headers, proxy);
|
||||||
|
return new Response(response, { status: 200, statusText: 'OK' });
|
||||||
|
} catch (proxyError) {
|
||||||
|
// 如果代理失败,记录错误并尝试直接下载
|
||||||
|
console.error('代理下载失败,尝试直接下载:', proxyError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fetchRes = await fetch(url, { headers, redirect: 'follow' }).catch((err) => {
|
const fetchRes = await fetch(url, { headers, redirect: 'follow' }).catch((err) => {
|
||||||
if (err.cause) {
|
if (err.cause) {
|
||||||
throw err.cause;
|
throw err.cause;
|
||||||
@@ -124,6 +141,220 @@ async function tryDownload (options: string | HttpDownloadOptions, useReferer: b
|
|||||||
return fetchRes;
|
return fetchRes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 HTTP/HTTPS 代理下载文件
|
||||||
|
*/
|
||||||
|
function httpDownloadWithProxy (url: string, headers: Record<string, string>, proxy: string): Promise<Buffer> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const targetUrl = new URL(url);
|
||||||
|
const proxyUrl = new URL(proxy);
|
||||||
|
|
||||||
|
const isTargetHttps = targetUrl.protocol === 'https:';
|
||||||
|
const proxyPort = parseInt(proxyUrl.port) || (proxyUrl.protocol === 'https:' ? 443 : 80);
|
||||||
|
|
||||||
|
// 代理认证头
|
||||||
|
const proxyAuthHeader = proxyUrl.username && proxyUrl.password
|
||||||
|
? { 'Proxy-Authorization': 'Basic ' + Buffer.from(`${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`).toString('base64') }
|
||||||
|
: {};
|
||||||
|
|
||||||
|
if (isTargetHttps) {
|
||||||
|
// HTTPS 目标:需要通过 CONNECT 建立隧道
|
||||||
|
const connectReq = http.request({
|
||||||
|
host: proxyUrl.hostname,
|
||||||
|
port: proxyPort,
|
||||||
|
method: 'CONNECT',
|
||||||
|
path: `${targetUrl.hostname}:${targetUrl.port || 443}`,
|
||||||
|
headers: {
|
||||||
|
'Host': `${targetUrl.hostname}:${targetUrl.port || 443}`,
|
||||||
|
...proxyAuthHeader,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
connectReq.on('connect', (res, socket) => {
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
socket.destroy();
|
||||||
|
reject(new Error(`代理 CONNECT 失败: ${res.statusCode} ${res.statusMessage}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在隧道上建立 TLS 连接
|
||||||
|
const tlsSocket = tls.connect({
|
||||||
|
socket: socket,
|
||||||
|
servername: targetUrl.hostname,
|
||||||
|
rejectUnauthorized: true,
|
||||||
|
}, () => {
|
||||||
|
// TLS 握手成功,发送 HTTP 请求
|
||||||
|
const requestPath = targetUrl.pathname + targetUrl.search;
|
||||||
|
const requestHeaders = {
|
||||||
|
...headers,
|
||||||
|
'Host': targetUrl.hostname,
|
||||||
|
'Connection': 'close',
|
||||||
|
};
|
||||||
|
|
||||||
|
const headerLines = Object.entries(requestHeaders)
|
||||||
|
.map(([key, value]) => `${key}: ${value}`)
|
||||||
|
.join('\r\n');
|
||||||
|
|
||||||
|
const httpRequest = `GET ${requestPath} HTTP/1.1\r\n${headerLines}\r\n\r\n`;
|
||||||
|
tlsSocket.write(httpRequest);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 解析 HTTP 响应
|
||||||
|
let responseData = Buffer.alloc(0);
|
||||||
|
let headersParsed = false;
|
||||||
|
let statusCode = 0;
|
||||||
|
let isChunked = false;
|
||||||
|
let bodyData = Buffer.alloc(0);
|
||||||
|
let redirectLocation: string | null = null;
|
||||||
|
|
||||||
|
tlsSocket.on('data', (chunk: Buffer) => {
|
||||||
|
responseData = Buffer.concat([responseData, chunk]);
|
||||||
|
|
||||||
|
if (!headersParsed) {
|
||||||
|
const headerEndIndex = responseData.indexOf('\r\n\r\n');
|
||||||
|
if (headerEndIndex !== -1) {
|
||||||
|
headersParsed = true;
|
||||||
|
const headerStr = responseData.subarray(0, headerEndIndex).toString();
|
||||||
|
const headerLines = headerStr.split('\r\n');
|
||||||
|
|
||||||
|
// 解析状态码
|
||||||
|
const statusLine = headerLines[0];
|
||||||
|
const statusMatch = statusLine?.match(/HTTP\/\d\.\d\s+(\d+)/);
|
||||||
|
statusCode = statusMatch ? parseInt(statusMatch[1]!) : 0;
|
||||||
|
|
||||||
|
// 解析响应头
|
||||||
|
for (const line of headerLines.slice(1)) {
|
||||||
|
const [key, ...valueParts] = line.split(':');
|
||||||
|
const value = valueParts.join(':').trim();
|
||||||
|
if (key?.toLowerCase() === 'transfer-encoding' && value.toLowerCase() === 'chunked') {
|
||||||
|
isChunked = true;
|
||||||
|
} else if (key?.toLowerCase() === 'location') {
|
||||||
|
redirectLocation = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyData = responseData.subarray(headerEndIndex + 4);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bodyData = Buffer.concat([bodyData, chunk]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tlsSocket.on('end', () => {
|
||||||
|
// 处理重定向
|
||||||
|
if (statusCode >= 300 && statusCode < 400 && redirectLocation) {
|
||||||
|
const redirectUrl = redirectLocation.startsWith('http')
|
||||||
|
? redirectLocation
|
||||||
|
: `${targetUrl.protocol}//${targetUrl.host}${redirectLocation}`;
|
||||||
|
httpDownloadWithProxy(redirectUrl, headers, proxy).then(resolve).catch(reject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusCode !== 200) {
|
||||||
|
reject(new Error(`下载失败: ${statusCode}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 chunked 编码
|
||||||
|
if (isChunked) {
|
||||||
|
resolve(parseChunkedBody(bodyData));
|
||||||
|
} else {
|
||||||
|
resolve(bodyData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tlsSocket.on('error', (err) => {
|
||||||
|
reject(new Error(`TLS 连接错误: ${err.message}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
connectReq.on('error', (err) => {
|
||||||
|
reject(new Error(`代理连接错误: ${err.message}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
connectReq.end();
|
||||||
|
} else {
|
||||||
|
// HTTP 目标:直接通过代理请求
|
||||||
|
const req = http.request({
|
||||||
|
host: proxyUrl.hostname,
|
||||||
|
port: proxyPort,
|
||||||
|
method: 'GET',
|
||||||
|
path: url, // 完整 URL
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
'Host': targetUrl.hostname,
|
||||||
|
...proxyAuthHeader,
|
||||||
|
},
|
||||||
|
}, (response) => {
|
||||||
|
handleResponse(response, resolve, reject, url, headers, proxy);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (err) => {
|
||||||
|
reject(new Error(`代理请求错误: ${err.message}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 chunked 编码的响应体
|
||||||
|
*/
|
||||||
|
function parseChunkedBody (data: Buffer): Buffer {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
while (offset < data.length) {
|
||||||
|
// 查找 chunk 大小行的结束
|
||||||
|
const lineEnd = data.indexOf('\r\n', offset);
|
||||||
|
if (lineEnd === -1) break;
|
||||||
|
|
||||||
|
const sizeStr = data.subarray(offset, lineEnd).toString().split(';')[0]; // 忽略 chunk 扩展
|
||||||
|
const chunkSize = parseInt(sizeStr!, 16);
|
||||||
|
|
||||||
|
if (chunkSize === 0) break; // 最后一个 chunk
|
||||||
|
|
||||||
|
const chunkStart = lineEnd + 2;
|
||||||
|
const chunkEnd = chunkStart + chunkSize;
|
||||||
|
|
||||||
|
if (chunkEnd > data.length) break;
|
||||||
|
|
||||||
|
chunks.push(data.subarray(chunkStart, chunkEnd));
|
||||||
|
offset = chunkEnd + 2; // 跳过 chunk 数据后的 \r\n
|
||||||
|
}
|
||||||
|
|
||||||
|
return Buffer.concat(chunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 HTTP 响应
|
||||||
|
*/
|
||||||
|
function handleResponse (
|
||||||
|
response: http.IncomingMessage,
|
||||||
|
resolve: (value: Buffer) => void,
|
||||||
|
reject: (reason: Error) => void,
|
||||||
|
_url: string,
|
||||||
|
headers: Record<string, string>,
|
||||||
|
proxy: string
|
||||||
|
): void {
|
||||||
|
// 处理重定向
|
||||||
|
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
||||||
|
httpDownloadWithProxy(response.headers.location, headers, proxy).then(resolve).catch(reject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
reject(new Error(`下载失败: ${response.statusCode} ${response.statusMessage}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
response.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||||
|
response.on('end', () => resolve(Buffer.concat(chunks)));
|
||||||
|
response.on('error', reject);
|
||||||
|
}
|
||||||
|
|
||||||
export async function httpDownload (options: string | HttpDownloadOptions): Promise<Buffer> {
|
export async function httpDownload (options: string | HttpDownloadOptions): Promise<Buffer> {
|
||||||
const useReferer = typeof options === 'string';
|
const useReferer = typeof options === 'string';
|
||||||
let resp = await tryDownload(options);
|
let resp = await tryDownload(options);
|
||||||
@@ -176,7 +407,7 @@ export async function checkUriType (Uri: string) {
|
|||||||
return { Uri, Type: FileUriType.Unknown };
|
return { Uri, Type: FileUriType.Unknown };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uriToLocalFile (dir: string, uri: string, filename: string = randomUUID(), headers?: Record<string, string>): Promise<Uri2LocalRes> {
|
export async function uriToLocalFile (dir: string, uri: string, filename: string = randomUUID(), headers?: Record<string, string>, proxy?: string): Promise<Uri2LocalRes> {
|
||||||
const { Uri: HandledUri, Type: UriType } = await checkUriType(uri);
|
const { Uri: HandledUri, Type: UriType } = await checkUriType(uri);
|
||||||
|
|
||||||
const filePath = path.join(dir, filename);
|
const filePath = path.join(dir, filename);
|
||||||
@@ -191,7 +422,7 @@ export async function uriToLocalFile (dir: string, uri: string, filename: string
|
|||||||
}
|
}
|
||||||
|
|
||||||
case FileUriType.Remote: {
|
case FileUriType.Remote: {
|
||||||
const buffer = await httpDownload({ url: HandledUri, headers: headers ?? {} });
|
const buffer = await httpDownload({ url: HandledUri, headers: headers ?? {}, proxy });
|
||||||
fs.writeFileSync(filePath, buffer);
|
fs.writeFileSync(filePath, buffer);
|
||||||
return { success: true, errMsg: '', fileName: filename, path: filePath };
|
return { success: true, errMsg: '', fileName: filename, path: filePath };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,13 +220,13 @@ export function parseAppidFromMajor (nodeMajor: string): string | undefined {
|
|||||||
// ============== GitHub Tags 获取 ==============
|
// ============== GitHub Tags 获取 ==============
|
||||||
// 使用 mirror 模块统一管理镜像
|
// 使用 mirror 模块统一管理镜像
|
||||||
|
|
||||||
export async function getAllTags (): Promise<{ tags: string[], mirror: string; }> {
|
export async function getAllTags (mirror?: string): Promise<{ tags: string[], mirror: string; }> {
|
||||||
return getAllTagsFromMirror('NapNeko', 'NapCatQQ');
|
return getAllTagsFromMirror('NapNeko', 'NapCatQQ', mirror);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function getLatestTag (): Promise<string> {
|
export async function getLatestTag (mirror?: string): Promise<string> {
|
||||||
const { tags } = await getAllTags();
|
const { tags } = await getAllTags(mirror);
|
||||||
|
|
||||||
// 使用 SemVer 规范排序
|
// 使用 SemVer 规范排序
|
||||||
tags.sort((a, b) => compareSemVer(a, b));
|
tags.sort((a, b) => compareSemVer(a, b));
|
||||||
|
|||||||
@@ -58,12 +58,12 @@ export class LimitedHashTable<K, V> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取最近刚写入的几个值
|
// 获取最近刚写入的几个值
|
||||||
getHeads (size: number): { key: K; value: V }[] | undefined {
|
getHeads (size: number): { key: K; value: V; }[] | undefined {
|
||||||
const keyList = this.getKeyList();
|
const keyList = this.getKeyList();
|
||||||
if (keyList.length === 0) {
|
if (keyList.length === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const result: { key: K; value: V }[] = [];
|
const result: { key: K; value: V; }[] = [];
|
||||||
const listSize = Math.min(size, keyList.length);
|
const listSize = Math.min(size, keyList.length);
|
||||||
for (let i = 0; i < listSize; i++) {
|
for (let i = 0; i < listSize; i++) {
|
||||||
const key = keyList[listSize - i];
|
const key = keyList[listSize - i];
|
||||||
@@ -108,7 +108,7 @@ class MessageUniqueWrapper {
|
|||||||
return shortId;
|
return shortId;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMsgIdAndPeerByShortId (shortId: number): { MsgId: string; Peer: Peer } | undefined {
|
getMsgIdAndPeerByShortId (shortId: number): { MsgId: string; Peer: Peer; } | undefined {
|
||||||
const data = this.msgDataMap.getKey(shortId);
|
const data = this.msgDataMap.getKey(shortId);
|
||||||
if (data) {
|
if (data) {
|
||||||
const [msgId, chatTypeStr, peerUid] = data.split('|');
|
const [msgId, chatTypeStr, peerUid] = data.split('|');
|
||||||
@@ -136,6 +136,12 @@ class MessageUniqueWrapper {
|
|||||||
this.msgIdMap.resize(maxSize);
|
this.msgIdMap.resize(maxSize);
|
||||||
this.msgDataMap.resize(maxSize);
|
this.msgDataMap.resize(maxSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isShortId (message_id: string): boolean {
|
||||||
|
const num = Number(message_id);
|
||||||
|
// 判断是否是整数并且在 INT32 的范围内
|
||||||
|
return Number.isInteger(num) && num >= -2147483648 && num <= 2147483647;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MessageUnique: MessageUniqueWrapper = new MessageUniqueWrapper();
|
export const MessageUnique: MessageUniqueWrapper = new MessageUniqueWrapper();
|
||||||
|
|||||||
@@ -23,66 +23,50 @@ import { PromiseTimer } from './helper';
|
|||||||
* 懒加载测速:首次使用时自动测速,缓存 30 分钟
|
* 懒加载测速:首次使用时自动测速,缓存 30 分钟
|
||||||
*/
|
*/
|
||||||
export const GITHUB_FILE_MIRRORS = [
|
export const GITHUB_FILE_MIRRORS = [
|
||||||
// 延迟 < 800ms 的最快镜像
|
'https://github.chenc.dev/',
|
||||||
'https://github.chenc.dev/', // 666ms
|
'https://ghproxy.cfd/',
|
||||||
'https://ghproxy.cfd/', // 719ms - 支持重定向
|
'https://github.tbedu.top/',
|
||||||
'https://github.tbedu.top/', // 760ms
|
'https://ghproxy.cc/',
|
||||||
'https://ghps.cc/', // 768ms
|
'https://gh.monlor.com/',
|
||||||
'https://gh.llkk.cc/', // 774ms
|
'https://cdn.akaere.online/',
|
||||||
'https://ghproxy.cc/', // 777ms
|
'https://gh.idayer.com/',
|
||||||
'https://gh.monlor.com/', // 779ms
|
'https://gh.llkk.cc/',
|
||||||
'https://cdn.akaere.online/', // 784ms
|
'https://ghpxy.hwinzniej.top/',
|
||||||
// 延迟 800-1000ms 的快速镜像
|
'https://github-proxy.memory-echoes.cn/',
|
||||||
'https://gh.idayer.com/', // 869ms
|
'https://git.yylx.win/',
|
||||||
'https://gh-proxy.net/', // 885ms
|
'https://gitproxy.mrhjx.cn/',
|
||||||
'https://ghpxy.hwinzniej.top/', // 890ms
|
'https://gh.fhjhy.top/',
|
||||||
'https://github-proxy.memory-echoes.cn/', // 896ms
|
'https://gp.zkitefly.eu.org/',
|
||||||
'https://git.yylx.win/', // 917ms
|
'https://gh-proxy.com/',
|
||||||
'https://gitproxy.mrhjx.cn/', // 950ms
|
'https://ghfile.geekertao.top/',
|
||||||
'https://jiashu.1win.eu.org/', // 954ms
|
'https://j.1lin.dpdns.org/',
|
||||||
'https://ghproxy.cn/', // 981ms
|
'https://ghproxy.imciel.com/',
|
||||||
// 延迟 1000-1500ms 的中速镜像
|
'https://github-proxy.teach-english.tech/',
|
||||||
'https://gh.fhjhy.top/', // 1014ms
|
'https://gh.927223.xyz/',
|
||||||
'https://gp.zkitefly.eu.org/', // 1015ms
|
'https://github.ednovas.xyz/',
|
||||||
'https://gh-proxy.com/', // 1022ms
|
'https://ghf.xn--eqrr82bzpe.top/',
|
||||||
'https://hub.gitmirror.com/', // 1027ms
|
'https://gh.dpik.top/',
|
||||||
'https://ghfile.geekertao.top/', // 1029ms
|
'https://gh.jasonzeng.dev/',
|
||||||
'https://j.1lin.dpdns.org/', // 1037ms
|
'https://gh.xxooo.cf/',
|
||||||
'https://ghproxy.imciel.com/', // 1047ms
|
'https://gh.bugdey.us.kg/',
|
||||||
'https://github-proxy.teach-english.tech/', // 1047ms
|
'https://ghm.078465.xyz/',
|
||||||
'https://gh.927223.xyz/', // 1071ms
|
'https://j.1win.ggff.net/',
|
||||||
'https://github.ednovas.xyz/', // 1099ms
|
'https://tvv.tw/',
|
||||||
'https://ghf.xn--eqrr82bzpe.top/',// 1122ms
|
'https://gitproxy.127731.xyz/',
|
||||||
'https://gh.dpik.top/', // 1131ms
|
'https://gh.inkchills.cn/',
|
||||||
'https://gh.jasonzeng.dev/', // 1139ms
|
'https://ghproxy.cxkpro.top/',
|
||||||
'https://gh.xxooo.cf/', // 1157ms
|
'https://gh.sixyin.com/',
|
||||||
'https://gh.bugdey.us.kg/', // 1228ms
|
'https://github.geekery.cn/',
|
||||||
'https://ghm.078465.xyz/', // 1289ms
|
'https://git.669966.xyz/',
|
||||||
'https://j.1win.ggff.net/', // 1329ms
|
'https://gh.5050net.cn/',
|
||||||
'https://tvv.tw/', // 1393ms
|
'https://gh.felicity.ac.cn/',
|
||||||
'https://gh.chjina.com/', // 1446ms
|
'https://github.dpik.top/',
|
||||||
'https://gitproxy.127731.xyz/', // 1458ms
|
'https://ghp.keleyaa.com/',
|
||||||
// 延迟 1500-2500ms 的较慢镜像
|
'https://gh.wsmdn.dpdns.org/',
|
||||||
'https://gh.inkchills.cn/', // 1617ms
|
'https://ghproxy.monkeyray.net/',
|
||||||
'https://ghproxy.cxkpro.top/', // 1651ms
|
'https://fastgit.cc/',
|
||||||
'https://gh.sixyin.com/', // 1686ms
|
'https://gh.catmak.name/',
|
||||||
'https://github.geekery.cn/', // 1734ms
|
'https://gh.noki.icu/',
|
||||||
'https://git.669966.xyz/', // 1824ms
|
|
||||||
'https://gh.5050net.cn/', // 1858ms
|
|
||||||
'https://gh.felicity.ac.cn/', // 1903ms
|
|
||||||
'https://gh.ddlc.top/', // 2056ms
|
|
||||||
'https://cf.ghproxy.cc/', // 2058ms
|
|
||||||
'https://gitproxy.click/', // 2068ms
|
|
||||||
'https://github.dpik.top/', // 2313ms
|
|
||||||
'https://gh.zwnes.xyz/', // 2434ms
|
|
||||||
'https://ghp.keleyaa.com/', // 2440ms
|
|
||||||
'https://gh.wsmdn.dpdns.org/', // 2744ms
|
|
||||||
// 延迟 > 2500ms 的慢速镜像(作为备用)
|
|
||||||
'https://ghproxy.monkeyray.net/', // 3023ms
|
|
||||||
'https://fastgit.cc/', // 3369ms
|
|
||||||
'https://cdn.gh-proxy.com/', // 3394ms
|
|
||||||
'https://gh.catmak.name/', // 4119ms
|
|
||||||
'https://gh.noki.icu/', // 5990ms
|
|
||||||
'', // 原始 URL(无镜像)
|
'', // 原始 URL(无镜像)
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -109,7 +93,6 @@ export const GITHUB_RAW_MIRRORS = [
|
|||||||
// 测试确认支持 raw 文件的镜像
|
// 测试确认支持 raw 文件的镜像
|
||||||
'https://github.chenc.dev/https://raw.githubusercontent.com',
|
'https://github.chenc.dev/https://raw.githubusercontent.com',
|
||||||
'https://ghproxy.cfd/https://raw.githubusercontent.com',
|
'https://ghproxy.cfd/https://raw.githubusercontent.com',
|
||||||
'https://gh.llkk.cc/https://raw.githubusercontent.com',
|
|
||||||
'https://ghproxy.cc/https://raw.githubusercontent.com',
|
'https://ghproxy.cc/https://raw.githubusercontent.com',
|
||||||
'https://gh-proxy.net/https://raw.githubusercontent.com',
|
'https://gh-proxy.net/https://raw.githubusercontent.com',
|
||||||
];
|
];
|
||||||
@@ -150,7 +133,7 @@ const defaultConfig: MirrorConfig = {
|
|||||||
apiMirrors: GITHUB_API_MIRRORS,
|
apiMirrors: GITHUB_API_MIRRORS,
|
||||||
rawMirrors: GITHUB_RAW_MIRRORS,
|
rawMirrors: GITHUB_RAW_MIRRORS,
|
||||||
nightlyLinkMirrors: NIGHTLY_LINK_MIRRORS,
|
nightlyLinkMirrors: NIGHTLY_LINK_MIRRORS,
|
||||||
timeout: 10000, // 10秒超时,平衡速度和可靠性
|
timeout: 5000, // 5秒超时,平衡速度和可靠性
|
||||||
enabled: true,
|
enabled: true,
|
||||||
customMirror: undefined,
|
customMirror: undefined,
|
||||||
};
|
};
|
||||||
@@ -274,7 +257,7 @@ export async function getFastMirrors (forceRefresh: boolean = false): Promise<st
|
|||||||
async function performMirrorTest (): Promise<string[]> {
|
async function performMirrorTest (): Promise<string[]> {
|
||||||
// 开始镜像测速
|
// 开始镜像测速
|
||||||
|
|
||||||
const timeout = 8000; // 测速超时 8 秒
|
const timeout = 3000; // 测速超时 3 秒
|
||||||
|
|
||||||
// 并行测试所有镜像
|
// 并行测试所有镜像
|
||||||
const mirrors = currentConfig.fileMirrors.filter(m => m);
|
const mirrors = currentConfig.fileMirrors.filter(m => m);
|
||||||
@@ -400,17 +383,14 @@ export async function testUrlHead (url: string, timeout: number = 5000): Promise
|
|||||||
}, (res) => {
|
}, (res) => {
|
||||||
const statusCode = res.statusCode || 0;
|
const statusCode = res.statusCode || 0;
|
||||||
const contentType = (res.headers['content-type'] as string) || '';
|
const contentType = (res.headers['content-type'] as string) || '';
|
||||||
const contentLength = parseInt((res.headers['content-length'] as string) || '0', 10);
|
|
||||||
|
|
||||||
// 验证条件:
|
// 简化验证条件:
|
||||||
// 1. 状态码 2xx 或 3xx
|
// 1. 状态码 2xx 或 3xx
|
||||||
// 2. Content-Type 不应该是 text/html(表示错误页面)
|
// 2. Content-Type 不应该是 text/html(表示错误页面)
|
||||||
// 3. 对于 .zip 文件,Content-Length 应该 > 1MB(避免获取到错误页面)
|
|
||||||
const isValidStatus = statusCode >= 200 && statusCode < 400;
|
const isValidStatus = statusCode >= 200 && statusCode < 400;
|
||||||
const isNotHtmlError = !contentType.includes('text/html');
|
const isNotHtmlError = !contentType.includes('text/html');
|
||||||
const isValidSize = url.endsWith('.zip') ? contentLength > 1024 * 1024 : true;
|
|
||||||
|
|
||||||
resolve(isValidStatus && isNotHtmlError && isValidSize);
|
resolve(isValidStatus && isNotHtmlError);
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on('error', () => resolve(false));
|
req.on('error', () => resolve(false));
|
||||||
@@ -454,10 +434,9 @@ export async function validateUrl (url: string, timeout: number = 5000): Promise
|
|||||||
const contentType = (res.headers['content-type'] as string) || '';
|
const contentType = (res.headers['content-type'] as string) || '';
|
||||||
const contentLength = parseInt((res.headers['content-length'] as string) || '0', 10);
|
const contentLength = parseInt((res.headers['content-length'] as string) || '0', 10);
|
||||||
|
|
||||||
// 验证条件
|
// 简化验证条件
|
||||||
const isValidStatus = statusCode >= 200 && statusCode < 400;
|
const isValidStatus = statusCode >= 200 && statusCode < 400;
|
||||||
const isNotHtmlError = !contentType.includes('text/html');
|
const isNotHtmlError = !contentType.includes('text/html');
|
||||||
const isValidSize = url.endsWith('.zip') ? contentLength > 1024 * 1024 : true;
|
|
||||||
|
|
||||||
if (!isValidStatus) {
|
if (!isValidStatus) {
|
||||||
resolve({
|
resolve({
|
||||||
@@ -475,14 +454,6 @@ export async function validateUrl (url: string, timeout: number = 5000): Promise
|
|||||||
contentLength,
|
contentLength,
|
||||||
error: '返回了 HTML 页面而非文件',
|
error: '返回了 HTML 页面而非文件',
|
||||||
});
|
});
|
||||||
} else if (!isValidSize) {
|
|
||||||
resolve({
|
|
||||||
valid: false,
|
|
||||||
statusCode,
|
|
||||||
contentType,
|
|
||||||
contentLength,
|
|
||||||
error: `文件过小 (${contentLength} bytes),可能是错误页面`,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
resolve({
|
resolve({
|
||||||
valid: true,
|
valid: true,
|
||||||
@@ -559,21 +530,21 @@ export async function findAvailableDownloadUrl (
|
|||||||
const testWithValidation = async (url: string): Promise<boolean> => {
|
const testWithValidation = async (url: string): Promise<boolean> => {
|
||||||
if (validateContent) {
|
if (validateContent) {
|
||||||
const result = await validateUrl(url, timeout);
|
const result = await validateUrl(url, timeout);
|
||||||
// 额外检查文件大小
|
// 额外检查文件大小(仅当指定了 minFileSize 时)
|
||||||
if (result.valid && minFileSize && result.contentLength && result.contentLength < minFileSize) {
|
if (result.valid && minFileSize && result.contentLength && result.contentLength < minFileSize) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return result.valid;
|
return result.valid;
|
||||||
}
|
}
|
||||||
return testMethod === 'head' ? testUrlHead(url, timeout) : testUrl(url, timeout);
|
// 不验证内容,只检查状态码
|
||||||
|
const isValid = testMethod === 'head' ? await testUrlHead(url, timeout) : await testUrl(url, timeout);
|
||||||
|
return isValid;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. 如果设置了自定义镜像,优先使用
|
// 1. 如果设置了自定义镜像,直接使用(不测试,信任用户选择)
|
||||||
if (customMirror) {
|
if (customMirror) {
|
||||||
const customUrl = buildMirrorUrl(originalUrl, customMirror);
|
const customUrl = buildMirrorUrl(originalUrl, customMirror);
|
||||||
if (await testWithValidation(customUrl)) {
|
return customUrl;
|
||||||
return customUrl;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 先测试原始 URL
|
// 2. 先测试原始 URL
|
||||||
@@ -585,7 +556,7 @@ export async function findAvailableDownloadUrl (
|
|||||||
let testedCount = 0;
|
let testedCount = 0;
|
||||||
for (const mirror of mirrors) {
|
for (const mirror of mirrors) {
|
||||||
if (!mirror) continue; // 跳过空字符串
|
if (!mirror) continue; // 跳过空字符串
|
||||||
|
|
||||||
// 特殊处理 nightly.link URL
|
// 特殊处理 nightly.link URL
|
||||||
let mirrorUrl: string;
|
let mirrorUrl: string;
|
||||||
if (originalUrl.includes('nightly.link')) {
|
if (originalUrl.includes('nightly.link')) {
|
||||||
@@ -594,7 +565,7 @@ export async function findAvailableDownloadUrl (
|
|||||||
} else {
|
} else {
|
||||||
mirrorUrl = buildMirrorUrl(originalUrl, mirror);
|
mirrorUrl = buildMirrorUrl(originalUrl, mirror);
|
||||||
}
|
}
|
||||||
|
|
||||||
testedCount++;
|
testedCount++;
|
||||||
if (await testWithValidation(mirrorUrl)) {
|
if (await testWithValidation(mirrorUrl)) {
|
||||||
return mirrorUrl;
|
return mirrorUrl;
|
||||||
@@ -650,8 +621,15 @@ function compareSemVerSimple (a: string, b: string): number {
|
|||||||
* 从 tags 列表中获取最新的 release tag
|
* 从 tags 列表中获取最新的 release tag
|
||||||
* 不依赖 GitHub API
|
* 不依赖 GitHub API
|
||||||
*/
|
*/
|
||||||
export async function getLatestReleaseTag (owner: string, repo: string): Promise<string> {
|
// Update definitions validation locally first if possible.
|
||||||
const result = await getAllGitHubTags(owner, repo);
|
// I'll assume valid typescript.
|
||||||
|
// I will split this into two tool calls to avoid complexity.
|
||||||
|
// 1. Update mirror.ts
|
||||||
|
// 2. Update UpdateNapCat.ts
|
||||||
|
|
||||||
|
// This tool call: Update mirror.ts
|
||||||
|
export async function getLatestReleaseTag (owner: string, repo: string, mirror?: string): Promise<string> {
|
||||||
|
const result = await getAllGitHubTags(owner, repo, mirror);
|
||||||
|
|
||||||
// 过滤出符合 semver 的 tags
|
// 过滤出符合 semver 的 tags
|
||||||
const releaseTags = result.tags.filter(tag => SEMVER_REGEX.test(tag));
|
const releaseTags = result.tags.filter(tag => SEMVER_REGEX.test(tag));
|
||||||
@@ -701,6 +679,8 @@ export async function getGitHubRelease (
|
|||||||
assetNames?: string[];
|
assetNames?: string[];
|
||||||
/** 是否需要获取 changelog(需要调用 API) */
|
/** 是否需要获取 changelog(需要调用 API) */
|
||||||
fetchChangelog?: boolean;
|
fetchChangelog?: boolean;
|
||||||
|
/** 指定镜像 */
|
||||||
|
mirror?: string;
|
||||||
} = {}
|
} = {}
|
||||||
): Promise<{
|
): Promise<{
|
||||||
tag_name: string;
|
tag_name: string;
|
||||||
@@ -710,15 +690,16 @@ export async function getGitHubRelease (
|
|||||||
}>;
|
}>;
|
||||||
body?: string;
|
body?: string;
|
||||||
}> {
|
}> {
|
||||||
const { assetNames = [], fetchChangelog = false } = options;
|
const { assetNames = [], fetchChangelog = false, mirror } = options;
|
||||||
|
|
||||||
// 1. 获取实际的 tag 名称
|
// 1. 获取实际的 tag 名称
|
||||||
let actualTag: string;
|
let actualTag: string;
|
||||||
if (tag === 'latest') {
|
if (tag === 'latest') {
|
||||||
actualTag = await getLatestReleaseTag(owner, repo);
|
actualTag = await getLatestReleaseTag(owner, repo, mirror);
|
||||||
} else {
|
} else {
|
||||||
actualTag = tag;
|
actualTag = tag;
|
||||||
}
|
}
|
||||||
|
// ...
|
||||||
|
|
||||||
// 2. 构建 assets 列表(不需要 API)
|
// 2. 构建 assets 列表(不需要 API)
|
||||||
const assets = assetNames.map(name => ({
|
const assets = assetNames.map(name => ({
|
||||||
@@ -782,8 +763,8 @@ const tagsCache: Map<string, TagsCache> = new Map();
|
|||||||
* 获取所有 GitHub tags(带缓存)
|
* 获取所有 GitHub tags(带缓存)
|
||||||
* 优化:并行请求多个镜像,使用第一个成功返回的结果
|
* 优化:并行请求多个镜像,使用第一个成功返回的结果
|
||||||
*/
|
*/
|
||||||
export async function getAllGitHubTags (owner: string, repo: string): Promise<{ tags: string[], mirror: string; }> {
|
export async function getAllGitHubTags (owner: string, repo: string, mirror?: string): Promise<{ tags: string[], mirror: string; }> {
|
||||||
const cacheKey = `${owner}/${repo}`;
|
const cacheKey = `${owner}/${repo}/${mirror || 'auto'}`;
|
||||||
|
|
||||||
// 检查缓存
|
// 检查缓存
|
||||||
const cached = tagsCache.get(cacheKey);
|
const cached = tagsCache.get(cacheKey);
|
||||||
@@ -805,7 +786,7 @@ export async function getAllGitHubTags (owner: string, repo: string): Promise<{
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 尝试从 URL 获取 tags
|
// 尝试从 URL 获取 tags
|
||||||
const fetchFromUrl = async (url: string, mirror: string): Promise<{ tags: string[], mirror: string; } | null> => {
|
const fetchFromUrl = async (url: string, usedMirror: string): Promise<{ tags: string[], mirror: string; } | null> => {
|
||||||
try {
|
try {
|
||||||
const raw = await PromiseTimer(
|
const raw = await PromiseTimer(
|
||||||
RequestUtil.HttpGetText(url),
|
RequestUtil.HttpGetText(url),
|
||||||
@@ -813,79 +794,55 @@ export async function getAllGitHubTags (owner: string, repo: string): Promise<{
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 检查返回内容是否有效(不是 HTML 错误页面)
|
// 检查返回内容是否有效(不是 HTML 错误页面)
|
||||||
if (raw.includes('<!DOCTYPE') || raw.includes('<html')) {
|
if (raw.includes('refs/tags')) {
|
||||||
return null;
|
return { tags: parseTags(raw), mirror: usedMirror };
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = parseTags(raw);
|
|
||||||
if (tags.length > 0) {
|
|
||||||
return { tags, mirror };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
// 忽略错误
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取快速镜像列表
|
// 准备镜像列表
|
||||||
let fastMirrors: string[] = [];
|
let mirrors: string[] = [];
|
||||||
try {
|
if (mirror) {
|
||||||
fastMirrors = await getFastMirrors();
|
// 如果指定了镜像,只使用该镜像
|
||||||
} catch {
|
mirrors = [mirror];
|
||||||
// 忽略错误
|
} else {
|
||||||
|
// 否则使用 auto 逻辑,利用缓存的快速镜像列表
|
||||||
|
mirrors = await getFastMirrors();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建 URL 列表(取前 5 个快速镜像 + 原始 URL 并行请求)
|
// 并行请求
|
||||||
const topMirrors = fastMirrors.slice(0, 5);
|
const promises = mirrors.map(m => {
|
||||||
const mirrorUrls = [
|
const url = m ? buildMirrorUrl(baseUrl, m) : baseUrl;
|
||||||
{ url: baseUrl, mirror: 'github.com' }, // 原始 URL
|
return fetchFromUrl(url, m || 'https://github.com');
|
||||||
...topMirrors.filter(m => m).map(m => ({ url: buildMirrorUrl(baseUrl, m), mirror: m })),
|
});
|
||||||
];
|
|
||||||
|
|
||||||
// 并行请求所有镜像,使用 Promise.any 获取第一个成功的结果
|
|
||||||
try {
|
try {
|
||||||
const result = await Promise.any(
|
const result = await Promise.any(promises.filter(p => p !== null) as Promise<{ tags: string[], mirror: string; } | null>[]);
|
||||||
mirrorUrls.map(async ({ url, mirror }) => {
|
|
||||||
const res = await fetchFromUrl(url, mirror);
|
|
||||||
if (res) return res;
|
|
||||||
throw new Error('Failed');
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// 缓存结果
|
|
||||||
tagsCache.set(cacheKey, { tags: result.tags, mirror: result.mirror, timestamp: Date.now() });
|
|
||||||
return result;
|
|
||||||
} catch {
|
|
||||||
// Promise.any 全部失败,回退到顺序尝试剩余镜像
|
|
||||||
}
|
|
||||||
|
|
||||||
// 回退:顺序尝试剩余镜像
|
|
||||||
const remainingMirrors = fastMirrors.slice(5).filter(m => m);
|
|
||||||
for (const mirror of remainingMirrors) {
|
|
||||||
const url = buildMirrorUrl(baseUrl, mirror);
|
|
||||||
const result = await fetchFromUrl(url, mirror);
|
|
||||||
if (result) {
|
if (result) {
|
||||||
tagsCache.set(cacheKey, { tags: result.tags, mirror: result.mirror, timestamp: Date.now() });
|
tagsCache.set(cacheKey, {
|
||||||
|
tags: result.tags,
|
||||||
|
mirror: result.mirror,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
// all failed
|
||||||
}
|
}
|
||||||
|
|
||||||
// 最后尝试所有镜像
|
if (mirror) {
|
||||||
const allMirrors = currentConfig.fileMirrors.filter(m => m && !fastMirrors.includes(m));
|
throw new Error(`指定镜像 ${mirror} 获取 tags 失败`);
|
||||||
for (const mirror of allMirrors) {
|
|
||||||
const url = buildMirrorUrl(baseUrl, mirror);
|
|
||||||
const result = await fetchFromUrl(url, mirror);
|
|
||||||
if (result) {
|
|
||||||
tagsCache.set(cacheKey, { tags: result.tags, mirror: result.mirror, timestamp: Date.now() });
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('无法获取 tags,所有源都不可用');
|
throw new Error('无法获取 tags,所有镜像源都不可用');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== Action Artifacts 支持 ==============
|
// ============== Action Artifacts 支持 ==============
|
||||||
|
|
||||||
|
// ActionArtifact 接口定义
|
||||||
export interface ActionArtifact {
|
export interface ActionArtifact {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -895,12 +852,14 @@ export interface ActionArtifact {
|
|||||||
archive_download_url: string;
|
archive_download_url: string;
|
||||||
workflow_run_id?: number;
|
workflow_run_id?: number;
|
||||||
head_sha?: string;
|
head_sha?: string;
|
||||||
|
workflow_title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============== Action Artifacts 缓存 ==============
|
// ============== Action Artifacts 缓存 ==============
|
||||||
|
|
||||||
interface ArtifactsCache {
|
interface ArtifactsCache {
|
||||||
artifacts: ActionArtifact[];
|
artifacts: ActionArtifact[];
|
||||||
|
mirror: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -920,68 +879,116 @@ export function clearArtifactsCache (): void {
|
|||||||
* 当 api.github.com 不可用时使用
|
* 当 api.github.com 不可用时使用
|
||||||
* 页面格式: https://github.com/{owner}/{repo}/actions/workflows/{workflow}
|
* 页面格式: https://github.com/{owner}/{repo}/actions/workflows/{workflow}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async function getWorkflowRunsFromHtml (
|
async function getWorkflowRunsFromHtml (
|
||||||
owner: string,
|
owner: string,
|
||||||
repo: string,
|
repo: string,
|
||||||
workflow: string = 'build.yml',
|
workflow: string = 'build.yml',
|
||||||
maxRuns: number = 10
|
maxRuns: number = 10,
|
||||||
): Promise<Array<{ id: number; created_at: string; }>> {
|
mirror?: string
|
||||||
|
): Promise<{ runs: Array<{ id: number; created_at: string; title: string; }>; mirror: string; }> {
|
||||||
const baseUrl = `https://github.com/${owner}/${repo}/actions/workflows/${workflow}`;
|
const baseUrl = `https://github.com/${owner}/${repo}/actions/workflows/${workflow}`;
|
||||||
|
|
||||||
// 尝试使用镜像获取 HTML
|
// 尝试使用镜像获取 HTML
|
||||||
const mirrors = ['', ...currentConfig.fileMirrors.filter(m => m)];
|
// 如果指定了 mirror,则只使用该 mirror
|
||||||
|
let mirrors: string[] = [];
|
||||||
for (const mirror of mirrors) {
|
if (mirror) {
|
||||||
|
mirrors = [mirror];
|
||||||
|
} else {
|
||||||
|
// 使用缓存的快速镜像列表
|
||||||
|
mirrors = await getFastMirrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const mirrorItem of mirrors) {
|
||||||
try {
|
try {
|
||||||
const url = mirror ? buildMirrorUrl(baseUrl, mirror) : baseUrl;
|
const allRuns: Array<{ id: number; created_at: string; title: string; }> = [];
|
||||||
|
|
||||||
const html = await PromiseTimer(
|
|
||||||
RequestUtil.HttpGetText(url),
|
|
||||||
10000
|
|
||||||
);
|
|
||||||
|
|
||||||
// 从 HTML 中提取 run IDs 和时间
|
|
||||||
// 格式: href="/NapNeko/NapCatQQ/actions/runs/20676123968"
|
|
||||||
// 时间格式: <relative-time datetime="2026-01-03T10:37:29Z"
|
|
||||||
const runPattern = new RegExp(`href="/${owner}/${repo}/actions/runs/(\\d+)"`, 'gi');
|
|
||||||
const timePattern = /<relative-time\s+datetime="([^"]+)"/gi;
|
|
||||||
|
|
||||||
// 提取所有时间
|
|
||||||
const times: string[] = [];
|
|
||||||
let timeMatch;
|
|
||||||
while ((timeMatch = timePattern.exec(html)) !== null) {
|
|
||||||
times.push(timeMatch[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const runs: Array<{ id: number; created_at: string; }> = [];
|
|
||||||
const foundIds = new Set<number>();
|
const foundIds = new Set<number>();
|
||||||
let timeIndex = 0;
|
let page = 1;
|
||||||
|
const maxPages = 10; // 防止无限请求,最多翻10页(约250个条目)
|
||||||
let match;
|
|
||||||
while ((match = runPattern.exec(html)) !== null && runs.length < maxRuns) {
|
while (allRuns.length < maxRuns && page <= maxPages) {
|
||||||
const id = parseInt(match[1]);
|
const pageUrl = page > 1 ? `${baseUrl}?page=${page}` : baseUrl;
|
||||||
if (!foundIds.has(id)) {
|
const url = mirrorItem ? buildMirrorUrl(pageUrl, mirrorItem) : pageUrl;
|
||||||
foundIds.add(id);
|
|
||||||
// 尝试获取对应的时间,每个 run 通常有两个时间(桌面和移动端显示)
|
const html = await PromiseTimer(
|
||||||
// 所以每找到一个 run,跳过两个时间
|
RequestUtil.HttpGetText(url),
|
||||||
const created_at = times[timeIndex] || new Date().toISOString();
|
10000
|
||||||
timeIndex += 2; // 跳过两个时间(桌面端和移动端各一个)
|
);
|
||||||
runs.push({
|
|
||||||
id,
|
// 使用 Block 分割策略,更稳健地关联 ID 和时间
|
||||||
created_at,
|
const rows = html.split('<div class="Box-row');
|
||||||
});
|
let foundOnThisPage = 0;
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
// 提取 Run ID 和 Status
|
||||||
|
// <a href="/NapNeko/NapCatQQ/actions/runs/20799940346" ... aria-label="completed successfully: ...">
|
||||||
|
const runMatch = new RegExp(`href="/${owner}/${repo}/actions/runs/(\\d+)"[^>]*aria-label="([^"]*)"`, 'i').exec(row);
|
||||||
|
|
||||||
|
if (!runMatch || !runMatch[1] || !runMatch[2]) continue;
|
||||||
|
|
||||||
|
const id = parseInt(runMatch[1]);
|
||||||
|
const ariaLabel = runMatch[2];
|
||||||
|
const ariaLabelLower = ariaLabel.toLowerCase();
|
||||||
|
|
||||||
|
// 只需要判断 completed
|
||||||
|
if (ariaLabelLower.includes('completed')) {
|
||||||
|
if (!foundIds.has(id)) {
|
||||||
|
// 提取时间 (取 Block 内的第一个 relative-time)
|
||||||
|
const timeMatch = /<relative-time\s+datetime="([^"]+)"/.exec(row);
|
||||||
|
if (timeMatch && timeMatch[1]) {
|
||||||
|
foundIds.add(id);
|
||||||
|
foundOnThisPage++;
|
||||||
|
|
||||||
|
// 优先从 markdown-title class 提取标题
|
||||||
|
let title = '';
|
||||||
|
const titleMatch = /class="[^"]*markdown-title[^"]*"[^>]*>([\s\S]*?)<\/span>/i.exec(row);
|
||||||
|
if (titleMatch && titleMatch[1]) {
|
||||||
|
title = titleMatch[1].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没找到,回退到 aria-label 逻辑
|
||||||
|
if (!title) {
|
||||||
|
title = ariaLabel;
|
||||||
|
const prefixMatch = /^(completed successfully:\s*)/i.exec(title);
|
||||||
|
if (prefixMatch) {
|
||||||
|
title = title.substring(prefixMatch[0].length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allRuns.push({
|
||||||
|
id,
|
||||||
|
created_at: timeMatch[1],
|
||||||
|
title: title.trim()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果本页没有找到任何 completed 的 run(但页面可能不为空),或者页面内容太少(可能是最后一页或错误)
|
||||||
|
// 这里简化判断: 如果本页没提取到任何有效数据,就认为没有更多数据了
|
||||||
|
if (foundOnThisPage === 0) {
|
||||||
|
// 也要考虑到可能是页面解析失败或者全是 failed 状态
|
||||||
|
// 检查是否有翻页按钮可能更复杂,暂时假设如果一整页都没有 successful run,可能后面也没有了,或者我们已经获取够多了
|
||||||
|
// 为了稳健,如果本页没找到,且 allRuns 还没满,尝试下一页 (除非页面很小说明是空页)
|
||||||
|
if (rows.length < 2) { // 只有 split 的第一个空元素
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页逻辑:总是尝试下一页,直到满足 maxRuns
|
||||||
|
page++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (runs.length > 0) {
|
if (allRuns.length > 0) {
|
||||||
return runs;
|
return { runs: allRuns, mirror: mirrorItem || 'https://github.com' };
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return { runs: [], mirror: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -996,122 +1003,49 @@ async function getArtifactsFromNightlyLink (
|
|||||||
owner: string,
|
owner: string,
|
||||||
repo: string,
|
repo: string,
|
||||||
workflow: string = 'build.yml',
|
workflow: string = 'build.yml',
|
||||||
branch: string = 'main',
|
_branch: string = 'main',
|
||||||
maxRuns: number = 10
|
maxRuns: number = 10,
|
||||||
): Promise<ActionArtifact[]> {
|
mirror?: string
|
||||||
let workflowRuns: Array<{ id: number; head_sha?: string; created_at: string; }> = [];
|
): Promise<{ artifacts: ActionArtifact[], mirror: string; }> {
|
||||||
|
// 策略: 优先使用 nightly.link(更稳定,无需认证)+ HTML 解析
|
||||||
// 策略1: 优先尝试 GitHub API
|
|
||||||
try {
|
try {
|
||||||
const endpoint = `https://api.github.com/repos/${owner}/${repo}/actions/workflows/${workflow}/runs?branch=${branch}&status=success&per_page=${maxRuns}`;
|
// 以前尝试使用 GitHub API,现在弃用,完全使用 HTML 解析逻辑
|
||||||
|
// 并获取 workflow // 直接从 HTML 页面解析
|
||||||
const runsResponse = await PromiseTimer(
|
const { runs: workflowRuns, mirror: runsMirror } = await getWorkflowRunsFromHtml(owner, repo, workflow, maxRuns, mirror);
|
||||||
RequestUtil.HttpGetJson<{
|
|
||||||
workflow_runs: Array<{ id: number; head_sha: string; created_at: string; }>;
|
|
||||||
}>(endpoint, 'GET', undefined, {
|
|
||||||
'User-Agent': 'NapCat',
|
|
||||||
'Accept': 'application/vnd.github.v3+json',
|
|
||||||
}),
|
|
||||||
10000
|
|
||||||
);
|
|
||||||
|
|
||||||
if (runsResponse.workflow_runs && runsResponse.workflow_runs.length > 0) {
|
if (workflowRuns.length === 0) {
|
||||||
workflowRuns = runsResponse.workflow_runs;
|
return { artifacts: [], mirror: runsMirror };
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
// API 请求失败,继续尝试 HTML 解析
|
|
||||||
}
|
|
||||||
|
|
||||||
// 策略2: API 失败时,从 HTML 页面解析
|
// 直接拼接 nightly.link URL
|
||||||
if (workflowRuns.length === 0) {
|
// 格式: https://nightly.link/{owner}/{repo}/actions/runs/{run_id}/{artifact_name}.zip
|
||||||
workflowRuns = await getWorkflowRunsFromHtml(owner, repo, workflow, maxRuns);
|
const artifacts: ActionArtifact[] = [];
|
||||||
}
|
const artifactNames = ['NapCat.Framework', 'NapCat.Shell']; // 已知的 artifact 名称
|
||||||
|
|
||||||
if (workflowRuns.length === 0) {
|
// 如果 HTML 解析使用的 mirror 是 github.com(空),则 nightly.link 使用默认配置
|
||||||
return [];
|
// 如果使用了镜像,可能需要特殊的 nightly.link 镜像,或者这里仅记录 HTML 来源镜像
|
||||||
}
|
// 实际上 nightly.link 本身就是一个服务,我们使用配置中的 nightlyLinkMirrors
|
||||||
|
const baseNightlyMirror = currentConfig.nightlyLinkMirrors[0] || 'https://nightly.link';
|
||||||
|
|
||||||
// 直接拼接 nightly.link URL
|
for (const run of workflowRuns) {
|
||||||
// 格式: https://nightly.link/{owner}/{repo}/actions/runs/{run_id}/{artifact_name}.zip
|
for (const artifactName of artifactNames) {
|
||||||
const artifacts: ActionArtifact[] = [];
|
artifacts.push({
|
||||||
const artifactNames = ['NapCat.Framework', 'NapCat.Shell']; // 已知的 artifact 名称
|
id: run.id,
|
||||||
|
name: artifactName,
|
||||||
for (const run of workflowRuns) {
|
size_in_bytes: 0,
|
||||||
for (const artifactName of artifactNames) {
|
created_at: run.created_at,
|
||||||
const mirror = currentConfig.nightlyLinkMirrors[0] || 'https://nightly.link';
|
expires_at: new Date(new Date(run.created_at).getTime() + 3 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
artifacts.push({
|
archive_download_url: `${baseNightlyMirror}/${owner}/${repo}/actions/runs/${run.id}/${artifactName}.zip`,
|
||||||
id: run.id,
|
workflow_run_id: run.id,
|
||||||
name: artifactName,
|
workflow_title: run.title,
|
||||||
size_in_bytes: 0,
|
});
|
||||||
created_at: run.created_at,
|
|
||||||
expires_at: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
archive_download_url: `${mirror}/${owner}/${repo}/actions/runs/${run.id}/${artifactName}.zip`,
|
|
||||||
workflow_run_id: run.id,
|
|
||||||
head_sha: run.head_sha,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return artifacts;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通过 GitHub API 获取 artifacts(主要方案)
|
|
||||||
*/
|
|
||||||
async function getArtifactsFromAPI (
|
|
||||||
owner: string,
|
|
||||||
repo: string,
|
|
||||||
workflow: string = 'build.yml',
|
|
||||||
branch: string = 'main',
|
|
||||||
maxRuns: number = 10
|
|
||||||
): Promise<ActionArtifact[]> {
|
|
||||||
const endpoint = `https://api.github.com/repos/${owner}/${repo}/actions/workflows/${workflow}/runs?branch=${branch}&status=success&per_page=${maxRuns}`;
|
|
||||||
|
|
||||||
const runsResponse = await PromiseTimer(
|
|
||||||
RequestUtil.HttpGetJson<{
|
|
||||||
workflow_runs: Array<{ id: number; head_sha: string; created_at: string; }>;
|
|
||||||
}>(endpoint, 'GET', undefined, {
|
|
||||||
'User-Agent': 'NapCat',
|
|
||||||
'Accept': 'application/vnd.github.v3+json',
|
|
||||||
}),
|
|
||||||
10000
|
|
||||||
);
|
|
||||||
|
|
||||||
const workflowRuns = runsResponse.workflow_runs;
|
|
||||||
if (!workflowRuns || workflowRuns.length === 0) {
|
|
||||||
throw new Error('No successful workflow runs found');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取所有 runs 的 artifacts
|
|
||||||
const allArtifacts: ActionArtifact[] = [];
|
|
||||||
|
|
||||||
for (const run of workflowRuns) {
|
|
||||||
try {
|
|
||||||
const artifactsEndpoint = `https://api.github.com/repos/${owner}/${repo}/actions/runs/${run.id}/artifacts`;
|
|
||||||
const artifactsResponse = await PromiseTimer(
|
|
||||||
RequestUtil.HttpGetJson<{
|
|
||||||
artifacts: ActionArtifact[];
|
|
||||||
}>(artifactsEndpoint, 'GET', undefined, {
|
|
||||||
'User-Agent': 'NapCat',
|
|
||||||
'Accept': 'application/vnd.github.v3+json',
|
|
||||||
}),
|
|
||||||
10000
|
|
||||||
);
|
|
||||||
|
|
||||||
if (artifactsResponse.artifacts) {
|
|
||||||
// 为每个 artifact 添加 run 信息
|
|
||||||
for (const artifact of artifactsResponse.artifacts) {
|
|
||||||
artifact.workflow_run_id = run.id;
|
|
||||||
artifact.head_sha = run.head_sha;
|
|
||||||
allArtifacts.push(artifact);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
// 单个 run 获取失败,继续下一个
|
|
||||||
}
|
}
|
||||||
}
|
return { artifacts, mirror: runsMirror };
|
||||||
|
|
||||||
return allArtifacts;
|
} catch {
|
||||||
|
return { artifacts: [], mirror: '' };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1121,48 +1055,41 @@ async function getArtifactsFromAPI (
|
|||||||
* 策略:
|
* 策略:
|
||||||
* 1. 检查缓存(10分钟有效)
|
* 1. 检查缓存(10分钟有效)
|
||||||
* 2. 优先尝试从 nightly.link 获取(无需认证,更稳定)
|
* 2. 优先尝试从 nightly.link 获取(无需认证,更稳定)
|
||||||
* 3. 如果失败,回退到 GitHub API
|
* 3. 这里的实现已经完全移除了对 GitHub API 的依赖,直接解析 HTML
|
||||||
*/
|
*/
|
||||||
export async function getLatestActionArtifacts (
|
export async function getLatestActionArtifacts (
|
||||||
owner: string,
|
owner: string,
|
||||||
repo: string,
|
repo: string,
|
||||||
workflow: string = 'build.yml',
|
workflow: string = 'build.yml',
|
||||||
branch: string = 'main',
|
branch: string = 'main',
|
||||||
maxRuns: number = 10
|
maxRuns: number = 10,
|
||||||
): Promise<ActionArtifact[]> {
|
mirror?: string
|
||||||
const cacheKey = `${owner}/${repo}/${workflow}/${branch}`;
|
): Promise<{ artifacts: ActionArtifact[], mirror: string; }> {
|
||||||
|
const cacheKey = `${owner}/${repo}/${workflow}/${branch}/${mirror || 'auto'}`;
|
||||||
|
|
||||||
// 检查缓存
|
// 检查缓存
|
||||||
const cached = artifactsCache.get(cacheKey);
|
const cached = artifactsCache.get(cacheKey);
|
||||||
if (cached && (Date.now() - cached.timestamp) < ARTIFACTS_CACHE_TTL) {
|
if (cached && (Date.now() - cached.timestamp) < ARTIFACTS_CACHE_TTL) {
|
||||||
return cached.artifacts;
|
return { artifacts: cached.artifacts, mirror: cached.mirror };
|
||||||
}
|
}
|
||||||
|
|
||||||
let artifacts: ActionArtifact[] = [];
|
let result: { artifacts: ActionArtifact[], mirror: string; } = { artifacts: [], mirror: '' };
|
||||||
|
|
||||||
// 策略1: 优先使用 nightly.link(更稳定,无需认证)
|
// 策略: 优先使用 nightly.link(更稳定,无需认证)+ HTML 解析
|
||||||
try {
|
try {
|
||||||
artifacts = await getArtifactsFromNightlyLink(owner, repo, workflow, branch, maxRuns);
|
result = await getArtifactsFromNightlyLink(owner, repo, workflow, branch, maxRuns, mirror);
|
||||||
} catch {
|
} catch {
|
||||||
// nightly.link 获取失败
|
// 获取失败
|
||||||
}
|
|
||||||
|
|
||||||
// 策略2: 回退到 GitHub API
|
|
||||||
if (artifacts.length === 0) {
|
|
||||||
try {
|
|
||||||
artifacts = await getArtifactsFromAPI(owner, repo, workflow, branch, maxRuns);
|
|
||||||
} catch {
|
|
||||||
// API 获取失败
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缓存结果(即使为空也缓存,避免频繁请求)
|
// 缓存结果(即使为空也缓存,避免频繁请求)
|
||||||
if (artifacts.length > 0) {
|
if (result.artifacts.length > 0) {
|
||||||
artifactsCache.set(cacheKey, {
|
artifactsCache.set(cacheKey, {
|
||||||
artifacts,
|
artifacts: result.artifacts,
|
||||||
|
mirror: result.mirror,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return artifacts;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,9 +65,11 @@ export function compareSemVer (v1: string, v2: string): -1 | 0 | 1 {
|
|||||||
const a = parseSemVer(v1);
|
const a = parseSemVer(v1);
|
||||||
const b = parseSemVer(v2);
|
const b = parseSemVer(v2);
|
||||||
|
|
||||||
if (!a.valid || !b.valid) {
|
if (!a.valid && !b.valid) {
|
||||||
return 0;
|
return v1.localeCompare(v2) as -1 | 0 | 1;
|
||||||
}
|
}
|
||||||
|
if (!a.valid) return -1;
|
||||||
|
if (!b.valid) return 1;
|
||||||
|
|
||||||
// 比较主版本号
|
// 比较主版本号
|
||||||
if (a.major !== b.major) return a.major > b.major ? 1 : -1;
|
if (a.major !== b.major) return a.major > b.major ? 1 : -1;
|
||||||
|
|||||||
307
packages/napcat-core/apis/flash.ts
Normal file
307
packages/napcat-core/apis/flash.ts
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
import { GeneralCallResult, InstanceContext, NapCatCore } from '@/napcat-core';
|
||||||
|
import {
|
||||||
|
createFlashTransferResult,
|
||||||
|
FileListResponse,
|
||||||
|
FlashFileSetInfo,
|
||||||
|
SendStatus,
|
||||||
|
UploadSceneType,
|
||||||
|
} from '@/napcat-core/data/flash';
|
||||||
|
import { Peer } from '@/napcat-core/types';
|
||||||
|
|
||||||
|
export class NTQQFlashApi {
|
||||||
|
context: InstanceContext;
|
||||||
|
core: NapCatCore;
|
||||||
|
|
||||||
|
constructor (context: InstanceContext, core: NapCatCore) {
|
||||||
|
this.context = context;
|
||||||
|
this.core = core;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发起闪传上传任务
|
||||||
|
* @param fileListToUpload 上传文件绝对路径的列表,可以是文件夹!!
|
||||||
|
* @param thumbnailPath
|
||||||
|
* @param filesetName
|
||||||
|
*/
|
||||||
|
async createFlashTransferUploadTask (fileListToUpload: string[], thumbnailPath: string, filesetName: string): Promise<GeneralCallResult & {
|
||||||
|
createFlashTransferResult: createFlashTransferResult;
|
||||||
|
seq: number;
|
||||||
|
}> {
|
||||||
|
const flashService = this.context.session.getFlashTransferService();
|
||||||
|
|
||||||
|
const timestamp: number = Date.now();
|
||||||
|
const selfInfo = this.core.selfInfo;
|
||||||
|
|
||||||
|
const fileUploadArg = {
|
||||||
|
screen: 1, // 1
|
||||||
|
name: filesetName,
|
||||||
|
uploaders: [{
|
||||||
|
uin: selfInfo.uin,
|
||||||
|
uid: selfInfo.uid,
|
||||||
|
sendEntrance: '',
|
||||||
|
nickname: selfInfo.nick,
|
||||||
|
}],
|
||||||
|
coverPath: thumbnailPath,
|
||||||
|
paths: fileListToUpload,
|
||||||
|
excludePaths: [],
|
||||||
|
expireLeftTime: 0,
|
||||||
|
isNeedDelDeviceInfo: false,
|
||||||
|
isNeedDelLocation: false,
|
||||||
|
coverOriginalInfos: [
|
||||||
|
{
|
||||||
|
path: fileListToUpload[0] || '',
|
||||||
|
thumbnailPath,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
uploadSceneType: UploadSceneType.KUPLOADSCENEAIOFILESELECTOR, // 不知道怎么枚举 先硬编码吧 (PC QQ 10)
|
||||||
|
detectPrivacyInfoResult: {
|
||||||
|
exists: false,
|
||||||
|
allDetectResults: new Map(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadResult = await flashService.createFlashTransferUploadTask(timestamp, fileUploadArg);
|
||||||
|
if (uploadResult.result === 0) {
|
||||||
|
this.context.logger.log('[Flash] 发起闪传任务成功');
|
||||||
|
return uploadResult;
|
||||||
|
} else {
|
||||||
|
this.context.logger.logError('[Flash] 发起闪传上传任务失败!!');
|
||||||
|
return uploadResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载闪传文件集
|
||||||
|
* @param fileSetId
|
||||||
|
*/
|
||||||
|
async downloadFileSetBySetId (fileSetId: string): Promise<GeneralCallResult & {
|
||||||
|
extraInfo: unknown;
|
||||||
|
}> {
|
||||||
|
const flashService = this.context.session.getFlashTransferService();
|
||||||
|
|
||||||
|
const result = await flashService.startFileSetDownload(fileSetId, 1, { isIncludeCompressInnerFiles: false }); // 为了方便,暂时硬编码
|
||||||
|
if (result.result === 0) {
|
||||||
|
this.context.logger.log('[Flash] 成功开始下载文件集');
|
||||||
|
} else {
|
||||||
|
this.context.logger.logError('[Flash] 尝试下载文件集失败!');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取闪传的外链分享
|
||||||
|
* @param fileSetId
|
||||||
|
*/
|
||||||
|
async getShareLinkBySetId (fileSetId: string): Promise<GeneralCallResult & {
|
||||||
|
shareLink: string;
|
||||||
|
expireTimestamp: string;
|
||||||
|
}> {
|
||||||
|
const flashService = this.context.session.getFlashTransferService();
|
||||||
|
|
||||||
|
const result = await flashService.getShareLinkReq(fileSetId);
|
||||||
|
if (result.result === 0) {
|
||||||
|
this.context.logger.log('[Flash] 获取闪传外链分享成功:', result.shareLink);
|
||||||
|
} else {
|
||||||
|
this.context.logger.logError('[Flash] 获取闪传外链失败!!');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从分享外链获取文件集id
|
||||||
|
* @param shareCode
|
||||||
|
*/
|
||||||
|
async fromShareLinkFindSetId (shareCode: string): Promise<GeneralCallResult & {
|
||||||
|
fileSetId: string;
|
||||||
|
}> {
|
||||||
|
const flashService = this.context.session.getFlashTransferService();
|
||||||
|
|
||||||
|
const result = await flashService.getFileSetIdByCode(shareCode);
|
||||||
|
if (result.result === 0) {
|
||||||
|
this.context.logger.log('[Flash] 获取shareCode的文件集Id成功!');
|
||||||
|
} else {
|
||||||
|
this.context.logger.logError('[Flash] 获取文件集ID失败!!');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取fileSet的文件结构信息 (未来可能需要深度遍历)
|
||||||
|
* == 注意返回结构和其它的不同,没有GeneralCallResult!!! ==
|
||||||
|
* @param fileSetId
|
||||||
|
*/
|
||||||
|
async getFileListBySetId (fileSetId: string): Promise<FileListResponse> {
|
||||||
|
const flashService = this.context.session.getFlashTransferService();
|
||||||
|
|
||||||
|
const requestArg = {
|
||||||
|
seq: 0,
|
||||||
|
fileSetId,
|
||||||
|
isUseCache: false,
|
||||||
|
sceneType: 1, // 硬编码
|
||||||
|
reqInfos: [
|
||||||
|
{
|
||||||
|
count: 18, // 18 ??
|
||||||
|
paginationInfo: {},
|
||||||
|
parentId: '',
|
||||||
|
reqIndexPath: '',
|
||||||
|
reqDepth: 1,
|
||||||
|
filterCondition: {
|
||||||
|
fileCategory: 0,
|
||||||
|
filterType: 0,
|
||||||
|
},
|
||||||
|
sortConditions: [
|
||||||
|
{
|
||||||
|
sortField: 0,
|
||||||
|
sortOrder: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isNeedPhysicalInfoReady: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const result = await flashService.getFileList(requestArg);
|
||||||
|
if (result.rsp.result === 0) {
|
||||||
|
this.context.logger.log('[Flash] 获取fileSet文件信息成功!');
|
||||||
|
return result.rsp;
|
||||||
|
} else {
|
||||||
|
this.context.logger.logError(`[Flash] 获取文件信息失败:ErrMsg: ${result.rsp.errMs}`);
|
||||||
|
return result.rsp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取闪传文件集合信息
|
||||||
|
* @param fileSetId
|
||||||
|
*/
|
||||||
|
async getFileSetIndoBySetId (fileSetId: string): Promise<GeneralCallResult & {
|
||||||
|
seq: number;
|
||||||
|
isCache: boolean;
|
||||||
|
fileSet: FlashFileSetInfo;
|
||||||
|
}> {
|
||||||
|
const flashService = this.context.session.getFlashTransferService();
|
||||||
|
|
||||||
|
const requestArg = {
|
||||||
|
fileSetId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await flashService.getFileSet(requestArg);
|
||||||
|
if (result.result === 0) {
|
||||||
|
this.context.logger.log('[Flash] 获取闪传文件集信息成功!');
|
||||||
|
} else {
|
||||||
|
this.context.logger.logError('[Flash] 获取闪传文件信息失败!!');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送闪传消息(私聊/群聊)
|
||||||
|
* @param fileSetId
|
||||||
|
* @param peer
|
||||||
|
*/
|
||||||
|
async sendFlashMessage (fileSetId: string, peer: Peer): Promise<{
|
||||||
|
errCode: number,
|
||||||
|
errMsg: string,
|
||||||
|
rsp: {
|
||||||
|
sendStatus: SendStatus[];
|
||||||
|
};
|
||||||
|
}> {
|
||||||
|
const flashService = this.context.session.getFlashTransferService();
|
||||||
|
|
||||||
|
const target = {
|
||||||
|
destUid: peer.peerUid,
|
||||||
|
destType: peer.chatType,
|
||||||
|
// destUin: peer.peerUin,
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestsArg = {
|
||||||
|
fileSetId,
|
||||||
|
targets: [target],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await flashService.sendFlashTransferMsg(requestsArg);
|
||||||
|
if (result.errCode === 0) {
|
||||||
|
this.context.logger.log('[Flash] 消息发送成功');
|
||||||
|
} else {
|
||||||
|
this.context.logger.logError(`[Flash] 消息发送失败!!原因:${result.errMsg}`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取闪传文件集中某个文件的下载URL(外链)
|
||||||
|
* @param fileSetId
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
async getFileTransUrl (fileSetId: string, options: { fileName?: string; fileIndex?: number; }): Promise<GeneralCallResult & {
|
||||||
|
transferUrl: string;
|
||||||
|
}> {
|
||||||
|
const flashService = this.context.session.getFlashTransferService();
|
||||||
|
const result = await this.getFileListBySetId(fileSetId);
|
||||||
|
|
||||||
|
const { fileName, fileIndex } = options;
|
||||||
|
|
||||||
|
let targetFile: any;
|
||||||
|
let file: any;
|
||||||
|
|
||||||
|
const allFolder = result.fileLists;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-labels
|
||||||
|
searchLoop: for (const folder of allFolder) {
|
||||||
|
const fileList = folder.fileList;
|
||||||
|
for (let i = 0; i < fileList.length; i++) {
|
||||||
|
file = fileList[i];
|
||||||
|
|
||||||
|
if (fileName !== undefined && file.name === fileName) {
|
||||||
|
targetFile = file;
|
||||||
|
// eslint-disable-next-line no-labels
|
||||||
|
break searchLoop;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileIndex !== undefined && i === fileIndex) {
|
||||||
|
targetFile = file;
|
||||||
|
// eslint-disable-next-line no-labels
|
||||||
|
break searchLoop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetFile === undefined) {
|
||||||
|
this.context.logger.logError('[Flash] 未找到对应文件!!');
|
||||||
|
return {
|
||||||
|
result: -1,
|
||||||
|
errMsg: '未找到对应文件',
|
||||||
|
transferUrl: '',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.context.logger.log('[Flash] 找到对应文件,准备尝试获取传输链接');
|
||||||
|
const res = await flashService.startFileTransferUrl(targetFile);
|
||||||
|
return {
|
||||||
|
result: 0,
|
||||||
|
errMsg: '',
|
||||||
|
transferUrl: res.url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createFileThumbnail (filePath: string): Promise<any> {
|
||||||
|
const msgService = this.context.session.getMsgService();
|
||||||
|
const savePath = msgService.getFileThumbSavePathForSend(750, true);
|
||||||
|
|
||||||
|
const result = await this.core.util.createThumbnailImage(
|
||||||
|
'flashtransfer',
|
||||||
|
filePath,
|
||||||
|
savePath,
|
||||||
|
{
|
||||||
|
width: 520,
|
||||||
|
height: 520,
|
||||||
|
},
|
||||||
|
'jpeg',
|
||||||
|
null
|
||||||
|
);
|
||||||
|
if (result.result === 0) {
|
||||||
|
this.context.logger.log('获取缩略图成功!!');
|
||||||
|
result.targetPath = savePath;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,7 +43,7 @@ export class NTQQFriendApi {
|
|||||||
return retMap;
|
return retMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
async delBuudy (uid: string, tempBlock = false, tempBothDel = false) {
|
async delBuddy (uid: string, tempBlock = false, tempBothDel = false) {
|
||||||
return this.context.session.getBuddyService().delBuddy({
|
return this.context.session.getBuddyService().delBuddy({
|
||||||
friendUid: uid,
|
friendUid: uid,
|
||||||
tempBlock,
|
tempBlock,
|
||||||
|
|||||||
@@ -7,3 +7,5 @@ export * from './webapi';
|
|||||||
export * from './system';
|
export * from './system';
|
||||||
export * from './packet';
|
export * from './packet';
|
||||||
export * from './file';
|
export * from './file';
|
||||||
|
export * from './online';
|
||||||
|
export * from './flash';
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ export class NTQQMsgApi {
|
|||||||
return this.context.session.getMsgService().getSourceOfReplyMsgV2(peer, clientSeq, time);
|
return this.context.session.getMsgService().getSourceOfReplyMsgV2(peer, clientSeq, time);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMsgEmojiLikesList (peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number = 20) {
|
async getMsgEmojiLikesList (peer: Peer, msgSeq: string, emojiId: string, emojiType: string, cookie: string = '', count: number = 20) {
|
||||||
// 注意此处emojiType 可选值一般为1-2 2好像是unicode表情dec值 大部分情况 Taged Mlikiowa
|
// 注意此处emojiType 可选值一般为1-2 2好像是unicode表情dec值 大部分情况 Taged Mlikiowa
|
||||||
return this.context.session.getMsgService().getMsgEmojiLikesList(peer, msgSeq, emojiId, emojiType, '', false, count);
|
return this.context.session.getMsgService().getMsgEmojiLikesList(peer, msgSeq, emojiId, emojiType, cookie, false, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setEmojiLike (peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) {
|
async setEmojiLike (peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) {
|
||||||
|
|||||||
241
packages/napcat-core/apis/online.ts
Normal file
241
packages/napcat-core/apis/online.ts
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
import { InstanceContext, NapCatCore } from '@/napcat-core';
|
||||||
|
import { Peer } from '@/napcat-core/types';
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import { GeneralCallResultStatus } from '@/napcat-core/services/common';
|
||||||
|
import { sleep } from '@/napcat-common/src/helper';
|
||||||
|
|
||||||
|
const normalizePath = (p: string) => path.normalize(p).toLowerCase();
|
||||||
|
|
||||||
|
export class NTQQOnlineApi {
|
||||||
|
context: InstanceContext;
|
||||||
|
core: NapCatCore;
|
||||||
|
|
||||||
|
constructor (context: InstanceContext, core: NapCatCore) {
|
||||||
|
this.context = context;
|
||||||
|
this.core = core;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 这里不等待node返回,因为the fuck wrapper.node 根本不返回(会卡死不知道为什么)!!! 只能手动查询判断死活
|
||||||
|
* @param peer
|
||||||
|
* @param filePath
|
||||||
|
* @param fileName
|
||||||
|
*/
|
||||||
|
async sendOnlineFile (peer: Peer, filePath: string, fileName: string): Promise<any> {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
throw new Error(`[NapCat] 文件不存在: ${filePath}`);
|
||||||
|
}
|
||||||
|
const actualFileName = fileName || path.basename(filePath);
|
||||||
|
const fileSize = fs.statSync(filePath).size.toString();
|
||||||
|
|
||||||
|
const fileElementToSend = [{
|
||||||
|
elementType: 23,
|
||||||
|
elementId: '',
|
||||||
|
fileElement: {
|
||||||
|
fileName: actualFileName,
|
||||||
|
filePath,
|
||||||
|
fileSize,
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
|
||||||
|
const msgService = this.context.session.getMsgService();
|
||||||
|
const startTime = Math.floor(Date.now() / 1000) - 2; // 容错时间窗口
|
||||||
|
|
||||||
|
msgService.sendMsg('0', peer, fileElementToSend, new Map()).catch((_e: any) => {
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxRetries = 10;
|
||||||
|
let retryCount = 0;
|
||||||
|
|
||||||
|
while (retryCount < maxRetries) {
|
||||||
|
await sleep(1000);
|
||||||
|
retryCount++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const msgListResult = await msgService.getOnlineFileMsgs(peer);
|
||||||
|
|
||||||
|
const msgs = msgListResult?.msgList || [];
|
||||||
|
|
||||||
|
const foundMsg = msgs.find((msg: any) => {
|
||||||
|
if (parseInt(msg.msgTime) < startTime) return false;
|
||||||
|
|
||||||
|
const validElement = msg.elements.find((el: any) => {
|
||||||
|
if (el.elementType !== 23 || !el.fileElement) return false;
|
||||||
|
|
||||||
|
const isNameMatch = el.fileElement.fileName === actualFileName;
|
||||||
|
const isPathMatch = normalizePath(el.fileElement.filePath) === normalizePath(filePath);
|
||||||
|
|
||||||
|
return isNameMatch && isPathMatch;
|
||||||
|
});
|
||||||
|
|
||||||
|
return !!validElement;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (foundMsg) {
|
||||||
|
const targetElement = foundMsg.elements.find((el: any) => el.elementType === 23);
|
||||||
|
this.context.logger.log('[OnlineFile] 在线文件发送成功!');
|
||||||
|
return {
|
||||||
|
result: GeneralCallResultStatus.OK,
|
||||||
|
errMsg: '',
|
||||||
|
msgId: foundMsg.msgId,
|
||||||
|
elementId: targetElement?.elementId || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (_e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.context.logger.logError('[OnlineFile] 在线文件发送失败!!!');
|
||||||
|
return {
|
||||||
|
result: GeneralCallResultStatus.ERROR,
|
||||||
|
errMsg: '[NapCat] Send Online File Timeout: Message not found in history.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送在线文件夹
|
||||||
|
* @param peer
|
||||||
|
* @param folderPath
|
||||||
|
* @param folderName
|
||||||
|
*/
|
||||||
|
async sendOnlineFolder (peer: Peer, folderPath: string, folderName?: string): Promise<any> {
|
||||||
|
const actualFolderName = folderName || path.basename(folderPath);
|
||||||
|
|
||||||
|
if (!fs.existsSync(folderPath)) {
|
||||||
|
return { result: GeneralCallResultStatus.ERROR, errMsg: `Folder not found: ${folderPath}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.statSync(folderPath).isDirectory()) {
|
||||||
|
return { result: GeneralCallResultStatus.ERROR, errMsg: `Path is not a directory: ${folderPath}` };
|
||||||
|
}
|
||||||
|
const folderElementItem = {
|
||||||
|
elementType: 30,
|
||||||
|
elementId: '',
|
||||||
|
fileElement: {
|
||||||
|
fileName: actualFolderName,
|
||||||
|
filePath: folderPath,
|
||||||
|
fileSize: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const msgService = this.context.session.getMsgService();
|
||||||
|
const startTime = Math.floor(Date.now() / 1000) - 2;
|
||||||
|
msgService.sendMsg('0', peer, [folderElementItem], new Map()).catch((_e: any) => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxRetries = 10;
|
||||||
|
let retryCount = 0;
|
||||||
|
|
||||||
|
while (retryCount < maxRetries) {
|
||||||
|
await sleep(1000);
|
||||||
|
retryCount++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const msgListResult = await msgService.getOnlineFileMsgs(peer);
|
||||||
|
const msgs = msgListResult?.msgList || [];
|
||||||
|
|
||||||
|
const foundMsg = msgs.find((msg: any) => {
|
||||||
|
if (parseInt(msg.msgTime) < startTime) return false;
|
||||||
|
|
||||||
|
const validElement = msg.elements.find((el: any) => {
|
||||||
|
if (el.elementType !== 30 || !el.fileElement) return false;
|
||||||
|
|
||||||
|
const isNameMatch = el.fileElement.fileName === actualFolderName;
|
||||||
|
const isPathMatch = normalizePath(el.fileElement.filePath) === normalizePath(folderPath);
|
||||||
|
|
||||||
|
return isNameMatch && isPathMatch;
|
||||||
|
});
|
||||||
|
return !!validElement;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (foundMsg) {
|
||||||
|
const targetElement = foundMsg.elements.find((el: any) => el.elementType === 30);
|
||||||
|
this.context.logger.log('[OnlineFile] 在线文件夹发送成功!');
|
||||||
|
return {
|
||||||
|
result: GeneralCallResultStatus.OK,
|
||||||
|
errMsg: '',
|
||||||
|
msgId: foundMsg.msgId,
|
||||||
|
elementId: targetElement?.elementId || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (_e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.context.logger.logError('[OnlineFile] 在线文件发送失败!!!');
|
||||||
|
return {
|
||||||
|
result: GeneralCallResultStatus.ERROR,
|
||||||
|
errMsg: '[NapCat] Send Online Folder Timeout: Message not found in history.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取好友的在线文件消息
|
||||||
|
* @param peer
|
||||||
|
*/
|
||||||
|
async getOnlineFileMsg (peer: Peer): Promise<any> {
|
||||||
|
const msgService = this.context.session.getMsgService();
|
||||||
|
return await msgService.getOnlineFileMsgs(peer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消在线文件的发送
|
||||||
|
* @param peer
|
||||||
|
* @param msgId
|
||||||
|
*/
|
||||||
|
async cancelMyOnlineFileMsg (peer: Peer, msgId: string): Promise<void> {
|
||||||
|
const msgService = this.context.session.getMsgService();
|
||||||
|
await msgService.cancelSendMsg(peer, msgId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拒绝接收在线文件
|
||||||
|
* @param peer
|
||||||
|
* @param msgId
|
||||||
|
* @param elementId
|
||||||
|
*/
|
||||||
|
async refuseOnlineFileMsg (peer: Peer, msgId: string, elementId: string): Promise<void> {
|
||||||
|
const msgService = this.context.session.getMsgService();
|
||||||
|
const arrToSend = {
|
||||||
|
msgId,
|
||||||
|
peerUid: peer.peerUid,
|
||||||
|
chatType: 1,
|
||||||
|
elementId,
|
||||||
|
downloadType: 1,
|
||||||
|
downSourceType: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
await msgService.refuseGetRichMediaElement(arrToSend);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收在线文件/文件夹
|
||||||
|
* @param peer
|
||||||
|
* @param msgId
|
||||||
|
* @param elementId
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
async receiveOnlineFileOrFolder (peer: Peer, msgId: string, elementId: string): Promise<any> {
|
||||||
|
const msgService = this.context.session.getMsgService();
|
||||||
|
const arrToSend = {
|
||||||
|
msgId,
|
||||||
|
peerUid: peer.peerUid,
|
||||||
|
chatType: 1,
|
||||||
|
elementId,
|
||||||
|
downSourceType: 1,
|
||||||
|
downloadType: 1,
|
||||||
|
};
|
||||||
|
return await msgService.getRichMediaElement(arrToSend);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线文件/文件夹转离线
|
||||||
|
* @param peer
|
||||||
|
* @param msgId
|
||||||
|
*/
|
||||||
|
async switchFileToOffline (peer: Peer, msgId: string): Promise<void> {
|
||||||
|
const msgService = this.context.session.getMsgService();
|
||||||
|
await msgService.switchToOfflineSendMsg(peer, msgId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,17 @@ import { createHash } from 'node:crypto';
|
|||||||
import { basename } from 'node:path';
|
import { basename } from 'node:path';
|
||||||
import { qunAlbumControl } from '../data/webapi';
|
import { qunAlbumControl } from '../data/webapi';
|
||||||
import { createAlbumCommentRequest, createAlbumFeedPublish, createAlbumMediaFeed } from '../data/album';
|
import { createAlbumCommentRequest, createAlbumFeedPublish, createAlbumMediaFeed } from '../data/album';
|
||||||
|
export interface SetNoticeRetSuccess {
|
||||||
|
ec: number;
|
||||||
|
em: string;
|
||||||
|
id: number;
|
||||||
|
ltsm: number;
|
||||||
|
new_fid: string;
|
||||||
|
read_only: number;
|
||||||
|
role: number;
|
||||||
|
srv_code: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class NTQQWebApi {
|
export class NTQQWebApi {
|
||||||
context: InstanceContext;
|
context: InstanceContext;
|
||||||
core: NapCatCore;
|
core: NapCatCore;
|
||||||
@@ -25,12 +36,12 @@ export class NTQQWebApi {
|
|||||||
async shareDigest (groupCode: string, msgSeq: string, msgRandom: string, targetGroupCode: string) {
|
async shareDigest (groupCode: string, msgSeq: string, msgRandom: string, targetGroupCode: string) {
|
||||||
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
||||||
const url = `https://qun.qq.com/cgi-bin/group_digest/share_digest?${new URLSearchParams({
|
const url = `https://qun.qq.com/cgi-bin/group_digest/share_digest?${new URLSearchParams({
|
||||||
bkn: this.getBknFromCookie(cookieObject),
|
bkn: this.getBknFromCookie(cookieObject),
|
||||||
group_code: groupCode,
|
group_code: groupCode,
|
||||||
msg_seq: msgSeq,
|
msg_seq: msgSeq,
|
||||||
msg_random: msgRandom,
|
msg_random: msgRandom,
|
||||||
target_group_code: targetGroupCode,
|
target_group_code: targetGroupCode,
|
||||||
}).toString()}`;
|
}).toString()}`;
|
||||||
try {
|
try {
|
||||||
return RequestUtil.HttpGetText(url, 'GET', '', { Cookie: this.cookieToString(cookieObject) });
|
return RequestUtil.HttpGetText(url, 'GET', '', { Cookie: this.cookieToString(cookieObject) });
|
||||||
} catch {
|
} catch {
|
||||||
@@ -52,11 +63,11 @@ export class NTQQWebApi {
|
|||||||
async getGroupEssenceMsg (GroupCode: string, page_start: number = 0, page_limit: number = 50) {
|
async getGroupEssenceMsg (GroupCode: string, page_start: number = 0, page_limit: number = 50) {
|
||||||
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
||||||
const url = `https://qun.qq.com/cgi-bin/group_digest/digest_list?${new URLSearchParams({
|
const url = `https://qun.qq.com/cgi-bin/group_digest/digest_list?${new URLSearchParams({
|
||||||
bkn: this.getBknFromCookie(cookieObject),
|
bkn: this.getBknFromCookie(cookieObject),
|
||||||
page_start: page_start.toString(),
|
page_start: page_start.toString(),
|
||||||
page_limit: page_limit.toString(),
|
page_limit: page_limit.toString(),
|
||||||
group_code: GroupCode,
|
group_code: GroupCode,
|
||||||
}).toString()}`;
|
}).toString()}`;
|
||||||
try {
|
try {
|
||||||
const ret = await RequestUtil.HttpGetJson<GroupEssenceMsgRet>(
|
const ret = await RequestUtil.HttpGetJson<GroupEssenceMsgRet>(
|
||||||
url,
|
url,
|
||||||
@@ -76,16 +87,16 @@ export class NTQQWebApi {
|
|||||||
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
||||||
const retList: Promise<WebApiGroupMemberRet>[] = [];
|
const retList: Promise<WebApiGroupMemberRet>[] = [];
|
||||||
const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>(
|
const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>(
|
||||||
`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
|
`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
|
||||||
st: '0',
|
st: '0',
|
||||||
end: '40',
|
end: '40',
|
||||||
sort: '1',
|
sort: '1',
|
||||||
gc: GroupCode,
|
gc: GroupCode,
|
||||||
bkn: this.getBknFromCookie(cookieObject),
|
bkn: this.getBknFromCookie(cookieObject),
|
||||||
}).toString()}`,
|
}).toString()}`,
|
||||||
'POST',
|
'POST',
|
||||||
'',
|
'',
|
||||||
{ Cookie: this.cookieToString(cookieObject) }
|
{ Cookie: this.cookieToString(cookieObject) }
|
||||||
);
|
);
|
||||||
if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
|
if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
|
||||||
return [];
|
return [];
|
||||||
@@ -101,16 +112,16 @@ export class NTQQWebApi {
|
|||||||
// 遍历批量请求
|
// 遍历批量请求
|
||||||
for (let i = 2; i <= PageNum; i++) {
|
for (let i = 2; i <= PageNum; i++) {
|
||||||
const ret = RequestUtil.HttpGetJson<WebApiGroupMemberRet>(
|
const ret = RequestUtil.HttpGetJson<WebApiGroupMemberRet>(
|
||||||
`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
|
`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
|
||||||
st: ((i - 1) * 40).toString(),
|
st: ((i - 1) * 40).toString(),
|
||||||
end: (i * 40).toString(),
|
end: (i * 40).toString(),
|
||||||
sort: '1',
|
sort: '1',
|
||||||
gc: GroupCode,
|
gc: GroupCode,
|
||||||
bkn: this.getBknFromCookie(cookieObject),
|
bkn: this.getBknFromCookie(cookieObject),
|
||||||
}).toString()}`,
|
}).toString()}`,
|
||||||
'POST',
|
'POST',
|
||||||
'',
|
'',
|
||||||
{ Cookie: this.cookieToString(cookieObject) }
|
{ Cookie: this.cookieToString(cookieObject) }
|
||||||
);
|
);
|
||||||
retList.push(ret);
|
retList.push(ret);
|
||||||
}
|
}
|
||||||
@@ -153,16 +164,7 @@ export class NTQQWebApi {
|
|||||||
imgWidth: number = 540,
|
imgWidth: number = 540,
|
||||||
imgHeight: number = 300
|
imgHeight: number = 300
|
||||||
) {
|
) {
|
||||||
interface SetNoticeRetSuccess {
|
|
||||||
ec: number;
|
|
||||||
em: string;
|
|
||||||
id: number;
|
|
||||||
ltsm: number;
|
|
||||||
new_fid: string;
|
|
||||||
read_only: number;
|
|
||||||
role: number;
|
|
||||||
srv_code: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
||||||
|
|
||||||
@@ -178,18 +180,18 @@ export class NTQQWebApi {
|
|||||||
imgHeight: imgHeight.toString(),
|
imgHeight: imgHeight.toString(),
|
||||||
};
|
};
|
||||||
const ret: SetNoticeRetSuccess = await RequestUtil.HttpGetJson<SetNoticeRetSuccess>(
|
const ret: SetNoticeRetSuccess = await RequestUtil.HttpGetJson<SetNoticeRetSuccess>(
|
||||||
`https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?${new URLSearchParams({
|
`https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?${new URLSearchParams({
|
||||||
bkn: this.getBknFromCookie(cookieObject),
|
bkn: this.getBknFromCookie(cookieObject),
|
||||||
qid: GroupCode,
|
qid: GroupCode,
|
||||||
text: Content,
|
text: Content,
|
||||||
pinned: pinned.toString(),
|
pinned: pinned.toString(),
|
||||||
type: type.toString(),
|
type: type.toString(),
|
||||||
settings,
|
settings,
|
||||||
...(picId === '' ? {} : externalParam),
|
...(picId === '' ? {} : externalParam),
|
||||||
}).toString()}`,
|
}).toString()}`,
|
||||||
'POST',
|
'POST',
|
||||||
'',
|
'',
|
||||||
{ Cookie: this.cookieToString(cookieObject) }
|
{ Cookie: this.cookieToString(cookieObject) }
|
||||||
);
|
);
|
||||||
return ret;
|
return ret;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -201,20 +203,20 @@ export class NTQQWebApi {
|
|||||||
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
||||||
try {
|
try {
|
||||||
const ret = await RequestUtil.HttpGetJson<WebApiGroupNoticeRet>(
|
const ret = await RequestUtil.HttpGetJson<WebApiGroupNoticeRet>(
|
||||||
`https://web.qun.qq.com/cgi-bin/announce/get_t_list?${new URLSearchParams({
|
`https://web.qun.qq.com/cgi-bin/announce/get_t_list?${new URLSearchParams({
|
||||||
bkn: this.getBknFromCookie(cookieObject),
|
bkn: this.getBknFromCookie(cookieObject),
|
||||||
qid: GroupCode,
|
qid: GroupCode,
|
||||||
ft: '23',
|
ft: '23',
|
||||||
ni: '1',
|
ni: '1',
|
||||||
n: '1',
|
n: '1',
|
||||||
i: '1',
|
i: '1',
|
||||||
log_read: '1',
|
log_read: '1',
|
||||||
platform: '1',
|
platform: '1',
|
||||||
s: '-1',
|
s: '-1',
|
||||||
}).toString()}&n=20`,
|
}).toString()}&n=20`,
|
||||||
'GET',
|
'GET',
|
||||||
'',
|
'',
|
||||||
{ Cookie: this.cookieToString(cookieObject) }
|
{ Cookie: this.cookieToString(cookieObject) }
|
||||||
);
|
);
|
||||||
return ret?.ec === 0 ? ret : undefined;
|
return ret?.ec === 0 ? ret : undefined;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -222,17 +224,17 @@ export class NTQQWebApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getDataInternal (cookieObject: { [key: string]: string }, groupCode: string, type: number) {
|
private async getDataInternal (cookieObject: { [key: string]: string; }, groupCode: string, type: number) {
|
||||||
let resJson;
|
let resJson;
|
||||||
try {
|
try {
|
||||||
const res = await RequestUtil.HttpGetText(
|
const res = await RequestUtil.HttpGetText(
|
||||||
`https://qun.qq.com/interactive/honorlist?${new URLSearchParams({
|
`https://qun.qq.com/interactive/honorlist?${new URLSearchParams({
|
||||||
gc: groupCode,
|
gc: groupCode,
|
||||||
type: type.toString(),
|
type: type.toString(),
|
||||||
}).toString()}`,
|
}).toString()}`,
|
||||||
'GET',
|
'GET',
|
||||||
'',
|
'',
|
||||||
{ Cookie: this.cookieToString(cookieObject) }
|
{ Cookie: this.cookieToString(cookieObject) }
|
||||||
);
|
);
|
||||||
const match = /window\.__INITIAL_STATE__=(.*?);/.exec(res);
|
const match = /window\.__INITIAL_STATE__=(.*?);/.exec(res);
|
||||||
if (match?.[1]) {
|
if (match?.[1]) {
|
||||||
@@ -245,7 +247,7 @@ export class NTQQWebApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getHonorList (cookieObject: { [key: string]: string }, groupCode: string, type: number) {
|
private async getHonorList (cookieObject: { [key: string]: string; }, groupCode: string, type: number) {
|
||||||
const data = await this.getDataInternal(cookieObject, groupCode, type);
|
const data = await this.getDataInternal(cookieObject, groupCode, type);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
this.context.logger.logError(`获取类型 ${type} 的荣誉信息失败`);
|
this.context.logger.logError(`获取类型 ${type} 的荣誉信息失败`);
|
||||||
@@ -304,11 +306,11 @@ export class NTQQWebApi {
|
|||||||
return HonorInfo;
|
return HonorInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private cookieToString (cookieObject: { [key: string]: string }) {
|
private cookieToString (cookieObject: { [key: string]: string; }) {
|
||||||
return Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ');
|
return Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ');
|
||||||
}
|
}
|
||||||
|
|
||||||
public getBknFromCookie (cookieObject: { [key: string]: string }) {
|
public getBknFromCookie (cookieObject: { [key: string]: string; }) {
|
||||||
const sKey = cookieObject['skey'] as string;
|
const sKey = cookieObject['skey'] as string;
|
||||||
|
|
||||||
let hash = 5381;
|
let hash = 5381;
|
||||||
@@ -361,7 +363,7 @@ export class NTQQWebApi {
|
|||||||
uin,
|
uin,
|
||||||
getMemberRole: '0',
|
getMemberRole: '0',
|
||||||
});
|
});
|
||||||
const response = await RequestUtil.HttpGetJson<{ data: { album: Array<{ id: string, title: string }> } }>(api + params.toString(), 'GET', '', {
|
const response = await RequestUtil.HttpGetJson<{ data: { album: Array<{ id: string, title: string; }>; }; }>(api + params.toString(), 'GET', '', {
|
||||||
Cookie: cookies,
|
Cookie: cookies,
|
||||||
});
|
});
|
||||||
return response.data.album;
|
return response.data.album;
|
||||||
@@ -384,7 +386,7 @@ export class NTQQWebApi {
|
|||||||
sAlbumID,
|
sAlbumID,
|
||||||
});
|
});
|
||||||
const api = `https://h5.qzone.qq.com/webapp/json/sliceUpload/FileBatchControl/${img_md5}?g_tk=${GTK}`;
|
const api = `https://h5.qzone.qq.com/webapp/json/sliceUpload/FileBatchControl/${img_md5}?g_tk=${GTK}`;
|
||||||
const post = await RequestUtil.HttpGetJson<{ data: { session: string }, ret: number, msg: string }>(api, 'POST', body, {
|
const post = await RequestUtil.HttpGetJson<{ data: { session: string; }, ret: number, msg: string; }>(api, 'POST', body, {
|
||||||
Cookie: cookie,
|
Cookie: cookie,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
});
|
});
|
||||||
@@ -430,7 +432,7 @@ export class NTQQWebApi {
|
|||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const post = await response.json() as { ret: number, msg: string }; if (post.ret !== 0) {
|
const post = await response.json() as { ret: number, msg: string; }; if (post.ret !== 0) {
|
||||||
throw new Error(`分片 ${seq} 上传失败: ${post.msg}`);
|
throw new Error(`分片 ${seq} 上传失败: ${post.msg}`);
|
||||||
}
|
}
|
||||||
offset += chunk.length;
|
offset += chunk.length;
|
||||||
@@ -475,10 +477,10 @@ export class NTQQWebApi {
|
|||||||
const client_key = Date.now() * 1000;
|
const client_key = Date.now() * 1000;
|
||||||
return await this.context.session.getAlbumService().doQunComment(
|
return await this.context.session.getAlbumService().doQunComment(
|
||||||
random_seq, {
|
random_seq, {
|
||||||
map_info: [],
|
map_info: [],
|
||||||
map_bytes_info: [],
|
map_bytes_info: [],
|
||||||
map_user_account: [],
|
map_user_account: [],
|
||||||
},
|
},
|
||||||
qunId,
|
qunId,
|
||||||
2,
|
2,
|
||||||
createAlbumMediaFeed(uin, albumId, lloc),
|
createAlbumMediaFeed(uin, albumId, lloc),
|
||||||
@@ -509,13 +511,13 @@ export class NTQQWebApi {
|
|||||||
const uin = this.core.selfInfo.uin || '10001';
|
const uin = this.core.selfInfo.uin || '10001';
|
||||||
return await this.context.session.getAlbumService().doQunLike(
|
return await this.context.session.getAlbumService().doQunLike(
|
||||||
random_seq, {
|
random_seq, {
|
||||||
map_info: [],
|
map_info: [],
|
||||||
map_bytes_info: [],
|
map_bytes_info: [],
|
||||||
map_user_account: [],
|
map_user_account: [],
|
||||||
}, {
|
}, {
|
||||||
id,
|
id,
|
||||||
status: 1,
|
status: 1,
|
||||||
},
|
},
|
||||||
createAlbumFeedPublish(qunId, uin, albumId, lloc)
|
createAlbumFeedPublish(qunId, uin, albumId, lloc)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
358
packages/napcat-core/data/flash.ts
Normal file
358
packages/napcat-core/data/flash.ts
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
export interface FlashBaseRequest {
|
||||||
|
fileSetId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploaderInfo {
|
||||||
|
uin: string,
|
||||||
|
nickname: string,
|
||||||
|
uid: string,
|
||||||
|
sendEntrance: string, // ""
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface thumbnailInfo {
|
||||||
|
id: string,
|
||||||
|
url: {
|
||||||
|
spec: number,
|
||||||
|
uri: string,
|
||||||
|
}[],
|
||||||
|
localCachePath: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SendTarget {
|
||||||
|
destType: number; // 1私聊
|
||||||
|
destUin?: string,
|
||||||
|
destUid: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SendTargetRequests {
|
||||||
|
fileSetId: string;
|
||||||
|
targets: SendTarget[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DownloadStatusInfo {
|
||||||
|
result: number; // 0
|
||||||
|
fileSetId: string;
|
||||||
|
status: number;
|
||||||
|
info: {
|
||||||
|
curDownLoadFailFileNum: number,
|
||||||
|
curDownLoadedPauseFileNum: number,
|
||||||
|
curDownLoadedFileNum: number,
|
||||||
|
curRealDownLoadedFileNum: number,
|
||||||
|
curDownloadingFileNum: number,
|
||||||
|
totalDownLoadedFileNum: number,
|
||||||
|
curDownLoadedBytes: string, // "0"
|
||||||
|
totalDownLoadedBytes: string,
|
||||||
|
curSpeedBps: number,
|
||||||
|
avgSpeedBps: number,
|
||||||
|
maxSpeedBps: number,
|
||||||
|
remainDownLoadSeconds: number,
|
||||||
|
failFileIdList: [],
|
||||||
|
allFileIdList: [],
|
||||||
|
hasNormalFileDownloading: boolean,
|
||||||
|
onlyCompressInnerFileDownloading: boolean,
|
||||||
|
isAllFileAlreadyDownloaded: boolean,
|
||||||
|
saveFileSetDir: string,
|
||||||
|
allWaitingStatusTask: boolean,
|
||||||
|
downloadSceneType: DownloadSceneType,
|
||||||
|
retryCount: number,
|
||||||
|
statisticInfo: {
|
||||||
|
downloadTaskId: string,
|
||||||
|
downloadFilesetName: string,
|
||||||
|
downloadFileTypeDistribution: string,
|
||||||
|
downloadFileSizeDistribution: string;
|
||||||
|
},
|
||||||
|
albumStorageFailImageNum: number,
|
||||||
|
albumStorageFailVideoNum: number,
|
||||||
|
albumStorageFailFileIdList: [],
|
||||||
|
albumStorageSucImageNum: number,
|
||||||
|
albumStorageSucVideoNum: number,
|
||||||
|
albumStorageSucFileIdList: [],
|
||||||
|
albumStorageFileNum: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface physicalInfo {
|
||||||
|
id: string,
|
||||||
|
url: string,
|
||||||
|
status: number, // 2 已下载
|
||||||
|
processing: string,
|
||||||
|
localPath: string,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
time: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface downloadInfo {
|
||||||
|
status: number,
|
||||||
|
curDownLoadBytes: string,
|
||||||
|
totalFileBytes: string,
|
||||||
|
errorCode: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface uploadInfo {
|
||||||
|
uploadedBytes: string,
|
||||||
|
errorCode: number,
|
||||||
|
svrRrrCode: number,
|
||||||
|
errMsg: string,
|
||||||
|
isNeedDelDeviceInfo: boolean,
|
||||||
|
thumbnailUploadState: number;
|
||||||
|
isSecondHit: boolean,
|
||||||
|
hasModifiedErr: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface folderUploadInfo {
|
||||||
|
totalUploadedFileSize: string;
|
||||||
|
successCount: number;
|
||||||
|
failedCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface folderDownloadInfo {
|
||||||
|
totalDownloadedFileSize: string;
|
||||||
|
totalFileSize: string;
|
||||||
|
totalDownloadFileCount: number;
|
||||||
|
successCount: number;
|
||||||
|
failedCount: number;
|
||||||
|
pausedCount: number;
|
||||||
|
cancelCount: number;
|
||||||
|
downloadingCount: number;
|
||||||
|
partialDownloadCount: number;
|
||||||
|
curLevelDownloadedFileCount: number;
|
||||||
|
curLevelUnDownloadedFileCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface compressFileFolderInfo {
|
||||||
|
downloadStatus: number;
|
||||||
|
saveFileDirPath: string;
|
||||||
|
totalFileCount: string;
|
||||||
|
totalFileSize: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface albumStorgeInfo {
|
||||||
|
status: number;
|
||||||
|
localIdentifier: string;
|
||||||
|
errorCode: number;
|
||||||
|
timeCost: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlashOneFileInfo {
|
||||||
|
fileSetId: string;
|
||||||
|
cliFileId: string; // client?? 或许可以换取url
|
||||||
|
compressedFileFolderId: string;
|
||||||
|
archiveIndex: 0;
|
||||||
|
indexPath: string;
|
||||||
|
isDir: boolean; // 文件或者文件夹!!
|
||||||
|
parentId: string;
|
||||||
|
depth: number; // 1
|
||||||
|
cliFileIndex: number;
|
||||||
|
fileType: number; // 枚举!! 已完成枚举!!
|
||||||
|
name: string;
|
||||||
|
namePinyin: string;
|
||||||
|
isCover: boolean;
|
||||||
|
isCoverOriginal: boolean;
|
||||||
|
fileSize: string;
|
||||||
|
fileCount: number;
|
||||||
|
thumbnail: thumbnailInfo;
|
||||||
|
physical: physicalInfo;
|
||||||
|
srvFileId: string; // service?? 服务器上面的id吗?
|
||||||
|
srvParentFileId: string;
|
||||||
|
svrLastUpdateTimestamp: string;
|
||||||
|
downloadInfo: downloadInfo;
|
||||||
|
saveFilePath: string;
|
||||||
|
search_relative_path: string;
|
||||||
|
disk_relative_path: string;
|
||||||
|
uploadInfo: uploadInfo;
|
||||||
|
status: number;
|
||||||
|
uploadStatus: number; // 3已上传成功
|
||||||
|
downloadStatus: number; // 0未下载
|
||||||
|
folderUploadInfo: folderUploadInfo;
|
||||||
|
folderDownloadInfo: folderDownloadInfo;
|
||||||
|
sha1: string;
|
||||||
|
bookmark: string;
|
||||||
|
compressFileFolderInfo: compressFileFolderInfo;
|
||||||
|
uploadPauseReason: string;
|
||||||
|
downloadPauseReason: string;
|
||||||
|
filePhysicalSize: string;
|
||||||
|
thumbnail_sha1: string | null;
|
||||||
|
thumbnail_size: string | null;
|
||||||
|
needAlbumStorage: boolean;
|
||||||
|
albumStorageInfo: albumStorgeInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface fileListsInfo {
|
||||||
|
parentId: string,
|
||||||
|
depth: number, // 1
|
||||||
|
fileList: FlashOneFileInfo[],
|
||||||
|
paginationInfo: {};
|
||||||
|
isEnd: boolean,
|
||||||
|
isCache: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileListResponse {
|
||||||
|
seq: number,
|
||||||
|
result: number,
|
||||||
|
errMs: string,
|
||||||
|
fileLists: fileListsInfo[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface createFlashTransferResult {
|
||||||
|
fileSetId: string,
|
||||||
|
shareLink: string,
|
||||||
|
expireTime: string,
|
||||||
|
expireLeftTime: string,
|
||||||
|
}
|
||||||
|
export enum UploadSceneType {
|
||||||
|
KUPLOADSCENEUNKNOWN,
|
||||||
|
KUPLOADSCENEFLOATWINDOWRIGHTCLICKMENU,
|
||||||
|
KUPLOADSCENEFLOATWINDOWDRAG,
|
||||||
|
KUPLOADSCENEFLOATWINDOWFILESELECTOR,
|
||||||
|
KUPLOADSCENEFLOATWINDOWSHORTCUTKEYCTRLCV,
|
||||||
|
KUPLOADSCENEH5LAUNCHCLIENTRIGHTCLICKMENU,
|
||||||
|
KUPLOADSCENEH5LAUNCHCLIENTDRAG,
|
||||||
|
KUPLOADSCENEH5LAUNCHCLIENTFILESELECTOR,
|
||||||
|
KUPLOADSCENEH5LAUNCHCLIENTSHORTCUTKEYCTRLCV,
|
||||||
|
KUPLOADSCENEAIODRAG,
|
||||||
|
KUPLOADSCENEAIOFILESELECTOR,
|
||||||
|
KUPLOADSCENEAIOSHORTCUTKEYCTRLCV
|
||||||
|
}
|
||||||
|
export interface StartFlashTaskRequests {
|
||||||
|
screen: number; // 1 PC-QQ
|
||||||
|
name?: string;
|
||||||
|
uploaders: UploaderInfo[];
|
||||||
|
permission?: {};
|
||||||
|
coverPath?: string;
|
||||||
|
paths: string[]; // 文件的绝对路径,可以是文件夹
|
||||||
|
excludePaths?: string[];
|
||||||
|
expireLeftTime?: number, // 0
|
||||||
|
isNeedDelDeviceInfo: boolean,
|
||||||
|
isNeedDelLocation: boolean,
|
||||||
|
coverOriginalInfos?: {
|
||||||
|
path: string,
|
||||||
|
thumbnailPath: string,
|
||||||
|
}[],
|
||||||
|
uploadSceneType: UploadSceneType, // 不知道怎么枚举 先硬编码吧 (PC QQ 10)
|
||||||
|
detectPrivacyInfoResult: {
|
||||||
|
exists: boolean,
|
||||||
|
allDetectResults: {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export enum BusiScene {
|
||||||
|
KBUSISCENEINVALID,
|
||||||
|
KBUSISCENEFLASHSCENE
|
||||||
|
}
|
||||||
|
export interface FileListInfoRequests {
|
||||||
|
seq: number, // 0
|
||||||
|
fileSetId: string,
|
||||||
|
isUseCache: boolean,
|
||||||
|
sceneType: BusiScene, // 1
|
||||||
|
reqInfos: {
|
||||||
|
count: number, // 18 ?? 硬编码吧 不懂
|
||||||
|
paginationInfo: {},
|
||||||
|
parentId: string,
|
||||||
|
reqIndexPath: string,
|
||||||
|
reqDepth: number, // 1
|
||||||
|
filterCondition: {
|
||||||
|
fileCategory: number,
|
||||||
|
filterType: number,
|
||||||
|
}, // 0
|
||||||
|
sortConditions: {
|
||||||
|
sortField: number,
|
||||||
|
sortOrder: number,
|
||||||
|
}[],
|
||||||
|
isNeedPhysicalInfoReady: boolean;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
export enum DownloadSceneType {
|
||||||
|
KDOWNLOADSCENEUNKNOWN,
|
||||||
|
KDOWNLOADSCENEARKC2C,
|
||||||
|
KDOWNLOADSCENEARKC2CDETAILPAGE,
|
||||||
|
KDOWNLOADSCENEARKGROUP,
|
||||||
|
KDOWNLOADSCENEARKGROUPDETAILPAGE,
|
||||||
|
KDOWNLOADSCENELINKC2C,
|
||||||
|
KDOWNLOADSCENELINKGROUP,
|
||||||
|
KDOWNLOADSCENELINKCHANNEL,
|
||||||
|
KDOWNLOADSCENELINKTEMPCHAT,
|
||||||
|
KDOWNLOADSCENELINKOTHERINQQ,
|
||||||
|
KDOWNLOADSCENESCANQRCODE,
|
||||||
|
KDWONLOADSCENEFLASHTRANSFERCENTERCLIENT,
|
||||||
|
KDWONLOADSCENEFLASHTRANSFERCENTERSCHEMA
|
||||||
|
}
|
||||||
|
export interface FlashFileSetInfo {
|
||||||
|
fileSetId: string,
|
||||||
|
name: string,
|
||||||
|
namePinyin: string,
|
||||||
|
totalFileCount: number,
|
||||||
|
totalFileSize: number,
|
||||||
|
permission: {},
|
||||||
|
shareInfo: {
|
||||||
|
shareLink: string,
|
||||||
|
extractionCode: string,
|
||||||
|
},
|
||||||
|
cover: {
|
||||||
|
id: string,
|
||||||
|
urls: [
|
||||||
|
{
|
||||||
|
spec: number, // 2
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
],
|
||||||
|
localCachePath: string;
|
||||||
|
},
|
||||||
|
uploaders: [
|
||||||
|
{
|
||||||
|
uin: string,
|
||||||
|
nickname: string,
|
||||||
|
uid: string,
|
||||||
|
sendEntrance: string;
|
||||||
|
}
|
||||||
|
],
|
||||||
|
expireLeftTime: number,
|
||||||
|
aiClusteringStatus: {
|
||||||
|
firstClusteringList: [],
|
||||||
|
shouldPull: boolean;
|
||||||
|
},
|
||||||
|
createTime: number,
|
||||||
|
expireTime: number,
|
||||||
|
firstLevelItemCount: 1,
|
||||||
|
svrLastUpdateTimestamp: 0,
|
||||||
|
taskId: string, // 同 fileSetId
|
||||||
|
uploadInfo: {
|
||||||
|
totalUploadedFileSize: number,
|
||||||
|
successCount: number,
|
||||||
|
failedCount: number;
|
||||||
|
},
|
||||||
|
downloadInfo: {
|
||||||
|
totalDownloadedFileSize: 0,
|
||||||
|
totalFileSize: 0,
|
||||||
|
totalDownloadFileCount: 0,
|
||||||
|
successCount: 0,
|
||||||
|
failedCount: 0,
|
||||||
|
pausedCount: 0,
|
||||||
|
cancelCount: 0,
|
||||||
|
status: 0,
|
||||||
|
curLevelDownloadedFileCount: number,
|
||||||
|
curLevelUnDownloadedFileCount: 0;
|
||||||
|
},
|
||||||
|
transferType: number,
|
||||||
|
isLocalCreate: true,
|
||||||
|
status: number, // todo 枚举全部状态
|
||||||
|
uploadStatus: number, // todo 同上
|
||||||
|
uploadPauseReason: 0,
|
||||||
|
downloadStatus: 0,
|
||||||
|
downloadPauseReason: 0,
|
||||||
|
saveFileSetDir: string,
|
||||||
|
uploadSceneType: UploadSceneType,
|
||||||
|
downloadSceneType: DownloadSceneType, // 0 PC-QQ 103 web
|
||||||
|
retryCount: number,
|
||||||
|
isMergeShareUpload: 0,
|
||||||
|
isRemoveDeviceInfo: boolean,
|
||||||
|
isRemoveLocation: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SendStatus {
|
||||||
|
result: number,
|
||||||
|
msg: string,
|
||||||
|
target: {
|
||||||
|
destType: number,
|
||||||
|
destUid: string,
|
||||||
|
};
|
||||||
|
}
|
||||||
8
packages/napcat-core/external/appid.json
vendored
8
packages/napcat-core/external/appid.json
vendored
@@ -518,5 +518,13 @@
|
|||||||
"9.9.26-44725": {
|
"9.9.26-44725": {
|
||||||
"appid": 537337569,
|
"appid": 537337569,
|
||||||
"qua": "V1_WIN_NQ_9.9.26_44725_GW_B"
|
"qua": "V1_WIN_NQ_9.9.26_44725_GW_B"
|
||||||
|
},
|
||||||
|
"9.9.27-45627": {
|
||||||
|
"appid": 537340060,
|
||||||
|
"qua": "V1_WIN_NQ_9.9.26_45627_GW_B"
|
||||||
|
},
|
||||||
|
"6.9.88-44725": {
|
||||||
|
"appid": 537337594,
|
||||||
|
"qua": "V1_MAC_NQ_6.9.88_44725_GW_B"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
12
packages/napcat-core/external/napi2native.json
vendored
12
packages/napcat-core/external/napi2native.json
vendored
@@ -154,5 +154,17 @@
|
|||||||
"9.9.26-44725-x64": {
|
"9.9.26-44725-x64": {
|
||||||
"send": "0A18D0C",
|
"send": "0A18D0C",
|
||||||
"recv": "1D4BF0D"
|
"recv": "1D4BF0D"
|
||||||
|
},
|
||||||
|
"9.9.27-45627-x64": {
|
||||||
|
"send": "0A697CC",
|
||||||
|
"recv": "1E86AC1"
|
||||||
|
},
|
||||||
|
"6.9.88-44725-x64": {
|
||||||
|
"send": "2756EF6",
|
||||||
|
"recv": "0A36152"
|
||||||
|
},
|
||||||
|
"6.9.88-44725-arm64": {
|
||||||
|
"send": "2313C68",
|
||||||
|
"recv": "09693E4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
12
packages/napcat-core/external/packet.json
vendored
12
packages/napcat-core/external/packet.json
vendored
@@ -662,5 +662,17 @@
|
|||||||
"9.9.26-44725-x64": {
|
"9.9.26-44725-x64": {
|
||||||
"send": "2CEBB20",
|
"send": "2CEBB20",
|
||||||
"recv": "2CEF0A0"
|
"recv": "2CEF0A0"
|
||||||
|
},
|
||||||
|
"9.9.27-45627-x64": {
|
||||||
|
"send": "2E59CC0",
|
||||||
|
"recv": "2E5D240"
|
||||||
|
},
|
||||||
|
"6.9.88-44725-x64": {
|
||||||
|
"send": "451FE90",
|
||||||
|
"recv": "4522A40"
|
||||||
|
},
|
||||||
|
"6.9.88-44725-arm64": {
|
||||||
|
"send": "3D79168",
|
||||||
|
"recv": "3D7BA78"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,14 +2,14 @@ import * as crypto from 'node:crypto';
|
|||||||
import { PacketMsg } from '@/napcat-core/packet/message/message';
|
import { PacketMsg } from '@/napcat-core/packet/message/message';
|
||||||
|
|
||||||
interface ForwardMsgJson {
|
interface ForwardMsgJson {
|
||||||
app: string
|
app: string;
|
||||||
config: ForwardMsgJsonConfig,
|
config: ForwardMsgJsonConfig,
|
||||||
desc: string,
|
desc: string,
|
||||||
extra: ForwardMsgJsonExtra,
|
extra: ForwardMsgJsonExtra,
|
||||||
meta: ForwardMsgJsonMeta,
|
meta: ForwardMsgJsonMeta,
|
||||||
prompt: string,
|
prompt: string,
|
||||||
ver: string,
|
ver: string,
|
||||||
view: string
|
view: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ForwardMsgJsonConfig {
|
interface ForwardMsgJsonConfig {
|
||||||
@@ -17,7 +17,7 @@ interface ForwardMsgJsonConfig {
|
|||||||
forward: number,
|
forward: number,
|
||||||
round: number,
|
round: number,
|
||||||
type: string,
|
type: string,
|
||||||
width: number
|
width: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ForwardMsgJsonExtra {
|
interface ForwardMsgJsonExtra {
|
||||||
@@ -26,17 +26,17 @@ interface ForwardMsgJsonExtra {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ForwardMsgJsonMeta {
|
interface ForwardMsgJsonMeta {
|
||||||
detail: ForwardMsgJsonMetaDetail
|
detail: ForwardMsgJsonMetaDetail;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ForwardMsgJsonMetaDetail {
|
interface ForwardMsgJsonMetaDetail {
|
||||||
news: {
|
news: {
|
||||||
text: string
|
text: string;
|
||||||
}[],
|
}[],
|
||||||
resid: string,
|
resid: string,
|
||||||
source: string,
|
source: string,
|
||||||
summary: string,
|
summary: string,
|
||||||
uniseq: string
|
uniseq: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ForwardAdaptMsg {
|
interface ForwardAdaptMsg {
|
||||||
@@ -50,8 +50,8 @@ interface ForwardAdaptMsgElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ForwardMsgBuilder {
|
export class ForwardMsgBuilder {
|
||||||
private static build (resId: string, msg: ForwardAdaptMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string): ForwardMsgJson {
|
private static build (resId: string, msg: ForwardAdaptMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string, uuid?: string): ForwardMsgJson {
|
||||||
const id = crypto.randomUUID();
|
const id = uuid ?? crypto.randomUUID();
|
||||||
const isGroupMsg = msg.some(m => m.isGroupMsg);
|
const isGroupMsg = msg.some(m => m.isGroupMsg);
|
||||||
if (!source) {
|
if (!source) {
|
||||||
source = msg.length === 0 ? '聊天记录' : (isGroupMsg ? '群聊的聊天记录' : msg.map(m => m.senderName).filter((v, i, a) => a.indexOf(v) === i).slice(0, 4).join('和') + '的聊天记录');
|
source = msg.length === 0 ? '聊天记录' : (isGroupMsg ? '群聊的聊天记录' : msg.map(m => m.senderName).filter((v, i, a) => a.indexOf(v) === i).slice(0, 4).join('和') + '的聊天记录');
|
||||||
@@ -104,13 +104,19 @@ export class ForwardMsgBuilder {
|
|||||||
return this.build(resId, []);
|
return this.build(resId, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromPacketMsg (resId: string, packetMsg: PacketMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string): ForwardMsgJson {
|
static fromPacketMsg (resId: string, packetMsg: PacketMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string, uuid?: string): ForwardMsgJson {
|
||||||
return this.build(resId, packetMsg.map(msg => ({
|
return this.build(resId, packetMsg.map(msg => ({
|
||||||
senderName: msg.senderName,
|
senderName: msg.senderName,
|
||||||
isGroupMsg: msg.groupId !== undefined,
|
isGroupMsg: msg.groupId !== undefined,
|
||||||
msg: msg.msg.map(m => ({
|
msg: msg.msg.map(m => ({
|
||||||
preview: m.valid ? m.toPreview() : '[该消息类型暂不支持查看]',
|
preview: m.valid ? m.toPreview() : '[该消息类型暂不支持查看]',
|
||||||
})),
|
})),
|
||||||
})), source, news, summary, prompt);
|
})),
|
||||||
|
source,
|
||||||
|
news,
|
||||||
|
summary,
|
||||||
|
prompt,
|
||||||
|
uuid,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import fs from 'node:fs/promises';
|
|||||||
import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/napcat-core/index';
|
import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/napcat-core/index';
|
||||||
import { ILogWrapper } from 'napcat-common/src/log-interface';
|
import { ILogWrapper } from 'napcat-common/src/log-interface';
|
||||||
import EventEmitter from 'node:events';
|
import EventEmitter from 'node:events';
|
||||||
|
|
||||||
export enum LogLevel {
|
export enum LogLevel {
|
||||||
DEBUG = 'debug',
|
DEBUG = 'debug',
|
||||||
INFO = 'info',
|
INFO = 'info',
|
||||||
@@ -263,7 +264,13 @@ function msgElementToText (element: MessageElement, msg: RawMessage, recursiveLe
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (element.fileElement) {
|
if (element.fileElement) {
|
||||||
return `[文件 ${element.fileElement.fileName}]`;
|
if (element.fileElement.fileUuid) {
|
||||||
|
return `[文件 ${element.fileElement.fileName}]`;
|
||||||
|
} else if (element.elementType === ElementType.TOFURECORD) {
|
||||||
|
return `[在线文件 ${element.fileElement.fileName}]`;
|
||||||
|
} else if (element.elementType === ElementType.ONLINEFOLDER) {
|
||||||
|
return `[在线文件夹 ${element.fileElement.fileName}/]`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element.videoElement) {
|
if (element.videoElement) {
|
||||||
@@ -287,7 +294,11 @@ function msgElementToText (element: MessageElement, msg: RawMessage, recursiveLe
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (element.markdownElement) {
|
if (element.markdownElement) {
|
||||||
return '[Markdown 消息]';
|
if (element.markdownElement?.mdSummary) {
|
||||||
|
return element.markdownElement.mdSummary;
|
||||||
|
} else {
|
||||||
|
return '[Markdown 消息]';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element.multiForwardMsgElement) {
|
if (element.multiForwardMsgElement) {
|
||||||
@@ -296,6 +307,8 @@ function msgElementToText (element: MessageElement, msg: RawMessage, recursiveLe
|
|||||||
|
|
||||||
if (element.elementType === ElementType.GreyTip) {
|
if (element.elementType === ElementType.GreyTip) {
|
||||||
return '[灰条消息]';
|
return '[灰条消息]';
|
||||||
|
} else if (element.elementType === ElementType.FILE) {
|
||||||
|
return '[文件发送中]';
|
||||||
}
|
}
|
||||||
|
|
||||||
return `[未实现 (ElementType = ${element.elementType})]`;
|
return `[未实现 (ElementType = ${element.elementType})]`;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import AppidTable from '@/napcat-core/external/appid.json';
|
|||||||
import { LogWrapper } from './log';
|
import { LogWrapper } from './log';
|
||||||
import { getMajorPath } from '@/napcat-core/index';
|
import { getMajorPath } from '@/napcat-core/index';
|
||||||
import { QQAppidTableType, QQPackageInfoType, QQVersionConfigType } from 'napcat-common/src/types';
|
import { QQAppidTableType, QQPackageInfoType, QQVersionConfigType } from 'napcat-common/src/types';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
export class QQBasicInfoWrapper {
|
export class QQBasicInfoWrapper {
|
||||||
QQMainPath: string | undefined;
|
QQMainPath: string | undefined;
|
||||||
@@ -21,6 +22,10 @@ export class QQBasicInfoWrapper {
|
|||||||
// 基础目录获取
|
// 基础目录获取
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.QQMainPath = process.execPath;
|
this.QQMainPath = process.execPath;
|
||||||
|
if (process.platform === 'darwin' && path.basename(this.QQMainPath) === 'QQ Helper') {
|
||||||
|
// 实用进程特殊处理 实用进程目录和QQ差远了
|
||||||
|
this.QQMainPath = path.resolve(path.dirname(this.QQMainPath), '../../../../', 'MacOS', 'QQ');
|
||||||
|
}
|
||||||
this.QQVersionConfigPath = getQQVersionConfigPath(this.QQMainPath);
|
this.QQVersionConfigPath = getQQVersionConfigPath(this.QQMainPath);
|
||||||
|
|
||||||
// 基础信息获取 无快更则启用默认模板填充
|
// 基础信息获取 无快更则启用默认模板填充
|
||||||
@@ -99,7 +104,10 @@ export class QQBasicInfoWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAppidV2ByMajor (QQVersion: string) {
|
getAppidV2ByMajor (QQVersion: string) {
|
||||||
const majorPath = getMajorPath(QQVersion);
|
if (!this.QQMainPath) {
|
||||||
|
throw new Error('QQMainPath未定义 无法通过Major获取Appid');
|
||||||
|
}
|
||||||
|
const majorPath = getMajorPath(QQVersion, this.QQMainPath);
|
||||||
const appid = parseAppidFromMajor(majorPath);
|
const appid = parseAppidFromMajor(majorPath);
|
||||||
return appid;
|
return appid;
|
||||||
}
|
}
|
||||||
|
|||||||
140
packages/napcat-core/helper/session-proxy.ts
Normal file
140
packages/napcat-core/helper/session-proxy.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { NodeIQQNTWrapperSession } from '@/napcat-core/wrapper';
|
||||||
|
import { ServiceNamingMapping } from '@/napcat-core/services/index';
|
||||||
|
import { NTEventWrapper } from './event';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Service 方法的代理
|
||||||
|
* 拦截所有方法调用,通过 EventWrapper 进行调用
|
||||||
|
*/
|
||||||
|
function createServiceMethodProxy<S extends keyof ServiceNamingMapping>(
|
||||||
|
serviceName: S,
|
||||||
|
originalService: ServiceNamingMapping[S],
|
||||||
|
eventWrapper: NTEventWrapper
|
||||||
|
): ServiceNamingMapping[S] {
|
||||||
|
return new Proxy(originalService as object, {
|
||||||
|
get(target, prop, receiver) {
|
||||||
|
const originalValue = Reflect.get(target, prop, receiver);
|
||||||
|
|
||||||
|
// 如果不是函数,直接返回原始值
|
||||||
|
if (typeof originalValue !== 'function') {
|
||||||
|
return originalValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const methodName = prop as string;
|
||||||
|
|
||||||
|
// 返回一个包装函数,通过 EventWrapper 调用
|
||||||
|
return function (this: unknown, ...args: unknown[]) {
|
||||||
|
// 构造 EventWrapper 需要的路径格式: ServiceName/MethodName
|
||||||
|
const eventPath = `${serviceName}/${methodName}`;
|
||||||
|
|
||||||
|
// 尝试通过 EventWrapper 调用
|
||||||
|
try {
|
||||||
|
// 使用 callNoListenerEvent 的底层实现逻辑
|
||||||
|
const eventFunc = (eventWrapper as any).createEventFunction(eventPath);
|
||||||
|
if (eventFunc) {
|
||||||
|
return eventFunc(...args);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 如果 EventWrapper 调用失败,回退到原始调用
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回退到原始方法调用
|
||||||
|
return originalValue.apply(originalService, args);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}) as ServiceNamingMapping[S];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Session 的双层代理
|
||||||
|
* 第一层:拦截 getXXXService 方法
|
||||||
|
* 第二层:拦截 Service 上的具体方法调用
|
||||||
|
*/
|
||||||
|
export function createSessionProxy(
|
||||||
|
session: NodeIQQNTWrapperSession,
|
||||||
|
eventWrapper: NTEventWrapper
|
||||||
|
): NodeIQQNTWrapperSession {
|
||||||
|
// 缓存已代理的 Service,避免重复创建
|
||||||
|
const serviceProxyCache = new Map<string, unknown>();
|
||||||
|
|
||||||
|
return new Proxy(session, {
|
||||||
|
get(target, prop, receiver) {
|
||||||
|
const propName = prop as string;
|
||||||
|
|
||||||
|
// 检查是否是 getXXXService 方法
|
||||||
|
if (typeof propName === 'string' && propName.startsWith('get') && propName.endsWith('Service')) {
|
||||||
|
// 提取 Service 名称: getMsgService -> NodeIKernelMsgService
|
||||||
|
const servicePart = propName.slice(3); // 移除 'get' 前缀
|
||||||
|
const serviceName = `NodeIKernel${servicePart}` as keyof ServiceNamingMapping;
|
||||||
|
|
||||||
|
// 返回一个函数,该函数返回代理后的 Service
|
||||||
|
return function () {
|
||||||
|
// 检查缓存
|
||||||
|
if (serviceProxyCache.has(serviceName)) {
|
||||||
|
return serviceProxyCache.get(serviceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取原始 Service
|
||||||
|
const originalGetter = Reflect.get(target, prop, receiver) as () => unknown;
|
||||||
|
const originalService = originalGetter.call(target);
|
||||||
|
|
||||||
|
// 检查是否在 ServiceNamingMapping 中定义
|
||||||
|
if (isKnownService(serviceName)) {
|
||||||
|
// 创建 Service 方法代理
|
||||||
|
const proxiedService = createServiceMethodProxy(
|
||||||
|
serviceName,
|
||||||
|
originalService as ServiceNamingMapping[typeof serviceName],
|
||||||
|
eventWrapper
|
||||||
|
);
|
||||||
|
serviceProxyCache.set(serviceName, proxiedService);
|
||||||
|
return proxiedService;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未知的 Service,直接返回原始对象
|
||||||
|
serviceProxyCache.set(serviceName, originalService);
|
||||||
|
return originalService;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非 getXXXService 方法,直接返回原始值
|
||||||
|
return Reflect.get(target, prop, receiver);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查 Service 名称是否在已知的映射中
|
||||||
|
*/
|
||||||
|
function isKnownService(serviceName: string): serviceName is keyof ServiceNamingMapping {
|
||||||
|
const knownServices: string[] = [
|
||||||
|
'NodeIKernelAvatarService',
|
||||||
|
'NodeIKernelBuddyService',
|
||||||
|
'NodeIKernelFileAssistantService',
|
||||||
|
'NodeIKernelGroupService',
|
||||||
|
'NodeIKernelLoginService',
|
||||||
|
'NodeIKernelMsgService',
|
||||||
|
'NodeIKernelOnlineStatusService',
|
||||||
|
'NodeIKernelProfileLikeService',
|
||||||
|
'NodeIKernelProfileService',
|
||||||
|
'NodeIKernelTicketService',
|
||||||
|
'NodeIKernelStorageCleanService',
|
||||||
|
'NodeIKernelRobotService',
|
||||||
|
'NodeIKernelRichMediaService',
|
||||||
|
'NodeIKernelDbToolsService',
|
||||||
|
'NodeIKernelTipOffService',
|
||||||
|
'NodeIKernelSearchService',
|
||||||
|
'NodeIKernelCollectionService',
|
||||||
|
];
|
||||||
|
return knownServices.includes(serviceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建带有 EventWrapper 集成的 InstanceContext
|
||||||
|
* 这是推荐的使用方式,在创建 context 时自动代理 session
|
||||||
|
*/
|
||||||
|
export function createProxiedSession(
|
||||||
|
session: NodeIQQNTWrapperSession,
|
||||||
|
eventWrapper: NTEventWrapper
|
||||||
|
): NodeIQQNTWrapperSession {
|
||||||
|
return createSessionProxy(session, eventWrapper);
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
NTQQSystemApi,
|
NTQQSystemApi,
|
||||||
NTQQUserApi,
|
NTQQUserApi,
|
||||||
NTQQWebApi,
|
NTQQWebApi,
|
||||||
|
NTQQFlashApi,
|
||||||
|
NTQQOnlineApi,
|
||||||
} from '@/napcat-core/apis';
|
} from '@/napcat-core/apis';
|
||||||
import { NTQQCollectionApi } from '@/napcat-core/apis/collection';
|
import { NTQQCollectionApi } from '@/napcat-core/apis/collection';
|
||||||
import {
|
import {
|
||||||
@@ -23,7 +25,8 @@ import path from 'node:path';
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { hostname, systemName, systemVersion } from 'napcat-common/src/system';
|
import { hostname, systemName, systemVersion } from 'napcat-common/src/system';
|
||||||
import { NTEventWrapper } from '@/napcat-core/helper/event';
|
import { NTEventWrapper } from '@/napcat-core/helper/event';
|
||||||
import { KickedOffLineInfo, SelfInfo, SelfStatusInfo } from '@/napcat-core/types';
|
import { createSessionProxy } from '@/napcat-core/helper/session-proxy';
|
||||||
|
import { KickedOffLineInfo, RawMessage, SelfInfo, SelfStatusInfo } from '@/napcat-core/types';
|
||||||
import { NapCatConfigLoader, NapcatConfigSchema } from '@/napcat-core/helper/config';
|
import { NapCatConfigLoader, NapcatConfigSchema } from '@/napcat-core/helper/config';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/napcat-core/listeners';
|
import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/napcat-core/listeners';
|
||||||
@@ -37,6 +40,14 @@ export * from './wrapper';
|
|||||||
export * from './types/index';
|
export * from './types/index';
|
||||||
export * from './services/index';
|
export * from './services/index';
|
||||||
export * from './listeners/index';
|
export * from './listeners/index';
|
||||||
|
export * from './apis/index';
|
||||||
|
export * from './helper/log';
|
||||||
|
export * from './helper/qq-basic-info';
|
||||||
|
export * from './helper/event';
|
||||||
|
export * from './helper/config';
|
||||||
|
export * from './helper/config-base';
|
||||||
|
export * from './helper/proxy-handler';
|
||||||
|
export * from './helper/session-proxy';
|
||||||
|
|
||||||
export enum NapCatCoreWorkingEnv {
|
export enum NapCatCoreWorkingEnv {
|
||||||
Unknown = 0,
|
Unknown = 0,
|
||||||
@@ -44,20 +55,23 @@ export enum NapCatCoreWorkingEnv {
|
|||||||
Framework = 2,
|
Framework = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadQQWrapper (QQVersion: string): WrapperNodeApi {
|
export function loadQQWrapper (execPath: string | undefined, QQVersion: string): WrapperNodeApi {
|
||||||
if (process.env['NAPCAT_WRAPPER_PATH']) {
|
if (process.env['NAPCAT_WRAPPER_PATH']) {
|
||||||
const wrapperPath = process.env['NAPCAT_WRAPPER_PATH'];
|
const wrapperPath = process.env['NAPCAT_WRAPPER_PATH'];
|
||||||
const nativemodule: { exports: WrapperNodeApi; } = { exports: {} as WrapperNodeApi };
|
const nativemodule: { exports: WrapperNodeApi; } = { exports: {} as WrapperNodeApi };
|
||||||
process.dlopen(nativemodule, wrapperPath);
|
process.dlopen(nativemodule, wrapperPath);
|
||||||
return nativemodule.exports;
|
return nativemodule.exports;
|
||||||
}
|
}
|
||||||
|
if (!execPath) {
|
||||||
|
throw new Error('无法加载Wrapper,execPath未定义');
|
||||||
|
}
|
||||||
let appPath;
|
let appPath;
|
||||||
if (os.platform() === 'darwin') {
|
if (os.platform() === 'darwin') {
|
||||||
appPath = path.resolve(path.dirname(process.execPath), '../Resources/app');
|
appPath = path.resolve(path.dirname(execPath), '../Resources/app');
|
||||||
} else if (os.platform() === 'linux') {
|
} else if (os.platform() === 'linux') {
|
||||||
appPath = path.resolve(path.dirname(process.execPath), './resources/app');
|
appPath = path.resolve(path.dirname(execPath), './resources/app');
|
||||||
} else {
|
} else {
|
||||||
appPath = path.resolve(path.dirname(process.execPath), `./versions/${QQVersion}/`);
|
appPath = path.resolve(path.dirname(execPath), `./versions/${QQVersion}/`);
|
||||||
}
|
}
|
||||||
let wrapperNodePath = path.resolve(appPath, 'wrapper.node');
|
let wrapperNodePath = path.resolve(appPath, 'wrapper.node');
|
||||||
if (!fs.existsSync(wrapperNodePath)) {
|
if (!fs.existsSync(wrapperNodePath)) {
|
||||||
@@ -65,21 +79,22 @@ export function loadQQWrapper (QQVersion: string): WrapperNodeApi {
|
|||||||
}
|
}
|
||||||
// 老版本兼容 未来去掉
|
// 老版本兼容 未来去掉
|
||||||
if (!fs.existsSync(wrapperNodePath)) {
|
if (!fs.existsSync(wrapperNodePath)) {
|
||||||
wrapperNodePath = path.join(path.dirname(process.execPath), `./resources/app/versions/${QQVersion}/wrapper.node`);
|
wrapperNodePath = path.join(path.dirname(execPath), `./resources/app/versions/${QQVersion}/wrapper.node`);
|
||||||
}
|
}
|
||||||
const nativemodule: { exports: WrapperNodeApi; } = { exports: {} as WrapperNodeApi };
|
const nativemodule: { exports: WrapperNodeApi; } = { exports: {} as WrapperNodeApi };
|
||||||
process.dlopen(nativemodule, wrapperNodePath);
|
process.dlopen(nativemodule, wrapperNodePath);
|
||||||
|
process.env['NAPCAT_WRAPPER_PATH'] = wrapperNodePath;
|
||||||
return nativemodule.exports;
|
return nativemodule.exports;
|
||||||
}
|
}
|
||||||
export function getMajorPath (QQVersion: string): string {
|
export function getMajorPath (execPath: string, QQVersion: string): string {
|
||||||
// major.node
|
// major.node
|
||||||
let appPath;
|
let appPath;
|
||||||
if (os.platform() === 'darwin') {
|
if (os.platform() === 'darwin') {
|
||||||
appPath = path.resolve(path.dirname(process.execPath), '../Resources/app');
|
appPath = path.resolve(path.dirname(execPath), '../Resources/app');
|
||||||
} else if (os.platform() === 'linux') {
|
} else if (os.platform() === 'linux') {
|
||||||
appPath = path.resolve(path.dirname(process.execPath), './resources/app');
|
appPath = path.resolve(path.dirname(execPath), './resources/app');
|
||||||
} else {
|
} else {
|
||||||
appPath = path.resolve(path.dirname(process.execPath), `./versions/${QQVersion}/`);
|
appPath = path.resolve(path.dirname(execPath), `./versions/${QQVersion}/`);
|
||||||
}
|
}
|
||||||
let majorPath = path.resolve(appPath, 'major.node');
|
let majorPath = path.resolve(appPath, 'major.node');
|
||||||
if (!fs.existsSync(majorPath)) {
|
if (!fs.existsSync(majorPath)) {
|
||||||
@@ -87,7 +102,7 @@ export function getMajorPath (QQVersion: string): string {
|
|||||||
}
|
}
|
||||||
// 老版本兼容 未来去掉
|
// 老版本兼容 未来去掉
|
||||||
if (!fs.existsSync(majorPath)) {
|
if (!fs.existsSync(majorPath)) {
|
||||||
majorPath = path.join(path.dirname(process.execPath), `./resources/app/versions/${QQVersion}/major.node`);
|
majorPath = path.join(path.dirname(execPath), `./resources/app/versions/${QQVersion}/major.node`);
|
||||||
}
|
}
|
||||||
return majorPath;
|
return majorPath;
|
||||||
}
|
}
|
||||||
@@ -106,9 +121,19 @@ export class NapCatCore {
|
|||||||
// 通过构造器递过去的 runtime info 应该尽量少
|
// 通过构造器递过去的 runtime info 应该尽量少
|
||||||
constructor (context: InstanceContext, selfInfo: SelfInfo) {
|
constructor (context: InstanceContext, selfInfo: SelfInfo) {
|
||||||
this.selfInfo = selfInfo;
|
this.selfInfo = selfInfo;
|
||||||
this.context = context;
|
// 先用原始 session 创建 eventWrapper
|
||||||
this.util = this.context.wrapper.NodeQQNTWrapperUtil;
|
|
||||||
this.eventWrapper = new NTEventWrapper(context.session);
|
this.eventWrapper = new NTEventWrapper(context.session);
|
||||||
|
// 通过环境变量 NAPCAT_SESSION_PROXY 开启 session 代理
|
||||||
|
if (process.env['NAPCAT_SESSION_PROXY'] === '1') {
|
||||||
|
const proxiedSession = createSessionProxy(context.session, this.eventWrapper);
|
||||||
|
this.context = {
|
||||||
|
...context,
|
||||||
|
session: proxiedSession,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
this.util = this.context.wrapper.NodeQQNTWrapperUtil;
|
||||||
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath, NapcatConfigSchema);
|
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath, NapcatConfigSchema);
|
||||||
this.apis = {
|
this.apis = {
|
||||||
FileApi: new NTQQFileApi(this.context, this),
|
FileApi: new NTQQFileApi(this.context, this),
|
||||||
@@ -120,6 +145,8 @@ export class NapCatCore {
|
|||||||
MsgApi: new NTQQMsgApi(this.context, this),
|
MsgApi: new NTQQMsgApi(this.context, this),
|
||||||
UserApi: new NTQQUserApi(this.context, this),
|
UserApi: new NTQQUserApi(this.context, this),
|
||||||
GroupApi: new NTQQGroupApi(this.context, this),
|
GroupApi: new NTQQGroupApi(this.context, this),
|
||||||
|
FlashApi: new NTQQFlashApi(this.context, this),
|
||||||
|
OnlineApi: new NTQQOnlineApi(this.context, this),
|
||||||
};
|
};
|
||||||
container.bind(NapCatCore).toConstantValue(this);
|
container.bind(NapCatCore).toConstantValue(this);
|
||||||
container.bind(TypedEventEmitter).toConstantValue(this.event);
|
container.bind(TypedEventEmitter).toConstantValue(this.event);
|
||||||
@@ -175,6 +202,11 @@ export class NapCatCore {
|
|||||||
async initNapCatCoreListeners () {
|
async initNapCatCoreListeners () {
|
||||||
const msgListener = new NodeIKernelMsgListener();
|
const msgListener = new NodeIKernelMsgListener();
|
||||||
|
|
||||||
|
// 在线文件/文件夹消息
|
||||||
|
msgListener.onRecvOnlineFileMsg = (msgs: RawMessage[]) => {
|
||||||
|
msgs.forEach(msg => this.context.logger.logMessage(msg, this.selfInfo));
|
||||||
|
};
|
||||||
|
|
||||||
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
|
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
|
||||||
// 下线通知
|
// 下线通知
|
||||||
const tips = `[KickedOffLine] [${Info.tipsTitle}] ${Info.tipsDesc}`;
|
const tips = `[KickedOffLine] [${Info.tipsTitle}] ${Info.tipsDesc}`;
|
||||||
@@ -294,4 +326,6 @@ export interface StableNTApiWrapper {
|
|||||||
MsgApi: NTQQMsgApi,
|
MsgApi: NTQQMsgApi,
|
||||||
UserApi: NTQQUserApi,
|
UserApi: NTQQUserApi,
|
||||||
GroupApi: NTQQGroupApi;
|
GroupApi: NTQQGroupApi;
|
||||||
|
FlashApi: NTQQFlashApi,
|
||||||
|
OnlineApi: NTQQOnlineApi,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { OidbPacket } from '@/napcat-core/packet/transformer/base';
|
|||||||
import { ImageOcrResult } from '@/napcat-core/packet/entities/ocrResult';
|
import { ImageOcrResult } from '@/napcat-core/packet/entities/ocrResult';
|
||||||
import { gunzipSync } from 'zlib';
|
import { gunzipSync } from 'zlib';
|
||||||
import { PacketMsgConverter } from '@/napcat-core/packet/message/converter';
|
import { PacketMsgConverter } from '@/napcat-core/packet/message/converter';
|
||||||
|
import { UploadForwardMsgParams } from '@/napcat-core/packet/transformer/message/UploadForwardMsgV2';
|
||||||
|
|
||||||
export class PacketOperationContext {
|
export class PacketOperationContext {
|
||||||
private readonly context: PacketContext;
|
private readonly context: PacketContext;
|
||||||
@@ -26,7 +27,7 @@ export class PacketOperationContext {
|
|||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendPacket<T extends boolean = false>(pkt: OidbPacket, rsp?: T): Promise<T extends true ? Buffer : void> {
|
async sendPacket<T extends boolean = false> (pkt: OidbPacket, rsp?: T): Promise<T extends true ? Buffer : void> {
|
||||||
return await this.context.client.sendOidbPacket(pkt, rsp);
|
return await this.context.client.sendOidbPacket(pkt, rsp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,12 +95,15 @@ export class PacketOperationContext {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
);
|
);
|
||||||
const res = await Promise.allSettled(reqList);
|
const res = await Promise.allSettled(reqList);
|
||||||
this.context.logger.info(`上传资源${res.length}个,失败${res.filter((r) => r.status === 'rejected').length}个`);
|
const failedCount = res.filter((r) => r.status === 'rejected').length;
|
||||||
res.forEach((result, index) => {
|
if (failedCount > 0) {
|
||||||
if (result.status === 'rejected') {
|
this.context.logger.warn(`上传资源${res.length}个,失败${failedCount}个`);
|
||||||
this.context.logger.error(`上传第${index + 1}个资源失败:${result.reason.stack}`);
|
res.forEach((result, index) => {
|
||||||
}
|
if (result.status === 'rejected') {
|
||||||
});
|
this.context.logger.error(`上传第${index + 1}个资源失败:${result.reason.stack}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async UploadImage (img: PacketMsgPicElement) {
|
async UploadImage (img: PacketMsgPicElement) {
|
||||||
@@ -224,7 +228,15 @@ export class PacketOperationContext {
|
|||||||
const res = trans.UploadForwardMsg.parse(resp);
|
const res = trans.UploadForwardMsg.parse(resp);
|
||||||
return res.result.resId;
|
return res.result.resId;
|
||||||
}
|
}
|
||||||
|
async UploadForwardMsgV2 (msg: UploadForwardMsgParams[], groupUin: number = 0) {
|
||||||
|
//await this.SendPreprocess(msg, groupUin);
|
||||||
|
// 遍历上传资源
|
||||||
|
await Promise.allSettled(msg.map(async (item) => { return await this.SendPreprocess(item.actionMsg, groupUin); }));
|
||||||
|
const req = trans.UploadForwardMsgV2.build(this.context.napcore.basicInfo.uid, msg, groupUin);
|
||||||
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||||
|
const res = trans.UploadForwardMsg.parse(resp);
|
||||||
|
return res.result.resId;
|
||||||
|
}
|
||||||
async MoveGroupFile (
|
async MoveGroupFile (
|
||||||
groupUin: number,
|
groupUin: number,
|
||||||
fileUUID: string,
|
fileUUID: string,
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import zlib from 'node:zlib';
|
||||||
|
import * as proto from '@/napcat-core/packet/transformer/proto';
|
||||||
|
import { NapProtoMsg } from 'napcat-protobuf';
|
||||||
|
import { OidbPacket, PacketBufBuilder, PacketTransformer } from '@/napcat-core/packet/transformer/base';
|
||||||
|
import { PacketMsg } from '@/napcat-core/packet/message/message';
|
||||||
|
|
||||||
|
export interface UploadForwardMsgParams {
|
||||||
|
actionCommand: string;
|
||||||
|
actionMsg: PacketMsg[];
|
||||||
|
}
|
||||||
|
class UploadForwardMsgV2 extends PacketTransformer<typeof proto.SendLongMsgResp> {
|
||||||
|
build (selfUid: string, msg: UploadForwardMsgParams[], groupUin: number = 0): OidbPacket {
|
||||||
|
const reqdata = msg.map((item) => ({
|
||||||
|
actionCommand: item.actionCommand,
|
||||||
|
actionData: {
|
||||||
|
msgBody: this.msgBuilder.buildFakeMsg(selfUid, item.actionMsg),
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
const longMsgResultData = new NapProtoMsg(proto.LongMsgResult).encode(
|
||||||
|
{
|
||||||
|
action: reqdata,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const payload = zlib.gzipSync(Buffer.from(longMsgResultData));
|
||||||
|
const req = new NapProtoMsg(proto.SendLongMsgReq).encode(
|
||||||
|
{
|
||||||
|
info: {
|
||||||
|
type: groupUin === 0 ? 1 : 3,
|
||||||
|
uid: {
|
||||||
|
uid: groupUin === 0 ? selfUid : groupUin.toString(),
|
||||||
|
},
|
||||||
|
groupUin,
|
||||||
|
payload,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
field1: 4, field2: 1, field3: 7, field4: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
cmd: 'trpc.group.long_msg_interface.MsgService.SsoSendLongMsg',
|
||||||
|
data: PacketBufBuilder(req),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
parse (data: Buffer) {
|
||||||
|
return new NapProtoMsg(proto.SendLongMsgResp).decode(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new UploadForwardMsgV2();
|
||||||
@@ -2,3 +2,4 @@ export { default as UploadForwardMsg } from './UploadForwardMsg';
|
|||||||
export { default as FetchGroupMessage } from './FetchGroupMessage';
|
export { default as FetchGroupMessage } from './FetchGroupMessage';
|
||||||
export { default as FetchC2CMessage } from './FetchC2CMessage';
|
export { default as FetchC2CMessage } from './FetchC2CMessage';
|
||||||
export { default as DownloadForwardMsg } from './DownloadForwardMsg';
|
export { default as DownloadForwardMsg } from './DownloadForwardMsg';
|
||||||
|
export { default as UploadForwardMsgV2 } from './UploadForwardMsgV2';
|
||||||
303
packages/napcat-core/services/NodeIKernelFlashTransferService.ts
Normal file
303
packages/napcat-core/services/NodeIKernelFlashTransferService.ts
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
import { GeneralCallResult } from './common';
|
||||||
|
import {
|
||||||
|
SendStatus,
|
||||||
|
StartFlashTaskRequests,
|
||||||
|
createFlashTransferResult,
|
||||||
|
FlashBaseRequest,
|
||||||
|
FlashFileSetInfo,
|
||||||
|
FileListInfoRequests,
|
||||||
|
FileListResponse,
|
||||||
|
DownloadStatusInfo,
|
||||||
|
SendTargetRequests,
|
||||||
|
FlashOneFileInfo,
|
||||||
|
DownloadSceneType,
|
||||||
|
} from '../data/flash';
|
||||||
|
|
||||||
|
export interface NodeIKernelFlashTransferService {
|
||||||
|
/**
|
||||||
|
* 开始闪传服务 并上传文件/文件夹(可以多选,非常好用)
|
||||||
|
* @param timestamp
|
||||||
|
* @param fileInfo
|
||||||
|
*/
|
||||||
|
createFlashTransferUploadTask (timestamp: number, fileInfo: StartFlashTaskRequests): Promise<GeneralCallResult & {
|
||||||
|
createFlashTransferResult: createFlashTransferResult;
|
||||||
|
seq: number;
|
||||||
|
}>; // 2 arg 重点 // 自动上传
|
||||||
|
|
||||||
|
createMergeShareTask (...args: unknown[]): unknown; // 2 arg
|
||||||
|
|
||||||
|
updateFlashTransfer (...args: unknown[]): unknown; // 2 arg
|
||||||
|
|
||||||
|
getFileSetList (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
getFileSetListCount (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取file set 的信息
|
||||||
|
* @param fileSetIdDict
|
||||||
|
*/
|
||||||
|
getFileSet (fileSetIdDict: FlashBaseRequest): Promise<GeneralCallResult & {
|
||||||
|
seq: number;
|
||||||
|
isCache: boolean;
|
||||||
|
fileSet: FlashFileSetInfo;
|
||||||
|
}>; // 1 arg
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取file set 里面的文件信息(文件夹结构)
|
||||||
|
* @param requestArgs
|
||||||
|
*/
|
||||||
|
getFileList (requestArgs: FileListInfoRequests): Promise<{
|
||||||
|
rsp: FileListResponse;
|
||||||
|
}>; // 1 arg 这个方法QQ有bug??? 并没有,是我参数有问题
|
||||||
|
|
||||||
|
getDownloadedFileCount (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
getLocalFileList (...args: unknown[]): unknown; // 3 arg
|
||||||
|
|
||||||
|
batchRemoveUserFileSetHistory (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分享链接
|
||||||
|
* @param fileSetId
|
||||||
|
*/
|
||||||
|
getShareLinkReq (fileSetId: string): Promise<GeneralCallResult & {
|
||||||
|
shareLink: string;
|
||||||
|
expireTimestamp: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 由分享链接到fileSetId
|
||||||
|
* @param shareCode
|
||||||
|
*/
|
||||||
|
getFileSetIdByCode (shareCode: string): Promise<GeneralCallResult & {
|
||||||
|
fileSetId: string;
|
||||||
|
}>; // 1 arg code == share code
|
||||||
|
|
||||||
|
batchRemoveFile (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
checkUploadPathValid (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
cleanFailedFiles (...args: unknown[]): unknown; // 2 arg
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 暂停所有的任务
|
||||||
|
*/
|
||||||
|
resumeAllUnfinishedTasks (): unknown; // 0 arg !!
|
||||||
|
|
||||||
|
addFileSetUploadListener (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
removeFileSetUploadListener (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始上传任务 适用于已暂停的
|
||||||
|
* @param fileSetId
|
||||||
|
*/
|
||||||
|
startFileSetUpload (fileSetId: string): void; // 1 arg 并不是新建任务,应该是暂停后的启动
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束,无法再次启动
|
||||||
|
* @param fileSetId
|
||||||
|
*/
|
||||||
|
stopFileSetUpload (fileSetId: string): void; // 1 arg stop 后start无效
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 暂停上传
|
||||||
|
* @param fileSetId
|
||||||
|
*/
|
||||||
|
pauseFileSetUpload (fileSetId: string): void; // 1 arg 暂停上传
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 继续上传
|
||||||
|
* @param args
|
||||||
|
*/
|
||||||
|
resumeFileSetUpload (...args: unknown[]): unknown; // 1 arg 继续
|
||||||
|
|
||||||
|
pauseFileUpload (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
resumeFileUpload (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
stopFileUpload (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
asyncGetThumbnailPath (...args: unknown[]): unknown; // 2 arg
|
||||||
|
|
||||||
|
setDownLoadDefaultFileDir (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
setFileSetDownloadDir (...args: unknown[]): unknown; // 2 arg
|
||||||
|
|
||||||
|
getFileSetDownloadDir (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
setFlashTransferDir (...args: unknown[]): unknown; // 2 arg
|
||||||
|
|
||||||
|
addFileSetDownloadListener (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
removeFileSetDownloadListener (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始下载file set的函数 同开始上传
|
||||||
|
* @param fileSetId
|
||||||
|
* @param downloadSceneType 下载类型 //因为没有peer,其实可以硬编码为1 (好友私聊)
|
||||||
|
* @param arg // 默认为false
|
||||||
|
*/
|
||||||
|
startFileSetDownload (fileSetId: string, downloadSceneType: DownloadSceneType, downloadOptionParams: { isIncludeCompressInnerFiles: boolean; }): Promise<GeneralCallResult & {
|
||||||
|
extraInfo: 0;
|
||||||
|
}>; // 3 arg
|
||||||
|
|
||||||
|
stopFileSetDownload (fileSetId: string, downloadOptionParams: { isIncludeCompressInnerFiles: boolean; }): Promise<GeneralCallResult & {
|
||||||
|
extraInfo: 0;
|
||||||
|
}>; // 2 arg 结束不可重启!!
|
||||||
|
|
||||||
|
pauseFileSetDownload (fileSetId: string, downloadOptionParams: { isIncludeCompressInnerFiles: boolean; }): Promise<GeneralCallResult & {
|
||||||
|
extraInfo: 0;
|
||||||
|
}>; // 2 arg
|
||||||
|
|
||||||
|
resumeFileSetDownload (fileSetId: string, downloadOptionParams: { isIncludeCompressInnerFiles: boolean; }): Promise<GeneralCallResult & {
|
||||||
|
extraInfo: 0;
|
||||||
|
}>; // 2 arg
|
||||||
|
|
||||||
|
startFileListDownLoad (...args: unknown[]): unknown; // 4 arg // 大概率是选择set里面的部分文件进行下载,没必要,不想写
|
||||||
|
|
||||||
|
pauseFileListDownLoad (...args: unknown[]): unknown; // 2 arg
|
||||||
|
|
||||||
|
resumeFileListDownLoad (...args: unknown[]): unknown; // 2 arg
|
||||||
|
|
||||||
|
stopFileListDownLoad (...args: unknown[]): unknown; // 2 arg
|
||||||
|
|
||||||
|
startThumbnailListDownload (fileSetId: string): Promise<GeneralCallResult>; // 1 arg // 缩略图下载
|
||||||
|
|
||||||
|
stopThumbnailListDownload (fileSetId: string): Promise<GeneralCallResult>; // 1 arg
|
||||||
|
|
||||||
|
asyncRequestDownLoadStatus (fileSetId: string): Promise<DownloadStatusInfo>; // 1 arg
|
||||||
|
|
||||||
|
startFileTransferUrl (fileInfo: FlashOneFileInfo): Promise<{
|
||||||
|
ret: number,
|
||||||
|
url: string,
|
||||||
|
expireTimestampSeconds: string;
|
||||||
|
}>; // 1 arg
|
||||||
|
|
||||||
|
startFileListDownLoadBySessionId (...args: unknown[]): unknown; // 2 arg
|
||||||
|
|
||||||
|
addFileSetSimpleStatusListener (...args: unknown[]): unknown; // 2 arg
|
||||||
|
|
||||||
|
addFileSetSimpleStatusMonitoring (...args: unknown[]): unknown; // 2 arg
|
||||||
|
|
||||||
|
removeFileSetSimpleStatusMonitoring (...args: unknown[]): unknown; // 2 arg
|
||||||
|
|
||||||
|
removeFileSetSimpleStatusListener (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
addDesktopFileSetSimpleStatusListener (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
addDesktopFileSetSimpleStatusMonitoring (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
removeDesktopFileSetSimpleStatusMonitoring (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
removeDesktopFileSetSimpleStatusListener (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
addFileSetSimpleUploadInfoListener (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
addFileSetSimpleUploadInfoMonitoring (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
removeFileSetSimpleUploadInfoMonitoring (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
removeFileSetSimpleUploadInfoListener (...args: unknown[]): unknown; // 1 arg
|
||||||
|
/**
|
||||||
|
* 发送闪传消息
|
||||||
|
* @param sendArgs
|
||||||
|
*/
|
||||||
|
sendFlashTransferMsg (sendArgs: SendTargetRequests): Promise<{
|
||||||
|
errCode: number,
|
||||||
|
errMsg: string,
|
||||||
|
rsp: {
|
||||||
|
sendStatus: SendStatus[];
|
||||||
|
};
|
||||||
|
}>; // 1 arg 估计是file set id
|
||||||
|
|
||||||
|
addFlashTransferTaskInfoListener (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
removeFlashTransferTaskInfoListener (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
retrieveLocalLastFailedSetTasksInfo (): unknown; // 0 arg
|
||||||
|
|
||||||
|
getFailedFileList (fileSetId: string): Promise<{
|
||||||
|
rsp: {
|
||||||
|
seq: number;
|
||||||
|
result: number;
|
||||||
|
errMs: string;
|
||||||
|
fileSetId: string;
|
||||||
|
fileList: [];
|
||||||
|
};
|
||||||
|
}>; // 1 arg
|
||||||
|
|
||||||
|
getLocalFileListByStatuses (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
addTransferStateListener (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
removeTransferStateListener (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
getFileSetFirstClusteringList (...args: unknown[]): unknown; // 3 arg
|
||||||
|
|
||||||
|
getFileSetClusteringList (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
addFileSetClusteringListListener (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
removeFileSetClusteringListListener (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
getFileSetClusteringDetail (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
doAIOFlashTransferBubbleActionWithStatus (...args: unknown[]): unknown; // 4 arg
|
||||||
|
|
||||||
|
getFilesTransferProgress (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
pollFilesTransferProgress (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
cancelPollFilesTransferProgress (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
checkDownloadStatusBeforeLocalFileOper (...args: unknown[]): unknown; // 3 arg
|
||||||
|
|
||||||
|
getCompressedFileFolder (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
addFolderListener (...args: unknown[]): unknown; // 1 arg
|
||||||
|
|
||||||
|
removeFolderListener (...args: unknown[]): unknown;
|
||||||
|
|
||||||
|
addCompressedFileListener (...args: unknown[]): unknown;
|
||||||
|
|
||||||
|
removeCompressedFileListener (...args: unknown[]): unknown;
|
||||||
|
|
||||||
|
getFileCategoryList (...args: unknown[]): unknown;
|
||||||
|
|
||||||
|
addDeviceStatusListener (...args: unknown[]): unknown;
|
||||||
|
|
||||||
|
removeDeviceStatusListener (...args: unknown[]): unknown;
|
||||||
|
|
||||||
|
checkDeviceStatus (...args: unknown[]): unknown;
|
||||||
|
|
||||||
|
pauseAllTasks (...args: unknown[]): unknown; // 2 arg
|
||||||
|
|
||||||
|
resumePausedTasksAfterDeviceStatus (...args: unknown[]): unknown;
|
||||||
|
|
||||||
|
onSystemGoingToSleep (...args: unknown[]): unknown;
|
||||||
|
|
||||||
|
onSystemWokeUp (...args: unknown[]): unknown;
|
||||||
|
|
||||||
|
getFileMetas (...args: unknown[]): unknown;
|
||||||
|
|
||||||
|
addDownloadCntStatisticsListener (...args: unknown[]): unknown;
|
||||||
|
|
||||||
|
removeDownloadCntStatisticsListener (...args: unknown[]): unknown;
|
||||||
|
|
||||||
|
detectPrivacyInfoInPaths (...args: unknown[]): unknown;
|
||||||
|
|
||||||
|
getFileThumbnailUrl (...args: unknown[]): unknown;
|
||||||
|
|
||||||
|
handleDownloadFinishAfterSaveToAlbum (...args: unknown[]): unknown;
|
||||||
|
|
||||||
|
checkBatchFilesDownloadStatus (...args: unknown[]): unknown;
|
||||||
|
|
||||||
|
onCheckAlbumStorageStatusResult (...args: unknown[]): unknown;
|
||||||
|
|
||||||
|
addFileAlbumStorageListener (...args: unknown[]): unknown;
|
||||||
|
|
||||||
|
removeFileAlbumStorageListener (...args: unknown[]): unknown;
|
||||||
|
|
||||||
|
refreshFolderStatus (...args: unknown[]): unknown;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
|||||||
export enum GeneralCallResultStatus {
|
export enum GeneralCallResultStatus {
|
||||||
OK = 0,
|
OK = 0,
|
||||||
|
ERROR = -1,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GeneralCallResult {
|
export interface GeneralCallResult {
|
||||||
|
|||||||
21
packages/napcat-core/types/flashfile.ts
Normal file
21
packages/napcat-core/types/flashfile.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export enum fileType {
|
||||||
|
MP3 = 1,
|
||||||
|
VIDEO = 2,
|
||||||
|
DOC = 3,
|
||||||
|
ZIP = 4,
|
||||||
|
XLS = 6,
|
||||||
|
PPT = 7,
|
||||||
|
CODE = 8,
|
||||||
|
PDF = 9,
|
||||||
|
TXT = 10,
|
||||||
|
UNKNOW = 11,
|
||||||
|
FOLDER = 25,
|
||||||
|
IMG = 26,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FileStatus {
|
||||||
|
UPLOADING = 0,
|
||||||
|
// DOWNLOADED = 1, ??? 不太清楚
|
||||||
|
OK = 2,
|
||||||
|
STOP = 3,
|
||||||
|
}
|
||||||
@@ -11,3 +11,7 @@ export * from './constant';
|
|||||||
export * from './graytip';
|
export * from './graytip';
|
||||||
export * from './emoji';
|
export * from './emoji';
|
||||||
export * from './service';
|
export * from './service';
|
||||||
|
export * from './adapter';
|
||||||
|
export * from './contact';
|
||||||
|
export * from './file';
|
||||||
|
export * from './flashfile';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NTGroupMemberRole } from '@/napcat-core/index';
|
import { NTGroupMemberRole } from './group';
|
||||||
import { ActionBarElement, ArkElement, AvRecordElement, CalendarElement, FaceBubbleElement, FaceElement, FileElement, GiphyElement, GrayTipElement, MarketFaceElement, PicElement, PttElement, RecommendedMsgElement, ReplyElement, ShareLocationElement, StructLongMsgElement, TaskTopMsgElement, TextElement, TofuRecordElement, VideoElement, YoloGameResultElement } from './element';
|
import { ActionBarElement, ArkElement, AvRecordElement, CalendarElement, FaceBubbleElement, FaceElement, FileElement, GiphyElement, GrayTipElement, MarketFaceElement, PicElement, PttElement, RecommendedMsgElement, ReplyElement, ShareLocationElement, StructLongMsgElement, TaskTopMsgElement, TextElement, TofuRecordElement, VideoElement, YoloGameResultElement } from './element';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -66,13 +66,14 @@ export enum ElementType {
|
|||||||
YOLOGAMERESULT = 20,
|
YOLOGAMERESULT = 20,
|
||||||
AVRECORD = 21,
|
AVRECORD = 21,
|
||||||
FEED = 22,
|
FEED = 22,
|
||||||
TOFURECORD = 23,
|
TOFURECORD = 23, // tofu record?? 在线文件的id是这个
|
||||||
ACEBUBBLE = 24,
|
ACEBUBBLE = 24,
|
||||||
ACTIVITY = 25,
|
ACTIVITY = 25,
|
||||||
TOFU = 26,
|
TOFU = 26,
|
||||||
FACEBUBBLE = 27,
|
FACEBUBBLE = 27,
|
||||||
SHARELOCATION = 28,
|
SHARELOCATION = 28,
|
||||||
TASKTOPMSG = 29,
|
TASKTOPMSG = 29,
|
||||||
|
ONLINEFOLDER = 30, // 在线文件夹
|
||||||
RECOMMENDEDMSG = 43,
|
RECOMMENDEDMSG = 43,
|
||||||
ACTIONBAR = 44,
|
ACTIONBAR = 44,
|
||||||
}
|
}
|
||||||
@@ -303,11 +304,40 @@ export enum NTVideoType {
|
|||||||
VIDEO_FORMAT_WMV = 3,
|
VIDEO_FORMAT_WMV = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 闪传图标
|
||||||
|
*/
|
||||||
|
export interface FlashTransferIcon {
|
||||||
|
spec: number;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 闪传文件信息
|
||||||
|
*/
|
||||||
|
export interface FlashTransferInfo {
|
||||||
|
filesetId: string;
|
||||||
|
name: string;
|
||||||
|
fileSize: string;
|
||||||
|
thnumbnail: {
|
||||||
|
id: string;
|
||||||
|
urls: FlashTransferIcon[];
|
||||||
|
localCachePath: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Markdown元素接口
|
* Markdown元素接口
|
||||||
*/
|
*/
|
||||||
export interface MarkdownElement {
|
export interface MarkdownElement {
|
||||||
content: string;
|
content: string;
|
||||||
|
style?: {};
|
||||||
|
processMsg?: string;
|
||||||
|
mdSummary?: string;
|
||||||
|
mdExtType?: number;
|
||||||
|
mdExtInfo?: {
|
||||||
|
flashTransferInfo: FlashTransferInfo;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -57,24 +57,24 @@ export interface BaseInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 音乐信息
|
// 音乐信息
|
||||||
interface MusicInfo {
|
export interface MusicInfo {
|
||||||
buf: string;
|
buf: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 视频业务信息
|
// 视频业务信息
|
||||||
interface VideoBizInfo {
|
export interface VideoBizInfo {
|
||||||
cid: string;
|
cid: string;
|
||||||
tvUrl: string;
|
tvUrl: string;
|
||||||
synchType: string;
|
synchType: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 视频信息
|
// 视频信息
|
||||||
interface VideoInfo {
|
export interface VideoInfo {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 扩展在线业务信息
|
// 扩展在线业务信息
|
||||||
interface ExtOnlineBusinessInfo {
|
export interface ExtOnlineBusinessInfo {
|
||||||
buf: string;
|
buf: string;
|
||||||
customStatus: unknown;
|
customStatus: unknown;
|
||||||
videoBizInfo: VideoBizInfo;
|
videoBizInfo: VideoBizInfo;
|
||||||
@@ -82,12 +82,12 @@ interface ExtOnlineBusinessInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 扩展缓冲区
|
// 扩展缓冲区
|
||||||
interface ExtBuffer {
|
export interface ExtBuffer {
|
||||||
buf: string;
|
buf: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户状态
|
// 用户状态
|
||||||
interface UserStatus {
|
export interface UserStatus {
|
||||||
uid: string;
|
uid: string;
|
||||||
uin: string;
|
uin: string;
|
||||||
status: number;
|
status: number;
|
||||||
@@ -109,14 +109,14 @@ interface UserStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 特权图标
|
// 特权图标
|
||||||
interface PrivilegeIcon {
|
export interface PrivilegeIcon {
|
||||||
jumpUrl: string;
|
jumpUrl: string;
|
||||||
openIconList: unknown[];
|
openIconList: unknown[];
|
||||||
closeIconList: unknown[];
|
closeIconList: unknown[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 增值服务信息
|
// 增值服务信息
|
||||||
interface VasInfo {
|
export interface VasInfo {
|
||||||
vipFlag: boolean;
|
vipFlag: boolean;
|
||||||
yearVipFlag: boolean;
|
yearVipFlag: boolean;
|
||||||
svipFlag: boolean;
|
svipFlag: boolean;
|
||||||
@@ -149,7 +149,7 @@ interface VasInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 关系标志
|
// 关系标志
|
||||||
interface RelationFlags {
|
export interface RelationFlags {
|
||||||
topTime: string;
|
topTime: string;
|
||||||
isBlock: boolean;
|
isBlock: boolean;
|
||||||
isMsgDisturb: boolean;
|
isMsgDisturb: boolean;
|
||||||
@@ -167,7 +167,7 @@ interface RelationFlags {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 通用扩展信息
|
// 通用扩展信息
|
||||||
interface CommonExt {
|
export interface CommonExt {
|
||||||
constellation: number;
|
constellation: number;
|
||||||
shengXiao: number;
|
shengXiao: number;
|
||||||
kBloodType: number;
|
kBloodType: number;
|
||||||
@@ -193,14 +193,14 @@ export enum BuddyListReqType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 图片信息
|
// 图片信息
|
||||||
interface Pic {
|
export interface Pic {
|
||||||
picId: string;
|
picId: string;
|
||||||
picTime: number;
|
picTime: number;
|
||||||
picUrlMap: Record<string, string>;
|
picUrlMap: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 照片墙
|
// 照片墙
|
||||||
interface PhotoWall {
|
export interface PhotoWall {
|
||||||
picList: Pic[];
|
picList: Pic[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ export interface ModifyProfileParams {
|
|||||||
nick: string;
|
nick: string;
|
||||||
longNick: string;
|
longNick: string;
|
||||||
sex: NTSex;
|
sex: NTSex;
|
||||||
birthday: { birthday_year: string, birthday_month: string, birthday_day: string };
|
birthday: { birthday_year: string, birthday_month: string, birthday_day: string; };
|
||||||
location: unknown;
|
location: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NodeIDependsAdapter, NodeIDispatcherAdapter, NodeIGlobalAdapter } from './adapters';
|
import { NodeIDependsAdapter, NodeIDispatcherAdapter, NodeIGlobalAdapter } from './adapters';
|
||||||
import {
|
import {
|
||||||
|
GeneralCallResult,
|
||||||
NodeIKernelAvatarService,
|
NodeIKernelAvatarService,
|
||||||
NodeIKernelBuddyService,
|
NodeIKernelBuddyService,
|
||||||
NodeIKernelGroupService,
|
NodeIKernelGroupService,
|
||||||
@@ -27,77 +28,78 @@ import { NodeIKernelMSFService } from './services/NodeIKernelMSFService';
|
|||||||
import { NodeIkernelTestPerformanceService } from './services/NodeIkernelTestPerformanceService';
|
import { NodeIkernelTestPerformanceService } from './services/NodeIkernelTestPerformanceService';
|
||||||
import { NodeIKernelECDHService } from './services/NodeIKernelECDHService';
|
import { NodeIKernelECDHService } from './services/NodeIKernelECDHService';
|
||||||
import { NodeIO3MiscService } from './services/NodeIO3MiscService';
|
import { NodeIO3MiscService } from './services/NodeIO3MiscService';
|
||||||
|
import { NodeIKernelFlashTransferService } from './services/NodeIKernelFlashTransferService';
|
||||||
|
|
||||||
export interface NodeQQNTWrapperUtil {
|
export interface NodeQQNTWrapperUtil {
|
||||||
get(): NodeQQNTWrapperUtil;
|
get (): NodeQQNTWrapperUtil;
|
||||||
|
|
||||||
getNTUserDataInfoConfig(): string;
|
getNTUserDataInfoConfig (): string;
|
||||||
|
|
||||||
emptyWorkingSet(n: number): void;
|
emptyWorkingSet (n: number): void;
|
||||||
|
|
||||||
getSsoCmdOfOidbReq(arg1: number, arg2: number): unknown;
|
getSsoCmdOfOidbReq (arg1: number, arg2: number): unknown;
|
||||||
|
|
||||||
getSsoBufferOfOidbReq(...args: unknown[]): unknown; // 有点看不懂参数定义 待补充 好像是三个参数
|
getSsoBufferOfOidbReq (...args: unknown[]): unknown; // 有点看不懂参数定义 待补充 好像是三个参数
|
||||||
|
|
||||||
getOidbRspInfo(arg: string): unknown; // 可能是错的
|
getOidbRspInfo (arg: string): unknown; // 可能是错的
|
||||||
|
|
||||||
getFileSize(path: string): Promise<number>; // 直接的猜测
|
getFileSize (path: string): Promise<number>; // 直接的猜测
|
||||||
|
|
||||||
genFileMd5Buf(arg: string): unknown; // 可能是错的
|
genFileMd5Buf (arg: string): unknown; // 可能是错的
|
||||||
|
|
||||||
genFileMd5Hex(path: string): unknown; // 直接的猜测
|
genFileMd5Hex (path: string): unknown; // 直接的猜测
|
||||||
|
|
||||||
genFileShaBuf(path: string): unknown; // 直接的猜测
|
genFileShaBuf (path: string): unknown; // 直接的猜测
|
||||||
|
|
||||||
genFileCumulateSha1(path: string): unknown; // 直接的猜测
|
genFileCumulateSha1 (path: string): unknown; // 直接的猜测
|
||||||
|
|
||||||
genFileShaHex(path: string): unknown; // 直接的猜测
|
genFileShaHex (path: string): unknown; // 直接的猜测
|
||||||
|
|
||||||
fileIsExist(path: string): unknown;
|
fileIsExist (path: string): unknown;
|
||||||
|
|
||||||
startTrace(path: string): unknown; // 可能是错的
|
startTrace (path: string): unknown; // 可能是错的
|
||||||
|
|
||||||
copyFile(src: string, dst: string): unknown;
|
copyFile (src: string, dst: string): unknown;
|
||||||
|
|
||||||
genFileShaAndMd5Hex(path: string, unknown: number): unknown; // 可能是错的
|
genFileShaAndMd5Hex (path: string, unknown: number): unknown; // 可能是错的
|
||||||
|
|
||||||
setTraceInfo(unknown: unknown): unknown;
|
setTraceInfo (unknown: unknown): unknown;
|
||||||
|
|
||||||
encodeOffLine(unknown: unknown): unknown;
|
encodeOffLine (unknown: unknown): unknown;
|
||||||
|
|
||||||
decodeOffLine(arg: string): unknown; // 可能是错的 传递hex
|
decodeOffLine (arg: string): unknown; // 可能是错的 传递hex
|
||||||
|
|
||||||
DecoderRecentInfo(arg: string): unknown; // 可能是错的 传递hex
|
DecoderRecentInfo (arg: string): unknown; // 可能是错的 传递hex
|
||||||
|
|
||||||
getPinyin(arg0: string, arg1: boolean): unknown;
|
getPinyin (arg0: string, arg1: boolean): unknown;
|
||||||
|
|
||||||
matchInPinyin(arg0: unknown[], arg1: string): unknown; // 参数特复杂 arg0是个复杂数据类型
|
matchInPinyin (arg0: unknown[], arg1: string): unknown; // 参数特复杂 arg0是个复杂数据类型
|
||||||
|
|
||||||
makeDirByPath(arg0: string): unknown;
|
makeDirByPath (arg0: string): unknown;
|
||||||
|
|
||||||
emptyWorkingSet(arg0: number): unknown; // 参数是UINT32
|
emptyWorkingSet (arg0: number): unknown; // 参数是UINT32
|
||||||
|
|
||||||
runProcess(arg0: string, arg1: boolean): unknown;
|
runProcess (arg0: string, arg1: boolean): unknown;
|
||||||
|
|
||||||
runProcessArgs(arg0: string, arg1: { [key: string]: string }, arg2: boolean): unknown;
|
runProcessArgs (arg0: string, arg1: { [key: string]: string; }, arg2: boolean): unknown;
|
||||||
|
|
||||||
calcThumbSize(arg0: number, arg1: number, arg2: unknown): unknown;
|
calcThumbSize (arg0: number, arg1: number, arg2: unknown): unknown;
|
||||||
|
|
||||||
fullWordToHalfWord(word: string): unknown;
|
fullWordToHalfWord (word: string): unknown;
|
||||||
|
|
||||||
getNTUserDataInfoConfig(): unknown;
|
getNTUserDataInfoConfig (): unknown;
|
||||||
|
|
||||||
pathIsReadableAndWriteable(path: string): unknown; // 直接的猜测
|
pathIsReadableAndWriteable (path: string): unknown; // 直接的猜测
|
||||||
|
|
||||||
resetUserDataSavePathToDocument(): unknown;
|
resetUserDataSavePathToDocument (): unknown;
|
||||||
|
|
||||||
getSoBuildInfo(): unknown; // 例如 0[0]_d491dc01e0a_0
|
getSoBuildInfo (): unknown; // 例如 0[0]_d491dc01e0a_0
|
||||||
|
|
||||||
registerCountInstruments(arg0: string, arg1: string[], arg2: number, arg3: number): unknown;
|
registerCountInstruments (arg0: string, arg1: string[], arg2: number, arg3: number): unknown;
|
||||||
|
|
||||||
registerValueInstruments(arg0: string, arg1: string[], arg2: number, arg3: number): unknown;
|
registerValueInstruments (arg0: string, arg1: string[], arg2: number, arg3: number): unknown;
|
||||||
|
|
||||||
registerValueInstrumentsWithBoundary(
|
registerValueInstrumentsWithBoundary (
|
||||||
arg0: string,
|
arg0: string,
|
||||||
arg1: unknown,
|
arg1: unknown,
|
||||||
arg2: unknown,
|
arg2: unknown,
|
||||||
@@ -105,7 +107,7 @@ export interface NodeQQNTWrapperUtil {
|
|||||||
arg4: number,
|
arg4: number,
|
||||||
): unknown;
|
): unknown;
|
||||||
|
|
||||||
reportCountIndicators(
|
reportCountIndicators (
|
||||||
arg0: string,
|
arg0: string,
|
||||||
arg1: Map<unknown, unknown>,
|
arg1: Map<unknown, unknown>,
|
||||||
arg2: string,
|
arg2: string,
|
||||||
@@ -113,7 +115,7 @@ export interface NodeQQNTWrapperUtil {
|
|||||||
arg4: boolean,
|
arg4: boolean,
|
||||||
): unknown;
|
): unknown;
|
||||||
|
|
||||||
reportValueIndicators(
|
reportValueIndicators (
|
||||||
arg0: string,
|
arg0: string,
|
||||||
arg1: Map<unknown, unknown>,
|
arg1: Map<unknown, unknown>,
|
||||||
arg2: string,
|
arg2: string,
|
||||||
@@ -121,140 +123,154 @@ export interface NodeQQNTWrapperUtil {
|
|||||||
arg4: number,
|
arg4: number,
|
||||||
): unknown;
|
): unknown;
|
||||||
|
|
||||||
checkNewUserDataSaveDirAvailable(arg0: string): unknown;
|
checkNewUserDataSaveDirAvailable (arg0: string): unknown;
|
||||||
|
|
||||||
copyUserData(arg0: string, arg1: string): Promise<unknown>;
|
copyUserData (arg0: string, arg1: string): Promise<unknown>;
|
||||||
|
|
||||||
setUserDataSaveDirectory(arg0: string): Promise<unknown>;
|
setUserDataSaveDirectory (arg0: string): Promise<unknown>;
|
||||||
|
|
||||||
hasOtherRunningQQProcess(): boolean;
|
hasOtherRunningQQProcess (): boolean;
|
||||||
|
|
||||||
quitAllRunningQQProcess(arg: boolean): unknown;
|
quitAllRunningQQProcess (arg: boolean): unknown;
|
||||||
|
|
||||||
checkNvidiaConfig(): unknown;
|
checkNvidiaConfig (): unknown;
|
||||||
|
|
||||||
repairNvidiaConfig(): unknown;
|
repairNvidiaConfig (): unknown;
|
||||||
|
|
||||||
getNvidiaDriverVersion(): unknown;
|
getNvidiaDriverVersion (): unknown;
|
||||||
|
|
||||||
isNull(): unknown;
|
isNull (): unknown;
|
||||||
|
|
||||||
|
createThumbnailImage (
|
||||||
|
serviceName: string,
|
||||||
|
filePath: string,
|
||||||
|
targetPath: string,
|
||||||
|
imgSize: {
|
||||||
|
width: number,
|
||||||
|
height: number;
|
||||||
|
},
|
||||||
|
fileFormat: string,
|
||||||
|
arg: number | null | undefined, // null undefined都行
|
||||||
|
): Promise<GeneralCallResult & { targetPath?: string; }>;
|
||||||
}
|
}
|
||||||
export interface NodeIQQNTStartupSessionWrapper {
|
export interface NodeIQQNTStartupSessionWrapper {
|
||||||
create(): NodeIQQNTStartupSessionWrapper;
|
create (): NodeIQQNTStartupSessionWrapper;
|
||||||
stop(): void;
|
stop (): void;
|
||||||
start(): void;
|
start (): void;
|
||||||
createWithModuleList(uk: unknown): unknown;
|
createWithModuleList (uk: unknown): unknown;
|
||||||
getSessionIdList(): unknown;
|
getSessionIdList (): unknown;
|
||||||
}
|
}
|
||||||
export interface NodeIQQNTWrapperSession {
|
export interface NodeIQQNTWrapperSession {
|
||||||
getNTWrapperSession(str: string): NodeIQQNTWrapperSession;
|
getNTWrapperSession (str: string): NodeIQQNTWrapperSession;
|
||||||
|
|
||||||
get(): NodeIQQNTWrapperSession;
|
get (): NodeIQQNTWrapperSession;
|
||||||
|
|
||||||
new(): NodeIQQNTWrapperSession;
|
new(): NodeIQQNTWrapperSession;
|
||||||
|
|
||||||
create(): NodeIQQNTWrapperSession;
|
create (): NodeIQQNTWrapperSession;
|
||||||
|
|
||||||
init(
|
init (
|
||||||
wrapperSessionInitConfig: WrapperSessionInitConfig,
|
wrapperSessionInitConfig: WrapperSessionInitConfig,
|
||||||
nodeIDependsAdapter: NodeIDependsAdapter,
|
nodeIDependsAdapter: NodeIDependsAdapter,
|
||||||
nodeIDispatcherAdapter: NodeIDispatcherAdapter,
|
nodeIDispatcherAdapter: NodeIDispatcherAdapter,
|
||||||
nodeIKernelSessionListener: NodeIKernelSessionListener,
|
nodeIKernelSessionListener: NodeIKernelSessionListener,
|
||||||
): void;
|
): void;
|
||||||
|
|
||||||
startNT(session: number): void;
|
startNT (session: number): void;
|
||||||
|
|
||||||
startNT(): void;
|
startNT (): void;
|
||||||
|
|
||||||
getBdhUploadService(): unknown;
|
getBdhUploadService (): unknown;
|
||||||
|
|
||||||
getECDHService(): NodeIKernelECDHService;
|
getECDHService (): NodeIKernelECDHService;
|
||||||
|
|
||||||
getMsgService(): NodeIKernelMsgService;
|
getMsgService (): NodeIKernelMsgService;
|
||||||
|
|
||||||
getProfileService(): NodeIKernelProfileService;
|
getProfileService (): NodeIKernelProfileService;
|
||||||
|
|
||||||
getProfileLikeService(): NodeIKernelProfileLikeService;
|
getProfileLikeService (): NodeIKernelProfileLikeService;
|
||||||
|
|
||||||
getGroupService(): NodeIKernelGroupService;
|
getGroupService (): NodeIKernelGroupService;
|
||||||
|
|
||||||
getStorageCleanService(): NodeIKernelStorageCleanService;
|
getStorageCleanService (): NodeIKernelStorageCleanService;
|
||||||
|
|
||||||
getBuddyService(): NodeIKernelBuddyService;
|
getBuddyService (): NodeIKernelBuddyService;
|
||||||
|
|
||||||
getRobotService(): NodeIKernelRobotService;
|
getRobotService (): NodeIKernelRobotService;
|
||||||
|
|
||||||
getTicketService(): NodeIKernelTicketService;
|
getTicketService (): NodeIKernelTicketService;
|
||||||
|
|
||||||
getTipOffService(): NodeIKernelTipOffService;
|
getTipOffService (): NodeIKernelTipOffService;
|
||||||
|
|
||||||
getNodeMiscService(): NodeIKernelNodeMiscService;
|
getNodeMiscService (): NodeIKernelNodeMiscService;
|
||||||
|
|
||||||
getRichMediaService(): NodeIKernelRichMediaService;
|
getRichMediaService (): NodeIKernelRichMediaService;
|
||||||
|
|
||||||
getMsgBackupService(): NodeIKernelMsgBackupService;
|
getMsgBackupService (): NodeIKernelMsgBackupService;
|
||||||
|
|
||||||
getAlbumService(): NodeIKernelAlbumService;
|
getAlbumService (): NodeIKernelAlbumService;
|
||||||
|
|
||||||
getTianShuService(): NodeIKernelTianShuService;
|
getTianShuService (): NodeIKernelTianShuService;
|
||||||
|
|
||||||
getUnitedConfigService(): NodeIKernelUnitedConfigService;
|
getUnitedConfigService (): NodeIKernelUnitedConfigService;
|
||||||
|
|
||||||
getSearchService(): NodeIKernelSearchService;
|
getSearchService (): NodeIKernelSearchService;
|
||||||
|
|
||||||
getDirectSessionService(): unknown;
|
getFlashTransferService (): NodeIKernelFlashTransferService;
|
||||||
|
|
||||||
getRDeliveryService(): unknown;
|
getDirectSessionService (): unknown;
|
||||||
|
|
||||||
getAvatarService(): NodeIKernelAvatarService;
|
getRDeliveryService (): unknown;
|
||||||
|
|
||||||
getFeedChannelService(): unknown;
|
getAvatarService (): NodeIKernelAvatarService;
|
||||||
|
|
||||||
getYellowFaceService(): unknown;
|
getFeedChannelService (): unknown;
|
||||||
|
|
||||||
getCollectionService(): NodeIKernelCollectionService;
|
getYellowFaceService (): unknown;
|
||||||
|
|
||||||
getSettingService(): unknown;
|
getCollectionService (): NodeIKernelCollectionService;
|
||||||
|
|
||||||
getQiDianService(): unknown;
|
getSettingService (): unknown;
|
||||||
|
|
||||||
getFileAssistantService(): unknown;
|
getQiDianService (): unknown;
|
||||||
|
|
||||||
getGuildService(): unknown;
|
getFileAssistantService (): unknown;
|
||||||
|
|
||||||
getSkinService(): unknown;
|
getGuildService (): unknown;
|
||||||
|
|
||||||
getTestPerformanceService(): NodeIkernelTestPerformanceService;
|
getSkinService (): unknown;
|
||||||
|
|
||||||
getQQPlayService(): unknown;
|
getTestPerformanceService (): NodeIkernelTestPerformanceService;
|
||||||
|
|
||||||
getDbToolsService(): unknown;
|
getQQPlayService (): unknown;
|
||||||
|
|
||||||
getUixConvertService(): NodeIKernelUixConvertService;
|
getDbToolsService (): unknown;
|
||||||
|
|
||||||
getOnlineStatusService(): unknown;
|
getUixConvertService (): NodeIKernelUixConvertService;
|
||||||
|
|
||||||
getRemotingService(): unknown;
|
getOnlineStatusService (): unknown;
|
||||||
|
|
||||||
getGroupTabService(): unknown;
|
getRemotingService (): unknown;
|
||||||
|
|
||||||
getGroupSchoolService(): unknown;
|
getGroupTabService (): unknown;
|
||||||
|
|
||||||
getLiteBusinessService(): unknown;
|
getGroupSchoolService (): unknown;
|
||||||
|
|
||||||
getGuildMsgService(): unknown;
|
getLiteBusinessService (): unknown;
|
||||||
|
|
||||||
getLockService(): unknown;
|
getGuildMsgService (): unknown;
|
||||||
|
|
||||||
getMSFService(): NodeIKernelMSFService;
|
getLockService (): unknown;
|
||||||
|
|
||||||
getGuildHotUpdateService(): unknown;
|
getMSFService (): NodeIKernelMSFService;
|
||||||
|
|
||||||
getAVSDKService(): unknown;
|
getGuildHotUpdateService (): unknown;
|
||||||
|
|
||||||
getRecentContactService(): NodeIKernelRecentContactService;
|
getAVSDKService (): unknown;
|
||||||
|
|
||||||
getConfigMgrService(): unknown;
|
getRecentContactService (): NodeIKernelRecentContactService;
|
||||||
|
|
||||||
|
getConfigMgrService (): unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EnginInitDesktopConfig {
|
export interface EnginInitDesktopConfig {
|
||||||
@@ -268,20 +284,20 @@ export interface EnginInitDesktopConfig {
|
|||||||
global_path_config: {
|
global_path_config: {
|
||||||
desktopGlobalPath: string;
|
desktopGlobalPath: string;
|
||||||
};
|
};
|
||||||
thumb_config: { maxSide: 324; minSide: 48; longLimit: 6; density: 2 };
|
thumb_config: { maxSide: 324; minSide: 48; longLimit: 6; density: 2; };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NodeIQQNTWrapperEngine {
|
export interface NodeIQQNTWrapperEngine {
|
||||||
get(): NodeIQQNTWrapperEngine;
|
get (): NodeIQQNTWrapperEngine;
|
||||||
|
|
||||||
initWithDeskTopConfig(config: EnginInitDesktopConfig, nodeIGlobalAdapter: NodeIGlobalAdapter): void;
|
initWithDeskTopConfig (config: EnginInitDesktopConfig, nodeIGlobalAdapter: NodeIGlobalAdapter): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WrapperNodeApi {
|
export interface WrapperNodeApi {
|
||||||
NodeIO3MiscService: NodeIO3MiscService;
|
NodeIO3MiscService: NodeIO3MiscService;
|
||||||
NodeQQNTWrapperUtil: NodeQQNTWrapperUtil;
|
NodeQQNTWrapperUtil: NodeQQNTWrapperUtil;
|
||||||
NodeIQQNTWrapperSession: NodeIQQNTWrapperSession;
|
NodeIQQNTWrapperSession: NodeIQQNTWrapperSession;
|
||||||
NodeIQQNTStartupSessionWrapper: NodeIQQNTStartupSessionWrapper
|
NodeIQQNTStartupSessionWrapper: NodeIQQNTStartupSessionWrapper;
|
||||||
NodeIQQNTWrapperEngine: NodeIQQNTWrapperEngine;
|
NodeIQQNTWrapperEngine: NodeIQQNTWrapperEngine;
|
||||||
NodeIKernelLoginService: NodeIKernelLoginService;
|
NodeIKernelLoginService: NodeIKernelLoginService;
|
||||||
|
|
||||||
|
|||||||
4
packages/napcat-develop/config/.env.example
Normal file
4
packages/napcat-develop/config/.env.example
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
NAPCAT_DISABLE_PIPE=1
|
||||||
|
NAPCAT_DISABLE_MULTI_PROCESS=1
|
||||||
|
NAPCAT_WEBUI_JWT_SECRET_KEY=napcat_dev_secret_key
|
||||||
|
NAPCAT_WEBUI_SECRET_KEY=napcatqq
|
||||||
39
packages/napcat-develop/config/onebot11.json
Normal file
39
packages/napcat-develop/config/onebot11.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"network": {
|
||||||
|
"httpServers": [
|
||||||
|
{
|
||||||
|
"enable": true,
|
||||||
|
"name": "HTTP",
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 3000,
|
||||||
|
"enableCors": true,
|
||||||
|
"enableWebsocket": false,
|
||||||
|
"messagePostFormat": "array",
|
||||||
|
"token": "",
|
||||||
|
"debug": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"httpSseServers": [],
|
||||||
|
"httpClients": [],
|
||||||
|
"websocketServers": [
|
||||||
|
{
|
||||||
|
"enable": true,
|
||||||
|
"name": "WebSocket",
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 3001,
|
||||||
|
"reportSelfMessage": false,
|
||||||
|
"enableForcePushEvent": true,
|
||||||
|
"messagePostFormat": "array",
|
||||||
|
"token": "",
|
||||||
|
"debug": false,
|
||||||
|
"heartInterval": 30000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"websocketClients": [],
|
||||||
|
"plugins": []
|
||||||
|
},
|
||||||
|
"musicSignUrl": "",
|
||||||
|
"enableLocalFile2Url": false,
|
||||||
|
"parseMultMsg": false,
|
||||||
|
"imageDownloadProxy": ""
|
||||||
|
}
|
||||||
@@ -78,7 +78,7 @@ async function copyAll () {
|
|||||||
process.env.NAPCAT_WORKDIR = TARGET_DIR;
|
process.env.NAPCAT_WORKDIR = TARGET_DIR;
|
||||||
// 开发环境使用固定密钥
|
// 开发环境使用固定密钥
|
||||||
process.env.NAPCAT_WEBUI_JWT_SECRET_KEY = 'napcat_dev_secret_key';
|
process.env.NAPCAT_WEBUI_JWT_SECRET_KEY = 'napcat_dev_secret_key';
|
||||||
process.env.NAPCAT_WEBUI_SECRET_KEY = 'napcat';
|
process.env.NAPCAT_WEBUI_SECRET_KEY = 'napcatqq';
|
||||||
console.log('Loading NapCat module...');
|
console.log('Loading NapCat module...');
|
||||||
await import(pathToFileURL(NAPCAT_MJS_PATH).href);
|
await import(pathToFileURL(NAPCAT_MJS_PATH).href);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,28 @@
|
|||||||
{
|
{
|
||||||
"name": "napcat-develop",
|
"name": "napcat-develop",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "powershell ./nodeTest.ps1"
|
"dev": "powershell ./nodeTest.ps1",
|
||||||
|
"copy-env": "xcopy config ..\\napcat-shell\\dist\\config /E /I /Y"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"require": "./index.js"
|
||||||
},
|
},
|
||||||
"exports": {
|
"./*": {
|
||||||
".": {
|
"require": "./*"
|
||||||
"require": "./index.js"
|
|
||||||
},
|
|
||||||
"./*": {
|
|
||||||
"require": "./*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"fs-extra": "^11.3.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^22.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"fs-extra": "^11.3.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NapCatPathWrapper } from 'napcat-common/src/path';
|
import { NapCatPathWrapper } from 'napcat-common/src/path';
|
||||||
import { InitWebUi, WebUiConfig, webUiRuntimePort } from 'napcat-webui-backend/index';
|
import { InitWebUi, WebUiConfig, webUiRuntimePort } from 'napcat-webui-backend/index';
|
||||||
import { NapCatOneBot11Adapter } from 'napcat-onebot/index';
|
import { NapCatAdapterManager } from 'napcat-adapter';
|
||||||
import { NativePacketHandler } from 'napcat-core/packet/handler/client';
|
import { NativePacketHandler } from 'napcat-core/packet/handler/client';
|
||||||
import { FFmpegService } from 'napcat-core/helper/ffmpeg/ffmpeg';
|
import { FFmpegService } from 'napcat-core/helper/ffmpeg/ffmpeg';
|
||||||
import { logSubscription, LogWrapper } from 'napcat-core/helper/log';
|
import { logSubscription, LogWrapper } from 'napcat-core/helper/log';
|
||||||
@@ -38,7 +38,7 @@ export async function NCoreInitFramework (
|
|||||||
const logger = new LogWrapper(pathWrapper.logsPath);
|
const logger = new LogWrapper(pathWrapper.logsPath);
|
||||||
await applyPendingUpdates(pathWrapper, logger);
|
await applyPendingUpdates(pathWrapper, logger);
|
||||||
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
|
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
|
||||||
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVersion());
|
const wrapper = loadQQWrapper(basicInfoWrapper.QQMainPath, basicInfoWrapper.getFullQQVersion());
|
||||||
const nativePacketHandler = new NativePacketHandler({ logger }); // 初始化 NativePacketHandler 用于后续使用
|
const nativePacketHandler = new NativePacketHandler({ logger }); // 初始化 NativePacketHandler 用于后续使用
|
||||||
// nativePacketHandler.onAll((packet) => {
|
// nativePacketHandler.onAll((packet) => {
|
||||||
// console.log('[Packet]', packet.uin, packet.cmd, packet.hex_data);
|
// console.log('[Packet]', packet.uin, packet.cmd, packet.hex_data);
|
||||||
@@ -79,11 +79,14 @@ export async function NCoreInitFramework (
|
|||||||
// 启动WebUi
|
// 启动WebUi
|
||||||
WebUiDataRuntime.setWorkingEnv(NapCatCoreWorkingEnv.Framework);
|
WebUiDataRuntime.setWorkingEnv(NapCatCoreWorkingEnv.Framework);
|
||||||
InitWebUi(logger, pathWrapper, logSubscription, statusHelperSubscription).then().catch(e => logger.logError(e));
|
InitWebUi(logger, pathWrapper, logSubscription, statusHelperSubscription).then().catch(e => logger.logError(e));
|
||||||
// 初始化LLNC的Onebot实现
|
// 使用 NapCatAdapterManager 统一管理协议适配器
|
||||||
const oneBotAdapter = new NapCatOneBot11Adapter(loaderObject.core, loaderObject.context, pathWrapper);
|
const adapterManager = new NapCatAdapterManager(loaderObject.core, loaderObject.context, pathWrapper);
|
||||||
// 注册到 WebUiDataRuntime,供调试功能使用
|
await adapterManager.initAdapters();
|
||||||
WebUiDataRuntime.setOneBotContext(oneBotAdapter);
|
// 注册 OneBot 适配器到 WebUiDataRuntime,供调试功能使用
|
||||||
await oneBotAdapter.InitOneBot();
|
const oneBotAdapter = adapterManager.getOneBotAdapter();
|
||||||
|
if (oneBotAdapter) {
|
||||||
|
WebUiDataRuntime.setOneBotContext(oneBotAdapter);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NapCatFramework {
|
export class NapCatFramework {
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
{
|
{
|
||||||
"name": "napcat-framework",
|
"name": "napcat-framework",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
|
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./index.ts"
|
||||||
},
|
},
|
||||||
"exports": {
|
"./*": {
|
||||||
".": {
|
"import": "./*"
|
||||||
"import": "./index.ts"
|
|
||||||
},
|
|
||||||
"./*": {
|
|
||||||
"import": "./*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"napcat-core": "workspace:*",
|
|
||||||
"napcat-common": "workspace:*",
|
|
||||||
"napcat-onebot": "workspace:*",
|
|
||||||
"napcat-webui-backend": "workspace:*",
|
|
||||||
"napcat-vite": "workspace:*",
|
|
||||||
"napcat-qrcode": "workspace:*"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^22.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"napcat-core": "workspace:*",
|
||||||
|
"napcat-common": "workspace:*",
|
||||||
|
"napcat-adapter": "workspace:*",
|
||||||
|
"napcat-webui-backend": "workspace:*",
|
||||||
|
"napcat-vite": "workspace:*",
|
||||||
|
"napcat-qrcode": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,9 @@ const FrameworkBaseConfig = () =>
|
|||||||
'@/napcat-onebot': resolve(__dirname, '../napcat-onebot'),
|
'@/napcat-onebot': resolve(__dirname, '../napcat-onebot'),
|
||||||
'@/napcat-pty': resolve(__dirname, '../napcat-pty'),
|
'@/napcat-pty': resolve(__dirname, '../napcat-pty'),
|
||||||
'@/napcat-webui-backend': resolve(__dirname, '../napcat-webui-backend'),
|
'@/napcat-webui-backend': resolve(__dirname, '../napcat-webui-backend'),
|
||||||
'@/image-size': resolve(__dirname, '../image-size'),
|
'@/napcat-image-size': resolve(__dirname, '../napcat-image-size'),
|
||||||
|
'@/napcat-protocol': resolve(__dirname, '../napcat-protocol'),
|
||||||
|
'@/napcat-adapter': resolve(__dirname, '../napcat-adapter'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
BIN
packages/napcat-image-size/resource/test-20x20.jpg
Normal file
BIN
packages/napcat-image-size/resource/test-20x20.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
packages/napcat-image-size/resource/test-20x20.png
Normal file
BIN
packages/napcat-image-size/resource/test-20x20.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
packages/napcat-image-size/resource/test-20x20.tiff
Normal file
BIN
packages/napcat-image-size/resource/test-20x20.tiff
Normal file
Binary file not shown.
BIN
packages/napcat-image-size/resource/test-20x20.webp
Normal file
BIN
packages/napcat-image-size/resource/test-20x20.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 962 B |
BIN
packages/napcat-image-size/resource/test-490x498.gif
Normal file
BIN
packages/napcat-image-size/resource/test-490x498.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
@@ -1,5 +1,12 @@
|
|||||||
|
import { BmpParser } from '@/napcat-image-size/src/parser/BmpParser';
|
||||||
|
import { GifParser } from '@/napcat-image-size/src/parser/GifParser';
|
||||||
|
import { JpegParser } from '@/napcat-image-size/src/parser/JpegParser';
|
||||||
|
import { PngParser } from '@/napcat-image-size/src/parser/PngParser';
|
||||||
|
import { TiffParser } from '@/napcat-image-size/src/parser/TiffParser';
|
||||||
|
import { WebpParser } from '@/napcat-image-size/src/parser/WebpParser';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { ReadStream } from 'fs';
|
import { ReadStream } from 'fs';
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
export interface ImageSize {
|
export interface ImageSize {
|
||||||
width: number;
|
width: number;
|
||||||
@@ -12,17 +19,18 @@ export enum ImageType {
|
|||||||
BMP = 'bmp',
|
BMP = 'bmp',
|
||||||
GIF = 'gif',
|
GIF = 'gif',
|
||||||
WEBP = 'webp',
|
WEBP = 'webp',
|
||||||
|
TIFF = 'tiff',
|
||||||
UNKNOWN = 'unknown',
|
UNKNOWN = 'unknown',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImageParser {
|
export interface ImageParser {
|
||||||
readonly type: ImageType;
|
readonly type: ImageType;
|
||||||
canParse(buffer: Buffer): boolean;
|
canParse (buffer: Buffer): boolean;
|
||||||
parseSize(stream: ReadStream): Promise<ImageSize | undefined>;
|
parseSize (stream: ReadStream): Promise<ImageSize | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 魔术匹配
|
// 魔术匹配
|
||||||
function matchMagic (buffer: Buffer, magic: number[], offset = 0): boolean {
|
export function matchMagic (buffer: Buffer, magic: number[], offset = 0): boolean {
|
||||||
if (buffer.length < offset + magic.length) {
|
if (buffer.length < offset + magic.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -35,316 +43,39 @@ function matchMagic (buffer: Buffer, magic: number[], offset = 0): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PNG解析器
|
// 所有解析器实例
|
||||||
class PngParser implements ImageParser {
|
const parserInstances = {
|
||||||
readonly type = ImageType.PNG;
|
png: new PngParser(),
|
||||||
// PNG 魔术头:89 50 4E 47 0D 0A 1A 0A
|
jpeg: new JpegParser(),
|
||||||
private readonly PNG_SIGNATURE = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
|
bmp: new BmpParser(),
|
||||||
|
gif: new GifParser(),
|
||||||
|
webp: new WebpParser(),
|
||||||
|
tiff: new TiffParser(),
|
||||||
|
};
|
||||||
|
|
||||||
canParse (buffer: Buffer): boolean {
|
// 首字节到可能的图片类型映射,用于快速筛选
|
||||||
return matchMagic(buffer, this.PNG_SIGNATURE);
|
const firstByteMap = new Map<number, ImageType[]>([
|
||||||
}
|
[0x42, [ImageType.BMP]], // 'B' - BMP
|
||||||
|
[0x47, [ImageType.GIF]], // 'G' - GIF
|
||||||
|
[0x49, [ImageType.TIFF]], // 'I' - TIFF (II - little endian)
|
||||||
|
[0x4D, [ImageType.TIFF]], // 'M' - TIFF (MM - big endian)
|
||||||
|
[0x52, [ImageType.WEBP]], // 'R' - RIFF (WebP)
|
||||||
|
[0x89, [ImageType.PNG]], // PNG signature
|
||||||
|
[0xFF, [ImageType.JPEG]], // JPEG SOI
|
||||||
|
]);
|
||||||
|
|
||||||
async parseSize (stream: ReadStream): Promise<ImageSize | undefined> {
|
// 类型到解析器的映射
|
||||||
return new Promise((resolve, reject) => {
|
const typeToParser = new Map<ImageType, ImageParser>([
|
||||||
stream.once('error', reject);
|
[ImageType.PNG, parserInstances.png],
|
||||||
stream.once('readable', () => {
|
[ImageType.JPEG, parserInstances.jpeg],
|
||||||
const buf = stream.read(24) as Buffer;
|
[ImageType.BMP, parserInstances.bmp],
|
||||||
if (!buf || buf.length < 24) {
|
[ImageType.GIF, parserInstances.gif],
|
||||||
return resolve(undefined);
|
[ImageType.WEBP, parserInstances.webp],
|
||||||
}
|
[ImageType.TIFF, parserInstances.tiff],
|
||||||
if (this.canParse(buf)) {
|
]);
|
||||||
const width = buf.readUInt32BE(16);
|
|
||||||
const height = buf.readUInt32BE(20);
|
|
||||||
resolve({ width, height });
|
|
||||||
} else {
|
|
||||||
resolve(undefined);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// JPEG解析器
|
// 所有解析器列表(用于回退)
|
||||||
class JpegParser implements ImageParser {
|
const parsers: ReadonlyArray<ImageParser> = Object.values(parserInstances);
|
||||||
readonly type = ImageType.JPEG;
|
|
||||||
// JPEG 魔术头:FF D8
|
|
||||||
private readonly JPEG_SIGNATURE = [0xFF, 0xD8];
|
|
||||||
|
|
||||||
// JPEG标记常量
|
|
||||||
private readonly SOF_MARKERS = {
|
|
||||||
SOF0: 0xC0, // 基线DCT
|
|
||||||
SOF1: 0xC1, // 扩展顺序DCT
|
|
||||||
SOF2: 0xC2, // 渐进式DCT
|
|
||||||
SOF3: 0xC3, // 无损
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
// 非SOF标记
|
|
||||||
private readonly NON_SOF_MARKERS: number[] = [
|
|
||||||
0xC4, // DHT
|
|
||||||
0xC8, // JPEG扩展
|
|
||||||
0xCC, // DAC
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
canParse (buffer: Buffer): boolean {
|
|
||||||
return matchMagic(buffer, this.JPEG_SIGNATURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
isSOFMarker (marker: number): boolean {
|
|
||||||
return (
|
|
||||||
marker === this.SOF_MARKERS.SOF0 ||
|
|
||||||
marker === this.SOF_MARKERS.SOF1 ||
|
|
||||||
marker === this.SOF_MARKERS.SOF2 ||
|
|
||||||
marker === this.SOF_MARKERS.SOF3
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
isNonSOFMarker (marker: number): boolean {
|
|
||||||
return this.NON_SOF_MARKERS.includes(marker);
|
|
||||||
}
|
|
||||||
|
|
||||||
async parseSize (stream: ReadStream): Promise<ImageSize | undefined> {
|
|
||||||
return new Promise<ImageSize | undefined>((resolve, reject) => {
|
|
||||||
const BUFFER_SIZE = 1024; // 读取块大小,可以根据需要调整
|
|
||||||
let buffer = Buffer.alloc(0);
|
|
||||||
let offset = 0;
|
|
||||||
let found = false;
|
|
||||||
|
|
||||||
// 处理错误
|
|
||||||
stream.on('error', (err) => {
|
|
||||||
stream.destroy();
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 处理数据块
|
|
||||||
stream.on('data', (chunk: Buffer | string) => {
|
|
||||||
// 追加新数据到缓冲区
|
|
||||||
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
||||||
buffer = Buffer.concat([buffer.subarray(offset), chunkBuffer]);
|
|
||||||
offset = 0;
|
|
||||||
|
|
||||||
// 保持缓冲区在合理大小内,只保留最后的部分用于跨块匹配
|
|
||||||
const bufferSize = buffer.length;
|
|
||||||
const MIN_REQUIRED_BYTES = 10; // SOF段最低字节数
|
|
||||||
|
|
||||||
// 从JPEG头部后开始扫描
|
|
||||||
while (offset < bufferSize - MIN_REQUIRED_BYTES) {
|
|
||||||
// 寻找FF标记
|
|
||||||
if (buffer[offset] === 0xFF && buffer[offset + 1]! >= 0xC0 && buffer[offset + 1]! <= 0xCF) {
|
|
||||||
const marker = buffer[offset + 1];
|
|
||||||
if (!marker) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// 跳过非SOF标记
|
|
||||||
if (this.isNonSOFMarker(marker)) {
|
|
||||||
offset += 2;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理SOF标记 (包含尺寸信息)
|
|
||||||
if (this.isSOFMarker(marker)) {
|
|
||||||
// 确保缓冲区中有足够数据读取尺寸
|
|
||||||
if (offset + 9 < bufferSize) {
|
|
||||||
// 解析尺寸: FF XX YY YY PP HH HH WW WW ...
|
|
||||||
// XX = 标记, YY YY = 段长度, PP = 精度, HH HH = 高, WW WW = 宽
|
|
||||||
const height = buffer.readUInt16BE(offset + 5);
|
|
||||||
const width = buffer.readUInt16BE(offset + 7);
|
|
||||||
|
|
||||||
found = true;
|
|
||||||
stream.destroy();
|
|
||||||
resolve({ width, height });
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
// 如果缓冲区内数据不够,保留当前位置等待更多数据
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
offset++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 缓冲区管理: 如果处理了许多数据但没找到标记,
|
|
||||||
// 保留最后N字节用于跨块匹配,丢弃之前的数据
|
|
||||||
if (offset > BUFFER_SIZE) {
|
|
||||||
const KEEP_BYTES = 20; // 保留足够数据以处理跨块边界的情况
|
|
||||||
if (offset > KEEP_BYTES) {
|
|
||||||
buffer = buffer.subarray(offset - KEEP_BYTES);
|
|
||||||
offset = KEEP_BYTES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 处理流结束
|
|
||||||
stream.on('end', () => {
|
|
||||||
if (!found) {
|
|
||||||
resolve(undefined);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BMP解析器
|
|
||||||
class BmpParser implements ImageParser {
|
|
||||||
readonly type = ImageType.BMP;
|
|
||||||
// BMP 魔术头:42 4D (BM)
|
|
||||||
private readonly BMP_SIGNATURE = [0x42, 0x4D];
|
|
||||||
|
|
||||||
canParse (buffer: Buffer): boolean {
|
|
||||||
return matchMagic(buffer, this.BMP_SIGNATURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
async parseSize (stream: ReadStream): Promise<ImageSize | undefined> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
stream.once('error', reject);
|
|
||||||
stream.once('readable', () => {
|
|
||||||
const buf = stream.read(26) as Buffer;
|
|
||||||
if (!buf || buf.length < 26) {
|
|
||||||
return resolve(undefined);
|
|
||||||
}
|
|
||||||
if (this.canParse(buf)) {
|
|
||||||
const width = buf.readUInt32LE(18);
|
|
||||||
const height = buf.readUInt32LE(22);
|
|
||||||
resolve({ width, height });
|
|
||||||
} else {
|
|
||||||
resolve(undefined);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GIF解析器
|
|
||||||
class GifParser implements ImageParser {
|
|
||||||
readonly type = ImageType.GIF;
|
|
||||||
// GIF87a 魔术头:47 49 46 38 37 61
|
|
||||||
private readonly GIF87A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x37, 0x61];
|
|
||||||
// GIF89a 魔术头:47 49 46 38 39 61
|
|
||||||
private readonly GIF89A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61];
|
|
||||||
|
|
||||||
canParse (buffer: Buffer): boolean {
|
|
||||||
return (
|
|
||||||
matchMagic(buffer, this.GIF87A_SIGNATURE) ||
|
|
||||||
matchMagic(buffer, this.GIF89A_SIGNATURE)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async parseSize (stream: ReadStream): Promise<ImageSize | undefined> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
stream.once('error', reject);
|
|
||||||
stream.once('readable', () => {
|
|
||||||
const buf = stream.read(10) as Buffer;
|
|
||||||
if (!buf || buf.length < 10) {
|
|
||||||
return resolve(undefined);
|
|
||||||
}
|
|
||||||
if (this.canParse(buf)) {
|
|
||||||
const width = buf.readUInt16LE(6);
|
|
||||||
const height = buf.readUInt16LE(8);
|
|
||||||
resolve({ width, height });
|
|
||||||
} else {
|
|
||||||
resolve(undefined);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WEBP解析器 - 完整支持VP8, VP8L, VP8X格式
|
|
||||||
class WebpParser implements ImageParser {
|
|
||||||
readonly type = ImageType.WEBP;
|
|
||||||
// WEBP RIFF 头:52 49 46 46 (RIFF)
|
|
||||||
private readonly RIFF_SIGNATURE = [0x52, 0x49, 0x46, 0x46];
|
|
||||||
// WEBP 魔术头:57 45 42 50 (WEBP)
|
|
||||||
private readonly WEBP_SIGNATURE = [0x57, 0x45, 0x42, 0x50];
|
|
||||||
|
|
||||||
// WEBP 块头
|
|
||||||
private readonly CHUNK_VP8 = [0x56, 0x50, 0x38, 0x20]; // "VP8 "
|
|
||||||
private readonly CHUNK_VP8L = [0x56, 0x50, 0x38, 0x4C]; // "VP8L"
|
|
||||||
private readonly CHUNK_VP8X = [0x56, 0x50, 0x38, 0x58]; // "VP8X"
|
|
||||||
|
|
||||||
canParse (buffer: Buffer): boolean {
|
|
||||||
return (
|
|
||||||
buffer.length >= 12 &&
|
|
||||||
matchMagic(buffer, this.RIFF_SIGNATURE, 0) &&
|
|
||||||
matchMagic(buffer, this.WEBP_SIGNATURE, 8)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
isChunkType (buffer: Buffer, offset: number, chunkType: number[]): boolean {
|
|
||||||
return buffer.length >= offset + 4 && matchMagic(buffer, chunkType, offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
async parseSize (stream: ReadStream): Promise<ImageSize | undefined> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// 需要读取足够的字节来检测所有三种格式
|
|
||||||
const MAX_HEADER_SIZE = 32;
|
|
||||||
let totalBytes = 0;
|
|
||||||
let buffer = Buffer.alloc(0);
|
|
||||||
|
|
||||||
stream.on('error', reject);
|
|
||||||
|
|
||||||
stream.on('data', (chunk: Buffer | string) => {
|
|
||||||
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
||||||
buffer = Buffer.concat([buffer, chunkBuffer]);
|
|
||||||
totalBytes += chunk.length;
|
|
||||||
|
|
||||||
// 检查是否有足够的字节进行格式检测
|
|
||||||
if (totalBytes >= MAX_HEADER_SIZE) {
|
|
||||||
stream.destroy();
|
|
||||||
|
|
||||||
// 检查基本的WEBP签名
|
|
||||||
if (!this.canParse(buffer)) {
|
|
||||||
return resolve(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查chunk头部,位于字节12-15
|
|
||||||
if (this.isChunkType(buffer, 12, this.CHUNK_VP8)) {
|
|
||||||
// VP8格式 - 标准WebP
|
|
||||||
// 宽度和高度在帧头中
|
|
||||||
const width = buffer.readUInt16LE(26) & 0x3FFF;
|
|
||||||
const height = buffer.readUInt16LE(28) & 0x3FFF;
|
|
||||||
return resolve({ width, height });
|
|
||||||
} else if (this.isChunkType(buffer, 12, this.CHUNK_VP8L)) {
|
|
||||||
// VP8L格式 - 无损WebP
|
|
||||||
// 1字节标记后是14位宽度和14位高度
|
|
||||||
const bits = buffer.readUInt32LE(21);
|
|
||||||
const width = 1 + (bits & 0x3FFF);
|
|
||||||
const height = 1 + ((bits >> 14) & 0x3FFF);
|
|
||||||
return resolve({ width, height });
|
|
||||||
} else if (this.isChunkType(buffer, 12, this.CHUNK_VP8X)) {
|
|
||||||
// VP8X格式 - 扩展WebP
|
|
||||||
// 24位宽度和高度(减去1)
|
|
||||||
if (!buffer[24] || !buffer[25] || !buffer[26] || !buffer[27] || !buffer[28] || !buffer[29]) {
|
|
||||||
return resolve(undefined);
|
|
||||||
}
|
|
||||||
const width = 1 + ((buffer[24] | (buffer[25] << 8) | (buffer[26] << 16)) & 0xFFFFFF);
|
|
||||||
const height = 1 + ((buffer[27] | (buffer[28] << 8) | (buffer[29] << 16)) & 0xFFFFFF);
|
|
||||||
return resolve({ width, height });
|
|
||||||
} else {
|
|
||||||
// 未知的WebP子格式
|
|
||||||
return resolve(undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.on('end', () => {
|
|
||||||
// 如果没有读到足够的字节
|
|
||||||
if (totalBytes < MAX_HEADER_SIZE) {
|
|
||||||
resolve(undefined);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsers: ReadonlyArray<ImageParser> = [
|
|
||||||
new PngParser(),
|
|
||||||
new JpegParser(),
|
|
||||||
new BmpParser(),
|
|
||||||
new GifParser(),
|
|
||||||
new WebpParser(),
|
|
||||||
];
|
|
||||||
|
|
||||||
export async function detectImageType (filePath: string): Promise<ImageType> {
|
export async function detectImageType (filePath: string): Promise<ImageType> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -354,18 +85,22 @@ export async function detectImageType (filePath: string): Promise<ImageType> {
|
|||||||
end: 63,
|
end: 63,
|
||||||
});
|
});
|
||||||
|
|
||||||
let buffer: Buffer | null = null;
|
const chunks: Buffer[] = [];
|
||||||
|
|
||||||
stream.once('error', (err) => {
|
stream.on('error', (err) => {
|
||||||
stream.destroy();
|
stream.destroy();
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
stream.once('readable', () => {
|
stream.on('data', (chunk: Buffer | string) => {
|
||||||
buffer = stream.read(64) as Buffer;
|
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||||
stream.destroy();
|
chunks.push(chunkBuffer);
|
||||||
|
});
|
||||||
|
|
||||||
if (!buffer) {
|
stream.on('end', () => {
|
||||||
|
const buffer = Buffer.concat(chunks);
|
||||||
|
|
||||||
|
if (buffer.length === 0) {
|
||||||
return resolve(ImageType.UNKNOWN);
|
return resolve(ImageType.UNKNOWN);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,12 +112,6 @@ export async function detectImageType (filePath: string): Promise<ImageType> {
|
|||||||
|
|
||||||
resolve(ImageType.UNKNOWN);
|
resolve(ImageType.UNKNOWN);
|
||||||
});
|
});
|
||||||
|
|
||||||
stream.once('end', () => {
|
|
||||||
if (!buffer) {
|
|
||||||
resolve(ImageType.UNKNOWN);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,7 +119,7 @@ export async function imageSizeFromFile (filePath: string): Promise<ImageSize |
|
|||||||
try {
|
try {
|
||||||
// 先检测类型
|
// 先检测类型
|
||||||
const type = await detectImageType(filePath);
|
const type = await detectImageType(filePath);
|
||||||
const parser = parsers.find(p => p.type === type);
|
const parser = typeToParser.get(type);
|
||||||
if (!parser) {
|
if (!parser) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -422,3 +151,71 @@ export async function imageSizeFallBack (
|
|||||||
): Promise<ImageSize> {
|
): Promise<ImageSize> {
|
||||||
return await imageSizeFromFile(filePath) ?? fallback;
|
return await imageSizeFromFile(filePath) ?? fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从 Buffer 创建可读流
|
||||||
|
function bufferToReadStream (buffer: Buffer): ReadStream {
|
||||||
|
const readable = new Readable({
|
||||||
|
read () {
|
||||||
|
this.push(buffer);
|
||||||
|
this.push(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return readable as unknown as ReadStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 Buffer 检测图片类型(使用首字节快速筛选)
|
||||||
|
export function detectImageTypeFromBuffer (buffer: Buffer): ImageType {
|
||||||
|
if (buffer.length === 0) {
|
||||||
|
return ImageType.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstByte = buffer[0]!;
|
||||||
|
const possibleTypes = firstByteMap.get(firstByte);
|
||||||
|
|
||||||
|
if (possibleTypes) {
|
||||||
|
// 根据首字节快速筛选可能的类型
|
||||||
|
for (const type of possibleTypes) {
|
||||||
|
const parser = typeToParser.get(type);
|
||||||
|
if (parser && parser.canParse(buffer)) {
|
||||||
|
return parser.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回退:遍历所有解析器
|
||||||
|
for (const parser of parsers) {
|
||||||
|
if (parser.canParse(buffer)) {
|
||||||
|
return parser.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ImageType.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 Buffer 解析图片尺寸
|
||||||
|
export async function imageSizeFromBuffer (buffer: Buffer): Promise<ImageSize | undefined> {
|
||||||
|
const type = detectImageTypeFromBuffer(buffer);
|
||||||
|
const parser = typeToParser.get(type);
|
||||||
|
if (!parser) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stream = bufferToReadStream(buffer);
|
||||||
|
return await parser.parseSize(stream);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`解析图片尺寸出错: ${err}`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 Buffer 解析图片尺寸,带回退值
|
||||||
|
export async function imageSizeFromBufferFallBack (
|
||||||
|
buffer: Buffer,
|
||||||
|
fallback: ImageSize = {
|
||||||
|
width: 1024,
|
||||||
|
height: 1024,
|
||||||
|
}
|
||||||
|
): Promise<ImageSize> {
|
||||||
|
return await imageSizeFromBuffer(buffer) ?? fallback;
|
||||||
|
}
|
||||||
|
|||||||
32
packages/napcat-image-size/src/parser/BmpParser.ts
Normal file
32
packages/napcat-image-size/src/parser/BmpParser.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { ImageParser, ImageType, matchMagic, ImageSize } from '@/napcat-image-size/src';
|
||||||
|
import { ReadStream } from 'fs';
|
||||||
|
|
||||||
|
// BMP解析器
|
||||||
|
export class BmpParser implements ImageParser {
|
||||||
|
readonly type = ImageType.BMP;
|
||||||
|
// BMP 魔术头:42 4D (BM)
|
||||||
|
private readonly BMP_SIGNATURE = [0x42, 0x4D];
|
||||||
|
|
||||||
|
canParse (buffer: Buffer): boolean {
|
||||||
|
return matchMagic(buffer, this.BMP_SIGNATURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseSize (stream: ReadStream): Promise<ImageSize | undefined> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
stream.once('error', reject);
|
||||||
|
stream.once('readable', () => {
|
||||||
|
const buf = stream.read(26) as Buffer;
|
||||||
|
if (!buf || buf.length < 26) {
|
||||||
|
return resolve(undefined);
|
||||||
|
}
|
||||||
|
if (this.canParse(buf)) {
|
||||||
|
const width = buf.readUInt32LE(18);
|
||||||
|
const height = buf.readUInt32LE(22);
|
||||||
|
resolve({ width, height });
|
||||||
|
} else {
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
37
packages/napcat-image-size/src/parser/GifParser.ts
Normal file
37
packages/napcat-image-size/src/parser/GifParser.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { ImageParser, ImageType, matchMagic, ImageSize } from '@/napcat-image-size/src';
|
||||||
|
import { ReadStream } from 'fs';
|
||||||
|
|
||||||
|
// GIF解析器
|
||||||
|
export class GifParser implements ImageParser {
|
||||||
|
readonly type = ImageType.GIF;
|
||||||
|
// GIF87a 魔术头:47 49 46 38 37 61
|
||||||
|
private readonly GIF87A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x37, 0x61];
|
||||||
|
// GIF89a 魔术头:47 49 46 38 39 61
|
||||||
|
private readonly GIF89A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61];
|
||||||
|
|
||||||
|
canParse (buffer: Buffer): boolean {
|
||||||
|
return (
|
||||||
|
matchMagic(buffer, this.GIF87A_SIGNATURE) ||
|
||||||
|
matchMagic(buffer, this.GIF89A_SIGNATURE)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseSize (stream: ReadStream): Promise<ImageSize | undefined> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
stream.once('error', reject);
|
||||||
|
stream.once('readable', () => {
|
||||||
|
const buf = stream.read(10) as Buffer;
|
||||||
|
if (!buf || buf.length < 10) {
|
||||||
|
return resolve(undefined);
|
||||||
|
}
|
||||||
|
if (this.canParse(buf)) {
|
||||||
|
const width = buf.readUInt16LE(6);
|
||||||
|
const height = buf.readUInt16LE(8);
|
||||||
|
resolve({ width, height });
|
||||||
|
} else {
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
123
packages/napcat-image-size/src/parser/JpegParser.ts
Normal file
123
packages/napcat-image-size/src/parser/JpegParser.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { ImageParser, ImageType, matchMagic, ImageSize } from '@/napcat-image-size/src';
|
||||||
|
import { ReadStream } from 'fs';
|
||||||
|
|
||||||
|
// JPEG解析器
|
||||||
|
export class JpegParser implements ImageParser {
|
||||||
|
readonly type = ImageType.JPEG;
|
||||||
|
// JPEG 魔术头:FF D8
|
||||||
|
private readonly JPEG_SIGNATURE = [0xFF, 0xD8];
|
||||||
|
|
||||||
|
// JPEG标记常量
|
||||||
|
private readonly SOF_MARKERS = {
|
||||||
|
SOF0: 0xC0, // 基线DCT
|
||||||
|
SOF1: 0xC1, // 扩展顺序DCT
|
||||||
|
SOF2: 0xC2, // 渐进式DCT
|
||||||
|
SOF3: 0xC3, // 无损
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// 非SOF标记
|
||||||
|
private readonly NON_SOF_MARKERS: number[] = [
|
||||||
|
0xC4, // DHT
|
||||||
|
0xC8, // JPEG扩展
|
||||||
|
0xCC, // DAC
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
canParse (buffer: Buffer): boolean {
|
||||||
|
return matchMagic(buffer, this.JPEG_SIGNATURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
isSOFMarker (marker: number): boolean {
|
||||||
|
return (
|
||||||
|
marker === this.SOF_MARKERS.SOF0 ||
|
||||||
|
marker === this.SOF_MARKERS.SOF1 ||
|
||||||
|
marker === this.SOF_MARKERS.SOF2 ||
|
||||||
|
marker === this.SOF_MARKERS.SOF3
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isNonSOFMarker (marker: number): boolean {
|
||||||
|
return this.NON_SOF_MARKERS.includes(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseSize (stream: ReadStream): Promise<ImageSize | undefined> {
|
||||||
|
return new Promise<ImageSize | undefined>((resolve, reject) => {
|
||||||
|
const BUFFER_SIZE = 1024; // 读取块大小,可以根据需要调整
|
||||||
|
let buffer = Buffer.alloc(0);
|
||||||
|
let offset = 0;
|
||||||
|
let found = false;
|
||||||
|
|
||||||
|
// 处理错误
|
||||||
|
stream.on('error', (err) => {
|
||||||
|
stream.destroy();
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理数据块
|
||||||
|
stream.on('data', (chunk: Buffer | string) => {
|
||||||
|
// 追加新数据到缓冲区
|
||||||
|
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||||
|
buffer = Buffer.concat([buffer.subarray(offset), chunkBuffer]);
|
||||||
|
offset = 0;
|
||||||
|
|
||||||
|
// 保持缓冲区在合理大小内,只保留最后的部分用于跨块匹配
|
||||||
|
const bufferSize = buffer.length;
|
||||||
|
const MIN_REQUIRED_BYTES = 10; // SOF段最低字节数
|
||||||
|
|
||||||
|
|
||||||
|
// 从JPEG头部后开始扫描
|
||||||
|
while (offset < bufferSize - MIN_REQUIRED_BYTES) {
|
||||||
|
// 寻找FF标记
|
||||||
|
if (buffer[offset] === 0xFF && buffer[offset + 1]! >= 0xC0 && buffer[offset + 1]! <= 0xCF) {
|
||||||
|
const marker = buffer[offset + 1];
|
||||||
|
if (!marker) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// 跳过非SOF标记
|
||||||
|
if (this.isNonSOFMarker(marker)) {
|
||||||
|
offset += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理SOF标记 (包含尺寸信息)
|
||||||
|
if (this.isSOFMarker(marker)) {
|
||||||
|
// 确保缓冲区中有足够数据读取尺寸
|
||||||
|
if (offset + 9 < bufferSize) {
|
||||||
|
// 解析尺寸: FF XX YY YY PP HH HH WW WW ...
|
||||||
|
// XX = 标记, YY YY = 段长度, PP = 精度, HH HH = 高, WW WW = 宽
|
||||||
|
const height = buffer.readUInt16BE(offset + 5);
|
||||||
|
const width = buffer.readUInt16BE(offset + 7);
|
||||||
|
|
||||||
|
found = true;
|
||||||
|
stream.destroy();
|
||||||
|
resolve({ width, height });
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// 如果缓冲区内数据不够,保留当前位置等待更多数据
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
offset++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓冲区管理: 如果处理了许多数据但没找到标记,
|
||||||
|
// 保留最后N字节用于跨块匹配,丢弃之前的数据
|
||||||
|
if (offset > BUFFER_SIZE) {
|
||||||
|
const KEEP_BYTES = 20; // 保留足够数据以处理跨块边界的情况
|
||||||
|
if (offset > KEEP_BYTES) {
|
||||||
|
buffer = buffer.subarray(offset - KEEP_BYTES);
|
||||||
|
offset = KEEP_BYTES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理流结束
|
||||||
|
stream.on('end', () => {
|
||||||
|
if (!found) {
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
32
packages/napcat-image-size/src/parser/PngParser.ts
Normal file
32
packages/napcat-image-size/src/parser/PngParser.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { ImageParser, ImageType, matchMagic, ImageSize } from '@/napcat-image-size/src';
|
||||||
|
import { ReadStream } from 'fs';
|
||||||
|
|
||||||
|
// PNG解析器
|
||||||
|
export class PngParser implements ImageParser {
|
||||||
|
readonly type = ImageType.PNG;
|
||||||
|
// PNG 魔术头:89 50 4E 47 0D 0A 1A 0A
|
||||||
|
private readonly PNG_SIGNATURE = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
|
||||||
|
|
||||||
|
canParse (buffer: Buffer): boolean {
|
||||||
|
return matchMagic(buffer, this.PNG_SIGNATURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseSize (stream: ReadStream): Promise<ImageSize | undefined> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
stream.once('error', reject);
|
||||||
|
stream.once('readable', () => {
|
||||||
|
const buf = stream.read(24) as Buffer;
|
||||||
|
if (!buf || buf.length < 24) {
|
||||||
|
return resolve(undefined);
|
||||||
|
}
|
||||||
|
if (this.canParse(buf)) {
|
||||||
|
const width = buf.readUInt32BE(16);
|
||||||
|
const height = buf.readUInt32BE(20);
|
||||||
|
resolve({ width, height });
|
||||||
|
} else {
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
124
packages/napcat-image-size/src/parser/TiffParser.ts
Normal file
124
packages/napcat-image-size/src/parser/TiffParser.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { ImageParser, ImageType, matchMagic, ImageSize } from '@/napcat-image-size/src';
|
||||||
|
import { ReadStream } from 'fs';
|
||||||
|
|
||||||
|
// TIFF解析器
|
||||||
|
export class TiffParser implements ImageParser {
|
||||||
|
readonly type = ImageType.TIFF;
|
||||||
|
// TIFF Little Endian 魔术头:49 49 2A 00 (II)
|
||||||
|
private readonly TIFF_LE_SIGNATURE = [0x49, 0x49, 0x2A, 0x00];
|
||||||
|
// TIFF Big Endian 魔术头:4D 4D 00 2A (MM)
|
||||||
|
private readonly TIFF_BE_SIGNATURE = [0x4D, 0x4D, 0x00, 0x2A];
|
||||||
|
|
||||||
|
canParse (buffer: Buffer): boolean {
|
||||||
|
return (
|
||||||
|
matchMagic(buffer, this.TIFF_LE_SIGNATURE) ||
|
||||||
|
matchMagic(buffer, this.TIFF_BE_SIGNATURE)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseSize (stream: ReadStream): Promise<ImageSize | undefined> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
let totalBytes = 0;
|
||||||
|
const MAX_BYTES = 64 * 1024; // 最多读取 64KB
|
||||||
|
|
||||||
|
stream.on('error', reject);
|
||||||
|
|
||||||
|
stream.on('data', (chunk: Buffer | string) => {
|
||||||
|
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||||
|
chunks.push(chunkBuffer);
|
||||||
|
totalBytes += chunkBuffer.length;
|
||||||
|
|
||||||
|
if (totalBytes >= MAX_BYTES) {
|
||||||
|
stream.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('end', () => {
|
||||||
|
const buffer = Buffer.concat(chunks);
|
||||||
|
const size = this.parseTiffSize(buffer);
|
||||||
|
resolve(size);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('close', () => {
|
||||||
|
if (chunks.length > 0) {
|
||||||
|
const buffer = Buffer.concat(chunks);
|
||||||
|
const size = this.parseTiffSize(buffer);
|
||||||
|
resolve(size);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseTiffSize (buffer: Buffer): ImageSize | undefined {
|
||||||
|
if (buffer.length < 8) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断字节序
|
||||||
|
const isLittleEndian = buffer[0] === 0x49; // 'I'
|
||||||
|
|
||||||
|
const readUInt16 = isLittleEndian
|
||||||
|
? (offset: number) => buffer.readUInt16LE(offset)
|
||||||
|
: (offset: number) => buffer.readUInt16BE(offset);
|
||||||
|
|
||||||
|
const readUInt32 = isLittleEndian
|
||||||
|
? (offset: number) => buffer.readUInt32LE(offset)
|
||||||
|
: (offset: number) => buffer.readUInt32BE(offset);
|
||||||
|
|
||||||
|
// 获取第一个 IFD 的偏移量
|
||||||
|
const ifdOffset = readUInt32(4);
|
||||||
|
if (ifdOffset + 2 > buffer.length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取 IFD 条目数量
|
||||||
|
const numEntries = readUInt16(ifdOffset);
|
||||||
|
let width: number | undefined;
|
||||||
|
let height: number | undefined;
|
||||||
|
|
||||||
|
// TIFF 标签
|
||||||
|
const TAG_IMAGE_WIDTH = 0x0100;
|
||||||
|
const TAG_IMAGE_HEIGHT = 0x0101;
|
||||||
|
|
||||||
|
// 遍历 IFD 条目
|
||||||
|
for (let i = 0; i < numEntries; i++) {
|
||||||
|
const entryOffset = ifdOffset + 2 + i * 12;
|
||||||
|
if (entryOffset + 12 > buffer.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tag = readUInt16(entryOffset);
|
||||||
|
const type = readUInt16(entryOffset + 2);
|
||||||
|
// const count = readUInt32(entryOffset + 4);
|
||||||
|
|
||||||
|
// 根据类型读取值
|
||||||
|
let value: number;
|
||||||
|
if (type === 3) {
|
||||||
|
// SHORT (2 bytes)
|
||||||
|
value = readUInt16(entryOffset + 8);
|
||||||
|
} else if (type === 4) {
|
||||||
|
// LONG (4 bytes)
|
||||||
|
value = readUInt32(entryOffset + 8);
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag === TAG_IMAGE_WIDTH) {
|
||||||
|
width = value;
|
||||||
|
} else if (tag === TAG_IMAGE_HEIGHT) {
|
||||||
|
height = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width !== undefined && height !== undefined) {
|
||||||
|
return { width, height };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width !== undefined && height !== undefined) {
|
||||||
|
return { width, height };
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
90
packages/napcat-image-size/src/parser/WebpParser.ts
Normal file
90
packages/napcat-image-size/src/parser/WebpParser.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { ImageParser, ImageType, matchMagic, ImageSize } from '@/napcat-image-size/src';
|
||||||
|
import { ReadStream } from 'fs';
|
||||||
|
|
||||||
|
// WEBP解析器 - 完整支持VP8, VP8L, VP8X格式
|
||||||
|
export class WebpParser implements ImageParser {
|
||||||
|
readonly type = ImageType.WEBP;
|
||||||
|
// WEBP RIFF 头:52 49 46 46 (RIFF)
|
||||||
|
private readonly RIFF_SIGNATURE = [0x52, 0x49, 0x46, 0x46];
|
||||||
|
// WEBP 魔术头:57 45 42 50 (WEBP)
|
||||||
|
private readonly WEBP_SIGNATURE = [0x57, 0x45, 0x42, 0x50];
|
||||||
|
|
||||||
|
// WEBP 块头
|
||||||
|
private readonly CHUNK_VP8 = [0x56, 0x50, 0x38, 0x20]; // "VP8 "
|
||||||
|
private readonly CHUNK_VP8L = [0x56, 0x50, 0x38, 0x4C]; // "VP8L"
|
||||||
|
private readonly CHUNK_VP8X = [0x56, 0x50, 0x38, 0x58]; // "VP8X"
|
||||||
|
|
||||||
|
canParse (buffer: Buffer): boolean {
|
||||||
|
return (
|
||||||
|
buffer.length >= 12 &&
|
||||||
|
matchMagic(buffer, this.RIFF_SIGNATURE, 0) &&
|
||||||
|
matchMagic(buffer, this.WEBP_SIGNATURE, 8)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isChunkType (buffer: Buffer, offset: number, chunkType: number[]): boolean {
|
||||||
|
return buffer.length >= offset + 4 && matchMagic(buffer, chunkType, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseSize (stream: ReadStream): Promise<ImageSize | undefined> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 需要读取足够的字节来检测所有三种格式
|
||||||
|
const MAX_HEADER_SIZE = 32;
|
||||||
|
let totalBytes = 0;
|
||||||
|
let buffer = Buffer.alloc(0);
|
||||||
|
|
||||||
|
stream.on('error', reject);
|
||||||
|
|
||||||
|
stream.on('data', (chunk: Buffer | string) => {
|
||||||
|
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||||
|
buffer = Buffer.concat([buffer, chunkBuffer]);
|
||||||
|
totalBytes += chunk.length;
|
||||||
|
|
||||||
|
// 检查是否有足够的字节进行格式检测
|
||||||
|
if (totalBytes >= MAX_HEADER_SIZE) {
|
||||||
|
stream.destroy();
|
||||||
|
|
||||||
|
// 检查基本的WEBP签名
|
||||||
|
if (!this.canParse(buffer)) {
|
||||||
|
return resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查chunk头部,位于字节12-15
|
||||||
|
if (this.isChunkType(buffer, 12, this.CHUNK_VP8)) {
|
||||||
|
// VP8格式 - 标准WebP
|
||||||
|
// 宽度和高度在帧头中
|
||||||
|
const width = buffer.readUInt16LE(26) & 0x3FFF;
|
||||||
|
const height = buffer.readUInt16LE(28) & 0x3FFF;
|
||||||
|
return resolve({ width, height });
|
||||||
|
} else if (this.isChunkType(buffer, 12, this.CHUNK_VP8L)) {
|
||||||
|
// VP8L格式 - 无损WebP
|
||||||
|
// 1字节标记后是14位宽度和14位高度
|
||||||
|
const bits = buffer.readUInt32LE(21);
|
||||||
|
const width = 1 + (bits & 0x3FFF);
|
||||||
|
const height = 1 + ((bits >> 14) & 0x3FFF);
|
||||||
|
return resolve({ width, height });
|
||||||
|
} else if (this.isChunkType(buffer, 12, this.CHUNK_VP8X)) {
|
||||||
|
// VP8X格式 - 扩展WebP
|
||||||
|
// 24位宽度和高度(减去1)
|
||||||
|
if (buffer.length < 30) {
|
||||||
|
return resolve(undefined);
|
||||||
|
}
|
||||||
|
const width = 1 + ((buffer[24]! | (buffer[25]! << 8) | (buffer[26]! << 16)) & 0xFFFFFF);
|
||||||
|
const height = 1 + ((buffer[27]! | (buffer[28]! << 8) | (buffer[29]! << 16)) & 0xFFFFFF);
|
||||||
|
return resolve({ width, height });
|
||||||
|
} else {
|
||||||
|
// 未知的WebP子格式
|
||||||
|
return resolve(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('end', () => {
|
||||||
|
// 如果没有读到足够的字节
|
||||||
|
if (totalBytes < MAX_HEADER_SIZE) {
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -5,9 +5,18 @@ import { NapCatOneBot11Adapter, OB11Return } from '@/napcat-onebot/index';
|
|||||||
import { NetworkAdapterConfig } from '../config/config';
|
import { NetworkAdapterConfig } from '../config/config';
|
||||||
import { TSchema } from '@sinclair/typebox';
|
import { TSchema } from '@sinclair/typebox';
|
||||||
import { StreamPacket, StreamPacketBasic, StreamStatus } from './stream/StreamBasic';
|
import { StreamPacket, StreamPacketBasic, StreamStatus } from './stream/StreamBasic';
|
||||||
|
export const ActionExamples = {
|
||||||
|
Common: {
|
||||||
|
errors: [
|
||||||
|
{ code: 1400, description: '请求参数错误或业务逻辑执行失败' },
|
||||||
|
{ code: 1401, description: '权限不足' },
|
||||||
|
{ code: 1404, description: '资源不存在' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export class OB11Response {
|
export class OB11Response {
|
||||||
private static createResponse<T>(data: T, status: string, retcode: number, message: string = '', echo: unknown = null, useStream: boolean = false): OB11Return<T> {
|
private static createResponse<T> (data: T, status: string, retcode: number, message: string = '', echo: unknown = null, useStream: boolean = false): OB11Return<T> {
|
||||||
return {
|
return {
|
||||||
status,
|
status,
|
||||||
retcode,
|
retcode,
|
||||||
@@ -19,11 +28,11 @@ export class OB11Response {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static res<T>(data: T, status: string, retcode: number, message: string = '', echo: unknown = null, useStream: boolean = false): OB11Return<T> {
|
static res<T> (data: T, status: string, retcode: number, message: string = '', echo: unknown = null, useStream: boolean = false): OB11Return<T> {
|
||||||
return this.createResponse(data, status, retcode, message, echo, useStream);
|
return this.createResponse(data, status, retcode, message, echo, useStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
static ok<T>(data: T, echo: unknown = null, useStream: boolean = false): OB11Return<T> {
|
static ok<T> (data: T, echo: unknown = null, useStream: boolean = false): OB11Return<T> {
|
||||||
return this.createResponse(data, 'ok', 0, '', echo, useStream);
|
return this.createResponse(data, 'ok', 0, '', echo, useStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,15 +41,22 @@ export class OB11Response {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
export abstract class OneBotRequestToolkit {
|
export abstract class OneBotRequestToolkit {
|
||||||
abstract send<T>(packet: StreamPacket<T>): Promise<void>;
|
abstract send<T> (packet: StreamPacket<T>): Promise<void>;
|
||||||
}
|
}
|
||||||
export abstract class OneBotAction<PayloadType, ReturnDataType> {
|
export abstract class OneBotAction<PayloadType, ReturnDataType> {
|
||||||
actionName: typeof ActionName[keyof typeof ActionName] = ActionName.Unknown;
|
actionName: typeof ActionName[keyof typeof ActionName] = ActionName.Unknown;
|
||||||
core: NapCatCore;
|
core: NapCatCore;
|
||||||
private validate?: ValidateFunction<unknown> = undefined;
|
private validate?: ValidateFunction<unknown> = undefined;
|
||||||
payloadSchema?: TSchema = undefined;
|
payloadSchema?: TSchema = undefined;
|
||||||
|
returnSchema?: TSchema = undefined;
|
||||||
|
payloadExample?: unknown = undefined;
|
||||||
|
returnExample?: unknown = undefined;
|
||||||
|
actionSummary: string = '';
|
||||||
|
actionDescription: string = '';
|
||||||
|
actionTags: string[] = [];
|
||||||
obContext: NapCatOneBot11Adapter;
|
obContext: NapCatOneBot11Adapter;
|
||||||
useStream: boolean = false;
|
useStream: boolean = false;
|
||||||
|
errorExamples: Array<{ code: number, description: string; }> = ActionExamples.Common.errors;
|
||||||
|
|
||||||
constructor (obContext: NapCatOneBot11Adapter, core: NapCatCore) {
|
constructor (obContext: NapCatOneBot11Adapter, core: NapCatCore) {
|
||||||
this.obContext = obContext;
|
this.obContext = obContext;
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
export const ExtendsActionsExamples = {
|
||||||
|
OCRImage: {
|
||||||
|
payload: { image: 'image_id_123' },
|
||||||
|
response: { texts: [{ text: '识别内容', coordinates: [] }] },
|
||||||
|
},
|
||||||
|
GetAiCharacters: {
|
||||||
|
payload: { group_id: '123456' },
|
||||||
|
response: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
characters: [
|
||||||
|
{ character_id: 'id', character_name: 'name', preview_url: 'url' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
GetClientkey: {
|
||||||
|
payload: {},
|
||||||
|
response: { clientkey: 'abcdef123456' },
|
||||||
|
},
|
||||||
|
SetQQAvatar: {
|
||||||
|
payload: { file: 'base64://...' },
|
||||||
|
response: null,
|
||||||
|
},
|
||||||
|
SetGroupKickMembers: {
|
||||||
|
payload: { group_id: '123456', user_id: ['123456789'], reject_add_request: false },
|
||||||
|
response: null,
|
||||||
|
},
|
||||||
|
TranslateEnWordToZn: {
|
||||||
|
payload: { words: ['hello'] },
|
||||||
|
response: { words: ['你好'] },
|
||||||
|
},
|
||||||
|
GetRkey: {
|
||||||
|
payload: {},
|
||||||
|
response: { rkey: '...' },
|
||||||
|
},
|
||||||
|
SetLongNick: {
|
||||||
|
payload: { longNick: '个性签名' },
|
||||||
|
response: null,
|
||||||
|
},
|
||||||
|
SetSpecialTitle: {
|
||||||
|
payload: { group_id: '123456', user_id: '123456789', special_title: '头衔' },
|
||||||
|
response: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
22
packages/napcat-onebot/action/example/FileActionsExamples.ts
Normal file
22
packages/napcat-onebot/action/example/FileActionsExamples.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export const FileActionsExamples = {
|
||||||
|
GetFile: {
|
||||||
|
payload: { file: 'file_id_123' },
|
||||||
|
response: { file: '/path/to/file', url: 'http://...', file_size: 1024, file_name: 'test.jpg' },
|
||||||
|
},
|
||||||
|
GetGroupFileUrl: {
|
||||||
|
payload: { group_id: '123456', file_id: 'file_id_123', busid: 102 },
|
||||||
|
response: { url: 'http://...' },
|
||||||
|
},
|
||||||
|
GetImage: {
|
||||||
|
payload: { file: 'image_id_123' },
|
||||||
|
response: { file: '/path/to/image', url: 'http://...' },
|
||||||
|
},
|
||||||
|
GetPrivateFileUrl: {
|
||||||
|
payload: { user_id: '123456789', file_id: 'file_id_123' },
|
||||||
|
response: { url: 'http://...' },
|
||||||
|
},
|
||||||
|
GetRecord: {
|
||||||
|
payload: { file: 'record_id_123', out_format: 'mp3' },
|
||||||
|
response: { file: '/path/to/record', url: 'http://...' },
|
||||||
|
},
|
||||||
|
};
|
||||||
102
packages/napcat-onebot/action/example/GoCQHTTPActionsExamples.ts
Normal file
102
packages/napcat-onebot/action/example/GoCQHTTPActionsExamples.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
export const GoCQHTTPActionsExamples = {
|
||||||
|
GetStrangerInfo: {
|
||||||
|
payload: { user_id: '123456789' },
|
||||||
|
response: { user_id: 123456789, nickname: '昵称', sex: 'unknown' },
|
||||||
|
},
|
||||||
|
GetGroupHonorInfo: {
|
||||||
|
payload: { group_id: '123456', type: 'all' },
|
||||||
|
response: { group_id: 123456, current_talkative: {}, talkative_list: [] },
|
||||||
|
},
|
||||||
|
GetForwardMsg: {
|
||||||
|
payload: { message_id: '123456' },
|
||||||
|
response: { messages: [] },
|
||||||
|
},
|
||||||
|
SendForwardMsg: {
|
||||||
|
payload: { group_id: '123456', messages: [] },
|
||||||
|
response: { message_id: 123456 },
|
||||||
|
},
|
||||||
|
GetGroupAtAllRemain: {
|
||||||
|
payload: { group_id: '123456' },
|
||||||
|
response: { can_at_all: true, remain_at_all_count_for_group: 10, remain_at_all_count_for_self: 10 },
|
||||||
|
},
|
||||||
|
CreateGroupFileFolder: {
|
||||||
|
payload: { group_id: '123456', name: '测试目录' },
|
||||||
|
response: { result: {}, groupItem: {} },
|
||||||
|
},
|
||||||
|
DeleteGroupFile: {
|
||||||
|
payload: { group_id: '123456', file_id: 'file_uuid_123' },
|
||||||
|
response: {},
|
||||||
|
},
|
||||||
|
DeleteGroupFileFolder: {
|
||||||
|
payload: { group_id: '123456', folder_id: 'folder_uuid_123' },
|
||||||
|
response: {},
|
||||||
|
},
|
||||||
|
DownloadFile: {
|
||||||
|
payload: { url: 'https://example.com/file.png', thread_count: 1, headers: 'User-Agent: NapCat' },
|
||||||
|
response: { file: '/path/to/downloaded/file' },
|
||||||
|
},
|
||||||
|
GetFriendMsgHistory: {
|
||||||
|
payload: { user_id: '123456789', message_seq: 0, count: 20 },
|
||||||
|
response: { messages: [] },
|
||||||
|
},
|
||||||
|
GetGroupFilesByFolder: {
|
||||||
|
payload: { group_id: '123456', folder_id: 'folder_id' },
|
||||||
|
response: { files: [], folders: [] },
|
||||||
|
},
|
||||||
|
GetGroupFileSystemInfo: {
|
||||||
|
payload: { group_id: '123456' },
|
||||||
|
response: { file_count: 10, limit_count: 10000, used_space: 1024, total_space: 10737418240 },
|
||||||
|
},
|
||||||
|
GetGroupMsgHistory: {
|
||||||
|
payload: { group_id: '123456', message_seq: 0, count: 20 },
|
||||||
|
response: { messages: [] },
|
||||||
|
},
|
||||||
|
GetGroupRootFiles: {
|
||||||
|
payload: { group_id: '123456' },
|
||||||
|
response: { files: [], folders: [] },
|
||||||
|
},
|
||||||
|
GetOnlineClient: {
|
||||||
|
payload: { no_cache: false },
|
||||||
|
response: [],
|
||||||
|
},
|
||||||
|
GoCQHTTPCheckUrlSafely: {
|
||||||
|
payload: { url: 'https://example.com' },
|
||||||
|
response: { level: 1 },
|
||||||
|
},
|
||||||
|
GoCQHTTPDeleteFriend: {
|
||||||
|
payload: { user_id: '123456789' },
|
||||||
|
response: {},
|
||||||
|
},
|
||||||
|
GoCQHTTPGetModelShow: {
|
||||||
|
payload: { model: 'iPhone 13' },
|
||||||
|
response: { variants: [] },
|
||||||
|
},
|
||||||
|
GoCQHTTPSetModelShow: {
|
||||||
|
payload: { model: 'iPhone 13', model_show: 'iPhone 13' },
|
||||||
|
response: {},
|
||||||
|
},
|
||||||
|
QuickAction: {
|
||||||
|
payload: { context: {}, operation: {} },
|
||||||
|
response: {},
|
||||||
|
},
|
||||||
|
SendGroupNotice: {
|
||||||
|
payload: { group_id: '123456', content: '公告内容', image: 'base64://...' },
|
||||||
|
response: {},
|
||||||
|
},
|
||||||
|
SetGroupPortrait: {
|
||||||
|
payload: { group_id: '123456', file: 'base64://...' },
|
||||||
|
response: { result: 0, errMsg: '' },
|
||||||
|
},
|
||||||
|
SetQQProfile: {
|
||||||
|
payload: { nickname: '新昵称', personal_note: '个性签名' },
|
||||||
|
response: {},
|
||||||
|
},
|
||||||
|
UploadGroupFile: {
|
||||||
|
payload: { group_id: '123456', file: '/path/to/file', name: 'test.txt' },
|
||||||
|
response: { file_id: 'file_uuid_123' },
|
||||||
|
},
|
||||||
|
UploadPrivateFile: {
|
||||||
|
payload: { user_id: '123456789', file: '/path/to/file', name: 'test.txt' },
|
||||||
|
response: { file_id: 'file_uuid_123' },
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
export const GroupActionsExamples = {
|
||||||
|
DelEssenceMsg: {
|
||||||
|
payload: { message_id: 123456 },
|
||||||
|
response: null,
|
||||||
|
},
|
||||||
|
DelGroupNotice: {
|
||||||
|
payload: { group_id: '123456', notice_id: 'notice_123' },
|
||||||
|
response: null,
|
||||||
|
},
|
||||||
|
GetGroupDetailInfo: {
|
||||||
|
payload: { group_id: '123456' },
|
||||||
|
response: { group_id: 123456, group_name: '测试群', member_count: 100, max_member_count: 500 },
|
||||||
|
},
|
||||||
|
GetGroupEssence: {
|
||||||
|
payload: { group_id: '123456' },
|
||||||
|
response: [{ message_id: 123456, sender_id: 123456, sender_nick: '昵称', operator_id: 123456, operator_nick: '昵称', operator_time: 1710000000, content: '精华内容' }],
|
||||||
|
},
|
||||||
|
GetGroupInfo: {
|
||||||
|
payload: { group_id: '123456' },
|
||||||
|
response: { group_id: 123456, group_name: '测试群', member_count: 100, max_member_count: 500 },
|
||||||
|
},
|
||||||
|
GetGroupList: {
|
||||||
|
payload: {},
|
||||||
|
response: [{ group_id: 123456, group_name: '测试群', member_count: 100, max_member_count: 500 }],
|
||||||
|
},
|
||||||
|
GetGroupMemberInfo: {
|
||||||
|
payload: { group_id: '123456', user_id: '123456789' },
|
||||||
|
response: { group_id: 123456, user_id: 123456789, nickname: '昵称', card: '名片', role: 'member' },
|
||||||
|
},
|
||||||
|
GetGroupMemberList: {
|
||||||
|
payload: { group_id: '123456' },
|
||||||
|
response: [{ group_id: 123456, user_id: 123456789, nickname: '昵称', card: '名片', role: 'member' }],
|
||||||
|
},
|
||||||
|
GetGroupNotice: {
|
||||||
|
payload: { group_id: '123456' },
|
||||||
|
response: [{ notice_id: 'notice_123', sender_id: 123456, publish_time: 1710000000, message: { text: '公告内容', image: [] } }],
|
||||||
|
},
|
||||||
|
SendGroupMsg: {
|
||||||
|
payload: { group_id: '123456', message: 'hello' },
|
||||||
|
response: { message_id: 123456 },
|
||||||
|
},
|
||||||
|
SetEssenceMsg: {
|
||||||
|
payload: { message_id: 123456 },
|
||||||
|
response: null,
|
||||||
|
},
|
||||||
|
SetGroupAddRequest: {
|
||||||
|
payload: { flag: 'flag_123', sub_type: 'add', approve: true },
|
||||||
|
response: null,
|
||||||
|
},
|
||||||
|
SetGroupAdmin: {
|
||||||
|
payload: { group_id: '123456', user_id: '123456789', enable: true },
|
||||||
|
response: null,
|
||||||
|
},
|
||||||
|
SetGroupBan: {
|
||||||
|
payload: { group_id: '123456', user_id: '123456789', duration: 1800 },
|
||||||
|
response: null,
|
||||||
|
},
|
||||||
|
SetGroupCard: {
|
||||||
|
payload: { group_id: '123456', user_id: '123456789', card: '新名片' },
|
||||||
|
response: null,
|
||||||
|
},
|
||||||
|
SetGroupKick: {
|
||||||
|
payload: { group_id: '123456', user_id: '123456789', reject_add_request: false },
|
||||||
|
response: null,
|
||||||
|
},
|
||||||
|
SetGroupLeave: {
|
||||||
|
payload: { group_id: '123456', is_dismiss: false },
|
||||||
|
response: null,
|
||||||
|
},
|
||||||
|
SetGroupName: {
|
||||||
|
payload: { group_id: '123456', group_name: '新群名' },
|
||||||
|
response: null,
|
||||||
|
},
|
||||||
|
SetGroupWholeBan: {
|
||||||
|
payload: { group_id: '123456', enable: true },
|
||||||
|
response: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export const GuildActionsExamples = {
|
||||||
|
GetGuildList: {
|
||||||
|
payload: {},
|
||||||
|
response: [{ guild_id: '123456', guild_name: '测试频道' }],
|
||||||
|
},
|
||||||
|
GetGuildProfile: {
|
||||||
|
payload: { guild_id: '123456' },
|
||||||
|
response: { guild_id: '123456', guild_name: '测试频道', guild_display_id: '123' },
|
||||||
|
},
|
||||||
|
};
|
||||||
10
packages/napcat-onebot/action/example/NewActionsExamples.ts
Normal file
10
packages/napcat-onebot/action/example/NewActionsExamples.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export const NewActionsExamples = {
|
||||||
|
GetDoubtFriendsAddRequest: {
|
||||||
|
payload: { count: 10 },
|
||||||
|
response: [{ user_id: 123456789, nickname: '昵称', age: 20, sex: 'male', reason: '申请理由', flag: 'flag_123' }],
|
||||||
|
},
|
||||||
|
SetDoubtFriendsAddRequest: {
|
||||||
|
payload: { flag: 'flag_123', approve: true },
|
||||||
|
response: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export const PacketActionsExamples = {
|
||||||
|
GetPacketStatus: {
|
||||||
|
payload: {},
|
||||||
|
response: { status: 'ok' },
|
||||||
|
},
|
||||||
|
SendPoke: {
|
||||||
|
payload: { user_id: '123456789' },
|
||||||
|
response: {},
|
||||||
|
},
|
||||||
|
SetGroupTodo: {
|
||||||
|
payload: { group_id: '123456', message_id: '123456789' },
|
||||||
|
response: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
export const SystemActionsExamples = {
|
||||||
|
CanSendImage: {
|
||||||
|
payload: {},
|
||||||
|
response: { yes: true },
|
||||||
|
},
|
||||||
|
CanSendRecord: {
|
||||||
|
payload: {},
|
||||||
|
response: { yes: true },
|
||||||
|
},
|
||||||
|
CleanCache: {
|
||||||
|
payload: {},
|
||||||
|
response: {},
|
||||||
|
},
|
||||||
|
GetCredentials: {
|
||||||
|
payload: {},
|
||||||
|
response: { cookies: '...', csrf_token: 123456789 },
|
||||||
|
},
|
||||||
|
GetCSRF: {
|
||||||
|
payload: {},
|
||||||
|
response: { token: 123456789 },
|
||||||
|
},
|
||||||
|
GetLoginInfo: {
|
||||||
|
payload: {},
|
||||||
|
response: { user_id: 123456789, nickname: '机器人' },
|
||||||
|
},
|
||||||
|
GetStatus: {
|
||||||
|
payload: {},
|
||||||
|
response: { online: true, good: true },
|
||||||
|
},
|
||||||
|
GetSystemMsg: {
|
||||||
|
payload: {},
|
||||||
|
response: { invited_requests: [], join_requests: [] },
|
||||||
|
},
|
||||||
|
GetVersionInfo: {
|
||||||
|
payload: {},
|
||||||
|
response: { app_name: 'NapCatQQ', app_version: '1.0.0', protocol_version: 'v11' },
|
||||||
|
},
|
||||||
|
SetRestart: {
|
||||||
|
payload: { delay: 0 },
|
||||||
|
response: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
38
packages/napcat-onebot/action/example/UserActionsExamples.ts
Normal file
38
packages/napcat-onebot/action/example/UserActionsExamples.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
export const UserActionsExamples = {
|
||||||
|
GetCookies: {
|
||||||
|
payload: { domain: 'qun.qq.com' },
|
||||||
|
response: { cookies: 'p_skey=xxx; p_uin=o0123456789;' },
|
||||||
|
},
|
||||||
|
GetFriendList: {
|
||||||
|
payload: {},
|
||||||
|
response: [{ user_id: 123456789, nickname: '昵称', remark: '备注' }],
|
||||||
|
},
|
||||||
|
GetRecentContact: {
|
||||||
|
payload: { count: 10 },
|
||||||
|
response: [
|
||||||
|
{
|
||||||
|
lastestMsg: 'hello',
|
||||||
|
peerUin: '123456789',
|
||||||
|
remark: 'remark',
|
||||||
|
msgTime: '1710000000',
|
||||||
|
chatType: 1,
|
||||||
|
msgId: '12345',
|
||||||
|
sendNickName: 'nick',
|
||||||
|
sendMemberName: 'card',
|
||||||
|
peerName: 'name',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
SendLike: {
|
||||||
|
payload: { user_id: '123456789', times: 10 },
|
||||||
|
response: {},
|
||||||
|
},
|
||||||
|
SetFriendAddRequest: {
|
||||||
|
payload: { flag: 'flag_123', approve: true, remark: '好友' },
|
||||||
|
response: {},
|
||||||
|
},
|
||||||
|
SetFriendRemark: {
|
||||||
|
payload: { user_id: '123456789', remark: '新备注' },
|
||||||
|
response: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,8 +1,15 @@
|
|||||||
import { ActionName } from '@/napcat-onebot/action/router';
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
import { OneBotAction } from '../OneBotAction';
|
import { OneBotAction } from '../OneBotAction';
|
||||||
|
import { Type } from '@sinclair/typebox';
|
||||||
|
|
||||||
export class BotExit extends OneBotAction<void, void> {
|
export class BotExit extends OneBotAction<void, void> {
|
||||||
override actionName = ActionName.Exit;
|
override actionName = ActionName.Exit;
|
||||||
|
override payloadSchema = Type.Object({});
|
||||||
|
override returnSchema = Type.Object({});
|
||||||
|
override actionSummary = '退出登录';
|
||||||
|
override actionTags = ['系统扩展'];
|
||||||
|
override payloadExample = {};
|
||||||
|
override returnExample = null;
|
||||||
|
|
||||||
async _handle () {
|
async _handle () {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
@@ -2,21 +2,36 @@ import { ActionName } from '@/napcat-onebot/action/router';
|
|||||||
import { OneBotAction } from '../OneBotAction';
|
import { OneBotAction } from '../OneBotAction';
|
||||||
import { Static, Type } from '@sinclair/typebox';
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
const PayloadSchema = Type.Object({
|
||||||
group_id: Type.Union([Type.Number(), Type.String()]),
|
group_id: Type.String({ description: '群号' }),
|
||||||
bot_appid: Type.String(),
|
bot_appid: Type.String({ description: '机器人AppID' }),
|
||||||
button_id: Type.String({ default: '' }),
|
button_id: Type.String({ default: '', description: '按钮ID' }),
|
||||||
callback_data: Type.String({ default: '' }),
|
callback_data: Type.String({ default: '', description: '回调数据' }),
|
||||||
msg_seq: Type.String({ default: '10086' }),
|
msg_seq: Type.String({ default: '10086', description: '消息序列号' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
type PayloadType = Static<typeof PayloadSchema>;
|
||||||
|
|
||||||
export class ClickInlineKeyboardButton extends OneBotAction<Payload, unknown> {
|
const ReturnSchema = Type.Any({ description: '点击结果' });
|
||||||
|
|
||||||
|
type ReturnType = Static<typeof ReturnSchema>;
|
||||||
|
|
||||||
|
export class ClickInlineKeyboardButton extends OneBotAction<PayloadType, ReturnType> {
|
||||||
override actionName = ActionName.ClickInlineKeyboardButton;
|
override actionName = ActionName.ClickInlineKeyboardButton;
|
||||||
override payloadSchema = SchemaData;
|
override payloadSchema = PayloadSchema;
|
||||||
|
override returnSchema = ReturnSchema;
|
||||||
async _handle (payload: Payload) {
|
override actionSummary = '点击内联键盘按钮';
|
||||||
|
override actionTags = ['消息扩展'];
|
||||||
|
override payloadExample = {
|
||||||
|
group_id: '123456',
|
||||||
|
bot_appid: '1234567890',
|
||||||
|
button_id: 'btn_1',
|
||||||
|
callback_data: '',
|
||||||
|
msg_seq: '10086'
|
||||||
|
};
|
||||||
|
override returnExample = {
|
||||||
|
};
|
||||||
|
async _handle (payload: PayloadType) {
|
||||||
return await this.core.apis.MsgApi.clickInlineKeyboardButton({
|
return await this.core.apis.MsgApi.clickInlineKeyboardButton({
|
||||||
buttonId: payload.button_id,
|
buttonId: payload.button_id,
|
||||||
peerId: payload.group_id.toString(),
|
peerId: payload.group_id.toString(),
|
||||||
|
|||||||
@@ -2,18 +2,33 @@ import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
|||||||
import { ActionName } from '@/napcat-onebot/action/router';
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
import { Type, Static } from '@sinclair/typebox';
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
const PayloadSchema = Type.Object({
|
||||||
rawData: Type.String(),
|
rawData: Type.String({ description: '原始数据' }),
|
||||||
brief: Type.String(),
|
brief: Type.String({ description: '简要描述' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
type PayloadType = Static<typeof PayloadSchema>;
|
||||||
|
|
||||||
export class CreateCollection extends OneBotAction<Payload, unknown> {
|
const ReturnSchema = Type.Any({ description: '创建结果' });
|
||||||
|
|
||||||
|
type ReturnType = Static<typeof ReturnSchema>;
|
||||||
|
|
||||||
|
export class CreateCollection extends OneBotAction<PayloadType, ReturnType> {
|
||||||
override actionName = ActionName.CreateCollection;
|
override actionName = ActionName.CreateCollection;
|
||||||
override payloadSchema = SchemaData;
|
override payloadSchema = PayloadSchema;
|
||||||
|
override returnSchema = ReturnSchema;
|
||||||
|
override actionSummary = '创建收藏';
|
||||||
|
override actionTags = ['扩展接口'];
|
||||||
|
override payloadExample = {
|
||||||
|
rawData: '收藏内容',
|
||||||
|
brief: '收藏标题'
|
||||||
|
};
|
||||||
|
override returnExample = {
|
||||||
|
result: 0,
|
||||||
|
errMsg: ''
|
||||||
|
};
|
||||||
|
|
||||||
async _handle (payload: Payload) {
|
async _handle (payload: PayloadType) {
|
||||||
return await this.core.apis.CollectionApi.createCollection(
|
return await this.core.apis.CollectionApi.createCollection(
|
||||||
this.core.selfInfo.uin,
|
this.core.selfInfo.uin,
|
||||||
this.core.selfInfo.uid,
|
this.core.selfInfo.uid,
|
||||||
|
|||||||
@@ -2,19 +2,34 @@ import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
|||||||
import { ActionName } from '@/napcat-onebot/action/router';
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
import { Static, Type } from '@sinclair/typebox';
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
const PayloadSchema = Type.Object({
|
||||||
group_id: Type.String(),
|
group_id: Type.String({ description: '群号' }),
|
||||||
album_id: Type.String(),
|
album_id: Type.String({ description: '相册ID' }),
|
||||||
lloc: Type.String(),
|
lloc: Type.String({ description: '媒体ID (lloc)' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
type PayloadType = Static<typeof PayloadSchema>;
|
||||||
|
|
||||||
export class DelGroupAlbumMedia extends OneBotAction<Payload, unknown> {
|
const ReturnSchema = Type.Any({ description: '删除结果' });
|
||||||
|
|
||||||
|
type ReturnType = Static<typeof ReturnSchema>;
|
||||||
|
|
||||||
|
export class DelGroupAlbumMedia extends OneBotAction<PayloadType, ReturnType> {
|
||||||
override actionName = ActionName.DelGroupAlbumMedia;
|
override actionName = ActionName.DelGroupAlbumMedia;
|
||||||
override payloadSchema = SchemaData;
|
override actionSummary = '删除群相册媒体';
|
||||||
|
override actionTags = ['群组扩展'];
|
||||||
|
override payloadExample = {
|
||||||
|
group_id: '123456',
|
||||||
|
album_id: 'album_id_1',
|
||||||
|
lloc: 'media_id_1',
|
||||||
|
};
|
||||||
|
override returnExample = {
|
||||||
|
result: {}
|
||||||
|
};
|
||||||
|
override payloadSchema = PayloadSchema;
|
||||||
|
override returnSchema = ReturnSchema;
|
||||||
|
|
||||||
async _handle (payload: Payload) {
|
async _handle (payload: PayloadType) {
|
||||||
return await this.core.apis.WebApi.deleteAlbumMediaByNTQQ(
|
return await this.core.apis.WebApi.deleteAlbumMediaByNTQQ(
|
||||||
payload.group_id,
|
payload.group_id,
|
||||||
payload.album_id,
|
payload.album_id,
|
||||||
|
|||||||
@@ -2,20 +2,32 @@ import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
|||||||
import { ActionName } from '@/napcat-onebot/action/router';
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
import { Static, Type } from '@sinclair/typebox';
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
export const DoGroupAlbumCommentPayloadSchema = Type.Object({
|
||||||
group_id: Type.String(),
|
group_id: Type.String({ description: '群号' }),
|
||||||
album_id: Type.String(),
|
album_id: Type.String({ description: '相册 ID' }),
|
||||||
lloc: Type.String(),
|
lloc: Type.String({ description: '图片 ID' }),
|
||||||
content: Type.String(),
|
content: Type.String({ description: '评论内容' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
export type DoGroupAlbumCommentPayload = Static<typeof DoGroupAlbumCommentPayloadSchema>;
|
||||||
|
|
||||||
export class DoGroupAlbumComment extends OneBotAction<Payload, unknown> {
|
export class DoGroupAlbumComment extends OneBotAction<DoGroupAlbumCommentPayload, any> {
|
||||||
override actionName = ActionName.DoGroupAlbumComment;
|
override actionName = ActionName.DoGroupAlbumComment;
|
||||||
override payloadSchema = SchemaData;
|
override actionSummary = '发表群相册评论';
|
||||||
|
override actionTags = ['群组扩展'];
|
||||||
|
override payloadExample = {
|
||||||
|
group_id: '123456',
|
||||||
|
album_id: 'album_id_1',
|
||||||
|
lloc: 'media_id_1',
|
||||||
|
content: '很有意思'
|
||||||
|
};
|
||||||
|
override returnExample = {
|
||||||
|
result: {}
|
||||||
|
};
|
||||||
|
override payloadSchema = DoGroupAlbumCommentPayloadSchema;
|
||||||
|
override returnSchema = Type.Any({ description: '评论结果' });
|
||||||
|
|
||||||
async _handle (payload: Payload) {
|
async _handle (payload: DoGroupAlbumCommentPayload) {
|
||||||
return await this.core.apis.WebApi.doAlbumMediaPlainCommentByNTQQ(
|
return await this.core.apis.WebApi.doAlbumMediaPlainCommentByNTQQ(
|
||||||
payload.group_id,
|
payload.group_id,
|
||||||
payload.album_id,
|
payload.album_id,
|
||||||
|
|||||||
@@ -2,18 +2,32 @@ import { Type, Static } from '@sinclair/typebox';
|
|||||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||||
import { ActionName } from '@/napcat-onebot/action/router';
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
const PayloadSchema = Type.Object({
|
||||||
count: Type.Union([Type.Number(), Type.String()], { default: 48 }),
|
count: Type.Union([Type.Number(), Type.String()], { default: 48, description: '获取数量' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
type PayloadType = Static<typeof PayloadSchema>;
|
||||||
|
|
||||||
export class FetchCustomFace extends OneBotAction<Payload, string[]> {
|
const ReturnSchema = Type.Array(Type.String(), { description: '表情URL列表' });
|
||||||
|
|
||||||
|
type ReturnType = Static<typeof ReturnSchema>;
|
||||||
|
|
||||||
|
export class FetchCustomFace extends OneBotAction<PayloadType, ReturnType> {
|
||||||
override actionName = ActionName.FetchCustomFace;
|
override actionName = ActionName.FetchCustomFace;
|
||||||
override payloadSchema = SchemaData;
|
override payloadSchema = PayloadSchema;
|
||||||
|
override returnSchema = ReturnSchema;
|
||||||
|
override actionSummary = '获取自定义表情';
|
||||||
|
override actionTags = ['系统扩展'];
|
||||||
|
override payloadExample = {
|
||||||
|
count: 10
|
||||||
|
};
|
||||||
|
override returnExample = [
|
||||||
|
'http://example.com/face1.png'
|
||||||
|
];
|
||||||
|
|
||||||
async _handle (payload: Payload) {
|
async _handle (payload: PayloadType) {
|
||||||
const ret = await this.core.apis.MsgApi.fetchFavEmojiList(+payload.count);
|
const ret = await this.core.apis.MsgApi.fetchFavEmojiList(+payload.count);
|
||||||
return ret.emojiInfoList.map(e => e.url);
|
return ret.emojiInfoList.map(e => e.url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,28 +2,68 @@ import { Type, Static } from '@sinclair/typebox';
|
|||||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||||
import { ActionName } from '@/napcat-onebot/action/router';
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
import { MessageUnique } from 'napcat-common/src/message-unique';
|
import { MessageUnique } from 'napcat-common/src/message-unique';
|
||||||
import { type NTQQMsgApi } from 'napcat-core/apis';
|
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
const PayloadSchema = Type.Object({
|
||||||
message_id: Type.Union([Type.Number(), Type.String()]),
|
message_id: Type.Union([Type.Number(), Type.String()], { description: '消息ID' }),
|
||||||
emojiId: Type.Union([Type.Number(), Type.String()]),
|
emojiId: Type.Union([Type.Number(), Type.String()], { description: '表情ID' }),
|
||||||
emojiType: Type.Union([Type.Number(), Type.String()]),
|
emojiType: Type.Union([Type.Number(), Type.String()], { description: '表情类型' }),
|
||||||
count: Type.Union([Type.Number(), Type.String()], { default: 20 }),
|
count: Type.Union([Type.Number(), Type.String()], { default: 20, description: '获取数量' }),
|
||||||
|
cookie: Type.String({ default: '', description: '分页Cookie' })
|
||||||
});
|
});
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
type PayloadType = Static<typeof PayloadSchema>;
|
||||||
|
|
||||||
export class FetchEmojiLike extends OneBotAction<Payload, Awaited<ReturnType<NTQQMsgApi['getMsgEmojiLikesList']>>> {
|
const ReturnSchema = Type.Object({
|
||||||
|
emojiLikesList: Type.Array(Type.Object({
|
||||||
|
tinyId: Type.String({ description: 'TinyID' }),
|
||||||
|
nickName: Type.String({ description: '昵称' }),
|
||||||
|
headUrl: Type.String({ description: '头像URL' }),
|
||||||
|
}), { description: '表情回应列表' }),
|
||||||
|
cookie: Type.String({ description: '分页Cookie' }),
|
||||||
|
isLastPage: Type.Boolean({ description: '是否最后一页' }),
|
||||||
|
isFirstPage: Type.Boolean({ description: '是否第一页' }),
|
||||||
|
result: Type.Number({ description: '结果状态码' }),
|
||||||
|
errMsg: Type.String({ description: '错 误信息' }),
|
||||||
|
}, { description: '表情回应详情' });
|
||||||
|
|
||||||
|
type ReturnType = Static<typeof ReturnSchema>;
|
||||||
|
|
||||||
|
export class FetchEmojiLike extends OneBotAction<PayloadType, ReturnType> {
|
||||||
override actionName = ActionName.FetchEmojiLike;
|
override actionName = ActionName.FetchEmojiLike;
|
||||||
override payloadSchema = SchemaData;
|
override actionSummary = '获取表情点赞详情';
|
||||||
|
override actionTags = ['消息扩展'];
|
||||||
|
override payloadExample = {
|
||||||
|
message_id: 12345,
|
||||||
|
emojiId: '123',
|
||||||
|
emojiType: 1,
|
||||||
|
count: 10,
|
||||||
|
cookie: ''
|
||||||
|
};
|
||||||
|
override returnExample = {
|
||||||
|
emojiLikesList: [
|
||||||
|
{
|
||||||
|
tinyId: '123456',
|
||||||
|
nickName: '测试用户',
|
||||||
|
headUrl: 'http://example.com/avatar.png'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
cookie: '',
|
||||||
|
isLastPage: true,
|
||||||
|
isFirstPage: true,
|
||||||
|
result: 0,
|
||||||
|
errMsg: ''
|
||||||
|
};
|
||||||
|
override payloadSchema = PayloadSchema;
|
||||||
|
override returnSchema = ReturnSchema;
|
||||||
|
|
||||||
async _handle (payload: Payload) {
|
async _handle (payload: PayloadType): Promise<ReturnType> {
|
||||||
const msgIdPeer = MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id);
|
const msgIdPeer = MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id);
|
||||||
if (!msgIdPeer) throw new Error('消息不存在');
|
if (!msgIdPeer) throw new Error('消息不存在');
|
||||||
const msg = (await this.core.apis.MsgApi.getMsgsByMsgId(msgIdPeer.Peer, [msgIdPeer.MsgId])).msgList[0];
|
const msg = (await this.core.apis.MsgApi.getMsgsByMsgId(msgIdPeer.Peer, [msgIdPeer.MsgId])).msgList[0];
|
||||||
if (!msg) throw new Error('消息不存在');
|
if (!msg) throw new Error('消息不存在');
|
||||||
return await this.core.apis.MsgApi.getMsgEmojiLikesList(
|
const res = await this.core.apis.MsgApi.getMsgEmojiLikesList(
|
||||||
msgIdPeer.Peer, msg.msgSeq, payload.emojiId.toString(), payload.emojiType.toString(), +payload.count
|
msgIdPeer.Peer, msg.msgSeq, payload.emojiId.toString(), payload.emojiType.toString(), payload.cookie, +payload.count
|
||||||
);
|
);
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,46 @@
|
|||||||
import { ActionName } from '@/napcat-onebot/action/router';
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
||||||
import { AIVoiceChatType } from 'napcat-core/packet/entities/aiChat';
|
|
||||||
import { Type, Static } from '@sinclair/typebox';
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
import { ExtendsActionsExamples } from '../example/ExtendsActionsExamples';
|
||||||
group_id: Type.Union([Type.Number(), Type.String()]),
|
|
||||||
chat_type: Type.Union([Type.Union([Type.Number(), Type.String()])], { default: 1 }),
|
const PayloadSchema = Type.Object({
|
||||||
|
group_id: Type.String({ description: '群号' }),
|
||||||
|
chat_type: Type.Union([Type.Number(), Type.String()], { default: 1, description: '聊天类型' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
type PayloadType = Static<typeof PayloadSchema>;
|
||||||
|
|
||||||
interface GetAiCharactersResponse {
|
const ReturnSchema = Type.Array(
|
||||||
type: string;
|
Type.Object({
|
||||||
characters: {
|
type: Type.String({ description: '角色类型' }),
|
||||||
character_id: string;
|
characters: Type.Array(
|
||||||
character_name: string;
|
Type.Object({
|
||||||
preview_url: string;
|
character_id: Type.String({ description: '角色ID' }),
|
||||||
}[];
|
character_name: Type.String({ description: '角色名称' }),
|
||||||
}
|
preview_url: Type.String({ description: '预览URL' }),
|
||||||
|
}),
|
||||||
|
{ description: '角色列表' }
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
{ description: 'AI角色列表' }
|
||||||
|
);
|
||||||
|
|
||||||
export class GetAiCharacters extends GetPacketStatusDepends<Payload, GetAiCharactersResponse[]> {
|
type ReturnType = Static<typeof ReturnSchema>;
|
||||||
|
|
||||||
|
export class GetAiCharacters extends GetPacketStatusDepends<PayloadType, ReturnType> {
|
||||||
override actionName = ActionName.GetAiCharacters;
|
override actionName = ActionName.GetAiCharacters;
|
||||||
override payloadSchema = SchemaData;
|
override payloadSchema = PayloadSchema;
|
||||||
|
override returnSchema = ReturnSchema;
|
||||||
|
override actionSummary = '获取AI角色列表';
|
||||||
|
override actionDescription = '获取群聊中的AI角色列表';
|
||||||
|
override actionTags = ['扩展接口'];
|
||||||
|
override payloadExample = ExtendsActionsExamples.GetAiCharacters.payload;
|
||||||
|
override returnExample = ExtendsActionsExamples.GetAiCharacters.response;
|
||||||
|
|
||||||
async _handle (payload: Payload) {
|
async _handle (payload: PayloadType) {
|
||||||
const rawList = await this.core.apis.PacketApi.pkt.operation.FetchAiVoiceList(+payload.group_id, +payload.chat_type as AIVoiceChatType);
|
const chatTypeNum = Number(payload.chat_type);
|
||||||
|
const rawList = await this.core.apis.PacketApi.pkt.operation.FetchAiVoiceList(+payload.group_id, chatTypeNum);
|
||||||
return rawList?.map((item) => ({
|
return rawList?.map((item) => ({
|
||||||
type: item.category,
|
type: item.category,
|
||||||
characters: item.voices.map((voice) => ({
|
characters: item.voices.map((voice) => ({
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
import { ActionName } from '@/napcat-onebot/action/router';
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
import { OneBotAction } from '../OneBotAction';
|
import { OneBotAction } from '../OneBotAction';
|
||||||
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
|
||||||
interface GetClientkeyResponse {
|
import { ExtendsActionsExamples } from '../example/ExtendsActionsExamples';
|
||||||
clientkey?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GetClientkey extends OneBotAction<void, GetClientkeyResponse> {
|
const ReturnSchema = Type.Object({
|
||||||
|
clientkey: Type.Optional(Type.String({ description: '客户端Key' })),
|
||||||
|
}, { description: '获取ClientKey结果' });
|
||||||
|
|
||||||
|
type ReturnType = Static<typeof ReturnSchema>;
|
||||||
|
|
||||||
|
export class GetClientkey extends OneBotAction<void, ReturnType> {
|
||||||
override actionName = ActionName.GetClientkey;
|
override actionName = ActionName.GetClientkey;
|
||||||
|
override payloadSchema = Type.Object({});
|
||||||
|
override returnSchema = ReturnSchema;
|
||||||
|
override actionSummary = '获取ClientKey';
|
||||||
|
override actionDescription = '获取当前登录帐号的ClientKey';
|
||||||
|
override actionTags = ['扩展接口'];
|
||||||
|
override payloadExample = ExtendsActionsExamples.GetClientkey.payload;
|
||||||
|
override returnExample = ExtendsActionsExamples.GetClientkey.response;
|
||||||
|
|
||||||
async _handle () {
|
async _handle () {
|
||||||
return { clientkey: (await this.core.apis.UserApi.forceFetchClientKey()).clientKey };
|
return { clientkey: (await this.core.apis.UserApi.forceFetchClientKey()).clientKey };
|
||||||
|
|||||||
@@ -1,20 +1,81 @@
|
|||||||
import { type NTQQCollectionApi } from 'napcat-core/apis/collection';
|
|
||||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||||
import { ActionName } from '@/napcat-onebot/action/router';
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
import { Type, Static } from '@sinclair/typebox';
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
const PayloadSchema = Type.Object({
|
||||||
category: Type.Union([Type.Number(), Type.String()]),
|
category: Type.String({ description: '分类ID' }),
|
||||||
count: Type.Union([Type.Union([Type.Number(), Type.String()])], { default: 1 }),
|
count: Type.String({ default: '50', description: '获取数量' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
type PayloadType = Static<typeof PayloadSchema>;
|
||||||
|
|
||||||
export class GetCollectionList extends OneBotAction<Payload, Awaited<ReturnType<NTQQCollectionApi['getAllCollection']>>> {
|
const ReturnSchema = Type.Any({ description: '收藏列表' });
|
||||||
|
|
||||||
|
type ReturnType = Static<typeof ReturnSchema>;
|
||||||
|
|
||||||
|
export class GetCollectionList extends OneBotAction<PayloadType, ReturnType> {
|
||||||
override actionName = ActionName.GetCollectionList;
|
override actionName = ActionName.GetCollectionList;
|
||||||
override payloadSchema = SchemaData;
|
override payloadSchema = PayloadSchema;
|
||||||
|
override returnSchema = ReturnSchema;
|
||||||
|
override actionSummary = '获取收藏列表';
|
||||||
|
override actionTags = ['系统扩展'];
|
||||||
|
override payloadExample = {
|
||||||
|
category: '0',
|
||||||
|
count: '50'
|
||||||
|
};
|
||||||
|
override returnExample = {
|
||||||
|
errCode: 0,
|
||||||
|
errMsg: "",
|
||||||
|
collectionSearchList: {
|
||||||
|
collectionItemList: [
|
||||||
|
{
|
||||||
|
cid: "123456",
|
||||||
|
type: 8,
|
||||||
|
status: 1,
|
||||||
|
author: {
|
||||||
|
type: 2,
|
||||||
|
numId: "123456",
|
||||||
|
strId: "昵称",
|
||||||
|
groupId: "123456",
|
||||||
|
groupName: "群名",
|
||||||
|
uid: "123456"
|
||||||
|
},
|
||||||
|
bid: 1,
|
||||||
|
category: 1,
|
||||||
|
createTime: "1769169157000",
|
||||||
|
collectTime: "1769413477691",
|
||||||
|
modifyTime: "1769413477691",
|
||||||
|
sequence: "1769413476735",
|
||||||
|
shareUrl: "",
|
||||||
|
customGroupId: 0,
|
||||||
|
securityBeat: false,
|
||||||
|
summary: {
|
||||||
|
textSummary: null,
|
||||||
|
linkSummary: null,
|
||||||
|
gallerySummary: null,
|
||||||
|
audioSummary: null,
|
||||||
|
videoSummary: null,
|
||||||
|
fileSummary: null,
|
||||||
|
locationSummary: null,
|
||||||
|
richMediaSummary: {
|
||||||
|
title: "",
|
||||||
|
subTitle: "",
|
||||||
|
brief: "text",
|
||||||
|
picList: [],
|
||||||
|
contentType: 1,
|
||||||
|
originalUri: "",
|
||||||
|
publisher: "",
|
||||||
|
richMediaVersion: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
hasMore: false,
|
||||||
|
bottomTimeStamp: "1769413477691"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
async _handle (payload: Payload) {
|
async _handle (payload: PayloadType) {
|
||||||
return await this.core.apis.CollectionApi.getAllCollection(+payload.category, +payload.count);
|
return await this.core.apis.CollectionApi.getAllCollection(+payload.category, +payload.count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
88
packages/napcat-onebot/action/extends/GetEmojiLikes.ts
Normal file
88
packages/napcat-onebot/action/extends/GetEmojiLikes.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||||
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
|
import { MessageUnique } from 'napcat-common/src/message-unique';
|
||||||
|
import { Peer, ChatType } from '@/napcat-core';
|
||||||
|
|
||||||
|
const PayloadSchema = Type.Object({
|
||||||
|
group_id: Type.Optional(Type.String({ description: '群号,短ID可不传' })),
|
||||||
|
message_id: Type.String({ description: '消息ID,可以传递长ID或短ID' }),
|
||||||
|
emoji_id: Type.String({ description: '表情ID' }),
|
||||||
|
emoji_type: Type.Optional(Type.String({ description: '表情类型' })),
|
||||||
|
count: Type.Number({ default: 0, description: '数量,0代表全部' }),
|
||||||
|
});
|
||||||
|
type PayloadType = Static<typeof PayloadSchema>;
|
||||||
|
|
||||||
|
const ReturnSchema = Type.Object({
|
||||||
|
emoji_like_list: Type.Array(
|
||||||
|
Type.Object({
|
||||||
|
user_id: Type.String({ description: '点击者QQ号' }),
|
||||||
|
nick_name: Type.String({ description: '昵称?' }),
|
||||||
|
}),
|
||||||
|
{ description: '表情回应列表' }
|
||||||
|
),
|
||||||
|
});
|
||||||
|
type ReturnType = Static<typeof ReturnSchema>;
|
||||||
|
|
||||||
|
export class GetEmojiLikes extends OneBotAction<PayloadType, ReturnType> {
|
||||||
|
override actionName = ActionName.GetEmojiLikes;
|
||||||
|
override actionSummary = '获取消息表情点赞列表';
|
||||||
|
override actionTags = ['消息扩展'];
|
||||||
|
override payloadExample = {
|
||||||
|
message_id: '12345',
|
||||||
|
emoji_id: '123'
|
||||||
|
};
|
||||||
|
override returnExample = {
|
||||||
|
emoji_like_list: [
|
||||||
|
{
|
||||||
|
user_id: '654321',
|
||||||
|
nick_name: '测试用户'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
override payloadSchema = PayloadSchema;
|
||||||
|
override returnSchema = ReturnSchema;
|
||||||
|
|
||||||
|
async _handle (payload: PayloadType) {
|
||||||
|
let peer: Peer;
|
||||||
|
let msgId: string;
|
||||||
|
|
||||||
|
if (MessageUnique.isShortId(payload.message_id)) {
|
||||||
|
const msgIdPeer = MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id);
|
||||||
|
if (!msgIdPeer) throw new Error('消息不存在');
|
||||||
|
peer = msgIdPeer.Peer;
|
||||||
|
msgId = msgIdPeer.MsgId;
|
||||||
|
} else {
|
||||||
|
if (!payload.group_id) throw new Error('长ID模式下必须提供群号');
|
||||||
|
peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: payload.group_id };
|
||||||
|
msgId = payload.message_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const msg = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgId])).msgList[0];
|
||||||
|
if (!msg) throw new Error('消息不存在');
|
||||||
|
|
||||||
|
const emojiType = payload.emoji_type ?? (payload.emoji_id.length > 3 ? '2' : '1');
|
||||||
|
const emojiLikeList: Array<{ user_id: string; nick_name: string; }> = [];
|
||||||
|
let cookie = '';
|
||||||
|
let needFetchCount = payload.count == 0 ? 200 : Math.ceil(payload.count / 15);
|
||||||
|
for (let page = 0; page < needFetchCount; page++) {
|
||||||
|
const res = await this.core.apis.MsgApi.getMsgEmojiLikesList(
|
||||||
|
peer, msg.msgSeq, payload.emoji_id.toString(), emojiType, cookie, 15
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Array.isArray(res.emojiLikesList)) {
|
||||||
|
for (const like of res.emojiLikesList) {
|
||||||
|
emojiLikeList.push({ user_id: like.tinyId, nick_name: like.nickName });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.isLastPage || !res.cookie) break;
|
||||||
|
cookie = res.cookie;
|
||||||
|
}
|
||||||
|
// 切断多余部分
|
||||||
|
if (payload.count > 0) {
|
||||||
|
emojiLikeList.splice(payload.count);
|
||||||
|
}
|
||||||
|
return { emoji_like_list: emojiLikeList };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,40 @@
|
|||||||
import { OB11Construct } from '@/napcat-onebot/helper/data';
|
import { OB11Construct } from '@/napcat-onebot/helper/data';
|
||||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||||
import { ActionName } from '@/napcat-onebot/action/router';
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
import { OB11UserSchema } from '../schemas';
|
||||||
|
|
||||||
export class GetFriendWithCategory extends OneBotAction<void, unknown> {
|
const ReturnSchema = Type.Array(
|
||||||
|
Type.Object({
|
||||||
|
categoryId: Type.Number({ description: '分组ID' }),
|
||||||
|
categoryName: Type.String({ description: '分组名称' }),
|
||||||
|
categoryMbCount: Type.Number({ description: '分组内好友数量' }),
|
||||||
|
buddyList: Type.Array(OB11UserSchema, { description: '好友列表' }),
|
||||||
|
}),
|
||||||
|
{ description: '带分组的好友列表' }
|
||||||
|
);
|
||||||
|
|
||||||
|
type ReturnType = Static<typeof ReturnSchema>;
|
||||||
|
|
||||||
|
export class GetFriendWithCategory extends OneBotAction<void, ReturnType> {
|
||||||
override actionName = ActionName.GetFriendsWithCategory;
|
override actionName = ActionName.GetFriendsWithCategory;
|
||||||
|
override payloadSchema = Type.Object({});
|
||||||
|
override returnSchema = ReturnSchema;
|
||||||
|
override actionSummary = '获取带分组的好友列表';
|
||||||
|
override actionTags = ['用户扩展'];
|
||||||
|
override payloadExample = {};
|
||||||
|
override returnExample = [
|
||||||
|
{
|
||||||
|
categoryId: 1,
|
||||||
|
categoryName: '我的好友',
|
||||||
|
categoryMbCount: 1,
|
||||||
|
buddyList: []
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
async _handle () {
|
async _handle () {
|
||||||
return (await this.core.apis.FriendApi.getBuddyV2ExWithCate()).map(category => ({
|
const categories = await this.core.apis.FriendApi.getBuddyV2ExWithCate();
|
||||||
|
return categories.map(category => ({
|
||||||
...category,
|
...category,
|
||||||
buddyList: OB11Construct.friends(category.buddyList),
|
buddyList: OB11Construct.friends(category.buddyList),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,16 +1,51 @@
|
|||||||
import { GroupNotifyMsgStatus } from 'napcat-core';
|
import { GroupNotifyMsgStatus } from 'napcat-core';
|
||||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||||
import { ActionName } from '@/napcat-onebot/action/router';
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
import { Notify } from '@/napcat-onebot/types';
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
|
||||||
export default class GetGroupAddRequest extends OneBotAction<null, Notify[] | null> {
|
const ReturnSchema = Type.Array(
|
||||||
|
Type.Object({
|
||||||
|
request_id: Type.Number({ description: '请求ID' }),
|
||||||
|
invitor_uin: Type.Number({ description: '邀请者QQ' }),
|
||||||
|
invitor_nick: Type.Optional(Type.String({ description: '邀请者昵称' })),
|
||||||
|
group_id: Type.Number({ description: '群号' }),
|
||||||
|
message: Type.Optional(Type.String({ description: '验证信息' })),
|
||||||
|
group_name: Type.Optional(Type.String({ description: '群名称' })),
|
||||||
|
checked: Type.Boolean({ description: '是否已处理' }),
|
||||||
|
actor: Type.Number({ description: '处理者QQ' }),
|
||||||
|
requester_nick: Type.Optional(Type.String({ description: '请求者昵称' })),
|
||||||
|
}),
|
||||||
|
{ description: '群通知列表' }
|
||||||
|
);
|
||||||
|
|
||||||
|
type ReturnType = Static<typeof ReturnSchema>;
|
||||||
|
|
||||||
|
export default class GetGroupAddRequest extends OneBotAction<void, ReturnType> {
|
||||||
override actionName = ActionName.GetGroupIgnoreAddRequest;
|
override actionName = ActionName.GetGroupIgnoreAddRequest;
|
||||||
|
override payloadSchema = Type.Object({});
|
||||||
|
override returnSchema = ReturnSchema;
|
||||||
|
override actionSummary = '获取群被忽略的加群请求';
|
||||||
|
override actionTags = ['群组接口'];
|
||||||
|
override payloadExample = {};
|
||||||
|
override returnExample = [
|
||||||
|
{
|
||||||
|
request_id: 12345,
|
||||||
|
invitor_uin: 123456789,
|
||||||
|
invitor_nick: '邀请者',
|
||||||
|
group_id: 123456789,
|
||||||
|
message: '加群请求',
|
||||||
|
group_name: '群名称',
|
||||||
|
checked: false,
|
||||||
|
actor: 0,
|
||||||
|
requester_nick: '请求者'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
async _handle (): Promise<Notify[] | null> {
|
async _handle (): Promise<ReturnType> {
|
||||||
const NTQQUserApi = this.core.apis.UserApi;
|
const NTQQUserApi = this.core.apis.UserApi;
|
||||||
const NTQQGroupApi = this.core.apis.GroupApi;
|
const NTQQGroupApi = this.core.apis.GroupApi;
|
||||||
const ignoredNotifies = await NTQQGroupApi.getSingleScreenNotifies(true, 10);
|
const ignoredNotifies = await NTQQGroupApi.getSingleScreenNotifies(true, 10);
|
||||||
const retData: Notify[] = [];
|
const retData: ReturnType = [];
|
||||||
|
|
||||||
const notifyPromises = ignoredNotifies
|
const notifyPromises = ignoredNotifies
|
||||||
.filter(notify => notify.type === 7)
|
.filter(notify => notify.type === 7)
|
||||||
|
|||||||
@@ -2,19 +2,35 @@ import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
|||||||
import { ActionName } from '@/napcat-onebot/action/router';
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
import { Static, Type } from '@sinclair/typebox';
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
const PayloadSchema = Type.Object({
|
||||||
group_id: Type.String(),
|
group_id: Type.String({ description: '群号' }),
|
||||||
album_id: Type.String(),
|
album_id: Type.String({ description: '相册ID' }),
|
||||||
attach_info: Type.String({ default: '' }),
|
attach_info: Type.String({ default: '', description: '附加信息(用于分页)' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
type PayloadType = Static<typeof PayloadSchema>;
|
||||||
|
|
||||||
export class GetGroupAlbumMediaList extends OneBotAction<Payload, unknown> {
|
const ReturnSchema = Type.Any({ description: '相册媒体列表' });
|
||||||
|
|
||||||
|
type ReturnType = Static<typeof ReturnSchema>;
|
||||||
|
|
||||||
|
export class GetGroupAlbumMediaList extends OneBotAction<PayloadType, ReturnType> {
|
||||||
override actionName = ActionName.GetGroupAlbumMediaList;
|
override actionName = ActionName.GetGroupAlbumMediaList;
|
||||||
override payloadSchema = SchemaData;
|
override actionSummary = '获取群相册媒体列表';
|
||||||
|
override actionTags = ['群组扩展'];
|
||||||
|
override payloadExample = {
|
||||||
|
group_id: '123456',
|
||||||
|
album_id: 'album_id_1',
|
||||||
|
};
|
||||||
|
override returnExample = {
|
||||||
|
media_list: [
|
||||||
|
{ media_id: 'media_id_1', url: 'http://example.com/1.jpg' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
override payloadSchema = PayloadSchema;
|
||||||
|
override returnSchema = ReturnSchema;
|
||||||
|
|
||||||
async _handle (payload: Payload) {
|
async _handle (payload: PayloadType) {
|
||||||
return await this.core.apis.WebApi.getAlbumMediaListByNTQQ(
|
return await this.core.apis.WebApi.getAlbumMediaListByNTQQ(
|
||||||
payload.group_id,
|
payload.group_id,
|
||||||
payload.album_id,
|
payload.album_id,
|
||||||
|
|||||||
@@ -1,17 +1,30 @@
|
|||||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||||
import { ActionName } from '@/napcat-onebot/action/router';
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
import { Type, Static } from '@sinclair/typebox';
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
const SchemaData = Type.Object({
|
const PayloadSchema = Type.Object({
|
||||||
group_id: Type.Union([Type.Number(), Type.String()]),
|
group_id: Type.String({ description: '群号' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
type PayloadType = Static<typeof PayloadSchema>;
|
||||||
|
|
||||||
export class GetGroupInfoEx extends OneBotAction<Payload, unknown> {
|
const ReturnSchema = Type.Any({ description: '群扩展信息' });
|
||||||
|
|
||||||
|
type ReturnType = Static<typeof ReturnSchema>;
|
||||||
|
|
||||||
|
export class GetGroupInfoEx extends OneBotAction<PayloadType, ReturnType> {
|
||||||
override actionName = ActionName.GetGroupInfoEx;
|
override actionName = ActionName.GetGroupInfoEx;
|
||||||
override payloadSchema = SchemaData;
|
override actionSummary = '获取群详细信息 (扩展)';
|
||||||
|
override actionTags = ['群组扩展'];
|
||||||
|
override payloadExample = {
|
||||||
|
group_id: '123456'
|
||||||
|
};
|
||||||
|
override returnExample = {
|
||||||
|
|
||||||
|
};
|
||||||
|
override payloadSchema = PayloadSchema;
|
||||||
|
override returnSchema = ReturnSchema;
|
||||||
|
|
||||||
async _handle (payload: Payload) {
|
async _handle (payload: PayloadType) {
|
||||||
return (await this.core.apis.GroupApi.getGroupExtFE0Info([payload.group_id.toString()])).result.groupExtInfos.get(payload.group_id.toString());
|
return (await this.core.apis.GroupApi.getGroupExtFE0Info([payload.group_id.toString()])).result.groupExtInfos.get(payload.group_id.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,79 @@
|
|||||||
import { ActionName } from '@/napcat-onebot/action/router';
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
||||||
import { MiniAppInfo, MiniAppInfoHelper } from 'napcat-core/packet/utils/helper/miniAppHelper';
|
import { MiniAppInfo, MiniAppInfoHelper } from 'napcat-core/packet/utils/helper/miniAppHelper';
|
||||||
import { MiniAppData, MiniAppRawData, MiniAppReqCustomParams, MiniAppReqParams } from 'napcat-core/packet/entities/miniApp';
|
import { MiniAppReqCustomParams, MiniAppReqParams } from 'napcat-core/packet/entities/miniApp';
|
||||||
import { Static, Type } from '@sinclair/typebox';
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
|
|
||||||
const SchemaData = Type.Union([
|
const PayloadSchema = Type.Union([
|
||||||
Type.Object({
|
Type.Object({
|
||||||
type: Type.Union([Type.Literal('bili'), Type.Literal('weibo')]),
|
type: Type.Union([Type.Literal('bili'), Type.Literal('weibo')], { description: '模板类型' }),
|
||||||
title: Type.String(),
|
title: Type.String({ description: '标题' }),
|
||||||
desc: Type.String(),
|
desc: Type.String({ description: '描述' }),
|
||||||
picUrl: Type.String(),
|
picUrl: Type.String({ description: '图片URL' }),
|
||||||
jumpUrl: Type.String(),
|
jumpUrl: Type.String({ description: '跳转URL' }),
|
||||||
webUrl: Type.Optional(Type.String()),
|
webUrl: Type.Optional(Type.String({ description: '网页URL' })),
|
||||||
rawArkData: Type.Optional(Type.Union([Type.String()])),
|
rawArkData: Type.Optional(Type.Union([Type.String()], { description: '是否返回原始Ark数据' })),
|
||||||
}),
|
}),
|
||||||
Type.Object({
|
Type.Object({
|
||||||
title: Type.String(),
|
title: Type.String({ description: '标题' }),
|
||||||
desc: Type.String(),
|
desc: Type.String({ description: '描述' }),
|
||||||
picUrl: Type.String(),
|
picUrl: Type.String({ description: '图片URL' }),
|
||||||
jumpUrl: Type.String(),
|
jumpUrl: Type.String({ description: '跳转URL' }),
|
||||||
iconUrl: Type.String(),
|
iconUrl: Type.String({ description: '图标URL' }),
|
||||||
webUrl: Type.Optional(Type.String()),
|
webUrl: Type.Optional(Type.String({ description: '网页URL' })),
|
||||||
appId: Type.String(),
|
appId: Type.String({ description: '小程序AppID' }),
|
||||||
scene: Type.Union([Type.Number(), Type.String()]),
|
scene: Type.String({ description: '场景ID' }),
|
||||||
templateType: Type.Union([Type.Number(), Type.String()]),
|
templateType: Type.String({ description: '模板类型' }),
|
||||||
businessType: Type.Union([Type.Number(), Type.String()]),
|
businessType: Type.String({ description: '业务类型' }),
|
||||||
verType: Type.Union([Type.Number(), Type.String()]),
|
verType: Type.String({ description: '版本类型' }),
|
||||||
shareType: Type.Union([Type.Number(), Type.String()]),
|
shareType: Type.String({ description: '分享类型' }),
|
||||||
versionId: Type.String(),
|
versionId: Type.String({ description: '版本ID' }),
|
||||||
sdkId: Type.String(),
|
sdkId: Type.String({ description: 'SDK ID' }),
|
||||||
withShareTicket: Type.Union([Type.Number(), Type.String()]),
|
withShareTicket: Type.String({ description: '是否携带分享票据' }),
|
||||||
rawArkData: Type.Optional(Type.Union([Type.String()])),
|
rawArkData: Type.Optional(Type.String({ description: '是否返回原始Ark数据' })),
|
||||||
}),
|
}),
|
||||||
]);
|
], { description: '小程序Ark参数' });
|
||||||
type Payload = Static<typeof SchemaData>;
|
|
||||||
|
|
||||||
export class GetMiniAppArk extends GetPacketStatusDepends<Payload, {
|
type PayloadType = Static<typeof PayloadSchema>;
|
||||||
data: MiniAppData | MiniAppRawData
|
|
||||||
}> {
|
const ReturnSchema = Type.Object({
|
||||||
|
data: Type.Any({ description: 'Ark数据' }),
|
||||||
|
}, { description: '获取小程序Ark结果' });
|
||||||
|
|
||||||
|
type ReturnType = Static<typeof ReturnSchema>;
|
||||||
|
|
||||||
|
export class GetMiniAppArk extends GetPacketStatusDepends<PayloadType, ReturnType> {
|
||||||
override actionName = ActionName.GetMiniAppArk;
|
override actionName = ActionName.GetMiniAppArk;
|
||||||
override payloadSchema = SchemaData;
|
override payloadSchema = PayloadSchema;
|
||||||
|
override returnSchema = ReturnSchema; override actionSummary = '获取小程序 Ark';
|
||||||
async _handle (payload: Payload) {
|
override actionTags = ['系统扩展'];
|
||||||
|
override payloadExample = {
|
||||||
|
type: 'bili',
|
||||||
|
title: '测试标题',
|
||||||
|
desc: '测试描述',
|
||||||
|
picUrl: 'http://example.com/pic.jpg',
|
||||||
|
jumpUrl: 'http://example.com'
|
||||||
|
};
|
||||||
|
override returnExample = {
|
||||||
|
data: {
|
||||||
|
ark: 'ark_content'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
async _handle (payload: PayloadType) {
|
||||||
let reqParam: MiniAppReqParams;
|
let reqParam: MiniAppReqParams;
|
||||||
const customParams = {
|
const customParams: MiniAppReqCustomParams = {
|
||||||
title: payload.title,
|
title: payload.title,
|
||||||
desc: payload.desc,
|
desc: payload.desc,
|
||||||
picUrl: payload.picUrl,
|
picUrl: payload.picUrl,
|
||||||
jumpUrl: payload.jumpUrl,
|
jumpUrl: payload.jumpUrl,
|
||||||
webUrl: payload.webUrl,
|
webUrl: payload.webUrl ?? '',
|
||||||
} as MiniAppReqCustomParams;
|
};
|
||||||
if ('type' in payload) {
|
if ('type' in payload) {
|
||||||
reqParam = MiniAppInfoHelper.generateReq(customParams, MiniAppInfo.get(payload.type)!.template);
|
const template = MiniAppInfo.get(payload.type)?.template;
|
||||||
|
if (!template) {
|
||||||
|
throw new Error('未知的模板类型');
|
||||||
|
}
|
||||||
|
reqParam = MiniAppInfoHelper.generateReq(customParams, template);
|
||||||
} else {
|
} else {
|
||||||
const { appId, scene, iconUrl, templateType, businessType, verType, shareType, versionId, withShareTicket } = payload;
|
const { appId, scene, iconUrl, templateType, businessType, verType, shareType, versionId, withShareTicket } = payload;
|
||||||
reqParam = MiniAppInfoHelper.generateReq(
|
reqParam = MiniAppInfoHelper.generateReq(
|
||||||
|
|||||||
@@ -1,36 +1,65 @@
|
|||||||
import { NTVoteInfo } from 'napcat-core';
|
|
||||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||||
import { ActionName } from '@/napcat-onebot/action/router';
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
import { Type, Static } from '@sinclair/typebox';
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
const PayloadSchema = Type.Object({
|
||||||
user_id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
|
user_id: Type.Optional(Type.String({ description: 'QQ号' })),
|
||||||
start: Type.Union([Type.Number(), Type.String()], { default: 0 }),
|
start: Type.Union([Type.Number(), Type.String()], { default: 0, description: '起始位置' }),
|
||||||
count: Type.Union([Type.Number(), Type.String()], { default: 10 }),
|
count: Type.Union([Type.Number(), Type.String()], { default: 10, description: '获取数量' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
type PayloadType = Static<typeof PayloadSchema>;
|
||||||
|
|
||||||
export class GetProfileLike extends OneBotAction<Payload, {
|
const ReturnSchema = Type.Object({
|
||||||
uid: string;
|
uid: Type.String({ description: '用户UID' }),
|
||||||
time: string;
|
time: Type.String({ description: '时间' }),
|
||||||
favoriteInfo: {
|
favoriteInfo: Type.Object({
|
||||||
userInfos: Array<NTVoteInfo>;
|
userInfos: Type.Array(Type.Any(), { description: '点赞用户信息' }),
|
||||||
total_count: number;
|
total_count: Type.Number({ description: '总点赞数' }),
|
||||||
last_time: number;
|
last_time: Type.Number({ description: '最后点赞时间' }),
|
||||||
today_count: number;
|
today_count: Type.Number({ description: '今日点赞数' }),
|
||||||
};
|
}),
|
||||||
voteInfo: {
|
voteInfo: Type.Object({
|
||||||
total_count: number;
|
total_count: Type.Number({ description: '总点赞数' }),
|
||||||
new_count: number;
|
new_count: Type.Number({ description: '新增点赞数' }),
|
||||||
new_nearby_count: number;
|
new_nearby_count: Type.Number({ description: '新增附近点赞数' }),
|
||||||
last_visit_time: number;
|
last_visit_time: Type.Number({ description: '最后访问时间' }),
|
||||||
userInfos: Array<NTVoteInfo>;
|
userInfos: Type.Array(Type.Any(), { description: '点赞用户信息' }),
|
||||||
};
|
}),
|
||||||
}> {
|
}, { description: '点赞详情' });
|
||||||
|
|
||||||
|
type ReturnType = Static<typeof ReturnSchema>;
|
||||||
|
|
||||||
|
export class GetProfileLike extends OneBotAction<PayloadType, ReturnType> {
|
||||||
override actionName = ActionName.GetProfileLike;
|
override actionName = ActionName.GetProfileLike;
|
||||||
override payloadSchema = SchemaData;
|
override payloadSchema = PayloadSchema;
|
||||||
async _handle (payload: Payload) {
|
override returnSchema = ReturnSchema;
|
||||||
|
override actionSummary = '获取资料点赞';
|
||||||
|
override actionTags = ['用户扩展'];
|
||||||
|
override payloadExample = {
|
||||||
|
user_id: '123456789',
|
||||||
|
start: 0,
|
||||||
|
count: 10
|
||||||
|
};
|
||||||
|
override returnExample = {
|
||||||
|
uid: 'u_123',
|
||||||
|
time: '1734567890',
|
||||||
|
favoriteInfo: {
|
||||||
|
userInfos: [],
|
||||||
|
total_count: 10,
|
||||||
|
last_time: 1734567890,
|
||||||
|
today_count: 5
|
||||||
|
},
|
||||||
|
voteInfo: {
|
||||||
|
total_count: 100,
|
||||||
|
new_count: 2,
|
||||||
|
new_nearby_count: 0,
|
||||||
|
last_visit_time: 1734567890,
|
||||||
|
userInfos: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async _handle (payload: PayloadType): Promise<ReturnType> {
|
||||||
const isSelf = this.core.selfInfo.uin === payload.user_id || !payload.user_id;
|
const isSelf = this.core.selfInfo.uin === payload.user_id || !payload.user_id;
|
||||||
const userUid = isSelf || !payload.user_id ? this.core.selfInfo.uid : await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
|
const userUid = isSelf || !payload.user_id ? this.core.selfInfo.uid : await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
|
||||||
const type = isSelf ? 2 : 1;
|
const type = isSelf ? 2 : 1;
|
||||||
|
|||||||
@@ -1,18 +1,37 @@
|
|||||||
import { NTQQWebApi } from 'napcat-core/apis';
|
|
||||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||||
import { ActionName } from '@/napcat-onebot/action/router';
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
import { Static, Type } from '@sinclair/typebox';
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
const SchemaData = Type.Object({
|
import { NTQQWebApi } from 'napcat-core/apis';
|
||||||
group_id: Type.String(),
|
|
||||||
|
const PayloadSchema = Type.Object({
|
||||||
|
group_id: Type.String({ description: '群号' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
type PayloadType = Static<typeof PayloadSchema>;
|
||||||
|
|
||||||
export class GetQunAlbumList extends OneBotAction<Payload, Awaited<ReturnType<NTQQWebApi['getAlbumListByNTQQ']>>['response']['album_list']> {
|
const ReturnSchema = Type.Array(Type.Any(), { description: '群相册列表' });
|
||||||
|
|
||||||
|
type GetQunAlbumListReturn = Awaited<globalThis.ReturnType<NTQQWebApi['getAlbumListByNTQQ']>>['response']['album_list'];
|
||||||
|
|
||||||
|
export class GetQunAlbumList extends OneBotAction<PayloadType, GetQunAlbumListReturn> {
|
||||||
override actionName = ActionName.GetQunAlbumList;
|
override actionName = ActionName.GetQunAlbumList;
|
||||||
override payloadSchema = SchemaData;
|
override actionSummary = '获取群相册列表';
|
||||||
|
override actionTags = ['群组扩展'];
|
||||||
|
override payloadExample = {
|
||||||
|
group_id: '123456',
|
||||||
|
};
|
||||||
|
override returnExample = [
|
||||||
|
{
|
||||||
|
album_id: 'album_1',
|
||||||
|
album_name: '测试相册',
|
||||||
|
cover_url: 'http://example.com/cover.jpg',
|
||||||
|
create_time: 1734567890
|
||||||
|
}
|
||||||
|
];
|
||||||
|
override payloadSchema = PayloadSchema;
|
||||||
|
override returnSchema = ReturnSchema;
|
||||||
|
|
||||||
async _handle (payload: Payload) {
|
async _handle (payload: PayloadType): Promise<GetQunAlbumListReturn> {
|
||||||
return (await this.core.apis.WebApi.getAlbumListByNTQQ(payload.group_id)).response.album_list;
|
return (await this.core.apis.WebApi.getAlbumListByNTQQ(payload.group_id)).response.album_list;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,24 @@
|
|||||||
import { ActionName } from '@/napcat-onebot/action/router';
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
||||||
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
|
||||||
export class GetRkey extends GetPacketStatusDepends<void, Array<unknown>> {
|
const ReturnSchema = Type.Array(Type.Any(), { description: 'Rkey列表' });
|
||||||
|
|
||||||
|
type ReturnType = Static<typeof ReturnSchema>;
|
||||||
|
|
||||||
|
export class GetRkey extends GetPacketStatusDepends<void, ReturnType> {
|
||||||
override actionName = ActionName.GetRkey;
|
override actionName = ActionName.GetRkey;
|
||||||
|
override payloadSchema = Type.Object({});
|
||||||
|
override returnSchema = ReturnSchema;
|
||||||
|
override actionSummary = '获取 RKey';
|
||||||
|
override actionTags = ['系统扩展'];
|
||||||
|
override payloadExample = {};
|
||||||
|
override returnExample = [
|
||||||
|
{
|
||||||
|
"key": "rkey_value",
|
||||||
|
"expired": 1734567890
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
async _handle () {
|
async _handle () {
|
||||||
return await this.core.apis.PacketApi.pkt.operation.FetchRkey();
|
return await this.core.apis.PacketApi.pkt.operation.FetchRkey();
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||||
import { ActionName } from '@/napcat-onebot/action/router';
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
|
||||||
export class GetRobotUinRange extends OneBotAction<void, Array<unknown>> {
|
const ReturnSchema = Type.Array(Type.Any(), { description: '机器人Uin范围列表' });
|
||||||
|
|
||||||
|
type ReturnType = Static<typeof ReturnSchema>;
|
||||||
|
|
||||||
|
export class GetRobotUinRange extends OneBotAction<void, ReturnType> {
|
||||||
override actionName = ActionName.GetRobotUinRange;
|
override actionName = ActionName.GetRobotUinRange;
|
||||||
|
override actionSummary = '获取机器人 UIN 范围';
|
||||||
|
override actionTags = ['系统扩展'];
|
||||||
|
override payloadExample = {};
|
||||||
|
override returnExample = [
|
||||||
|
{ minUin: '12345678', maxUin: '87654321' }
|
||||||
|
];
|
||||||
|
override payloadSchema = Type.Object({});
|
||||||
|
override returnSchema = ReturnSchema;
|
||||||
|
|
||||||
async _handle () {
|
async _handle () {
|
||||||
return await this.core.apis.UserApi.getRobotUinRange();
|
return await this.core.apis.UserApi.getRobotUinRange();
|
||||||
|
|||||||
@@ -2,26 +2,37 @@ import { PacketBuf } from 'napcat-core/packet/transformer/base';
|
|||||||
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
|
||||||
import { ActionName } from '@/napcat-onebot/action/router';
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
import { ProtoBuf, ProtoBufBase, PBUint32, PBString } from 'napcat.protobuf';
|
import { ProtoBuf, ProtoBufBase, PBUint32, PBString } from 'napcat.protobuf';
|
||||||
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
|
||||||
interface Friend {
|
const ReturnSchema = Type.Array(
|
||||||
uin: number;
|
Type.Object({
|
||||||
uid: string;
|
uin: Type.Number({ description: 'QQ号' }),
|
||||||
nick_name: string;
|
uid: Type.String({ description: '用户UID' }),
|
||||||
age: number;
|
nick_name: Type.String({ description: '昵称' }),
|
||||||
source: string;
|
age: Type.Number({ description: '年龄' }),
|
||||||
}
|
source: Type.String({ description: '来源' }),
|
||||||
|
}),
|
||||||
|
{ description: '单向好友列表' }
|
||||||
|
);
|
||||||
|
|
||||||
interface Block {
|
type ReturnType = Static<typeof ReturnSchema>;
|
||||||
str_uid: string;
|
|
||||||
bytes_source: string;
|
|
||||||
uint32_sex: number;
|
|
||||||
uint32_age: number;
|
|
||||||
bytes_nick: string;
|
|
||||||
uint64_uin: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GetUnidirectionalFriendList extends OneBotAction<void, Friend[]> {
|
export class GetUnidirectionalFriendList extends OneBotAction<void, ReturnType> {
|
||||||
override actionName = ActionName.GetUnidirectionalFriendList;
|
override actionName = ActionName.GetUnidirectionalFriendList;
|
||||||
|
override payloadSchema = Type.Object({});
|
||||||
|
override returnSchema = ReturnSchema;
|
||||||
|
override actionSummary = '获取单向好友列表';
|
||||||
|
override actionTags = ['用户扩展'];
|
||||||
|
override payloadExample = {};
|
||||||
|
override returnExample = [
|
||||||
|
{
|
||||||
|
uin: 123456789,
|
||||||
|
uid: 'u_123',
|
||||||
|
nick_name: '单向好友',
|
||||||
|
age: 20,
|
||||||
|
source: '来源'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
async pack_data (data: string): Promise<Uint8Array> {
|
async pack_data (data: string): Promise<Uint8Array> {
|
||||||
return ProtoBuf(class extends ProtoBufBase {
|
return ProtoBuf(class extends ProtoBufBase {
|
||||||
@@ -30,7 +41,7 @@ export class GetUnidirectionalFriendList extends OneBotAction<void, Friend[]> {
|
|||||||
}).encode();
|
}).encode();
|
||||||
}
|
}
|
||||||
|
|
||||||
async _handle (): Promise<Friend[]> {
|
async _handle (): Promise<ReturnType> {
|
||||||
const self_id = this.core.selfInfo.uin;
|
const self_id = this.core.selfInfo.uin;
|
||||||
const req_json = {
|
const req_json = {
|
||||||
uint64_uin: self_id,
|
uint64_uin: self_id,
|
||||||
@@ -40,10 +51,18 @@ export class GetUnidirectionalFriendList extends OneBotAction<void, Friend[]> {
|
|||||||
};
|
};
|
||||||
const packed_data = await this.pack_data(JSON.stringify(req_json));
|
const packed_data = await this.pack_data(JSON.stringify(req_json));
|
||||||
const data = Buffer.from(packed_data);
|
const data = Buffer.from(packed_data);
|
||||||
const rsq = { cmd: 'MQUpdateSvc_com_qq_ti.web.OidbSvc.0xe17_0', data: data as PacketBuf };
|
const rsq = { cmd: 'MQUpdateSvc_com_qq_ti.web.OidbSvc.0xe17_0', data: data as unknown as PacketBuf };
|
||||||
const rsp_data = await this.core.apis.PacketApi.pkt.operation.sendPacket(rsq, true);
|
const rsp_data = await this.core.apis.PacketApi.pkt.operation.sendPacket(rsq, true);
|
||||||
const block_json = ProtoBuf(class extends ProtoBufBase { data = PBString(4); }).decode(rsp_data);
|
const block_json = ProtoBuf(class extends ProtoBufBase { data = PBString(4); }).decode(rsp_data);
|
||||||
const block_list: Block[] = JSON.parse(block_json.data).rpt_block_list;
|
interface BlockItem {
|
||||||
|
uint64_uin: number;
|
||||||
|
str_uid: string;
|
||||||
|
bytes_nick: string;
|
||||||
|
uint32_age: number;
|
||||||
|
bytes_source: string;
|
||||||
|
}
|
||||||
|
const block_data: { rpt_block_list: BlockItem[]; } = JSON.parse(block_json.data);
|
||||||
|
const block_list = block_data.rpt_block_list;
|
||||||
|
|
||||||
return block_list.map((block) => ({
|
return block_list.map((block) => ({
|
||||||
uin: block.uint64_uin,
|
uin: block.uint64_uin,
|
||||||
|
|||||||
@@ -2,17 +2,38 @@ import { ActionName } from '@/napcat-onebot/action/router';
|
|||||||
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
||||||
import { Static, Type } from '@sinclair/typebox';
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
const PayloadSchema = Type.Object({
|
||||||
user_id: Type.Union([Type.Number(), Type.String()]),
|
user_id: Type.String({ description: 'QQ号' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
type PayloadType = Static<typeof PayloadSchema>;
|
||||||
|
|
||||||
export class GetUserStatus extends GetPacketStatusDepends<Payload, { status: number; ext_status: number; } | undefined> {
|
const ReturnSchema = Type.Object({
|
||||||
|
status: Type.Number({ description: '在线状态' }),
|
||||||
|
ext_status: Type.Number({ description: '扩展状态' }),
|
||||||
|
}, { description: '用户状态' });
|
||||||
|
|
||||||
|
type ReturnType = Static<typeof ReturnSchema>;
|
||||||
|
|
||||||
|
export class GetUserStatus extends GetPacketStatusDepends<PayloadType, ReturnType> {
|
||||||
override actionName = ActionName.GetUserStatus;
|
override actionName = ActionName.GetUserStatus;
|
||||||
override payloadSchema = SchemaData;
|
override payloadSchema = PayloadSchema;
|
||||||
|
override returnSchema = ReturnSchema;
|
||||||
|
override actionSummary = '获取用户在线状态';
|
||||||
|
override actionTags = ['系统扩展'];
|
||||||
|
override payloadExample = {
|
||||||
|
user_id: '123456789'
|
||||||
|
};
|
||||||
|
override returnExample = {
|
||||||
|
status: 10,
|
||||||
|
ext_status: 0
|
||||||
|
};
|
||||||
|
|
||||||
async _handle (payload: Payload) {
|
async _handle (payload: PayloadType) {
|
||||||
return await this.core.apis.PacketApi.pkt.operation.GetStrangerStatus(+payload.user_id);
|
const res = await this.core.apis.PacketApi.pkt.operation.GetStrangerStatus(+payload.user_id);
|
||||||
|
if (!res) {
|
||||||
|
throw new Error('无法获取用户状态');
|
||||||
|
}
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,24 +3,38 @@ import { FileNapCatOneBotUUID } from 'napcat-common/src/file-uuid';
|
|||||||
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
||||||
import { Static, Type } from '@sinclair/typebox';
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
const PayloadSchema = Type.Object({
|
||||||
group_id: Type.Union([Type.Number(), Type.String()]),
|
group_id: Type.String({ description: '群号' }),
|
||||||
file_id: Type.String(),
|
file_id: Type.String({ description: '文件ID' }),
|
||||||
current_parent_directory: Type.String(),
|
current_parent_directory: Type.String({ description: '当前父目录' }),
|
||||||
target_parent_directory: Type.String(),
|
target_parent_directory: Type.String({ description: '目标父目录' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
type PayloadType = Static<typeof PayloadSchema>;
|
||||||
|
|
||||||
interface MoveGroupFileResponse {
|
const ReturnSchema = Type.Object({
|
||||||
ok: boolean;
|
ok: Type.Boolean({ description: '是否成功' }),
|
||||||
}
|
}, { description: '移动文件结果' });
|
||||||
|
|
||||||
export class MoveGroupFile extends GetPacketStatusDepends<Payload, MoveGroupFileResponse> {
|
type ReturnType = Static<typeof ReturnSchema>;
|
||||||
|
|
||||||
|
export class MoveGroupFile extends GetPacketStatusDepends<PayloadType, ReturnType> {
|
||||||
override actionName = ActionName.MoveGroupFile;
|
override actionName = ActionName.MoveGroupFile;
|
||||||
override payloadSchema = SchemaData;
|
override actionSummary = '移动群文件';
|
||||||
|
override actionTags = ['文件扩展'];
|
||||||
|
override payloadExample = {
|
||||||
|
group_id: '123456',
|
||||||
|
file_id: '/file_id',
|
||||||
|
current_parent_directory: '/current_folder_id',
|
||||||
|
target_parent_directory: '/target_folder_id',
|
||||||
|
};
|
||||||
|
override returnExample = {
|
||||||
|
ok: true
|
||||||
|
};
|
||||||
|
override payloadSchema = PayloadSchema;
|
||||||
|
override returnSchema = ReturnSchema;
|
||||||
|
|
||||||
async _handle (payload: Payload) {
|
async _handle (payload: PayloadType) {
|
||||||
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
|
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
|
||||||
if (contextMsgFile?.fileUUID) {
|
if (contextMsgFile?.fileUUID) {
|
||||||
await this.core.apis.PacketApi.pkt.operation.MoveGroupFile(+payload.group_id, contextMsgFile.fileUUID, payload.current_parent_directory, payload.target_parent_directory);
|
await this.core.apis.PacketApi.pkt.operation.MoveGroupFile(+payload.group_id, contextMsgFile.fileUUID, payload.current_parent_directory, payload.target_parent_directory);
|
||||||
|
|||||||
@@ -3,18 +3,29 @@ import { ActionName } from '@/napcat-onebot/action/router';
|
|||||||
import { checkFileExist, uriToLocalFile } from 'napcat-common/src/file';
|
import { checkFileExist, uriToLocalFile } from 'napcat-common/src/file';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { Static, Type } from '@sinclair/typebox';
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
import { GeneralCallResultStatus } from 'napcat-core';
|
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
import { ExtendsActionsExamples } from '../example/ExtendsActionsExamples';
|
||||||
image: Type.String(),
|
|
||||||
|
const PayloadSchema = Type.Object({
|
||||||
|
image: Type.String({ description: '图片路径、URL或Base64' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
type PayloadType = Static<typeof PayloadSchema>;
|
||||||
|
|
||||||
class OCRImageBase extends OneBotAction<Payload, GeneralCallResultStatus> {
|
const ReturnSchema = Type.Any({ description: 'OCR结果' });
|
||||||
override payloadSchema = SchemaData;
|
|
||||||
|
|
||||||
async _handle (payload: Payload) {
|
type ReturnType = Static<typeof ReturnSchema>;
|
||||||
|
|
||||||
|
class OCRImageBase extends OneBotAction<PayloadType, ReturnType> {
|
||||||
|
override payloadSchema = PayloadSchema;
|
||||||
|
override returnSchema = ReturnSchema;
|
||||||
|
override actionSummary = '图片 OCR 识别';
|
||||||
|
override actionDescription = '识别图片中的文字内容(仅Windows端支持)';
|
||||||
|
override actionTags = ['扩展接口'];
|
||||||
|
override payloadExample = ExtendsActionsExamples.OCRImage.payload;
|
||||||
|
override returnExample = ExtendsActionsExamples.OCRImage.response;
|
||||||
|
|
||||||
|
async _handle (payload: PayloadType): Promise<ReturnType> {
|
||||||
const { path, success } = await uriToLocalFile(this.core.NapCatTempPath, payload.image);
|
const { path, success } = await uriToLocalFile(this.core.NapCatTempPath, payload.image);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
throw new Error(`OCR ${payload.image}失败, image字段可能格式不正确`);
|
throw new Error(`OCR ${payload.image}失败, image字段可能格式不正确`);
|
||||||
@@ -37,8 +48,10 @@ class OCRImageBase extends OneBotAction<Payload, GeneralCallResultStatus> {
|
|||||||
|
|
||||||
export class OCRImage extends OCRImageBase {
|
export class OCRImage extends OCRImageBase {
|
||||||
override actionName = ActionName.OCRImage;
|
override actionName = ActionName.OCRImage;
|
||||||
|
override actionSummary = '图片 OCR 识别';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IOCRImage extends OCRImageBase {
|
export class IOCRImage extends OCRImageBase {
|
||||||
override actionName = ActionName.IOCRImage;
|
override actionName = ActionName.IOCRImage;
|
||||||
|
override actionSummary = '图片 OCR 识别 (内部)';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,24 +3,38 @@ import { FileNapCatOneBotUUID } from 'napcat-common/src/file-uuid';
|
|||||||
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
|
||||||
import { Static, Type } from '@sinclair/typebox';
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
const PayloadSchema = Type.Object({
|
||||||
group_id: Type.Union([Type.Number(), Type.String()]),
|
group_id: Type.String({ description: '群号' }),
|
||||||
file_id: Type.String(),
|
file_id: Type.String({ description: '文件ID' }),
|
||||||
current_parent_directory: Type.String(),
|
current_parent_directory: Type.String({ description: '当前父目录' }),
|
||||||
new_name: Type.String(),
|
new_name: Type.String({ description: '新文件名' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
type PayloadType = Static<typeof PayloadSchema>;
|
||||||
|
|
||||||
interface RenameGroupFileResponse {
|
const ReturnSchema = Type.Object({
|
||||||
ok: boolean;
|
ok: Type.Boolean({ description: '是否成功' }),
|
||||||
}
|
}, { description: '重命名文件结果' });
|
||||||
|
|
||||||
export class RenameGroupFile extends GetPacketStatusDepends<Payload, RenameGroupFileResponse> {
|
type ReturnType = Static<typeof ReturnSchema>;
|
||||||
|
|
||||||
|
export class RenameGroupFile extends GetPacketStatusDepends<PayloadType, ReturnType> {
|
||||||
override actionName = ActionName.RenameGroupFile;
|
override actionName = ActionName.RenameGroupFile;
|
||||||
override payloadSchema = SchemaData;
|
override actionSummary = '重命名群文件';
|
||||||
|
override actionTags = ['文件扩展'];
|
||||||
|
override payloadExample = {
|
||||||
|
group_id: '123456',
|
||||||
|
file_id: '/file_id',
|
||||||
|
current_parent_directory: '/',
|
||||||
|
new_name: 'new_name.jpg'
|
||||||
|
};
|
||||||
|
override returnExample = {
|
||||||
|
ok: true
|
||||||
|
};
|
||||||
|
override payloadSchema = PayloadSchema;
|
||||||
|
override returnSchema = ReturnSchema;
|
||||||
|
|
||||||
async _handle (payload: Payload) {
|
async _handle (payload: PayloadType) {
|
||||||
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
|
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
|
||||||
if (contextMsgFile?.fileUUID) {
|
if (contextMsgFile?.fileUUID) {
|
||||||
await this.core.apis.PacketApi.pkt.operation.RenameGroupFile(+payload.group_id, contextMsgFile.fileUUID, payload.current_parent_directory, payload.new_name);
|
await this.core.apis.PacketApi.pkt.operation.RenameGroupFile(+payload.group_id, contextMsgFile.fileUUID, payload.current_parent_directory, payload.new_name);
|
||||||
|
|||||||
@@ -3,20 +3,35 @@ import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketS
|
|||||||
import { ActionName } from '@/napcat-onebot/action/router';
|
import { ActionName } from '@/napcat-onebot/action/router';
|
||||||
import { Static, Type } from '@sinclair/typebox';
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
const PayloadSchema = Type.Object({
|
||||||
cmd: Type.String(),
|
cmd: Type.String({ description: '命令字' }),
|
||||||
data: Type.String(),
|
data: Type.String({ description: '十六进制数据' }),
|
||||||
rsp: Type.Union([Type.String(), Type.Boolean()], { default: true }),
|
rsp: Type.Union([Type.String(), Type.Boolean()], { default: true, description: '是否等待响应' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
type PayloadType = Static<typeof PayloadSchema>;
|
||||||
|
|
||||||
export class SendPacket extends GetPacketStatusDepends<Payload, string | undefined> {
|
const ReturnSchema = Type.Union([Type.String({ description: '响应十六进制数据' }), Type.Undefined()], { description: '发包结果' });
|
||||||
override payloadSchema = SchemaData;
|
|
||||||
|
type ReturnType = Static<typeof ReturnSchema>;
|
||||||
|
|
||||||
|
export class SendPacket extends GetPacketStatusDepends<PayloadType, ReturnType> {
|
||||||
|
override payloadSchema = PayloadSchema;
|
||||||
|
override returnSchema = ReturnSchema;
|
||||||
override actionName = ActionName.SendPacket;
|
override actionName = ActionName.SendPacket;
|
||||||
async _handle (payload: Payload) {
|
override actionSummary = '发送原始数据包';
|
||||||
|
override actionTags = ['系统扩展'];
|
||||||
|
override payloadExample = {
|
||||||
|
cmd: 'Example.Cmd',
|
||||||
|
data: '123456',
|
||||||
|
rsp: true
|
||||||
|
};
|
||||||
|
override returnExample = '123456';
|
||||||
|
|
||||||
|
async _handle (payload: PayloadType) {
|
||||||
const rsp = typeof payload.rsp === 'boolean' ? payload.rsp : payload.rsp === 'true';
|
const rsp = typeof payload.rsp === 'boolean' ? payload.rsp : payload.rsp === 'true';
|
||||||
const data = await this.core.apis.PacketApi.pkt.operation.sendPacket({ cmd: payload.cmd, data: Buffer.from(payload.data, 'hex') as PacketBuf }, rsp);
|
const packetData = Buffer.from(payload.data, 'hex') as unknown as PacketBuf;
|
||||||
|
const data = await this.core.apis.PacketApi.pkt.operation.sendPacket({ cmd: payload.cmd, data: packetData }, rsp);
|
||||||
return typeof data === 'object' ? data.toString('hex') : undefined;
|
return typeof data === 'object' ? data.toString('hex') : undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user