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 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:`karin-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};
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
- * 页面本体是个 iframe,指向插件自己的配置面板 `/kkk`(原版 SPA,免登录)。
8
- * 注意:控制台把自定义页面直接挂在 #app 下,**不在布局里**,所以左侧那条图标栏
9
- * (nav.layout-activity,fixed、64px、z-index 100)和底部状态栏会盖在页面上,
10
- * 而 iframe 默认高度只有 150px。这里在挂载后量一下这两条的实际尺寸,
11
- * 把 iframe 摆到它们右边的剩余区域,并随窗口变化跟着更新。
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 { defineComponent, h, onMounted, onUnmounted, ref } from 'vue'
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 status = document.querySelector('footer.layout-status') as HTMLElement | null
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 = (status ? Math.round(status.getBoundingClientRect().height) : FALLBACK_STATUS) + 8
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
- return () => h('iframe', { src: '/kkk', style: style.value })
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 l,ref as u,onMounted as d,onUnmounted as p,h as f}from"vue";const h="kkk-console",r=64,k=28;function v(s){s.page({name:"kkk 配置",path:"/kkk-config",desc:"解析 / 面板 / 画质 / 图片切片等设置,改完直接生效",authority:4,icon:"activity:plugin",component:l({setup(){const n=u({}),t=()=>{const o=document.querySelector("nav.layout-activity"),i=document.querySelector("footer.layout-status"),c=o&&Math.round(o.getBoundingClientRect().right)||r,a=(i?Math.round(i.getBoundingClientRect().height):k)+8;n.value={position:"fixed",left:c+"px",top:"0px",width:"calc(100vw - "+c+"px)",height:"calc(100vh - "+a+"px)",border:"0",display:"block",background:"#fff",zIndex:"1"}};let e=null;return d(()=>{t(),e=setInterval(t,1e3),window.addEventListener("resize",t)}),p(()=>{e&&clearInterval(e),window.removeEventListener("resize",t)}),()=>f("iframe",{src:"/kkk",style:n.value})}})})}export{v as apply,h as name};
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(false).description('配置面板 /kkk 是否要求先登录 Koishi 控制台。默认关(免登录,打开就能改配置);装了 auth 插件且希望面板也走登录时再打开'),
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
- // 只有「不需要登录」时才自动登录;开了 auth 插件就得让用户自己登控制台
104
- if (authRequired())
105
- return raw;
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
- * 默认**免登录**:装了 auth 插件的生产环境也会直接进界面(否则用户要先登控制台才能改配置,很别扭)。
114
- * 要恢复成「必须登录控制台」,把配置里的 `webUiAuth` 打开即可。
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
- try {
127
- const rows = await database.get('token', { id: Number(match[2]), token: match[3] }, ['expiredAt']);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-kkk",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "抖音 / B站 / 快手 / 小红书 视频解析与动态推送,带 QQ 交互面板与弹幕烧录(karin-plugin-kkk 的 Koishi 移植版)",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
@@ -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(false).description('配置面板 /kkk 是否要求先登录 Koishi 控制台。默认关(免登录,打开就能改配置);装了 auth 插件且希望面板也走登录时再打开'),
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
- // 只有「不需要登录」时才自动登录;开了 auth 插件就得让用户自己登控制台
112
- if (authRequired()) return raw
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
- * 默认**免登录**:装了 auth 插件的生产环境也会直接进界面(否则用户要先登控制台才能改配置,很别扭)。
123
- * 要恢复成「必须登录控制台」,把配置里的 `webUiAuth` 打开即可。
123
+ * 默认**要求登录**:装了 auth 插件的部署,只有登录 Koishi 控制台后
124
+ * (由控制台页面通过 RPC 换取面板 token)才能打开面板;
125
+ * 没装 auth 插件的部署本来就没有登录这回事,这里不生效。
126
+ * 想改成完全公开,把配置里的 `webUiAuth` 关掉即可。
124
127
  */
125
- const authRequired = () => (config as any)?.webUiAuth === true
128
+ const authRequired = () => (config as any)?.webUiAuth !== false && !!(ctx as any).get?.('auth')
126
129
 
127
- /** 控制台登录态:auth 插件的 cookie 形如 name=id:token,用它查 token 表 */
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
- try {
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
  })