Compare commits

...

111 Commits

Author SHA1 Message Date
手瓜一十雪
3069900202 fix: fallback 2025-01-22 15:07:44 +08:00
pk5ls20
c46fb0f48a fix: 6.9.63 -> 6.9.65 [skip ci] 2025-01-22 14:50:48 +08:00
pk5ls20
07cd8f883e feat: 31363 2025-01-22 14:47:48 +08:00
手瓜一十雪
cfdb9d64ad fix: #722 2025-01-22 14:45:02 +08:00
Mlikiowa
b73e3aa3b7 release: v4.4.3 2025-01-22 01:35:33 +00:00
手瓜一十雪
cd315b0e71 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-01-22 09:33:23 +08:00
手瓜一十雪
4d4d79e66f fix #727 2025-01-22 09:33:19 +08:00
手瓜一十雪
395ce97a78 Merge pull request #726 from Caulic/main
fix: 无法修改网络配置表单值
2025-01-22 09:16:53 +08:00
Caulic
e44e8423d0 fix: 无法修改网络配置表单值 2025-01-22 01:16:35 +08:00
手瓜一十雪
fa13a56697 Merge pull request #725 from clansty/feat/face_resultId
update face list
2025-01-21 22:45:25 +08:00
Clansty
6383164aec update face list 2025-01-21 22:42:37 +08:00
Mlikiowa
d9adfad1c0 release: v4.4.2 2025-01-21 14:15:28 +00:00
手瓜一十雪
901828f5a6 Merge pull request #724 from clansty/feat/face_resultId
为新的接龙表情提供 resultId 和 chainCount 返回
2025-01-21 22:14:18 +08:00
Clansty
2a4b0cbc09 force resultId to string 2025-01-21 22:10:52 +08:00
Clansty
c5434efd56 为新的接龙表情提供 resultId 和 chainCount 返回 2025-01-21 22:02:35 +08:00
手瓜一十雪
b73f283095 Merge pull request #717 from NapNeko/dependabot/npm_and_yarn/file-type-20.0.0
chore(deps-dev): bump file-type from 19.6.0 to 20.0.0
2025-01-21 21:55:45 +08:00
Mlikiowa
24ef54f01c release: v4.4.1 2025-01-21 13:46:31 +00:00
手瓜一十雪
bff3b85337 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-01-21 21:46:08 +08:00
手瓜一十雪
811d9a7237 fix 2025-01-21 21:46:00 +08:00
Mlikiowa
a764cb8dc2 release: v4.4.0 2025-01-21 13:45:37 +00:00
手瓜一十雪
9204b9b286 fix: #723 2025-01-21 21:43:11 +08:00
手瓜一十雪
da94faa9bb fix 2025-01-21 20:41:13 +08:00
手瓜一十雪
4b53e9a895 fix: 进一步重构 2025-01-21 20:40:52 +08:00
Mlikiowa
f5db96187b release: v4.3.9 2025-01-20 09:35:55 +00:00
手瓜一十雪
857b191b03 feat: sse完全体 2025-01-20 17:35:31 +08:00
Mlikiowa
09014d1ab5 release: v4.3.8 2025-01-20 09:18:32 +00:00
手瓜一十雪
7557b71869 fix: httpSseServers DefaultConfig 2025-01-20 17:02:52 +08:00
dependabot[bot]
9c4751794f chore(deps-dev): bump file-type from 19.6.0 to 20.0.0
Bumps [file-type](https://github.com/sindresorhus/file-type) from 19.6.0 to 20.0.0.
- [Release notes](https://github.com/sindresorhus/file-type/releases)
- [Commits](https://github.com/sindresorhus/file-type/compare/v19.6.0...v20.0.0)

---
updated-dependencies:
- dependency-name: file-type
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-20 08:59:49 +00:00
Mlikiowa
d07187bd5d release: v4.3.7 2025-01-20 08:48:13 +00:00
手瓜一十雪
2c6a6ba440 fix: type 2025-01-20 16:47:06 +08:00
手瓜一十雪
4592bf7817 fix: nickname可能为null 2025-01-20 16:06:34 +08:00
Mlikiowa
afd6d450a0 release: v4.3.6 2025-01-20 06:51:29 +00:00
手瓜一十雪
b134849dcf feat: 支持文件名发送&兼容单空格问题 2025-01-20 14:51:08 +08:00
手瓜一十雪
e7d0f6d6da feat: 更合适的记录与rkey限制 2025-01-19 15:55:56 +08:00
手瓜一十雪
16a29b0127 feat: file 2025-01-19 15:47:09 +08:00
pk5ls20
1f5596ef16 Merge pull request #715 from FfmpegZZZ/main
chore:移除失效链接
2025-01-19 15:19:01 +08:00
Ffmpeg
bef05432d0 Update README.md 2025-01-19 15:10:18 +08:00
手瓜一十雪
67533d7743 docs: 已重写部分实现 2025-01-13 20:37:43 +08:00
Mlikiowa
0cc86c6348 release: v4.3.5 2025-01-13 12:36:23 +00:00
手瓜一十雪
607dd68620 refactor: 标准化与提高缓存策略 2025-01-13 20:35:52 +08:00
手瓜一十雪
7c8cbc0799 style: lint 2025-01-13 20:30:57 +08:00
手瓜一十雪
ec0c2e8c33 refactor: 大部分文件处理部分 2025-01-13 20:30:08 +08:00
手瓜一十雪
7f3dbe0552 fix: groupfile file_size 2025-01-13 19:38:29 +08:00
手瓜一十雪
0e9044e0c8 drop: umami 2025-01-13 19:32:34 +08:00
手瓜一十雪
3171640193 Merge pull request #701 from Stapxs/main
HTTP SSE 消息上报模式
2025-01-13 19:26:56 +08:00
手瓜一十雪
a56cee3485 fix: #682 2025-01-13 19:25:08 +08:00
pk5ls20
c8ee371982 feat: packet ocr 2025-01-12 10:13:12 +08:00
pk5ls20
5778daeb60 refactor: packet 2025-01-11 16:23:02 +08:00
手瓜一十雪
f51f3b9861 fix 2025-01-11 14:29:44 +08:00
Mlikiowa
44dd1a0b02 release: v4.3.4 2025-01-11 04:16:54 +00:00
pk5ls20
61a00ffcbf feat: 31245 2025-01-11 12:10:46 +08:00
pk5ls20
4b0a0f0a32 feat: #702 2025-01-10 15:23:40 +08:00
stapxs
a3088fb8bc 质量保障 2025-01-09 13:42:55 +08:00
stapxs
88fd1f9eb1 http sse 消息上报模式 2025-01-09 13:22:46 +08:00
手瓜一十雪
15156bac1e fix: log 2025-01-07 20:49:49 +08:00
Mlikiowa
a898d2e7be release: v4.3.3 2025-01-07 11:31:56 +00:00
手瓜一十雪
95b003802c fix: win 31245 2025-01-07 19:31:35 +08:00
Mlikiowa
95c9eae4ed release: v4.3.2 2025-01-07 11:30:35 +00:00
手瓜一十雪
e3814403e4 fix: #682 2025-01-07 19:28:46 +08:00
手瓜一十雪
3d16d52dd8 fix: #696 fallback 2025-01-07 18:51:04 +08:00
Mlikiowa
1ae47fffb4 release: v4.2.68 2025-01-06 13:08:43 +00:00
手瓜一十雪
4e7096b9e2 feat: offset win 31219 2025-01-06 21:08:18 +08:00
Mlikiowa
8cc9b7f6a7 release: v4.2.67 2025-01-05 02:50:38 +00:00
手瓜一十雪
fb45c1020e revert: 再也不用umami了 2025-01-05 10:49:49 +08:00
Mlikiowa
e9db4ae8f4 release: v4.2.66 2025-01-04 11:52:38 +00:00
手瓜一十雪
c46ec32bd6 fix: 简化代码 2025-01-04 13:12:04 +08:00
手瓜一十雪
c58a26ed99 fix 2025-01-04 13:06:37 +08:00
手瓜一十雪
a66f5e4971 fix 2025-01-04 13:03:29 +08:00
pk5ls20
574c8c6089 fix: SendGroupAiRecord
- smtx
2025-01-04 12:48:53 +08:00
手瓜一十雪
67afd95910 feat: getUserDetailInfoV2 2025-01-03 21:38:57 +08:00
手瓜一十雪
f7d0cb0be7 fix 2025-01-03 21:07:26 +08:00
手瓜一十雪
be9b68a0b1 feat: CancelableTask&Fallback 2025-01-03 20:46:51 +08:00
Mlikiowa
4637414af2 release: v4.2.65 2025-01-03 05:14:05 +00:00
手瓜一十雪
4bd92a72bd fix: ua agent 2025-01-03 13:09:43 +08:00
手瓜一十雪
a3be26f3e4 fix: error 2025-01-03 12:44:55 +08:00
Mlikiowa
675c906cbf release: v4.2.64 2024-12-31 14:42:17 +00:00
手瓜一十雪
6be6023236 Merge pull request #679 from wu-yafeng/main
fix #678
2024-12-31 22:41:34 +08:00
WuYafeng
42cee0d018 在没有写入权限时记录warn日志 2024-12-31 22:38:01 +08:00
WuYafeng
041f725748 Update config.ts 2024-12-31 22:25:05 +08:00
WuYafeng
0594d61631 Update config.ts 2024-12-31 22:20:34 +08:00
Mlikiowa
15cae6b765 release: v4.2.63 2024-12-31 13:52:37 +00:00
pk5ls20
b984116c35 fix: #677 2024-12-31 21:34:45 +08:00
Mlikiowa
13bda6e3f4 release: v4.2.62 2024-12-31 08:35:55 +00:00
手瓜一十雪
c0d18549d1 fix 2024-12-31 15:09:08 +08:00
手瓜一十雪
3caff72fce fix: cache trace 2024-12-31 13:10:58 +08:00
Mlikiowa
1313e9c3f4 release: v4.2.61 2024-12-30 15:59:33 +00:00
手瓜一十雪
0848d5a39e feat: clouflare 2024-12-30 23:59:09 +08:00
手瓜一十雪
7660646059 fix 2024-12-30 22:16:09 +08:00
Mlikiowa
bcd90fc744 release: v4.2.60 2024-12-30 12:48:11 +00:00
手瓜一十雪
638fc22d62 fix:error 2024-12-30 20:47:50 +08:00
手瓜一十雪
c87d365b88 fix 2024-12-30 20:42:23 +08:00
Mlikiowa
aee9602f25 release: v4.2.59 2024-12-30 12:29:33 +00:00
手瓜一十雪
976fbd0220 fix: 修复精确统计 2024-12-30 20:27:28 +08:00
手瓜一十雪
afd955d06f fix: error 2024-12-30 20:14:51 +08:00
手瓜一十雪
4d548da66b fix: console-log 2024-12-30 19:56:06 +08:00
手瓜一十雪
41b70f53d1 fix: LoginErrorCode 2024-12-30 19:29:14 +08:00
Wesley F. Young
a47a618bcd make author NapNeko in manifest.json
毕竟我已经脱离一线开发挺久了(
2024-12-30 18:40:57 +08:00
Mlikiowa
62170a30af release: v4.2.58 2024-12-30 09:56:02 +00:00
手瓜一十雪
780c5ac23c fix 2024-12-30 17:55:40 +08:00
手瓜一十雪
9fba519a5a fix 2024-12-30 17:55:13 +08:00
Mlikiowa
3cd0e7d26b release: v4.2.57 2024-12-30 09:54:40 +00:00
手瓜一十雪
a8fd6af994 Merge pull request #676 from NapNeko/dependabot/npm_and_yarn/commander-13.0.0
chore(deps-dev): bump commander from 12.1.0 to 13.0.0
2024-12-30 17:54:03 +08:00
手瓜一十雪
4000b89644 fix 2024-12-30 17:53:05 +08:00
Mlikiowa
9c00bbc0b7 release: v4.2.56 2024-12-30 08:55:36 +00:00
手瓜一十雪
a2989d3b38 fix:workname 2024-12-30 16:54:40 +08:00
Mlikiowa
fc731b60d5 release: v4.2.54 2024-12-30 08:47:57 +00:00
手瓜一十雪
193980dd4a fix: trace 2024-12-30 16:47:33 +08:00
dependabot[bot]
35427b0768 chore(deps-dev): bump commander from 12.1.0 to 13.0.0
Bumps [commander](https://github.com/tj/commander.js) from 12.1.0 to 13.0.0.
- [Release notes](https://github.com/tj/commander.js/releases)
- [Changelog](https://github.com/tj/commander.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tj/commander.js/compare/v12.1.0...v13.0.0)

---
updated-dependencies:
- dependency-name: commander
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-30 08:36:27 +00:00
Mlikiowa
73ea130e40 release: v4.2.53 2024-12-30 08:34:23 +00:00
手瓜一十雪
5667e6aaee fix 2024-12-30 16:34:01 +08:00
Mlikiowa
fbd626131d release: v4.2.52 2024-12-30 08:17:53 +00:00
97 changed files with 1762 additions and 1058 deletions

2
.gitignore vendored
View File

@@ -1,14 +1,12 @@
# Develop
node_modules/
package-lock.json
pnpm-lock.yaml
out/
dist/
/src/core.lib/common/
/localdebug/
# Editor
.vscode/*
!.vscode/extensions.json
.idea/*

9
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,9 @@
{
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": {
".env.universal": ".env.*",
"tsconfig.json": "tsconfig.*.json, env.d.ts, vite.config.ts",
"package.json": "package-lock.json, eslint*, .prettier*, .editorconfig, manifest.json, logo.png, .gitignore, LICENSE"
}
}

View File

@@ -32,12 +32,12 @@ NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
[Server.Other](https://docs.napcat.cyou/)
[Qbot.News](https://neko.qbot.news)
[NapCat.Wiki](https://www.napcat.wiki)
## 回家旅途
[QQ Group#1](https://qm.qq.com/q/I6LU87a0Yq)
[QQ Group#2](https://qm.qq.com/q/uqh4I87KoM)
[QQ Group#2](https://qm.qq.com/q/HaRcfrHpUk)
[Telegram](https://t.me/MelodicMoonlight)
@@ -46,7 +46,7 @@ NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
## 性能设计/协议标准
NapCat 已实现90+的 OneBot / GoCQ 标准接口,并提供兼容性保留接口,其设计理念遵守 无数据库/异步优先/后台刷新 的性能思想。
由此设计带来一系列好处在开发中获取群员列表通常小于50Ms单条文本消息发送在320Ms以内在1k+的群聊流畅运行,同时带来一些副作用,上报数据中大量使用Magic生成字段消息Id无法持久无法上报撤回消息原始内容。
由此设计带来一系列好处在开发中获取群员列表通常小于50Ms单条文本消息发送在320Ms以内在1k+的群聊流畅运行同时带来一些副作用消息Id无法持久无法上报撤回消息原始内容。
NapCat 在设计理念下遵守 OneBot 规范大多数要求并且积极改进,任何合理的标准化 Issue 与 Pr 将被接收。

View File

@@ -4,16 +4,12 @@
"name": "NapCatQQ",
"slug": "NapCat.Framework",
"description": "高性能的 OneBot 11 协议实现",
"version": "4.2.51",
"version": "4.4.3",
"icon": "./logo.png",
"authors": [
{
"name": "MliKiowa",
"link": "https://github.com/MliKiowa"
},
{
"name": "Young",
"link": "https://github.com/Wesley-Young"
"name": "NapNeko",
"link": "https://github.com/NapNeko"
}
],
"repository": {

View File

@@ -11,85 +11,66 @@
<t-divider align="right">
<t-button @click="addConfig()">
<template #icon><add-icon /></template>
添加配置</t-button
>
添加配置</t-button>
</t-divider>
</div>
<div v-if="loadPage" ref="setting" class="setting">
<t-tabs ref="tabsRef" :style="{ width: tabsWidth + 'px' }" default-value="all" @change="selectType">
<t-tab-panel value="all" label="全部"></t-tab-panel>
<t-tab-panel value="httpServers" label="HTTP 服务器"></t-tab-panel>
<t-tab-panel value="httpSseServers" label="HTTP SSE 服务器"></t-tab-panel>
<t-tab-panel value="httpClients" label="HTTP 客户端"></t-tab-panel>
<t-tab-panel value="websocketServers" label="WebSocket 服务器"></t-tab-panel>
<t-tab-panel value="websocketClients" label="WebSocket 客户端"></t-tab-panel>
</t-tabs>
</div>
<t-loading attach="#alice" :loading="!loadPage" :showOverlay="false">
<div id="alice" v-if="!loadPage" style="height: 80vh;position: relative" ></div>
<div id="alice" v-if="!loadPage" style="height: 80vh;position: relative"></div>
</t-loading>
<div v-if="loadPage" class="card-box" :style="{ width: tabsWidth + 'px' }">
<div class="setting-box" :style="{ maxHeight: cardHeight + 'px' }" v-if="cardConfig.length > 0">
<div v-for="(item, index) in cardConfig" :key="index">
<t-card
:title="item.name"
:description="item.type"
:style="{ width: cardWidth + 'px' }"
:header-bordered="true"
class="setting-card"
>
<t-card :title="item.name" :description="item.type" :style="{ width: cardWidth + 'px' }"
:header-bordered="true" class="setting-card">
<template #actions>
<t-space>
<edit2-icon size="20px" @click="editConfig(item)"></edit2-icon>
<t-popconfirm content="确认删除" @confirm="delConfig(item)">
<t-popconfirm content="确认删除" @confirm="delConfig(item)">
<delete-icon size="20px"></delete-icon>
</t-popconfirm>
</t-space>
</template>
<div class="setting-content">
<t-card
class="card-address"
:style="{
borderLeft:
'7px solid ' + (item.enable ? 'var(--td-success-color)' : 'var(--td-error-color)'),
}"
>
<t-card class="card-address" :style="{
borderLeft:
'7px solid ' + (item.enable ? 'var(--td-success-color)' : 'var(--td-error-color)'),
}">
<div class="local-box" v-if="item.host && item.port">
<server-filled-icon class="local-icon" size="20px" @click="toggleProperty(item, 'enable')"></server-filled-icon>
<server-filled-icon class="local-icon" size="20px"
@click="toggleProperty(item, 'enable')"></server-filled-icon>
<strong class="local">{{ item.host }}:{{ item.port }}</strong>
<copy-icon
class="copy-icon"
size="20px"
@click="copyText(item.host + ':' + item.port)"
></copy-icon>
<copy-icon class="copy-icon" size="20px"
@click="copyText(item.host + ':' + item.port)"></copy-icon>
</div>
<div class="local-box" v-if="item.url">
<server-filled-icon class="local-icon" size="20px" @click="toggleProperty(item, 'enable')"></server-filled-icon>
<server-filled-icon class="local-icon" size="20px"
@click="toggleProperty(item, 'enable')"></server-filled-icon>
<strong class="local">{{ item.url }}</strong>
<copy-icon class="copy-icon" size="20px" @click="copyText(item.url)"></copy-icon>
</div>
</t-card>
<t-collapse :default-value="[0]" expand-mutex style="margin-top: 10px" class="info-coll">
<t-collapse-panel header="基础信息">
<t-descriptions
size="small"
:layout="infoOneCol ? 'vertical' : 'horizontal'"
class="setting-base-info"
>
<t-descriptions size="small" :layout="infoOneCol ? 'vertical' : 'horizontal'"
class="setting-base-info">
<t-descriptions-item v-if="item.token" label="连接密钥">
<div v-if="mediumScreen.matches || largeScreen.matches" class="token-view">
<span>{{ showToken ? item.token : '******' }}</span>
<browse-icon
class="browse-icon"
v-if="showToken"
size="18px"
@click="showToken = false"
></browse-icon>
<browse-off-icon
class="browse-icon"
v-else
size="18px"
@click="showToken = true"
></browse-off-icon>
<browse-icon class="browse-icon" v-if="showToken" size="18px"
@click="showToken = false"></browse-icon>
<browse-off-icon class="browse-icon" v-else size="18px"
@click="showToken = true"></browse-off-icon>
</div>
<div v-else>
<t-popup :showArrow="true" trigger="click">
@@ -106,60 +87,36 @@
</t-descriptions>
</t-collapse-panel>
<t-collapse-panel header="状态信息">
<t-descriptions
size="small"
:layout="infoOneCol ? 'vertical' : 'horizontal'"
class="setting-base-info"
>
<t-descriptions size="small" :layout="infoOneCol ? 'vertical' : 'horizontal'"
class="setting-base-info">
<t-descriptions-item v-if="item.hasOwnProperty('debug')" label="调试日志">
<t-tag
:class="item.debug ? 'tag-item-on' : 'tag-item-off'"
@click="toggleProperty(item, 'debug')"
>
{{ item.debug ? '开启' : '关闭' }}</t-tag
>
<t-tag :class="item.debug ? 'tag-item-on' : 'tag-item-off'"
@click="toggleProperty(item, 'debug')">
{{ item.debug ? '开启' : '关闭' }}</t-tag>
</t-descriptions-item>
<t-descriptions-item
v-if="item.hasOwnProperty('enableWebsocket')"
label="Websocket 功能"
>
<t-tag
:class="item.enableWebsocket ? 'tag-item-on' : 'tag-item-off'"
@click="toggleProperty(item, 'enableWebsocket')"
>
{{ item.enableWebsocket ? '启用' : '禁用' }}</t-tag
>
<t-descriptions-item v-if="item.hasOwnProperty('enableWebsocket')"
label="Websocket 功能">
<t-tag :class="item.enableWebsocket ? 'tag-item-on' : 'tag-item-off'"
@click="toggleProperty(item, 'enableWebsocket')">
{{ item.enableWebsocket ? '启用' : '禁用' }}</t-tag>
</t-descriptions-item>
<t-descriptions-item
v-if="item.hasOwnProperty('enableCors')"
label="跨域放行"
>
<t-tag :class="item.enableCors ? 'tag-item-on' : 'tag-item-off'" @click="toggleProperty(item, 'enableCors')">
{{ item.enableCors ? '开启' : '关闭' }}</t-tag
>
<t-descriptions-item v-if="item.hasOwnProperty('enableCors')" label="跨域放行">
<t-tag :class="item.enableCors ? 'tag-item-on' : 'tag-item-off'"
@click="toggleProperty(item, 'enableCors')">
{{ item.enableCors ? '开启' : '关闭' }}</t-tag>
</t-descriptions-item>
<t-descriptions-item
v-if="item.hasOwnProperty('enableForcePushEvent')"
label="上报自身消息"
>
<t-tag
:class="item.reportSelfMessage ? 'tag-item-on' : 'tag-item-off'"
@click="toggleProperty(item, 'reportSelfMessage')"
>
{{ item.reportSelfMessage ? '开启' : '关闭' }}</t-tag
>
<t-descriptions-item v-if="item.hasOwnProperty('enableForcePushEvent')"
label="上报自身消息">
<t-tag :class="item.reportSelfMessage ? 'tag-item-on' : 'tag-item-off'"
@click="toggleProperty(item, 'reportSelfMessage')">
{{ item.reportSelfMessage ? '开启' : '关闭' }}</t-tag>
</t-descriptions-item>
<t-descriptions-item
v-if="item.hasOwnProperty('enableForcePushEvent')"
label="强制推送事件"
>
<t-tag
class="tag-item"
<t-descriptions-item v-if="item.hasOwnProperty('enableForcePushEvent')"
label="强制推送事件">
<t-tag class="tag-item"
:class="item.enableForcePushEvent ? 'tag-item-on' : 'tag-item-off'"
@click="toggleProperty(item, 'enableForcePushEvent')"
>
{{ item.enableForcePushEvent ? '开启' : '关闭' }}</t-tag
>
@click="toggleProperty(item, 'enableForcePushEvent')">
{{ item.enableForcePushEvent ? '开启' : '关闭' }}</t-tag>
</t-descriptions-item>
</t-descriptions>
</t-collapse-panel>
@@ -173,32 +130,19 @@
<t-empty class="card-none" title="暂无网络配置"> </t-empty>
</t-card>
</div>
<t-dialog
v-model:visible="visibleBody"
:header="dialogTitle"
:destroy-on-close="true"
:show-in-attached-element="true"
:on-confirm="saveConfig"
class=".t-dialog__ctx .t-dialog__position"
>
<t-dialog v-model:visible="visibleBody" :header="dialogTitle" :destroy-on-close="true"
:show-in-attached-element="true" :on-confirm="saveConfig" class=".t-dialog__ctx .t-dialog__position">
<div slot="body" class="dialog-body">
<t-form ref="form" :data="newTab" labelAlign="left" :model="newTab">
<t-form-item
style="text-align: left"
:rules="[{ required: true, message: '请输入名称', trigger: 'blur' }]"
label="名称"
name="name"
>
<t-form-item style="text-align: left" :rules="[{ required: true, message: '请输入名称', trigger: 'blur' }]"
label="名称" name="name">
<t-input v-model="newTab.name" />
</t-form-item>
<t-form-item
style="text-align: left"
:rules="[{ required: true, message: '请选择类型', trigger: 'change' }]"
label="类型"
name="type"
>
<t-form-item style="text-align: left" :rules="[{ required: true, message: '请选择类型', trigger: 'change' }]"
label="类型" name="type">
<t-select v-model="newTab.type" @change="onloadDefault">
<t-option value="httpServers">HTTP 服务器</t-option>
<t-option value="httpSseServers">HTTP SSE 服务器</t-option>
<t-option value="httpClients">HTTP 客户端</t-option>
<t-option value="websocketServers">WebSocket 服务器</t-option>
<t-option value="websocketClients">WebSocket 客户端</t-option>
@@ -207,7 +151,7 @@
<div>
<component
:is="resolveDynamicComponent(getComponent(newTab.type as ComponentKey))"
:config="newTab.data"
:config="newTab"
/>
</div>
</t-form>
@@ -226,12 +170,10 @@ import {
BrowseIcon,
Wifi1Icon,
} from 'tdesign-icons-vue-next';
import { onMounted, onUnmounted, ref, resolveDynamicComponent, watch } from 'vue';
import emitter from '@/ts/event-bus';
import {
mergeNetworkDefaultConfig,
mergeOneBotConfigs,
NetworkConfig,
loadConfig as loadConfigOnebot,
NetworkAdapterConfig,
NetworkConfigKey,
OneBotConfig,
} from '../../../src/onebot/config/config';
import HttpServerComponent from '@/pages/network/HttpServerComponent.vue';
@@ -240,6 +182,9 @@ import WebsocketServerComponent from '@/pages/network/WebsocketServerComponent.v
import WebsocketClientComponent from '@/pages/network/WebsocketClientComponent.vue';
import { MessagePlugin } from 'tdesign-vue-next';
import { QQLoginManager } from '@/backend/shell';
import { onMounted, onUnmounted, ref, watch, resolveDynamicComponent } from 'vue';
import emitter from '@/ts/event-bus';
import HttpSseServerComponent from './network/HttpSseServerComponent.vue';
const showToken = ref<boolean>(false);
const infoOneCol = ref<boolean>(true);
@@ -256,16 +201,17 @@ const visibleBody = ref<boolean>(false);
const newTab = ref<{ name: string; data: any; type: string }>({ name: '', data: {}, type: '' });
const dialogTitle = ref<string>('');
type ComponentKey = keyof typeof mergeNetworkDefaultConfig;
type ComponentKey = Exclude<NetworkConfigKey, 'plugins'>
const componentMap: Record<
ComponentKey,
| typeof HttpServerComponent
| typeof HttpClientComponent
| typeof WebsocketServerComponent
| typeof WebsocketClientComponent
| typeof HttpSseServerComponent
> = {
httpServers: HttpServerComponent,
httpSseServers: HttpSseServerComponent,
httpClients: HttpClientComponent,
websocketServers: WebsocketServerComponent,
websocketClients: WebsocketClientComponent,
@@ -276,9 +222,10 @@ const operateType = ref<string>('');
//配置项索引
const configIndex = ref<number>(0);
//保存时所用数据
const networkConfig: NetworkConfig & { [key: string]: any } = {
const networkConfig: { [key: string]: any } = {
websocketClients: [],
websocketServers: [],
httpSseServers: [],
httpClients: [],
httpServers: [],
};
@@ -289,6 +236,7 @@ const WebConfg = ref(
['all', []],
['httpServers', []],
['httpClients', []],
['httpSseServers', []],
['websocketServers', []],
['websocketClients', []],
])
@@ -296,6 +244,7 @@ const WebConfg = ref(
const typeCh: Record<ComponentKey, string> = {
httpServers: 'HTTP 服务器',
httpClients: 'HTTP 客户端',
httpSseServers: 'HTTP SSE 服务器',
websocketServers: 'WebSocket 服务器',
websocketClients: 'WebSocket 客户端',
};
@@ -315,15 +264,12 @@ const addConfig = () => {
};
const editConfig = (item: any) => {
dialogTitle.value = '修改配置';
const type = getKeyByValue(typeCh, item.type);
if (type) {
newTab.value = { name: item.name, data: item, type: type };
}
dialogTitle.value = '编辑配置';
newTab.value = { name: item.name, data: { ...item }, type: getKeyByValue(typeCh, item.type) || '' };
operateType.value = 'edit';
configIndex.value = networkConfig[newTab.value.type].findIndex((obj: any) => obj.name === item.name);
visibleBody.value = true;
};
const toggleProperty = async (item: any, tagData: string) => {
const type = getKeyByValue(typeCh, item.type);
const newData = { ...item };
@@ -349,11 +295,11 @@ const delConfig = (item: any) => {
};
const selectType = (key: ComponentKey) => {
cardConfig.value = WebConfg.value.get(key);
cardConfig.value = WebConfg.value.get(key) || [];
};
const onloadDefault = (key: ComponentKey) => {
newTab.value.data = structuredClone(mergeNetworkDefaultConfig[key]);
newTab.value.data = {};
};
//检测重名
const checkName = (name: string) => {
@@ -383,7 +329,7 @@ const saveConfig = async () => {
}
const userConfig = await getOB11Config();
if (!userConfig) return;
userConfig.network = networkConfig;
userConfig.network = networkConfig as any;
const success = await setOB11Config(userConfig);
if (success) {
operateType.value = '';
@@ -416,12 +362,12 @@ const setOB11Config = async (config: OneBotConfig): Promise<boolean> => {
};
//获取卡片数据
const getAllData = (data: NetworkConfig) => {
const getAllData = (data: { [key: string]: Array<NetworkAdapterConfig> }) => {
cardConfig.value = [];
WebConfg.value.set('all', []);
for (const key in data) {
const configs = data[key as keyof NetworkConfig];
if (key in mergeNetworkDefaultConfig) {
const configs = data[key as keyof NetworkAdapterConfig];
if (key in networkConfig) {
networkConfig[key] = [...configs];
const newConfigsArray = configs.map((config: any) => ({
...config,
@@ -442,13 +388,12 @@ const loadConfig = async () => {
try {
const userConfig = await getOB11Config();
if (!userConfig) return;
const mergedConfig = mergeOneBotConfigs(userConfig);
const mergedConfig = loadConfigOnebot(userConfig);
getAllData(mergedConfig.network);
} catch (error) {
console.error('Error loading config:', error);
}
};
const copyText = async (text: string) => {
const textarea = document.createElement('textarea');
textarea.value = text;
@@ -474,9 +419,9 @@ const handleResize = () => {
cardWidth.value = tabsWidth.value;
}
loadPage.value = true;
setTimeout(()=>{
setTimeout(() => {
cardHeight.value = window.innerHeight - (headerBox.value?.offsetHeight ?? 0) - (setting.value?.offsetHeight ?? 0) - 21;
},300)
}, 300)
};
emitter.on('sendWidth', (width) => {
if (typeof width === 'string') {
@@ -486,30 +431,30 @@ emitter.on('sendWidth', (width) => {
});
watch(menuWidth, (newValue, oldValue) => {
loadPage.value = false;
setTimeout(()=>{
setTimeout(() => {
handleResize();
},300)
}, 300)
});
onMounted(() => {
loadConfig();
const cachedWidth = localStorage.getItem('menuWidth');
if (cachedWidth) {
menuWidth.value = parseInt(cachedWidth);
setTimeout(()=>{
setTimeout(() => {
handleResize();
},300)
}, 300)
}
window.addEventListener('resize', ()=>{
setTimeout(()=>{
window.addEventListener('resize', () => {
setTimeout(() => {
handleResize();
},300)
}, 300)
});
});
onUnmounted(() => {
window.removeEventListener('resize', ()=>{
setTimeout(()=>{
window.removeEventListener('resize', () => {
setTimeout(() => {
handleResize();
},300)
}, 300)
});
});
</script>
@@ -550,9 +495,11 @@ onUnmounted(() => {
display: flex;
margin-top: 2px;
}
.local-icon {
flex: 1;
}
.local {
flex: 6;
margin: 0 10px 0 10px;
@@ -579,22 +526,26 @@ onUnmounted(() => {
text-overflow: ellipsis;
}
.tag-item-on{
.tag-item-on {
color: white;
cursor: pointer;
background-image: linear-gradient(to top, #0ba360 0%, #3cba92 100%) !important;
}
.tag-item-off{
.tag-item-off {
color: white;
cursor: pointer;
background-image: linear-gradient(to top, rgba(255, 8, 68, 0.93) 0%, #D54941 100%) !important;
}
.browse-icon {
flex: 2;
}
:global(.t-dialog__ctx .t-dialog__position) {
padding: 48px 10px;
}
@media (max-width: 1024px) {
.setting-box {
grid-template-columns: 1fr 1fr;
@@ -644,7 +595,7 @@ onUnmounted(() => {
padding: 0 var(--td-comp-paddingLR-l) !important;
}
.setting-base-info tr > td:last-child {
.setting-base-info tr>td:last-child {
text-align: right;
}

View File

@@ -2,22 +2,22 @@
<div>
<t-form labelAlign="left">
<t-form-item label="启用">
<t-switch v-model="config.enable" />
<t-switch v-model="props.config.data.enable" />
</t-form-item>
<t-form-item label="URL">
<t-input v-model="config.url" />
<t-input v-model="props.config.data.url" />
</t-form-item>
<t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
<t-select v-model="props.config.data.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="报告自身消息">
<t-switch v-model="config.reportSelfMessage" />
<t-switch v-model="props.config.data.reportSelfMessage" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" />
<t-input v-model="props.config.data.token" />
</t-form-item>
<t-form-item label="调试模式">
<t-switch v-model="config.debug" />
<t-switch v-model="props.config.data.debug" />
</t-form-item>
</t-form>
</div>
@@ -27,20 +27,32 @@
import { ref, watch } from 'vue';
import { HttpClientConfig } from '../../../../src/onebot/config/config';
const defaultConfig: HttpClientConfig = {
name: 'http-client',
enable: false,
url: 'http://localhost:8080',
messagePostFormat: 'array',
reportSelfMessage: false,
token: '',
debug: false,
};
const props = defineProps<{
config: HttpClientConfig;
config: { data: HttpClientConfig };
}>();
props.config.data = { ...defaultConfig, ...props.config.data };
const messageFormatOptions = ref([
{ label: 'Array', value: 'array' },
{ label: 'String', value: 'string' },
]);
watch(
() => props.config.messagePostFormat,
() => props.config.data.messagePostFormat,
(newValue) => {
if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array';
props.config.data.messagePostFormat = 'array';
}
}
);

View File

@@ -2,28 +2,28 @@
<div>
<t-form labelAlign="left">
<t-form-item label="启用">
<t-switch v-model="config.enable" />
<t-switch v-model="props.config.data.enable" />
</t-form-item>
<t-form-item label="端口">
<t-input v-model.number="config.port" type="number" />
<t-input v-model.number="props.config.data.port" type="number" />
</t-form-item>
<t-form-item label="主机">
<t-input v-model="config.host" type="text" />
<t-input v-model="props.config.data.host" type="text" />
</t-form-item>
<t-form-item label="启用 CORS">
<t-switch v-model="config.enableCors" />
<t-switch v-model="props.config.data.enableCors" />
</t-form-item>
<t-form-item label="启用 WS">
<t-switch v-model="config.enableWebsocket" />
<t-switch v-model="props.config.data.enableWebsocket" />
</t-form-item>
<t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
<t-select v-model="props.config.data.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" type="text" />
<t-input v-model="props.config.data.token" type="text" />
</t-form-item>
<t-form-item label="调试模式">
<t-switch v-model="config.debug" />
<t-switch v-model="props.config.data.debug" />
</t-form-item>
</t-form>
</div>
@@ -33,20 +33,34 @@
import { ref, watch } from 'vue';
import { HttpServerConfig } from '../../../../src/onebot/config/config';
const defaultConfig: HttpServerConfig = {
name: 'http-server',
enable: false,
port: 3000,
host: '0.0.0.0',
enableCors: true,
enableWebsocket: true,
messagePostFormat: 'array',
token: '',
debug: false,
};
const props = defineProps<{
config: HttpServerConfig;
config: { data: HttpServerConfig };
}>();
props.config.data = { ...defaultConfig, ...props.config.data };
const messageFormatOptions = ref([
{ label: 'Array', value: 'array' },
{ label: 'String', value: 'string' },
]);
watch(
() => props.config.messagePostFormat,
() => props.config.data.messagePostFormat,
(newValue) => {
if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array';
props.config.data.messagePostFormat = 'array';
}
}
);

View File

@@ -0,0 +1,73 @@
<template>
<div>
<t-form labelAlign="left">
<t-form-item label="启用">
<t-switch v-model="props.config.data.enable" />
</t-form-item>
<t-form-item label="端口">
<t-input v-model.number="props.config.data.port" type="number" />
</t-form-item>
<t-form-item label="主机">
<t-input v-model="props.config.data.host" type="text" />
</t-form-item>
<t-form-item label="报告自身消息">
<t-switch v-model="props.config.data.reportSelfMessage" />
</t-form-item>
<t-form-item label="启用 CORS">
<t-switch v-model="props.config.data.enableCors" />
</t-form-item>
<t-form-item label="启用 WS">
<t-switch v-model="props.config.data.enableWebsocket" />
</t-form-item>
<t-form-item label="消息格式">
<t-select v-model="props.config.data.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="props.config.data.token" type="text" />
</t-form-item>
<t-form-item label="调试模式">
<t-switch v-model="props.config.data.debug" />
</t-form-item>
</t-form>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { HttpSseServerConfig } from '../../../../src/onebot/config/config';
const defaultConfig: HttpSseServerConfig = {
name: 'http-sse-server',
enable: false,
port: 3000,
host: '0.0.0.0',
enableCors: true,
enableWebsocket: true,
messagePostFormat: 'array',
token: '',
debug: false,
reportSelfMessage: false,
};
const props = defineProps<{
config: { data: HttpSseServerConfig };
}>();
props.config.data = { ...defaultConfig, ...props.config.data };
const messageFormatOptions = ref([
{ label: 'Array', value: 'array' },
{ label: 'String', value: 'string' },
]);
watch(
() => props.config.data.messagePostFormat,
(newValue) => {
if (newValue !== 'array' && newValue !== 'string') {
props.config.data.messagePostFormat = 'array';
}
}
);
</script>
<style scoped></style>

View File

@@ -2,25 +2,25 @@
<div>
<t-form labelAlign="left">
<t-form-item label="启用">
<t-switch v-model="config.enable" />
<t-switch v-model="props.config.data.enable" />
</t-form-item>
<t-form-item label="URL">
<t-input v-model="config.url" />
<t-input v-model="props.config.data.url" />
</t-form-item>
<t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
<t-select v-model="props.config.data.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="报告自身消息">
<t-switch v-model="config.reportSelfMessage" />
<t-switch v-model="props.config.data.reportSelfMessage" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" />
<t-input v-model="props.config.data.token" />
</t-form-item>
<t-form-item label="调试模式">
<t-switch v-model="config.debug" />
<t-switch v-model="props.config.data.debug" />
</t-form-item>
<t-form-item label="心跳间隔">
<t-input v-model.number="config.heartInterval" type="number" />
<t-input v-model.number="props.config.data.heartInterval" type="number" />
</t-form-item>
</t-form>
</div>
@@ -30,20 +30,34 @@
import { ref, watch } from 'vue';
import { WebsocketClientConfig } from '../../../../src/onebot/config/config';
const defaultConfig: WebsocketClientConfig = {
name: 'websocket-client',
enable: false,
url: 'ws://localhost:8082',
messagePostFormat: 'array',
reportSelfMessage: false,
reconnectInterval: 5000,
token: '',
debug: false,
heartInterval: 30000,
};
const props = defineProps<{
config: WebsocketClientConfig;
config: { data: WebsocketClientConfig };
}>();
props.config.data = { ...defaultConfig, ...props.config.data };
const messageFormatOptions = ref([
{ label: 'Array', value: 'array' },
{ label: 'String', value: 'string' },
]);
watch(
() => props.config.messagePostFormat,
() => props.config.data.messagePostFormat,
(newValue) => {
if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array';
props.config.data.messagePostFormat = 'array';
}
}
);

View File

@@ -2,31 +2,31 @@
<div>
<t-form labelAlign="left">
<t-form-item label="启用">
<t-switch v-model="config.enable" />
<t-switch v-model="props.config.data.enable" />
</t-form-item>
<t-form-item label="主机">
<t-input v-model="config.host" />
<t-input v-model="props.config.data.host" />
</t-form-item>
<t-form-item label="端口">
<t-input v-model.number="config.port" type="number" />
<t-input v-model.number="props.config.data.port" type="number" />
</t-form-item>
<t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
<t-select v-model="props.config.data.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="上报自身消息">
<t-switch v-model="config.reportSelfMessage" />
<t-switch v-model="props.config.data.reportSelfMessage" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" />
<t-input v-model="props.config.data.token" />
</t-form-item>
<t-form-item label="强制推送事件">
<t-switch v-model="config.enableForcePushEvent" />
<t-switch v-model="props.config.data.enableForcePushEvent" />
</t-form-item>
<t-form-item label="调试模式">
<t-switch v-model="config.debug" />
<t-switch v-model="props.config.data.debug" />
</t-form-item>
<t-form-item label="心跳间隔">
<t-input v-model.number="config.heartInterval" type="number" />
<t-input v-model.number="props.config.data.heartInterval" type="number" />
</t-form-item>
</t-form>
</div>
@@ -36,20 +36,35 @@
import { ref, watch } from 'vue';
import { WebsocketServerConfig } from '../../../../src/onebot/config/config';
const defaultConfig: WebsocketServerConfig = {
name: 'websocket-server',
enable: false,
host: '0.0.0.0',
port: 3001,
messagePostFormat: 'array',
reportSelfMessage: false,
token: '',
enableForcePushEvent: true,
debug: false,
heartInterval: 30000,
};
const props = defineProps<{
config: WebsocketServerConfig;
config: { data: WebsocketServerConfig };
}>();
props.config.data = { ...defaultConfig, ...props.config.data };
const messageFormatOptions = ref([
{ label: 'Array', value: 'array' },
{ label: 'String', value: 'string' },
]);
watch(
() => props.config.messagePostFormat,
() => props.config.data.messagePostFormat,
(newValue) => {
if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array';
props.config.data.messagePostFormat = 'array';
}
}
);

View File

@@ -2,7 +2,7 @@
"name": "napcat",
"private": true,
"type": "module",
"version": "4.2.51",
"version": "4.4.3",
"scripts": {
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
@@ -37,13 +37,13 @@
"@typescript-eslint/parser": "^8.3.0",
"ajv": "^8.13.0",
"async-mutex": "^0.5.0",
"commander": "^12.1.0",
"commander": "^13.0.0",
"cors": "^2.8.5",
"eslint": "^9.14.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"fast-xml-parser": "^4.3.6",
"file-type": "^19.0.0",
"file-type": "^20.0.0",
"globals": "^15.12.0",
"image-size": "^1.1.1",
"typescript": "^5.3.3",

107
src/common/cancel-task.ts Normal file
View File

@@ -0,0 +1,107 @@
export type TaskExecutor<T> = (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void, onCancel: (callback: () => void) => void) => void;
export class CancelableTask<T> {
private promise: Promise<T>;
private cancelCallback: (() => void) | null = null;
private isCanceled = false;
private cancelListeners: Array<() => void> = [];
constructor(executor: TaskExecutor<T>) {
this.promise = new Promise<T>((resolve, reject) => {
const onCancel = (callback: () => void) => {
this.cancelCallback = callback;
};
executor(
(value) => {
if (!this.isCanceled) {
resolve(value);
}
},
(reason) => {
if (!this.isCanceled) {
reject(reason);
}
},
onCancel
);
});
}
public cancel() {
if (this.cancelCallback) {
this.cancelCallback();
}
this.isCanceled = true;
this.cancelListeners.forEach(listener => listener());
}
public isTaskCanceled(): boolean {
return this.isCanceled;
}
public onCancel(listener: () => void) {
this.cancelListeners.push(listener);
}
public then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
): Promise<TResult1 | TResult2> {
return this.promise.then(onfulfilled, onrejected);
}
public catch<TResult = never>(
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null
): Promise<T | TResult> {
return this.promise.catch(onrejected);
}
public finally(onfinally?: (() => void) | undefined | null): Promise<T> {
return this.promise.finally(onfinally);
}
[Symbol.asyncIterator]() {
return {
next: () => this.promise.then(value => ({ value, done: true })),
};
}
}
async function demoAwait() {
const executor: TaskExecutor<number> = (resolve, reject, onCancel) => {
let count = 0;
const intervalId = setInterval(() => {
count++;
console.log(`Task is running... Count: ${count}`);
if (count === 5) {
clearInterval(intervalId);
resolve(count);
}
}, 1000);
onCancel(() => {
clearInterval(intervalId);
console.log('Task has been canceled.');
reject(new Error('Task was canceled'));
});
};
const task = new CancelableTask(executor);
task.onCancel(() => {
console.log('Cancel listener triggered.');
});
setTimeout(() => {
task.cancel(); // 取消任务
}, 6000);
try {
const result = await task;
console.log(`Task completed with result: ${result}`);
} catch (error) {
console.error('Task failed:', error);
}
}

22
src/common/decorator.ts Normal file
View File

@@ -0,0 +1,22 @@
// decoratorAsyncMethod(this,function,wrapper)
async function decoratorMethod<T, R>(
target: T,
method: () => Promise<R>,
wrapper: (result: R) => Promise<any>,
executeImmediately: boolean = true
): Promise<any> {
const execute = async () => {
try {
const result = await method.call(target);
return wrapper(result);
} catch (error) {
return Promise.reject(error instanceof Error ? error : new Error(String(error)));
}
};
if (executeImmediately) {
return execute();
} else {
return execute;
}
}

43
src/common/fall-back.ts Normal file
View File

@@ -0,0 +1,43 @@
type Handler<T> = () => T | Promise<T>;
type Checker<T> = (result: T) => T | Promise<T>;
export class Fallback<T> {
private handlers: Handler<T>[] = [];
private checker: Checker<T>;
constructor(checker?: Checker<T>) {
this.checker = checker || (async (result: T) => result);
}
add(handler: Handler<T>): this {
this.handlers.push(handler);
return this;
}
// 执行处理程序链
async run(): Promise<T> {
const errors: Error[] = [];
for (const handler of this.handlers) {
try {
const result = await handler();
const data = await this.checker(result);
if (data) {
return data;
}
} catch (error) {
console.log(error);
errors.push(error instanceof Error ? error : new Error(String(error)));
}
}
throw new AggregateError(errors, 'All handlers failed');
}
}
export class FallbackUtil {
static boolchecker<T>(value: T, condition: boolean): T {
if (condition) {
return value;
} else {
throw new Error('Condition is false, throwing error');
}
}
}

121
src/common/file-uuid.ts Normal file
View File

@@ -0,0 +1,121 @@
import { Peer } from '@/core';
import { randomUUID } from 'crypto';
class TimeBasedCache<K, V> {
private cache = new Map<K, { value: V, timestamp: number, frequency: number }>();
private keyList = new Set<K>();
private operationCount = 0;
constructor(private maxCapacity: number, private ttl: number = 30 * 1000 * 60, private cleanupCount: number = 10) {}
public put(key: K, value: V): void {
const timestamp = Date.now();
const cacheEntry = { value, timestamp, frequency: 1 };
this.cache.set(key, cacheEntry);
this.keyList.add(key);
this.operationCount++;
if (this.keyList.size > this.maxCapacity) this.evict();
if (this.operationCount >= this.cleanupCount) this.cleanup(this.cleanupCount);
}
public get(key: K): V | undefined {
const entry = this.cache.get(key);
if (entry && Date.now() - entry.timestamp < this.ttl) {
entry.timestamp = Date.now();
entry.frequency++;
this.operationCount++;
if (this.operationCount >= this.cleanupCount) this.cleanup(this.cleanupCount);
return entry.value;
} else {
this.deleteKey(key);
}
return undefined;
}
private cleanup(count: number): void {
const currentTime = Date.now();
let cleaned = 0;
for (const key of this.keyList) {
if (cleaned >= count) break;
const entry = this.cache.get(key);
if (entry && currentTime - entry.timestamp >= this.ttl) {
this.deleteKey(key);
cleaned++;
}
}
this.operationCount = 0; // 重置操作计数器
}
private deleteKey(key: K): void {
this.cache.delete(key);
this.keyList.delete(key);
}
private evict(): void {
while (this.keyList.size > this.maxCapacity) {
let oldestKey: K | undefined;
let minFrequency = Infinity;
for (const key of this.keyList) {
const entry = this.cache.get(key);
if (entry && entry.frequency < minFrequency) {
minFrequency = entry.frequency;
oldestKey = key;
}
}
if (oldestKey !== undefined) this.deleteKey(oldestKey);
}
}
}
interface FileUUIDData {
peer: Peer;
modelId?: string;
fileId?: string;
msgId?: string;
elementId?: string;
fileUUID?: string;
}
class FileUUIDManager {
private cache: TimeBasedCache<string, FileUUIDData>;
constructor(ttl: number) {
this.cache = new TimeBasedCache<string, FileUUIDData>(5000, ttl);
}
public encode(data: FileUUIDData, endString: string = "", customUUID?: string): string {
const uuid = customUUID ? customUUID : randomUUID().replace(/-/g, '') + endString;
this.cache.put(uuid, data);
return uuid;
}
public decode(uuid: string): FileUUIDData | undefined {
return this.cache.get(uuid);
}
}
export class FileNapCatOneBotUUIDWrap {
private manager: FileUUIDManager;
constructor(ttl: number = 86400000) {
this.manager = new FileUUIDManager(ttl);
}
public encodeModelId(peer: Peer, modelId: string, fileId: string, fileUUID: string = "", endString: string = "", customUUID?: string): string {
return this.manager.encode({ peer, modelId, fileId, fileUUID }, endString, customUUID);
}
public decodeModelId(uuid: string): FileUUIDData | undefined {
return this.manager.decode(uuid);
}
public encode(peer: Peer, msgId: string, elementId: string, fileUUID: string = "", customUUID?: string): string {
return this.manager.encode({ peer, msgId, elementId, fileUUID }, "", customUUID);
}
public decode(uuid: string): FileUUIDData | undefined {
return this.manager.decode(uuid);
}
}
export const FileNapCatOneBotUUID = new FileNapCatOneBotUUIDWrap();

View File

@@ -181,28 +181,28 @@ export async function uriToLocalFile(dir: string, uri: string, filename: string
const filePath = path.join(dir, filename);
switch (UriType) {
case FileUriType.Local: {
const fileExt = path.extname(HandledUri);
const localFileName = path.basename(HandledUri, fileExt) + fileExt;
const tempFilePath = path.join(dir, filename + fileExt);
fs.copyFileSync(HandledUri, tempFilePath);
return { success: true, errMsg: '', fileName: localFileName, path: tempFilePath };
}
case FileUriType.Local: {
const fileExt = path.extname(HandledUri);
const localFileName = path.basename(HandledUri, fileExt) + fileExt;
const tempFilePath = path.join(dir, filename + fileExt);
fs.copyFileSync(HandledUri, tempFilePath);
return { success: true, errMsg: '', fileName: localFileName, path: tempFilePath };
}
case FileUriType.Remote: {
const buffer = await httpDownload({ url: HandledUri, headers: headers });
fs.writeFileSync(filePath, buffer, { flag: 'wx' });
return { success: true, errMsg: '', fileName: filename, path: filePath };
}
case FileUriType.Remote: {
const buffer = await httpDownload({ url: HandledUri, headers: headers });
fs.writeFileSync(filePath, buffer);
return { success: true, errMsg: '', fileName: filename, path: filePath };
}
case FileUriType.Base64: {
const base64 = HandledUri.replace(/^base64:\/\//, '');
const base64Buffer = Buffer.from(base64, 'base64');
fs.writeFileSync(filePath, base64Buffer, { flag: 'wx' });
return { success: true, errMsg: '', fileName: filename, path: filePath };
}
case FileUriType.Base64: {
const base64 = HandledUri.replace(/^base64:\/\//, '');
const base64Buffer = Buffer.from(base64, 'base64');
fs.writeFileSync(filePath, base64Buffer);
return { success: true, errMsg: '', fileName: filename, path: filePath };
}
default:
return { success: false, errMsg: `识别URL失败, uri= ${uri}`, fileName: '', path: '' };
default:
return { success: false, errMsg: `识别URL失败, uri= ${uri}`, fileName: '', path: '' };
}
}

View File

@@ -1,7 +1,7 @@
import path from 'node:path';
import fs from 'fs';
import os from 'node:os';
import { Peer, QQLevel } from '@/core';
import { QQLevel } from '@/core';
export async function solveProblem<T extends (...arg: any[]) => any>(func: T, ...args: Parameters<T>): Promise<ReturnType<T> | undefined> {
return new Promise<ReturnType<T> | undefined>((resolve) => {
@@ -24,81 +24,6 @@ export async function solveAsyncProblem<T extends (...args: any[]) => Promise<an
});
}
export class FileNapCatOneBotUUID {
static encodeModelId(peer: Peer, modelId: string, fileId: string, fileUUID: string = "", endString: string = ""): string {
const data = `NapCatOneBot|ModelIdFile|${peer.chatType}|${peer.peerUid}|${modelId}|${fileId}|${fileUUID}`;
//前四个字节塞data长度
const length = Buffer.alloc(4 + data.length);
length.writeUInt32BE(data.length * 2, 0);//储存data的hex长度
length.write(data, 4);
return length.toString('hex') + endString;
}
static decodeModelId(uuid: string): undefined | {
peer: Peer,
modelId: string,
fileId: string,
fileUUID?: string
} {
//前四个字节是data长度
const length = Buffer.from(uuid.slice(0, 8), 'hex').readUInt32BE(0);
//根据length计算需要读取的长度
const dataId = uuid.slice(8, 8 + length);
//hex还原为string
const realData = Buffer.from(dataId, 'hex').toString();
if (!realData.startsWith('NapCatOneBot|ModelIdFile|')) return undefined;
const data = realData.split('|');
if (data.length < 6) return undefined; // compatibility requirement
const [, , chatType, peerUid, modelId, fileId, fileUUID = undefined] = data;
return {
peer: {
chatType: +chatType,
peerUid: peerUid,
},
modelId,
fileId,
fileUUID
};
}
static encode(peer: Peer, msgId: string, elementId: string, fileUUID: string = "", endString: string = ""): string {
const data = `NapCatOneBot|MsgFile|${peer.chatType}|${peer.peerUid}|${msgId}|${elementId}|${fileUUID}`;
//前四个字节塞data长度
//一个字节8位 一个ascii字符1字节 一个hex字符4位 表示一个ascii字符需要两个hex字符
const length = Buffer.alloc(4 + data.length);
length.writeUInt32BE(data.length * 2, 0);
length.write(data, 4);
return length.toString('hex') + endString;
}
static decode(uuid: string): undefined | {
peer: Peer,
msgId: string,
elementId: string,
fileUUID?: string
} {
//前四个字节是data长度
const length = Buffer.from(uuid.slice(0, 8), 'hex').readUInt32BE(0);
//根据length计算需要读取的长度
const dataId = uuid.slice(8, 8 + length);
//hex还原为string
const realData = Buffer.from(dataId, 'hex').toString();
if (!realData.startsWith('NapCatOneBot|MsgFile|')) return undefined;
const data = realData.split('|');
if (data.length < 6) return undefined; // compatibility requirement
const [, , chatType, peerUid, msgId, elementId, fileUUID = undefined] = data;
return {
peer: {
chatType: +chatType,
peerUid: peerUid,
},
msgId,
elementId,
fileUUID
};
}
}
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@@ -78,7 +78,7 @@ class MessageUniqueWrapper {
private readonly msgDataMap: LimitedHashTable<string, number>;
private readonly msgIdMap: LimitedHashTable<string, number>;
constructor(maxMap: number = 1000) {
constructor(maxMap: number = 5000) {
this.msgIdMap = new LimitedHashTable<string, number>(maxMap);
this.msgDataMap = new LimitedHashTable<string, number>(maxMap);
}

View File

@@ -1,126 +0,0 @@
import https from 'node:https';
import { napCatVersion } from './version';
import os from 'os';
export class UmamiTraceCore {
napcatVersion = napCatVersion;
qqversion = '1.0.0';
guid = 'default-user';
heartbeatInterval: NodeJS.Timeout | null = null;
website: string = '1fabb2b1-c3a3-4416-b1be-31e2cbdce978';
referrer: string = 'https://trace.napneko.icu/';
hostname: string = 'trace.napneko.icu';
ua: string = '';
init(qqversion: string, guid: string) {
this.qqversion = qqversion;
let UaList = {
'linux': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML like Gecko) Chrome/124.0.0.0 Safari/537.36 PTST/240508.140043',
'win32': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.2128.93 Safari/537.36',
'darwin': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36',
};
try {
if (process.platform === 'win32') {
const ntVersion = os.release();
UaList.win32 = `Mozilla/5.0 (Windows NT ${ntVersion}; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.2128.93 Safari/537.36`;
} else if (process.platform === 'darwin') {
const macVersion = os.release();
UaList.darwin = `Mozilla/5.0 (Macintosh; Intel Mac OS X ${macVersion}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36`;
}
} catch (error) {
this.ua = UaList.win32;
}
this.ua = UaList[process.platform as keyof typeof UaList] || UaList.win32;
this.identifyUser(guid);
this.startHeartbeat();
}
identifyUser(guid: string) {
this.guid = guid;
const data = {
napcat_version: this.napcatVersion,
qq_version: this.qqversion,
guid: guid
};
this.sendRequest({ website: this.website, ...data }, 'identify');
}
sendEvent(event: string, data?: object) {
const env = process.env;
const language = env.LANG || env.LANGUAGE || env.LC_ALL || env.LC_MESSAGES;
const payload = {
name: event,
hostname: this.hostname,
referrer: this.referrer,
website: this.website,
language: language || 'es-US',
napcat_version: this.napcatVersion,
qq_version: this.qqversion,
...data
};
this.sendRequest(payload);
}
sendTrace(eventName: string) {
const payload = {
website: this.website,
hostname: this.hostname,
title: 'NapCat ' + this.napcatVersion,
url: `/${this.qqversion}/${this.napcatVersion}/${eventName}`,
referrer: this.referrer,
};
this.sendRequest(payload);
}
sendRequest(payload: object, type = 'event') {
const options = {
hostname: '104.19.42.72', // 固定 IP 或者从 hostUrl 获取
port: 443,
path: '/api/send',
method: 'POST',
headers: {
"Host": "umami.napneko.icu",
"Content-Type": "application/json",
"User-Agent": this.ua
}
};
const request = https.request(options, (res) => {
res.on('error', (error) => {
});
res.on('data', (data) => {
});
});
request.write(JSON.stringify({ type, payload }));
request.end();
}
startHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
}
this.heartbeatInterval = setInterval(() => {
this.sendEvent('heartbeat', {
title: 'NapCat ' + this.napcatVersion,
language: process.env.LANG || 'en-US',
url: `/${this.qqversion}/${this.napcatVersion}/heartbeat`,
version: this.napcatVersion,
qq_version: this.qqversion,
user_id: this.guid
});
}, 5 * 60 * 1000);
}
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
}
export const UmamiTrace = new UmamiTraceCore();

View File

@@ -1 +1 @@
export const napCatVersion = '4.2.51';
export const napCatVersion = '4.4.3';

View File

@@ -462,7 +462,7 @@ export class NTQQFileApi {
rkeyData.private_rkey = tempRkeyData.private_rkey;
rkeyData.online_rkey = tempRkeyData.expired_time > Date.now() / 1000;
} catch (e) {
this.context.logger.logError('获取rkey失败 Fallback Old Mode', e);
this.context.logger.logDebug('获取rkey失败 Fallback Old Mode', e);
}
}
@@ -474,7 +474,7 @@ export class NTQQFileApi {
if (rkeyData.online_rkey) {
return IMAGE_HTTP_HOST_NT + `/download?appid=${appid}&fileid=${imageFileId}&rkey=${rkey}`;
}
return IMAGE_HTTP_HOST + `/download?appid=${appid}&fileid=${imageFileId}&rkey=${rkey}`;
return IMAGE_HTTP_HOST + `/download?appid=${appid}&fileid=${imageFileId}&rkey=${rkey}&spec=0`;
}
private getImageUrlFromMd5(fileMd5: string | undefined, md5HexStr: string | undefined): string {

View File

@@ -44,7 +44,7 @@ export class NTQQGroupApi {
async initCache() {
for (const group of await this.getGroups(true)) {
this.refreshGroupMemberCache(group.groupCode).then().catch();
this.refreshGroupMemberCache(group.groupCode, false).then().catch(e => this.context.logger.logError(e));
}
}
@@ -126,14 +126,23 @@ export class NTQQGroupApi {
return this.context.session.getGroupService().getAllMemberList(groupCode, forced);
}
async refreshGroupMemberCache(groupCode: string) {
try {
const members = await this.getGroupMemberAll(groupCode, true);
this.groupMemberCache.set(groupCode, members.result.infos);
} catch (e) {
this.context.logger.logError(`刷新群成员缓存失败, 群号: ${groupCode}, 错误: ${e}`);
async refreshGroupMemberCache(groupCode: string, isWait = true) {
const updateCache = async () => {
try {
const members = await this.getGroupMemberAll(groupCode, true);
this.groupMemberCache.set(groupCode, members.result.infos);
} catch (e) {
this.context.logger.logError(`刷新群成员缓存失败, 群号: ${groupCode}, 错误: ${e}`);
}
};
if (isWait) {
await updateCache();
} else {
updateCache();
}
return this.groupMemberCache;
return this.groupMemberCache.get(groupCode);
}
async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
@@ -143,7 +152,7 @@ export class NTQQGroupApi {
// 获取群成员缓存
let members = this.groupMemberCache.get(groupCodeStr);
if (!members) {
members = (await this.refreshGroupMemberCache(groupCodeStr)).get(groupCodeStr);
members = (await this.refreshGroupMemberCache(groupCodeStr, true));
}
const getMember = () => {
@@ -157,7 +166,7 @@ export class NTQQGroupApi {
let member = getMember();
// 如果缓存中不存在该成员,尝试刷新缓存
if (!member) {
members = (await this.refreshGroupMemberCache(groupCodeStr)).get(groupCodeStr);
members = (await this.refreshGroupMemberCache(groupCodeStr, true));
member = getMember();
}
return member;

View File

@@ -2,8 +2,7 @@ import { ModifyProfileParams, User, UserDetailSource } from '@/core/types';
import { RequestUtil } from '@/common/request';
import { InstanceContext, NapCatCore, ProfileBizType } from '..';
import { solveAsyncProblem } from '@/common/helper';
import { promisify } from 'node:util';
import { LRUCache } from '@/common/lru-cache';
import { Fallback, FallbackUtil } from '@/common/fall-back';
export class NTQQUserApi {
context: InstanceContext;
@@ -108,6 +107,19 @@ export class NTQQUserApi {
return retUser;
}
async getUserDetailInfoV2(uid: string): Promise<User> {
const fallback = new Fallback<User>((user) => FallbackUtil.boolchecker(user, user !== undefined && user.uin !== '0'))
.add(() => this.fetchUserDetailInfo(uid, UserDetailSource.KDB))
.add(() => this.fetchUserDetailInfo(uid, UserDetailSource.KSERVER));
const retUser = await fallback.run().then(async (user) => {
if (user && user.uin === '0') {
user.uin = await this.core.apis.UserApi.getUidByUinV2(uid) ?? '0';
}
return user;
});
return retUser;
}
async modifySelfProfile(param: ModifyProfileParams) {
return this.context.session.getProfileService().modifyDesktopMiniProfile(param);
}
@@ -169,46 +181,34 @@ export class NTQQUserApi {
return skey;
}
async getUidByUinV2(Uin: string) {
if (!Uin) {
async getUidByUinV2(uin: string) {
if (!uin) {
return '';
}
const services = [
() => this.context.session.getUixConvertService().getUid([Uin]).then((data) => data.uidInfo.get(Uin)).catch(() => undefined),
() => promisify<string, string[], Map<string, string>>
(this.context.session.getProfileService().getUidByUin)('FriendsServiceImpl', [Uin]).then((data) => data.get(Uin)).catch(() => undefined),
() => this.context.session.getGroupService().getUidByUins([Uin]).then((data) => data.uids.get(Uin)).catch(() => undefined),
() => this.getUserDetailInfoByUin(Uin).then((data) => data.detail.uid).catch(() => undefined),
];
let uid: string | undefined = undefined;
for (const service of services) {
uid = await service();
if (uid && uid.indexOf('*') == -1 && uid !== '') {
break;
}
}
const fallback =
new Fallback<string | undefined>((uid) => FallbackUtil.boolchecker(uid, uid !== undefined && uid.indexOf('*') === -1 && uid !== ''))
.add(() => this.context.session.getUixConvertService().getUid([uin]).then((data) => data.uidInfo.get(uin)))
.add(() => this.context.session.getProfileService().getUidByUin('FriendsServiceImpl', [uin]).get(uin))
.add(() => this.context.session.getGroupService().getUidByUins([uin]).then((data) => data.uids.get(uin)))
.add(() => this.getUserDetailInfoByUin(uin).then((data) => data.detail.uid));
const uid = await fallback.run().catch(() => '');
return uid ?? '';
}
async getUinByUidV2(Uid: string) {
if (!Uid) {
async getUinByUidV2(uid: string) {
if (!uid) {
return '0';
}
const services = [
() => this.context.session.getUixConvertService().getUin([Uid]).then((data) => data.uinInfo.get(Uid)).catch(() => undefined),
() => this.context.session.getGroupService().getUinByUids([Uid]).then((data) => data.uins.get(Uid)).catch(() => undefined),
() => promisify<string, string[], Map<string, string>>
(this.context.session.getProfileService().getUinByUid)('FriendsServiceImpl', [Uid]).then((data) => data.get(Uid)).catch(() => undefined),
() => this.core.apis.FriendApi.getBuddyIdMap(true).then((data) => data.getKey(Uid)).catch(() => undefined),
() => this.getUserDetailInfo(Uid).then((data) => data.uin).catch(() => undefined),
];
let uin: string | undefined = undefined;
for (const service of services) {
uin = await service();
if (uin && uin !== '0' && uin !== '') {
break;
}
}
const fallback = new Fallback<string | undefined>((uin) => FallbackUtil.boolchecker(uin, uin !== undefined && uin !== '0' && uin !== ''))
.add(() => this.context.session.getUixConvertService().getUin([uid]).then((data) => data.uinInfo.get(uid)))
.add(() => this.context.session.getProfileService().getUinByUid('FriendsServiceImpl', [uid]).get(uid))
.add(() => this.context.session.getGroupService().getUinByUids([uid]).then((data) => data.uins.get(uid)))
.add(() => this.getUserDetailInfo(uid).then((data) => data.uin));
const uin = await fallback.run().catch(() => '0');
return uin ?? '0';
}

View File

@@ -8,7 +8,7 @@ import {
WebHonorType,
} from '@/core';
import { NapCatCore } from '..';
import { createReadStream, readFileSync, statSync } from 'node:fs';
import { readFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { basename } from 'node:path';

View File

@@ -158,5 +158,33 @@
"6.9.63-30899": {
"appid": 537263820,
"qua": "V1_MAC_NQ_6.9.63_30899_GW_B"
},
"9.9.17-31219": {
"appid": 537266450,
"qua": "V1_WIN_NQ_9.9.17_31219_GW_B"
},
"9.9.17-31245": {
"appid": 537266450,
"qua": "V1_WIN_NQ_9.9.17_31245_GW_B"
},
"3.2.15-31245": {
"appid": 537266485,
"qua": "V1_LNX_NQ_3.2.15_31245_GW_B"
},
"6.9.63-31245": {
"appid": 537266474,
"qua": "V1_MAC_NQ_6.9.63_31245_GW_B"
},
"3.2.15-31363": {
"appid": 537266535,
"qua": "V1_LNX_NQ_3.2.15_31363_GW_B"
},
"6.9.65-31363": {
"appid": 537266524,
"qua": "V1_MAC_NQ_6.9.65_31363_GW_B"
},
"9.9.17-31363": {
"appid": 537266500,
"qua": "V1_WIN_NQ_9.9.17_31363_GW_B"
}
}
}

View File

@@ -2406,6 +2406,193 @@
"AQLid": "211",
"QHide": "1",
"EMCode": "309"
},
{
"QSid": "424",
"QDes": "/续标识",
"IQLid": "424",
"AQLid": "424",
"EMCode": "10424",
"QHide": "0",
"AniStickerType": 1,
"AniStickerPackId": "1",
"AniStickerId": "52"
},
{
"QSid": "415",
"QDes": "/划龙舟",
"IQLid": "415",
"AQLid": "415",
"EMCode": "10415",
"QHide": "0",
"AniStickerType": 3,
"AniStickerPackId": "1",
"AniStickerId": "43"
},
{
"QSid": "416",
"QDes": "/中龙舟",
"IQLid": "416",
"AQLid": "416",
"EMCode": "10416",
"QHide": "1",
"AniStickerType": 3,
"AniStickerPackId": "1",
"AniStickerId": "44"
},
{
"QSid": "417",
"QDes": "/大龙舟",
"IQLid": "417",
"AQLid": "417",
"EMCode": "10417",
"QHide": "1",
"AniStickerType": 3,
"AniStickerPackId": "1",
"AniStickerId": "45"
},
{
"QSid": "425",
"QDes": "/求放过",
"IQLid": "425",
"AQLid": "425",
"EMCode": "10425",
"QHide": "0",
"AniStickerType": 1,
"AniStickerPackId": "1",
"AniStickerId": "53"
},
{
"QSid": "427",
"QDes": "/偷感",
"IQLid": "427",
"AQLid": "427",
"EMCode": "10427",
"QHide": "0",
"AniStickerType": 1,
"AniStickerPackId": "1",
"AniStickerId": "55"
},
{
"QSid": "426",
"QDes": "/玩火",
"IQLid": "426",
"AQLid": "426",
"EMCode": "10426",
"QHide": "0",
"AniStickerType": 1,
"AniStickerPackId": "1",
"AniStickerId": "54"
},
{
"QSid": "419",
"QDes": "/火车",
"IQLid": "419",
"AQLid": "419",
"EMCode": "10419",
"QHide": "0",
"AniStickerType": 3,
"AniStickerPackId": "1",
"AniStickerId": "47"
},
{
"QSid": "420",
"QDes": "/中火车",
"IQLid": "420",
"AQLid": "420",
"EMCode": "10420",
"QHide": "1",
"AniStickerType": 3,
"AniStickerPackId": "1",
"AniStickerId": "48"
},
{
"QSid": "421",
"QDes": "/大火车",
"IQLid": "421",
"AQLid": "421",
"EMCode": "10421",
"QHide": "1",
"AniStickerType": 3,
"AniStickerPackId": "1",
"AniStickerId": "49"
},
{
"QSid": "429",
"QDes": "/蛇年快乐",
"IQLid": "429",
"AQLid": "429",
"EMCode": "10429",
"QHide": "0",
"AniStickerType": 3,
"AniStickerPackId": "1",
"AniStickerId": "56"
},
{
"QSid": "430",
"QDes": "/蛇身",
"IQLid": "430",
"AQLid": "430",
"EMCode": "10430",
"QHide": "1",
"AniStickerType": 3,
"AniStickerPackId": "1",
"AniStickerId": "57"
},
{
"QSid": "431",
"QDes": "/蛇尾",
"IQLid": "431",
"AQLid": "431",
"EMCode": "10431",
"QHide": "1",
"AniStickerType": 3,
"AniStickerPackId": "1",
"AniStickerId": "56"
},
{
"QSid": "428",
"QDes": "/收到",
"IQLid": "428",
"AQLid": "428",
"EMCode": "10428",
"QHide": "0",
"AniStickerType": 0,
"AniStickerPackId": "0",
"AniStickerId": "0"
},
{
"QSid": "422",
"QDes": "/粽于等到你",
"IQLid": "422",
"AQLid": "422",
"EMCode": "10422",
"QHide": "1",
"AniStickerType": 1,
"AniStickerPackId": "1",
"AniStickerId": "50"
},
{
"QSid": "423",
"QDes": "/复兴号",
"IQLid": "423",
"AQLid": "423",
"EMCode": "10423",
"QHide": "1",
"AniStickerType": 1,
"AniStickerPackId": "1",
"AniStickerId": "51"
},
{
"QSid": "432",
"QDes": "/灵蛇献瑞",
"IQLid": "432",
"AQLid": "432",
"EMCode": "10432",
"QHide": "1",
"AniStickerType": 1,
"AniStickerPackId": "1",
"AniStickerId": "59"
}
],
"emoji": [

View File

@@ -202,5 +202,49 @@
"6.9.63-30899-arm64": {
"send": "41DCBD8",
"recv": "41DF3F0"
},
"9.9.17-31219-x64": {
"send": "39C1350",
"recv": "39C5784"
},
"9.9.17-31245-x64": {
"send": "39C1350",
"recv": "39C5784"
},
"6.9.63.31245-x64": {
"send": "4720A40",
"recv": "47232AC"
},
"6.9.63-31245-arm64": {
"send": "41DCBD8",
"recv": "422D4E8"
},
"3.2.15-31245-x64": {
"send": "A550F80",
"recv": "A554880"
},
"3.2.15-31245-arm64": {
"send": "71BEBB8",
"recv": "71C23F0"
},
"9.9.17-31363-x64": {
"send": "39C1910",
"recv": "39C5d44"
},
"3.2.15-31363-x64": {
"send": "A554500",
"recv": "A557E00"
},
"3.2.15-31363-arm64": {
"send": "71BFD48",
"recv": "71C3580"
},
"6.9.65.31363-x64": {
"send": "4720E80",
"recv": "47236EC"
},
"6.9.65.31363-arm64": {
"send": "422CEF8",
"recv": "422F710"
}
}
}

View File

@@ -15,6 +15,10 @@ export class RkeyManager {
private_rkey: '',
expired_time: 0,
};
private failureCount: number = 0;
private lastFailureTimestamp: number = 0;
private readonly FAILURE_LIMIT: number = 8;
private readonly ONE_DAY: number = 24 * 60 * 60 * 1000;
constructor(serverUrl: string[], logger: LogWrapper) {
this.logger = logger;
@@ -22,11 +26,21 @@ export class RkeyManager {
}
async getRkey() {
const now = new Date().getTime();
if (now - this.lastFailureTimestamp > this.ONE_DAY) {
this.failureCount = 0; // 重置失败计数器
}
if (this.failureCount >= this.FAILURE_LIMIT) {
this.logger.logError(`[Rkey] 服务存在异常, 图片使用FallBack机制`);
throw new Error('获取rkey失败次数过多请稍后再试');
}
if (this.isExpired()) {
try {
await this.refreshRkey();
} catch (e) {
throw new Error(`获取rkey失败: ${e}`);//外抛
throw new Error(`${e}`);//外抛
}
}
return this.rkeyData;
@@ -34,7 +48,6 @@ export class RkeyManager {
isExpired(): boolean {
const now = new Date().getTime() / 1000;
// console.log(`now: ${now}, expired_time: ${this.rkeyData.expired_time}`);
return now > this.rkeyData.expired_time;
}
@@ -48,14 +61,17 @@ export class RkeyManager {
private_rkey: temp.private_rkey.slice(6),
expired_time: temp.expired_time
};
this.failureCount = 0;
return;
} catch (e) {
this.logger.logError(`[Rkey] Get Rkey ${url} Error `, e);
this.logger.logError(`[Rkey] 异常服务 ${url} 异常 / `, e);
this.failureCount++;
this.lastFailureTimestamp = new Date().getTime();
//是否为最后一个url
if (url === this.serverUrl[this.serverUrl.length - 1]) {
throw new Error(`获取rkey失败: ${e}`);//外抛
}
}
}
}
}
}

View File

@@ -30,7 +30,6 @@ import os from 'node:os';
import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
import { proxiedListenerOf } from '@/common/proxy-handler';
import { NTQQPacketApi } from './apis/packet';
import { UmamiTrace } from '@/common/umami';
export * from './wrapper';
export * from './types';
export * from './services';
@@ -155,11 +154,6 @@ export class NapCatCore {
const msgListener = new NodeIKernelMsgListener();
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
if (this.context.workingEnv === NapCatCoreWorkingEnv.Framework) {
UmamiTrace.sendEvent('framework/kickoff');
} else {
UmamiTrace.sendEvent('shell/kickoff');
}
// 下线通知
this.context.logger.logError('[KickedOffLine] [' + Info.tipsTitle + '] ' + Info.tipsDesc);
this.selfInfo.online = false;

View File

@@ -1,8 +1,9 @@
import { LRUCache } from "@/common/lru-cache";
import crypto, { createHash } from "crypto";
import { PacketContext } from "@/core/packet/context/packetContext";
import { OidbPacket, PacketHexStr } from "@/core/packet/transformer/base";
import { LogStack } from "@/core/packet/context/clientContext";
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
import { PacketLogger } from "@/core/packet/context/loggerContext";
export interface RecvPacket {
type: string, // 仅recv
@@ -27,13 +28,15 @@ function randText(len: number): string {
export abstract class IPacketClient {
protected readonly context: PacketContext;
protected readonly napcore: NapCoreContext;
protected readonly logger: PacketLogger;
protected readonly cb = new LRUCache<string, (json: RecvPacketData) => Promise<void>>(500); // trace_id-type callback
logStack: LogStack;
available: boolean = false;
protected constructor(context: PacketContext, logStack: LogStack) {
this.context = context;
protected constructor(napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) {
this.napcore = napCore;
this.logger = logger;
this.logStack = logStack;
}
@@ -81,7 +84,7 @@ export abstract class IPacketClient {
const md5 = crypto.createHash('md5').update(data).digest('hex');
const trace_id = (randText(4) + md5 + data).slice(0, data.length / 2);
return this.sendCommand(cmd, data, trace_id, rsp, 20000, async () => {
await this.context.napcore.sendSsoCmdReqByContend(cmd, trace_id);
await this.napcore.sendSsoCmdReqByContend(cmd, trace_id);
});
}

View File

@@ -5,8 +5,9 @@ import fs from "fs";
import { IPacketClient } from "@/core/packet/client/baseClient";
import { constants } from "node:os";
import { LRUCache } from "@/common/lru-cache";
import { PacketContext } from "@/core/packet/context/packetContext";
import { LogStack } from "@/core/packet/context/clientContext";
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
import { PacketLogger } from "@/core/packet/context/loggerContext";
// 0 send 1 recv
export interface NativePacketExportType {
@@ -19,8 +20,8 @@ export class NativePacketClient extends IPacketClient {
private readonly MoeHooExport: { exports: NativePacketExportType } = { exports: {} };
private readonly sendEvent = new LRUCache<number, string>(500); // seq->trace_id
constructor(context: PacketContext, logStack: LogStack) {
super(context, logStack);
constructor(napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) {
super(napCore, logger, logStack);
}
check(): boolean {

View File

@@ -1,7 +1,8 @@
import { Data, WebSocket, ErrorEvent } from "ws";
import { IPacketClient, RecvPacket } from "@/core/packet/client/baseClient";
import { PacketContext } from "@/core/packet/context/packetContext";
import { LogStack } from "@/core/packet/context/clientContext";
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
import { PacketLogger } from "@/core/packet/context/loggerContext";
export class WsPacketClient extends IPacketClient {
private websocket: WebSocket | null = null;
@@ -13,15 +14,15 @@ export class WsPacketClient extends IPacketClient {
private isInitialized: boolean = false;
private initPayload: { pid: number, recv: string, send: string } | null = null;
constructor(context: PacketContext, logStack: LogStack) {
super(context, logStack);
this.clientUrl = this.context.napcore.config.packetServer
? this.clientUrlWrap(this.context.napcore.config.packetServer)
constructor(napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) {
super(napCore, logger, logStack);
this.clientUrl = this.napcore.config.packetServer
? this.clientUrlWrap(this.napcore.config.packetServer)
: this.clientUrlWrap('127.0.0.1:8083');
}
check(): boolean {
if (!this.context.napcore.config.packetServer) {
if (!this.napcore.config.packetServer) {
this.logStack.pushLogWarn(`wsPacketClient 未配置服务器地址`);
return false;
}
@@ -67,7 +68,7 @@ export class WsPacketClient extends IPacketClient {
this.websocket.onopen = () => {
this.available = true;
this.reconnectAttempts = 0;
this.context.logger.info(`wsPacketClient 已连接到 ${this.clientUrl}`);
this.logger.info(`wsPacketClient 已连接到 ${this.clientUrl}`);
if (!this.isInitialized && this.initPayload) {
this.websocket!.send(JSON.stringify({
action: 'init',
@@ -79,15 +80,15 @@ export class WsPacketClient extends IPacketClient {
};
this.websocket.onclose = () => {
this.available = false;
this.context.logger.warn(`WebSocket 连接关闭,尝试重连...`);
this.logger.warn(`WebSocket 连接关闭,尝试重连...`);
reject(new Error('WebSocket 连接关闭'));
};
this.websocket.onmessage = (event) => this.handleMessage(event.data).catch(err => {
this.context.logger.error(`处理消息时出错: ${err}`);
this.logger.error(`处理消息时出错: ${err}`);
});
this.websocket.onerror = (event: ErrorEvent) => {
this.available = false;
this.context.logger.error(`WebSocket 出错: ${event.message}`);
this.logger.error(`WebSocket 出错: ${event.message}`);
this.websocket?.close();
reject(new Error(`WebSocket 出错: ${event.message}`));
};
@@ -106,7 +107,7 @@ export class WsPacketClient extends IPacketClient {
const event = this.cb.get(`${trace_id_md5}${action}`);
if (event) await event(json.data);
} catch (error) {
this.context.logger.error(`解析ws消息时出错: ${(error as Error).message}`);
this.logger.error(`解析ws消息时出错: ${(error as Error).message}`);
}
}
}

View File

@@ -23,9 +23,7 @@ export class PacketClientSession {
get operation() {
return this.context.operation;
}
get client() {
return this.context.client;
}
// TODO: global message element adapter (?
get msgConverter() {
return this.context.msgConverter;

View File

@@ -1,17 +1,17 @@
import { PacketContext } from "@/core/packet/context/packetContext";
import { IPacketClient } from "@/core/packet/client/baseClient";
import { NativePacketClient } from "@/core/packet/client/nativeClient";
import { WsPacketClient } from "@/core/packet/client/wsClient";
import { OidbPacket } from "@/core/packet/transformer/base";
import { PacketLogger } from "@/core/packet/context/loggerContext";
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
type clientPriority = {
[key: number]: (context: PacketContext, logStack: LogStack) => IPacketClient;
[key: number]: (napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) => IPacketClient;
}
const clientPriority: clientPriority = {
10: (context: PacketContext, logStack: LogStack) => new NativePacketClient(context, logStack),
1: (context: PacketContext, logStack: LogStack) => new WsPacketClient(context, logStack),
10: (napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) => new NativePacketClient(napCore, logger, logStack),
1: (napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) => new WsPacketClient(napCore, logger, logStack),
};
export class LogStack {
@@ -51,13 +51,15 @@ export class LogStack {
}
export class PacketClientContext {
private readonly context: PacketContext;
private readonly napCore: NapCoreContext;
private readonly logger: PacketLogger;
private readonly logStack: LogStack;
private readonly _client: IPacketClient;
constructor(context: PacketContext) {
this.context = context;
this.logStack = new LogStack(context.logger);
constructor(napCore: NapCoreContext, logger: PacketLogger) {
this.napCore = napCore;
this.logger = logger;
this.logStack = new LogStack(logger);
this._client = this.newClient();
}
@@ -79,23 +81,23 @@ export class PacketClientContext {
}
private newClient(): IPacketClient {
const prefer = this.context.napcore.config.packetBackend;
const prefer = this.napCore.config.packetBackend;
let client: IPacketClient | null;
switch (prefer) {
case "native":
this.context.logger.info("使用指定的 NativePacketClient 作为后端");
client = new NativePacketClient(this.context, this.logStack);
this.logger.info("使用指定的 NativePacketClient 作为后端");
client = new NativePacketClient(this.napCore, this.logger, this.logStack);
break;
case "frida":
this.context.logger.info("[Core] [Packet] 使用指定的 FridaPacketClient 作为后端");
client = new WsPacketClient(this.context, this.logStack);
this.logger.info("[Core] [Packet] 使用指定的 FridaPacketClient 作为后端");
client = new WsPacketClient(this.napCore, this.logger, this.logStack);
break;
case "auto":
case undefined:
client = this.judgeClient();
break;
default:
this.context.logger.error(`未知的PacketBackend ${prefer},请检查配置文件!`);
this.logger.error(`未知的PacketBackend ${prefer},请检查配置文件!`);
client = null;
}
if (!client?.check()) {
@@ -110,7 +112,7 @@ export class PacketClientContext {
private judgeClient(): IPacketClient {
const sortedClients = Object.entries(clientPriority)
.map(([priority, clientFactory]) => {
const client = clientFactory(this.context, this.logStack);
const client = clientFactory(this.napCore, this.logger, this.logStack);
const score = +priority * +client.check();
return { client, score };
})
@@ -120,7 +122,7 @@ export class PacketClientContext {
if (!selectedClient) {
throw new Error("[Core] [Packet] 无可用的后端NapCat.Packet将不会加载");
}
this.context.logger.info(`自动选择 ${selectedClient.constructor.name} 作为后端`);
this.logger.info(`自动选择 ${selectedClient.constructor.name} 作为后端`);
return selectedClient;
}
}

View File

@@ -1,12 +1,12 @@
import { LogLevel, LogWrapper } from "@/common/log";
import { PacketContext } from "@/core/packet/context/packetContext";
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
// TODO: check bind?
export class PacketLogger {
private readonly napLogger: LogWrapper;
constructor(context: PacketContext) {
this.napLogger = context.napcore.logger;
constructor(napcore: NapCoreContext) {
this.napLogger = napcore.logger;
}
private _log(level: LogLevel, ...msg: any[]): void {

View File

@@ -13,15 +13,22 @@ import { MiniAppRawData, MiniAppReqParams } from "@/core/packet/entities/miniApp
import { AIVoiceChatType } from "@/core/packet/entities/aiChat";
import { NapProtoDecodeStructType, NapProtoEncodeStructType } from "@napneko/nap-proto-core";
import { IndexNode, MsgInfo } from "@/core/packet/transformer/proto";
import { OidbPacket } from "@/core/packet/transformer/base";
import { ImageOcrResult } from "@/core/packet/entities/ocrResult";
export class PacketOperationContext {
private readonly context: PacketContext;
constructor(context: PacketContext) {
this.context = context;
}
async sendPacket<T extends boolean = false>(pkt: OidbPacket, rsp?: T): Promise<T extends true ? Buffer : void> {
return await this.context.client.sendOidbPacket(pkt, rsp);
}
async GroupPoke(groupUin: number, uin: number) {
const req = trans.SendPoke.build(groupUin, uin);
const req = trans.SendPoke.build(uin, groupUin);
await this.context.client.sendOidbPacket(req);
}
@@ -90,6 +97,46 @@ export class PacketOperationContext {
});
}
async UploadImage(img: PacketMsgPicElement) {
await this.context.highway.uploadImage({
chatType: ChatType.KCHATTYPEC2C,
peerUid: this.context.napcore.basicInfo.uid
}, img);
const index = img.msgInfo?.msgInfoBody?.at(0)?.index;
if (!index) {
throw new Error('img.msgInfo?.msgInfoBody![0].index! is undefined');
}
return await this.GetImageUrl(this.context.napcore.basicInfo.uid, index);
}
async GetImageUrl(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>) {
const req = trans.DownloadImage.build(selfUid, node);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.DownloadImage.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async ImageOCR(imgUrl: string) {
const req = trans.ImageOCR.build(imgUrl);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.ImageOCR.parse(resp);
return {
texts: res.ocrRspBody.textDetections.map((item) => {
return {
text: item.detectedText,
confidence: item.confidence,
coordinates: item.polygon.coordinates.map((c) => {
return {
x: c.x,
y: c.y
};
}),
};
}),
language: res.ocrRspBody.language
} as ImageOcrResult;
}
async UploadForwardMsg(msg: PacketMsg[], groupUin: number = 0) {
await this.UploadResources(msg, groupUin);
const req = trans.UploadForwardMsg.build(this.context.napcore.basicInfo.uid, msg, groupUin);

View File

@@ -7,19 +7,19 @@ import { PacketOperationContext } from "@/core/packet/context/operationContext";
import { PacketMsgConverter } from "@/core/packet/message/converter";
export class PacketContext {
readonly msgConverter: PacketMsgConverter;
readonly napcore: NapCoreContext;
readonly logger: PacketLogger;
readonly client: PacketClientContext;
readonly highway: PacketHighwayContext;
readonly msgConverter: PacketMsgConverter;
readonly operation: PacketOperationContext;
constructor(core: NapCatCore) {
this.napcore = new NapCoreContext(core);
this.logger = new PacketLogger(this);
this.client = new PacketClientContext(this);
this.highway = new PacketHighwayContext(this);
this.msgConverter = new PacketMsgConverter();
this.napcore = new NapCoreContext(core);
this.logger = new PacketLogger(this.napcore);
this.client = new PacketClientContext(this.napcore, this.logger);
this.highway = new PacketHighwayContext(this.napcore, this.logger, this.client);
this.operation = new PacketOperationContext(this);
}
}

View File

@@ -0,0 +1,15 @@
export interface ImageOcrResult {
texts: TextDetection[];
language: string;
}
export interface TextDetection {
text: string;
confidence: number;
coordinates: Coordinate[];
}
export interface Coordinate {
x: number;
y: number;
}

View File

@@ -1,5 +1,4 @@
import { PacketHighwayClient } from "@/core/packet/highway/client";
import { PacketContext } from "@/core/packet/context/packetContext";
import { PacketLogger } from "@/core/packet/context/loggerContext";
import FetchSessionKey from "@/core/packet/transformer/highway/FetchSessionKey";
import { int32ip2str, oidbIpv4s2HighwayIpv4s } from "@/core/packet/highway/utils";
@@ -16,6 +15,8 @@ import { NapProtoMsg } from "@napneko/nap-proto-core";
import * as proto from "@/core/packet/transformer/proto";
import * as trans from "@/core/packet/transformer";
import fs from "fs";
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
import { PacketClientContext } from "@/core/packet/context/clientContext";
export const BlockSize = 1024 * 1024;
@@ -33,23 +34,25 @@ export interface PacketHighwaySig {
}
export class PacketHighwayContext {
private readonly context: PacketContext;
private readonly napcore: NapCoreContext;
private readonly client: PacketClientContext;
protected sig: PacketHighwaySig;
protected logger: PacketLogger;
protected hwClient: PacketHighwayClient;
private cachedPrepareReq: Promise<void> | null = null;
constructor(context: PacketContext) {
this.context = context;
constructor(napcore: NapCoreContext, logger: PacketLogger, client: PacketClientContext) {
this.napcore = napcore;
this.client = client;
this.sig = {
uin: String(context.napcore.basicInfo.uin),
uid: context.napcore.basicInfo.uid,
uin: String(this.napcore.basicInfo.uin),
uid: this.napcore.basicInfo.uid,
sigSession: null,
sessionKey: null,
serverAddr: [],
};
this.logger = context.logger;
this.hwClient = new PacketHighwayClient(this.sig, context.logger);
this.logger = logger;
this.hwClient = new PacketHighwayClient(this.sig, this.logger);
}
private async checkAvailable() {
@@ -66,7 +69,7 @@ export class PacketHighwayContext {
private async prepareUpload(): Promise<void> {
this.logger.debug('[Highway] on prepareUpload!');
const packet = FetchSessionKey.build();
const req = await this.context.client.sendOidbPacket(packet, true);
const req = await this.client.sendOidbPacket(packet, true);
const rsp = FetchSessionKey.parse(req);
this.sig.sigSession = rsp.httpConn.sigSession;
this.sig.sessionKey = rsp.httpConn.sessionKey;
@@ -136,7 +139,7 @@ export class PacketHighwayContext {
private async uploadGroupImage(groupUin: number, img: PacketMsgPicElement): Promise<void> {
img.sha1 = Buffer.from(await calculateSha1(img.path)).toString('hex');
const req = UploadGroupImage.build(groupUin, img);
const resp = await this.context.client.sendOidbPacket(req, true);
const resp = await this.client.sendOidbPacket(req, true);
const preRespData = UploadGroupImage.parse(resp);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
@@ -173,7 +176,7 @@ export class PacketHighwayContext {
private async uploadC2CImage(peerUid: string, img: PacketMsgPicElement): Promise<void> {
img.sha1 = Buffer.from(await calculateSha1(img.path)).toString('hex');
const req = trans.UploadPrivateImage.build(peerUid, img);
const resp = await this.context.client.sendOidbPacket(req, true);
const resp = await this.client.sendOidbPacket(req, true);
const preRespData = trans.UploadPrivateImage.parse(resp);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
@@ -211,7 +214,7 @@ export class PacketHighwayContext {
video.fileSha1 = Buffer.from(await calculateSha1(video.filePath)).toString('hex');
video.thumbSha1 = Buffer.from(await calculateSha1(video.thumbPath)).toString('hex');
const req = trans.UploadGroupVideo.build(groupUin, video);
const resp = await this.context.client.sendOidbPacket(req, true);
const resp = await this.client.sendOidbPacket(req, true);
const preRespData = trans.UploadGroupVideo.parse(resp);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
@@ -276,7 +279,7 @@ export class PacketHighwayContext {
video.fileSha1 = Buffer.from(await calculateSha1(video.filePath)).toString('hex');
video.thumbSha1 = Buffer.from(await calculateSha1(video.thumbPath)).toString('hex');
const req = trans.UploadPrivateVideo.build(peerUid, video);
const resp = await this.context.client.sendOidbPacket(req, true);
const resp = await this.client.sendOidbPacket(req, true);
const preRespData = trans.UploadPrivateVideo.parse(resp);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
@@ -339,7 +342,7 @@ export class PacketHighwayContext {
private async uploadGroupPtt(groupUin: number, ptt: PacketMsgPttElement): Promise<void> {
ptt.fileSha1 = Buffer.from(await calculateSha1(ptt.filePath)).toString('hex');
const req = trans.UploadGroupPtt.build(groupUin, ptt);
const resp = await this.context.client.sendOidbPacket(req, true);
const resp = await this.client.sendOidbPacket(req, true);
const preRespData = trans.UploadGroupPtt.parse(resp);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
@@ -375,7 +378,7 @@ export class PacketHighwayContext {
private async uploadC2CPtt(peerUid: string, ptt: PacketMsgPttElement): Promise<void> {
ptt.fileSha1 = Buffer.from(await calculateSha1(ptt.filePath)).toString('hex');
const req = trans.UploadPrivatePtt.build(peerUid, ptt);
const resp = await this.context.client.sendOidbPacket(req, true);
const resp = await this.client.sendOidbPacket(req, true);
const preRespData = trans.UploadPrivatePtt.parse(resp);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
@@ -413,7 +416,7 @@ export class PacketHighwayContext {
file.fileMd5 = await computeMd5AndLengthWithLimit(file.filePath);
file.fileSha1 = await calculateSha1(file.filePath);
const req = trans.UploadGroupFile.build(groupUin, file);
const resp = await this.context.client.sendOidbPacket(req, true);
const resp = await this.client.sendOidbPacket(req, true);
const preRespData = trans.UploadGroupFile.parse(resp);
if (!preRespData?.upload?.boolFileExist) {
this.logger.debug(`[Highway] uploadGroupFileReq file not exist, need upload!`);
@@ -476,7 +479,7 @@ export class PacketHighwayContext {
file.fileMd5 = await computeMd5AndLengthWithLimit(file.filePath);
file.fileSha1 = await calculateSha1(file.filePath);
const req = await trans.UploadPrivateFile.build(this.sig.uid, peerUid, file);
const res = await this.context.client.sendOidbPacket(req, true);
const res = await this.client.sendOidbPacket(req, true);
const preRespData = trans.UploadPrivateFile.parse(res);
if (!preRespData.upload?.boolFileExist) {
this.logger.debug(`[Highway] uploadC2CFileReq file not exist, need upload!`);
@@ -531,7 +534,7 @@ export class PacketHighwayContext {
file.fileUuid = preRespData.upload?.uuid;
file.fileHash = preRespData.upload?.fileAddon;
const fileExistReq = trans.DownloadOfflineFile.build(file.fileUuid!, file.fileHash!, this.sig.uid, peerUid);
const fileExistRes = await this.context.client.sendOidbPacket(fileExistReq, true);
const fileExistRes = await this.client.sendOidbPacket(fileExistReq, true);
file._e37_800_rsp = trans.DownloadOfflineFile.parse(fileExistRes);
file._private_send_uid = this.sig.uid;
file._private_recv_uid = peerUid;

View File

@@ -160,10 +160,12 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
export class PacketMsgFaceElement extends IPacketMsgElement<SendFaceElement> {
faceId: number;
isLargeFace: boolean;
resultId?: string;
constructor(element: SendFaceElement) {
super(element);
this.faceId = element.faceElement.faceIndex;
this.resultId = element.faceElement.resultId;
this.isLargeFace = element.faceElement.faceType === FaceType.AniSticke;
}
@@ -176,10 +178,10 @@ export class PacketMsgFaceElement extends IPacketMsgElement<SendFaceElement> {
aniStickerPackId: "1",
aniStickerId: "8",
faceId: this.faceId,
field4: 1,
field6: "",
sourceType: 1,
resultId: this.resultId,
preview: "",
field9: 1
randomType: 1
}),
businessType: 1
}
@@ -256,6 +258,8 @@ export class PacketMsgPicElement extends IPacketMsgElement<SendPicElement> {
width: number;
height: number;
picType: PicType;
picSubType: number;
summary: string;
sha1: string | null = null;
msgInfo: NapProtoEncodeStructType<typeof MsgInfo> | null = null;
groupPicExt: NapProtoEncodeStructType<typeof CustomFace> | null = null;
@@ -270,6 +274,10 @@ export class PacketMsgPicElement extends IPacketMsgElement<SendPicElement> {
this.width = element.picElement.picWidth;
this.height = element.picElement.picHeight;
this.picType = element.picElement.picType;
this.picSubType = element.picElement.picSubType ?? 0;
this.summary = element.picElement.summary === '' ? (
element.picElement.picSubType === 0 ? '[图片]' : '[动画表情]'
) : element.picElement.summary;
}
get valid(): boolean {
@@ -288,7 +296,7 @@ export class PacketMsgPicElement extends IPacketMsgElement<SendPicElement> {
}
toPreview(): string {
return "[图片]";
return this.summary;
}
}

View File

@@ -0,0 +1,37 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
class ImageOCR extends PacketTransformer<typeof proto.OidbSvcTrpcTcp0xE07_0_Response> {
constructor() {
super();
}
build(url: string): OidbPacket {
const body = new NapProtoMsg(proto.OidbSvcTrpcTcp0xE07_0).encode(
{
version: 1,
client: 0,
entrance: 1,
ocrReqBody: {
imageUrl: url,
originMd5: "",
afterCompressMd5: "",
afterCompressFileSize: "",
afterCompressWeight: "",
afterCompressHeight: "",
isCut: false,
}
}
);
return OidbBase.build(0XEB7, 1, body, false, false);
}
parse(data: Buffer) {
const base = OidbBase.parse(data);
return new NapProtoMsg(proto.OidbSvcTrpcTcp0xE07_0_Response).decode(base.body);
}
}
export default new ImageOCR();

View File

@@ -5,3 +5,4 @@ export { default as GroupSign } from './GroupSign';
export { default as GetStrangerInfo } from './GetStrangerInfo';
export { default as SendPoke } from './SendPoke';
export { default as SetSpecialTitle } from './SetSpecialTitle';
export { default as ImageOCR } from './ImageOCR';

View File

@@ -0,0 +1,51 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoEncodeStructType, NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
import { IndexNode } from "@/core/packet/transformer/proto";
class DownloadImage extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
constructor() {
super();
}
build(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>): OidbPacket {
const body = new NapProtoMsg(proto.NTV2RichMediaReq).encode({
reqHead: {
common: {
requestId: 1,
command: 100
},
scene: {
requestType: 2,
businessType: 1,
sceneType: 1,
c2C: {
accountType: 2,
targetUid: selfUid
},
},
client: {
agentType: 2,
}
},
download: {
node: node,
download: {
video: {
busiType: 0,
sceneType: 0
}
}
}
});
return OidbBase.build(0x11C5, 200, body, true, false);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody);
}
}
export default new DownloadImage();

View File

@@ -58,8 +58,11 @@ class UploadGroupImage extends PacketTransformer<typeof proto.NTV2RichMediaResp>
compatQMsgSceneType: 2,
extBizInfo: {
pic: {
bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'),
textSummary: "Nya~", // TODO:
bizType: img.picSubType,
bytesPbReserveTroop: {
subType: img.picSubType,
},
textSummary: img.summary,
},
video: {
bytesPbReserve: Buffer.alloc(0),

View File

@@ -58,8 +58,11 @@ class UploadPrivateImage extends PacketTransformer<typeof proto.NTV2RichMediaRes
compatQMsgSceneType: 1,
extBizInfo: {
pic: {
bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'),
textSummary: "Nya~", // TODO:
bizType: img.picSubType,
bytesPbReserveC2C: {
subType: img.picSubType,
},
textSummary: img.summary,
},
video: {
bytesPbReserve: Buffer.alloc(0),

View File

@@ -11,3 +11,4 @@ export { default as UploadPrivateFile } from './UploadPrivateFile';
export { default as UploadPrivateImage } from './UploadPrivateImage';
export { default as UploadPrivatePtt } from './UploadPrivatePtt';
export { default as UploadPrivateVideo } from './UploadPrivateVideo';
export { default as DownloadImage } from './DownloadImage';

View File

@@ -29,3 +29,4 @@ export * from "./oidb/Oidb.0xEB7";
export * from "./oidb/Oidb.0xED3_1";
export * from "./oidb/Oidb.0XFE1_2";
export * from "./oidb/OidbBase";
export * from "./oidb/Oidb.0xE07";

View File

@@ -342,11 +342,11 @@ export const QBigFaceExtra = {
AniStickerPackId: ProtoField(1, ScalarType.STRING, true),
AniStickerId: ProtoField(2, ScalarType.STRING, true),
faceId: ProtoField(3, ScalarType.INT32, true),
Field4: ProtoField(4, ScalarType.INT32, true),
sourceType: ProtoField(4, ScalarType.INT32, true),
AniStickerType: ProtoField(5, ScalarType.INT32, true),
field6: ProtoField(6, ScalarType.STRING, true),
resultId: ProtoField(6, ScalarType.STRING, true),
preview: ProtoField(7, ScalarType.STRING, true),
field9: ProtoField(9, ScalarType.INT32, true),
randomType: ProtoField(9, ScalarType.INT32, true),
};
export const QSmallFaceExtra = {

View File

@@ -0,0 +1,59 @@
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
export const OidbSvcTrpcTcp0xE07_0 = {
version: ProtoField(1, ScalarType.UINT32),
client: ProtoField(2, ScalarType.UINT32),
entrance: ProtoField(3, ScalarType.UINT32),
ocrReqBody: ProtoField(10, () => OcrReqBody, true),
};
export const OcrReqBody = {
imageUrl: ProtoField(1, ScalarType.STRING),
languageType: ProtoField(2, ScalarType.UINT32),
scene: ProtoField(3, ScalarType.UINT32),
originMd5: ProtoField(10, ScalarType.STRING),
afterCompressMd5: ProtoField(11, ScalarType.STRING),
afterCompressFileSize: ProtoField(12, ScalarType.STRING),
afterCompressWeight: ProtoField(13, ScalarType.STRING),
afterCompressHeight: ProtoField(14, ScalarType.STRING),
isCut: ProtoField(15, ScalarType.BOOL),
};
export const OidbSvcTrpcTcp0xE07_0_Response = {
retCode: ProtoField(1, ScalarType.INT32),
errMsg: ProtoField(2, ScalarType.STRING),
wording: ProtoField(3, ScalarType.STRING),
ocrRspBody: ProtoField(10, () => OcrRspBody),
};
export const OcrRspBody = {
textDetections: ProtoField(1, () => TextDetection, false, true),
language: ProtoField(2, ScalarType.STRING),
requestId: ProtoField(3, ScalarType.STRING),
ocrLanguageList: ProtoField(101, ScalarType.STRING, false, true),
dstTranslateLanguageList: ProtoField(102, ScalarType.STRING, false, true),
languageList: ProtoField(103, () => Language, false, true),
afterCompressWeight: ProtoField(111, ScalarType.UINT32),
afterCompressHeight: ProtoField(112, ScalarType.UINT32),
};
export const TextDetection = {
detectedText: ProtoField(1, ScalarType.STRING),
confidence: ProtoField(2, ScalarType.UINT32),
polygon: ProtoField(3, () => Polygon),
advancedInfo: ProtoField(4, ScalarType.STRING),
};
export const Polygon = {
coordinates: ProtoField(1, () => Coordinate, false, true),
};
export const Coordinate = {
x: ProtoField(1, ScalarType.INT32),
y: ProtoField(2, ScalarType.INT32),
};
export const Language = {
languageCode: ProtoField(1, ScalarType.STRING),
languageDesc: ProtoField(2, ScalarType.STRING),
};

View File

@@ -189,8 +189,8 @@ export const VideoExtBizInfo = {
export const PicExtBizInfo = {
BizType: ProtoField(1, ScalarType.UINT32),
TextSummary: ProtoField(2, ScalarType.STRING),
BytesPbReserveC2c: ProtoField(11, ScalarType.BYTES),
BytesPbReserveTroop: ProtoField(12, ScalarType.BYTES),
BytesPbReserveC2c: ProtoField(11, () => BytesPbReserveC2c),
BytesPbReserveTroop: ProtoField(12, () => BytesPbReserveTroop),
FromScene: ProtoField(1001, ScalarType.UINT32),
ToScene: ProtoField(1002, ScalarType.UINT32),
OldFileId: ProtoField(1003, ScalarType.UINT32),
@@ -211,3 +211,27 @@ export const UploadInfo = {
FileInfo: ProtoField(1, () => FileInfo),
SubFileType: ProtoField(2, ScalarType.UINT32),
};
export const BytesPbReserveC2c = {
subType: ProtoField(1, ScalarType.UINT32),
field3: ProtoField(3, ScalarType.UINT32),
field4: ProtoField(4, ScalarType.UINT32),
field8: ProtoField(8, ScalarType.STRING),
field10: ProtoField(10, ScalarType.UINT32),
field12: ProtoField(12, ScalarType.STRING),
field18: ProtoField(18, ScalarType.STRING),
field19: ProtoField(19, ScalarType.STRING),
field20: ProtoField(20, ScalarType.BYTES),
};
export const BytesPbReserveTroop = {
subType: ProtoField(1, ScalarType.UINT32),
field3: ProtoField(3, ScalarType.UINT32),
field4: ProtoField(4, ScalarType.UINT32),
field9: ProtoField(9, ScalarType.STRING),
field10: ProtoField(10, ScalarType.UINT32),
field12: ProtoField(12, ScalarType.STRING),
field18: ProtoField(18, ScalarType.STRING),
field19: ProtoField(19, ScalarType.STRING),
field21: ProtoField(21, ScalarType.BYTES),
};

View File

@@ -40,6 +40,7 @@ export interface FaceElement {
resultId?: string;
surpriseId?: string;
randomType?: number;
chainCount?: number;
}
export interface GrayTipRovokeElement {
operatorRole: string;
@@ -348,4 +349,4 @@ export type SendShareLocationElement = SendElementBase<ElementType.SHARELOCATION
export type SendMessageElement = SendTextElement | SendPttElement |
SendPicElement | SendReplyElement | SendFaceElement | SendMarketFaceElement | SendFileElement |
SendVideoElement | SendArkElement | SendMarkdownElement | SendShareLocationElement;
SendVideoElement | SendArkElement | SendMarkdownElement | SendShareLocationElement;

View File

@@ -9,4 +9,5 @@ export * from './sign';
export * from './element';
export * from './constant';
export * from './graytip';
export * from './emoji';
export * from './emoji';
export * from './service';

35
src/core/types/service.ts Normal file
View File

@@ -0,0 +1,35 @@
export enum LoginErrorCode {
KLOGINERRORACCOUNTNOTUIN = 140022018,
KLOGINERRORACCOUNTORPASSWORDERROR = 140022013,
KLOGINERRORBLACKACCOUNT = 150022021,
KLOGINERRORDEFAULT = 140022000,
KLOGINERROREXPIRETICKET = 140022014,
KLOGINERRORFROZEN = 140022005,
KLOGINERRORILLAGETICKET = 140022016,
KLOGINERRORINVAILDCOOKIE = 140022012,
KLOGINERRORINVALIDPARAMETER = 140022001,
KLOGINERRORKICKEDTICKET = 140022015,
KLOGINERRORMUTIPLEPASSWORDINCORRECT = 150022029,
KLOGINERRORNEEDUPDATE = 140022004,
KLOGINERRORNEEDVERIFYREALNAME = 140022019,
KLOGINERRORNEWDEVICE = 140022010,
KLOGINERRORNICEACCOUNTEXPIRED = 150022020,
KLOGINERRORNICEACCOUNTPARENTCHILDEXPIRED = 150022025,
KLOGINERRORPASSWORD = 2,
KLOGINERRORPROOFWATER = 140022008,
KLOGINERRORPROTECT = 140022006,
KLOGINERRORREFUSEPASSOWRDLOGIN = 140022009,
KLOGINERRORREMINDCANAELLATEDSTATUS = 150022028,
KLOGINERRORSCAN = 1,
KLOGINERRORSCCESS = 0,
KLOGINERRORSECBEAT = 140022017,
KLOGINERRORSMSINVALID = 150022026,
KLOGINERRORSTRICK = 140022007,
KLOGINERRORSYSTEMFAILED = 140022002,
KLOGINERRORTGTGTEXCHAGEA1FORBID = 150022027,
KLOGINERRORTIMEOUTRETRY = 140022003,
KLOGINERRORTOOMANYTIMESTODAY = 150022023,
KLOGINERRORTOOOFTEN = 150022022,
KLOGINERRORUNREGISTERED = 150022024,
KLOGINERRORUNUSUALDEVICE = 140022011,
}

View File

@@ -18,7 +18,7 @@ export interface BuddyCategoryType {
export interface CoreInfo {
uid: string;
uin: string;
nick: string;
nick?: string;
remark: string;
}

View File

@@ -9,7 +9,6 @@ import { NodeIKernelLoginService } from '@/core/services';
import { NodeIQQNTWrapperSession, WrapperNodeApi } from '@/core/wrapper';
import { InitWebUi, WebUiConfig } from '@/webui';
import { NapCatOneBot11Adapter } from '@/onebot';
import { UmamiTrace } from '@/common/umami';
//Framework ES入口文件
export async function getWebUiUrl() {
@@ -26,7 +25,6 @@ export async function NCoreInitFramework(
console.log('NapCat Framework App Loading...');
process.on('uncaughtException', (err) => {
UmamiTrace.sendEvent('framework/error', { name: err.name });
console.log('[NapCat] [Error] Unhandled Exception:', err.message);
});
@@ -38,10 +36,6 @@ export async function NCoreInitFramework(
const logger = new LogWrapper(pathWrapper.logsPath);
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVesion());
let guid = loginService.getMachineGuid();
UmamiTrace.init(basicInfoWrapper.getFullQQVesion(), guid);
UmamiTrace.sendTrace('framework/boot');
UmamiTrace.sendEvent('framework/login');
//直到登录成功后,执行下一步
const selfInfo = await new Promise<SelfInfo>((resolveSelfInfo) => {
const loginListener = new NodeIKernelLoginListener();

View File

@@ -0,0 +1 @@
import '@/universal/napcat';

View File

@@ -2,6 +2,7 @@ import { ActionName, BaseCheckResult } from './router';
import Ajv, { ErrorObject, ValidateFunction } from 'ajv';
import { NapCatCore } from '@/core';
import { NapCatOneBot11Adapter, OB11Return } from '@/onebot';
import { NetworkAdapterConfig } from '../config/config';
export class OB11Response {
private static createResponse<T>(data: T, status: string, retcode: number, message: string = '', echo: any = null): OB11Return<T> {
@@ -55,13 +56,13 @@ export abstract class OneBotAction<PayloadType, ReturnDataType> {
return { valid: true };
}
public async handle(payload: PayloadType, adaptername: string): Promise<OB11Return<ReturnDataType | null>> {
public async handle(payload: PayloadType, adaptername: string, config: NetworkAdapterConfig): Promise<OB11Return<ReturnDataType | null>> {
const result = await this.check(payload);
if (!result.valid) {
return OB11Response.error(result.message, 400);
}
try {
const resData = await this._handle(payload, adaptername);
const resData = await this._handle(payload, adaptername, config);
return OB11Response.ok(resData);
} catch (e: any) {
this.core.context.logger.logError('发生错误', e);
@@ -69,13 +70,13 @@ export abstract class OneBotAction<PayloadType, ReturnDataType> {
}
}
public async websocketHandle(payload: PayloadType, echo: any, adaptername: string): Promise<OB11Return<ReturnDataType | null>> {
public async websocketHandle(payload: PayloadType, echo: any, adaptername: string, config: NetworkAdapterConfig): Promise<OB11Return<ReturnDataType | null>> {
const result = await this.check(payload);
if (!result.valid) {
return OB11Response.error(result.message, 1400, echo);
}
try {
const resData = await this._handle(payload, adaptername);
const resData = await this._handle(payload, adaptername, config);
return OB11Response.ok(resData, echo);
} catch (e: any) {
this.core.context.logger.logError('发生错误', e);
@@ -83,5 +84,5 @@ export abstract class OneBotAction<PayloadType, ReturnDataType> {
}
}
abstract _handle(payload: PayloadType, adaptername: string): PromiseLike<ReturnDataType>;
abstract _handle(payload: PayloadType, adaptername: string, config: NetworkAdapterConfig): Promise<ReturnDataType>;
}

View File

@@ -14,22 +14,23 @@ class OCRImageBase extends OneBotAction<Payload, any> {
payloadSchema = SchemaData;
async _handle(payload: Payload) {
const { path, success } = (await uriToLocalFile(this.core.NapCatTempPath, payload.image));
const { path, success } = await uriToLocalFile(this.core.NapCatTempPath, payload.image);
if (!success) {
throw new Error(`OCR ${payload.image}失败,image字段可能格式不正确`);
throw new Error(`OCR ${payload.image}失败, image字段可能格式不正确`);
}
if (path) {
await checkFileExist(path, 5000); // 避免崩溃
const ret = await this.core.apis.SystemApi.ocrImage(path);
fs.unlink(path, () => { });
if (!ret) {
throw new Error(`OCR ${payload.image}失败`);
try {
await checkFileExist(path, 5000); // 避免崩溃
const ret = await this.core.apis.SystemApi.ocrImage(path);
if (!ret) {
throw new Error(`OCR ${payload.image}失败`);
}
return ret.result;
} finally {
fs.unlink(path, () => { });
}
return ret.result;
}
fs.unlink(path, () => { });
throw new Error(`OCR ${payload.image}失败,文件可能不存在`);
throw new Error(`OCR ${payload.image}失败, 文件可能不存在`);
}
}
@@ -39,4 +40,4 @@ export class OCRImage extends OCRImageBase {
export class IOCRImage extends OCRImageBase {
actionName = ActionName.IOCRImage;
}
}

View File

@@ -15,7 +15,7 @@ export class SendPacket extends GetPacketStatusDepends<Payload, any> {
actionName = ActionName.SendPacket;
async _handle(payload: Payload) {
const rsp = typeof payload.rsp === 'boolean' ? payload.rsp : payload.rsp === 'true';
const data = await this.core.apis.PacketApi.pkt.client.sendOidbPacket({ cmd: payload.cmd, data: payload.data as any }, rsp);
const data = await this.core.apis.PacketApi.pkt.operation.sendPacket({ cmd: payload.cmd, data: payload.data as any }, rsp);
return typeof data === 'object' ? data.toString('hex') : undefined;
}
}

View File

@@ -17,7 +17,7 @@ class SetGroupSignBase extends GetPacketStatusDepends<Payload, any> {
}
export class SetGroupSign extends SetGroupSignBase {
actionName = ActionName.SendGroupSign;
actionName = ActionName.SetGroupSign;
}
export class SendGroupSign extends SetGroupSignBase {

View File

@@ -1,6 +1,6 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import fs from 'fs/promises';
import { FileNapCatOneBotUUID } from '@/common/helper';
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
import { ActionName } from '@/onebot/action/router';
import { OB11MessageImage, OB11MessageVideo } from '@/onebot/types';
import { Static, Type } from '@sinclair/typebox';
@@ -28,7 +28,7 @@ export class GetFileBase extends OneBotAction<GetFilePayload, GetFileResponse> {
payload.file ||= payload.file_id || '';
//接收消息标记模式
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file);
if (contextMsgFile) {
if (contextMsgFile && contextMsgFile.msgId && contextMsgFile.elementId) {
const { peer, msgId, elementId } = contextMsgFile;
const downloadPath = await this.core.apis.FileApi.downloadMedia(msgId, peer.chatType, peer.peerUid, elementId, '', '');
const rawMessage = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgId]))?.msgList
@@ -68,7 +68,7 @@ export class GetFileBase extends OneBotAction<GetFilePayload, GetFileResponse> {
//群文件模式
const contextModelIdFile = FileNapCatOneBotUUID.decodeModelId(payload.file);
if (contextModelIdFile) {
if (contextModelIdFile && contextModelIdFile.modelId) {
const { peer, modelId } = contextModelIdFile;
const downloadPath = await this.core.apis.FileApi.downloadFileForModelId(peer, modelId, '');
const res: GetFileResponse = {

View File

@@ -1,5 +1,5 @@
import { ActionName } from '@/onebot/action/router';
import { FileNapCatOneBotUUID } from "@/common/helper";
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus";
import { Static, Type } from '@sinclair/typebox';

View File

@@ -1,7 +1,7 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { FileNapCatOneBotUUID } from '@/common/helper';
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
@@ -16,7 +16,7 @@ export class DeleteGroupFile extends OneBotAction<Payload, any> {
payloadSchema = SchemaData;
async _handle(payload: Payload) {
const data = FileNapCatOneBotUUID.decodeModelId(payload.file_id);
if (!data) throw new Error('Invalid file_id');
if (!data || !data.fileId) throw new Error('Invalid file_id');
return await this.core.apis.GroupApi.delGroupFile(payload.group_id.toString(), [data.fileId]);
}
}

View File

@@ -3,8 +3,9 @@ import { OB11Message } from '@/onebot';
import { ActionName } from '@/onebot/action/router';
import { ChatType } from '@/core/types';
import { MessageUnique } from '@/common/message-unique';
import { AdapterConfigWrap } from '@/onebot/config/config';
import { Static, Type } from '@sinclair/typebox';
import { NetworkAdapterConfig } from '@/onebot/config/config';
interface Response {
messages: OB11Message[];
@@ -23,7 +24,7 @@ export default class GetFriendMsgHistory extends OneBotAction<Payload, Response>
actionName = ActionName.GetFriendMsgHistory;
payloadSchema = SchemaData;
async _handle(payload: Payload, adapter: string): Promise<Response> {
async _handle(payload: Payload, adapter: string, config: NetworkAdapterConfig): Promise<Response> {
//处理参数
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
@@ -42,10 +43,9 @@ export default class GetFriendMsgHistory extends OneBotAction<Payload, Response>
await Promise.all(msgList.map(async msg => {
msg.id = MessageUnique.createUniqueMsgId({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId);
}));
const network = Object.values(this.obContext.configLoader.configData.network) as Array<AdapterConfigWrap>;
//烘焙消息
const ob11MsgList = (await Promise.all(
msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg, network.flat().find(e => e.name === adapter)?.messagePostFormat ?? 'array')))
msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg, config.messagePostFormat)))
).filter(msg => msg !== undefined);
return { 'messages': ob11MsgList };
}

View File

@@ -3,8 +3,8 @@ import { OB11Message } from '@/onebot';
import { ActionName } from '@/onebot/action/router';
import { ChatType, Peer } from '@/core/types';
import { MessageUnique } from '@/common/message-unique';
import { AdapterConfigWrap } from '@/onebot/config/config';
import { Static, Type } from '@sinclair/typebox';
import { NetworkAdapterConfig } from '@/onebot/config/config';
interface Response {
messages: OB11Message[];
@@ -25,7 +25,7 @@ export default class GoCQHTTPGetGroupMsgHistory extends OneBotAction<Payload, Re
actionName = ActionName.GoCQHTTP_GetGroupMsgHistory;
payloadSchema = SchemaData;
async _handle(payload: Payload, adapter: string): Promise<Response> {
async _handle(payload: Payload, adapter: string, config: NetworkAdapterConfig): Promise<Response> {
//处理参数
const isReverseOrder = typeof payload.reverseOrder === 'string' ? payload.reverseOrder === 'true' : !!payload.reverseOrder;
const peer: Peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: payload.group_id.toString() };
@@ -41,11 +41,9 @@ export default class GoCQHTTPGetGroupMsgHistory extends OneBotAction<Payload, Re
await Promise.all(msgList.map(async msg => {
msg.id = MessageUnique.createUniqueMsgId({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId);
}));
const network = Object.values(this.obContext.configLoader.configData.network) as Array<AdapterConfigWrap>;
//烘焙消息
const msgFormat = network.flat().find(e => e.name === adapter)?.messagePostFormat ?? 'array';
const ob11MsgList = (await Promise.all(
msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg, msgFormat)))
msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg, config.messagePostFormat)))
).filter(msg => msg !== undefined);
return { 'messages': ob11MsgList };
}

View File

@@ -13,7 +13,7 @@ type Payload = Static<typeof SchemaData>;
export default class GoCQHTTPGetStrangerInfo extends OneBotAction<Payload, OB11User> {
actionName = ActionName.GoCQHTTP_GetStrangerInfo;
payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<OB11User> {
const user_id = payload.user_id.toString();
const extendData = await this.core.apis.UserApi.getUserDetailInfoByUin(user_id);

View File

@@ -3,8 +3,8 @@ import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { MessageUnique } from '@/common/message-unique';
import crypto from 'crypto';
import { AdapterConfigWrap } from '@/onebot/config/config';
import { Static, Type } from '@sinclair/typebox';
import { NetworkAdapterConfig } from '@/onebot/config/config';
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
@@ -27,9 +27,7 @@ export class GetGroupEssence extends OneBotAction<Payload, any> {
};
}
async _handle(payload: Payload, adapter: string) {
const network = Object.values(this.obContext.configLoader.configData.network) as Array<AdapterConfigWrap>;
const msgFormat = network.flat().find(e => e.name === adapter)?.messagePostFormat ?? 'array';
async _handle(payload: Payload, adapter: string, config: NetworkAdapterConfig) {
const msglist = (await this.core.apis.WebApi.getGroupEssenceMsgAll(payload.group_id.toString())).flatMap((e) => e.data.msg_list);
if (!msglist) {
throw new Error('获取失败');
@@ -50,7 +48,7 @@ export class GetGroupEssence extends OneBotAction<Payload, any> {
operator_nick: msg.add_digest_nick,
message_id: message_id,
operator_time: msg.add_digest_time,
content: (await this.obContext.apis.MsgApi.parseMessage(rawMessage, msgFormat))?.message
content: (await this.obContext.apis.MsgApi.parseMessage(rawMessage, config.messagePostFormat))?.message
};
}
const msgTempData = JSON.stringify({

View File

@@ -26,20 +26,29 @@ class GetGroupMemberInfo extends OneBotAction<Payload, OB11GroupMember> {
return uid;
}
async _handle(payload: Payload) {
const isNocache = this.parseBoolean(payload.no_cache ?? true);
const uid = await this.getUid(payload.user_id);
const groupMember = this.core.apis.GroupApi.groupMemberCache.get(payload.group_id.toString())?.get(uid);
let [member, info] = await Promise.all([
private async getGroupMemberInfo(payload: Payload, uid: string, isNocache: boolean) {
const groupMemberCache = this.core.apis.GroupApi.groupMemberCache.get(payload.group_id.toString());
const groupMember = groupMemberCache?.get(uid);
const [member, info] = await Promise.all([
this.core.apis.GroupApi.getGroupMemberEx(payload.group_id.toString(), uid, isNocache),
this.core.apis.UserApi.getUserDetailInfo(uid),
]);
if (!member || !groupMember) throw new Error(`群(${payload.group_id})成员${payload.user_id}不存在`);
if (info) {
member = { ...groupMember, ...member, ...info };
} else {
return info ? { ...groupMember, ...member, ...info } : member;
}
async _handle(payload: Payload) {
const isNocache = this.parseBoolean(payload.no_cache ?? true);
const uid = await this.getUid(payload.user_id);
const member = await this.getGroupMemberInfo(payload, uid, isNocache);
if (!member) {
this.core.context.logger.logDebug(`获取群成员详细信息失败, 只能返回基础信息`);
}
return OB11Construct.groupMember(payload.group_id.toString(), member);
}
}

View File

@@ -3,6 +3,7 @@ import { OB11Construct } from '@/onebot/helper/data';
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
import { GroupMember } from '@/core';
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
@@ -17,25 +18,32 @@ export class GetGroupMemberList extends OneBotAction<Payload, OB11GroupMember[]>
async _handle(payload: Payload) {
const groupIdStr = payload.group_id.toString();
const noCache = payload.no_cache ? this.stringToBoolean(payload.no_cache) : false;
const noCache = this.parseBoolean(payload.no_cache ?? false);
const groupMembers = await this.getGroupMembers(groupIdStr, noCache);
const _groupMembers = await Promise.all(
Array.from(groupMembers.values()).map(item =>
OB11Construct.groupMember(groupIdStr, item)
)
);
return Array.from(new Map(_groupMembers.map(member => [member.user_id, member])).values());
}
private parseBoolean(value: boolean | string): boolean {
return typeof value === 'string' ? value === 'true' : value;
}
private async getGroupMembers(groupIdStr: string, noCache: boolean): Promise<Map<string, GroupMember>> {
const memberCache = this.core.apis.GroupApi.groupMemberCache;
let groupMembers = memberCache.get(groupIdStr);
if (noCache || !groupMembers) {
this.core.apis.GroupApi.refreshGroupMemberCache(groupIdStr).then().catch();
//下次刷新
groupMembers = memberCache.get(groupIdStr);
const data = this.core.apis.GroupApi.refreshGroupMemberCache(groupIdStr, true).then().catch();
groupMembers = memberCache.get(groupIdStr) || (await data);
if (!groupMembers) {
throw new Error(`Failed to get group member list for group ${groupIdStr}`);
}
}
const memberPromises = Array.from(groupMembers.values()).map(item =>
OB11Construct.groupMember(groupIdStr, item)
);
const _groupMembers = await Promise.all(memberPromises);
const MemberMap = new Map(_groupMembers.map(member => [member.user_id, member]));
return Array.from(MemberMap.values());
return groupMembers;
}
stringToBoolean(str: string | boolean): boolean {
return typeof str === 'boolean' ? str : str.toLowerCase() === "true";
}
}
}

View File

@@ -14,6 +14,6 @@ export class GroupPoke extends GetPacketStatusDepends<Payload, any> {
payloadSchema = SchemaData;
async _handle(payload: Payload) {
await this.core.apis.PacketApi.pkt.operation.GroupPoke(+payload.user_id, +payload.group_id);
await this.core.apis.PacketApi.pkt.operation.GroupPoke(+payload.group_id, +payload.user_id);
}
}

View File

@@ -21,15 +21,9 @@ export class SendGroupAiRecord extends GetPacketStatusDepends<Payload, {
payloadSchema = SchemaData;
async _handle(payload: Payload) {
const rawRsp = await this.core.apis.PacketApi.pkt.operation.GetAiVoice(+payload.group_id, payload.character, payload.text, AIVoiceChatType.Sound);
const url = await this.core.apis.PacketApi.pkt.operation.GetGroupPttUrl(+payload.group_id, rawRsp.msgInfoBody[0].index);
const { path, errMsg, success } = (await uriToLocalFile(this.core.NapCatTempPath, url));
if (!success) {
throw new Error(errMsg);
}
const peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: payload.group_id.toString() } as Peer;
const element = await this.core.apis.FileApi.createValidSendPttElement(path);
const sendRes = await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(peer, [element], [path]);
return { message_id: sendRes.id ?? -1 };
await this.core.apis.PacketApi.pkt.operation.GetAiVoice(+payload.group_id, payload.character, payload.text, AIVoiceChatType.Sound);
return {
message_id: 0 // can't get message_id from GetAiVoice
};
}
}

View File

@@ -19,7 +19,7 @@ export default class SetGroupAddRequest extends OneBotAction<Payload, null> {
const flag = payload.flag.toString();
const approve = payload.approve?.toString() !== 'false';
const reason = payload.reason ?? ' ';
let invite_notify = this.obContext.apis.MsgApi.notifyGroupInvite.get(flag);
const invite_notify = this.obContext.apis.MsgApi.notifyGroupInvite.get(flag);
const notify = invite_notify ?? await this.findNotify(flag);
if (!notify) {
throw new Error('No such request');

View File

@@ -3,8 +3,8 @@ import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { MessageUnique } from '@/common/message-unique';
import { RawMessage } from '@/core';
import { AdapterConfigWrap } from '@/onebot/config/config';
import { Static, Type } from '@sinclair/typebox';
import { NetworkAdapterConfig } from '@/onebot/config/config';
export type ReturnDataType = OB11Message
@@ -18,10 +18,8 @@ class GetMsg extends OneBotAction<Payload, OB11Message> {
actionName = ActionName.GetMsg;
payloadSchema = SchemaData;
async _handle(payload: Payload, adapter: string) {
async _handle(payload: Payload, adapter: string, config: NetworkAdapterConfig) {
// log("history msg ids", Object.keys(msgHistory));
const network = Object.values(this.obContext.configLoader.configData.network) as Array<AdapterConfigWrap>;
const msgFormat = network.flat().find(e => e.name === adapter)?.messagePostFormat ?? 'array';
if (!payload.message_id) {
throw Error('参数message_id不能为空');
}
@@ -38,7 +36,7 @@ class GetMsg extends OneBotAction<Payload, OB11Message> {
} else {
msg = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgIdWithPeer?.MsgId || payload.message_id.toString()])).msgList[0];
}
const retMsg = await this.obContext.apis.MsgApi.parseMessage(msg, msgFormat);
const retMsg = await this.obContext.apis.MsgApi.parseMessage(msg, config.messagePostFormat);
if (!retMsg) throw Error('消息为空');
try {
retMsg.message_id = MessageUnique.createUniqueMsgId(peer, msg.msgId)!;

View File

@@ -15,9 +15,9 @@ export class SendPoke extends GetPacketStatusDepends<Payload, any> {
async _handle(payload: Payload) {
if (payload.group_id) {
this.core.apis.PacketApi.pkt.operation.GroupPoke(+payload.group_id, +payload.user_id);
await this.core.apis.PacketApi.pkt.operation.GroupPoke(+payload.group_id, +payload.user_id);
} else {
this.core.apis.PacketApi.pkt.operation.FriendPoke(+payload.user_id);
await this.core.apis.PacketApi.pkt.operation.FriendPoke(+payload.user_id);
}
}
}
}

View File

@@ -1,7 +1,7 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { AdapterConfigWrap } from '@/onebot/config/config';
import { NetworkAdapterConfig } from '@/onebot/config/config';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
@@ -14,16 +14,14 @@ export default class GetRecentContact extends OneBotAction<Payload, any> {
actionName = ActionName.GetRecentContact;
payloadSchema = SchemaData;
async _handle(payload: Payload, adapter: string) {
async _handle(payload: Payload, adapter: string, config: NetworkAdapterConfig) {
const ret = await this.core.apis.UserApi.getRecentContactListSnapShot(+payload.count);
const network = Object.values(this.obContext.configLoader.configData.network) as Array<AdapterConfigWrap>;
//烘焙消息
const msgFormat = network.flat().find(e => e.name === adapter)?.messagePostFormat ?? 'array';
return await Promise.all(ret.info.changedList.map(async (t) => {
const FastMsg = await this.core.apis.MsgApi.getMsgsByMsgId({ chatType: t.chatType, peerUid: t.peerUid }, [t.msgId]);
if (FastMsg.msgList.length > 0) {
//扩展ret.info.changedList
const lastestMsg = await this.obContext.apis.MsgApi.parseMessage(FastMsg.msgList[0], msgFormat);
const lastestMsg = await this.obContext.apis.MsgApi.parseMessage(FastMsg.msgList[0], config.messagePostFormat);
return {
lastestMsg: lastestMsg,
peerUin: t.peerUin,

View File

@@ -23,7 +23,7 @@ import { OB11GroupTitleEvent } from '@/onebot/event/notice/OB11GroupTitleEvent';
import { OB11GroupUploadNoticeEvent } from '../event/notice/OB11GroupUploadNoticeEvent';
import { OB11GroupNameEvent } from '../event/notice/OB11GroupNameEvent';
import { pathToFileURL } from 'node:url';
import { FileNapCatOneBotUUID } from '@/common/helper';
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
import { OB11GroupIncreaseEvent } from '../event/notice/OB11GroupIncreaseEvent';
export class OneBotGroupApi {
@@ -199,7 +199,7 @@ export class OneBotGroupApi {
id: FileNapCatOneBotUUID.encode({
chatType: ChatType.KCHATTYPEGROUP,
peerUid: msg.peerUid,
}, msg.msgId, elementWrapper.elementId, elementWrapper?.fileElement?.fileUuid, "." + element.fileName),
}, msg.msgId, elementWrapper.elementId, elementWrapper?.fileElement?.fileUuid, element.fileName),
url: pathToFileURL(element.filePath).href,
name: element.fileName,
size: parseInt(element.fileSize),
@@ -218,12 +218,12 @@ export class OneBotGroupApi {
element.groupName,
);
} else if (element.type === TipGroupElementType.KSHUTUP) {
let event = await this.parseGroupBanEvent(msg.peerUid, elementWrapper);
const event = await this.parseGroupBanEvent(msg.peerUid, elementWrapper);
return event;
} else if (element.type === TipGroupElementType.KMEMBERADD) {
// 自己的通知 协议推送为type->85 在这里实现为了避免邀请出现问题
if (element.memberUid == this.core.selfInfo.uid) {
this.core.apis.GroupApi.refreshGroupMemberCache(msg.peerUid).then().catch();
await this.core.apis.GroupApi.refreshGroupMemberCache(msg.peerUid, true);
return new OB11GroupIncreaseEvent(
this.core,
parseInt(msg.peerUid),

View File

@@ -1,4 +1,4 @@
import { FileNapCatOneBotUUID } from '@/common/helper';
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
import { MessageUnique } from '@/common/message-unique';
import { pathToFileURL } from 'node:url';
import {
@@ -20,7 +20,7 @@ import {
GroupNotify,
} from '@/core';
import faceConfig from '@/core/external/face_config.json';
import { NapCatOneBot11Adapter, OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageFileBase, OB11MessageForward, } from '@/onebot';
import { NapCatOneBot11Adapter, OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageFileBase, OB11MessageForward, OB11MessageImage, OB11MessageVideo, } from '@/onebot';
import { OB11Construct } from '@/onebot/helper/data';
import { EventType } from '@/onebot/event/OneBotEvent';
import { encodeCQCode } from '@/onebot/helper/cqcode';
@@ -116,18 +116,22 @@ export class OneBotMsgApi {
peerUid: msg.peerUid,
guildId: '',
};
const encodedFileId = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, "." + element.fileName);
FileNapCatOneBotUUID.encode(
peer,
msg.msgId,
elementWrapper.elementId,
element.fileUuid,
element.fileName
);
return {
type: OB11MessageDataType.image,
data: {
summary: element.summary,
file: encodedFileId,
file: element.fileName,
sub_type: element.picSubType,
file_id: encodedFileId,
url: await this.core.apis.FileApi.getImageUrl(element),
path: element.filePath,
file_size: element.fileSize,
file_unique: element.md5HexStr ?? element.fileName,
},
};
} catch (e: any) {
@@ -142,15 +146,15 @@ export class OneBotMsgApi {
peerUid: msg.peerUid,
guildId: '',
};
const file = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileName);
return {
type: OB11MessageDataType.file,
data: {
file: element.fileName,
file: file,
path: element.filePath,
url: pathToFileURL(element.filePath).href,
file_id: FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, "." + element.fileName),
file_id: file,
file_size: element.fileSize,
file_unique: element.fileMd5 ?? element.fileSha ?? element.fileName,
},
};
},
@@ -186,7 +190,10 @@ export class OneBotMsgApi {
return {
type: OB11MessageDataType.face,
data: {
id: element.faceIndex.toString()
id: element.faceIndex.toString(),
raw: element,
resultId: element.resultId,
chainCount: element.chainCount,
},
};
}
@@ -201,18 +208,18 @@ export class OneBotMsgApi {
const { emojiId } = _;
const dir = emojiId.substring(0, 2);
const url = `https://gxh.vip.qq.com/club/item/parcel/item/${dir}/${emojiId}/raw300.gif`;
const filename = `${dir}-${emojiId}.gif`;
FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, "", filename);
return {
type: OB11MessageDataType.image,
data: {
summary: _.faceName, // 商城表情名称
file: 'marketface',
file_id: FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, "", "." + _.key + ".jpg"),
file: filename,
path: url,
url: url,
key: _.key,
emoji_id: _.emojiId,
emoji_package_id: _.emojiPackageId,
file_unique: _.key
},
};
},
@@ -327,16 +334,14 @@ export class OneBotMsgApi {
if (!videoDownUrl) {
videoDownUrl = element.filePath;
}
const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, "", "." + element.fileName);
const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileName);
return {
type: OB11MessageDataType.video,
data: {
file: fileCode,
path: videoDownUrl,
url: videoDownUrl ?? pathToFileURL(element.filePath).href,
file_id: fileCode,
file_size: element.fileSize,
file_unique: element.videoMd5 ?? element.thumbMd5 ?? element.fileName,
},
};
},
@@ -347,16 +352,14 @@ export class OneBotMsgApi {
peerUid: msg.peerUid,
guildId: '',
};
const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, "", "." + element.fileName);
const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, "", element.fileName);
return {
type: OB11MessageDataType.voice,
data: {
file: fileCode,
path: element.filePath,
url: pathToFileURL(element.filePath).href,
file_id: fileCode,
file_size: element.fileSize,
file_unique: element.fileUuid
},
};
},
@@ -464,7 +467,7 @@ export class OneBotMsgApi {
undefined;
},
[OB11MessageDataType.face]: async ({ data: { id } }) => {
[OB11MessageDataType.face]: async ({ data: { id, resultId, chainCount } }) => {
const parsedFaceId = +id;
// 从face_config.json中获取表情名称
const sysFaces = faceConfig.sysface;
@@ -491,6 +494,8 @@ export class OneBotMsgApi {
stickerType: face.AniStickerType,
packId: face.AniStickerPackId,
sourceType: 1,
resultId: resultId?.toString(),
chainCount,
},
};
},
@@ -797,6 +802,13 @@ export class OneBotMsgApi {
private async handlePrivateMessage(resMsg: OB11Message, msg: RawMessage) {
resMsg.sub_type = 'friend';
if (await this.core.apis.FriendApi.isBuddy(msg.senderUid)) {
const nickname = (await this.core.apis.UserApi.getCoreAndBaseInfo([msg.senderUid])).get(msg.senderUid)?.coreInfo.nick;
if (nickname) {
resMsg.sender.nickname = nickname;
return;
}
}
resMsg.sender.nickname = (await this.core.apis.UserApi.getUserDetailInfo(msg.senderUid)).nick;
}
@@ -899,16 +911,16 @@ export class OneBotMsgApi {
const calculateTotalSize = async (elements: SendMessageElement[]): Promise<number> => {
const sizePromises = elements.map(async element => {
switch (element.elementType) {
case ElementType.PTT:
return (await fsPromise.stat(element.pttElement.filePath)).size;
case ElementType.FILE:
return (await fsPromise.stat(element.fileElement.filePath)).size;
case ElementType.VIDEO:
return (await fsPromise.stat(element.videoElement.filePath)).size;
case ElementType.PIC:
return (await fsPromise.stat(element.picElement.sourcePath)).size;
default:
return 0;
case ElementType.PTT:
return (await fsPromise.stat(element.pttElement.filePath)).size;
case ElementType.FILE:
return (await fsPromise.stat(element.fileElement.filePath)).size;
case ElementType.VIDEO:
return (await fsPromise.stat(element.videoElement.filePath)).size;
case ElementType.PIC:
return (await fsPromise.stat(element.picElement.sourcePath)).size;
default:
return 0;
}
});
const sizes = await Promise.all(sizePromises);
@@ -949,46 +961,72 @@ export class OneBotMsgApi {
private async handleOb11FileLikeMessage(
{ data: inputdata }: OB11MessageFileBase,
{ deleteAfterSentFiles }: SendMessageContext,
{ deleteAfterSentFiles }: SendMessageContext
) {
const realUri = inputdata.url ?? inputdata.file ?? inputdata.path ?? '';
if (realUri.length === 0) {
let realUri = [inputdata.url, inputdata.file, inputdata.path].find(uri => uri && uri.trim()) ?? '';
if (!realUri) {
this.core.context.logger.logError('文件消息缺少参数', inputdata);
throw Error('文件消息缺少参数');
}
const {
path,
fileName,
errMsg,
success,
} = (await uriToLocalFile(this.core.NapCatTempPath, realUri));
if (!success) {
this.core.context.logger.logError('文件下载失败', errMsg);
throw Error('文件下载失败' + errMsg);
throw new Error('文件消息缺少参数');
}
deleteAfterSentFiles.push(path);
return { path, fileName: inputdata.name ?? fileName };
const downloadFile = async (uri: string) => {
const { path, fileName, errMsg, success } = await uriToLocalFile(this.core.NapCatTempPath, uri);
if (!success) {
this.core.context.logger.logError('文件下载失败', errMsg);
throw new Error('文件下载失败: ' + errMsg);
}
return { path, fileName };
};
try {
const { path, fileName } = await downloadFile(realUri);
deleteAfterSentFiles.push(path);
return { path, fileName: inputdata.name ?? fileName };
} catch {
realUri = await this.handleObfuckName(realUri);
const { path, fileName } = await downloadFile(realUri);
deleteAfterSentFiles.push(path);
return { path, fileName: inputdata.name ?? fileName };
}
}
async handleObfuckName(name: string) {
const contextMsgFile = FileNapCatOneBotUUID.decode(name);
if (contextMsgFile && contextMsgFile.msgId && contextMsgFile.elementId) {
const { peer, msgId, elementId } = contextMsgFile;
const rawMessage = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgId]))?.msgList.find(msg => msg.msgId === msgId);
const mixElement = rawMessage?.elements.find(e => e.elementId === elementId);
const mixElementInner = mixElement?.videoElement ?? mixElement?.fileElement ?? mixElement?.pttElement ?? mixElement?.picElement;
if (!mixElementInner) throw new Error('element not found');
let url = '';
if (mixElement?.picElement && rawMessage) {
const tempData =
await this.obContext.apis.MsgApi.rawToOb11Converters.picElement?.(mixElement?.picElement, rawMessage, mixElement, { parseMultMsg: false }) as OB11MessageImage | undefined;
url = tempData?.data.url ?? '';
}
if (mixElement?.videoElement && rawMessage) {
const tempData =
await this.obContext.apis.MsgApi.rawToOb11Converters.videoElement?.(mixElement?.videoElement, rawMessage, mixElement, { parseMultMsg: false }) as OB11MessageVideo | undefined;
url = tempData?.data.url ?? '';
}
return url !== '' ? url : await this.core.apis.FileApi.downloadMedia(msgId, peer.chatType, peer.peerUid, elementId, '', '');
}
throw new Error('文件名解析失败');
}
groupChangDecreseType2String(type: number): GroupDecreaseSubType {
switch (type) {
case 130:
return 'leave';
case 131:
return 'kick';
case 3:
return 'kick_me';
default:
return 'kick';
case 130:
return 'leave';
case 131:
return 'kick';
case 3:
return 'kick_me';
default:
return 'kick';
}
}
async waitGroupNotify(groupUin: string, memberUid?: string, operatorUid?: string) {
let groupRole = this.core.apis.GroupApi.groupMemberCache.get(groupUin)?.get(this.core.selfInfo.uid.toString())?.role;
let isAdminOrOwner = groupRole === 3 || groupRole === 4;
const groupRole = this.core.apis.GroupApi.groupMemberCache.get(groupUin)?.get(this.core.selfInfo.uid.toString())?.role;
const isAdminOrOwner = groupRole === 3 || groupRole === 4;
if (isAdminOrOwner && !operatorUid) {
let dataNotify: GroupNotify | undefined;
@@ -1015,8 +1053,8 @@ export class OneBotMsgApi {
// 邀请需要解grayTipElement
if (SysMessage.contentHead.type == 33 && SysMessage.body?.msgContent) {
const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent);
this.core.apis.GroupApi.refreshGroupMemberCache(groupChange.groupUin.toString()).then().catch();
let operatorUid = await this.waitGroupNotify(
await this.core.apis.GroupApi.refreshGroupMemberCache(groupChange.groupUin.toString(), true);
const operatorUid = await this.waitGroupNotify(
groupChange.groupUin.toString(),
groupChange.memberUid,
groupChange.operatorInfo ? Buffer.from(groupChange.operatorInfo).toString() : ''
@@ -1032,7 +1070,7 @@ export class OneBotMsgApi {
} else if (SysMessage.contentHead.type == 34 && SysMessage.body?.msgContent) {
const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent);
// 自身被踢出时operatorInfo会是一个protobuf 否则大多数情况为一个string
let operatorUid = await this.waitGroupNotify(
const operatorUid = await this.waitGroupNotify(
groupChange.groupUin.toString(),
groupChange.memberUid,
groupChange.decreaseType === 3 && groupChange.operatorInfo ?
@@ -1045,7 +1083,7 @@ export class OneBotMsgApi {
}, 5000);
// 自己被踢了 5S后回收
} else {
this.core.apis.GroupApi.refreshGroupMemberCache(groupChange.groupUin.toString()).then().catch();
await this.core.apis.GroupApi.refreshGroupMemberCache(groupChange.groupUin.toString(), true);
}
return new OB11GroupDecreaseEvent(
this.core,
@@ -1056,7 +1094,7 @@ export class OneBotMsgApi {
);
} else if (SysMessage.contentHead.type == 44 && SysMessage.body?.msgContent) {
const groupAmin = new NapProtoMsg(GroupAdmin).decode(SysMessage.body.msgContent);
this.core.apis.GroupApi.refreshGroupMemberCache(groupAmin.groupUin.toString()).then().catch();
await this.core.apis.GroupApi.refreshGroupMemberCache(groupAmin.groupUin.toString(), true);
let enabled = false;
let uid = '';
if (groupAmin.body.extraEnable != null) {
@@ -1073,17 +1111,17 @@ export class OneBotMsgApi {
enabled ? 'set' : 'unset'
);
} else if (SysMessage.contentHead.type == 87 && SysMessage.body?.msgContent) {
let groupInvite = new NapProtoMsg(GroupInvite).decode(SysMessage.body.msgContent);
const groupInvite = new NapProtoMsg(GroupInvite).decode(SysMessage.body.msgContent);
let request_seq = '';
try {
await this.core.eventWrapper.registerListen('NodeIKernelMsgListener/onRecvMsg', (msgs) => {
for (const msg of msgs) {
if (msg.senderUid === groupInvite.invitorUid && msg.msgType === 11) {
let jumpUrl = JSON.parse(msg.elements.find(e => e.elementType === 10)?.arkElement?.bytesData ?? '').meta?.news?.jumpUrl;
let jumpUrlParams = new URLSearchParams(jumpUrl);
let groupcode = jumpUrlParams.get('groupcode');
let receiveruin = jumpUrlParams.get('receiveruin');
let msgseq = jumpUrlParams.get('msgseq');
const jumpUrl = JSON.parse(msg.elements.find(e => e.elementType === 10)?.arkElement?.bytesData ?? '').meta?.news?.jumpUrl;
const jumpUrlParams = new URLSearchParams(jumpUrl);
const groupcode = jumpUrlParams.get('groupcode');
const receiveruin = jumpUrlParams.get('receiveruin');
const msgseq = jumpUrlParams.get('msgseq');
request_seq = msgseq ?? '';
if (groupcode === groupInvite.groupUin.toString() && receiveruin === this.core.selfInfo.uin) {
return true;
@@ -1129,7 +1167,7 @@ export class OneBotMsgApi {
waitStatus: 1,
},
status: 1
})
});
return new OB11GroupRequestEvent(
this.core,
+groupInvite.groupUin,

View File

@@ -92,7 +92,7 @@ export class OneBotQuickActionApi {
async handleGroupRequest(request: OB11GroupRequestEvent, quickAction: QuickActionGroupRequest) {
let invite_notify = this.obContext.apis.MsgApi.notifyGroupInvite.get(request.flag);
const invite_notify = this.obContext.apis.MsgApi.notifyGroupInvite.get(request.flag);
const notify = invite_notify ?? await this.findNotify(request.flag);
if (!isNull(quickAction.approve) && notify) {

View File

@@ -1,245 +1,108 @@
interface v1Config {
http: {
enable: boolean;
host: string;
port: number;
secret: string;
enableHeart: boolean;
enablePost: boolean;
postUrls: string[];
};
ws: {
enable: boolean;
host: string;
port: number;
};
reverseWs: {
enable: boolean;
urls: string[];
};
debug: boolean;
heartInterval: number;
messagePostFormat: string;
enableLocalFile2Url: boolean;
musicSignUrl: string;
reportSelfMessage: boolean;
token: string;
}
export interface AdapterConfigInner {
name: string;
enable: boolean;
import { Type, Static } from '@sinclair/typebox';
import Ajv from 'ajv';
}
export type AdapterConfigWrap = AdapterConfigInner & Partial<NetworkConfigAdapter>;
export interface AdapterConfig extends AdapterConfigInner {
[key: string]: any;
}
const createDefaultAdapterConfig = <T extends AdapterConfig>(config: T): T => config;
export interface PluginConfig extends AdapterConfig {
name: string;
enable: boolean;
messagePostFormat: string;
reportSelfMessage: boolean;
debug: boolean;
}
export const httpServerDefaultConfigs = createDefaultAdapterConfig({
name: 'http-server',
enable: false as boolean,
port: 3000,
host: '0.0.0.0',
enableCors: true,
enableWebsocket: true,
messagePostFormat: 'array',
token: '',
debug: false,
});
export type HttpServerConfig = typeof httpServerDefaultConfigs;
export const httpClientDefaultConfigs = createDefaultAdapterConfig({
name: 'http-client',
enable: false as boolean,
url: 'http://localhost:8080',
messagePostFormat: 'array',
reportSelfMessage: false,
token: '',
debug: false,
});
export type HttpClientConfig = typeof httpClientDefaultConfigs;
export const websocketServerDefaultConfigs = createDefaultAdapterConfig({
name: 'websocket-server',
enable: false as boolean,
host: '0.0.0.0',
port: 3001,
messagePostFormat: 'array',
reportSelfMessage: false,
token: '',
enableForcePushEvent: true,
debug: false,
heartInterval: 30000,
});
export type WebsocketServerConfig = typeof websocketServerDefaultConfigs;
export const websocketClientDefaultConfigs = createDefaultAdapterConfig({
name: 'websocket-client',
enable: false as boolean,
url: 'ws://localhost:8082',
messagePostFormat: 'array',
reportSelfMessage: false,
reconnectInterval: 5000,
token: '',
debug: false,
heartInterval: 30000,
});
export type WebsocketClientConfig = typeof websocketClientDefaultConfigs;
export interface NetworkConfig {
httpServers: Array<HttpServerConfig>;
httpClients: Array<HttpClientConfig>;
websocketServers: Array<WebsocketServerConfig>;
websocketClients: Array<WebsocketClientConfig>;
}
export function mergeConfigs<T extends AdapterConfig>(defaultConfig: T, userConfig: Partial<T>): T {
return { ...defaultConfig, ...userConfig };
}
export interface OneBotConfig {
network: NetworkConfig; // 网络配置
musicSignUrl: string; // 音乐签名地址
enableLocalFile2Url: boolean;
parseMultMsg: boolean;
}
const createDefaultConfig = <T>(config: T): T => config;
export const defaultOneBotConfigs = createDefaultConfig<OneBotConfig>({
network: {
httpServers: [],
httpClients: [],
websocketServers: [],
websocketClients: [],
},
musicSignUrl: '',
enableLocalFile2Url: false,
parseMultMsg: true
const HttpServerConfigSchema = Type.Object({
name: Type.String({ default: 'http-server' }),
enable: Type.Boolean({ default: false }),
port: Type.Number({ default: 3000 }),
host: Type.String({ default: '0.0.0.0' }),
enableCors: Type.Boolean({ default: true }),
enableWebsocket: Type.Boolean({ default: true }),
messagePostFormat: Type.String({ default: 'array' }),
token: Type.String({ default: '' }),
debug: Type.Boolean({ default: false })
});
export const mergeNetworkDefaultConfig = {
httpServers: httpServerDefaultConfigs,
httpClients: httpClientDefaultConfigs,
websocketServers: websocketServerDefaultConfigs,
websocketClients: websocketClientDefaultConfigs,
} as const;
const HttpSseServerConfigSchema = Type.Object({
name: Type.String({ default: 'http-sse-server' }),
enable: Type.Boolean({ default: false }),
port: Type.Number({ default: 3000 }),
host: Type.String({ default: '0.0.0.0' }),
enableCors: Type.Boolean({ default: true }),
enableWebsocket: Type.Boolean({ default: true }),
messagePostFormat: Type.String({ default: 'array' }),
token: Type.String({ default: '' }),
debug: Type.Boolean({ default: false }),
reportSelfMessage: Type.Boolean({ default: false })
});
export type NetworkConfigAdapter = HttpServerConfig | HttpClientConfig | WebsocketServerConfig | WebsocketClientConfig | PluginConfig;
type NetworkConfigKeys = keyof typeof mergeNetworkDefaultConfig;
const HttpClientConfigSchema = Type.Object({
name: Type.String({ default: 'http-client' }),
enable: Type.Boolean({ default: false }),
url: Type.String({ default: 'http://localhost:8080' }),
messagePostFormat: Type.String({ default: 'array' }),
reportSelfMessage: Type.Boolean({ default: false }),
token: Type.String({ default: '' }),
debug: Type.Boolean({ default: false })
});
export function mergeOneBotConfigs(
userConfig: Partial<OneBotConfig>,
defaultConfig: OneBotConfig = defaultOneBotConfigs
): OneBotConfig {
const mergedConfig = { ...defaultConfig };
const WebsocketServerConfigSchema = Type.Object({
name: Type.String({ default: 'websocket-server' }),
enable: Type.Boolean({ default: false }),
host: Type.String({ default: '0.0.0.0' }),
port: Type.Number({ default: 3001 }),
messagePostFormat: Type.String({ default: 'array' }),
reportSelfMessage: Type.Boolean({ default: false }),
token: Type.String({ default: '' }),
enableForcePushEvent: Type.Boolean({ default: true }),
debug: Type.Boolean({ default: false }),
heartInterval: Type.Number({ default: 30000 })
});
if (userConfig.network) {
mergedConfig.network = { ...defaultConfig.network };
for (const key in userConfig.network) {
const userNetworkConfig = userConfig.network[key as keyof NetworkConfig];
const defaultNetworkConfig = mergeNetworkDefaultConfig[key as NetworkConfigKeys];
if (Array.isArray(userNetworkConfig)) {
mergedConfig.network[key as keyof NetworkConfig] = userNetworkConfig.map<any>((e) =>
mergeConfigs(defaultNetworkConfig, e)
);
}
}
}
if (userConfig.musicSignUrl !== undefined) {
mergedConfig.musicSignUrl = userConfig.musicSignUrl;
}
if (userConfig.enableLocalFile2Url !== undefined) {
mergedConfig.enableLocalFile2Url = userConfig.enableLocalFile2Url;
}
if (userConfig.parseMultMsg !== undefined) {
mergedConfig.parseMultMsg = userConfig.parseMultMsg;
}
return mergedConfig;
}
const WebsocketClientConfigSchema = Type.Object({
name: Type.String({ default: 'websocket-client' }),
enable: Type.Boolean({ default: false }),
url: Type.String({ default: 'ws://localhost:8082' }),
messagePostFormat: Type.String({ default: 'array' }),
reportSelfMessage: Type.Boolean({ default: false }),
reconnectInterval: Type.Number({ default: 5000 }),
token: Type.String({ default: '' }),
debug: Type.Boolean({ default: false }),
heartInterval: Type.Number({ default: 30000 })
});
function checkIsOneBotConfigV1(v1Config: Partial<v1Config>): boolean {
return v1Config.http !== undefined || v1Config.ws !== undefined || v1Config.reverseWs !== undefined;
}
const PluginConfigSchema = Type.Object({
name: Type.String({ default: 'plugin' }),
enable: Type.Boolean({ default: false }),
messagePostFormat: Type.String({ default: 'array' }),
reportSelfMessage: Type.Boolean({ default: false }),
debug: Type.Boolean({ default: false }),
});
export function migrateOneBotConfigsV1(config: Partial<v1Config>): OneBotConfig {
if (!checkIsOneBotConfigV1(config)) {
return config as OneBotConfig;
const NetworkConfigSchema = Type.Object({
httpServers: Type.Array(HttpServerConfigSchema, { default: [] }),
httpSseServers: Type.Array(HttpSseServerConfigSchema, { default: [] }),
httpClients: Type.Array(HttpClientConfigSchema, { default: [] }),
websocketServers: Type.Array(WebsocketServerConfigSchema, { default: [] }),
websocketClients: Type.Array(WebsocketClientConfigSchema, { default: [] }),
plugins: Type.Array(PluginConfigSchema, { default: [] })
}, { default: {} });
const OneBotConfigSchema = Type.Object({
network: NetworkConfigSchema,
musicSignUrl: Type.String({ default: '' }),
enableLocalFile2Url: Type.Boolean({ default: false }),
parseMultMsg: Type.Boolean({ default: true })
});
export type OneBotConfig = Static<typeof OneBotConfigSchema>;
export type HttpServerConfig = Static<typeof HttpServerConfigSchema>;
export type HttpSseServerConfig = Static<typeof HttpSseServerConfigSchema>;
export type HttpClientConfig = Static<typeof HttpClientConfigSchema>;
export type WebsocketServerConfig = Static<typeof WebsocketServerConfigSchema>;
export type WebsocketClientConfig = Static<typeof WebsocketClientConfigSchema>;
export type PluginConfig = Static<typeof PluginConfigSchema>;
export type NetworkAdapterConfig = HttpServerConfig | HttpSseServerConfig | HttpClientConfig | WebsocketServerConfig | WebsocketClientConfig | PluginConfig;
export type NetworkConfigKey = keyof OneBotConfig['network'];
export function loadConfig(config: Partial<OneBotConfig>): OneBotConfig {
const ajv = new Ajv({ useDefaults: true });
const validate = ajv.compile(OneBotConfigSchema);
const valid = validate(config);
if (!valid) {
throw new Error(ajv.errorsText(validate.errors));
}
const mergedConfig = { ...defaultOneBotConfigs };
if (config.http) {
mergedConfig.network.httpServers = [
mergeConfigs(httpServerDefaultConfigs, {
name: 'http-server',
enable: config.http.enable,
port: config.http.port,
host: config.http.host,
token: config.http.secret,
debug: config.debug,
messagePostFormat: config.messagePostFormat,
}),
];
}
if (config.ws) {
mergedConfig.network.websocketServers = [
mergeConfigs(websocketServerDefaultConfigs, {
name: 'websocket-server',
enable: config.ws.enable,
port: config.ws.port,
host: config.ws.host,
token: config.token,
debug: config.debug,
messagePostFormat: config.messagePostFormat,
reportSelfMessage: config.reportSelfMessage,
}),
];
}
if (config.reverseWs) {
mergedConfig.network.websocketClients = config.reverseWs.urls.map((url) =>
mergeConfigs(websocketClientDefaultConfigs, {
name: 'websocket-client-' + config.reverseWs?.urls.indexOf(url).toString(),
enable: config.reverseWs?.enable,
url: url,
token: config.token,
debug: config.debug,
messagePostFormat: config.messagePostFormat,
reportSelfMessage: config.reportSelfMessage,
})
);
}
if (config.heartInterval) {
mergedConfig.network.websocketServers[0].heartInterval = config.heartInterval;
}
if (config.musicSignUrl) {
mergedConfig.musicSignUrl = config.musicSignUrl;
}
if (config.enableLocalFile2Url) {
mergedConfig.enableLocalFile2Url = config.enableLocalFile2Url;
}
return mergedConfig;
}
export function getConfigBoolKey(
configs: Array<NetworkConfigAdapter>,
prediction: (config: NetworkConfigAdapter) => boolean
): { positive: Array<string>, negative: Array<string> } {
const result: { positive: string[], negative: string[] } = { positive: [], negative: [] };
configs.forEach(config => {
if (prediction(config)) {
result.positive.push(config.name);
} else {
result.negative.push(config.name);
}
});
return result;
}
return config as OneBotConfig;
}

View File

@@ -1,4 +1,5 @@
import { calcQQLevel, FileNapCatOneBotUUID } from '@/common/helper';
import { calcQQLevel } from '@/common/helper';
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
import { FriendV2, Group, GroupFileInfoUpdateParamType, GroupMember, SelfInfo, NTSex } from '@/core';
import {
OB11Group,
@@ -22,7 +23,7 @@ export class OB11Construct {
...rawFriend.baseInfo,
...rawFriend.coreInfo,
user_id: parseInt(rawFriend.coreInfo.uin),
nickname: rawFriend.coreInfo.nick,
nickname: rawFriend.coreInfo.nick ?? "",
remark: rawFriend.coreInfo.remark ?? rawFriend.coreInfo.nick,
sex: this.sex(rawFriend.baseInfo.sex),
level: 0,
@@ -90,6 +91,7 @@ export class OB11Construct {
file_name: file.fileName,
busid: file.busId,
size: +file.fileSize,
file_size: +file.fileSize,
upload_time: file.uploadTime,
dead_time: file.deadTime,
modify_time: file.modifyTime,

View File

@@ -16,12 +16,12 @@ import {
} from '@/core';
import { OB11ConfigLoader } from '@/onebot/config';
import {
OB11ActiveHttpAdapter,
OB11ActiveWebSocketAdapter,
OB11HttpClientAdapter,
OB11WebSocketClientAdapter,
OB11NetworkManager,
OB11NetworkReloadType,
OB11PassiveHttpAdapter,
OB11PassiveWebSocketAdapter,
OB11HttpServerAdapter,
OB11WebSocketServerAdapter,
} from '@/onebot/network';
import { NapCatPathWrapper } from '@/common/path';
import {
@@ -44,15 +44,14 @@ import { LRUCache } from '@/common/lru-cache';
import { NodeIKernelRecentContactListener } from '@/core/listeners/NodeIKernelRecentContactListener';
import { BotOfflineEvent } from './event/notice/BotOfflineEvent';
import {
AdapterConfigWrap,
mergeOneBotConfigs,
migrateOneBotConfigsV1,
NetworkConfigAdapter,
NetworkAdapterConfig,
loadConfig,
OneBotConfig,
} from './config/config';
import { OB11Message } from './types';
import { OB11PluginAdapter } from './network/plugin';
import { IOB11NetworkAdapter } from "@/onebot/network/adapter";
import { OB11HttpSSEServerAdapter } from './network/http-server-sse';
//OneBot实现类
export class NapCatOneBot11Adapter {
@@ -70,8 +69,8 @@ export class NapCatOneBot11Adapter {
this.core = core;
this.context = context;
this.configLoader = new OB11ConfigLoader(core, pathWrapper.configPath);
this.configLoader.save(migrateOneBotConfigsV1(this.configLoader.configData));
this.configLoader.save(mergeOneBotConfigs(this.configLoader.configData));
this.configLoader.save(this.configLoader.configData);
this.configLoader.save(loadConfig(this.configLoader.configData));
this.apis = {
GroupApi: new OneBotGroupApi(this, core),
UserApi: new OneBotUserApi(this, core),
@@ -87,6 +86,9 @@ export class NapCatOneBot11Adapter {
for (const key of ob11Config.network.httpServers) {
log += `HTTP服务: ${key.host}:${key.port}, : ${key.enable ? '已启动' : '未启动'}\n`;
}
for (const key of ob11Config.network.httpSseServers) {
log += `HTTP-SSE服务: ${key.host}:${key.port}, : ${key.enable ? '已启动' : '未启动'}\n`;
}
for (const key of ob11Config.network.httpClients) {
log += `HTTP上报服务: ${key.url}, : ${key.enable ? '已启动' : '未启动'}\n`;
}
@@ -121,21 +123,28 @@ export class NapCatOneBot11Adapter {
for (const key of ob11Config.network.httpServers) {
if (key.enable) {
this.networkManager.registerAdapter(
new OB11PassiveHttpAdapter(key.name, key, this.core, this, this.actions)
new OB11HttpServerAdapter(key.name, key, this.core, this, this.actions)
);
}
}
for (const key of ob11Config.network.httpSseServers) {
if (key.enable) {
this.networkManager.registerAdapter(
new OB11HttpSSEServerAdapter(key.name, key, this.core, this, this.actions)
);
}
}
for (const key of ob11Config.network.httpClients) {
if (key.enable) {
this.networkManager.registerAdapter(
new OB11ActiveHttpAdapter(key.name, key, this.core, this, this.actions)
new OB11HttpClientAdapter(key.name, key, this.core, this, this.actions)
);
}
}
for (const key of ob11Config.network.websocketServers) {
if (key.enable) {
this.networkManager.registerAdapter(
new OB11PassiveWebSocketAdapter(
new OB11WebSocketServerAdapter(
key.name,
key,
this.core,
@@ -148,7 +157,7 @@ export class NapCatOneBot11Adapter {
for (const key of ob11Config.network.websocketClients) {
if (key.enable) {
this.networkManager.registerAdapter(
new OB11ActiveWebSocketAdapter(
new OB11WebSocketClientAdapter(
key.name,
key,
this.core,
@@ -169,8 +178,11 @@ export class NapCatOneBot11Adapter {
WebUiDataRuntime.setQQLoginStatus(true);
WebUiDataRuntime.setOnOB11ConfigChanged(async (newConfig) => {
const prev = this.configLoader.configData;
// 保证默认配置
newConfig = loadConfig(newConfig);
this.configLoader.save(newConfig);
this.context.logger.log(`OneBot11 配置更改:${JSON.stringify(prev)} -> ${JSON.stringify(newConfig)}`);
//this.context.logger.log(`OneBot11 配置更改:${JSON.stringify(prev)} -> ${JSON.stringify(newConfig)}`);
await this.reloadNetwork(prev, newConfig);
});
}
@@ -194,15 +206,16 @@ export class NapCatOneBot11Adapter {
this.context.logger.log(`[Notice] [OneBot11] 配置变更前:\n${prevLog}`);
this.context.logger.log(`[Notice] [OneBot11] 配置变更后:\n${newLog}`);
await this.handleConfigChange(prev.network.httpServers, now.network.httpServers, OB11PassiveHttpAdapter);
await this.handleConfigChange(prev.network.httpClients, now.network.httpClients, OB11ActiveHttpAdapter);
await this.handleConfigChange(prev.network.websocketServers, now.network.websocketServers, OB11PassiveWebSocketAdapter);
await this.handleConfigChange(prev.network.websocketClients, now.network.websocketClients, OB11ActiveWebSocketAdapter);
await this.handleConfigChange(prev.network.httpServers, now.network.httpServers, OB11HttpServerAdapter);
await this.handleConfigChange(prev.network.httpClients, now.network.httpClients, OB11HttpClientAdapter);
await this.handleConfigChange(prev.network.httpSseServers, now.network.httpSseServers, OB11HttpSSEServerAdapter);
await this.handleConfigChange(prev.network.websocketServers, now.network.websocketServers, OB11WebSocketServerAdapter);
await this.handleConfigChange(prev.network.websocketClients, now.network.websocketClients, OB11WebSocketClientAdapter);
}
private async handleConfigChange<CT extends NetworkConfigAdapter>(
prevConfig: NetworkConfigAdapter[],
nowConfig: NetworkConfigAdapter[],
private async handleConfigChange<CT extends NetworkAdapterConfig>(
prevConfig: NetworkAdapterConfig[],
nowConfig: NetworkAdapterConfig[],
adapterClass: new (
...args: ConstructorParameters<typeof IOB11NetworkAdapter<CT>>
) => IOB11NetworkAdapter<CT>
@@ -389,10 +402,7 @@ export class NapCatOneBot11Adapter {
) {
this.context.logger.logDebug('有加群请求');
try {
let requestUin = await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid);
if (isNaN(parseInt(requestUin))) {
requestUin = (await this.core.apis.UserApi.getUserDetailInfo(notify.user1.uid)).uin;
}
const requestUin = await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid);
const groupRequestEvent = new OB11GroupRequestEvent(
this.core,
parseInt(notify.group.groupCode),
@@ -467,7 +477,7 @@ export class NapCatOneBot11Adapter {
]);
}
private async handleMsg(message: RawMessage, network: Array<AdapterConfigWrap>) {
private async handleMsg(message: RawMessage, network: Array<NetworkAdapterConfig>) {
// 过滤无效消息
if (message.msgType === NTMsgType.KMSGTYPENULL) {
return;
@@ -496,7 +506,7 @@ export class NapCatOneBot11Adapter {
ob11Msg.arrayMsg.user_id.toString() == this.core.selfInfo.uin;
}
private createMsgMap(network: Array<AdapterConfigWrap>, ob11Msg: any, isSelfMsg: boolean, message: RawMessage): Map<string, OB11Message> {
private createMsgMap(network: Array<NetworkAdapterConfig>, ob11Msg: any, isSelfMsg: boolean, message: RawMessage): Map<string, OB11Message> {
const msgMap: Map<string, OB11Message> = new Map();
network.filter(e => e.enable).forEach(e => {
if (isSelfMsg || message.chatType !== ChatType.KCHATTYPEGROUP) {
@@ -513,7 +523,7 @@ export class NapCatOneBot11Adapter {
return msgMap;
}
private handleDebugNetwork(network: Array<AdapterConfigWrap>, msgMap: Map<string, OB11Message>, message: RawMessage) {
private handleDebugNetwork(network: Array<NetworkAdapterConfig>, msgMap: Map<string, OB11Message>, message: RawMessage) {
const debugNetwork = network.filter(e => e.enable && e.debug);
if (debugNetwork.length > 0) {
debugNetwork.forEach(adapter => {
@@ -527,7 +537,7 @@ export class NapCatOneBot11Adapter {
}
}
private handleNotReportSelfNetwork(network: Array<AdapterConfigWrap>, msgMap: Map<string, OB11Message>, isSelfMsg: boolean) {
private handleNotReportSelfNetwork(network: Array<NetworkAdapterConfig>, msgMap: Map<string, OB11Message>, isSelfMsg: boolean) {
if (isSelfMsg) {
const notReportSelfNetwork = network.filter(e => e.enable && (('reportSelfMessage' in e && !e.reportSelfMessage) || !('reportSelfMessage' in e)));
notReportSelfNetwork.forEach(adapter => {

View File

@@ -1,11 +1,11 @@
import { NetworkConfigAdapter } from "@/onebot/config/config";
import { NetworkAdapterConfig } from "@/onebot/config/config";
import { LogWrapper } from "@/common/log";
import { NapCatCore } from "@/core";
import { NapCatOneBot11Adapter } from "@/onebot";
import { ActionMap } from "@/onebot/action";
import { OB11EmitEventContent, OB11NetworkReloadType } from "@/onebot/network/index";
export abstract class IOB11NetworkAdapter<CT extends NetworkConfigAdapter> {
export abstract class IOB11NetworkAdapter<CT extends NetworkAdapterConfig> {
name: string;
isEnable: boolean = false;
config: CT;

View File

@@ -8,7 +8,7 @@ import { HttpClientConfig } from '@/onebot/config/config';
import { ActionMap } from '@/onebot/action';
import { IOB11NetworkAdapter } from "@/onebot/network/adapter";
export class OB11ActiveHttpAdapter extends IOB11NetworkAdapter<HttpClientConfig> {
export class OB11HttpClientAdapter extends IOB11NetworkAdapter<HttpClientConfig> {
constructor(
name: string, config: HttpClientConfig, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap
) {

View File

@@ -0,0 +1,34 @@
import { OB11EmitEventContent } from './index';
import { Request, Response } from 'express';
import { OB11Response } from '@/onebot/action/OneBotAction';
import { OB11HttpServerAdapter } from './http-server';
export class OB11HttpSSEServerAdapter extends OB11HttpServerAdapter {
private sseClients: Response[] = [];
async handleRequest(req: Request, res: Response): Promise<any> {
if (req.path === '/_events') {
return this.createSseSupport(req, res);
} else {
super.httpApiRequest(req, res);
}
}
private async createSseSupport(req: Request, res: Response) {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
this.sseClients.push(res);
req.on('close', () => {
this.sseClients = this.sseClients.filter((client) => client !== res);
});
}
onEvent<T extends OB11EmitEventContent>(event: T) {
this.sseClients.forEach((res) => {
res.write(`data: ${JSON.stringify(event)}\n\n`);
});
}
}

View File

@@ -1,4 +1,4 @@
import { OB11NetworkReloadType } from './index';
import { OB11EmitEventContent, OB11NetworkReloadType } from './index';
import express, { Express, Request, Response } from 'express';
import http from 'http';
import { NapCatCore } from '@/core';
@@ -9,7 +9,7 @@ import { HttpServerConfig } from '@/onebot/config/config';
import { NapCatOneBot11Adapter } from "@/onebot";
import { IOB11NetworkAdapter } from "@/onebot/network/adapter";
export class OB11PassiveHttpAdapter extends IOB11NetworkAdapter<HttpServerConfig> {
export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig> {
private app: Express | undefined;
private server: http.Server | undefined;
@@ -17,7 +17,7 @@ export class OB11PassiveHttpAdapter extends IOB11NetworkAdapter<HttpServerConfig
super(name, config, core, obContext, actions);
}
onEvent() {
onEvent<T extends OB11EmitEventContent>(event: T) {
// http server is passive, no need to emit event
}
@@ -82,12 +82,7 @@ export class OB11PassiveHttpAdapter extends IOB11NetworkAdapter<HttpServerConfig
}
}
private async handleRequest(req: Request, res: Response) {
if (!this.isEnable) {
this.core.context.logger.log(`[OneBot] [HTTP Server Adapter] Server is closed`);
return res.json(OB11Response.error('Server is closed', 200));
}
async httpApiRequest(req: Request, res: Response) {
let payload = req.body;
if (req.method == 'get') {
payload = req.query;
@@ -103,7 +98,7 @@ export class OB11PassiveHttpAdapter extends IOB11NetworkAdapter<HttpServerConfig
const action = this.actions.get(actionName as any);
if (action) {
try {
const result = await action.handle(payload, this.name);
const result = await action.handle(payload, this.name, this.config);
return res.json(result);
} catch (error: any) {
return res.json(OB11Response.error(error?.stack?.toString() || error?.message || 'Error Handle', 200));
@@ -113,6 +108,15 @@ export class OB11PassiveHttpAdapter extends IOB11NetworkAdapter<HttpServerConfig
}
}
async handleRequest(req: Request, res: Response) {
if (!this.isEnable) {
this.core.context.logger.log(`[OneBot] [HTTP Server Adapter] Server is closed`);
return res.json(OB11Response.error('Server is closed', 200));
}
return this.httpApiRequest(req, res);
}
async reload(newConfig: HttpServerConfig) {
const wasEnabled = this.isEnable;
const oldPort = this.config.port;

View File

@@ -1,6 +1,6 @@
import { OneBotEvent } from '@/onebot/event/OneBotEvent';
import { OB11Message } from '@/onebot';
import { NetworkConfigAdapter } from '@/onebot/config/config';
import { NetworkAdapterConfig } from '@/onebot/config/config';
import { IOB11NetworkAdapter } from "@/onebot/network/adapter";
export type OB11EmitEventContent = OneBotEvent | OB11Message;
@@ -13,7 +13,7 @@ export enum OB11NetworkReloadType {
}
export class OB11NetworkManager {
adapters: Map<string, IOB11NetworkAdapter<NetworkConfigAdapter>> = new Map();
adapters: Map<string, IOB11NetworkAdapter<NetworkAdapterConfig>> = new Map();
async openAllAdapters() {
return Promise.all(Array.from(this.adapters.values()).map(adapter => adapter.open()));
@@ -49,22 +49,22 @@ export class OB11NetworkManager {
}));
}
registerAdapter<CT extends NetworkConfigAdapter>(adapter: IOB11NetworkAdapter<CT>) {
registerAdapter<CT extends NetworkAdapterConfig>(adapter: IOB11NetworkAdapter<CT>) {
this.adapters.set(adapter.name, adapter);
}
async registerAdapterAndOpen<CT extends NetworkConfigAdapter>(adapter: IOB11NetworkAdapter<CT>) {
async registerAdapterAndOpen<CT extends NetworkAdapterConfig>(adapter: IOB11NetworkAdapter<CT>) {
this.registerAdapter(adapter);
await adapter.open();
}
async closeSomeAdapters<CT extends NetworkConfigAdapter>(adaptersToClose: IOB11NetworkAdapter<CT>[]) {
async closeSomeAdapters<CT extends NetworkAdapterConfig>(adaptersToClose: IOB11NetworkAdapter<CT>[]) {
for (const adapter of adaptersToClose) {
this.adapters.delete(adapter.name);
await adapter.close();
}
}
async closeSomeAdaterWhenOpen<CT extends NetworkConfigAdapter>(adaptersToClose: IOB11NetworkAdapter<CT>[]) {
async closeSomeAdaterWhenOpen<CT extends NetworkAdapterConfig>(adaptersToClose: IOB11NetworkAdapter<CT>[]) {
for (const adapter of adaptersToClose) {
this.adapters.delete(adapter.name);
if (adapter.isEnable) {
@@ -77,7 +77,7 @@ export class OB11NetworkManager {
return this.adapters.get(name);
}
async closeAdapterByPredicate(closeFilter: (adapter: IOB11NetworkAdapter<NetworkConfigAdapter>) => boolean) {
async closeAdapterByPredicate(closeFilter: (adapter: IOB11NetworkAdapter<NetworkAdapterConfig>) => boolean) {
const adaptersToClose = Array.from(this.adapters.values()).filter(closeFilter);
await this.closeSomeAdapters(adaptersToClose);
}
@@ -101,7 +101,7 @@ export class OB11NetworkManager {
}
}
export * from './active-http';
export * from './active-websocket';
export * from './passive-http';
export * from './passive-websocket';
export * from './http-client';
export * from './websocket-client';
export * from './http-server';
export * from './websocket-server';

View File

@@ -22,7 +22,7 @@ export class OB11PluginAdapter extends IOB11NetworkAdapter<PluginConfig> {
onEvent<T extends OB11EmitEventContent>(event: T) {
if (event.post_type === 'message') {
plugin_onmessage(this.config.name, this.core, this.obContext, event as OB11Message,this.actions).then().catch();
plugin_onmessage(this.config.name, this.core, this.obContext, event as OB11Message, this.actions, this).then().catch();
}
}

View File

@@ -10,7 +10,7 @@ import { WebsocketClientConfig } from '@/onebot/config/config';
import { NapCatOneBot11Adapter } from "@/onebot";
import { IOB11NetworkAdapter } from "@/onebot/network/adapter";
export class OB11ActiveWebSocketAdapter extends IOB11NetworkAdapter<WebsocketClientConfig> {
export class OB11WebSocketClientAdapter extends IOB11NetworkAdapter<WebsocketClientConfig> {
private connection: WebSocket | null = null;
private heartbeatRef: NodeJS.Timeout | null = null;
@@ -143,7 +143,7 @@ export class OB11ActiveWebSocketAdapter extends IOB11NetworkAdapter<WebsocketCli
this.checkStateAndReply<any>(OB11Response.error('不支持的Api ' + receiveData.action, 1404, echo));
return;
}
const retdata = await action.websocketHandle(receiveData.params, echo ?? '', this.name);
const retdata = await action.websocketHandle(receiveData.params, echo ?? '', this.name, this.config);
this.checkStateAndReply<any>({ ...retdata });
}
async reload(newConfig: WebsocketClientConfig) {

View File

@@ -13,7 +13,7 @@ import { WebsocketServerConfig } from '@/onebot/config/config';
import { NapCatOneBot11Adapter } from "@/onebot";
import { IOB11NetworkAdapter } from "@/onebot/network/adapter";
export class OB11PassiveWebSocketAdapter extends IOB11NetworkAdapter<WebsocketServerConfig> {
export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketServerConfig> {
wsServer: WebSocketServer;
wsClients: WebSocket[] = [];
wsClientsMutex = new Mutex();
@@ -176,7 +176,7 @@ export class OB11PassiveWebSocketAdapter extends IOB11NetworkAdapter<WebsocketSe
this.checkStateAndReply<any>(OB11Response.error('不支持的API ' + receiveData.action, 1404, echo), wsClient);
return;
}
const retdata = await action.websocketHandle(receiveData.params, echo ?? '', this.name);
const retdata = await action.websocketHandle(receiveData.params, echo ?? '', this.name, this.config);
this.checkStateAndReply<any>({ ...retdata }, wsClient);
}

View File

@@ -75,6 +75,7 @@ export interface OB11Sender {
}
export interface OB11GroupFile {
file_size: number; // 文件大小 GOCQHTTP 群文件Api扩展
group_id: number; // 群ID
file_id: string; // 文件ID
file_name: string; // 文件名称

View File

@@ -110,7 +110,6 @@ export interface OB11MessageContext {
// 文件消息基础接口定义
export interface OB11MessageFileBase {
data: {
file_unique?: string;
path?: string;
thumb?: string;
name?: string;
@@ -165,6 +164,8 @@ export interface OB11MessageFace {
type: OB11MessageDataType.face;
data: {
id: string;
resultId?: string;
chainCount?: number;
};
}
@@ -277,4 +278,4 @@ export interface OB11PostContext {
message_type?: 'private' | 'group';
user_id?: string;
group_id?: string;
}
}

View File

@@ -1,10 +1,11 @@
import { NapCatOneBot11Adapter, OB11Message } from "@/onebot";
import { NapCatCore } from "@/core";
import { ActionMap } from "@/onebot/action";
import { OB11PluginAdapter } from "@/onebot/network/plugin";
export const plugin_onmessage = async (adapter: string, core: NapCatCore, obCtx: NapCatOneBot11Adapter, message: OB11Message, action: ActionMap) => {
export const plugin_onmessage = async (adapter: string, core: NapCatCore, obCtx: NapCatOneBot11Adapter, message: OB11Message, action: ActionMap, instance: OB11PluginAdapter) => {
if (message.raw_message === 'ping') {
const ret = await action.get('send_group_msg')?.handle({ group_id: String(message.group_id), message: 'pong' }, adapter);
const ret = await action.get('send_group_msg')?.handle({ group_id: String(message.group_id), message: 'pong' }, adapter, instance.config);
console.log(ret);
}
};

View File

@@ -29,11 +29,9 @@ import { InitWebUi } from '@/webui';
import { WebUiDataRuntime } from '@/webui/src/helper/Data';
import { napCatVersion } from '@/common/version';
import { NodeIO3MiscListener } from '@/core/listeners/NodeIO3MiscListener';
import { UmamiTrace } from '@/common/umami';
// NapCat Shell App ES 入口文件
async function handleUncaughtExceptions(logger: LogWrapper) {
process.on('uncaughtException', (err) => {
UmamiTrace.sendEvent('framework/error', { name: err.name, message: err.message });
logger.logError('[NapCat] [Error] Unhandled Exception:', err.message);
});
process.on('unhandledRejection', (reason, promise) => {
@@ -153,9 +151,8 @@ async function handleLogin(
};
loginListener.onQRCodeSessionFailed = (errType: number, errCode: number, errMsg: string) => {
UmamiTrace.sendEvent('shell/qrlogin/error', { errType, errCode, errMsg });
if (!isLogined) {
logger.logError('[Core] [Login] Login Error,ErrCode: ', errCode, ' ErrMsg:', errMsg);
logger.logError('[Core] [Login] Login Error,ErrType: ', errType, ' ErrCode:', errCode);
if (errType == 1 && errCode == 3) {
// 二维码过期刷新
}
@@ -163,9 +160,8 @@ async function handleLogin(
}
};
loginListener.onLoginFailed = (args) => {
UmamiTrace.sendEvent('shell/login/error', { args });
logger.logError('[Core] [Login] Login Error , ErrInfo: ', args.toString());
loginListener.onLoginFailed = (...args) => {
logger.logError('[Core] [Login] Login Error , ErrInfo: ', JSON.stringify(args));
};
loginService.addKernelLoginListener(proxiedListenerOf(loginListener, logger));
@@ -226,7 +222,7 @@ async function handleLogin(
logger.log(`可用于快速登录的 QQ\n${historyLoginList
.map((u, index) => `${index + 1}. ${u.uin} ${u.nickName}`)
.join('\n')
}`);
}`);
}
loginService.getQRCodePicture();
}
@@ -298,10 +294,7 @@ export async function NCoreInitShell() {
const dataTimestape = new Date().getTime().toString();
o3Service.reportAmgomWeather('login', 'a1', [dataTimestape, '0', '0']);
UmamiTrace.init(basicInfoWrapper.getFullQQVesion(), loginService.getMachineGuid());
UmamiTrace.sendTrace('shell/boot');
const selfInfo = await handleLogin(loginService, logger, pathWrapper, quickLoginUin, historyLoginList);
UmamiTrace.sendEvent('shell/login');
const amgomDataPiece = 'eb1fd6ac257461580dc7438eb099f23aae04ca679f4d88f53072dc56e3bb1129';
o3Service.setAmgomDataPiece(basicInfoWrapper.QQVersionAppid, new Uint8Array(Buffer.from(amgomDataPiece, 'hex')));

6
src/universal/LiteLoader.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
declare global {
namespace globalThis {
var LiteLoader: symbol;
}
}
export {};

View File

@@ -1,7 +1,6 @@
import { NCoreInitShell } from "@/shell/base";
export * from "@/framework/napcat";
export * from "@/shell/base";
if ((global as any).LiteLoader == undefined) {
if (global.LiteLoader == undefined) {
NCoreInitShell();
}

View File

@@ -90,7 +90,7 @@ export class WebUiConfigWrapper {
try {
const configPath = resolve(webUiPathWrapper.configPath, './webui.json');
if (!await fs.access(configPath, constants.R_OK).then(() => true).catch(() => false)) {
if (!await fs.access(configPath, constants.F_OK).then(() => true).catch(() => false)) {
await fs.writeFile(configPath, JSON.stringify(defaultconfig, null, 4));
}
@@ -101,7 +101,12 @@ export class WebUiConfigWrapper {
if (!parsedConfig.prefix.startsWith('/')) parsedConfig.prefix = '/' + parsedConfig.prefix;
if (parsedConfig.prefix.endsWith('/')) parsedConfig.prefix = parsedConfig.prefix.slice(0, -1);
// 配置已经被操作过了,还是回写一下吧,不然新配置不会出现在配置文件里
await fs.writeFile(configPath, JSON.stringify(parsedConfig, null, 4));
if (await fs.access(configPath, constants.W_OK).then(() => true).catch(() => false)) {
await fs.writeFile(configPath, JSON.stringify(parsedConfig, null, 4));
}
else {
console.warn(`文件: ${configPath} 没有写入权限, 配置的更改部分可能会在重启后还原.`);
}
// 不希望回写的配置放后面
// 查询主机地址是否可用

View File

@@ -1,10 +1,10 @@
{
"compilerOptions": {
"target": "ES2020",
"target": "ES2021",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": [
"ES2020",
"ES2021",
"DOM",
"DOM.Iterable"
],