koishi-plugin-kkk 1.0.4 → 1.0.6
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 +14 -0
- package/assets/web/assets/ThemeSwitch-DFohhwd9.js +1 -1
- package/client/index.ts +46 -14
- package/dist/index.js +1 -1
- package/lib/index.js +1 -1
- package/lib/webui.js +111 -14
- package/package.json +1 -1
- package/scripts/patch-webui.mjs +2 -0
- package/src/index.ts +2 -2
- package/src/webui.ts +108 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
+
## 1.0.6
|
|
4
|
+
|
|
5
|
+
### 变更
|
|
6
|
+
- **配置面板默认要求登录**(`webUiAuth` 默认开):装了 auth 插件的部署,未登录时打开 `/kkk` 会直接看到
|
|
7
|
+
「需要先登录 Koishi 控制台」,静态资源与接口同样不放行。做法和控制台页面一致 ——
|
|
8
|
+
控制台里的「kkk 配置」页通过带权限校验的 RPC 向服务端换取一次性面板 token,再拼进 iframe 地址;
|
|
9
|
+
拿不到 token(未登录/权限不足)就不加载面板。没装 auth 插件的部署不受影响(本来也没有登录这回事)。
|
|
10
|
+
- 面板页面不再需要「HTTP 鉴权密钥」:能进来就说明已经授权,登录态由服务端自动补齐。
|
|
11
|
+
|
|
12
|
+
## 1.0.5
|
|
13
|
+
|
|
14
|
+
### 修复
|
|
15
|
+
- 「关于插件」页的大标题漏改,仍显示 `karin-plugin-kkk`,现在改为 `koishi-plugin-kkk`。
|
|
16
|
+
|
|
3
17
|
## 1.0.4
|
|
4
18
|
|
|
5
19
|
### 修复
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{C as e,m as t,r as n,t as r,u as i,w as a,yt as o}from"./index-BRROK0vh.js";var s=n(`circle-question-mark`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3`,key:`1u773s`}],[`path`,{d:`M12 17h.01`,key:`p32p05`}]]),c=n(`external-link`,[[`path`,{d:`M15 3h6v6`,key:`1q9fwt`}],[`path`,{d:`M10 14 21 3`,key:`gplh6r`}],[`path`,{d:`M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6`,key:`a6xqqp`}]]),l=n(`file-text`,[[`path`,{d:`M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z`,key:`1oefj6`}],[`path`,{d:`M14 2v5a1 1 0 0 0 1 1h5`,key:`wfsgrz`}],[`path`,{d:`M10 9H8`,key:`b1mrlr`}],[`path`,{d:`M16 13H8`,key:`t4e002`}],[`path`,{d:`M16 17H8`,key:`z1uh3a`}]]),u=n(`log-out`,[[`path`,{d:`m16 17 5-5-5-5`,key:`1bji2h`}],[`path`,{d:`M21 12H9`,key:`dn1m92`}],[`path`,{d:`M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4`,key:`1uf3rs`}]]),d=n(`menu`,[[`path`,{d:`M4 5h16`,key:`1tepv9`}],[`path`,{d:`M4 12h16`,key:`1lakjw`}],[`path`,{d:`M4 19h16`,key:`1djgab`}]]),f=n(`moon`,[[`path`,{d:`M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401`,key:`kfwtm`}]]),p=n(`package`,[[`path`,{d:`M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z`,key:`1a0edw`}],[`path`,{d:`M12 22V12`,key:`d0xqtd`}],[`polyline`,{points:`3.29 7 12 12 20.71 7`,key:`ousv84`}],[`path`,{d:`m7.5 4.27 9 5.15`,key:`1c824w`}]]),m=n(`settings`,[[`path`,{d:`M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915`,key:`1i5ecw`}],[`circle`,{cx:`12`,cy:`12`,r:`3`,key:`1v7zrd`}]]),h=n(`sun`,[[`circle`,{cx:`12`,cy:`12`,r:`4`,key:`4exip2`}],[`path`,{d:`M12 2v2`,key:`tus03m`}],[`path`,{d:`M12 20v2`,key:`1lh1kg`}],[`path`,{d:`m4.93 4.93 1.41 1.41`,key:`149t6j`}],[`path`,{d:`m17.66 17.66 1.41 1.41`,key:`ptbguv`}],[`path`,{d:`M2 12h2`,key:`1t8f8n`}],[`path`,{d:`M20 12h2`,key:`1q8mjw`}],[`path`,{d:`m6.34 17.66-1.41 1.41`,key:`1m8zz5`}],[`path`,{d:`m19.07 4.93-1.41 1.41`,key:`1shlcs`}]]),g=o(),_=()=>{let t=e=>{window.open(e,`_blank`)};return(0,g.jsxs)(e,{className:`mx-auto max-w-3xl`,children:[(0,g.jsxs)(e.Header,{className:`items-center text-center gap-4 pt-8 pb-6`,children:[(0,g.jsx)(p,{size:64,"aria-hidden":`true`}),(0,g.jsx)(e.Title,{children:`
|
|
1
|
+
import{C as e,m as t,r as n,t as r,u as i,w as a,yt as o}from"./index-BRROK0vh.js";var s=n(`circle-question-mark`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3`,key:`1u773s`}],[`path`,{d:`M12 17h.01`,key:`p32p05`}]]),c=n(`external-link`,[[`path`,{d:`M15 3h6v6`,key:`1q9fwt`}],[`path`,{d:`M10 14 21 3`,key:`gplh6r`}],[`path`,{d:`M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6`,key:`a6xqqp`}]]),l=n(`file-text`,[[`path`,{d:`M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z`,key:`1oefj6`}],[`path`,{d:`M14 2v5a1 1 0 0 0 1 1h5`,key:`wfsgrz`}],[`path`,{d:`M10 9H8`,key:`b1mrlr`}],[`path`,{d:`M16 13H8`,key:`t4e002`}],[`path`,{d:`M16 17H8`,key:`z1uh3a`}]]),u=n(`log-out`,[[`path`,{d:`m16 17 5-5-5-5`,key:`1bji2h`}],[`path`,{d:`M21 12H9`,key:`dn1m92`}],[`path`,{d:`M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4`,key:`1uf3rs`}]]),d=n(`menu`,[[`path`,{d:`M4 5h16`,key:`1tepv9`}],[`path`,{d:`M4 12h16`,key:`1lakjw`}],[`path`,{d:`M4 19h16`,key:`1djgab`}]]),f=n(`moon`,[[`path`,{d:`M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401`,key:`kfwtm`}]]),p=n(`package`,[[`path`,{d:`M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z`,key:`1a0edw`}],[`path`,{d:`M12 22V12`,key:`d0xqtd`}],[`polyline`,{points:`3.29 7 12 12 20.71 7`,key:`ousv84`}],[`path`,{d:`m7.5 4.27 9 5.15`,key:`1c824w`}]]),m=n(`settings`,[[`path`,{d:`M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915`,key:`1i5ecw`}],[`circle`,{cx:`12`,cy:`12`,r:`3`,key:`1v7zrd`}]]),h=n(`sun`,[[`circle`,{cx:`12`,cy:`12`,r:`4`,key:`4exip2`}],[`path`,{d:`M12 2v2`,key:`tus03m`}],[`path`,{d:`M12 20v2`,key:`1lh1kg`}],[`path`,{d:`m4.93 4.93 1.41 1.41`,key:`149t6j`}],[`path`,{d:`m17.66 17.66 1.41 1.41`,key:`ptbguv`}],[`path`,{d:`M2 12h2`,key:`1t8f8n`}],[`path`,{d:`M20 12h2`,key:`1q8mjw`}],[`path`,{d:`m6.34 17.66-1.41 1.41`,key:`1m8zz5`}],[`path`,{d:`m19.07 4.93-1.41 1.41`,key:`1shlcs`}]]),g=o(),_=()=>{let t=e=>{window.open(e,`_blank`)};return(0,g.jsxs)(e,{className:`mx-auto max-w-3xl`,children:[(0,g.jsxs)(e.Header,{className:`items-center text-center gap-4 pt-8 pb-6`,children:[(0,g.jsx)(p,{size:64,"aria-hidden":`true`}),(0,g.jsx)(e.Title,{children:`koishi-plugin-kkk`}),(0,g.jsx)(e.Description,{children:`版本 __KKK_VERSION__`})]}),(0,g.jsxs)(e.Content,{className:`flex flex-col gap-6 pb-8`,children:[(0,g.jsxs)(`section`,{children:[(0,g.jsx)(`h3`,{className:`mb-3 text-base font-semibold`,children:`插件简介`}),(0,g.jsx)(i,{children:`Koishi 的「抖音」「B站」视频解析/动态推送插件。 支持视频解析、动态推送、扫码登录等功能。`})]}),(0,g.jsxs)(`section`,{children:[(0,g.jsx)(`h3`,{className:`mb-3 text-base font-semibold`,children:`开发者`}),(0,g.jsx)(i,{children:`maimai993(Koishi 移植版;上游 karin-plugin-kkk by ikenxuan, sj817)`})]}),(0,g.jsxs)(`section`,{children:[(0,g.jsx)(`h3`,{className:`mb-3 text-base font-semibold`,children:`相关链接`}),(0,g.jsxs)(`div`,{className:`mt-3 flex flex-wrap gap-3`,children:[(0,g.jsxs)(a,{variant:`outline`,onPress:()=>t(`https://github.com/maimai993/koishi-plugin-kkk`),children:[(0,g.jsx)(c,{size:18,"aria-hidden":`true`}),(0,g.jsx)(`span`,{children:`GitHub 仓库`})]}),(0,g.jsxs)(a,{variant:`outline`,onPress:()=>t(`https://github.com/maimai993/koishi-plugin-kkk/issues`),children:[(0,g.jsx)(c,{size:18,"aria-hidden":`true`}),(0,g.jsx)(`span`,{children:`反馈问题`})]})]})]})]})]})},v=`https://github.com/maimai993.png`,y=({className:e})=>{let{isDark:n,toggleTheme:i}=r();return(0,g.jsxs)(t,{delay:1,closeDelay:1,children:[(0,g.jsx)(a,{isIconOnly:!0,"aria-label":n?`切换到浅色模式`:`切换到深色模式`,className:e,variant:`ghost`,onPress:i,children:n?(0,g.jsx)(h,{size:20,"aria-hidden":`true`}):(0,g.jsx)(f,{size:20,"aria-hidden":`true`})}),(0,g.jsx)(t.Content,{children:n?`浅色模式`:`深色模式`})]})};export{p as a,l as c,m as i,s as l,v as n,d as o,_ as r,u as s,y as t};
|
package/client/index.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* kkk 的控制台入口(浏览器端)。
|
|
3
3
|
*
|
|
4
|
-
* 用官方工具 `koishi-console build .` 打包(入口必须是 client/index.ts
|
|
5
|
-
* 产物在 dist/index.js,依赖已一起 bundle,浏览器可直接加载。
|
|
4
|
+
* 用官方工具 `koishi-console build .` 打包(入口必须是 client/index.ts),产物在 dist/index.js。
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* 面板本体是插件自带的 `/kkk`(独立页面)。qq-chat 那种做法是把界面注册成控制台页面、
|
|
7
|
+
* 天然跟着控制台登录态;独立 URL 没有这层保护,所以这里向服务端要一个「面板 token」:
|
|
8
|
+
* 1. `send('kkk/panel-token')` —— 服务端那个监听器带 authority: 4,
|
|
9
|
+
* 未登录 / 权限不足的控制台客户端根本调不到;
|
|
10
|
+
* 2. 拿到 token 后拼进 iframe 地址(/kkk?panel=<token>),服务端校验通过会给面板发 cookie,
|
|
11
|
+
* 后续静态资源与接口都靠它放行;
|
|
12
|
+
* 3. 拿不到 token(没登录)就只显示提示,不加载面板。
|
|
12
13
|
*/
|
|
13
|
-
import {
|
|
14
|
+
import { send } from '@koishijs/client'
|
|
15
|
+
import { computed, defineComponent, h, onMounted, onUnmounted, ref } from 'vue'
|
|
14
16
|
|
|
15
17
|
export const name = 'kkk-console'
|
|
16
18
|
|
|
@@ -23,16 +25,18 @@ export function apply (ctx: any) {
|
|
|
23
25
|
path: '/kkk-config',
|
|
24
26
|
desc: '解析 / 面板 / 画质 / 图片切片等设置,改完直接生效',
|
|
25
27
|
authority: 4,
|
|
26
|
-
// 图标名要来自控制台内置图标集(activity:*),写错了侧边栏会是一片空白
|
|
27
28
|
icon: 'activity:plugin',
|
|
28
29
|
component: defineComponent({
|
|
29
30
|
setup () {
|
|
30
31
|
const style = ref<Record<string, string>>({})
|
|
32
|
+
const status = ref<{ authRequired: boolean } | null>(null)
|
|
33
|
+
const panelToken = ref('')
|
|
34
|
+
const denied = ref(false)
|
|
31
35
|
const sync = () => {
|
|
32
36
|
const aside = document.querySelector('nav.layout-activity') as HTMLElement | null
|
|
33
|
-
const
|
|
37
|
+
const footer = document.querySelector('footer.layout-status') as HTMLElement | null
|
|
34
38
|
const left = aside ? Math.round(aside.getBoundingClientRect().right) || FALLBACK_ASIDE : FALLBACK_ASIDE
|
|
35
|
-
const bottom = (
|
|
39
|
+
const bottom = (footer ? Math.round(footer.getBoundingClientRect().height) : FALLBACK_STATUS) + 8
|
|
36
40
|
style.value = {
|
|
37
41
|
position: 'fixed',
|
|
38
42
|
left: left + 'px',
|
|
@@ -46,17 +50,45 @@ export function apply (ctx: any) {
|
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
let timer: any = null
|
|
49
|
-
onMounted(() => {
|
|
53
|
+
onMounted(async () => {
|
|
50
54
|
sync()
|
|
51
|
-
// 侧边栏会随窗口宽度/悬浮展开,量一次不够,定时校准最省心
|
|
52
55
|
timer = setInterval(sync, 1000)
|
|
53
56
|
window.addEventListener('resize', sync)
|
|
57
|
+
try {
|
|
58
|
+
const res = await fetch('/kkk/api/status').then((r) => r.json())
|
|
59
|
+
status.value = (res && res.data) || null
|
|
60
|
+
} catch {
|
|
61
|
+
status.value = null
|
|
62
|
+
}
|
|
63
|
+
if (!status.value || status.value.authRequired) {
|
|
64
|
+
try {
|
|
65
|
+
const token = await send('kkk/panel-token')
|
|
66
|
+
panelToken.value = typeof token === 'string' ? token : ''
|
|
67
|
+
} catch {
|
|
68
|
+
panelToken.value = ''
|
|
69
|
+
}
|
|
70
|
+
denied.value = !panelToken.value
|
|
71
|
+
}
|
|
54
72
|
})
|
|
55
73
|
onUnmounted(() => {
|
|
56
74
|
if (timer) clearInterval(timer)
|
|
57
75
|
window.removeEventListener('resize', sync)
|
|
58
76
|
})
|
|
59
|
-
|
|
77
|
+
const src = computed(() => {
|
|
78
|
+
const s = status.value
|
|
79
|
+
if (s && s.authRequired === false) return '/kkk'
|
|
80
|
+
return panelToken.value ? '/kkk?panel=' + encodeURIComponent(panelToken.value) : ''
|
|
81
|
+
})
|
|
82
|
+
return () => {
|
|
83
|
+
if (src.value) return h('iframe', { src: src.value, style: style.value })
|
|
84
|
+
return h('div', {
|
|
85
|
+
style: { display: 'flex', flexDirection: 'column', gap: '12px', alignItems: 'center', justifyContent: 'center', height: '100%', fontFamily: 'system-ui', color: '#666' }
|
|
86
|
+
}, [
|
|
87
|
+
h('h2', { style: { margin: 0, fontSize: '20px' } }, denied.value ? '请先登录 Koishi 控制台' : '正在打开配置面板……'),
|
|
88
|
+
denied.value ? h('p', { style: { margin: 0 } }, '登录后即可打开 kkk 配置面板。') : null,
|
|
89
|
+
denied.value ? h('a', { href: '/login', style: { color: '#3b82f6' } }, '去登录') : null
|
|
90
|
+
])
|
|
91
|
+
}
|
|
60
92
|
}
|
|
61
93
|
})
|
|
62
94
|
})
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{defineComponent as
|
|
1
|
+
import{send as v}from"@koishijs/client";import{defineComponent as y,ref as u,onMounted as h,onUnmounted as m,computed as g,h as o}from"vue";const A="kkk-console",f=64,x=28;function I(p){p.page({name:"kkk 配置",path:"/kkk-config",desc:"解析 / 面板 / 画质 / 图片切片等设置,改完直接生效",authority:4,icon:"activity:plugin",component:y({setup(){const r=u({}),t=u(null),n=u(""),l=u(false),a=()=>{const e=document.querySelector("nav.layout-activity"),i=document.querySelector("footer.layout-status"),d=e&&Math.round(e.getBoundingClientRect().right)||f,k=(i?Math.round(i.getBoundingClientRect().height):x)+8;r.value={position:"fixed",left:d+"px",top:"0px",width:"calc(100vw - "+d+"px)",height:"calc(100vh - "+k+"px)",border:"0",display:"block",background:"#fff",zIndex:"1"}};let s=null;h(async()=>{a(),s=setInterval(a,1e3),window.addEventListener("resize",a);try{const e=await fetch("/kkk/api/status").then(i=>i.json());t.value=e&&e.data||null}catch{t.value=null}if(!t.value||t.value.authRequired){try{const e=await v("kkk/panel-token");n.value=typeof e=="string"?e:""}catch{n.value=""}l.value=!n.value}}),m(()=>{s&&clearInterval(s),window.removeEventListener("resize",a)});const c=g(()=>{const e=t.value;return e&&e.authRequired===false?"/kkk":n.value?"/kkk?panel="+encodeURIComponent(n.value):""});return()=>c.value?o("iframe",{src:c.value,style:r.value}):o("div",{style:{display:"flex",flexDirection:"column",gap:"12px",alignItems:"center",justifyContent:"center",height:"100%",fontFamily:"system-ui",color:"#666"}},[o("h2",{style:{margin:0,fontSize:"20px"}},l.value?"请先登录 Koishi 控制台":"正在打开配置面板……"),l.value?o("p",{style:{margin:0}},"登录后即可打开 kkk 配置面板。"):null,l.value?o("a",{href:"/login",style:{color:"#3b82f6"}},"去登录"):null])}})})}export{I as apply,A as name};
|
package/lib/index.js
CHANGED
|
@@ -143,7 +143,7 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
143
143
|
dataPath: koishi_1.Schema.string().default('data').description('数据目录:配置、数据库、临时文件都放在这里'),
|
|
144
144
|
debug: koishi_1.Schema.boolean().default(false).description('在日志里输出调试信息,排查问题时才需要打开'),
|
|
145
145
|
autoParse: koishi_1.Schema.boolean().default(true).description('群里有人发链接(或回复一条带链接的消息)就自动解析,不用打指令'),
|
|
146
|
-
webUiAuth: koishi_1.Schema.boolean().default(
|
|
146
|
+
webUiAuth: koishi_1.Schema.boolean().default(true).description('配置面板 /kkk 是否要求先登录 Koishi 控制台(默认开)。装了 auth 插件的部署只有登录后才能打开面板;没装 auth 插件时本来就没有登录这一说,这里不生效'),
|
|
147
147
|
}).collapse().description('Koishi 原生设置(一般不用改,已折叠)'),
|
|
148
148
|
}),
|
|
149
149
|
koishi_1.Schema.object({
|
package/lib/webui.js
CHANGED
|
@@ -100,9 +100,9 @@ function registerWebUi({ ctx, config, rawConfig, logger, pluginRoot }) {
|
|
|
100
100
|
const raw = readWebFile('index.html');
|
|
101
101
|
if (!raw)
|
|
102
102
|
return Buffer.from('<h1>KKK Config</h1><p>assets/web/index.html 缺失</p>');
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
// 面板自己的登录态(Karın 版那套 accessToken)由这里自动补上:
|
|
104
|
+
// 能走到这一步说明请求已经通过鉴权(免登录,或者带着控制台换来的面板 token),
|
|
105
|
+
// 所以直接帮用户登进去,不用再面对一个「请输入 HTTP 鉴权密钥」的表单。
|
|
106
106
|
const text = raw.toString('utf-8');
|
|
107
107
|
return Buffer.from(text.includes('</body>') ? text.replace('</body>', autoLoginScript + '</body>') : text + autoLoginScript);
|
|
108
108
|
};
|
|
@@ -110,28 +110,75 @@ function registerWebUi({ ctx, config, rawConfig, logger, pluginRoot }) {
|
|
|
110
110
|
/**
|
|
111
111
|
* 面板(/kkk)是否需要登录。
|
|
112
112
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
113
|
+
* 默认**要求登录**:装了 auth 插件的部署,只有登录 Koishi 控制台后
|
|
114
|
+
* (由控制台页面通过 RPC 换取面板 token)才能打开面板;
|
|
115
|
+
* 没装 auth 插件的部署本来就没有登录这回事,这里不生效。
|
|
116
|
+
* 想改成完全公开,把配置里的 `webUiAuth` 关掉即可。
|
|
117
|
+
*/
|
|
118
|
+
const authRequired = () => config?.webUiAuth !== false && !!ctx.get?.('auth');
|
|
119
|
+
/** 打开面板时控制台会把登录 token 一起带过来,用它换一个面板自己的 cookie */
|
|
120
|
+
const tokenAuthed = async (id, token) => {
|
|
121
|
+
const aid = Number(id);
|
|
122
|
+
const value = String(token || '');
|
|
123
|
+
if (!Number.isFinite(aid) || !value)
|
|
124
|
+
return false;
|
|
125
|
+
const database = ctx.get?.('database');
|
|
126
|
+
if (!database?.get)
|
|
127
|
+
return false;
|
|
128
|
+
try {
|
|
129
|
+
const rows = await database.get('token', { id: aid, token: value }, ['expiredAt']);
|
|
130
|
+
return !!(rows?.[0] && Number(rows[0].expiredAt) > Date.now());
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* 控制台登录态。
|
|
138
|
+
*
|
|
139
|
+
* 两条路:
|
|
140
|
+
* 1. 控制台页面(左侧边栏「kkk 配置」)会带上 `?uid=&token=`,校验通过后落到面板自己的 cookie;
|
|
141
|
+
* 2. 之后页面里的静态资源和接口都靠这个 cookie 放行。
|
|
142
|
+
* 没装 auth 插件时 `authRequired()` 为假,一律放行。
|
|
115
143
|
*/
|
|
116
|
-
const authRequired = () => config?.webUiAuth === true;
|
|
117
|
-
/** 控制台登录态:auth 插件的 cookie 形如 name=id:token,用它查 token 表 */
|
|
118
144
|
const consoleAuthed = async (request) => {
|
|
119
145
|
if (!authRequired())
|
|
120
146
|
return true;
|
|
147
|
+
const cookie = String(request?.headers?.cookie || '');
|
|
148
|
+
const fromCookie = new RegExp(COOKIE_NAME + '=(\\d+):([0-9a-f]+)').exec(cookie);
|
|
149
|
+
if (fromCookie && await tokenAuthed(fromCookie[1], fromCookie[2]))
|
|
150
|
+
return true;
|
|
151
|
+
const query = request?.query ?? {};
|
|
152
|
+
// 控制台页面换来的面板 token(cookie 或 query 都认)
|
|
153
|
+
const fromPanelCookie = new RegExp(COOKIE_NAME + '_panel=([0-9a-f]+)').exec(cookie);
|
|
154
|
+
if (fromPanelCookie && panelTokenValid(fromPanelCookie[1]))
|
|
155
|
+
return true;
|
|
156
|
+
if (panelTokenValid(query.panel))
|
|
157
|
+
return true;
|
|
158
|
+
if (await tokenAuthed(query.uid, query.token))
|
|
159
|
+
return true;
|
|
160
|
+
// 兼容一些老版本控制台把 auth 放在 cookie 里的写法(name=id:token)
|
|
121
161
|
const database = ctx.get?.('database');
|
|
122
162
|
if (!database?.get)
|
|
123
163
|
return false;
|
|
124
|
-
const cookie = String(request?.headers?.cookie || '');
|
|
125
164
|
for (const match of cookie.matchAll(/(?:^|;\s*)([\w-]+)=(\d+):([0-9a-f]+)/g)) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (rows?.[0] && Number(rows[0].expiredAt) > Date.now())
|
|
129
|
-
return true;
|
|
130
|
-
}
|
|
131
|
-
catch { /* 试下一个 cookie */ }
|
|
165
|
+
if (await tokenAuthed(match[2], match[3]))
|
|
166
|
+
return true;
|
|
132
167
|
}
|
|
133
168
|
return false;
|
|
134
169
|
};
|
|
170
|
+
/** 面板自己的 cookie:`id:token` */
|
|
171
|
+
const rememberPanelToken = (response) => {
|
|
172
|
+
const query = response?.request?.query ?? {};
|
|
173
|
+
const maxAge = Math.floor(TOKEN_TTL / 1000);
|
|
174
|
+
if (query.panel && panelTokenValid(query.panel)) {
|
|
175
|
+
response.set('Set-Cookie', COOKIE_NAME + '_panel=' + String(query.panel) + '; Path=/kkk; HttpOnly; Max-Age=' + maxAge);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (query.uid && query.token) {
|
|
179
|
+
response.set('Set-Cookie', COOKIE_NAME + '=' + query.uid + ':' + query.token + '; Path=/kkk; HttpOnly; Max-Age=' + maxAge);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
135
182
|
/**
|
|
136
183
|
* 整理写回 koishi.yml 的那份配置:QQ 适配器的字段统一收进 `qq` 分组。
|
|
137
184
|
*
|
|
@@ -152,6 +199,45 @@ function registerWebUi({ ctx, config, rawConfig, logger, pluginRoot }) {
|
|
|
152
199
|
next.qq = group;
|
|
153
200
|
return next;
|
|
154
201
|
};
|
|
202
|
+
/* ---------------- 控制台 RPC:换一个面板专用 token ---------------- */
|
|
203
|
+
/**
|
|
204
|
+
* 面板 token:控制台页面(左侧边栏「kkk 配置」)通过 RPC 向服务端要一个,
|
|
205
|
+
* 再拼到 iframe 地址上(`/kkk?panel=<token>`)。服务端校验通过后给面板发 cookie,
|
|
206
|
+
* 后续静态资源与接口都靠它放行。
|
|
207
|
+
*
|
|
208
|
+
* 关键点:RPC 监听器带 `authority: 4`,**未登录或权限不足的客户端根本调不到**,
|
|
209
|
+
* 所以「不登录就打不开面板」这件事是服务端强制的,不依赖前端自觉。
|
|
210
|
+
*/
|
|
211
|
+
const panelTokens = new Map();
|
|
212
|
+
const PANEL_TOKEN_TTL = 10 * 60 * 1000;
|
|
213
|
+
const issuePanelToken = () => {
|
|
214
|
+
const value = node_crypto_1.default.randomBytes(16).toString('hex');
|
|
215
|
+
panelTokens.set(value, Date.now() + PANEL_TOKEN_TTL);
|
|
216
|
+
return value;
|
|
217
|
+
};
|
|
218
|
+
const panelTokenValid = (value) => {
|
|
219
|
+
const key = String(value || '');
|
|
220
|
+
const expire = panelTokens.get(key);
|
|
221
|
+
if (!expire)
|
|
222
|
+
return false;
|
|
223
|
+
if (expire < Date.now()) {
|
|
224
|
+
panelTokens.delete(key);
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
return true;
|
|
228
|
+
};
|
|
229
|
+
try {
|
|
230
|
+
const consoleService = ctx.console;
|
|
231
|
+
consoleService?.addListener?.('kkk/panel-token', function () {
|
|
232
|
+
// this 是发起调用的控制台客户端,auth 由 auth 插件写入
|
|
233
|
+
if (!this?.auth)
|
|
234
|
+
throw new Error('请先登录 Koishi 控制台');
|
|
235
|
+
return issuePanelToken();
|
|
236
|
+
}, { authority: 4 });
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
logger.debug('[kkk] 注册面板 token RPC 失败: ' + String(error?.message ?? error));
|
|
240
|
+
}
|
|
155
241
|
const tokens = new Map();
|
|
156
242
|
const issueToken = () => {
|
|
157
243
|
const accessToken = node_crypto_1.default.randomBytes(16).toString('hex');
|
|
@@ -254,6 +340,16 @@ function registerWebUi({ ctx, config, rawConfig, logger, pluginRoot }) {
|
|
|
254
340
|
fail(response, 500, String(error?.message ?? error));
|
|
255
341
|
}
|
|
256
342
|
});
|
|
343
|
+
/**
|
|
344
|
+
* 面板鉴权状态。控制台页面(iframe 的父页面)用它决定是直接打开面板还是提示先登录:
|
|
345
|
+
* 未登录时不要把 token 传进来,也就不会给面板发 cookie。
|
|
346
|
+
*/
|
|
347
|
+
server.get('/kkk/api/status', async (response) => {
|
|
348
|
+
ok(response, {
|
|
349
|
+
authRequired: authRequired(),
|
|
350
|
+
authed: await consoleAuthed(response),
|
|
351
|
+
}, '');
|
|
352
|
+
});
|
|
257
353
|
/* ---------------- 数据接口 ---------------- */
|
|
258
354
|
const botList = () => {
|
|
259
355
|
const bots = ctx.bots ?? [];
|
|
@@ -339,6 +435,7 @@ function registerWebUi({ ctx, config, rawConfig, logger, pluginRoot }) {
|
|
|
339
435
|
server.get(route, async (response) => {
|
|
340
436
|
if (await pageDenied(response))
|
|
341
437
|
return;
|
|
438
|
+
rememberPanelToken(response);
|
|
342
439
|
response.type = 'text/html; charset=utf-8';
|
|
343
440
|
response.body = indexHtml();
|
|
344
441
|
});
|
package/package.json
CHANGED
package/scripts/patch-webui.mjs
CHANGED
|
@@ -163,6 +163,8 @@ const TEXT_REPLACERS = [
|
|
|
163
163
|
[/https:\/\/github\.com\/ikenxuan\/karin-plugin-kkk/g, 'https://github.com/maimai993/koishi-plugin-kkk'],
|
|
164
164
|
[/https:\/\/github\.com\/ikenxuan\.png/g, 'https://github.com/maimai993.png'],
|
|
165
165
|
[/ikenxuan, sj817/g, 'maimai993(Koishi 移植版;上游 karin-plugin-kkk by ikenxuan, sj817)'],
|
|
166
|
+
// 「关于插件」页的大标题(JSX 里是反引号字符串)
|
|
167
|
+
[/`karin-plugin-kkk`/g, '`koishi-plugin-kkk`'],
|
|
166
168
|
[/版本 2\.33\.0/g, '版本 __KKK_VERSION__'],
|
|
167
169
|
[/Karin 插件配置管理面板/g, 'koishi-plugin-kkk 配置面板'],
|
|
168
170
|
[/\bKarin\b/g, 'Koishi'],
|
package/src/index.ts
CHANGED
|
@@ -95,7 +95,7 @@ export interface Config {
|
|
|
95
95
|
debug: boolean
|
|
96
96
|
/** 消息里的链接自动解析 */
|
|
97
97
|
autoParse: boolean
|
|
98
|
-
/** 配置面板(/kkk)是否要求先登录 Koishi
|
|
98
|
+
/** 配置面板(/kkk)是否要求先登录 Koishi 控制台(默认开;装了 auth 插件时生效) */
|
|
99
99
|
webUiAuth: boolean
|
|
100
100
|
/** QQ 平台解析前先发交互面板(Markdown + 按钮)让用户选解析内容和画质 */
|
|
101
101
|
qqPanel: boolean
|
|
@@ -139,7 +139,7 @@ export const Config: Schema<Config> = Schema.intersect([
|
|
|
139
139
|
dataPath: Schema.string().default('data').description('数据目录:配置、数据库、临时文件都放在这里'),
|
|
140
140
|
debug: Schema.boolean().default(false).description('在日志里输出调试信息,排查问题时才需要打开'),
|
|
141
141
|
autoParse: Schema.boolean().default(true).description('群里有人发链接(或回复一条带链接的消息)就自动解析,不用打指令'),
|
|
142
|
-
webUiAuth: Schema.boolean().default(
|
|
142
|
+
webUiAuth: Schema.boolean().default(true).description('配置面板 /kkk 是否要求先登录 Koishi 控制台(默认开)。装了 auth 插件的部署只有登录后才能打开面板;没装 auth 插件时本来就没有登录这一说,这里不生效'),
|
|
143
143
|
}).collapse().description('Koishi 原生设置(一般不用改,已折叠)'),
|
|
144
144
|
}),
|
|
145
145
|
Schema.object({
|
package/src/webui.ts
CHANGED
|
@@ -108,8 +108,9 @@ export function registerWebUi ({ ctx, config, rawConfig, logger, pluginRoot }: W
|
|
|
108
108
|
const indexHtml = (): Buffer => {
|
|
109
109
|
const raw = readWebFile('index.html')
|
|
110
110
|
if (!raw) return Buffer.from('<h1>KKK Config</h1><p>assets/web/index.html 缺失</p>')
|
|
111
|
-
//
|
|
112
|
-
|
|
111
|
+
// 面板自己的登录态(Karın 版那套 accessToken)由这里自动补上:
|
|
112
|
+
// 能走到这一步说明请求已经通过鉴权(免登录,或者带着控制台换来的面板 token),
|
|
113
|
+
// 所以直接帮用户登进去,不用再面对一个「请输入 HTTP 鉴权密钥」的表单。
|
|
113
114
|
const text = raw.toString('utf-8')
|
|
114
115
|
return Buffer.from(text.includes('</body>') ? text.replace('</body>', autoLoginScript + '</body>') : text + autoLoginScript)
|
|
115
116
|
}
|
|
@@ -119,26 +120,69 @@ export function registerWebUi ({ ctx, config, rawConfig, logger, pluginRoot }: W
|
|
|
119
120
|
/**
|
|
120
121
|
* 面板(/kkk)是否需要登录。
|
|
121
122
|
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
123
|
+
* 默认**要求登录**:装了 auth 插件的部署,只有登录 Koishi 控制台后
|
|
124
|
+
* (由控制台页面通过 RPC 换取面板 token)才能打开面板;
|
|
125
|
+
* 没装 auth 插件的部署本来就没有登录这回事,这里不生效。
|
|
126
|
+
* 想改成完全公开,把配置里的 `webUiAuth` 关掉即可。
|
|
124
127
|
*/
|
|
125
|
-
const authRequired = () => (config as any)?.webUiAuth
|
|
128
|
+
const authRequired = () => (config as any)?.webUiAuth !== false && !!(ctx as any).get?.('auth')
|
|
126
129
|
|
|
127
|
-
/**
|
|
130
|
+
/** 打开面板时控制台会把登录 token 一起带过来,用它换一个面板自己的 cookie */
|
|
131
|
+
const tokenAuthed = async (id: unknown, token: unknown): Promise<boolean> => {
|
|
132
|
+
const aid = Number(id)
|
|
133
|
+
const value = String(token || '')
|
|
134
|
+
if (!Number.isFinite(aid) || !value) return false
|
|
135
|
+
const database: any = (ctx as any).get?.('database')
|
|
136
|
+
if (!database?.get) return false
|
|
137
|
+
try {
|
|
138
|
+
const rows = await database.get('token', { id: aid, token: value }, ['expiredAt'])
|
|
139
|
+
return !!(rows?.[0] && Number(rows[0].expiredAt) > Date.now())
|
|
140
|
+
} catch {
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 控制台登录态。
|
|
147
|
+
*
|
|
148
|
+
* 两条路:
|
|
149
|
+
* 1. 控制台页面(左侧边栏「kkk 配置」)会带上 `?uid=&token=`,校验通过后落到面板自己的 cookie;
|
|
150
|
+
* 2. 之后页面里的静态资源和接口都靠这个 cookie 放行。
|
|
151
|
+
* 没装 auth 插件时 `authRequired()` 为假,一律放行。
|
|
152
|
+
*/
|
|
128
153
|
const consoleAuthed = async (request: any): Promise<boolean> => {
|
|
129
154
|
if (!authRequired()) return true
|
|
155
|
+
const cookie = String(request?.headers?.cookie || '')
|
|
156
|
+
const fromCookie = new RegExp(COOKIE_NAME + '=(\\d+):([0-9a-f]+)').exec(cookie)
|
|
157
|
+
if (fromCookie && await tokenAuthed(fromCookie[1], fromCookie[2])) return true
|
|
158
|
+
const query = request?.query ?? {}
|
|
159
|
+
// 控制台页面换来的面板 token(cookie 或 query 都认)
|
|
160
|
+
const fromPanelCookie = new RegExp(COOKIE_NAME + '_panel=([0-9a-f]+)').exec(cookie)
|
|
161
|
+
if (fromPanelCookie && panelTokenValid(fromPanelCookie[1])) return true
|
|
162
|
+
if (panelTokenValid(query.panel)) return true
|
|
163
|
+
if (await tokenAuthed(query.uid, query.token)) return true
|
|
164
|
+
// 兼容一些老版本控制台把 auth 放在 cookie 里的写法(name=id:token)
|
|
130
165
|
const database: any = (ctx as any).get?.('database')
|
|
131
166
|
if (!database?.get) return false
|
|
132
|
-
const cookie = String(request?.headers?.cookie || '')
|
|
133
167
|
for (const match of cookie.matchAll(/(?:^|;\s*)([\w-]+)=(\d+):([0-9a-f]+)/g)) {
|
|
134
|
-
|
|
135
|
-
const rows = await database.get('token', { id: Number(match[2]), token: match[3] }, ['expiredAt'])
|
|
136
|
-
if (rows?.[0] && Number(rows[0].expiredAt) > Date.now()) return true
|
|
137
|
-
} catch { /* 试下一个 cookie */ }
|
|
168
|
+
if (await tokenAuthed(match[2], match[3])) return true
|
|
138
169
|
}
|
|
139
170
|
return false
|
|
140
171
|
}
|
|
141
172
|
|
|
173
|
+
/** 面板自己的 cookie:`id:token` */
|
|
174
|
+
const rememberPanelToken = (response: any) => {
|
|
175
|
+
const query = response?.request?.query ?? {}
|
|
176
|
+
const maxAge = Math.floor(TOKEN_TTL / 1000)
|
|
177
|
+
if (query.panel && panelTokenValid(query.panel)) {
|
|
178
|
+
response.set('Set-Cookie', COOKIE_NAME + '_panel=' + String(query.panel) + '; Path=/kkk; HttpOnly; Max-Age=' + maxAge)
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
if (query.uid && query.token) {
|
|
182
|
+
response.set('Set-Cookie', COOKIE_NAME + '=' + query.uid + ':' + query.token + '; Path=/kkk; HttpOnly; Max-Age=' + maxAge)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
142
186
|
/**
|
|
143
187
|
* 整理写回 koishi.yml 的那份配置:QQ 适配器的字段统一收进 `qq` 分组。
|
|
144
188
|
*
|
|
@@ -159,6 +203,47 @@ export function registerWebUi ({ ctx, config, rawConfig, logger, pluginRoot }: W
|
|
|
159
203
|
return next
|
|
160
204
|
}
|
|
161
205
|
|
|
206
|
+
/* ---------------- 控制台 RPC:换一个面板专用 token ---------------- */
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 面板 token:控制台页面(左侧边栏「kkk 配置」)通过 RPC 向服务端要一个,
|
|
210
|
+
* 再拼到 iframe 地址上(`/kkk?panel=<token>`)。服务端校验通过后给面板发 cookie,
|
|
211
|
+
* 后续静态资源与接口都靠它放行。
|
|
212
|
+
*
|
|
213
|
+
* 关键点:RPC 监听器带 `authority: 4`,**未登录或权限不足的客户端根本调不到**,
|
|
214
|
+
* 所以「不登录就打不开面板」这件事是服务端强制的,不依赖前端自觉。
|
|
215
|
+
*/
|
|
216
|
+
const panelTokens = new Map<string, number>()
|
|
217
|
+
const PANEL_TOKEN_TTL = 10 * 60 * 1000
|
|
218
|
+
|
|
219
|
+
const issuePanelToken = (): string => {
|
|
220
|
+
const value = crypto.randomBytes(16).toString('hex')
|
|
221
|
+
panelTokens.set(value, Date.now() + PANEL_TOKEN_TTL)
|
|
222
|
+
return value
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const panelTokenValid = (value: unknown): boolean => {
|
|
226
|
+
const key = String(value || '')
|
|
227
|
+
const expire = panelTokens.get(key)
|
|
228
|
+
if (!expire) return false
|
|
229
|
+
if (expire < Date.now()) {
|
|
230
|
+
panelTokens.delete(key)
|
|
231
|
+
return false
|
|
232
|
+
}
|
|
233
|
+
return true
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const consoleService: any = (ctx as any).console
|
|
238
|
+
consoleService?.addListener?.('kkk/panel-token', function (this: any) {
|
|
239
|
+
// this 是发起调用的控制台客户端,auth 由 auth 插件写入
|
|
240
|
+
if (!this?.auth) throw new Error('请先登录 Koishi 控制台')
|
|
241
|
+
return issuePanelToken()
|
|
242
|
+
}, { authority: 4 })
|
|
243
|
+
} catch (error: any) {
|
|
244
|
+
logger.debug('[kkk] 注册面板 token RPC 失败: ' + String(error?.message ?? error))
|
|
245
|
+
}
|
|
246
|
+
|
|
162
247
|
const tokens = new Map<string, number>()
|
|
163
248
|
|
|
164
249
|
const issueToken = () => {
|
|
@@ -263,6 +348,17 @@ export function registerWebUi ({ ctx, config, rawConfig, logger, pluginRoot }: W
|
|
|
263
348
|
}
|
|
264
349
|
})
|
|
265
350
|
|
|
351
|
+
/**
|
|
352
|
+
* 面板鉴权状态。控制台页面(iframe 的父页面)用它决定是直接打开面板还是提示先登录:
|
|
353
|
+
* 未登录时不要把 token 传进来,也就不会给面板发 cookie。
|
|
354
|
+
*/
|
|
355
|
+
server.get('/kkk/api/status', async (response: any) => {
|
|
356
|
+
ok(response, {
|
|
357
|
+
authRequired: authRequired(),
|
|
358
|
+
authed: await consoleAuthed(response),
|
|
359
|
+
}, '')
|
|
360
|
+
})
|
|
361
|
+
|
|
266
362
|
/* ---------------- 数据接口 ---------------- */
|
|
267
363
|
|
|
268
364
|
const botList = () => {
|
|
@@ -348,6 +444,7 @@ export function registerWebUi ({ ctx, config, rawConfig, logger, pluginRoot }: W
|
|
|
348
444
|
for (const route of ['/kkk', '/kkk/', '/kkk/login']) {
|
|
349
445
|
server.get(route, async (response: any) => {
|
|
350
446
|
if (await pageDenied(response)) return
|
|
447
|
+
rememberPanelToken(response)
|
|
351
448
|
response.type = 'text/html; charset=utf-8'
|
|
352
449
|
response.body = indexHtml()
|
|
353
450
|
})
|