koishi-plugin-kkk 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/client/index.ts +50 -32
- package/dist/index.js +1 -1
- package/lib/karin/module/utils/Base.js +19 -9
- package/lib/qqFields.json +10 -0
- package/lib/webui.js +43 -18
- package/package.json +1 -1
- package/src/karin/module/utils/Base.ts +506 -497
- package/src/qqFields.json +10 -0
- package/src/webui.ts +38 -17
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
+
## 1.0.9
|
|
4
|
+
|
|
5
|
+
### 变更
|
|
6
|
+
- **面板不再从独立 URL 进**:`/kkk`、`/kkk/edit` 以及 `/kkk/assets/*` 在未授权时直接返回 404
|
|
7
|
+
(以前是一个「需要先登录」的提示页)。面板只从控制台侧边栏「kkk 配置」进,
|
|
8
|
+
凭据由控制台页面通过 RPC 换取的面板 token 交付。
|
|
9
|
+
- **QQ 平台单独的视频体积阈值**:新增配置项 `qqGroupFileLimitMB`(QQ 适配器分组,默认 30)——
|
|
10
|
+
视频超过该体积时改用「群文件」发送,不再受上游 `usegroupfile` 开关影响;
|
|
11
|
+
其它平台仍按上游的「群文件上传」+ `groupfilevalue` 判断。
|
|
12
|
+
|
|
13
|
+
### 修复
|
|
14
|
+
- 之前 30MB~100MB 的视频没有按群文件发送:运行时配置里 `groupfilevalue` 是 100(不是 30),
|
|
15
|
+
这一区间会走视频通道,被 QQ 压缩/改名。现在 QQ 用上面的独立阈值判断。
|
|
16
|
+
|
|
17
|
+
## 1.0.8
|
|
18
|
+
|
|
19
|
+
### 修复
|
|
20
|
+
- 控制台入口包加载失败导致**侧边栏「kkk 配置」整个消失**:上一版加 RPC 失败日志时把 `try / else` 括错了,
|
|
21
|
+
注册面板路由的那段代码跑到作用域外(`ReferenceError: server is not defined`),现在结构修正。
|
|
22
|
+
- 面板前端包禁用缓存(`/@plugin-<id>/index.js` 加 `no-store`):控制台默认不带缓存头,
|
|
23
|
+
浏览器会一直用旧 bundle,表现为「代码改了但界面没变」。
|
|
24
|
+
|
|
25
|
+
### 变更
|
|
26
|
+
- 控制台页面取登录凭据改成两条路依次尝试(RPC → store.user),都不行时**把失败原因显示在页面上**,
|
|
27
|
+
方便一眼看出是没登录还是别的问题。
|
|
28
|
+
|
|
3
29
|
## 1.0.7
|
|
4
30
|
|
|
5
31
|
### 修复
|
package/client/index.ts
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* kkk 的控制台入口(浏览器端)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 2. 拿到 token 后拼进 iframe 地址(/kkk?panel=<token>),服务端校验通过会给面板发 cookie,
|
|
11
|
-
* 后续静态资源与接口都靠它放行;
|
|
12
|
-
* 3. 拿不到 token(没登录)就只显示提示,不加载面板。
|
|
4
|
+
* 面板本体是插件自带的 `/kkk`(独立页面),独立 URL 没有控制台那层保护,
|
|
5
|
+
* 所以这里负责把「控制台已登录」这件事告诉面板。两条路依次尝试:
|
|
6
|
+
* 1. RPC `kkk/panel-token`(服务端监听器带 authority: 4,未登录根本调不到);
|
|
7
|
+
* 2. 控制台 store 里的 user。
|
|
8
|
+
* 拿到凭据后拼进 iframe 地址;两条路都不行才显示「请先登录」,并把最后一条错误原因带上,
|
|
9
|
+
* 方便一眼看出是没登录还是别的什么问题。
|
|
13
10
|
*/
|
|
14
|
-
import { send } from '@koishijs/client'
|
|
11
|
+
import { send, store } from '@koishijs/client'
|
|
15
12
|
import { computed, defineComponent, h, onMounted, onUnmounted, ref } from 'vue'
|
|
16
13
|
|
|
17
14
|
export const name = 'kkk-console'
|
|
@@ -30,8 +27,9 @@ export function apply (ctx: any) {
|
|
|
30
27
|
setup () {
|
|
31
28
|
const style = ref<Record<string, string>>({})
|
|
32
29
|
const status = ref<{ authRequired: boolean } | null>(null)
|
|
33
|
-
const
|
|
34
|
-
const
|
|
30
|
+
const target = ref('')
|
|
31
|
+
const reason = ref('')
|
|
32
|
+
const ready = ref(false)
|
|
35
33
|
const sync = () => {
|
|
36
34
|
const aside = document.querySelector('nav.layout-activity') as HTMLElement | null
|
|
37
35
|
const footer = document.querySelector('footer.layout-status') as HTMLElement | null
|
|
@@ -50,46 +48,66 @@ export function apply (ctx: any) {
|
|
|
50
48
|
}
|
|
51
49
|
}
|
|
52
50
|
let timer: any = null
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
window.addEventListener('resize', sync)
|
|
51
|
+
|
|
52
|
+
const openPanel = async () => {
|
|
53
|
+
let required = true
|
|
57
54
|
try {
|
|
58
55
|
const res = await fetch('/kkk/api/status').then((r) => r.json())
|
|
59
56
|
status.value = (res && res.data) || null
|
|
57
|
+
required = !status.value || status.value.authRequired !== false
|
|
60
58
|
} catch {
|
|
61
59
|
status.value = null
|
|
62
60
|
}
|
|
63
|
-
if (!
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
if (!required) { target.value = '/kkk'; ready.value = true; return }
|
|
62
|
+
|
|
63
|
+
// 1. RPC 换面板 token
|
|
64
|
+
try {
|
|
65
|
+
const token = await send('kkk/panel-token')
|
|
66
|
+
if (typeof token === 'string' && token) { target.value = '/kkk?panel=' + token; ready.value = true; return }
|
|
67
|
+
reason.value = 'RPC 没有返回 token'
|
|
68
|
+
} catch (error: any) {
|
|
69
|
+
reason.value = 'RPC 失败: ' + String(error?.message ?? error)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 2. store 里的 user(老版本控制台可能只在这里放登录信息)
|
|
73
|
+
try {
|
|
74
|
+
const user: any = (store as any)?.user
|
|
75
|
+
if (user?.token && user?.id) {
|
|
76
|
+
target.value = '/kkk?uid=' + user.id + '&token=' + user.token
|
|
77
|
+
ready.value = true
|
|
78
|
+
return
|
|
69
79
|
}
|
|
70
|
-
|
|
80
|
+
} catch (error: any) {
|
|
81
|
+
reason.value = reason.value || '读取 store.user 失败: ' + String(error?.message ?? error)
|
|
71
82
|
}
|
|
83
|
+
ready.value = true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
onMounted(() => {
|
|
87
|
+
sync()
|
|
88
|
+
timer = setInterval(sync, 1000)
|
|
89
|
+
window.addEventListener('resize', sync)
|
|
90
|
+
void openPanel()
|
|
72
91
|
})
|
|
73
92
|
onUnmounted(() => {
|
|
74
93
|
if (timer) clearInterval(timer)
|
|
75
94
|
window.removeEventListener('resize', sync)
|
|
76
95
|
})
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (s && s.authRequired === false) return '/kkk'
|
|
80
|
-
return panelToken.value ? '/kkk?panel=' + encodeURIComponent(panelToken.value) : ''
|
|
81
|
-
})
|
|
96
|
+
|
|
97
|
+
const src = computed(() => target.value)
|
|
82
98
|
return () => {
|
|
83
99
|
if (src.value) return h('iframe', { src: src.value, style: style.value })
|
|
100
|
+
if (!ready.value) return h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: '#666', fontFamily: 'system-ui' } }, '正在打开配置面板……')
|
|
84
101
|
return h('div', {
|
|
85
102
|
style: { display: 'flex', flexDirection: 'column', gap: '12px', alignItems: 'center', justifyContent: 'center', height: '100%', fontFamily: 'system-ui', color: '#666' }
|
|
86
103
|
}, [
|
|
87
|
-
h('h2', { style: { margin: 0, fontSize: '20px' } },
|
|
88
|
-
|
|
89
|
-
|
|
104
|
+
h('h2', { style: { margin: 0, fontSize: '20px' } }, '请先登录 Koishi 控制台'),
|
|
105
|
+
h('p', { style: { margin: 0 } }, '登录后即可打开 kkk 配置面板。'),
|
|
106
|
+
reason.value ? h('p', { style: { margin: 0, fontSize: '12px', color: '#999' } }, reason.value) : null,
|
|
107
|
+
h('a', { href: '/login', style: { color: '#3b82f6' } }, '去登录')
|
|
90
108
|
])
|
|
91
109
|
}
|
|
92
110
|
}
|
|
93
111
|
})
|
|
94
112
|
})
|
|
95
|
-
}
|
|
113
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{send as
|
|
1
|
+
import{send as m,store as g}from"@koishijs/client";import{defineComponent as h,ref as i,onMounted as x,onUnmounted as C,computed as S,h as t}from"vue";const L="kkk-console",k=64,w=28;function b(p){p.page({name:"kkk 配置",path:"/kkk-config",desc:"解析 / 面板 / 画质 / 图片切片等设置,改完直接生效",authority:4,icon:"activity:plugin",component:h({setup(){const v=i({}),r=i(null),s=i(""),n=i(""),o=i(false),u=()=>{const a=document.querySelector("nav.layout-activity"),l=document.querySelector("footer.layout-status"),e=a&&Math.round(a.getBoundingClientRect().right)||k,f=(l?Math.round(l.getBoundingClientRect().height):w)+8;v.value={position:"fixed",left:e+"px",top:"0px",width:"calc(100vw - "+e+"px)",height:"calc(100vh - "+f+"px)",border:"0",display:"block",background:"#fff",zIndex:"1"}};let c=null;const y=async()=>{var l;let a=true;try{const e=await fetch("/kkk/api/status").then(f=>f.json());r.value=e&&e.data||null,a=!r.value||r.value.authRequired!==false}catch{r.value=null}if(!a){s.value="/kkk",o.value=true;return}try{const e=await m("kkk/panel-token");if(typeof e=="string"&&e){s.value="/kkk?panel="+e,o.value=true;return}n.value="RPC 没有返回 token"}catch(e){n.value="RPC 失败: "+String((e==null?void 0:e.message)??e)}try{const e=(l=g)==null?void 0:l.user;if(e!=null&&e.token&&(e!=null&&e.id)){s.value="/kkk?uid="+e.id+"&token="+e.token,o.value=true;return}}catch(e){n.value=n.value||"读取 store.user 失败: "+String((e==null?void 0:e.message)??e)}o.value=true};x(()=>{u(),c=setInterval(u,1e3),window.addEventListener("resize",u),y()}),C(()=>{c&&clearInterval(c),window.removeEventListener("resize",u)});const d=S(()=>s.value);return()=>d.value?t("iframe",{src:d.value,style:v.value}):o.value?t("div",{style:{display:"flex",flexDirection:"column",gap:"12px",alignItems:"center",justifyContent:"center",height:"100%",fontFamily:"system-ui",color:"#666"}},[t("h2",{style:{margin:0,fontSize:"20px"}},"请先登录 Koishi 控制台"),t("p",{style:{margin:0}},"登录后即可打开 kkk 配置面板。"),n.value?t("p",{style:{margin:0,fontSize:"12px",color:"#999"}},n.value):null,t("a",{href:"/login",style:{color:"#3b82f6"}},"去登录")]):t("div",{style:{display:"flex",alignItems:"center",justifyContent:"center",height:"100%",color:"#666",fontFamily:"system-ui"}},"正在打开配置面板……")}})})}export{b as apply,L as name};
|
|
@@ -179,17 +179,27 @@ const uploadFile = async (event, file, videoUrl, options) => {
|
|
|
179
179
|
];
|
|
180
180
|
await node_karin_1.default.sendMsg(selfId, contact, message2);
|
|
181
181
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
182
|
+
/**
|
|
183
|
+
* 判断是否使用群文件上传。
|
|
184
|
+
*
|
|
185
|
+
* QQ 单独一套阈值(配置项 `qqGroupFileLimitMB`,默认 30):超过就按群文件发。
|
|
186
|
+
* 原因是 QQ 对富媒体视频会压缩、改名甚至丢掉后缀,几十 MB 的视频走「视频」通道基本没法看;
|
|
187
|
+
* 其它平台仍旧按上游的「群文件上传」开关 + `groupfilevalue` 走。
|
|
188
|
+
*/
|
|
189
|
+
const platform = String(event?.platform ?? event?.bot?.platform ?? '');
|
|
190
|
+
const qqLimit = Number(Config_1.Config.qqGroupFileLimitMB ?? 30);
|
|
191
|
+
const useGroupFile = /qq/i.test(platform) && qqLimit > 0
|
|
192
|
+
? newFileSize > qqLimit
|
|
193
|
+
: Config_1.Config.app.usegroupfile && newFileSize > Config_1.Config.app.groupfilevalue;
|
|
194
|
+
if (options)
|
|
195
|
+
options.useGroupFile = useGroupFile;
|
|
196
|
+
if (Config_1.Config.app.videoSendMode === 'base64' && !useGroupFile) {
|
|
187
197
|
const videoBuffer = node_fs_1.default.readFileSync(file.filepath);
|
|
188
198
|
File = `base64://${videoBuffer.toString('base64')}`;
|
|
189
199
|
node_karin_1.logger.mark(`已开启视频文件 base64转换 正在进行${node_karin_1.logger.yellow('base64转换中')}...`);
|
|
190
200
|
}
|
|
191
201
|
else
|
|
192
|
-
File =
|
|
202
|
+
File = useGroupFile ? file.filepath : `file://${file.filepath}`;
|
|
193
203
|
/**
|
|
194
204
|
* 发送前:撤掉上一条「收到请求,开始下载」,并回一句「发送中…」。
|
|
195
205
|
* 大文件上传(几十 MB 走文件通道)要等好几秒,没有任何反馈的话用户会以为机器人卡住了。
|
|
@@ -200,7 +210,7 @@ const uploadFile = async (event, file, videoUrl, options) => {
|
|
|
200
210
|
try {
|
|
201
211
|
if (!options?.active) {
|
|
202
212
|
await (0, QqPanel_1.recallLastPanel)(event);
|
|
203
|
-
const tipText =
|
|
213
|
+
const tipText = useGroupFile
|
|
204
214
|
? '发送中…(超过 QQ 限制,将以群文件发送)'
|
|
205
215
|
: '发送中…';
|
|
206
216
|
const tip = await event.reply(tipText);
|
|
@@ -213,7 +223,7 @@ const uploadFile = async (event, file, videoUrl, options) => {
|
|
|
213
223
|
try {
|
|
214
224
|
// 是主动消息
|
|
215
225
|
if (options?.active) {
|
|
216
|
-
if (
|
|
226
|
+
if (useGroupFile) {
|
|
217
227
|
// 是群文件
|
|
218
228
|
const bot = node_karin_1.default.getBot(String(options.activeOption?.uin));
|
|
219
229
|
node_karin_1.logger.mark(`${node_karin_1.logger.blue('主动消息:')} 视频大小: ${newFileSize.toFixed(1)}MB 正在通过${node_karin_1.logger.yellow('bot.uploadFile')}回复...`);
|
|
@@ -228,7 +238,7 @@ const uploadFile = async (event, file, videoUrl, options) => {
|
|
|
228
238
|
}
|
|
229
239
|
else {
|
|
230
240
|
// 不是主动消息
|
|
231
|
-
if (
|
|
241
|
+
if (useGroupFile) {
|
|
232
242
|
// 是文件
|
|
233
243
|
node_karin_1.logger.mark(`${node_karin_1.logger.blue('被动消息:')} 视频大小: ${newFileSize.toFixed(1)}MB 正在通过${node_karin_1.logger.yellow('e.bot.uploadFile')}回复...`);
|
|
234
244
|
await event.bot.uploadFile(event.contact, File, file.originTitle ? `${file.originTitle}.mp4` : `${File.split('/').pop()}`);
|
package/lib/qqFields.json
CHANGED
|
@@ -71,5 +71,15 @@
|
|
|
71
71
|
"label": "卡片文字识别密钥",
|
|
72
72
|
"secret": true,
|
|
73
73
|
"description": "群里转发过来的分享卡片没有链接,只能靠截图识别出标题和作者再去搜索。填自己的免费密钥更稳定:https://ocr.space/ocrapi 。留空会用公共测试密钥,容易被限流。"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"key": "qqGroupFileLimitMB",
|
|
77
|
+
"group": "视频发送",
|
|
78
|
+
"type": "number",
|
|
79
|
+
"default": 30,
|
|
80
|
+
"min": 0,
|
|
81
|
+
"max": 2000,
|
|
82
|
+
"label": "群文件发送阈值(MB)",
|
|
83
|
+
"description": "视频体积超过这个值时,改用「群文件」发送。QQ 对富媒体视频会压缩、改名甚至丢掉后缀,走群文件才能原样发出去;填 0 表示不启用。"
|
|
74
84
|
}
|
|
75
85
|
]
|
package/lib/webui.js
CHANGED
|
@@ -229,15 +229,20 @@ function registerWebUi({ ctx, config, rawConfig, logger, pluginRoot }) {
|
|
|
229
229
|
};
|
|
230
230
|
try {
|
|
231
231
|
const consoleService = ctx.console;
|
|
232
|
-
consoleService?.addListener
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
232
|
+
if (typeof consoleService?.addListener !== 'function') {
|
|
233
|
+
logger.warn('[kkk] 控制台没有 addListener,面板 token RPC 未注册(面板将无法从控制台获取登录态)');
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
consoleService.addListener('kkk/panel-token', function () {
|
|
237
|
+
// this 是发起调用的控制台客户端,auth 由 auth 插件写入
|
|
238
|
+
if (!this?.auth)
|
|
239
|
+
throw new Error('请先登录 Koishi 控制台');
|
|
240
|
+
return issuePanelToken();
|
|
241
|
+
}, { authority: 4 });
|
|
242
|
+
}
|
|
238
243
|
}
|
|
239
244
|
catch (error) {
|
|
240
|
-
logger.
|
|
245
|
+
logger.warn('[kkk] 注册面板 token RPC 失败: ' + String(error?.message ?? error));
|
|
241
246
|
}
|
|
242
247
|
const tokens = new Map();
|
|
243
248
|
const issueToken = () => {
|
|
@@ -401,20 +406,38 @@ function registerWebUi({ ctx, config, rawConfig, logger, pluginRoot }) {
|
|
|
401
406
|
return false;
|
|
402
407
|
if (await consoleAuthed(response))
|
|
403
408
|
return false;
|
|
404
|
-
|
|
405
|
-
response.
|
|
406
|
-
response.
|
|
407
|
-
|
|
408
|
-
'<div style="font-family:system-ui;padding:40px;line-height:1.8">',
|
|
409
|
-
'<h2>需要先登录 Koishi 控制台</h2>',
|
|
410
|
-
'<p>配置面板已开启登录校验(插件配置里的 <code>webUiAuth</code>)。</p>',
|
|
411
|
-
'<p><a href="/">去登录控制台</a>,登录后再回到本页面。</p>',
|
|
412
|
-
'</div>',
|
|
413
|
-
].join('');
|
|
409
|
+
// 直接当成「没有这个页面」:面板只从控制台侧边栏进,独立 URL 不给任何提示
|
|
410
|
+
response.status = 404;
|
|
411
|
+
response.type = 'text/plain; charset=utf-8';
|
|
412
|
+
response.body = 'Not Found';
|
|
414
413
|
return true;
|
|
415
414
|
};
|
|
415
|
+
/**
|
|
416
|
+
* 控制台下发的插件前端包(`/@plugin-<id>/index.js`)默认没有缓存头,
|
|
417
|
+
* 浏览器会拿旧的 bundle 一直用(改了客户端逻辑却看不到效果)。
|
|
418
|
+
* 这里给本插件自己的入口加 no-store —— qq-chat 也是这么处理的。
|
|
419
|
+
*/
|
|
420
|
+
const isOwnEntry = (requestPath) => {
|
|
421
|
+
if (!requestPath.includes('/@plugin-'))
|
|
422
|
+
return false;
|
|
423
|
+
const entries = ctx.console?.entries ?? {};
|
|
424
|
+
for (const key of Object.keys(entries)) {
|
|
425
|
+
if (!requestPath.startsWith('/@plugin-' + key))
|
|
426
|
+
continue;
|
|
427
|
+
const files = entries[key]?.files;
|
|
428
|
+
const list = Array.isArray(files) ? files : [files?.dev, files?.prod];
|
|
429
|
+
if (list.some((file) => typeof file === 'string' && file.includes('koishi-plugin-kkk')))
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
return false;
|
|
433
|
+
};
|
|
416
434
|
server.use(async (response, next) => {
|
|
417
435
|
const requestPath = String(response.path || '');
|
|
436
|
+
if (isOwnEntry(requestPath)) {
|
|
437
|
+
await next();
|
|
438
|
+
response.set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
418
441
|
if (!requestPath.startsWith('/kkk/assets/'))
|
|
419
442
|
return next();
|
|
420
443
|
if (await pageDenied(response))
|
|
@@ -446,7 +469,9 @@ function registerWebUi({ ctx, config, rawConfig, logger, pluginRoot }) {
|
|
|
446
469
|
});
|
|
447
470
|
}
|
|
448
471
|
/* ---------------- 简易编辑页(改插件配置) ---------------- */
|
|
449
|
-
server.get('/kkk/edit', (response) => {
|
|
472
|
+
server.get('/kkk/edit', async (response) => {
|
|
473
|
+
if (await pageDenied(response))
|
|
474
|
+
return;
|
|
450
475
|
let body;
|
|
451
476
|
try {
|
|
452
477
|
body = node_fs_1.default.readFileSync(node_path_1.default.join(pluginRoot, 'assets', 'webui.html'), 'utf-8');
|