Compare commits

..

7 Commits

Author SHA1 Message Date
时瑾
5bb8f9af8d fix: 刷新二维码清楚错误信息 2026-01-17 15:35:11 +08:00
时瑾
1b8860ea7d Merge branch 'main' into feature/relogin-enhancement 2026-01-17 15:24:44 +08:00
时瑾
434bc69ddb refactor: 重构重启流程,移除旧的重启逻辑,新增基于 WebUI 的重启请求处理 2026-01-17 15:24:03 +08:00
时瑾
de33ab10e5 Merge branch 'main' into feature/relogin-enhancement 2026-01-17 14:48:40 +08:00
时瑾
c44a7e4b57 cp napcat-shell-loader/launcher-win.bat 2026-01-14 23:27:26 +08:00
时瑾
b97a224a14 feat: 新增看门狗汪汪汪 2026-01-14 23:25:54 +08:00
时瑾
0918b17257 feat: 优化离线重连机制,增加前端登录错误提示与二维码刷新功能
- 增加全局掉线检测弹窗
- 增强登录错误解析,支持显示 serverErrorCode 和 message
- 优化二维码登录 UI,错误时显示详细原因并提供大按钮重新获取
- 核心层解耦,通过事件抛出 KickedOffLine 通知
- 支持前端点击刷新二维码接口
2026-01-14 18:54:59 +08:00
382 changed files with 6866 additions and 20979 deletions

View File

@@ -2,7 +2,7 @@
[使用文档](https://napneko.github.io/)
## Windows 一键包
我们提供了轻量化一键部署方案
我们提供了轻量化一键部署方案
相对于普通需要安装QQ的方案,下面已内置QQ和Napcat 阅读使用文档参考
你可以下载

View File

@@ -31,7 +31,7 @@
[使用文档](https://napneko.github.io/)
## Windows 一键包
我们提供了轻量化一键部署方案
我们提供了轻量化一键部署方案
相对于普通需要安装QQ的方案,下面已内置QQ和Napcat 阅读使用文档参考
你可以下载

View File

@@ -5,63 +5,6 @@ on:
types: [published]
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:
runs-on: ubuntu-latest
steps:
@@ -137,17 +80,4 @@ jobs:
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
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}\"}}"

View File

@@ -41,7 +41,6 @@ jobs:
pnpm test || exit 1
pnpm --filter napcat-webui-frontend run build || exit 1
pnpm run build:framework
pnpm --filter napcat-plugin-builtin run build || exit 1
mv packages/napcat-framework/dist framework-dist
cd framework-dist
npm install --omit=dev
@@ -84,7 +83,6 @@ jobs:
pnpm test || exit 1
pnpm --filter napcat-webui-frontend run build || exit 1
pnpm run build:shell
pnpm --filter napcat-plugin-builtin run build || exit 1
mv packages/napcat-shell/dist shell-dist
cd shell-dist
npm install --omit=dev

View File

@@ -10,7 +10,7 @@ permissions: write-all
env:
OPENROUTER_API_URL: https://91vip.futureppo.top/v1/chat/completions
OPENROUTER_MODEL: "copilot/gemini-3-flash-preview"
OPENROUTER_MODEL: "copilot/ant/gemini-3-flash-preview"
RELEASE_NAME: "NapCat"
jobs:
@@ -62,7 +62,6 @@ jobs:
pnpm i
pnpm --filter napcat-webui-frontend run build || exit 1
pnpm run build:framework
pnpm --filter napcat-plugin-builtin run build || exit 1
mv packages/napcat-framework/dist framework-dist
cd framework-dist
npm install --omit=dev
@@ -92,7 +91,6 @@ jobs:
pnpm i
pnpm --filter napcat-webui-frontend run build || exit 1
pnpm run build:shell
pnpm --filter napcat-plugin-builtin run build || exit 1
mv packages/napcat-shell/dist shell-dist
cd shell-dist
npm install --omit=dev

3
.gitignore vendored
View File

@@ -16,5 +16,4 @@ checkVersion.sh
bun.lockb
tests/run/
guild1.db-wal
guild1.db-shm
packages/napcat-develop/config/.env
guild1.db-shm

View File

@@ -50,7 +50,7 @@ _Modern protocol-side framework implemented based on NTQQ._
| Docs | [![Github.IO](https://img.shields.io/badge/docs%20on-Github.IO-orange)](https://napneko.github.io/) | [![Cloudflare.Worker](https://img.shields.io/badge/docs%20on-Cloudflare.Worker-black)](https://doc.napneko.icu/) | [![Cloudflare.HKServer](https://img.shields.io/badge/docs%20on-Cloudflare.HKServer-informational)](https://napcat.napneko.icu/) |
|:-:|:-:|:-:|:-:|
| Docs | [![Cloudflare.Pages](https://img.shields.io/badge/docs%20on-Cloudflare.Pages-blue)](https://napneko.pages.dev/) | [![Server.Other](https://img.shields.io/badge/docs%20on-Server.Other-green)](https://napcat.top/) | [![NapCat.Top](https://img.shields.io/badge/docs%20on-NapCat.Top-red)](https://napcat.top/) |
| Docs | [![Cloudflare.Pages](https://img.shields.io/badge/docs%20on-Cloudflare.Pages-blue)](https://napneko.pages.dev/) | [![Server.Other](https://img.shields.io/badge/docs%20on-Server.Other-green)](https://napcat.cyou/) | [![NapCat.Wiki](https://img.shields.io/badge/docs%20on-NapCat.Wiki-red)](https://www.napcat.wiki) |
|:-:|:-:|:-:|:-:|
| QQ Group | [![QQ Group#4](https://img.shields.io/badge/QQ%20Group%234-Join-blue)](https://qm.qq.com/q/CMmPbGw0jA) | [![QQ Group#3](https://img.shields.io/badge/QQ%20Group%233-Join-blue)](https://qm.qq.com/q/8zJMLjqy2Y) | [![QQ Group#2](https://img.shields.io/badge/QQ%20Group%232-Join-blue)](https://qm.qq.com/q/CMmPbGw0jA) | [![QQ Group#1](https://img.shields.io/badge/QQ%20Group%231-Join-blue)](https://qm.qq.com/q/I6LU87a0Yq) |

View File

@@ -6,10 +6,8 @@
"scripts": {
"build:shell": "pnpm --filter napcat-shell run build || 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: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",
"typecheck": "pnpm -r --if-present run typecheck",
"test": "pnpm --filter napcat-test run test",

View File

@@ -1,176 +0,0 @@
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';

View File

@@ -1,30 +0,0 @@
{
"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"
}
}

View File

@@ -1,28 +1,28 @@
{
"name": "napcat-common",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "src/index.ts",
"scripts": {
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
},
"exports": {
".": {
"import": "./src/index.ts"
"name": "napcat-common",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "src/index.ts",
"scripts": {
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
},
"./src/*": {
"import": "./src/*"
"exports": {
".": {
"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"
}
}

View File

@@ -2,14 +2,11 @@ import fs from 'fs';
import { stat } from 'fs/promises';
import crypto, { randomUUID } from 'crypto';
import path from 'node:path';
import http from 'node:http';
import tls from 'node:tls';
import { solveProblem } from '@/napcat-common/src/helper';
export interface HttpDownloadOptions {
url: string;
headers?: Record<string, string> | string;
proxy?: string;
}
type Uri2LocalRes = {
@@ -99,7 +96,6 @@ export function calculateFileMD5 (filePath: string): Promise<string> {
async function tryDownload (options: string | HttpDownloadOptions, useReferer: boolean = false): Promise<Response> {
let url: string;
let proxy: string | undefined;
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',
};
@@ -108,7 +104,6 @@ async function tryDownload (options: string | HttpDownloadOptions, useReferer: b
headers['Host'] = new URL(url).hostname;
} else {
url = options.url;
proxy = options.proxy;
if (options.headers) {
if (typeof options.headers === 'string') {
headers = JSON.parse(options.headers);
@@ -120,18 +115,6 @@ async function tryDownload (options: string | HttpDownloadOptions, useReferer: b
if (useReferer && !headers['Referer']) {
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) => {
if (err.cause) {
throw err.cause;
@@ -141,220 +124,6 @@ async function tryDownload (options: string | HttpDownloadOptions, useReferer: b
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> {
const useReferer = typeof options === 'string';
let resp = await tryDownload(options);
@@ -407,7 +176,7 @@ export async function checkUriType (Uri: string) {
return { Uri, Type: FileUriType.Unknown };
}
export async function uriToLocalFile (dir: string, uri: string, filename: string = randomUUID(), headers?: Record<string, string>, proxy?: string): Promise<Uri2LocalRes> {
export async function uriToLocalFile (dir: string, uri: string, filename: string = randomUUID(), headers?: Record<string, string>): Promise<Uri2LocalRes> {
const { Uri: HandledUri, Type: UriType } = await checkUriType(uri);
const filePath = path.join(dir, filename);
@@ -422,7 +191,7 @@ export async function uriToLocalFile (dir: string, uri: string, filename: string
}
case FileUriType.Remote: {
const buffer = await httpDownload({ url: HandledUri, headers: headers ?? {}, proxy });
const buffer = await httpDownload({ url: HandledUri, headers: headers ?? {} });
fs.writeFileSync(filePath, buffer);
return { success: true, errMsg: '', fileName: filename, path: filePath };
}

View File

@@ -220,13 +220,13 @@ export function parseAppidFromMajor (nodeMajor: string): string | undefined {
// ============== GitHub Tags 获取 ==============
// 使用 mirror 模块统一管理镜像
export async function getAllTags (mirror?: string): Promise<{ tags: string[], mirror: string; }> {
return getAllTagsFromMirror('NapNeko', 'NapCatQQ', mirror);
export async function getAllTags (): Promise<{ tags: string[], mirror: string; }> {
return getAllTagsFromMirror('NapNeko', 'NapCatQQ');
}
export async function getLatestTag (mirror?: string): Promise<string> {
const { tags } = await getAllTags(mirror);
export async function getLatestTag (): Promise<string> {
const { tags } = await getAllTags();
// 使用 SemVer 规范排序
tags.sort((a, b) => compareSemVer(a, b));

View File

@@ -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();
if (keyList.length === 0) {
return undefined;
}
const result: { key: K; value: V; }[] = [];
const result: { key: K; value: V }[] = [];
const listSize = Math.min(size, keyList.length);
for (let i = 0; i < listSize; i++) {
const key = keyList[listSize - i];
@@ -108,7 +108,7 @@ class MessageUniqueWrapper {
return shortId;
}
getMsgIdAndPeerByShortId (shortId: number): { MsgId: string; Peer: Peer; } | undefined {
getMsgIdAndPeerByShortId (shortId: number): { MsgId: string; Peer: Peer } | undefined {
const data = this.msgDataMap.getKey(shortId);
if (data) {
const [msgId, chatTypeStr, peerUid] = data.split('|');
@@ -136,12 +136,6 @@ class MessageUniqueWrapper {
this.msgIdMap.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();

View File

@@ -23,50 +23,66 @@ import { PromiseTimer } from './helper';
* 懒加载测速:首次使用时自动测速,缓存 30 分钟
*/
export const GITHUB_FILE_MIRRORS = [
'https://github.chenc.dev/',
'https://ghproxy.cfd/',
'https://github.tbedu.top/',
'https://ghproxy.cc/',
'https://gh.monlor.com/',
'https://cdn.akaere.online/',
'https://gh.idayer.com/',
'https://gh.llkk.cc/',
'https://ghpxy.hwinzniej.top/',
'https://github-proxy.memory-echoes.cn/',
'https://git.yylx.win/',
'https://gitproxy.mrhjx.cn/',
'https://gh.fhjhy.top/',
'https://gp.zkitefly.eu.org/',
'https://gh-proxy.com/',
'https://ghfile.geekertao.top/',
'https://j.1lin.dpdns.org/',
'https://ghproxy.imciel.com/',
'https://github-proxy.teach-english.tech/',
'https://gh.927223.xyz/',
'https://github.ednovas.xyz/',
'https://ghf.xn--eqrr82bzpe.top/',
'https://gh.dpik.top/',
'https://gh.jasonzeng.dev/',
'https://gh.xxooo.cf/',
'https://gh.bugdey.us.kg/',
'https://ghm.078465.xyz/',
'https://j.1win.ggff.net/',
'https://tvv.tw/',
'https://gitproxy.127731.xyz/',
'https://gh.inkchills.cn/',
'https://ghproxy.cxkpro.top/',
'https://gh.sixyin.com/',
'https://github.geekery.cn/',
'https://git.669966.xyz/',
'https://gh.5050net.cn/',
'https://gh.felicity.ac.cn/',
'https://github.dpik.top/',
'https://ghp.keleyaa.com/',
'https://gh.wsmdn.dpdns.org/',
'https://ghproxy.monkeyray.net/',
'https://fastgit.cc/',
'https://gh.catmak.name/',
'https://gh.noki.icu/',
// 延迟 < 800ms 的最快镜像
'https://github.chenc.dev/', // 666ms
'https://ghproxy.cfd/', // 719ms - 支持重定向
'https://github.tbedu.top/', // 760ms
'https://ghps.cc/', // 768ms
'https://gh.llkk.cc/', // 774ms
'https://ghproxy.cc/', // 777ms
'https://gh.monlor.com/', // 779ms
'https://cdn.akaere.online/', // 784ms
// 延迟 800-1000ms 的快速镜像
'https://gh.idayer.com/', // 869ms
'https://gh-proxy.net/', // 885ms
'https://ghpxy.hwinzniej.top/', // 890ms
'https://github-proxy.memory-echoes.cn/', // 896ms
'https://git.yylx.win/', // 917ms
'https://gitproxy.mrhjx.cn/', // 950ms
'https://jiashu.1win.eu.org/', // 954ms
'https://ghproxy.cn/', // 981ms
// 延迟 1000-1500ms 的中速镜像
'https://gh.fhjhy.top/', // 1014ms
'https://gp.zkitefly.eu.org/', // 1015ms
'https://gh-proxy.com/', // 1022ms
'https://hub.gitmirror.com/', // 1027ms
'https://ghfile.geekertao.top/', // 1029ms
'https://j.1lin.dpdns.org/', // 1037ms
'https://ghproxy.imciel.com/', // 1047ms
'https://github-proxy.teach-english.tech/', // 1047ms
'https://gh.927223.xyz/', // 1071ms
'https://github.ednovas.xyz/', // 1099ms
'https://ghf.xn--eqrr82bzpe.top/',// 1122ms
'https://gh.dpik.top/', // 1131ms
'https://gh.jasonzeng.dev/', // 1139ms
'https://gh.xxooo.cf/', // 1157ms
'https://gh.bugdey.us.kg/', // 1228ms
'https://ghm.078465.xyz/', // 1289ms
'https://j.1win.ggff.net/', // 1329ms
'https://tvv.tw/', // 1393ms
'https://gh.chjina.com/', // 1446ms
'https://gitproxy.127731.xyz/', // 1458ms
// 延迟 1500-2500ms 的较慢镜像
'https://gh.inkchills.cn/', // 1617ms
'https://ghproxy.cxkpro.top/', // 1651ms
'https://gh.sixyin.com/', // 1686ms
'https://github.geekery.cn/', // 1734ms
'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无镜像
];
@@ -93,6 +109,7 @@ export const GITHUB_RAW_MIRRORS = [
// 测试确认支持 raw 文件的镜像
'https://github.chenc.dev/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://gh-proxy.net/https://raw.githubusercontent.com',
];
@@ -133,7 +150,7 @@ const defaultConfig: MirrorConfig = {
apiMirrors: GITHUB_API_MIRRORS,
rawMirrors: GITHUB_RAW_MIRRORS,
nightlyLinkMirrors: NIGHTLY_LINK_MIRRORS,
timeout: 5000, // 5秒超时,平衡速度和可靠性
timeout: 10000, // 10秒超时,平衡速度和可靠性
enabled: true,
customMirror: undefined,
};
@@ -257,7 +274,7 @@ export async function getFastMirrors (forceRefresh: boolean = false): Promise<st
async function performMirrorTest (): Promise<string[]> {
// 开始镜像测速
const timeout = 3000; // 测速超时 3
const timeout = 8000; // 测速超时 8
// 并行测试所有镜像
const mirrors = currentConfig.fileMirrors.filter(m => m);
@@ -383,14 +400,17 @@ export async function testUrlHead (url: string, timeout: number = 5000): Promise
}, (res) => {
const statusCode = res.statusCode || 0;
const contentType = (res.headers['content-type'] as string) || '';
const contentLength = parseInt((res.headers['content-length'] as string) || '0', 10);
// 简化验证条件:
// 验证条件:
// 1. 状态码 2xx 或 3xx
// 2. Content-Type 不应该是 text/html表示错误页面
// 3. 对于 .zip 文件Content-Length 应该 > 1MB避免获取到错误页面
const isValidStatus = statusCode >= 200 && statusCode < 400;
const isNotHtmlError = !contentType.includes('text/html');
const isValidSize = url.endsWith('.zip') ? contentLength > 1024 * 1024 : true;
resolve(isValidStatus && isNotHtmlError);
resolve(isValidStatus && isNotHtmlError && isValidSize);
});
req.on('error', () => resolve(false));
@@ -434,9 +454,10 @@ export async function validateUrl (url: string, timeout: number = 5000): Promise
const contentType = (res.headers['content-type'] as string) || '';
const contentLength = parseInt((res.headers['content-length'] as string) || '0', 10);
// 简化验证条件
// 验证条件
const isValidStatus = statusCode >= 200 && statusCode < 400;
const isNotHtmlError = !contentType.includes('text/html');
const isValidSize = url.endsWith('.zip') ? contentLength > 1024 * 1024 : true;
if (!isValidStatus) {
resolve({
@@ -454,6 +475,14 @@ export async function validateUrl (url: string, timeout: number = 5000): Promise
contentLength,
error: '返回了 HTML 页面而非文件',
});
} else if (!isValidSize) {
resolve({
valid: false,
statusCode,
contentType,
contentLength,
error: `文件过小 (${contentLength} bytes),可能是错误页面`,
});
} else {
resolve({
valid: true,
@@ -530,21 +559,21 @@ export async function findAvailableDownloadUrl (
const testWithValidation = async (url: string): Promise<boolean> => {
if (validateContent) {
const result = await validateUrl(url, timeout);
// 额外检查文件大小(仅当指定了 minFileSize 时)
// 额外检查文件大小
if (result.valid && minFileSize && result.contentLength && result.contentLength < minFileSize) {
return false;
}
return result.valid;
}
// 不验证内容,只检查状态码
const isValid = testMethod === 'head' ? await testUrlHead(url, timeout) : await testUrl(url, timeout);
return isValid;
return testMethod === 'head' ? testUrlHead(url, timeout) : testUrl(url, timeout);
};
// 1. 如果设置了自定义镜像,直接使用(不测试,信任用户选择)
// 1. 如果设置了自定义镜像,优先使用
if (customMirror) {
const customUrl = buildMirrorUrl(originalUrl, customMirror);
return customUrl;
if (await testWithValidation(customUrl)) {
return customUrl;
}
}
// 2. 先测试原始 URL
@@ -556,7 +585,7 @@ export async function findAvailableDownloadUrl (
let testedCount = 0;
for (const mirror of mirrors) {
if (!mirror) continue; // 跳过空字符串
// 特殊处理 nightly.link URL
let mirrorUrl: string;
if (originalUrl.includes('nightly.link')) {
@@ -565,7 +594,7 @@ export async function findAvailableDownloadUrl (
} else {
mirrorUrl = buildMirrorUrl(originalUrl, mirror);
}
testedCount++;
if (await testWithValidation(mirrorUrl)) {
return mirrorUrl;
@@ -621,15 +650,8 @@ function compareSemVerSimple (a: string, b: string): number {
* 从 tags 列表中获取最新的 release tag
* 不依赖 GitHub API
*/
// Update definitions validation locally first if possible.
// 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);
export async function getLatestReleaseTag (owner: string, repo: string): Promise<string> {
const result = await getAllGitHubTags(owner, repo);
// 过滤出符合 semver 的 tags
const releaseTags = result.tags.filter(tag => SEMVER_REGEX.test(tag));
@@ -679,8 +701,6 @@ export async function getGitHubRelease (
assetNames?: string[];
/** 是否需要获取 changelog需要调用 API */
fetchChangelog?: boolean;
/** 指定镜像 */
mirror?: string;
} = {}
): Promise<{
tag_name: string;
@@ -690,16 +710,15 @@ export async function getGitHubRelease (
}>;
body?: string;
}> {
const { assetNames = [], fetchChangelog = false, mirror } = options;
const { assetNames = [], fetchChangelog = false } = options;
// 1. 获取实际的 tag 名称
let actualTag: string;
if (tag === 'latest') {
actualTag = await getLatestReleaseTag(owner, repo, mirror);
actualTag = await getLatestReleaseTag(owner, repo);
} else {
actualTag = tag;
}
// ...
// 2. 构建 assets 列表(不需要 API
const assets = assetNames.map(name => ({
@@ -763,8 +782,8 @@ const tagsCache: Map<string, TagsCache> = new Map();
* 获取所有 GitHub tags带缓存
* 优化:并行请求多个镜像,使用第一个成功返回的结果
*/
export async function getAllGitHubTags (owner: string, repo: string, mirror?: string): Promise<{ tags: string[], mirror: string; }> {
const cacheKey = `${owner}/${repo}/${mirror || 'auto'}`;
export async function getAllGitHubTags (owner: string, repo: string): Promise<{ tags: string[], mirror: string; }> {
const cacheKey = `${owner}/${repo}`;
// 检查缓存
const cached = tagsCache.get(cacheKey);
@@ -786,7 +805,7 @@ export async function getAllGitHubTags (owner: string, repo: string, mirror?: st
};
// 尝试从 URL 获取 tags
const fetchFromUrl = async (url: string, usedMirror: string): Promise<{ tags: string[], mirror: string; } | null> => {
const fetchFromUrl = async (url: string, mirror: string): Promise<{ tags: string[], mirror: string; } | null> => {
try {
const raw = await PromiseTimer(
RequestUtil.HttpGetText(url),
@@ -794,55 +813,79 @@ export async function getAllGitHubTags (owner: string, repo: string, mirror?: st
);
// 检查返回内容是否有效(不是 HTML 错误页面)
if (raw.includes('refs/tags')) {
return { tags: parseTags(raw), mirror: usedMirror };
if (raw.includes('<!DOCTYPE') || raw.includes('<html')) {
return null;
}
const tags = parseTags(raw);
if (tags.length > 0) {
return { tags, mirror };
}
return null;
} catch {
// 忽略错误
return null;
}
return null;
};
// 准备镜像列表
let mirrors: string[] = [];
if (mirror) {
// 如果指定了镜像,只使用该镜像
mirrors = [mirror];
} else {
// 否则使用 auto 逻辑,利用缓存的快速镜像列表
mirrors = await getFastMirrors();
// 获取快速镜像列表
let fastMirrors: string[] = [];
try {
fastMirrors = await getFastMirrors();
} catch {
// 忽略错误
}
// 并行请求
const promises = mirrors.map(m => {
const url = m ? buildMirrorUrl(baseUrl, m) : baseUrl;
return fetchFromUrl(url, m || 'https://github.com');
});
// 构建 URL 列表(取前 5 个快速镜像 + 原始 URL 并行请求
const topMirrors = fastMirrors.slice(0, 5);
const mirrorUrls = [
{ url: baseUrl, mirror: 'github.com' }, // 原始 URL
...topMirrors.filter(m => m).map(m => ({ url: buildMirrorUrl(baseUrl, m), mirror: m })),
];
// 并行请求所有镜像,使用 Promise.any 获取第一个成功的结果
try {
const result = await Promise.any(promises.filter(p => p !== null) as Promise<{ tags: string[], mirror: string; } | null>[]);
const result = await Promise.any(
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) {
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;
}
} catch {
// all failed
}
if (mirror) {
throw new Error(`指定镜像 ${mirror} 获取 tags 失败`);
// 最后尝试所有镜像
const allMirrors = currentConfig.fileMirrors.filter(m => m && !fastMirrors.includes(m));
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 支持 ==============
// ActionArtifact 接口定义
export interface ActionArtifact {
id: number;
name: string;
@@ -852,14 +895,12 @@ export interface ActionArtifact {
archive_download_url: string;
workflow_run_id?: number;
head_sha?: string;
workflow_title?: string;
}
// ============== Action Artifacts 缓存 ==============
interface ArtifactsCache {
artifacts: ActionArtifact[];
mirror: string;
timestamp: number;
}
@@ -879,116 +920,68 @@ export function clearArtifactsCache (): void {
* 当 api.github.com 不可用时使用
* 页面格式: https://github.com/{owner}/{repo}/actions/workflows/{workflow}
*/
async function getWorkflowRunsFromHtml (
owner: string,
repo: string,
workflow: string = 'build.yml',
maxRuns: number = 10,
mirror?: string
): Promise<{ runs: Array<{ id: number; created_at: string; title: string; }>; mirror: string; }> {
maxRuns: number = 10
): Promise<Array<{ id: number; created_at: string; }>> {
const baseUrl = `https://github.com/${owner}/${repo}/actions/workflows/${workflow}`;
// 尝试使用镜像获取 HTML
// 如果指定了 mirror则只使用该 mirror
let mirrors: string[] = [];
if (mirror) {
mirrors = [mirror];
} else {
// 使用缓存的快速镜像列表
mirrors = await getFastMirrors();
}
for (const mirrorItem of mirrors) {
const mirrors = ['', ...currentConfig.fileMirrors.filter(m => m)];
for (const mirror of mirrors) {
try {
const allRuns: Array<{ id: number; created_at: string; title: string; }> = [];
const foundIds = new Set<number>();
let page = 1;
const maxPages = 10; // 防止无限请求最多翻10页约250个条目
while (allRuns.length < maxRuns && page <= maxPages) {
const pageUrl = page > 1 ? `${baseUrl}?page=${page}` : baseUrl;
const url = mirrorItem ? buildMirrorUrl(pageUrl, mirrorItem) : pageUrl;
const html = await PromiseTimer(
RequestUtil.HttpGetText(url),
10000
);
// 使用 Block 分割策略,更稳健地关联 ID 和时间
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++;
const url = mirror ? buildMirrorUrl(baseUrl, mirror) : baseUrl;
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]);
}
if (allRuns.length > 0) {
return { runs: allRuns, mirror: mirrorItem || 'https://github.com' };
const runs: Array<{ id: number; created_at: string; }> = [];
const foundIds = new Set<number>();
let timeIndex = 0;
let match;
while ((match = runPattern.exec(html)) !== null && runs.length < maxRuns) {
const id = parseInt(match[1]);
if (!foundIds.has(id)) {
foundIds.add(id);
// 尝试获取对应的时间,每个 run 通常有两个时间(桌面和移动端显示)
// 所以每找到一个 run跳过两个时间
const created_at = times[timeIndex] || new Date().toISOString();
timeIndex += 2; // 跳过两个时间(桌面端和移动端各一个)
runs.push({
id,
created_at,
});
}
}
if (runs.length > 0) {
return runs;
}
} catch {
continue;
}
}
return { runs: [], mirror: '' };
return [];
}
/**
@@ -1003,49 +996,122 @@ async function getArtifactsFromNightlyLink (
owner: string,
repo: string,
workflow: string = 'build.yml',
_branch: string = 'main',
maxRuns: number = 10,
mirror?: string
): Promise<{ artifacts: ActionArtifact[], mirror: string; }> {
// 策略: 优先使用 nightly.link更稳定无需认证+ HTML 解析
branch: string = 'main',
maxRuns: number = 10
): Promise<ActionArtifact[]> {
let workflowRuns: Array<{ id: number; head_sha?: string; created_at: string; }> = [];
// 策略1: 优先尝试 GitHub API
try {
// 以前尝试使用 GitHub API现在弃用完全使用 HTML 解析逻辑
// 并获取 workflow // 直接从 HTML 页面解析
const { runs: workflowRuns, mirror: runsMirror } = await getWorkflowRunsFromHtml(owner, repo, workflow, maxRuns, mirror);
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
);
if (workflowRuns.length === 0) {
return { artifacts: [], mirror: runsMirror };
if (runsResponse.workflow_runs && runsResponse.workflow_runs.length > 0) {
workflowRuns = runsResponse.workflow_runs;
}
// 直接拼接 nightly.link URL
// 格式: https://nightly.link/{owner}/{repo}/actions/runs/{run_id}/{artifact_name}.zip
const artifacts: ActionArtifact[] = [];
const artifactNames = ['NapCat.Framework', 'NapCat.Shell']; // 已知的 artifact 名称
// 如果 HTML 解析使用的 mirror 是 github.com则 nightly.link 使用默认配置
// 如果使用了镜像,可能需要特殊的 nightly.link 镜像,或者这里仅记录 HTML 来源镜像
// 实际上 nightly.link 本身就是一个服务,我们使用配置中的 nightlyLinkMirrors
const baseNightlyMirror = currentConfig.nightlyLinkMirrors[0] || 'https://nightly.link';
for (const run of workflowRuns) {
for (const artifactName of artifactNames) {
artifacts.push({
id: run.id,
name: artifactName,
size_in_bytes: 0,
created_at: run.created_at,
expires_at: new Date(new Date(run.created_at).getTime() + 3 * 24 * 60 * 60 * 1000).toISOString(),
archive_download_url: `${baseNightlyMirror}/${owner}/${repo}/actions/runs/${run.id}/${artifactName}.zip`,
workflow_run_id: run.id,
workflow_title: run.title,
});
}
}
return { artifacts, mirror: runsMirror };
} catch {
return { artifacts: [], mirror: '' };
// API 请求失败,继续尝试 HTML 解析
}
// 策略2: API 失败时,从 HTML 页面解析
if (workflowRuns.length === 0) {
workflowRuns = await getWorkflowRunsFromHtml(owner, repo, workflow, maxRuns);
}
if (workflowRuns.length === 0) {
return [];
}
// 直接拼接 nightly.link URL
// 格式: https://nightly.link/{owner}/{repo}/actions/runs/{run_id}/{artifact_name}.zip
const artifacts: ActionArtifact[] = [];
const artifactNames = ['NapCat.Framework', 'NapCat.Shell']; // 已知的 artifact 名称
for (const run of workflowRuns) {
for (const artifactName of artifactNames) {
const mirror = currentConfig.nightlyLinkMirrors[0] || 'https://nightly.link';
artifacts.push({
id: run.id,
name: artifactName,
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 allArtifacts;
}
/**
@@ -1055,41 +1121,48 @@ async function getArtifactsFromNightlyLink (
* 策略:
* 1. 检查缓存10分钟有效
* 2. 优先尝试从 nightly.link 获取(无需认证,更稳定)
* 3. 这里的实现已经完全移除了对 GitHub API 的依赖,直接解析 HTML
* 3. 如果失败,回退到 GitHub API
*/
export async function getLatestActionArtifacts (
owner: string,
repo: string,
workflow: string = 'build.yml',
branch: string = 'main',
maxRuns: number = 10,
mirror?: string
): Promise<{ artifacts: ActionArtifact[], mirror: string; }> {
const cacheKey = `${owner}/${repo}/${workflow}/${branch}/${mirror || 'auto'}`;
maxRuns: number = 10
): Promise<ActionArtifact[]> {
const cacheKey = `${owner}/${repo}/${workflow}/${branch}`;
// 检查缓存
const cached = artifactsCache.get(cacheKey);
if (cached && (Date.now() - cached.timestamp) < ARTIFACTS_CACHE_TTL) {
return { artifacts: cached.artifacts, mirror: cached.mirror };
return cached.artifacts;
}
let result: { artifacts: ActionArtifact[], mirror: string; } = { artifacts: [], mirror: '' };
let artifacts: ActionArtifact[] = [];
// 策略: 优先使用 nightly.link更稳定无需认证+ HTML 解析
// 策略1: 优先使用 nightly.link更稳定无需认证
try {
result = await getArtifactsFromNightlyLink(owner, repo, workflow, branch, maxRuns, mirror);
artifacts = await getArtifactsFromNightlyLink(owner, repo, workflow, branch, maxRuns);
} catch {
// 获取失败
// nightly.link 获取失败
}
// 策略2: 回退到 GitHub API
if (artifacts.length === 0) {
try {
artifacts = await getArtifactsFromAPI(owner, repo, workflow, branch, maxRuns);
} catch {
// API 获取失败
}
}
// 缓存结果(即使为空也缓存,避免频繁请求)
if (result.artifacts.length > 0) {
if (artifacts.length > 0) {
artifactsCache.set(cacheKey, {
artifacts: result.artifacts,
mirror: result.mirror,
artifacts,
timestamp: Date.now(),
});
}
return result;
return artifacts;
}

View File

@@ -65,11 +65,9 @@ export function compareSemVer (v1: string, v2: string): -1 | 0 | 1 {
const a = parseSemVer(v1);
const b = parseSemVer(v2);
if (!a.valid && !b.valid) {
return v1.localeCompare(v2) as -1 | 0 | 1;
if (!a.valid || !b.valid) {
return 0;
}
if (!a.valid) return -1;
if (!b.valid) return 1;
// 比较主版本号
if (a.major !== b.major) return a.major > b.major ? 1 : -1;

View File

@@ -1,307 +0,0 @@
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;
}
}

View File

@@ -43,7 +43,7 @@ export class NTQQFriendApi {
return retMap;
}
async delBuddy (uid: string, tempBlock = false, tempBothDel = false) {
async delBuudy (uid: string, tempBlock = false, tempBothDel = false) {
return this.context.session.getBuddyService().delBuddy({
friendUid: uid,
tempBlock,

View File

@@ -7,5 +7,3 @@ export * from './webapi';
export * from './system';
export * from './packet';
export * from './file';
export * from './online';
export * from './flash';

View File

@@ -32,9 +32,9 @@ export class NTQQMsgApi {
return this.context.session.getMsgService().getSourceOfReplyMsgV2(peer, clientSeq, time);
}
async getMsgEmojiLikesList (peer: Peer, msgSeq: string, emojiId: string, emojiType: string, cookie: string = '', count: number = 20) {
async getMsgEmojiLikesList (peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number = 20) {
// 注意此处emojiType 可选值一般为1-2 2好像是unicode表情dec值 大部分情况 Taged Mlikiowa
return this.context.session.getMsgService().getMsgEmojiLikesList(peer, msgSeq, emojiId, emojiType, cookie, false, count);
return this.context.session.getMsgService().getMsgEmojiLikesList(peer, msgSeq, emojiId, emojiType, '', false, count);
}
async setEmojiLike (peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) {

View File

@@ -1,241 +0,0 @@
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);
}
}

View File

@@ -13,17 +13,6 @@ import { createHash } from 'node:crypto';
import { basename } from 'node:path';
import { qunAlbumControl } from '../data/webapi';
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 {
context: InstanceContext;
core: NapCatCore;
@@ -36,12 +25,12 @@ export class NTQQWebApi {
async shareDigest (groupCode: string, msgSeq: string, msgRandom: string, targetGroupCode: string) {
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({
bkn: this.getBknFromCookie(cookieObject),
group_code: groupCode,
msg_seq: msgSeq,
msg_random: msgRandom,
target_group_code: targetGroupCode,
}).toString()}`;
bkn: this.getBknFromCookie(cookieObject),
group_code: groupCode,
msg_seq: msgSeq,
msg_random: msgRandom,
target_group_code: targetGroupCode,
}).toString()}`;
try {
return RequestUtil.HttpGetText(url, 'GET', '', { Cookie: this.cookieToString(cookieObject) });
} catch {
@@ -63,11 +52,11 @@ export class NTQQWebApi {
async getGroupEssenceMsg (GroupCode: string, page_start: number = 0, page_limit: number = 50) {
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({
bkn: this.getBknFromCookie(cookieObject),
page_start: page_start.toString(),
page_limit: page_limit.toString(),
group_code: GroupCode,
}).toString()}`;
bkn: this.getBknFromCookie(cookieObject),
page_start: page_start.toString(),
page_limit: page_limit.toString(),
group_code: GroupCode,
}).toString()}`;
try {
const ret = await RequestUtil.HttpGetJson<GroupEssenceMsgRet>(
url,
@@ -87,16 +76,16 @@ export class NTQQWebApi {
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
const retList: Promise<WebApiGroupMemberRet>[] = [];
const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>(
`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
st: '0',
end: '40',
sort: '1',
gc: GroupCode,
bkn: this.getBknFromCookie(cookieObject),
}).toString()}`,
'POST',
'',
{ Cookie: this.cookieToString(cookieObject) }
`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
st: '0',
end: '40',
sort: '1',
gc: GroupCode,
bkn: this.getBknFromCookie(cookieObject),
}).toString()}`,
'POST',
'',
{ Cookie: this.cookieToString(cookieObject) }
);
if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
return [];
@@ -112,16 +101,16 @@ export class NTQQWebApi {
// 遍历批量请求
for (let i = 2; i <= PageNum; i++) {
const ret = RequestUtil.HttpGetJson<WebApiGroupMemberRet>(
`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
st: ((i - 1) * 40).toString(),
end: (i * 40).toString(),
sort: '1',
gc: GroupCode,
bkn: this.getBknFromCookie(cookieObject),
}).toString()}`,
'POST',
'',
{ Cookie: this.cookieToString(cookieObject) }
`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
st: ((i - 1) * 40).toString(),
end: (i * 40).toString(),
sort: '1',
gc: GroupCode,
bkn: this.getBknFromCookie(cookieObject),
}).toString()}`,
'POST',
'',
{ Cookie: this.cookieToString(cookieObject) }
);
retList.push(ret);
}
@@ -164,7 +153,16 @@ export class NTQQWebApi {
imgWidth: number = 540,
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');
@@ -180,18 +178,18 @@ export class NTQQWebApi {
imgHeight: imgHeight.toString(),
};
const ret: SetNoticeRetSuccess = await RequestUtil.HttpGetJson<SetNoticeRetSuccess>(
`https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?${new URLSearchParams({
bkn: this.getBknFromCookie(cookieObject),
qid: GroupCode,
text: Content,
pinned: pinned.toString(),
type: type.toString(),
settings,
...(picId === '' ? {} : externalParam),
}).toString()}`,
'POST',
'',
{ Cookie: this.cookieToString(cookieObject) }
`https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?${new URLSearchParams({
bkn: this.getBknFromCookie(cookieObject),
qid: GroupCode,
text: Content,
pinned: pinned.toString(),
type: type.toString(),
settings,
...(picId === '' ? {} : externalParam),
}).toString()}`,
'POST',
'',
{ Cookie: this.cookieToString(cookieObject) }
);
return ret;
} catch {
@@ -203,20 +201,20 @@ export class NTQQWebApi {
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
try {
const ret = await RequestUtil.HttpGetJson<WebApiGroupNoticeRet>(
`https://web.qun.qq.com/cgi-bin/announce/get_t_list?${new URLSearchParams({
bkn: this.getBknFromCookie(cookieObject),
qid: GroupCode,
ft: '23',
ni: '1',
n: '1',
i: '1',
log_read: '1',
platform: '1',
s: '-1',
}).toString()}&n=20`,
'GET',
'',
{ Cookie: this.cookieToString(cookieObject) }
`https://web.qun.qq.com/cgi-bin/announce/get_t_list?${new URLSearchParams({
bkn: this.getBknFromCookie(cookieObject),
qid: GroupCode,
ft: '23',
ni: '1',
n: '1',
i: '1',
log_read: '1',
platform: '1',
s: '-1',
}).toString()}&n=20`,
'GET',
'',
{ Cookie: this.cookieToString(cookieObject) }
);
return ret?.ec === 0 ? ret : undefined;
} catch {
@@ -224,17 +222,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;
try {
const res = await RequestUtil.HttpGetText(
`https://qun.qq.com/interactive/honorlist?${new URLSearchParams({
gc: groupCode,
type: type.toString(),
}).toString()}`,
'GET',
'',
{ Cookie: this.cookieToString(cookieObject) }
`https://qun.qq.com/interactive/honorlist?${new URLSearchParams({
gc: groupCode,
type: type.toString(),
}).toString()}`,
'GET',
'',
{ Cookie: this.cookieToString(cookieObject) }
);
const match = /window\.__INITIAL_STATE__=(.*?);/.exec(res);
if (match?.[1]) {
@@ -247,7 +245,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);
if (!data) {
this.context.logger.logError(`获取类型 ${type} 的荣誉信息失败`);
@@ -306,11 +304,11 @@ export class NTQQWebApi {
return HonorInfo;
}
private cookieToString (cookieObject: { [key: string]: string; }) {
private cookieToString (cookieObject: { [key: string]: string }) {
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;
let hash = 5381;
@@ -363,7 +361,7 @@ export class NTQQWebApi {
uin,
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,
});
return response.data.album;
@@ -386,7 +384,7 @@ export class NTQQWebApi {
sAlbumID,
});
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,
'Content-Type': 'application/json',
});
@@ -432,7 +430,7 @@ export class NTQQWebApi {
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}`);
}
offset += chunk.length;
@@ -477,10 +475,10 @@ export class NTQQWebApi {
const client_key = Date.now() * 1000;
return await this.context.session.getAlbumService().doQunComment(
random_seq, {
map_info: [],
map_bytes_info: [],
map_user_account: [],
},
map_info: [],
map_bytes_info: [],
map_user_account: [],
},
qunId,
2,
createAlbumMediaFeed(uin, albumId, lloc),
@@ -511,13 +509,13 @@ export class NTQQWebApi {
const uin = this.core.selfInfo.uin || '10001';
return await this.context.session.getAlbumService().doQunLike(
random_seq, {
map_info: [],
map_bytes_info: [],
map_user_account: [],
}, {
id,
status: 1,
},
map_info: [],
map_bytes_info: [],
map_user_account: [],
}, {
id,
status: 1,
},
createAlbumFeedPublish(qunId, uin, albumId, lloc)
);
}

View File

@@ -1,358 +0,0 @@
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,
};
}

View File

@@ -518,13 +518,5 @@
"9.9.26-44725": {
"appid": 537337569,
"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"
}
}

View File

@@ -154,17 +154,5 @@
"9.9.26-44725-x64": {
"send": "0A18D0C",
"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"
}
}

View File

@@ -662,17 +662,5 @@
"9.9.26-44725-x64": {
"send": "2CEBB20",
"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"
}
}

View File

@@ -2,14 +2,14 @@ import * as crypto from 'node:crypto';
import { PacketMsg } from '@/napcat-core/packet/message/message';
interface ForwardMsgJson {
app: string;
app: string
config: ForwardMsgJsonConfig,
desc: string,
extra: ForwardMsgJsonExtra,
meta: ForwardMsgJsonMeta,
prompt: string,
ver: string,
view: string;
view: string
}
interface ForwardMsgJsonConfig {
@@ -17,7 +17,7 @@ interface ForwardMsgJsonConfig {
forward: number,
round: number,
type: string,
width: number;
width: number
}
interface ForwardMsgJsonExtra {
@@ -26,17 +26,17 @@ interface ForwardMsgJsonExtra {
}
interface ForwardMsgJsonMeta {
detail: ForwardMsgJsonMetaDetail;
detail: ForwardMsgJsonMetaDetail
}
interface ForwardMsgJsonMetaDetail {
news: {
text: string;
text: string
}[],
resid: string,
source: string,
summary: string,
uniseq: string;
uniseq: string
}
interface ForwardAdaptMsg {
@@ -50,8 +50,8 @@ interface ForwardAdaptMsgElement {
}
export class ForwardMsgBuilder {
private static build (resId: string, msg: ForwardAdaptMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string, uuid?: string): ForwardMsgJson {
const id = uuid ?? crypto.randomUUID();
private static build (resId: string, msg: ForwardAdaptMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string): ForwardMsgJson {
const id = crypto.randomUUID();
const isGroupMsg = msg.some(m => m.isGroupMsg);
if (!source) {
source = msg.length === 0 ? '聊天记录' : (isGroupMsg ? '群聊的聊天记录' : msg.map(m => m.senderName).filter((v, i, a) => a.indexOf(v) === i).slice(0, 4).join('和') + '的聊天记录');
@@ -104,19 +104,13 @@ export class ForwardMsgBuilder {
return this.build(resId, []);
}
static fromPacketMsg (resId: string, packetMsg: PacketMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string, uuid?: string): ForwardMsgJson {
static fromPacketMsg (resId: string, packetMsg: PacketMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string): ForwardMsgJson {
return this.build(resId, packetMsg.map(msg => ({
senderName: msg.senderName,
isGroupMsg: msg.groupId !== undefined,
msg: msg.msg.map(m => ({
preview: m.valid ? m.toPreview() : '[该消息类型暂不支持查看]',
})),
})),
source,
news,
summary,
prompt,
uuid,
);
})), source, news, summary, prompt);
}
}

View File

@@ -5,7 +5,6 @@ import fs from 'node:fs/promises';
import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/napcat-core/index';
import { ILogWrapper } from 'napcat-common/src/log-interface';
import EventEmitter from 'node:events';
export enum LogLevel {
DEBUG = 'debug',
INFO = 'info',
@@ -264,13 +263,7 @@ function msgElementToText (element: MessageElement, msg: RawMessage, recursiveLe
}
if (element.fileElement) {
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}/]`;
}
return `[文件 ${element.fileElement.fileName}]`;
}
if (element.videoElement) {
@@ -294,11 +287,7 @@ function msgElementToText (element: MessageElement, msg: RawMessage, recursiveLe
}
if (element.markdownElement) {
if (element.markdownElement?.mdSummary) {
return element.markdownElement.mdSummary;
} else {
return '[Markdown 消息]';
}
return '[Markdown 消息]';
}
if (element.multiForwardMsgElement) {
@@ -307,8 +296,6 @@ function msgElementToText (element: MessageElement, msg: RawMessage, recursiveLe
if (element.elementType === ElementType.GreyTip) {
return '[灰条消息]';
} else if (element.elementType === ElementType.FILE) {
return '[文件发送中]';
}
return `[未实现 (ElementType = ${element.elementType})]`;

View File

@@ -5,7 +5,6 @@ import AppidTable from '@/napcat-core/external/appid.json';
import { LogWrapper } from './log';
import { getMajorPath } from '@/napcat-core/index';
import { QQAppidTableType, QQPackageInfoType, QQVersionConfigType } from 'napcat-common/src/types';
import path from 'node:path';
export class QQBasicInfoWrapper {
QQMainPath: string | undefined;
@@ -22,10 +21,6 @@ export class QQBasicInfoWrapper {
// 基础目录获取
this.context = context;
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);
// 基础信息获取 无快更则启用默认模板填充
@@ -104,10 +99,7 @@ export class QQBasicInfoWrapper {
}
getAppidV2ByMajor (QQVersion: string) {
if (!this.QQMainPath) {
throw new Error('QQMainPath未定义 无法通过Major获取Appid');
}
const majorPath = getMajorPath(QQVersion, this.QQMainPath);
const majorPath = getMajorPath(QQVersion);
const appid = parseAppidFromMajor(majorPath);
return appid;
}

View File

@@ -1,140 +0,0 @@
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);
}

View File

@@ -6,8 +6,6 @@ import {
NTQQSystemApi,
NTQQUserApi,
NTQQWebApi,
NTQQFlashApi,
NTQQOnlineApi,
} from '@/napcat-core/apis';
import { NTQQCollectionApi } from '@/napcat-core/apis/collection';
import {
@@ -25,8 +23,7 @@ import path from 'node:path';
import fs from 'node:fs';
import { hostname, systemName, systemVersion } from 'napcat-common/src/system';
import { NTEventWrapper } from '@/napcat-core/helper/event';
import { createSessionProxy } from '@/napcat-core/helper/session-proxy';
import { KickedOffLineInfo, RawMessage, SelfInfo, SelfStatusInfo } from '@/napcat-core/types';
import { KickedOffLineInfo, SelfInfo, SelfStatusInfo } from '@/napcat-core/types';
import { NapCatConfigLoader, NapcatConfigSchema } from '@/napcat-core/helper/config';
import os from 'node:os';
import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/napcat-core/listeners';
@@ -40,14 +37,6 @@ export * from './wrapper';
export * from './types/index';
export * from './services/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 {
Unknown = 0,
@@ -55,23 +44,20 @@ export enum NapCatCoreWorkingEnv {
Framework = 2,
}
export function loadQQWrapper (execPath: string | undefined, QQVersion: string): WrapperNodeApi {
export function loadQQWrapper (QQVersion: string): WrapperNodeApi {
if (process.env['NAPCAT_WRAPPER_PATH']) {
const wrapperPath = process.env['NAPCAT_WRAPPER_PATH'];
const nativemodule: { exports: WrapperNodeApi; } = { exports: {} as WrapperNodeApi };
process.dlopen(nativemodule, wrapperPath);
return nativemodule.exports;
}
if (!execPath) {
throw new Error('无法加载WrapperexecPath未定义');
}
let appPath;
if (os.platform() === 'darwin') {
appPath = path.resolve(path.dirname(execPath), '../Resources/app');
appPath = path.resolve(path.dirname(process.execPath), '../Resources/app');
} else if (os.platform() === 'linux') {
appPath = path.resolve(path.dirname(execPath), './resources/app');
appPath = path.resolve(path.dirname(process.execPath), './resources/app');
} else {
appPath = path.resolve(path.dirname(execPath), `./versions/${QQVersion}/`);
appPath = path.resolve(path.dirname(process.execPath), `./versions/${QQVersion}/`);
}
let wrapperNodePath = path.resolve(appPath, 'wrapper.node');
if (!fs.existsSync(wrapperNodePath)) {
@@ -79,22 +65,21 @@ export function loadQQWrapper (execPath: string | undefined, QQVersion: string):
}
// 老版本兼容 未来去掉
if (!fs.existsSync(wrapperNodePath)) {
wrapperNodePath = path.join(path.dirname(execPath), `./resources/app/versions/${QQVersion}/wrapper.node`);
wrapperNodePath = path.join(path.dirname(process.execPath), `./resources/app/versions/${QQVersion}/wrapper.node`);
}
const nativemodule: { exports: WrapperNodeApi; } = { exports: {} as WrapperNodeApi };
process.dlopen(nativemodule, wrapperNodePath);
process.env['NAPCAT_WRAPPER_PATH'] = wrapperNodePath;
return nativemodule.exports;
}
export function getMajorPath (execPath: string, QQVersion: string): string {
export function getMajorPath (QQVersion: string): string {
// major.node
let appPath;
if (os.platform() === 'darwin') {
appPath = path.resolve(path.dirname(execPath), '../Resources/app');
appPath = path.resolve(path.dirname(process.execPath), '../Resources/app');
} else if (os.platform() === 'linux') {
appPath = path.resolve(path.dirname(execPath), './resources/app');
appPath = path.resolve(path.dirname(process.execPath), './resources/app');
} else {
appPath = path.resolve(path.dirname(execPath), `./versions/${QQVersion}/`);
appPath = path.resolve(path.dirname(process.execPath), `./versions/${QQVersion}/`);
}
let majorPath = path.resolve(appPath, 'major.node');
if (!fs.existsSync(majorPath)) {
@@ -102,7 +87,7 @@ export function getMajorPath (execPath: string, QQVersion: string): string {
}
// 老版本兼容 未来去掉
if (!fs.existsSync(majorPath)) {
majorPath = path.join(path.dirname(execPath), `./resources/app/versions/${QQVersion}/major.node`);
majorPath = path.join(path.dirname(process.execPath), `./resources/app/versions/${QQVersion}/major.node`);
}
return majorPath;
}
@@ -121,19 +106,9 @@ export class NapCatCore {
// 通过构造器递过去的 runtime info 应该尽量少
constructor (context: InstanceContext, selfInfo: SelfInfo) {
this.selfInfo = selfInfo;
// 先用原始 session 创建 eventWrapper
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.context = context;
this.util = this.context.wrapper.NodeQQNTWrapperUtil;
this.eventWrapper = new NTEventWrapper(context.session);
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath, NapcatConfigSchema);
this.apis = {
FileApi: new NTQQFileApi(this.context, this),
@@ -145,8 +120,6 @@ export class NapCatCore {
MsgApi: new NTQQMsgApi(this.context, this),
UserApi: new NTQQUserApi(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(TypedEventEmitter).toConstantValue(this.event);
@@ -202,11 +175,6 @@ export class NapCatCore {
async initNapCatCoreListeners () {
const msgListener = new NodeIKernelMsgListener();
// 在线文件/文件夹消息
msgListener.onRecvOnlineFileMsg = (msgs: RawMessage[]) => {
msgs.forEach(msg => this.context.logger.logMessage(msg, this.selfInfo));
};
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
// 下线通知
const tips = `[KickedOffLine] [${Info.tipsTitle}] ${Info.tipsDesc}`;
@@ -326,6 +294,4 @@ export interface StableNTApiWrapper {
MsgApi: NTQQMsgApi,
UserApi: NTQQUserApi,
GroupApi: NTQQGroupApi;
FlashApi: NTQQFlashApi,
OnlineApi: NTQQOnlineApi,
}

View File

@@ -18,7 +18,6 @@ import { OidbPacket } from '@/napcat-core/packet/transformer/base';
import { ImageOcrResult } from '@/napcat-core/packet/entities/ocrResult';
import { gunzipSync } from 'zlib';
import { PacketMsgConverter } from '@/napcat-core/packet/message/converter';
import { UploadForwardMsgParams } from '@/napcat-core/packet/transformer/message/UploadForwardMsgV2';
export class PacketOperationContext {
private readonly context: PacketContext;
@@ -27,7 +26,7 @@ export class PacketOperationContext {
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);
}
@@ -95,15 +94,12 @@ export class PacketOperationContext {
.filter(Boolean)
);
const res = await Promise.allSettled(reqList);
const failedCount = res.filter((r) => r.status === 'rejected').length;
if (failedCount > 0) {
this.context.logger.warn(`上传资源${res.length}个,失败${failedCount}`);
res.forEach((result, index) => {
if (result.status === 'rejected') {
this.context.logger.error(`上传第${index + 1}个资源失败:${result.reason.stack}`);
}
});
}
this.context.logger.info(`上传资源${res.length}个,失败${res.filter((r) => r.status === 'rejected').length}`);
res.forEach((result, index) => {
if (result.status === 'rejected') {
this.context.logger.error(`上传第${index + 1}个资源失败:${result.reason.stack}`);
}
});
}
async UploadImage (img: PacketMsgPicElement) {
@@ -228,15 +224,7 @@ export class PacketOperationContext {
const res = trans.UploadForwardMsg.parse(resp);
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 (
groupUin: number,
fileUUID: string,

View File

@@ -1,51 +0,0 @@
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();

View File

@@ -2,4 +2,3 @@ export { default as UploadForwardMsg } from './UploadForwardMsg';
export { default as FetchGroupMessage } from './FetchGroupMessage';
export { default as FetchC2CMessage } from './FetchC2CMessage';
export { default as DownloadForwardMsg } from './DownloadForwardMsg';
export { default as UploadForwardMsgV2 } from './UploadForwardMsgV2';

View File

@@ -1,303 +0,0 @@
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

View File

@@ -1,6 +1,5 @@
export enum GeneralCallResultStatus {
OK = 0,
ERROR = -1,
}
export interface GeneralCallResult {

View File

@@ -1,21 +0,0 @@
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,
}

View File

@@ -11,7 +11,3 @@ export * from './constant';
export * from './graytip';
export * from './emoji';
export * from './service';
export * from './adapter';
export * from './contact';
export * from './file';
export * from './flashfile';

View File

@@ -1,4 +1,4 @@
import { NTGroupMemberRole } from './group';
import { NTGroupMemberRole } from '@/napcat-core/index';
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,14 +66,13 @@ export enum ElementType {
YOLOGAMERESULT = 20,
AVRECORD = 21,
FEED = 22,
TOFURECORD = 23, // tofu record?? 在线文件的id是这个
TOFURECORD = 23,
ACEBUBBLE = 24,
ACTIVITY = 25,
TOFU = 26,
FACEBUBBLE = 27,
SHARELOCATION = 28,
TASKTOPMSG = 29,
ONLINEFOLDER = 30, // 在线文件夹
RECOMMENDEDMSG = 43,
ACTIONBAR = 44,
}
@@ -304,40 +303,11 @@ export enum NTVideoType {
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元素接口
*/
export interface MarkdownElement {
content: string;
style?: {};
processMsg?: string;
mdSummary?: string;
mdExtType?: number;
mdExtInfo?: {
flashTransferInfo: FlashTransferInfo;
}
}
/**

View File

@@ -57,24 +57,24 @@ export interface BaseInfo {
}
// 音乐信息
export interface MusicInfo {
interface MusicInfo {
buf: string;
}
// 视频业务信息
export interface VideoBizInfo {
interface VideoBizInfo {
cid: string;
tvUrl: string;
synchType: string;
}
// 视频信息
export interface VideoInfo {
interface VideoInfo {
name: string;
}
// 扩展在线业务信息
export interface ExtOnlineBusinessInfo {
interface ExtOnlineBusinessInfo {
buf: string;
customStatus: unknown;
videoBizInfo: VideoBizInfo;
@@ -82,12 +82,12 @@ export interface ExtOnlineBusinessInfo {
}
// 扩展缓冲区
export interface ExtBuffer {
interface ExtBuffer {
buf: string;
}
// 用户状态
export interface UserStatus {
interface UserStatus {
uid: string;
uin: string;
status: number;
@@ -109,14 +109,14 @@ export interface UserStatus {
}
// 特权图标
export interface PrivilegeIcon {
interface PrivilegeIcon {
jumpUrl: string;
openIconList: unknown[];
closeIconList: unknown[];
}
// 增值服务信息
export interface VasInfo {
interface VasInfo {
vipFlag: boolean;
yearVipFlag: boolean;
svipFlag: boolean;
@@ -149,7 +149,7 @@ export interface VasInfo {
}
// 关系标志
export interface RelationFlags {
interface RelationFlags {
topTime: string;
isBlock: boolean;
isMsgDisturb: boolean;
@@ -167,7 +167,7 @@ export interface RelationFlags {
}
// 通用扩展信息
export interface CommonExt {
interface CommonExt {
constellation: number;
shengXiao: number;
kBloodType: number;
@@ -193,14 +193,14 @@ export enum BuddyListReqType {
}
// 图片信息
export interface Pic {
interface Pic {
picId: string;
picTime: number;
picUrlMap: Record<string, string>;
}
// 照片墙
export interface PhotoWall {
interface PhotoWall {
picList: Pic[];
}
@@ -247,7 +247,7 @@ export interface ModifyProfileParams {
nick: string;
longNick: string;
sex: NTSex;
birthday: { birthday_year: string, birthday_month: string, birthday_day: string; };
birthday: { birthday_year: string, birthday_month: string, birthday_day: string };
location: unknown;
}

View File

@@ -1,6 +1,5 @@
import { NodeIDependsAdapter, NodeIDispatcherAdapter, NodeIGlobalAdapter } from './adapters';
import {
GeneralCallResult,
NodeIKernelAvatarService,
NodeIKernelBuddyService,
NodeIKernelGroupService,
@@ -28,78 +27,77 @@ import { NodeIKernelMSFService } from './services/NodeIKernelMSFService';
import { NodeIkernelTestPerformanceService } from './services/NodeIkernelTestPerformanceService';
import { NodeIKernelECDHService } from './services/NodeIKernelECDHService';
import { NodeIO3MiscService } from './services/NodeIO3MiscService';
import { NodeIKernelFlashTransferService } from './services/NodeIKernelFlashTransferService';
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,
arg1: unknown,
arg2: unknown,
@@ -107,7 +105,7 @@ export interface NodeQQNTWrapperUtil {
arg4: number,
): unknown;
reportCountIndicators (
reportCountIndicators(
arg0: string,
arg1: Map<unknown, unknown>,
arg2: string,
@@ -115,7 +113,7 @@ export interface NodeQQNTWrapperUtil {
arg4: boolean,
): unknown;
reportValueIndicators (
reportValueIndicators(
arg0: string,
arg1: Map<unknown, unknown>,
arg2: string,
@@ -123,154 +121,140 @@ export interface NodeQQNTWrapperUtil {
arg4: number,
): 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;
createThumbnailImage (
serviceName: string,
filePath: string,
targetPath: string,
imgSize: {
width: number,
height: number;
},
fileFormat: string,
arg: number | null | undefined, // null undefined都行
): Promise<GeneralCallResult & { targetPath?: string; }>;
isNull(): unknown;
}
export interface NodeIQQNTStartupSessionWrapper {
create (): NodeIQQNTStartupSessionWrapper;
stop (): void;
start (): void;
createWithModuleList (uk: unknown): unknown;
getSessionIdList (): unknown;
create(): NodeIQQNTStartupSessionWrapper;
stop(): void;
start(): void;
createWithModuleList(uk: unknown): unknown;
getSessionIdList(): unknown;
}
export interface NodeIQQNTWrapperSession {
getNTWrapperSession (str: string): NodeIQQNTWrapperSession;
getNTWrapperSession(str: string): NodeIQQNTWrapperSession;
get (): NodeIQQNTWrapperSession;
get(): NodeIQQNTWrapperSession;
new(): NodeIQQNTWrapperSession;
create (): NodeIQQNTWrapperSession;
create(): NodeIQQNTWrapperSession;
init (
init(
wrapperSessionInitConfig: WrapperSessionInitConfig,
nodeIDependsAdapter: NodeIDependsAdapter,
nodeIDispatcherAdapter: NodeIDispatcherAdapter,
nodeIKernelSessionListener: NodeIKernelSessionListener,
): 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;
getFlashTransferService (): NodeIKernelFlashTransferService;
getDirectSessionService(): unknown;
getDirectSessionService (): unknown;
getRDeliveryService(): unknown;
getRDeliveryService (): unknown;
getAvatarService(): NodeIKernelAvatarService;
getAvatarService (): NodeIKernelAvatarService;
getFeedChannelService(): unknown;
getFeedChannelService (): unknown;
getYellowFaceService(): unknown;
getYellowFaceService (): unknown;
getCollectionService(): NodeIKernelCollectionService;
getCollectionService (): NodeIKernelCollectionService;
getSettingService(): unknown;
getSettingService (): unknown;
getQiDianService(): unknown;
getQiDianService (): unknown;
getFileAssistantService(): unknown;
getFileAssistantService (): unknown;
getGuildService(): unknown;
getGuildService (): unknown;
getSkinService(): unknown;
getSkinService (): unknown;
getTestPerformanceService(): NodeIkernelTestPerformanceService;
getTestPerformanceService (): NodeIkernelTestPerformanceService;
getQQPlayService(): unknown;
getQQPlayService (): unknown;
getDbToolsService(): unknown;
getDbToolsService (): unknown;
getUixConvertService(): NodeIKernelUixConvertService;
getUixConvertService (): NodeIKernelUixConvertService;
getOnlineStatusService(): unknown;
getOnlineStatusService (): unknown;
getRemotingService(): unknown;
getRemotingService (): unknown;
getGroupTabService(): unknown;
getGroupTabService (): unknown;
getGroupSchoolService(): unknown;
getGroupSchoolService (): unknown;
getLiteBusinessService(): unknown;
getLiteBusinessService (): unknown;
getGuildMsgService(): unknown;
getGuildMsgService (): unknown;
getLockService(): unknown;
getLockService (): unknown;
getMSFService(): NodeIKernelMSFService;
getMSFService (): NodeIKernelMSFService;
getGuildHotUpdateService(): unknown;
getGuildHotUpdateService (): unknown;
getAVSDKService(): unknown;
getAVSDKService (): unknown;
getRecentContactService(): NodeIKernelRecentContactService;
getRecentContactService (): NodeIKernelRecentContactService;
getConfigMgrService (): unknown;
getConfigMgrService(): unknown;
}
export interface EnginInitDesktopConfig {
@@ -284,20 +268,20 @@ export interface EnginInitDesktopConfig {
global_path_config: {
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 {
get (): NodeIQQNTWrapperEngine;
get(): NodeIQQNTWrapperEngine;
initWithDeskTopConfig (config: EnginInitDesktopConfig, nodeIGlobalAdapter: NodeIGlobalAdapter): void;
initWithDeskTopConfig(config: EnginInitDesktopConfig, nodeIGlobalAdapter: NodeIGlobalAdapter): void;
}
export interface WrapperNodeApi {
NodeIO3MiscService: NodeIO3MiscService;
NodeQQNTWrapperUtil: NodeQQNTWrapperUtil;
NodeIQQNTWrapperSession: NodeIQQNTWrapperSession;
NodeIQQNTStartupSessionWrapper: NodeIQQNTStartupSessionWrapper;
NodeIQQNTStartupSessionWrapper: NodeIQQNTStartupSessionWrapper
NodeIQQNTWrapperEngine: NodeIQQNTWrapperEngine;
NodeIKernelLoginService: NodeIKernelLoginService;

View File

@@ -1,4 +0,0 @@
NAPCAT_DISABLE_PIPE=1
NAPCAT_DISABLE_MULTI_PROCESS=1
NAPCAT_WEBUI_JWT_SECRET_KEY=napcat_dev_secret_key
NAPCAT_WEBUI_SECRET_KEY=napcatqq

View File

@@ -1,39 +0,0 @@
{
"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": ""
}

View File

@@ -78,7 +78,7 @@ async function copyAll () {
process.env.NAPCAT_WORKDIR = TARGET_DIR;
// 开发环境使用固定密钥
process.env.NAPCAT_WEBUI_JWT_SECRET_KEY = 'napcat_dev_secret_key';
process.env.NAPCAT_WEBUI_SECRET_KEY = 'napcatqq';
process.env.NAPCAT_WEBUI_SECRET_KEY = 'napcat';
console.log('Loading NapCat module...');
await import(pathToFileURL(NAPCAT_MJS_PATH).href);
}

View File

@@ -1,28 +1,27 @@
{
"name": "napcat-develop",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "index.js",
"scripts": {
"dev": "powershell ./nodeTest.ps1",
"copy-env": "xcopy config ..\\napcat-shell\\dist\\config /E /I /Y"
},
"exports": {
".": {
"require": "./index.js"
"name": "napcat-develop",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "index.js",
"scripts": {
"dev": "powershell ./nodeTest.ps1"
},
"./*": {
"require": "./*"
"exports": {
".": {
"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"
}
}

View File

@@ -1,6 +1,6 @@
import { NapCatPathWrapper } from 'napcat-common/src/path';
import { InitWebUi, WebUiConfig, webUiRuntimePort } from 'napcat-webui-backend/index';
import { NapCatAdapterManager } from 'napcat-adapter';
import { NapCatOneBot11Adapter } from 'napcat-onebot/index';
import { NativePacketHandler } from 'napcat-core/packet/handler/client';
import { FFmpegService } from 'napcat-core/helper/ffmpeg/ffmpeg';
import { logSubscription, LogWrapper } from 'napcat-core/helper/log';
@@ -38,7 +38,7 @@ export async function NCoreInitFramework (
const logger = new LogWrapper(pathWrapper.logsPath);
await applyPendingUpdates(pathWrapper, logger);
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
const wrapper = loadQQWrapper(basicInfoWrapper.QQMainPath, basicInfoWrapper.getFullQQVersion());
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVersion());
const nativePacketHandler = new NativePacketHandler({ logger }); // 初始化 NativePacketHandler 用于后续使用
// nativePacketHandler.onAll((packet) => {
// console.log('[Packet]', packet.uin, packet.cmd, packet.hex_data);
@@ -79,14 +79,11 @@ export async function NCoreInitFramework (
// 启动WebUi
WebUiDataRuntime.setWorkingEnv(NapCatCoreWorkingEnv.Framework);
InitWebUi(logger, pathWrapper, logSubscription, statusHelperSubscription).then().catch(e => logger.logError(e));
// 使用 NapCatAdapterManager 统一管理协议适配器
const adapterManager = new NapCatAdapterManager(loaderObject.core, loaderObject.context, pathWrapper);
await adapterManager.initAdapters();
// 注册 OneBot 适配器到 WebUiDataRuntime供调试功能使用
const oneBotAdapter = adapterManager.getOneBotAdapter();
if (oneBotAdapter) {
WebUiDataRuntime.setOneBotContext(oneBotAdapter);
}
// 初始化LLNC的Onebot实现
const oneBotAdapter = new NapCatOneBot11Adapter(loaderObject.core, loaderObject.context, pathWrapper);
// 注册到 WebUiDataRuntime供调试功能使用
WebUiDataRuntime.setOneBotContext(oneBotAdapter);
await oneBotAdapter.InitOneBot();
}
export class NapCatFramework {

View File

@@ -1,33 +1,33 @@
{
"name": "napcat-framework",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "index.ts",
"scripts": {
"build": "vite build",
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
},
"exports": {
".": {
"import": "./index.ts"
"name": "napcat-framework",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "index.ts",
"scripts": {
"build": "vite build",
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
},
"./*": {
"import": "./*"
"exports": {
".": {
"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"
}
}

View File

@@ -49,9 +49,7 @@ const FrameworkBaseConfig = () =>
'@/napcat-onebot': resolve(__dirname, '../napcat-onebot'),
'@/napcat-pty': resolve(__dirname, '../napcat-pty'),
'@/napcat-webui-backend': resolve(__dirname, '../napcat-webui-backend'),
'@/napcat-image-size': resolve(__dirname, '../napcat-image-size'),
'@/napcat-protocol': resolve(__dirname, '../napcat-protocol'),
'@/napcat-adapter': resolve(__dirname, '../napcat-adapter'),
'@/image-size': resolve(__dirname, '../image-size'),
},
},
build: {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 962 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -1,12 +1,5 @@
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 { ReadStream } from 'fs';
import { Readable } from 'stream';
export interface ImageSize {
width: number;
@@ -19,18 +12,17 @@ export enum ImageType {
BMP = 'bmp',
GIF = 'gif',
WEBP = 'webp',
TIFF = 'tiff',
UNKNOWN = 'unknown',
}
export interface ImageParser {
interface ImageParser {
readonly type: ImageType;
canParse (buffer: Buffer): boolean;
parseSize (stream: ReadStream): Promise<ImageSize | undefined>;
canParse(buffer: Buffer): boolean;
parseSize(stream: ReadStream): Promise<ImageSize | undefined>;
}
// 魔术匹配
export function matchMagic (buffer: Buffer, magic: number[], offset = 0): boolean {
function matchMagic (buffer: Buffer, magic: number[], offset = 0): boolean {
if (buffer.length < offset + magic.length) {
return false;
}
@@ -43,39 +35,316 @@ export function matchMagic (buffer: Buffer, magic: number[], offset = 0): boolea
return true;
}
// 所有解析器实例
const parserInstances = {
png: new PngParser(),
jpeg: new JpegParser(),
bmp: new BmpParser(),
gif: new GifParser(),
webp: new WebpParser(),
tiff: new TiffParser(),
};
// PNG解析器
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];
// 首字节到可能的图片类型映射,用于快速筛选
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
]);
canParse (buffer: Buffer): boolean {
return matchMagic(buffer, this.PNG_SIGNATURE);
}
// 类型到解析器的映射
const typeToParser = new Map<ImageType, ImageParser>([
[ImageType.PNG, parserInstances.png],
[ImageType.JPEG, parserInstances.jpeg],
[ImageType.BMP, parserInstances.bmp],
[ImageType.GIF, parserInstances.gif],
[ImageType.WEBP, parserInstances.webp],
[ImageType.TIFF, parserInstances.tiff],
]);
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);
}
});
});
}
}
// 所有解析器列表(用于回退)
const parsers: ReadonlyArray<ImageParser> = Object.values(parserInstances);
// JPEG解析器
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);
}
});
});
}
}
// 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> {
return new Promise((resolve, reject) => {
@@ -85,22 +354,18 @@ export async function detectImageType (filePath: string): Promise<ImageType> {
end: 63,
});
const chunks: Buffer[] = [];
let buffer: Buffer | null = null;
stream.on('error', (err) => {
stream.once('error', (err) => {
stream.destroy();
reject(err);
});
stream.on('data', (chunk: Buffer | string) => {
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
chunks.push(chunkBuffer);
});
stream.once('readable', () => {
buffer = stream.read(64) as Buffer;
stream.destroy();
stream.on('end', () => {
const buffer = Buffer.concat(chunks);
if (buffer.length === 0) {
if (!buffer) {
return resolve(ImageType.UNKNOWN);
}
@@ -112,6 +377,12 @@ export async function detectImageType (filePath: string): Promise<ImageType> {
resolve(ImageType.UNKNOWN);
});
stream.once('end', () => {
if (!buffer) {
resolve(ImageType.UNKNOWN);
}
});
});
}
@@ -119,7 +390,7 @@ export async function imageSizeFromFile (filePath: string): Promise<ImageSize |
try {
// 先检测类型
const type = await detectImageType(filePath);
const parser = typeToParser.get(type);
const parser = parsers.find(p => p.type === type);
if (!parser) {
return undefined;
}
@@ -151,71 +422,3 @@ export async function imageSizeFallBack (
): Promise<ImageSize> {
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;
}

View File

@@ -1,32 +0,0 @@
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);
}
});
});
}
}

View File

@@ -1,37 +0,0 @@
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);
}
});
});
}
}

View File

@@ -1,123 +0,0 @@
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);
}
});
});
}
}

View File

@@ -1,32 +0,0 @@
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);
}
});
});
}
}

View File

@@ -1,124 +0,0 @@
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;
}
}

View File

@@ -1,90 +0,0 @@
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);
}
});
});
}
}

View File

@@ -5,18 +5,9 @@ import { NapCatOneBot11Adapter, OB11Return } from '@/napcat-onebot/index';
import { NetworkAdapterConfig } from '../config/config';
import { TSchema } from '@sinclair/typebox';
import { StreamPacket, StreamPacketBasic, StreamStatus } from './stream/StreamBasic';
export const ActionExamples = {
Common: {
errors: [
{ code: 1400, description: '请求参数错误或业务逻辑执行失败' },
{ code: 1401, description: '权限不足' },
{ code: 1404, description: '资源不存在' }
]
}
};
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 {
status,
retcode,
@@ -28,11 +19,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);
}
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);
}
@@ -41,22 +32,15 @@ export class OB11Response {
}
}
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> {
actionName: typeof ActionName[keyof typeof ActionName] = ActionName.Unknown;
core: NapCatCore;
private validate?: ValidateFunction<unknown> = undefined;
payloadSchema?: TSchema = undefined;
returnSchema?: TSchema = undefined;
payloadExample?: unknown = undefined;
returnExample?: unknown = undefined;
actionSummary: string = '';
actionDescription: string = '';
actionTags: string[] = [];
obContext: NapCatOneBot11Adapter;
useStream: boolean = false;
errorExamples: Array<{ code: number, description: string; }> = ActionExamples.Common.errors;
constructor (obContext: NapCatOneBot11Adapter, core: NapCatCore) {
this.obContext = obContext;

View File

@@ -1,45 +0,0 @@
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,
},
};

View File

@@ -1,22 +0,0 @@
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://...' },
},
};

View File

@@ -1,102 +0,0 @@
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' },
},
};

View File

@@ -1,79 +0,0 @@
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,
},
};

View File

@@ -1,10 +0,0 @@
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' },
},
};

View File

@@ -1,10 +0,0 @@
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: {},
},
};

View File

@@ -1,14 +0,0 @@
export const PacketActionsExamples = {
GetPacketStatus: {
payload: {},
response: { status: 'ok' },
},
SendPoke: {
payload: { user_id: '123456789' },
response: {},
},
SetGroupTodo: {
payload: { group_id: '123456', message_id: '123456789' },
response: {},
},
};

View File

@@ -1,42 +0,0 @@
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: {},
},
};

View File

@@ -1,38 +0,0 @@
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: {},
},
};

View File

@@ -1,15 +1,8 @@
import { ActionName } from '@/napcat-onebot/action/router';
import { OneBotAction } from '../OneBotAction';
import { Type } from '@sinclair/typebox';
export class BotExit extends OneBotAction<void, void> {
override actionName = ActionName.Exit;
override payloadSchema = Type.Object({});
override returnSchema = Type.Object({});
override actionSummary = '退出登录';
override actionTags = ['系统扩展'];
override payloadExample = {};
override returnExample = null;
async _handle () {
process.exit(0);

View File

@@ -2,36 +2,21 @@ import { ActionName } from '@/napcat-onebot/action/router';
import { OneBotAction } from '../OneBotAction';
import { Static, Type } from '@sinclair/typebox';
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
bot_appid: Type.String({ description: '机器人AppID' }),
button_id: Type.String({ default: '', description: '按钮ID' }),
callback_data: Type.String({ default: '', description: '回调数据' }),
msg_seq: Type.String({ default: '10086', description: '消息序列号' }),
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
bot_appid: Type.String(),
button_id: Type.String({ default: '' }),
callback_data: Type.String({ default: '' }),
msg_seq: Type.String({ default: '10086' }),
});
type PayloadType = Static<typeof PayloadSchema>;
type Payload = Static<typeof SchemaData>;
const ReturnSchema = Type.Any({ description: '点击结果' });
type ReturnType = Static<typeof ReturnSchema>;
export class ClickInlineKeyboardButton extends OneBotAction<PayloadType, ReturnType> {
export class ClickInlineKeyboardButton extends OneBotAction<Payload, unknown> {
override actionName = ActionName.ClickInlineKeyboardButton;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
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) {
override payloadSchema = SchemaData;
async _handle (payload: Payload) {
return await this.core.apis.MsgApi.clickInlineKeyboardButton({
buttonId: payload.button_id,
peerId: payload.group_id.toString(),

View File

@@ -2,33 +2,18 @@ import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Type, Static } from '@sinclair/typebox';
const PayloadSchema = Type.Object({
rawData: Type.String({ description: '原始数据' }),
brief: Type.String({ description: '简要描述' }),
const SchemaData = Type.Object({
rawData: Type.String(),
brief: Type.String(),
});
type PayloadType = Static<typeof PayloadSchema>;
type Payload = Static<typeof SchemaData>;
const ReturnSchema = Type.Any({ description: '创建结果' });
type ReturnType = Static<typeof ReturnSchema>;
export class CreateCollection extends OneBotAction<PayloadType, ReturnType> {
export class CreateCollection extends OneBotAction<Payload, unknown> {
override actionName = ActionName.CreateCollection;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '创建收藏';
override actionTags = ['扩展接口'];
override payloadExample = {
rawData: '收藏内容',
brief: '收藏标题'
};
override returnExample = {
result: 0,
errMsg: ''
};
override payloadSchema = SchemaData;
async _handle (payload: PayloadType) {
async _handle (payload: Payload) {
return await this.core.apis.CollectionApi.createCollection(
this.core.selfInfo.uin,
this.core.selfInfo.uid,

View File

@@ -2,34 +2,19 @@ import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
album_id: Type.String({ description: '相册ID' }),
lloc: Type.String({ description: '媒体ID (lloc)' }),
const SchemaData = Type.Object({
group_id: Type.String(),
album_id: Type.String(),
lloc: Type.String(),
});
type PayloadType = Static<typeof PayloadSchema>;
type Payload = Static<typeof SchemaData>;
const ReturnSchema = Type.Any({ description: '删除结果' });
type ReturnType = Static<typeof ReturnSchema>;
export class DelGroupAlbumMedia extends OneBotAction<PayloadType, ReturnType> {
export class DelGroupAlbumMedia extends OneBotAction<Payload, unknown> {
override actionName = ActionName.DelGroupAlbumMedia;
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;
override payloadSchema = SchemaData;
async _handle (payload: PayloadType) {
async _handle (payload: Payload) {
return await this.core.apis.WebApi.deleteAlbumMediaByNTQQ(
payload.group_id,
payload.album_id,

View File

@@ -2,32 +2,20 @@ import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
export const DoGroupAlbumCommentPayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
album_id: Type.String({ description: '相册 ID' }),
lloc: Type.String({ description: '图片 ID' }),
content: Type.String({ description: '评论内容' }),
const SchemaData = Type.Object({
group_id: Type.String(),
album_id: Type.String(),
lloc: Type.String(),
content: Type.String(),
});
export type DoGroupAlbumCommentPayload = Static<typeof DoGroupAlbumCommentPayloadSchema>;
type Payload = Static<typeof SchemaData>;
export class DoGroupAlbumComment extends OneBotAction<DoGroupAlbumCommentPayload, any> {
export class DoGroupAlbumComment extends OneBotAction<Payload, unknown> {
override actionName = ActionName.DoGroupAlbumComment;
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: '评论结果' });
override payloadSchema = SchemaData;
async _handle (payload: DoGroupAlbumCommentPayload) {
async _handle (payload: Payload) {
return await this.core.apis.WebApi.doAlbumMediaPlainCommentByNTQQ(
payload.group_id,
payload.album_id,

View File

@@ -2,32 +2,18 @@ import { Type, Static } from '@sinclair/typebox';
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
const PayloadSchema = Type.Object({
count: Type.Union([Type.Number(), Type.String()], { default: 48, description: '获取数量' }),
const SchemaData = Type.Object({
count: Type.Union([Type.Number(), Type.String()], { default: 48 }),
});
type PayloadType = Static<typeof PayloadSchema>;
type Payload = Static<typeof SchemaData>;
const ReturnSchema = Type.Array(Type.String(), { description: '表情URL列表' });
type ReturnType = Static<typeof ReturnSchema>;
export class FetchCustomFace extends OneBotAction<PayloadType, ReturnType> {
export class FetchCustomFace extends OneBotAction<Payload, string[]> {
override actionName = ActionName.FetchCustomFace;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '获取自定义表情';
override actionTags = ['系统扩展'];
override payloadExample = {
count: 10
};
override returnExample = [
'http://example.com/face1.png'
];
override payloadSchema = SchemaData;
async _handle (payload: PayloadType) {
async _handle (payload: Payload) {
const ret = await this.core.apis.MsgApi.fetchFavEmojiList(+payload.count);
return ret.emojiInfoList.map(e => e.url);
}
}

View File

@@ -2,68 +2,28 @@ 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 { type NTQQMsgApi } from 'napcat-core/apis';
const PayloadSchema = Type.Object({
message_id: Type.Union([Type.Number(), Type.String()], { description: '消息ID' }),
emojiId: Type.Union([Type.Number(), Type.String()], { description: '表情ID' }),
emojiType: Type.Union([Type.Number(), Type.String()], { description: '表情类型' }),
count: Type.Union([Type.Number(), Type.String()], { default: 20, description: '获取数量' }),
cookie: Type.String({ default: '', description: '分页Cookie' })
const SchemaData = Type.Object({
message_id: Type.Union([Type.Number(), Type.String()]),
emojiId: Type.Union([Type.Number(), Type.String()]),
emojiType: Type.Union([Type.Number(), Type.String()]),
count: Type.Union([Type.Number(), Type.String()], { default: 20 }),
});
type PayloadType = Static<typeof PayloadSchema>;
type Payload = Static<typeof SchemaData>;
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> {
export class FetchEmojiLike extends OneBotAction<Payload, Awaited<ReturnType<NTQQMsgApi['getMsgEmojiLikesList']>>> {
override actionName = ActionName.FetchEmojiLike;
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;
override payloadSchema = SchemaData;
async _handle (payload: PayloadType): Promise<ReturnType> {
async _handle (payload: Payload) {
const msgIdPeer = MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id);
if (!msgIdPeer) throw new Error('消息不存在');
const msg = (await this.core.apis.MsgApi.getMsgsByMsgId(msgIdPeer.Peer, [msgIdPeer.MsgId])).msgList[0];
if (!msg) throw new Error('消息不存在');
const res = await this.core.apis.MsgApi.getMsgEmojiLikesList(
msgIdPeer.Peer, msg.msgSeq, payload.emojiId.toString(), payload.emojiType.toString(), payload.cookie, +payload.count
return await this.core.apis.MsgApi.getMsgEmojiLikesList(
msgIdPeer.Peer, msg.msgSeq, payload.emojiId.toString(), payload.emojiType.toString(), +payload.count
);
return res;
}
}

View File

@@ -1,46 +1,30 @@
import { ActionName } from '@/napcat-onebot/action/router';
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
import { AIVoiceChatType } from 'napcat-core/packet/entities/aiChat';
import { Type, Static } from '@sinclair/typebox';
import { ExtendsActionsExamples } from '../example/ExtendsActionsExamples';
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
chat_type: Type.Union([Type.Number(), Type.String()], { default: 1, description: '聊天类型' }),
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
chat_type: Type.Union([Type.Union([Type.Number(), Type.String()])], { default: 1 }),
});
type PayloadType = Static<typeof PayloadSchema>;
type Payload = Static<typeof SchemaData>;
const ReturnSchema = Type.Array(
Type.Object({
type: Type.String({ description: '角色类型' }),
characters: Type.Array(
Type.Object({
character_id: Type.String({ description: '角色ID' }),
character_name: Type.String({ description: '角色名称' }),
preview_url: Type.String({ description: '预览URL' }),
}),
{ description: '角色列表' }
),
}),
{ description: 'AI角色列表' }
);
interface GetAiCharactersResponse {
type: string;
characters: {
character_id: string;
character_name: string;
preview_url: string;
}[];
}
type ReturnType = Static<typeof ReturnSchema>;
export class GetAiCharacters extends GetPacketStatusDepends<PayloadType, ReturnType> {
export class GetAiCharacters extends GetPacketStatusDepends<Payload, GetAiCharactersResponse[]> {
override actionName = ActionName.GetAiCharacters;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '获取AI角色列表';
override actionDescription = '获取群聊中的AI角色列表';
override actionTags = ['扩展接口'];
override payloadExample = ExtendsActionsExamples.GetAiCharacters.payload;
override returnExample = ExtendsActionsExamples.GetAiCharacters.response;
override payloadSchema = SchemaData;
async _handle (payload: PayloadType) {
const chatTypeNum = Number(payload.chat_type);
const rawList = await this.core.apis.PacketApi.pkt.operation.FetchAiVoiceList(+payload.group_id, chatTypeNum);
async _handle (payload: Payload) {
const rawList = await this.core.apis.PacketApi.pkt.operation.FetchAiVoiceList(+payload.group_id, +payload.chat_type as AIVoiceChatType);
return rawList?.map((item) => ({
type: item.category,
characters: item.voices.map((voice) => ({

View File

@@ -1,24 +1,12 @@
import { ActionName } from '@/napcat-onebot/action/router';
import { OneBotAction } from '../OneBotAction';
import { Type, Static } from '@sinclair/typebox';
import { ExtendsActionsExamples } from '../example/ExtendsActionsExamples';
interface GetClientkeyResponse {
clientkey?: string;
}
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> {
export class GetClientkey extends OneBotAction<void, GetClientkeyResponse> {
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 () {
return { clientkey: (await this.core.apis.UserApi.forceFetchClientKey()).clientKey };

View File

@@ -1,81 +1,20 @@
import { type NTQQCollectionApi } from 'napcat-core/apis/collection';
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Type, Static } from '@sinclair/typebox';
const PayloadSchema = Type.Object({
category: Type.String({ description: '分类ID' }),
count: Type.String({ default: '50', description: '获取数量' }),
const SchemaData = Type.Object({
category: Type.Union([Type.Number(), Type.String()]),
count: Type.Union([Type.Union([Type.Number(), Type.String()])], { default: 1 }),
});
type PayloadType = Static<typeof PayloadSchema>;
type Payload = Static<typeof SchemaData>;
const ReturnSchema = Type.Any({ description: '收藏列表' });
type ReturnType = Static<typeof ReturnSchema>;
export class GetCollectionList extends OneBotAction<PayloadType, ReturnType> {
export class GetCollectionList extends OneBotAction<Payload, Awaited<ReturnType<NTQQCollectionApi['getAllCollection']>>> {
override actionName = ActionName.GetCollectionList;
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"
}
};
override payloadSchema = SchemaData;
async _handle (payload: PayloadType) {
async _handle (payload: Payload) {
return await this.core.apis.CollectionApi.getAllCollection(+payload.category, +payload.count);
}
}

View File

@@ -1,88 +0,0 @@
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 };
}
}

View File

@@ -1,40 +1,12 @@
import { OB11Construct } from '@/napcat-onebot/helper/data';
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Type, Static } from '@sinclair/typebox';
import { OB11UserSchema } from '../schemas';
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> {
export class GetFriendWithCategory extends OneBotAction<void, unknown> {
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 () {
const categories = await this.core.apis.FriendApi.getBuddyV2ExWithCate();
return categories.map(category => ({
return (await this.core.apis.FriendApi.getBuddyV2ExWithCate()).map(category => ({
...category,
buddyList: OB11Construct.friends(category.buddyList),
}));

View File

@@ -1,51 +1,16 @@
import { GroupNotifyMsgStatus } from 'napcat-core';
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Type, Static } from '@sinclair/typebox';
import { Notify } from '@/napcat-onebot/types';
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> {
export default class GetGroupAddRequest extends OneBotAction<null, Notify[] | null> {
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<ReturnType> {
async _handle (): Promise<Notify[] | null> {
const NTQQUserApi = this.core.apis.UserApi;
const NTQQGroupApi = this.core.apis.GroupApi;
const ignoredNotifies = await NTQQGroupApi.getSingleScreenNotifies(true, 10);
const retData: ReturnType = [];
const retData: Notify[] = [];
const notifyPromises = ignoredNotifies
.filter(notify => notify.type === 7)

View File

@@ -2,35 +2,19 @@ import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
album_id: Type.String({ description: '相册ID' }),
attach_info: Type.String({ default: '', description: '附加信息(用于分页)' }),
const SchemaData = Type.Object({
group_id: Type.String(),
album_id: Type.String(),
attach_info: Type.String({ default: '' }),
});
type PayloadType = Static<typeof PayloadSchema>;
type Payload = Static<typeof SchemaData>;
const ReturnSchema = Type.Any({ description: '相册媒体列表' });
type ReturnType = Static<typeof ReturnSchema>;
export class GetGroupAlbumMediaList extends OneBotAction<PayloadType, ReturnType> {
export class GetGroupAlbumMediaList extends OneBotAction<Payload, unknown> {
override actionName = ActionName.GetGroupAlbumMediaList;
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;
override payloadSchema = SchemaData;
async _handle (payload: PayloadType) {
async _handle (payload: Payload) {
return await this.core.apis.WebApi.getAlbumMediaListByNTQQ(
payload.group_id,
payload.album_id,

View File

@@ -1,30 +1,17 @@
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Type, Static } from '@sinclair/typebox';
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
});
type PayloadType = Static<typeof PayloadSchema>;
type Payload = Static<typeof SchemaData>;
const ReturnSchema = Type.Any({ description: '群扩展信息' });
type ReturnType = Static<typeof ReturnSchema>;
export class GetGroupInfoEx extends OneBotAction<PayloadType, ReturnType> {
export class GetGroupInfoEx extends OneBotAction<Payload, unknown> {
override actionName = ActionName.GetGroupInfoEx;
override actionSummary = '获取群详细信息 (扩展)';
override actionTags = ['群组扩展'];
override payloadExample = {
group_id: '123456'
};
override returnExample = {
};
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override payloadSchema = SchemaData;
async _handle (payload: PayloadType) {
async _handle (payload: Payload) {
return (await this.core.apis.GroupApi.getGroupExtFE0Info([payload.group_id.toString()])).result.groupExtInfos.get(payload.group_id.toString());
}
}

View File

@@ -1,79 +1,57 @@
import { ActionName } from '@/napcat-onebot/action/router';
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
import { MiniAppInfo, MiniAppInfoHelper } from 'napcat-core/packet/utils/helper/miniAppHelper';
import { MiniAppReqCustomParams, MiniAppReqParams } from 'napcat-core/packet/entities/miniApp';
import { MiniAppData, MiniAppRawData, MiniAppReqCustomParams, MiniAppReqParams } from 'napcat-core/packet/entities/miniApp';
import { Static, Type } from '@sinclair/typebox';
const PayloadSchema = Type.Union([
const SchemaData = Type.Union([
Type.Object({
type: Type.Union([Type.Literal('bili'), Type.Literal('weibo')], { description: '模板类型' }),
title: Type.String({ description: '标题' }),
desc: Type.String({ description: '描述' }),
picUrl: Type.String({ description: '图片URL' }),
jumpUrl: Type.String({ description: '跳转URL' }),
webUrl: Type.Optional(Type.String({ description: '网页URL' })),
rawArkData: Type.Optional(Type.Union([Type.String()], { description: '是否返回原始Ark数据' })),
type: Type.Union([Type.Literal('bili'), Type.Literal('weibo')]),
title: Type.String(),
desc: Type.String(),
picUrl: Type.String(),
jumpUrl: Type.String(),
webUrl: Type.Optional(Type.String()),
rawArkData: Type.Optional(Type.Union([Type.String()])),
}),
Type.Object({
title: Type.String({ description: '标题' }),
desc: Type.String({ description: '描述' }),
picUrl: Type.String({ description: '图片URL' }),
jumpUrl: Type.String({ description: '跳转URL' }),
iconUrl: Type.String({ description: '图标URL' }),
webUrl: Type.Optional(Type.String({ description: '网页URL' })),
appId: Type.String({ description: '小程序AppID' }),
scene: Type.String({ description: '场景ID' }),
templateType: Type.String({ description: '模板类型' }),
businessType: Type.String({ description: '业务类型' }),
verType: Type.String({ description: '版本类型' }),
shareType: Type.String({ description: '分享类型' }),
versionId: Type.String({ description: '版本ID' }),
sdkId: Type.String({ description: 'SDK ID' }),
withShareTicket: Type.String({ description: '是否携带分享票据' }),
rawArkData: Type.Optional(Type.String({ description: '是否返回原始Ark数据' })),
title: Type.String(),
desc: Type.String(),
picUrl: Type.String(),
jumpUrl: Type.String(),
iconUrl: Type.String(),
webUrl: Type.Optional(Type.String()),
appId: Type.String(),
scene: Type.Union([Type.Number(), Type.String()]),
templateType: Type.Union([Type.Number(), Type.String()]),
businessType: Type.Union([Type.Number(), Type.String()]),
verType: Type.Union([Type.Number(), Type.String()]),
shareType: Type.Union([Type.Number(), Type.String()]),
versionId: Type.String(),
sdkId: Type.String(),
withShareTicket: Type.Union([Type.Number(), Type.String()]),
rawArkData: Type.Optional(Type.Union([Type.String()])),
}),
], { description: '小程序Ark参数' });
]);
type Payload = Static<typeof SchemaData>;
type PayloadType = Static<typeof PayloadSchema>;
const ReturnSchema = Type.Object({
data: Type.Any({ description: 'Ark数据' }),
}, { description: '获取小程序Ark结果' });
type ReturnType = Static<typeof ReturnSchema>;
export class GetMiniAppArk extends GetPacketStatusDepends<PayloadType, ReturnType> {
export class GetMiniAppArk extends GetPacketStatusDepends<Payload, {
data: MiniAppData | MiniAppRawData
}> {
override actionName = ActionName.GetMiniAppArk;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema; override actionSummary = '获取小程序 Ark';
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) {
override payloadSchema = SchemaData;
async _handle (payload: Payload) {
let reqParam: MiniAppReqParams;
const customParams: MiniAppReqCustomParams = {
const customParams = {
title: payload.title,
desc: payload.desc,
picUrl: payload.picUrl,
jumpUrl: payload.jumpUrl,
webUrl: payload.webUrl ?? '',
};
webUrl: payload.webUrl,
} as MiniAppReqCustomParams;
if ('type' in payload) {
const template = MiniAppInfo.get(payload.type)?.template;
if (!template) {
throw new Error('未知的模板类型');
}
reqParam = MiniAppInfoHelper.generateReq(customParams, template);
reqParam = MiniAppInfoHelper.generateReq(customParams, MiniAppInfo.get(payload.type)!.template);
} else {
const { appId, scene, iconUrl, templateType, businessType, verType, shareType, versionId, withShareTicket } = payload;
reqParam = MiniAppInfoHelper.generateReq(

View File

@@ -1,65 +1,36 @@
import { NTVoteInfo } from 'napcat-core';
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Type, Static } from '@sinclair/typebox';
const PayloadSchema = Type.Object({
user_id: Type.Optional(Type.String({ description: 'QQ号' })),
start: Type.Union([Type.Number(), Type.String()], { default: 0, description: '起始位置' }),
count: Type.Union([Type.Number(), Type.String()], { default: 10, description: '获取数量' }),
const SchemaData = Type.Object({
user_id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
start: Type.Union([Type.Number(), Type.String()], { default: 0 }),
count: Type.Union([Type.Number(), Type.String()], { default: 10 }),
});
type PayloadType = Static<typeof PayloadSchema>;
type Payload = Static<typeof SchemaData>;
const ReturnSchema = Type.Object({
uid: Type.String({ description: '用户UID' }),
time: Type.String({ description: '时间' }),
favoriteInfo: Type.Object({
userInfos: Type.Array(Type.Any(), { description: '点赞用户信息' }),
total_count: Type.Number({ description: '总点赞数' }),
last_time: Type.Number({ description: '最后点赞时间' }),
today_count: Type.Number({ description: '今日点赞数' }),
}),
voteInfo: Type.Object({
total_count: Type.Number({ description: '总点赞数' }),
new_count: Type.Number({ description: '新增点赞数' }),
new_nearby_count: Type.Number({ description: '新增附近点赞数' }),
last_visit_time: Type.Number({ description: '最后访问时间' }),
userInfos: Type.Array(Type.Any(), { description: '点赞用户信息' }),
}),
}, { description: '点赞详情' });
type ReturnType = Static<typeof ReturnSchema>;
export class GetProfileLike extends OneBotAction<PayloadType, ReturnType> {
export class GetProfileLike extends OneBotAction<Payload, {
uid: string;
time: string;
favoriteInfo: {
userInfos: Array<NTVoteInfo>;
total_count: number;
last_time: number;
today_count: number;
};
voteInfo: {
total_count: number;
new_count: number;
new_nearby_count: number;
last_visit_time: number;
userInfos: Array<NTVoteInfo>;
};
}> {
override actionName = ActionName.GetProfileLike;
override payloadSchema = PayloadSchema;
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> {
override payloadSchema = SchemaData;
async _handle (payload: Payload) {
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 type = isSelf ? 2 : 1;

View File

@@ -1,37 +1,18 @@
import { NTQQWebApi } from 'napcat-core/apis';
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
import { NTQQWebApi } from 'napcat-core/apis';
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
const SchemaData = Type.Object({
group_id: Type.String(),
});
type PayloadType = Static<typeof PayloadSchema>;
type Payload = Static<typeof SchemaData>;
const ReturnSchema = Type.Array(Type.Any(), { description: '群相册列表' });
type GetQunAlbumListReturn = Awaited<globalThis.ReturnType<NTQQWebApi['getAlbumListByNTQQ']>>['response']['album_list'];
export class GetQunAlbumList extends OneBotAction<PayloadType, GetQunAlbumListReturn> {
export class GetQunAlbumList extends OneBotAction<Payload, Awaited<ReturnType<NTQQWebApi['getAlbumListByNTQQ']>>['response']['album_list']> {
override actionName = ActionName.GetQunAlbumList;
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;
override payloadSchema = SchemaData;
async _handle (payload: PayloadType): Promise<GetQunAlbumListReturn> {
async _handle (payload: Payload) {
return (await this.core.apis.WebApi.getAlbumListByNTQQ(payload.group_id)).response.album_list;
}
}

View File

@@ -1,24 +1,8 @@
import { ActionName } from '@/napcat-onebot/action/router';
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
import { Type, Static } from '@sinclair/typebox';
const ReturnSchema = Type.Array(Type.Any(), { description: 'Rkey列表' });
type ReturnType = Static<typeof ReturnSchema>;
export class GetRkey extends GetPacketStatusDepends<void, ReturnType> {
export class GetRkey extends GetPacketStatusDepends<void, Array<unknown>> {
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 () {
return await this.core.apis.PacketApi.pkt.operation.FetchRkey();

View File

@@ -1,21 +1,8 @@
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Type, Static } from '@sinclair/typebox';
const ReturnSchema = Type.Array(Type.Any(), { description: '机器人Uin范围列表' });
type ReturnType = Static<typeof ReturnSchema>;
export class GetRobotUinRange extends OneBotAction<void, ReturnType> {
export class GetRobotUinRange extends OneBotAction<void, Array<unknown>> {
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 () {
return await this.core.apis.UserApi.getRobotUinRange();

View File

@@ -2,37 +2,26 @@ import { PacketBuf } from 'napcat-core/packet/transformer/base';
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { ProtoBuf, ProtoBufBase, PBUint32, PBString } from 'napcat.protobuf';
import { Type, Static } from '@sinclair/typebox';
const ReturnSchema = Type.Array(
Type.Object({
uin: Type.Number({ description: 'QQ号' }),
uid: Type.String({ description: '用户UID' }),
nick_name: Type.String({ description: '昵称' }),
age: Type.Number({ description: '年龄' }),
source: Type.String({ description: '来源' }),
}),
{ description: '单向好友列表' }
);
interface Friend {
uin: number;
uid: string;
nick_name: string;
age: number;
source: string;
}
type ReturnType = Static<typeof ReturnSchema>;
interface Block {
str_uid: string;
bytes_source: string;
uint32_sex: number;
uint32_age: number;
bytes_nick: string;
uint64_uin: number;
}
export class GetUnidirectionalFriendList extends OneBotAction<void, ReturnType> {
export class GetUnidirectionalFriendList extends OneBotAction<void, Friend[]> {
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> {
return ProtoBuf(class extends ProtoBufBase {
@@ -41,7 +30,7 @@ export class GetUnidirectionalFriendList extends OneBotAction<void, ReturnType>
}).encode();
}
async _handle (): Promise<ReturnType> {
async _handle (): Promise<Friend[]> {
const self_id = this.core.selfInfo.uin;
const req_json = {
uint64_uin: self_id,
@@ -51,18 +40,10 @@ export class GetUnidirectionalFriendList extends OneBotAction<void, ReturnType>
};
const packed_data = await this.pack_data(JSON.stringify(req_json));
const data = Buffer.from(packed_data);
const rsq = { cmd: 'MQUpdateSvc_com_qq_ti.web.OidbSvc.0xe17_0', data: data as unknown as PacketBuf };
const rsq = { cmd: 'MQUpdateSvc_com_qq_ti.web.OidbSvc.0xe17_0', data: data as PacketBuf };
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);
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;
const block_list: Block[] = JSON.parse(block_json.data).rpt_block_list;
return block_list.map((block) => ({
uin: block.uint64_uin,

View File

@@ -2,38 +2,17 @@ import { ActionName } from '@/napcat-onebot/action/router';
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
import { Static, Type } from '@sinclair/typebox';
const PayloadSchema = Type.Object({
user_id: Type.String({ description: 'QQ号' }),
const SchemaData = Type.Object({
user_id: Type.Union([Type.Number(), Type.String()]),
});
type PayloadType = Static<typeof PayloadSchema>;
type Payload = Static<typeof SchemaData>;
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> {
export class GetUserStatus extends GetPacketStatusDepends<Payload, { status: number; ext_status: number; } | undefined> {
override actionName = ActionName.GetUserStatus;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '获取用户在线状态';
override actionTags = ['系统扩展'];
override payloadExample = {
user_id: '123456789'
};
override returnExample = {
status: 10,
ext_status: 0
};
override payloadSchema = SchemaData;
async _handle (payload: PayloadType) {
const res = await this.core.apis.PacketApi.pkt.operation.GetStrangerStatus(+payload.user_id);
if (!res) {
throw new Error('无法获取用户状态');
}
return res;
async _handle (payload: Payload) {
return await this.core.apis.PacketApi.pkt.operation.GetStrangerStatus(+payload.user_id);
}
}

View File

@@ -3,38 +3,24 @@ import { FileNapCatOneBotUUID } from 'napcat-common/src/file-uuid';
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
import { Static, Type } from '@sinclair/typebox';
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
file_id: Type.String({ description: '文件ID' }),
current_parent_directory: Type.String({ description: '当前父目录' }),
target_parent_directory: Type.String({ description: '目标父目录' }),
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
file_id: Type.String(),
current_parent_directory: Type.String(),
target_parent_directory: Type.String(),
});
type PayloadType = Static<typeof PayloadSchema>;
type Payload = Static<typeof SchemaData>;
const ReturnSchema = Type.Object({
ok: Type.Boolean({ description: '是否成功' }),
}, { description: '移动文件结果' });
interface MoveGroupFileResponse {
ok: boolean;
}
type ReturnType = Static<typeof ReturnSchema>;
export class MoveGroupFile extends GetPacketStatusDepends<PayloadType, ReturnType> {
export class MoveGroupFile extends GetPacketStatusDepends<Payload, MoveGroupFileResponse> {
override actionName = ActionName.MoveGroupFile;
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;
override payloadSchema = SchemaData;
async _handle (payload: PayloadType) {
async _handle (payload: Payload) {
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
if (contextMsgFile?.fileUUID) {
await this.core.apis.PacketApi.pkt.operation.MoveGroupFile(+payload.group_id, contextMsgFile.fileUUID, payload.current_parent_directory, payload.target_parent_directory);

View File

@@ -3,29 +3,18 @@ import { ActionName } from '@/napcat-onebot/action/router';
import { checkFileExist, uriToLocalFile } from 'napcat-common/src/file';
import fs from 'fs';
import { Static, Type } from '@sinclair/typebox';
import { GeneralCallResultStatus } from 'napcat-core';
import { ExtendsActionsExamples } from '../example/ExtendsActionsExamples';
const PayloadSchema = Type.Object({
image: Type.String({ description: '图片路径、URL或Base64' }),
const SchemaData = Type.Object({
image: Type.String(),
});
type PayloadType = Static<typeof PayloadSchema>;
type Payload = Static<typeof SchemaData>;
const ReturnSchema = Type.Any({ description: 'OCR结果' });
class OCRImageBase extends OneBotAction<Payload, GeneralCallResultStatus> {
override payloadSchema = SchemaData;
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> {
async _handle (payload: Payload) {
const { path, success } = await uriToLocalFile(this.core.NapCatTempPath, payload.image);
if (!success) {
throw new Error(`OCR ${payload.image}失败, image字段可能格式不正确`);
@@ -48,10 +37,8 @@ class OCRImageBase extends OneBotAction<PayloadType, ReturnType> {
export class OCRImage extends OCRImageBase {
override actionName = ActionName.OCRImage;
override actionSummary = '图片 OCR 识别';
}
export class IOCRImage extends OCRImageBase {
override actionName = ActionName.IOCRImage;
override actionSummary = '图片 OCR 识别 (内部)';
}

View File

@@ -3,38 +3,24 @@ import { FileNapCatOneBotUUID } from 'napcat-common/src/file-uuid';
import { GetPacketStatusDepends } from '@/napcat-onebot/action/packet/GetPacketStatus';
import { Static, Type } from '@sinclair/typebox';
const PayloadSchema = Type.Object({
group_id: Type.String({ description: '群号' }),
file_id: Type.String({ description: '文件ID' }),
current_parent_directory: Type.String({ description: '当前父目录' }),
new_name: Type.String({ description: '新文件名' }),
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
file_id: Type.String(),
current_parent_directory: Type.String(),
new_name: Type.String(),
});
type PayloadType = Static<typeof PayloadSchema>;
type Payload = Static<typeof SchemaData>;
const ReturnSchema = Type.Object({
ok: Type.Boolean({ description: '是否成功' }),
}, { description: '重命名文件结果' });
interface RenameGroupFileResponse {
ok: boolean;
}
type ReturnType = Static<typeof ReturnSchema>;
export class RenameGroupFile extends GetPacketStatusDepends<PayloadType, ReturnType> {
export class RenameGroupFile extends GetPacketStatusDepends<Payload, RenameGroupFileResponse> {
override actionName = ActionName.RenameGroupFile;
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;
override payloadSchema = SchemaData;
async _handle (payload: PayloadType) {
async _handle (payload: Payload) {
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
if (contextMsgFile?.fileUUID) {
await this.core.apis.PacketApi.pkt.operation.RenameGroupFile(+payload.group_id, contextMsgFile.fileUUID, payload.current_parent_directory, payload.new_name);

Some files were not shown because too many files have changed in this diff Show More