koishi-plugin-kkk 1.0.6 → 1.0.8
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 +23 -0
- package/client/index.ts +50 -32
- package/dist/index.js +1 -1
- package/lib/webui.js +42 -8
- package/package.json +1 -1
- package/src/webui.ts +37 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
+
## 1.0.8
|
|
4
|
+
|
|
5
|
+
### 修复
|
|
6
|
+
- 控制台入口包加载失败导致**侧边栏「kkk 配置」整个消失**:上一版加 RPC 失败日志时把 `try / else` 括错了,
|
|
7
|
+
注册面板路由的那段代码跑到作用域外(`ReferenceError: server is not defined`),现在结构修正。
|
|
8
|
+
- 面板前端包禁用缓存(`/@plugin-<id>/index.js` 加 `no-store`):控制台默认不带缓存头,
|
|
9
|
+
浏览器会一直用旧 bundle,表现为「代码改了但界面没变」。
|
|
10
|
+
|
|
11
|
+
### 变更
|
|
12
|
+
- 控制台页面取登录凭据改成两条路依次尝试(RPC → store.user),都不行时**把失败原因显示在页面上**,
|
|
13
|
+
方便一眼看出是没登录还是别的问题。
|
|
14
|
+
|
|
15
|
+
## 1.0.7
|
|
16
|
+
|
|
17
|
+
### 修复
|
|
18
|
+
- **面板报「加载配置失败 / 鉴权失败: 缺少authorization」**:面板自己的登录 token 存在服务端内存里,
|
|
19
|
+
Koishi 一重启就失效,而自动登录脚本看到 localStorage 里已经有 token 就跳过登录,
|
|
20
|
+
于是一直拿着废 token 请求、所有接口 401。现在:
|
|
21
|
+
- 每次打开面板都会重新换一个新的 token(不再因为 localStorage 有值就跳过);
|
|
22
|
+
- 接口鉴权同时接受「控制台换来的面板 cookie」,即使前端 token 过期也能正常读写配置。
|
|
23
|
+
- 1.0.6 引入的登录校验配合这次修复才算完整:现在(装了 auth 插件时)未登录打不开面板、
|
|
24
|
+
登录后配置能正常加载与保存。
|
|
25
|
+
|
|
3
26
|
## 1.0.6
|
|
4
27
|
|
|
5
28
|
### 变更
|
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};
|
package/lib/webui.js
CHANGED
|
@@ -78,7 +78,8 @@ function registerWebUi({ ctx, config, rawConfig, logger, pluginRoot }) {
|
|
|
78
78
|
// 面板把登录态存在 localStorage 的 accessToken / userId / refreshToken 三个键里,
|
|
79
79
|
// 登录页只是个表单壳子 —— 与其去点 DOM(React 受控组件很脆),不如直接调登录接口把凭据写进去再刷新。
|
|
80
80
|
try {
|
|
81
|
-
|
|
81
|
+
// 每次都重新换一次:面板自己的 token 存在服务端内存里,Koishi 重启就失效,
|
|
82
|
+
// 之前这里看到 localStorage 有值就直接 return,结果一直拿着废 token 请求,全部 401。
|
|
82
83
|
// 防呆:万一接口一直不给凭据,也别在这里无限刷新
|
|
83
84
|
if (sessionStorage.getItem('kkk-autologin') === '1') return;
|
|
84
85
|
sessionStorage.setItem('kkk-autologin', '1');
|
|
@@ -228,15 +229,20 @@ function registerWebUi({ ctx, config, rawConfig, logger, pluginRoot }) {
|
|
|
228
229
|
};
|
|
229
230
|
try {
|
|
230
231
|
const consoleService = ctx.console;
|
|
231
|
-
consoleService?.addListener
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
+
}
|
|
237
243
|
}
|
|
238
244
|
catch (error) {
|
|
239
|
-
logger.
|
|
245
|
+
logger.warn('[kkk] 注册面板 token RPC 失败: ' + String(error?.message ?? error));
|
|
240
246
|
}
|
|
241
247
|
const tokens = new Map();
|
|
242
248
|
const issueToken = () => {
|
|
@@ -249,6 +255,10 @@ function registerWebUi({ ctx, config, rawConfig, logger, pluginRoot }) {
|
|
|
249
255
|
const authed = (request) => {
|
|
250
256
|
if (!authRequired())
|
|
251
257
|
return true;
|
|
258
|
+
// 面板 cookie(控制台换来的)本身就是授权凭据,接口请求直接用账号密码也不用再走一遍
|
|
259
|
+
const panelCookie = new RegExp(COOKIE_NAME + '_panel=([0-9a-f]+)').exec(String(request?.headers?.cookie || ''));
|
|
260
|
+
if (panelCookie && panelTokenValid(panelCookie[1]))
|
|
261
|
+
return true;
|
|
252
262
|
const header = String(request?.headers?.authorization || request?.headers?.['x-access-token'] || '');
|
|
253
263
|
if (header && tokens.has(header.replace(/^Bearer\s+/i, '')))
|
|
254
264
|
return true;
|
|
@@ -408,8 +418,32 @@ function registerWebUi({ ctx, config, rawConfig, logger, pluginRoot }) {
|
|
|
408
418
|
].join('');
|
|
409
419
|
return true;
|
|
410
420
|
};
|
|
421
|
+
/**
|
|
422
|
+
* 控制台下发的插件前端包(`/@plugin-<id>/index.js`)默认没有缓存头,
|
|
423
|
+
* 浏览器会拿旧的 bundle 一直用(改了客户端逻辑却看不到效果)。
|
|
424
|
+
* 这里给本插件自己的入口加 no-store —— qq-chat 也是这么处理的。
|
|
425
|
+
*/
|
|
426
|
+
const isOwnEntry = (requestPath) => {
|
|
427
|
+
if (!requestPath.includes('/@plugin-'))
|
|
428
|
+
return false;
|
|
429
|
+
const entries = ctx.console?.entries ?? {};
|
|
430
|
+
for (const key of Object.keys(entries)) {
|
|
431
|
+
if (!requestPath.startsWith('/@plugin-' + key))
|
|
432
|
+
continue;
|
|
433
|
+
const files = entries[key]?.files;
|
|
434
|
+
const list = Array.isArray(files) ? files : [files?.dev, files?.prod];
|
|
435
|
+
if (list.some((file) => typeof file === 'string' && file.includes('koishi-plugin-kkk')))
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
return false;
|
|
439
|
+
};
|
|
411
440
|
server.use(async (response, next) => {
|
|
412
441
|
const requestPath = String(response.path || '');
|
|
442
|
+
if (isOwnEntry(requestPath)) {
|
|
443
|
+
await next();
|
|
444
|
+
response.set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
413
447
|
if (!requestPath.startsWith('/kkk/assets/'))
|
|
414
448
|
return next();
|
|
415
449
|
if (await pageDenied(response))
|
package/package.json
CHANGED
package/src/webui.ts
CHANGED
|
@@ -86,7 +86,8 @@ export function registerWebUi ({ ctx, config, rawConfig, logger, pluginRoot }: W
|
|
|
86
86
|
// 面板把登录态存在 localStorage 的 accessToken / userId / refreshToken 三个键里,
|
|
87
87
|
// 登录页只是个表单壳子 —— 与其去点 DOM(React 受控组件很脆),不如直接调登录接口把凭据写进去再刷新。
|
|
88
88
|
try {
|
|
89
|
-
|
|
89
|
+
// 每次都重新换一次:面板自己的 token 存在服务端内存里,Koishi 重启就失效,
|
|
90
|
+
// 之前这里看到 localStorage 有值就直接 return,结果一直拿着废 token 请求,全部 401。
|
|
90
91
|
// 防呆:万一接口一直不给凭据,也别在这里无限刷新
|
|
91
92
|
if (sessionStorage.getItem('kkk-autologin') === '1') return;
|
|
92
93
|
sessionStorage.setItem('kkk-autologin', '1');
|
|
@@ -235,13 +236,17 @@ export function registerWebUi ({ ctx, config, rawConfig, logger, pluginRoot }: W
|
|
|
235
236
|
|
|
236
237
|
try {
|
|
237
238
|
const consoleService: any = (ctx as any).console
|
|
238
|
-
consoleService?.addListener
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
239
|
+
if (typeof consoleService?.addListener !== 'function') {
|
|
240
|
+
logger.warn('[kkk] 控制台没有 addListener,面板 token RPC 未注册(面板将无法从控制台获取登录态)')
|
|
241
|
+
} else {
|
|
242
|
+
consoleService.addListener('kkk/panel-token', function (this: any) {
|
|
243
|
+
// this 是发起调用的控制台客户端,auth 由 auth 插件写入
|
|
244
|
+
if (!this?.auth) throw new Error('请先登录 Koishi 控制台')
|
|
245
|
+
return issuePanelToken()
|
|
246
|
+
}, { authority: 4 })
|
|
247
|
+
}
|
|
243
248
|
} catch (error: any) {
|
|
244
|
-
logger.
|
|
249
|
+
logger.warn('[kkk] 注册面板 token RPC 失败: ' + String(error?.message ?? error))
|
|
245
250
|
}
|
|
246
251
|
|
|
247
252
|
const tokens = new Map<string, number>()
|
|
@@ -256,6 +261,9 @@ export function registerWebUi ({ ctx, config, rawConfig, logger, pluginRoot }: W
|
|
|
256
261
|
|
|
257
262
|
const authed = (request: any): boolean => {
|
|
258
263
|
if (!authRequired()) return true
|
|
264
|
+
// 面板 cookie(控制台换来的)本身就是授权凭据,接口请求直接用账号密码也不用再走一遍
|
|
265
|
+
const panelCookie = new RegExp(COOKIE_NAME + '_panel=([0-9a-f]+)').exec(String(request?.headers?.cookie || ''))
|
|
266
|
+
if (panelCookie && panelTokenValid(panelCookie[1])) return true
|
|
259
267
|
const header = String(request?.headers?.authorization || request?.headers?.['x-access-token'] || '')
|
|
260
268
|
if (header && tokens.has(header.replace(/^Bearer\s+/i, ''))) return true
|
|
261
269
|
const cookie = String(request?.headers?.cookie || '')
|
|
@@ -419,8 +427,30 @@ export function registerWebUi ({ ctx, config, rawConfig, logger, pluginRoot }: W
|
|
|
419
427
|
return true
|
|
420
428
|
}
|
|
421
429
|
|
|
430
|
+
/**
|
|
431
|
+
* 控制台下发的插件前端包(`/@plugin-<id>/index.js`)默认没有缓存头,
|
|
432
|
+
* 浏览器会拿旧的 bundle 一直用(改了客户端逻辑却看不到效果)。
|
|
433
|
+
* 这里给本插件自己的入口加 no-store —— qq-chat 也是这么处理的。
|
|
434
|
+
*/
|
|
435
|
+
const isOwnEntry = (requestPath: string): boolean => {
|
|
436
|
+
if (!requestPath.includes('/@plugin-')) return false
|
|
437
|
+
const entries: any = (ctx as any).console?.entries ?? {}
|
|
438
|
+
for (const key of Object.keys(entries)) {
|
|
439
|
+
if (!requestPath.startsWith('/@plugin-' + key)) continue
|
|
440
|
+
const files: any = entries[key]?.files
|
|
441
|
+
const list = Array.isArray(files) ? files : [files?.dev, files?.prod]
|
|
442
|
+
if (list.some((file: any) => typeof file === 'string' && file.includes('koishi-plugin-kkk'))) return true
|
|
443
|
+
}
|
|
444
|
+
return false
|
|
445
|
+
}
|
|
446
|
+
|
|
422
447
|
server.use(async (response: any, next: any) => {
|
|
423
448
|
const requestPath = String(response.path || '')
|
|
449
|
+
if (isOwnEntry(requestPath)) {
|
|
450
|
+
await next()
|
|
451
|
+
response.set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
|
452
|
+
return
|
|
453
|
+
}
|
|
424
454
|
if (!requestPath.startsWith('/kkk/assets/')) return next()
|
|
425
455
|
if (await pageDenied(response)) return
|
|
426
456
|
const relative = decodeURIComponent(requestPath.slice('/kkk/assets/'.length))
|