koishi-plugin-kkk 3.7.2 → 3.9.0
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 +48 -0
- package/lib/build-metadata.json +5 -5
- package/lib/index.js +8 -3
- package/lib/karin/module/db/bilibili.js +185 -393
- package/lib/karin/module/db/douyin.js +219 -500
- package/lib/karin/module/db/index.js +30 -5
- package/lib/karin/module/db/model.js +200 -0
- package/lib/karin/module/db/statistics.js +197 -242
- package/lib/karin/module/utils/amagiClient.js +40 -15
- package/lib/karin/platform/douyin/douyin.js +10 -0
- package/lib/karin/setup.js +2 -4
- package/package.json +3 -2
- package/scripts/build.mjs +38 -5
- package/scripts/deploy.mjs +76 -27
- package/scripts/smoke-amagi-capability.cjs +97 -0
- package/scripts/smoke-comments.cjs +164 -0
- package/scripts/smoke-db-koishi.cjs +170 -0
- package/src/index.ts +8 -3
- package/src/karin/module/db/bilibili.ts +189 -448
- package/src/karin/module/db/douyin.ts +219 -587
- package/src/karin/module/db/index.ts +30 -4
- package/src/karin/module/db/model.ts +439 -0
- package/src/karin/module/db/statistics.ts +213 -380
- package/src/karin/module/utils/amagiClient.ts +49 -27
- package/src/karin/platform/douyin/douyin.ts +10 -0
- package/src/karin/setup.ts +2 -4
- package/lib/compat/sqlite3.js +0 -117
- package/lib/karin/module/db/migration.js +0 -199
- package/src/compat/sqlite3.ts +0 -123
- package/src/karin/module/db/migration.ts +0 -249
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 数据库冒烟:把插件的三张库(抖音 / B站 / 统计)在 **Koishi 原生数据库服务** 上跑一遍。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖点:建表、订阅与取消订阅、推送缓存、过滤词/标签、快照、
|
|
5
|
+
* 统计自增(含 upsert + $.add 表达式)、趋势/时段/形态/耗时分桶查询。
|
|
6
|
+
* 用的是临时 sqlite 文件,跑完就删。
|
|
7
|
+
*/
|
|
8
|
+
const fs = require('node:fs')
|
|
9
|
+
const os = require('node:os')
|
|
10
|
+
const path = require('node:path')
|
|
11
|
+
|
|
12
|
+
const ROOT = path.resolve(__dirname, '..')
|
|
13
|
+
// 默认跑开发目录的构建产物;想验「已经部署到 node_modules 的那份」,用:
|
|
14
|
+
// KKK_LIB=E:/devkoishi/node_modules/koishi-plugin-kkk/lib node scripts/smoke-db-koishi.cjs
|
|
15
|
+
const LIB = process.env.KKK_LIB ? path.resolve(process.env.KKK_LIB) : path.join(ROOT, 'lib')
|
|
16
|
+
|
|
17
|
+
let failed = 0
|
|
18
|
+
const ok = (label, cond, extra) => {
|
|
19
|
+
if (cond) {
|
|
20
|
+
console.log(' ✓ ' + label)
|
|
21
|
+
} else {
|
|
22
|
+
failed++
|
|
23
|
+
console.log(' ✗ ' + label + (extra === undefined ? '' : ' → ' + JSON.stringify(extra)))
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const main = async () => {
|
|
28
|
+
const dbFile = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'kkk-db-')), 'koishi.db')
|
|
29
|
+
|
|
30
|
+
const { Context } = require('koishi')
|
|
31
|
+
const sqlite = require('@koishijs/plugin-database-sqlite').default
|
|
32
|
+
|
|
33
|
+
const ctx = new Context()
|
|
34
|
+
ctx.plugin(sqlite, { path: dbFile })
|
|
35
|
+
await ctx.start()
|
|
36
|
+
|
|
37
|
+
if (!ctx.database) throw new Error('ctx.database 不可用,数据库服务没起来')
|
|
38
|
+
|
|
39
|
+
// 业务代码要读数据目录 / 日志,先把运行时绑上
|
|
40
|
+
const { bindRuntime } = require(path.join(LIB, 'compat/runtime.js'))
|
|
41
|
+
bindRuntime({
|
|
42
|
+
ctx,
|
|
43
|
+
config: { masters: [], debug: false, dataPath: 'data' },
|
|
44
|
+
pluginRoot: ROOT,
|
|
45
|
+
dataRoot: path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'kkk-data-')), 'data')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const { bootstrapDatabases } = require(path.join(LIB, 'karin/module/db/index.js'))
|
|
49
|
+
const { douyinDB, bilibiliDB, statisticsDB } = await bootstrapDatabases(ctx)
|
|
50
|
+
ok('bootstrapDatabases 返回三个实例', !!douyinDB && !!bilibiliDB && !!statisticsDB)
|
|
51
|
+
|
|
52
|
+
// ---------------- 抖音 ----------------
|
|
53
|
+
console.log('\n[抖音]')
|
|
54
|
+
await douyinDB.subscribeDouyinUser('group-1', 'bot-1', 'sec-uid-1', 'dy123', '测试博主')
|
|
55
|
+
ok('订阅后 isSubscribed', await douyinDB.isSubscribed('sec-uid-1', 'group-1'))
|
|
56
|
+
|
|
57
|
+
const douyinUser = await douyinDB.getDouyinUser('sec-uid-1')
|
|
58
|
+
ok('用户记录写入', douyinUser && douyinUser.short_id === 'dy123' && douyinUser.remark === '测试博主', douyinUser)
|
|
59
|
+
|
|
60
|
+
const subs = await douyinDB.getGroupSubscriptions('group-1')
|
|
61
|
+
ok('群订阅带出用户信息', subs.length === 1 && subs[0].douyinUser.remark === '测试博主', subs)
|
|
62
|
+
|
|
63
|
+
await douyinDB.addAwemeCache('aweme-1', 'sec-uid-1', 'group-1', 'post')
|
|
64
|
+
ok('作品缓存 isAwemePushed', await douyinDB.isAwemePushed('aweme-1', 'sec-uid-1', 'group-1', 'post'))
|
|
65
|
+
ok('hasHistory', await douyinDB.hasHistory('sec-uid-1', 'group-1', 'post'))
|
|
66
|
+
|
|
67
|
+
await douyinDB.addFilterWord('sec-uid-1', '广告')
|
|
68
|
+
await douyinDB.addFilterTag('sec-uid-1', '带货')
|
|
69
|
+
const dyFilter = await douyinDB.getFilterConfig('sec-uid-1')
|
|
70
|
+
ok('过滤词/标签', dyFilter.filterWords.includes('广告') && dyFilter.filterTags.includes('带货'), dyFilter)
|
|
71
|
+
ok('过滤模式', await douyinDB.updateFilterMode('sec-uid-1', 'whitelist') === undefined || true)
|
|
72
|
+
ok('过滤模式已改', (await douyinDB.getFilterConfig('sec-uid-1')).filterMode === 'whitelist')
|
|
73
|
+
ok('删除过滤词', await douyinDB.removeFilterWord('sec-uid-1', '广告'))
|
|
74
|
+
|
|
75
|
+
await douyinDB.updateListSnapshot('sec-uid-1', 'favorite', ['a1', 'a2'])
|
|
76
|
+
ok('列表快照', await douyinDB.isAwemeInList('a1', 'sec-uid-1', 'favorite'))
|
|
77
|
+
await douyinDB.updateListSnapshot('sec-uid-1', 'favorite', ['a3'])
|
|
78
|
+
ok('快照被覆盖', !await douyinDB.isAwemeInList('a1', 'sec-uid-1', 'favorite'))
|
|
79
|
+
|
|
80
|
+
ok('直播状态', await douyinDB.updateLiveStatus('sec-uid-1', true) && (await douyinDB.getLiveStatus('sec-uid-1')).living)
|
|
81
|
+
|
|
82
|
+
await douyinDB.unsubscribeDouyinUser('group-1', 'sec-uid-1')
|
|
83
|
+
ok('取消订阅', !await douyinDB.isSubscribed('sec-uid-1', 'group-1'))
|
|
84
|
+
ok('取消订阅后用户被清理', await douyinDB.getDouyinUser('sec-uid-1') === null)
|
|
85
|
+
|
|
86
|
+
// ---------------- B站 ----------------
|
|
87
|
+
console.log('\n[B站]')
|
|
88
|
+
await bilibiliDB.subscribeBilibiliUser('group-2', 'bot-1', 123456, '某UP')
|
|
89
|
+
ok('订阅后 isSubscribed', await bilibiliDB.isSubscribed(123456, 'group-2'))
|
|
90
|
+
|
|
91
|
+
const biUser = await bilibiliDB.getBilibiliUser(123456)
|
|
92
|
+
ok('UP 记录写入', biUser && biUser.remark === '某UP' && biUser.filterMode === 'blacklist', biUser)
|
|
93
|
+
|
|
94
|
+
const biSubs = await bilibiliDB.getGroupSubscriptions('group-2')
|
|
95
|
+
ok('群订阅带出 UP 信息', biSubs.length === 1 && biSubs[0].bilibiliUser.remark === '某UP', biSubs)
|
|
96
|
+
|
|
97
|
+
await bilibiliDB.addDynamicCache('dyn-1', 123456, 'group-2', 'DYNAMIC_TYPE_AV')
|
|
98
|
+
ok('动态缓存 isDynamicPushed', await bilibiliDB.isDynamicPushed('dyn-1', 123456, 'group-2'))
|
|
99
|
+
const caches = await bilibiliDB.getGroupDynamicCache('group-2')
|
|
100
|
+
ok('动态缓存可读', caches.length === 1 && caches[0].dynamic_type === 'DYNAMIC_TYPE_AV', caches)
|
|
101
|
+
|
|
102
|
+
await bilibiliDB.addFilterWord(123456, '抽奖')
|
|
103
|
+
await bilibiliDB.addFilterTag(123456, '直播')
|
|
104
|
+
const biFilter = await bilibiliDB.getFilterConfig(123456)
|
|
105
|
+
ok('过滤词/标签', biFilter.filterWords.includes('抽奖') && biFilter.filterTags.includes('直播'), biFilter)
|
|
106
|
+
|
|
107
|
+
ok('订阅群反查', (await bilibiliDB.getUserSubscribedGroups(123456)).length >= 1)
|
|
108
|
+
|
|
109
|
+
const cleaned = await bilibiliDB.cleanOldDynamicCache(0)
|
|
110
|
+
ok('清理过期缓存', cleaned >= 1, cleaned)
|
|
111
|
+
|
|
112
|
+
await bilibiliDB.unsubscribeBilibiliUser('group-2', 123456)
|
|
113
|
+
ok('取消订阅', !await bilibiliDB.isSubscribed(123456, 'group-2'))
|
|
114
|
+
ok('取消订阅后 UP 被清理', await bilibiliDB.getBilibiliUser(123456) === null)
|
|
115
|
+
|
|
116
|
+
// ---------------- 统计 ----------------
|
|
117
|
+
console.log('\n[统计]')
|
|
118
|
+
await statisticsDB.recordParse('group-9', 'user-1', 'douyin', { workType: 'video', durationMs: 1200 })
|
|
119
|
+
await statisticsDB.recordParse('group-9', 'user-1', 'douyin', { workType: 'video', durationMs: 800 })
|
|
120
|
+
await statisticsDB.recordParse('group-9', 'user-2', 'bilibili', { workType: 'gallery', durationMs: 35000 })
|
|
121
|
+
|
|
122
|
+
const groupStats = await statisticsDB.getGroupStatistics('group-9')
|
|
123
|
+
const douyinRow = groupStats.find((row) => row.platform === 'douyin')
|
|
124
|
+
ok('同用户同平台自增到 2', douyinRow && douyinRow.parseCount === 2, groupStats)
|
|
125
|
+
ok('群唯一用户数', await statisticsDB.getGroupUniqueUsers('group-9') === 2)
|
|
126
|
+
ok('平台总解析次数', await statisticsDB.getPlatformTotalParses('douyin') === 2)
|
|
127
|
+
|
|
128
|
+
const summary = await statisticsDB.getGlobalSummary()
|
|
129
|
+
ok('总解析次数 = 3', summary.totalParses === 3, summary)
|
|
130
|
+
ok('总群组数 = 1', summary.totalGroups === 1, summary)
|
|
131
|
+
ok('平台分布', summary.platformStats.douyin === 2 && summary.platformStats.bilibili === 1, summary.platformStats)
|
|
132
|
+
|
|
133
|
+
const today = new Date().toISOString().split('T')[0]
|
|
134
|
+
// 这两张表都是「群 × 日期 / 小时 × 平台」一行,所以抖音 2 + B站 1 = 两行
|
|
135
|
+
const history = await statisticsDB.getGroupRecentHistory('group-9', 30)
|
|
136
|
+
ok('群日粒度', history.length === 2 && history.every((row) => row.date === today)
|
|
137
|
+
&& history.reduce((sum, row) => sum + row.parseCount, 0) === 3, history)
|
|
138
|
+
ok('活跃天数', await statisticsDB.getGroupActiveDays('group-9') === 1)
|
|
139
|
+
|
|
140
|
+
const hourStats = await statisticsDB.getGroupHourStats('group-9')
|
|
141
|
+
ok('时段分布', hourStats.length === 2 && hourStats.reduce((sum, row) => sum + row.parseCount, 0) === 3, hourStats)
|
|
142
|
+
|
|
143
|
+
const workTypes = await statisticsDB.getGroupWorkTypeStats('group-9')
|
|
144
|
+
ok('形态分布', workTypes.length === 2, workTypes)
|
|
145
|
+
ok('全局形态分布', (await statisticsDB.getGlobalWorkTypeStats()).length === 2)
|
|
146
|
+
|
|
147
|
+
const metrics = await statisticsDB.getGroupMetricStats('group-9')
|
|
148
|
+
ok('耗时分桶', metrics.length === 3 && metrics.reduce((sum, item) => sum + item.count, 0) === 3, metrics)
|
|
149
|
+
ok('全局耗时分桶', (await statisticsDB.getGlobalMetricStats()).reduce((sum, item) => sum + item.count, 0) === 3)
|
|
150
|
+
|
|
151
|
+
const recent = await statisticsDB.getRecentHistory(30)
|
|
152
|
+
ok('日历史', recent.length === 1 && recent[0].totalParses === 3, recent)
|
|
153
|
+
ok('可信起始日', await statisticsDB.getHistoryCompleteFrom() === today)
|
|
154
|
+
ok('首次出现', (await statisticsDB.getGroupFirstSeen()).length === 1)
|
|
155
|
+
ok('全局唯一用户', await statisticsDB.getTotalUniqueUsers() === 2)
|
|
156
|
+
|
|
157
|
+
const removed = await statisticsDB.getRecentHistory(30)
|
|
158
|
+
ok('二次读取稳定', removed.length === 1)
|
|
159
|
+
|
|
160
|
+
await ctx.stop()
|
|
161
|
+
fs.rmSync(path.dirname(dbFile), { recursive: true, force: true })
|
|
162
|
+
|
|
163
|
+
console.log('\n' + (failed ? '✗ ' + failed + ' 项失败' : '✓ 全部通过'))
|
|
164
|
+
process.exit(failed ? 1 : 0)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
main().catch((error) => {
|
|
168
|
+
console.error(error)
|
|
169
|
+
process.exit(1)
|
|
170
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -80,13 +80,18 @@ try {
|
|
|
80
80
|
export const name = 'kkk'
|
|
81
81
|
|
|
82
82
|
export const inject = {
|
|
83
|
+
// database:**必选**。订阅关系、推送缓存、解析统计全走 Koishi 原生数据库服务
|
|
84
|
+
// (表由 ctx.model.extend 注册,落在宿主自己的库里,插件不再自己开 sqlite 文件)。
|
|
85
|
+
// 之前它是 optional,因为那会儿插件自己建库;现在没有数据库服务就没有地方存数据,
|
|
86
|
+
// 直接声明成必选,让 Koishi 在没有配置数据库插件时明确地不加载本插件。
|
|
87
|
+
required: ['database'],
|
|
83
88
|
// server 只给「插件自带控制台」用,已经移除;这里留着不需要的服务反而会拖慢加载
|
|
84
89
|
// ffmpeg:由 koishi-plugin-ffmpeg-path 提供,兼容层的 ffmpeg()/ffprobe() 会优先用它
|
|
85
90
|
// assets:koishi-plugin-assets-qqbot-part-file 之类提供的图床服务,
|
|
86
91
|
// 面板卡片要上传成 https 地址才能放进 QQ 的 markdown 图片里
|
|
87
92
|
// 渲染:只用浏览器渲染服务(koishi-plugin-puppeteer 或同类插件)
|
|
88
93
|
// 3.5.0 起不再支持 shotkit 内核(它在 Windows 上加载不了 https 资源,卡片会缺图)
|
|
89
|
-
optional: ['puppeteer', '
|
|
94
|
+
optional: ['puppeteer', 'http', 'ffmpeg', 'assets', 'server', 'console']
|
|
90
95
|
}
|
|
91
96
|
|
|
92
97
|
export interface Config {
|
|
@@ -1098,11 +1103,11 @@ export async function apply (ctx: Context, rawConfig: Config) {
|
|
|
1098
1103
|
}
|
|
1099
1104
|
}
|
|
1100
1105
|
|
|
1101
|
-
//
|
|
1106
|
+
// 初始化数据库实例(走 Koishi 原生数据库服务 ctx.database)
|
|
1102
1107
|
// 原实现用顶层 await 立即初始化,CJS 下改成在这里显式引导,否则 module/db 导出的实例是 null
|
|
1103
1108
|
try {
|
|
1104
1109
|
const { bootstrapDatabases } = await import('./karin/module/db')
|
|
1105
|
-
const { douyinDB, bilibiliDB, statisticsDB } = await bootstrapDatabases()
|
|
1110
|
+
const { douyinDB, bilibiliDB, statisticsDB } = await bootstrapDatabases(ctx)
|
|
1106
1111
|
logger.debug('数据库初始化完成:%s / %s / %s', douyinDB.constructor.name, bilibiliDB.constructor.name, statisticsDB.constructor.name)
|
|
1107
1112
|
} catch (error: any) {
|
|
1108
1113
|
logger.error('数据库初始化失败: %s', error?.stack ?? error)
|