koishi-plugin-kkk 3.2.2 → 3.3.0-beta.2
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 +17 -1
- package/assets/web/assets/index-BRROK0vh.js +1 -1
- package/lib/compat/node-karin.js +13 -1
- package/lib/index.js +25 -5
- package/lib/karin/module/utils/CardParser.js +13 -80
- package/lib/karin/module/utils/ErrorReport.js +8 -2
- package/lib/karin/platform/kuaishou/kuaishou.js +3 -1
- package/lib/karin/platform/xiaohongshu/xiaohongshu.js +3 -1
- package/lib/webui.js +37 -2
- package/package.json +1 -1
- package/scripts/patch-webui.mjs +70 -0
- package/scripts/smoke-console-hidden.cjs +70 -0
- package/scripts/smoke-imports.cjs +93 -0
- package/scripts/smoke-panel-tabs.cjs +58 -0
- package/src/compat/node-karin.ts +13 -1
- package/src/index.ts +26 -7
- package/src/karin/module/utils/CardParser.ts +13 -80
- package/src/karin/module/utils/ErrorReport.ts +8 -2
- package/src/karin/platform/kuaishou/kuaishou.ts +2 -0
- package/src/karin/platform/xiaohongshu/xiaohongshu.ts +2 -0
- package/src/webui.ts +40 -2
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 面板(/kkk)分类冒烟测试。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖两件事:
|
|
5
|
+
* 1. 「Koishi 设置」这一组分类确实注入进了面板前端包(分类入口 / 分发 case / 4 个字段路径),
|
|
6
|
+
* 并且**没有 masters**(主人账号属于 Koishi 权限体系,面板是免登录页面,不给它开口子);
|
|
7
|
+
* 2. 服务端(src/webui.ts)的白名单与面板字段一一对应 —— 前端能改的,服务端就认这几个;
|
|
8
|
+
* 前端塞别的东西进来,服务端会丢掉。
|
|
9
|
+
*
|
|
10
|
+
* 用法:node scripts/smoke-panel-tabs.cjs
|
|
11
|
+
*/
|
|
12
|
+
const fs = require('node:fs')
|
|
13
|
+
const path = require('node:path')
|
|
14
|
+
|
|
15
|
+
const pluginRoot = path.resolve(__dirname, '..')
|
|
16
|
+
const webAssets = path.join(pluginRoot, 'assets', 'web', 'assets')
|
|
17
|
+
const BT = String.fromCharCode(96)
|
|
18
|
+
|
|
19
|
+
const results = []
|
|
20
|
+
const check = (name, ok, detail) => {
|
|
21
|
+
results.push({ name, ok })
|
|
22
|
+
console.log((ok ? ' ✅ ' : ' ❌ ') + name + (detail ? ' —— ' + detail : ''))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const bundle = fs.readdirSync(webAssets).filter((name) => /^index-.*[.]js$/.test(name))
|
|
26
|
+
.map((name) => path.join(webAssets, name))
|
|
27
|
+
.sort((a, b) => fs.statSync(b).size - fs.statSync(a).size)[0]
|
|
28
|
+
const text = fs.readFileSync(bundle, 'utf8')
|
|
29
|
+
const count = (needle) => text.split(needle).length - 1
|
|
30
|
+
|
|
31
|
+
console.log('\n[1] 面板里多了「Koishi 设置」分类(' + path.basename(bundle) + ')')
|
|
32
|
+
check('分类入口存在且只有一个', count('{key:' + BT + 'koishi' + BT) === 1, '× ' + count('{key:' + BT + 'koishi' + BT))
|
|
33
|
+
check('分发 case 存在且只有一个', count('case' + BT + 'koishi' + BT) === 1, '× ' + count('case' + BT + 'koishi' + BT))
|
|
34
|
+
check('组件定义存在', count('KKKNativeConfig=') === 1)
|
|
35
|
+
|
|
36
|
+
console.log('\n[2] 这一组里是 4 个字段,且没有 masters')
|
|
37
|
+
for (const key of ['dataPath', 'debug', 'autoParse', 'webUiAuth']) {
|
|
38
|
+
const needle = BT + 'koishi' + BT + ',' + BT + key + BT
|
|
39
|
+
check('字段路径 ' + key, count(needle) === 1, '× ' + count(needle))
|
|
40
|
+
}
|
|
41
|
+
check('没有 masters(面板不给主人账号开口子)', count(BT + 'koishi' + BT + ',' + BT + 'masters' + BT) === 0)
|
|
42
|
+
|
|
43
|
+
console.log('\n[3] 没有把 undefined 塞进 JSX children(拼串时容易多一个逗号)')
|
|
44
|
+
check('不存在 children:[, 这种空位', count('children:[,') === 0)
|
|
45
|
+
|
|
46
|
+
console.log('\n[4] 服务端白名单与面板字段一一对应')
|
|
47
|
+
const webui = fs.readFileSync(path.join(pluginRoot, 'src', 'webui.ts'), 'utf8')
|
|
48
|
+
const match = webui.match(/const NATIVE_PANEL_KEYS = \[([^\]]*)\]/)
|
|
49
|
+
const keys = match ? match[1].split(',').map((item) => item.trim().replace(/['"]/g, '')).filter(Boolean) : []
|
|
50
|
+
check('找到 NATIVE_PANEL_KEYS', keys.length > 0, keys.join(', '))
|
|
51
|
+
check('白名单正好是面板那 4 个字段', JSON.stringify(keys) === JSON.stringify(['dataPath', 'debug', 'autoParse', 'webUiAuth']), JSON.stringify(keys))
|
|
52
|
+
check('白名单里没有 masters', !keys.includes('masters'))
|
|
53
|
+
check('GET 会把这一组返回给面板', /koishi: readNativePanelOptions\(config\)/.test(webui))
|
|
54
|
+
check('POST 会把这一组写回插件配置顶层', /const \{ qq, koishi: nativeGroup, \.\.\.upstream \} = normalizeLists\(body\)/.test(webui))
|
|
55
|
+
|
|
56
|
+
const failed = results.filter((item) => !item.ok)
|
|
57
|
+
console.log('\n=== ' + (results.length - failed.length) + '/' + results.length + ' 通过 ===')
|
|
58
|
+
process.exitCode = failed.length ? 1 : 0
|
package/src/compat/node-karin.ts
CHANGED
|
@@ -14,6 +14,9 @@ import path from 'node:path'
|
|
|
14
14
|
import { h } from 'koishi'
|
|
15
15
|
import type { Bot, Context, Session } from 'koishi'
|
|
16
16
|
|
|
17
|
+
// 版本比较复用注入器那边的实现(它处理了 -beta.1 这类预发布号的先后)
|
|
18
|
+
import { isSemverGreater } from '../karin/module/utils/semver'
|
|
19
|
+
|
|
17
20
|
import { logger } from './logger'
|
|
18
21
|
import { COLLECTED_MESSAGE_ID, collectForward } from './forward-collect'
|
|
19
22
|
import { UnconfirmedSendError, classifySendFailure, describeSendFailure, isPassiveLimitFailure } from './sendError'
|
|
@@ -963,9 +966,18 @@ export const checkPkgUpdate = async (name: string, _options?: { compare?: string
|
|
|
963
966
|
const response = await fetch('https://registry.npmmirror.com/' + name, { signal: AbortSignal.timeout(10000) })
|
|
964
967
|
if (!response.ok) return { status: 'error' as const, error: new Error('HTTP ' + response.status) }
|
|
965
968
|
const meta: any = await response.json()
|
|
966
|
-
const
|
|
969
|
+
const tags: Record<string, string> = meta?.['dist-tags'] ?? {}
|
|
970
|
+
/**
|
|
971
|
+
* 预览版(3.3.0-beta.1 这种)**不能拿 latest 比**:本地跑 beta 时 latest 还在 3.2.2,
|
|
972
|
+
* 按老逻辑会报「有新版本 3.2.2」—— 那其实是**降级**。
|
|
973
|
+
* 所以带 - 的本地版本改看 beta 通道;beta 没有(说明预览已经并进正式版)才回退到 latest,
|
|
974
|
+
* 并且只在远端确实比本地新时才说「有更新」。
|
|
975
|
+
*/
|
|
976
|
+
const isPreview = local.includes('-')
|
|
977
|
+
const remote = (isPreview ? (tags.beta || tags.latest) : tags.latest) || ''
|
|
967
978
|
if (!remote) return { status: 'error' as const, error: new Error('响应缺少 dist-tags.latest') }
|
|
968
979
|
if (remote === local) return { status: 'no' as const, local }
|
|
980
|
+
if (isPreview && !isSemverGreater(remote, local)) return { status: 'no' as const, local }
|
|
969
981
|
return { status: 'yes' as const, local, remote }
|
|
970
982
|
} catch (error) {
|
|
971
983
|
return { status: 'error' as const, error: error as Error }
|
package/src/index.ts
CHANGED
|
@@ -129,13 +129,32 @@ export interface Config {
|
|
|
129
129
|
/** 摊平后由「Koishi 原生设置」分组提供的字段 */
|
|
130
130
|
const NATIVE_KEYS = ['masters', 'dataPath', 'debug', 'autoParse', 'webUiAuth']
|
|
131
131
|
|
|
132
|
+
/**
|
|
133
|
+
* 控制台里**唯一可见**的一段文字:把用户指到 WebUI 面板去。
|
|
134
|
+
*
|
|
135
|
+
* 为什么把设置全藏了:同一份配置在控制台改一半、在面板改一半,两边都会把整份配置写回
|
|
136
|
+
* koishi.yml,很容易互相覆盖(用户实际遇到过「面板里关了、控制台一保存又回来了」)。
|
|
137
|
+
* 现在控制台不再提供编辑入口,只留这块说明。
|
|
138
|
+
*
|
|
139
|
+
* 注意:**隐藏不等于删除** —— 字段仍然在 schema 里声明着,所以在控制台里点保存
|
|
140
|
+
* 一样会把已有值带上,面板里的设置不会被清空。
|
|
141
|
+
*/
|
|
142
|
+
const WEBUI_GUIDE = [
|
|
143
|
+
'**所有设置都在 WebUI 配置面板里改**,地址:/kkk(打开 Koishi 控制台后,左侧边栏也有一个「**kkk 配置**」入口)。',
|
|
144
|
+
'',
|
|
145
|
+
'面板里包含:接口库 / 通用(含**错误上报**、在线播放器、合并转发)/ 抖音 / 哔哩哔哩 / 快手 / 小红书 / **QQ 适配器** / 推送列表。' +
|
|
146
|
+
'改完点右下角保存即可 —— 会写回 koishi.yml 并热重载,**不用重启**。',
|
|
147
|
+
'',
|
|
148
|
+
'**Koishi 控制台里的设置项已经全部隐藏**(值仍然保留,在这里点保存也不会把它们弄丢),免得两边各改一半、互相覆盖。',
|
|
149
|
+
'',
|
|
150
|
+
'面板里还有一组「**Koishi 设置**」(数据目录、调试日志、自动解析、面板是否要求先登录控制台)。' +
|
|
151
|
+
'只有 masters(主人账号)没有面板入口 —— 它属于 Koishi 的权限体系,而面板是免登录页面,' +
|
|
152
|
+
'需要直接改 koishi.yml 里本插件的配置段(改完重启 Koishi)。'
|
|
153
|
+
].join('\n')
|
|
154
|
+
|
|
132
155
|
export const Config: Schema<Config> = Schema.intersect([
|
|
133
156
|
Schema.object({
|
|
134
|
-
webuiGuide: Schema.const('').description(
|
|
135
|
-
'**本插件的配置都在配置面板里改**:Koishi 控制台左侧边栏 →「**kkk 配置**」。\n\n' +
|
|
136
|
-
'(原版配置面板:接口库 / 通用 / 抖音 / 哔哩哔哩 / 快手 / 小红书 / **QQ 适配器** / 推送列表,' +
|
|
137
|
-
'改完点右下角保存即可,写回 koishi.yml 并热重载,不用重启。)'
|
|
138
|
-
),
|
|
157
|
+
webuiGuide: Schema.const('').description(WEBUI_GUIDE),
|
|
139
158
|
// 全部隐藏:控制台表单里不显示,配置统一在控制台侧边栏的「kkk 配置」面板里改
|
|
140
159
|
qq: buildQqSchema(Schema).hidden().description('QQ 适配器(只对 QQ 平台生效)'),
|
|
141
160
|
advanced: Schema.object({
|
|
@@ -145,7 +164,7 @@ export const Config: Schema<Config> = Schema.intersect([
|
|
|
145
164
|
autoParse: Schema.boolean().default(true).description('群里有人发链接(或回复一条带链接的消息)就自动解析,不用打指令'),
|
|
146
165
|
webUiAuth: Schema.boolean().default(true).description('配置面板 /kkk 是否要求先登录 Koishi 控制台(默认开)。装了 auth 插件的部署只有登录后才能打开面板;没装 auth 插件时本来就没有登录这一说,这里不生效'),
|
|
147
166
|
}).collapse().hidden().description('Koishi 原生设置(一般不用改,已折叠)'),
|
|
148
|
-
}),
|
|
167
|
+
}).description('配置入口:请在 WebUI 面板(/kkk)里修改'),
|
|
149
168
|
Schema.object({
|
|
150
169
|
/**
|
|
151
170
|
* 合并转发(两级开关)。
|
|
@@ -182,7 +201,7 @@ export const Config: Schema<Config> = Schema.intersect([
|
|
|
182
201
|
xiaohongshu: Schema.boolean().default(false).description('小红书:单独打开合并转发(全局关着时生效)'),
|
|
183
202
|
xiaohongshuContent: Schema.array(Schema.union(['text', 'image', 'video', 'file'])).default([])
|
|
184
203
|
.description('小红书转发合并哪些内容(留空 = 用全局那份)')
|
|
185
|
-
}).description('合并转发(全局 + 各平台)'),
|
|
204
|
+
}).collapse().hidden().description('合并转发(全局 + 各平台)'),
|
|
186
205
|
upstream: buildUpstreamSchema(pluginRootDir).hidden().description(
|
|
187
206
|
'插件自身的配置(与 Karin 版 config.json 一致)。每项都带着上游默认值,枚举型字段是下拉框;' +
|
|
188
207
|
'**与默认值不同**的项会在启动时写回 config.json,保持默认值的项不写(这样你直接改文件的内容不会被覆盖)'
|
|
@@ -18,7 +18,6 @@ import { createHash } from 'node:crypto'
|
|
|
18
18
|
import { logger } from 'node-karin'
|
|
19
19
|
|
|
20
20
|
import { tryGetRuntime } from '../../../compat/runtime'
|
|
21
|
-
import { douyinFetcher } from './amagiClient'
|
|
22
21
|
import { Config } from './Config'
|
|
23
22
|
|
|
24
23
|
/** 卡片里挖出来的信息 */
|
|
@@ -417,55 +416,6 @@ export const searchBiliVideos = async (
|
|
|
417
416
|
}
|
|
418
417
|
}
|
|
419
418
|
|
|
420
|
-
/* ------------------------------------------------------------------ *
|
|
421
|
-
* 抖音搜索(直接用 amagi 的 search 端点)
|
|
422
|
-
* ------------------------------------------------------------------ */
|
|
423
|
-
|
|
424
|
-
export const searchDouyinWorks = async (
|
|
425
|
-
keyword: string,
|
|
426
|
-
limit = 8,
|
|
427
|
-
authors: string[] = []
|
|
428
|
-
): Promise<Array<{ aweme_id: string; desc: string; author: string; score: number }>> => {
|
|
429
|
-
try {
|
|
430
|
-
// 这版接口库的抖音 fetcher 不一定有 search(实测 6.6.0 上没有),没有就干脆跳过
|
|
431
|
-
const fetcher: any = douyinFetcher as any
|
|
432
|
-
if (typeof fetcher?.search !== 'function') {
|
|
433
|
-
logger.debug('[卡片解析] 当前接口库没有抖音搜索能力,跳过')
|
|
434
|
-
return []
|
|
435
|
-
}
|
|
436
|
-
const res: any = await fetcher.search({ query: String(keyword ?? '').trim(), type: 'video', number: limit })
|
|
437
|
-
const list: any[] =
|
|
438
|
-
res?.data?.data?.aweme_list ?? res?.data?.aweme_list ?? res?.aweme_list ?? []
|
|
439
|
-
const titleKey = normalizeText(keyword)
|
|
440
|
-
const authorKeys = authors.map((item) => normalizeText(item)).filter((item) => item.length >= 2)
|
|
441
|
-
return list
|
|
442
|
-
.filter((item: any) => item?.aweme_id)
|
|
443
|
-
.map((item: any, index: number) => {
|
|
444
|
-
const desc = String(item.desc ?? '')
|
|
445
|
-
const sim = titleSimilarity(normalizeText(desc), titleKey)
|
|
446
|
-
// 作者对上一样加分:抖音搜索噪声大,光靠标题常常分不出是哪一条
|
|
447
|
-
const authorKey = normalizeText(String(item.author?.nickname ?? ''))
|
|
448
|
-
let authorScore = 0
|
|
449
|
-
for (const key of authorKeys) {
|
|
450
|
-
if (!authorKey) break
|
|
451
|
-
if (authorKey === key) { authorScore = 60; break }
|
|
452
|
-
if (authorKey.includes(key) || key.includes(authorKey)) authorScore = Math.max(authorScore, 35)
|
|
453
|
-
}
|
|
454
|
-
return {
|
|
455
|
-
aweme_id: String(item.aweme_id),
|
|
456
|
-
desc,
|
|
457
|
-
author: String(item.author?.nickname ?? ''),
|
|
458
|
-
score: Math.round(sim * 100) + authorScore - index
|
|
459
|
-
}
|
|
460
|
-
})
|
|
461
|
-
.sort((left, right) => right.score - left.score)
|
|
462
|
-
.slice(0, Math.max(1, Math.min(20, limit)))
|
|
463
|
-
} catch (error: any) {
|
|
464
|
-
logger.warn('[卡片解析] 抖音搜索异常: ' + String(error?.message ?? error))
|
|
465
|
-
return []
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
419
|
/* ------------------------------------------------------------------ *
|
|
470
420
|
* 对外入口
|
|
471
421
|
* ------------------------------------------------------------------ */
|
|
@@ -477,6 +427,7 @@ export const searchDouyinWorks = async (
|
|
|
477
427
|
* 任何一步失败都返回 null(调用方据此走原来的「未找到链接」逻辑,不影响已有功能)。
|
|
478
428
|
*/
|
|
479
429
|
export type CardCandidate = {
|
|
430
|
+
/** 卡片解析只可能落在 B站上(抖音没有卡片消息) */
|
|
480
431
|
platform: 'bilibili' | 'douyin'
|
|
481
432
|
id: string
|
|
482
433
|
title: string
|
|
@@ -527,12 +478,7 @@ export const resolveCardToUrl = async (
|
|
|
527
478
|
return null
|
|
528
479
|
}
|
|
529
480
|
|
|
530
|
-
|
|
531
|
-
const hint = String(card.source ?? '') + ' ' + card.title + ' ' + card.desc + ' ' + upName + ' ' + ocrText
|
|
532
|
-
const looksDouyin = /抖音|douyin|快手|ks\./i.test(hint)
|
|
533
|
-
const looksBili = /bilibili|哔哩|B站|UP主/i.test(hint)
|
|
534
|
-
|
|
535
|
-
// ③ 先按最可能的平台搜,命中就返回
|
|
481
|
+
// ③ B站搜一把:标题 + 作者都对上才自动继续,否则把候选交给用户挑
|
|
536
482
|
const tryBili = async (): Promise<{ url?: string; candidates: CardCandidate[]; matchedAuthor?: string }> => {
|
|
537
483
|
const hits = await searchBiliVideos(keyword, card.title, upNames, 8)
|
|
538
484
|
const candidates = hits.slice(0, 6).map((item) => ({
|
|
@@ -547,30 +493,17 @@ export const resolveCardToUrl = async (
|
|
|
547
493
|
}
|
|
548
494
|
return { candidates }
|
|
549
495
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
const order = looksDouyin && !looksBili ? [tryDouyin, tryBili] : [tryBili, tryDouyin]
|
|
562
|
-
const candidates: CardCandidate[] = []
|
|
563
|
-
for (const attempt of order) {
|
|
564
|
-
const hit = await attempt()
|
|
565
|
-
if (hit) {
|
|
566
|
-
candidates.push(...hit.candidates)
|
|
567
|
-
if (hit.url) {
|
|
568
|
-
logger.mark('[卡片解析] 定位成功: ' + hit.url)
|
|
569
|
-
// 提示语里报「真正匹配上的那个 UP 名」,而不是卡片摘要里那个不准的
|
|
570
|
-
const matched = (hit as any).matchedAuthor || upName
|
|
571
|
-
return { url: hit.url, platform: hit.url.includes('bilibili') ? 'bilibili' : 'douyin', card, candidates, ocrText, upName: matched }
|
|
572
|
-
}
|
|
573
|
-
}
|
|
496
|
+
/**
|
|
497
|
+
* **只搜 B站**:抖音没有卡片消息这种玩法(那条「当前接口库没有抖音搜索能力」的日志就是
|
|
498
|
+
* 以前多此一举地搜抖音留下的)。卡片解析这条链路只处理 B站卡片(入口处已经按 source 过滤过),
|
|
499
|
+
* 少搜一个平台就少一次风控风险、也少几秒等待。
|
|
500
|
+
*/
|
|
501
|
+
const hit = await tryBili()
|
|
502
|
+
const candidates: CardCandidate[] = hit.candidates
|
|
503
|
+
if (hit.url) {
|
|
504
|
+
logger.mark('[卡片解析] 定位成功: ' + hit.url)
|
|
505
|
+
// 提示语里报「真正匹配上的那个 UP 名」,而不是卡片摘要里那个不准的
|
|
506
|
+
return { url: hit.url, platform: 'bilibili', card, candidates, ocrText, upName: hit.matchedAuthor || upName }
|
|
574
507
|
}
|
|
575
508
|
|
|
576
509
|
logger.mark('[卡片解析] 没能唯一定位(标题: ' + card.title + ',UP: ' + upName + '),候选 ' + candidates.length + ' 条')
|
|
@@ -325,10 +325,16 @@ export const errorHelpSegments = (report: ErrorReportResult | null, platform: st
|
|
|
325
325
|
? '错误信息已上传,ID:' + report.id
|
|
326
326
|
: '错误信息上传失败(不影响使用)'
|
|
327
327
|
const ask = '可以前往 QQ 群内寻找帮助,在群内发送错误 ID 或该错误图片'
|
|
328
|
+
/**
|
|
329
|
+
* 两段之间必须**空一行**(\n\n)。
|
|
330
|
+
*
|
|
331
|
+
* markdown 里单个换行不算换行:两段会被拼成一整行,QQ 那边按一行显示时字号会被撑得很难看
|
|
332
|
+
* (用户原话「不换行大小会崩」)。纯文本那条也一样留空行,两种平台的观感才对得上。
|
|
333
|
+
*/
|
|
328
334
|
if (officialQq) {
|
|
329
335
|
// QQ 官方机器人认 markdown:群号做成可点的加群链接
|
|
330
|
-
return [segment.markdown(head + '\n' + '可以前往 [QQ 群 ' + group + '](' + link + ') 内寻找帮助,在群内发送错误 ID 或该错误图片')]
|
|
336
|
+
return [segment.markdown(head + '\n\n' + '可以前往 [QQ 群 ' + group + '](' + link + ') 内寻找帮助,在群内发送错误 ID 或该错误图片')]
|
|
331
337
|
}
|
|
332
338
|
// 个人号(OneBot / NapCat)不渲染 markdown:裸链接 QQ 客户端自己会变蓝可点
|
|
333
|
-
return [segment.text(head + '\nQQ 群 ' + group + ':' + ask + '\n' + link)]
|
|
339
|
+
return [segment.text(head + '\n\nQQ 群 ' + group + ':' + ask + '\n' + link)]
|
|
334
340
|
}
|
|
@@ -7,6 +7,8 @@ import { ParseSteps, SendTasks } from '@/module/utils/ParseSteps'
|
|
|
7
7
|
import { sendParseTip } from '@/module/utils/parseTip'
|
|
8
8
|
import type { ParseWorkType } from '@/module/db'
|
|
9
9
|
import { Config } from '@/module/utils/Config'
|
|
10
|
+
// 注意用相对写法:@/ 别名在仓库里指向 karin/,@/player 会被解析成不存在的 karin/player
|
|
11
|
+
import { applyForceOnlinePlayer } from '../../../player'
|
|
10
12
|
import { kuaishouComments, type KuaishouDataResult, type KuaishouOneWorkPayload } from '@/platform/kuaishou'
|
|
11
13
|
import type { ExtendedKuaishouOptionsType, KuaishouDataTypes } from '@/types'
|
|
12
14
|
|
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
Render,
|
|
26
26
|
uploadFile
|
|
27
27
|
} from '@/module'
|
|
28
|
+
// 注意用相对写法:@/ 别名在仓库里指向 karin/,@/player 会被解析成不存在的 karin/player
|
|
29
|
+
import { applyForceOnlinePlayer } from '../../../player'
|
|
28
30
|
import { ParseSteps, SendTasks } from '@/module/utils/ParseSteps'
|
|
29
31
|
import type { ParseWorkType } from '@/module/db'
|
|
30
32
|
import { Config } from '@/module/utils/Config'
|
package/src/webui.ts
CHANGED
|
@@ -30,6 +30,33 @@ import { QQ_KEYS, readQqOptions } from './qqOptions'
|
|
|
30
30
|
const COOKIE_NAME = 'kkk_config_token'
|
|
31
31
|
const TOKEN_TTL = 7 * 24 * 60 * 60 * 1000
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* 面板「Koishi 设置」分类里能改的项 —— 也就是 Koishi 侧的原生选项(koishi.yml 里本插件的顶层键)。
|
|
35
|
+
*
|
|
36
|
+
* **masters 不在这里**:主人账号是 Koishi 的权限体系(谁能执行主人指令、谁收报错),
|
|
37
|
+
* 面板是免登录页面,不该给它开这个口子 —— 仍然只在 koishi.yml 里改。
|
|
38
|
+
* 控制台那边也已经全部隐藏,所以这 4 项是唯一还有图形入口的地方。
|
|
39
|
+
*/
|
|
40
|
+
const NATIVE_PANEL_KEYS = ['dataPath', 'debug', 'autoParse', 'webUiAuth'] as const
|
|
41
|
+
type NativePanelKey = typeof NATIVE_PANEL_KEYS[number]
|
|
42
|
+
|
|
43
|
+
/** 面板要显示的原生设置(只取白名单,缺的用默认值兜底) */
|
|
44
|
+
const NATIVE_PANEL_DEFAULTS: Record<NativePanelKey, any> = {
|
|
45
|
+
dataPath: 'data',
|
|
46
|
+
debug: false,
|
|
47
|
+
autoParse: true,
|
|
48
|
+
webUiAuth: true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const readNativePanelOptions = (source: any): Record<string, any> => {
|
|
52
|
+
const out: Record<string, any> = {}
|
|
53
|
+
for (const key of NATIVE_PANEL_KEYS) {
|
|
54
|
+
const value = source?.[key]
|
|
55
|
+
out[key] = value === undefined || value === null ? NATIVE_PANEL_DEFAULTS[key] : value
|
|
56
|
+
}
|
|
57
|
+
return out
|
|
58
|
+
}
|
|
59
|
+
|
|
33
60
|
export interface WebUiDeps {
|
|
34
61
|
ctx: Context
|
|
35
62
|
config: any
|
|
@@ -345,7 +372,8 @@ export function registerWebUi ({ ctx, config, rawConfig, logger, pluginRoot }: W
|
|
|
345
372
|
// 在 Koishi 这边它就存在 `upstream` 里,启动时同步进 config.json。
|
|
346
373
|
// 另外附一份 `qq`(「QQ 适配器」分类):面板/切片/番剧选集/卡片识别这些是 Koishi 侧才有的开关,
|
|
347
374
|
// 面板里作为独立分类显示,保存时由下面的 POST 拆出来写回 koishi.yml。
|
|
348
|
-
|
|
375
|
+
// koishi 这一组是「Koishi 设置」分类:Koishi 侧的原生选项(masters 不在里面,见 NATIVE_PANEL_KEYS)
|
|
376
|
+
ok(response, { ...(config?.upstream ?? {}), qq: readQqOptions(config), koishi: readNativePanelOptions(config) }, '')
|
|
349
377
|
})
|
|
350
378
|
|
|
351
379
|
server.post('/kkk/v1/config', async (response: any) => {
|
|
@@ -357,10 +385,20 @@ export function registerWebUi ({ ctx, config, rawConfig, logger, pluginRoot }: W
|
|
|
357
385
|
// - `qq`(面板里的「QQ 适配器」分类)→ 写回插件自己的配置(koishi.yml)
|
|
358
386
|
// - 其余整份 → upstream(Karin 版的 config.json 形状)
|
|
359
387
|
// 保存都走 scope.update → 落盘 koishi.yml → 热重载 → 启动时同步回 config.json
|
|
360
|
-
const { qq, ...upstream } = normalizeLists(body)
|
|
388
|
+
const { qq, koishi: nativeGroup, ...upstream } = normalizeLists(body)
|
|
389
|
+
// 「Koishi 设置」分类:只认白名单里的键(前端传什么都写不进来别的东西)
|
|
390
|
+
const native: Record<string, any> = {}
|
|
391
|
+
if (nativeGroup && typeof nativeGroup === 'object') {
|
|
392
|
+
for (const key of NATIVE_PANEL_KEYS) {
|
|
393
|
+
if (nativeGroup[key] === undefined) continue
|
|
394
|
+
native[key] = typeof NATIVE_PANEL_DEFAULTS[key] === 'boolean' ? !!nativeGroup[key] : String(nativeGroup[key])
|
|
395
|
+
}
|
|
396
|
+
}
|
|
361
397
|
await (ctx as any).scope.update(normalize({
|
|
362
398
|
...(rawConfig ?? config),
|
|
363
399
|
...(qq && typeof qq === 'object' ? { qq: { ...readQqOptions(config), ...qq } } : {}),
|
|
400
|
+
// 注意:原生这几项是**顶层键**(dataPath / debug / autoParse / webUiAuth),不要塞进分组
|
|
401
|
+
...native,
|
|
364
402
|
upstream
|
|
365
403
|
}))
|
|
366
404
|
/**
|