chatluna-plugin-blacklist 0.0.0-stage → 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 arrog1nt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,53 @@
1
- # Temporary Holding Version
1
+ # ChatLuna Blacklist
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ChatLuna 插件,让机器人自己拉黑辱骂、骚扰它的用户。拉黑由模型自行判断并调用工具,必须填写理由;已拉黑用户的消息不再进入对话,指令也不执行,直到解除拉黑。黑名单保存在数据库中,可通过工具或管理指令查看。
4
+
5
+ ## 功能
6
+
7
+ - `blacklist_user`:拉黑用户,参数为理由(必填)、可选的拉黑时长、用户 ID、名称和平台;时长以分钟计,不填或填 0 为永久;用户 ID 默认为当前对话的用户,平台默认取当前会话
8
+ - `unblacklist_user`:解除拉黑
9
+ - `list_blacklisted_users`:列出已拉黑用户的名称、ID、平台、理由、拉黑时间和期限,可按平台筛选
10
+ - 已拉黑用户的消息既不唤醒模型,也不写入会话记录;默认同时屏蔽其指令
11
+ - 限时拉黑到期后自动失效,无需手动解除
12
+ - 任何用户都可能被拉黑(包括管理员),只有机器人自身不能被拉黑;重复拉黑保留原理由和期限,不覆盖
13
+ - 黑名单按“平台+用户 ID”记录,对该平台下所有群聊和私聊生效
14
+
15
+ ## 设置页黑名单
16
+
17
+ 插件设置页底部逐行显示当前生效的黑名单:用户名称与 ID、平台、理由、拉黑时间和期限,限时拉黑显示剩余时间。列表随拉黑与解除实时更新,限时拉黑到期后自动移出。每行可直接解除拉黑,确认后立即生效,无需保存配置。需要加载 ChatLuna WebUI。
18
+
19
+ ## 权限
20
+
21
+ | 权限项 | 默认等级 | 说明 |
22
+ | --- | --- | --- |
23
+ | `blacklist:add` | 1 | 调用 `blacklist_user` |
24
+ | `blacklist:remove` | 1 | 调用 `unblacklist_user` |
25
+ | `tool:list_blacklisted_users` | 1 | 调用 `list_blacklisted_users` |
26
+
27
+ 工具调用时的身份是当前与机器人对话的用户,被拉黑的通常就是调用者本人,所以默认等级为 1,由模型自行判断。调高等级会让机器人无法拉黑普通用户。工具的启用范围在工具管理页设置。
28
+
29
+ ## 管理指令
30
+
31
+ 供人工查看和撤销机器人的拉黑记录,需要 Koishi 兼容层,权限等级 3 及以上可用:
32
+
33
+ | 指令 | 别名 | 说明 |
34
+ | --- | --- | --- |
35
+ | `blacklist` | `黑名单` | 查看已拉黑用户列表 |
36
+ | `blacklist.add [-t 时长] <用户> <理由>` | `拉黑` | 拉黑用户,理由必填;`-t` 指定时长,如 `30m`、`2h`、`1d`,默认永久 |
37
+ | `blacklist.remove <用户>` | `解除拉黑` | 解除拉黑 |
38
+
39
+ 用户参数使用 @ 提及,或写作 `@用户ID`。`-t` 须写在理由之前,否则会被当作理由的一部分。
40
+
41
+ ## 配置
42
+
43
+ | 配置项 | 默认值 | 说明 |
44
+ | --- | --- | --- |
45
+ | `blockCommands` | `true` | 屏蔽已拉黑用户的指令;关闭后只屏蔽对话 |
46
+
47
+ ## 与 `chatluna.ban` 的区别
48
+
49
+ `chatluna.ban` 写入一条 `deny *` 权限规则,拒绝用户使用指令和工具,但不阻止对话。本插件的黑名单直接拦截对话消息,并记录拉黑理由,两者可同时使用。
50
+
51
+ ## 许可证
52
+
53
+ MIT
@@ -0,0 +1,168 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { BanIcon, TimerIcon } from 'lucide-react'
3
+ import { apiErrorMessage, callAction, toast, useLive } from '@chatluna/ui-client/client'
4
+ import {
5
+ AlertDialog,
6
+ AlertDialogCancel,
7
+ AlertDialogContent,
8
+ AlertDialogDescription,
9
+ AlertDialogFooter,
10
+ AlertDialogHeader,
11
+ AlertDialogTitle,
12
+ Avatar,
13
+ AvatarFallback,
14
+ AvatarImage,
15
+ Badge,
16
+ Button,
17
+ Spinner,
18
+ } from '@chatluna/ui-kit'
19
+ import type { SchemaRendererProps } from '@chatluna/ui-schema-form/client'
20
+ import type { BlacklistEntry } from '../src/blacklist'
21
+
22
+ /** 当年的时间省略年份。 */
23
+ function timeOf(timestamp: number) {
24
+ const date = new Date(timestamp)
25
+ return date.toLocaleString('zh-CN', {
26
+ ...(date.getFullYear() === new Date().getFullYear() ? {} : { year: 'numeric' }),
27
+ month: '2-digit',
28
+ day: '2-digit',
29
+ hour: '2-digit',
30
+ minute: '2-digit',
31
+ hour12: false,
32
+ })
33
+ }
34
+
35
+ function remainingOf(ms: number) {
36
+ const minutes = Math.ceil(ms / 60_000)
37
+ if (minutes < 60) return `${minutes} 分钟`
38
+ const hours = Math.floor(minutes / 60)
39
+ if (hours < 24) return minutes % 60 ? `${hours} 小时 ${minutes % 60} 分钟` : `${hours} 小时`
40
+ return hours % 24 ? `${Math.floor(hours / 24)} 天 ${hours % 24} 小时` : `${Math.floor(hours / 24)} 天`
41
+ }
42
+
43
+ const displayOf = (entry: BlacklistEntry) => entry.name ? `${entry.name}(${entry.userId})` : entry.userId
44
+
45
+ /** 限时拉黑到期时服务端不推送,列表每 30 秒按本地时间刷新剩余时间并隐藏已到期的行。 */
46
+ function useNow() {
47
+ const [now, setNow] = useState(Date.now)
48
+ useEffect(() => {
49
+ const timer = setInterval(() => setNow(Date.now()), 30_000)
50
+ return () => clearInterval(timer)
51
+ }, [])
52
+ return now
53
+ }
54
+
55
+ /** 优先用拉黑时存下的头像;onebot 即 QQ,没存时按 QQ 号取头像;都没有时显示名称首字。 */
56
+ function avatarOf(entry: BlacklistEntry) {
57
+ if (entry.avatar) return entry.avatar
58
+ if (entry.platform === 'onebot' && /^\d+$/.test(entry.userId)) return `https://q1.qlogo.cn/g?b=qq&nk=${entry.userId}&s=100`
59
+ }
60
+
61
+ /**
62
+ * 设置页里的黑名单列表。它挂在不存值的 `list` 字段上:不调用 onChange,也不用 SchemaForm 的 Field 外壳,
63
+ * 避免出现恢复默认、JSON 编辑等只对配置值有意义的菜单。用分隔线列表而非表格:理由长短不一,固定列宽会把它挤成窄条;每人两行,理由超过两行时截断,悬停显示全文。
64
+ */
65
+ export function BlacklistTable(props: SchemaRendererProps) {
66
+ const list = useLive('blacklist.list')
67
+ const now = useNow()
68
+ const [removing, setRemoving] = useState<BlacklistEntry>()
69
+ const entries = (list.data ?? []).filter((entry) => entry.expiresAt === 0 || entry.expiresAt > now).reverse()
70
+ const label = String(props.schema.meta.label ?? props.path)
71
+ const description = props.schema.meta.description
72
+
73
+ const placeholder = list.status === 'loading' && list.data === undefined
74
+ ? <span className="inline-flex items-center gap-2"><Spinner />正在加载</span>
75
+ : list.status === 'offline' && list.data === undefined ? '无法连接服务器' : '暂无已拉黑用户'
76
+
77
+ return (
78
+ <div className="min-w-0 space-y-3 text-sm leading-5" data-blacklist-list="">
79
+ <div className="space-y-1">
80
+ <div className="flex items-center gap-2 text-base font-semibold leading-6">
81
+ {label}
82
+ {entries.length > 0 && <Badge variant="secondary">{entries.length}</Badge>}
83
+ </div>
84
+ {typeof description === 'string' && <p className="text-muted-foreground">{description}</p>}
85
+ </div>
86
+ {entries.length === 0
87
+ ? <div className="flex h-16 items-center justify-center rounded-lg border border-dashed text-muted-foreground">{placeholder}</div>
88
+ : (
89
+ <ul aria-label={label} className="divide-y overflow-hidden rounded-lg border">
90
+ {entries.map((entry) => {
91
+ const name = entry.name || entry.userId
92
+ const temporary = entry.expiresAt !== 0
93
+ return (
94
+ <li key={entry.id} className="flex items-center gap-3 px-3 py-2.5">
95
+ <Avatar className="self-start">
96
+ <AvatarImage src={avatarOf(entry)} alt="" referrerPolicy="no-referrer" />
97
+ <AvatarFallback>{Array.from(name)[0]}</AvatarFallback>
98
+ </Avatar>
99
+ <div className="min-w-0 flex-1">
100
+ <div className="flex min-w-0 items-baseline gap-2">
101
+ <span className="truncate font-medium">{name}</span>
102
+ <span className="shrink-0 text-xs leading-4 text-muted-foreground tabular-nums">
103
+ {entry.name ? `${entry.userId} · ` : ''}{entry.platform}
104
+ </span>
105
+ </div>
106
+ <p className="line-clamp-2 text-muted-foreground [overflow-wrap:anywhere]" title={entry.reason}>{entry.reason}</p>
107
+ </div>
108
+ <div className="flex shrink-0 flex-col items-end gap-1">
109
+ {temporary
110
+ ? <Badge variant="secondary" className="tabular-nums" title={`${timeOf(entry.expiresAt)} 到期`}><TimerIcon />剩 {remainingOf(entry.expiresAt - now)}</Badge>
111
+ : <Badge variant="destructive"><BanIcon />永久</Badge>}
112
+ <span className="text-xs leading-4 text-muted-foreground tabular-nums">{timeOf(entry.createdAt)} 拉黑</span>
113
+ </div>
114
+ <Button
115
+ type="button"
116
+ variant="ghost"
117
+ size="xs"
118
+ className="shrink-0 text-muted-foreground hover:text-foreground"
119
+ aria-label={`解除拉黑:${displayOf(entry)}`}
120
+ onClick={() => setRemoving(entry)}
121
+ >
122
+ 解除
123
+ </Button>
124
+ </li>
125
+ )
126
+ })}
127
+ </ul>
128
+ )}
129
+ <RemoveDialog entry={removing} onClose={() => setRemoving(undefined)} />
130
+ </div>
131
+ )
132
+ }
133
+
134
+ function RemoveDialog({ entry, onClose }: { entry: BlacklistEntry | undefined, onClose(): void }) {
135
+ const [pending, setPending] = useState(false)
136
+ const confirm = async () => {
137
+ if (!entry) return
138
+ setPending(true)
139
+ try {
140
+ await callAction('blacklist.remove', { id: entry.id })
141
+ toast.success(`已解除 ${displayOf(entry)} 的拉黑`)
142
+ onClose()
143
+ } catch (error) {
144
+ toast.error(apiErrorMessage(error))
145
+ } finally {
146
+ setPending(false)
147
+ }
148
+ }
149
+ return (
150
+ <AlertDialog open={entry !== undefined} onOpenChange={(open) => !open && onClose()}>
151
+ <AlertDialogContent size="sm">
152
+ <AlertDialogHeader>
153
+ <AlertDialogTitle>解除拉黑</AlertDialogTitle>
154
+ <AlertDialogDescription>
155
+ 解除后 {entry ? displayOf(entry) : ''} 可再次与机器人对话,拉黑理由一并删除。
156
+ </AlertDialogDescription>
157
+ </AlertDialogHeader>
158
+ <AlertDialogFooter>
159
+ <AlertDialogCancel>取消</AlertDialogCancel>
160
+ <Button variant="destructive" disabled={pending} onClick={() => void confirm()}>
161
+ {pending && <Spinner />}
162
+ 解除拉黑
163
+ </Button>
164
+ </AlertDialogFooter>
165
+ </AlertDialogContent>
166
+ </AlertDialog>
167
+ )
168
+ }
@@ -0,0 +1,7 @@
1
+ import type { Context } from 'cordis'
2
+ import { registerReactFormExtension } from '@chatluna/ui-schema-form/client'
3
+ import { BlacklistTable } from './blacklist-table'
4
+
5
+ export default function apply(ctx: Context) {
6
+ ctx.effect(() => registerReactFormExtension({ role: 'blacklist:list', component: BlacklistTable, priority: 30 }))
7
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "client/index.ts": {
3
+ "file": "client.js",
4
+ "name": "index",
5
+ "src": "client/index.ts",
6
+ "isEntry": true
7
+ }
8
+ }
@@ -0,0 +1,2 @@
1
+ /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial}}}@layer utilities{@layer plugin{.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.flex{display:flex}.inline-flex{display:inline-flex}.h-16{height:calc(var(--spacing,.25rem) * 16)}.min-w-0{min-width:0}.flex-1{flex:1}.shrink-0{flex-shrink:0}.flex-col{flex-direction:column}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-center{justify-content:center}.gap-1{gap:var(--spacing,.25rem)}.gap-2{gap:calc(var(--spacing,.25rem) * 2)}.gap-3{gap:calc(var(--spacing,.25rem) * 3)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing,.25rem) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing,.25rem) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing,.25rem) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing,.25rem) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}.self-start{align-self:flex-start}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.rounded-lg{border-radius:var(--radius)}.border{border-style:var(--tw-border-style);border-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.px-3{padding-inline:calc(var(--spacing,.25rem) * 3)}.py-2\.5{padding-block:calc(var(--spacing,.25rem) * 2.5)}.text-base{font-size:var(--text-base,1rem);line-height:var(--tw-leading,var(--text-base--line-height,calc(1.5 / 1)))}.text-sm{font-size:var(--text-sm,.875rem);line-height:var(--tw-leading,var(--text-sm--line-height,calc(1.25 / .875)))}.text-xs{font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)))}.leading-4{--tw-leading:calc(var(--spacing,.25rem) * 4);line-height:calc(var(--spacing,.25rem) * 4)}.leading-5{--tw-leading:calc(var(--spacing,.25rem) * 5);line-height:calc(var(--spacing,.25rem) * 5)}.leading-6{--tw-leading:calc(var(--spacing,.25rem) * 6);line-height:calc(var(--spacing,.25rem) * 6)}.font-medium{--tw-font-weight:var(--font-weight-medium,500);font-weight:var(--font-weight-medium,500)}.font-semibold{--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600)}.\[overflow-wrap\:anywhere\]{overflow-wrap:anywhere}.text-muted-foreground{color:var(--muted-foreground)}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}}@media (hover:hover){.hover\:text-foreground:hover{color:var(--foreground)}}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}
package/dist/client.js ADDED
@@ -0,0 +1 @@
1
+ import{registerReactFormExtension as e}from"@chatluna/ui-schema-form/client";import*as t from"react";import{createContext as n,createElement as r,forwardRef as i,useContext as a,useEffect as o,useState as s}from"react";import{apiErrorMessage as c,callAction as l,toast as u,useLive as d}from"@chatluna/ui-client/client";import{jsx as f,jsxs as p}from"react/jsx-runtime";import{Button as m}from"@base-ui/react/button";import{AlertDialog as h}from"@base-ui/react/alert-dialog";import{Avatar as g}from"@base-ui/react/avatar";import{mergeProps as _}from"@base-ui/react/merge-props";import{useRender as v}from"@base-ui/react/use-render";import{Combobox as y}from"@base-ui/react";import{Select as b}from"@base-ui/react/select";var x=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),ee=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase(),te=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(e,t,n)=>n?n.toUpperCase():t.toLowerCase()),S=e=>{let t=te(e);return t.charAt(0).toUpperCase()+t.slice(1)},C={xmlns:`http://www.w3.org/2000/svg`,width:24,height:24,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:2,strokeLinecap:`round`,strokeLinejoin:`round`},ne=e=>{for(let t in e)if(t.startsWith(`aria-`)||t===`role`||t===`title`)return!0;return!1},re=n({}),w=()=>a(re),ie=i(({color:e,size:t,strokeWidth:n,absoluteStrokeWidth:i,className:a=``,children:o,iconNode:s,...c},l)=>{let{size:u=24,strokeWidth:d=2,absoluteStrokeWidth:f=!1,color:p=`currentColor`,className:m=``}=w()??{},h=i??f?Number(n??d)*24/Number(t??u):n??d;return r(`svg`,{ref:l,...C,width:t??u??C.width,height:t??u??C.height,stroke:e??p,strokeWidth:h,className:x(`lucide`,m,a),...!o&&!ne(c)&&{"aria-hidden":`true`},...c},[...s.map(([e,t])=>r(e,t)),...Array.isArray(o)?o:[o]])}),T=(e,t)=>{let n=i(({className:n,...i},a)=>r(ie,{ref:a,iconNode:t,className:x(`lucide-${ee(S(e))}`,`lucide-${e}`,n),...i}));return n.displayName=S(e),n},E=T(`ban`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`M4.929 4.929 19.07 19.071`,key:`196cmz`}]]),D=T(`loader-circle`,[[`path`,{d:`M21 12a9 9 0 1 1-6.219-8.56`,key:`13zald`}]]),O=T(`timer`,[[`line`,{x1:`10`,x2:`14`,y1:`2`,y2:`2`,key:`14vaq8`}],[`line`,{x1:`12`,x2:`15`,y1:`14`,y2:`11`,key:`17fdiu`}],[`circle`,{cx:`12`,cy:`14`,r:`8`,key:`1e1u0o`}]]);function k(e){var t,n,r=``;if(typeof e==`string`||typeof e==`number`)r+=e;else if(typeof e==`object`){if(Array.isArray(e)){var i=e.length;for(t=0;t<i;t++)e[t]&&(n=k(e[t]))&&(r&&(r+=` `),r+=n)}else for(n in e)e[n]&&(r&&(r+=` `),r+=n)}return r}function A(){for(var e,t,n=0,r=``,i=arguments.length;n<i;n++)(e=arguments[n])&&(t=k(e))&&(r&&(r+=` `),r+=t);return r}var j=(e,t)=>{let n=Array(e.length+t.length);for(let t=0;t<e.length;t++)n[t]=e[t];for(let r=0;r<t.length;r++)n[e.length+r]=t[r];return n},ae=(e,t)=>({classGroupId:e,validator:t}),oe=(e=new Map,t=null,n)=>({nextPart:e,validators:t,classGroupId:n}),M=`-`,N=[],P=`arbitrary..`,F=e=>{let t=L(e),{conflictingClassGroups:n,conflictingClassGroupModifiers:r}=e;return{getClassGroupId:e=>{if(e.startsWith(`[`)&&e.endsWith(`]`))return se(e);let n=e.split(M);return I(n,+(n[0]===``&&n.length>1),t)},getConflictingClassGroupIds:(e,t)=>{if(t){let t=r[e],i=n[e];return t?i?j(i,t):t:i||N}return n[e]||N}}},I=(e,t,n)=>{if(e.length-t===0)return n.classGroupId;let r=e[t],i=n.nextPart.get(r);if(i){let n=I(e,t+1,i);if(n)return n}let a=n.validators;if(a===null)return;let o=t===0?e.join(M):e.slice(t).join(M),s=a.length;for(let e=0;e<s;e++){let t=a[e];if(t.validator(o))return t.classGroupId}},se=e=>e.slice(1,-1).indexOf(`:`)===-1?void 0:(()=>{let t=e.slice(1,-1),n=t.indexOf(`:`),r=t.slice(0,n);return r?P+r:void 0})(),L=e=>{let{theme:t,classGroups:n}=e;return ce(n,t)},ce=(e,t)=>{let n=oe();for(let r in e){let i=e[r];R(i,n,r,t)}return n},R=(e,t,n,r)=>{let i=e.length;for(let a=0;a<i;a++){let i=e[a];z(i,t,n,r)}},z=(e,t,n,r)=>{if(typeof e==`string`){B(e,t,n);return}if(typeof e==`function`){V(e,t,n,r);return}le(e,t,n,r)},B=(e,t,n)=>{let r=e===``?t:ue(t,e);r.classGroupId=n},V=(e,t,n,r)=>{if(de(e)){R(e(r),t,n,r);return}t.validators===null&&(t.validators=[]),t.validators.push(ae(n,e))},le=(e,t,n,r)=>{let i=Object.entries(e),a=i.length;for(let e=0;e<a;e++){let[a,o]=i[e];R(o,ue(t,a),n,r)}},ue=(e,t)=>{let n=e,r=t.split(M),i=r.length;for(let e=0;e<i;e++){let t=r[e],i=n.nextPart.get(t);i||(i=oe(),n.nextPart.set(t,i)),n=i}return n},de=e=>`isThemeGetter`in e&&e.isThemeGetter===!0,fe=e=>{if(e<1)return{get:()=>void 0,set:()=>{}};let t=0,n=Object.create(null),r=Object.create(null),i=(i,a)=>{n[i]=a,t++,t>e&&(t=0,r=n,n=Object.create(null))};return{get(e){let t=n[e];if(t!==void 0)return t;if((t=r[e])!==void 0)return i(e,t),t},set(e,t){e in n?n[e]=t:i(e,t)}}},pe=`!`,me=`:`,he=[],ge=(e,t,n,r,i)=>({modifiers:e,hasImportantModifier:t,baseClassName:n,maybePostfixModifierPosition:r,isExternal:i}),_e=e=>{let{prefix:t,experimentalParseClassName:n}=e,r=e=>{let t=[],n=0,r=0,i=0,a,o=e.length;for(let s=0;s<o;s++){let o=e[s];if(n===0&&r===0){if(o===me){t.push(e.slice(i,s)),i=s+1;continue}if(o===`/`){a=s;continue}}o===`[`?n++:o===`]`?n--:o===`(`?r++:o===`)`&&r--}let s=t.length===0?e:e.slice(i),c=s,l=!1;s.endsWith(pe)?(c=s.slice(0,-1),l=!0):s.startsWith(pe)&&(c=s.slice(1),l=!0);let u=a&&a>i?a-i:void 0;return ge(t,l,c,u)};if(t){let e=t+me,n=r;r=t=>t.startsWith(e)?n(t.slice(e.length)):ge(he,!1,t,void 0,!0)}if(n){let e=r;r=t=>n({className:t,parseClassName:e})}return r},ve=e=>{let t=new Map;return e.orderSensitiveModifiers.forEach((e,n)=>{t.set(e,1e6+n)}),e=>{let n=[],r=[];for(let i=0;i<e.length;i++){let a=e[i],o=a[0]===`[`,s=t.has(a);o||s?(r.length>0&&(r.sort(),n.push(...r),r=[]),n.push(a)):r.push(a)}return r.length>0&&(r.sort(),n.push(...r)),n}},ye=e=>({cache:fe(e.cacheSize),parseClassName:_e(e),sortModifiers:ve(e),postfixLookupClassGroupIds:be(e),...F(e)}),be=e=>{let t=Object.create(null),n=e.postfixLookupClassGroups;if(n)for(let e=0;e<n.length;e++)t[n[e]]=!0;return t},xe=/\s+/,Se=(e,t)=>{let{parseClassName:n,getClassGroupId:r,getConflictingClassGroupIds:i,sortModifiers:a,postfixLookupClassGroupIds:o}=t,s=[],c=e.trim().split(xe),l=``;for(let e=c.length-1;e>=0;--e){let t=c[e],{isExternal:u,modifiers:d,hasImportantModifier:f,baseClassName:p,maybePostfixModifierPosition:m}=n(t);if(u){l=t+(l.length>0?` `+l:l);continue}let h=!!m,g;if(h){g=r(p.substring(0,m));let e=g&&o[g]?r(p):void 0;e&&e!==g&&(g=e,h=!1)}else g=r(p);if(!g){if(!h){l=t+(l.length>0?` `+l:l);continue}if(g=r(p),!g){l=t+(l.length>0?` `+l:l);continue}h=!1}let _=d.length===0?``:d.length===1?d[0]:a(d).join(`:`),v=f?_+pe:_,y=v+g;if(s.indexOf(y)>-1)continue;s.push(y);let b=i(g,h);for(let e=0;e<b.length;++e){let t=b[e];s.push(v+t)}l=t+(l.length>0?` `+l:l)}return l},Ce=(...e)=>{let t=0,n,r,i=``;for(;t<e.length;)(n=e[t++])&&(r=we(n))&&(i&&(i+=` `),i+=r);return i},we=e=>{if(typeof e==`string`)return e;let t,n=``;for(let r=0;r<e.length;r++)e[r]&&(t=we(e[r]))&&(n&&(n+=` `),n+=t);return n},Te=(e,...t)=>{let n,r,i,a,o=o=>(n=ye(t.reduce((e,t)=>t(e),e())),r=n.cache.get,i=n.cache.set,a=s,s(o)),s=e=>{let t=r(e);if(t)return t;let a=Se(e,n);return i(e,a),a};return a=o,(...e)=>a(Ce(...e))},Ee=[],H=e=>{let t=t=>t[e]||Ee;return t.isThemeGetter=!0,t},De=/^\[(?:(\w[\w-]*):)?(.+)\]$/i,Oe=/^\((?:(\w[\w-]*):)?(.+)\)$/i,ke=/^\d+(?:\.\d+)?\/\d+(?:\.\d+)?$/,Ae=/^(\d+(\.\d+)?)?(xs|sm|md|lg|xl)$/,je=/\d+(%|px|r?em|[sdl]?v([hwib]|min|max)|pt|pc|in|cm|mm|cap|ch|ex|r?lh|cq(w|h|i|b|min|max))|\b(calc|min|max|clamp)\(.+\)|^0$/,Me=/^(rgba?|hsla?|hwb|(ok)?(lab|lch)|color-mix)\(.+\)$/,Ne=/^(inset_)?-?((\d+)?\.?(\d+)[a-z]+|0)_-?((\d+)?\.?(\d+)[a-z]+|0)/,Pe=/^(url|image|image-set|cross-fade|element|(repeating-)?(linear|radial|conic)-gradient)\(.+\)$/,U=e=>ke.test(e),W=e=>!!e&&!Number.isNaN(Number(e)),G=e=>!!e&&Number.isInteger(Number(e)),Fe=e=>e.endsWith(`%`)&&W(e.slice(0,-1)),K=e=>Ae.test(e),Ie=()=>!0,Le=e=>je.test(e)&&!Me.test(e),Re=()=>!1,ze=e=>Ne.test(e),Be=e=>Pe.test(e),Ve=e=>!q(e)&&!Y(e),He=e=>e.startsWith(`@container`)&&(e[10]===`/`&&e[11]!==void 0||e[11]===`s`&&e[16]!==void 0&&e.startsWith(`-size/`,10)||e[11]===`n`&&e[18]!==void 0&&e.startsWith(`-normal/`,10)),Ue=e=>Z(e,it,Re),q=e=>De.test(e),J=e=>Z(e,at,Le),We=e=>Z(e,ot,W),Ge=e=>Z(e,ct,Ie),Ke=e=>Z(e,st,Re),qe=e=>Z(e,nt,Re),Je=e=>Z(e,rt,Be),Ye=e=>Z(e,lt,ze),Y=e=>Oe.test(e),X=e=>Q(e,at),Xe=e=>Q(e,st),Ze=e=>Q(e,nt),Qe=e=>Q(e,it),$e=e=>Q(e,rt),et=e=>Q(e,lt,!0),tt=e=>Q(e,ct,!0),Z=(e,t,n)=>{let r=De.exec(e);return r?r[1]?t(r[1]):n(r[2]):!1},Q=(e,t,n=!1)=>{let r=Oe.exec(e);return r?r[1]?t(r[1]):n:!1},nt=e=>e===`position`||e===`percentage`,rt=e=>e===`image`||e===`url`,it=e=>e===`length`||e===`size`||e===`bg-size`,at=e=>e===`length`,ot=e=>e===`number`,st=e=>e===`family-name`,ct=e=>e===`number`||e===`weight`,lt=e=>e===`shadow`,ut=Te(()=>{let e=H(`color`),t=H(`font`),n=H(`text`),r=H(`font-weight`),i=H(`tracking`),a=H(`leading`),o=H(`breakpoint`),s=H(`container`),c=H(`spacing`),l=H(`radius`),u=H(`shadow`),d=H(`inset-shadow`),f=H(`text-shadow`),p=H(`drop-shadow`),m=H(`blur`),h=H(`perspective`),g=H(`aspect`),_=H(`ease`),v=H(`animate`),y=()=>[`auto`,`avoid`,`all`,`avoid-page`,`page`,`left`,`right`,`column`],b=()=>[`center`,`top`,`bottom`,`left`,`right`,`top-left`,`left-top`,`top-right`,`right-top`,`bottom-right`,`right-bottom`,`bottom-left`,`left-bottom`],x=()=>[...b(),Y,q],ee=()=>[`auto`,`hidden`,`clip`,`visible`,`scroll`],te=()=>[`auto`,`contain`,`none`],S=()=>[Y,q,c],C=()=>[U,`full`,`auto`,...S()],ne=()=>[G,`none`,`subgrid`,Y,q],re=()=>[`auto`,{span:[`full`,G,Y,q]},G,Y,q],w=()=>[G,`auto`,Y,q],ie=()=>[`auto`,`min`,`max`,`fr`,Y,q],T=()=>[`start`,`end`,`center`,`between`,`around`,`evenly`,`stretch`,`baseline`,`center-safe`,`end-safe`],E=()=>[`start`,`end`,`center`,`stretch`,`center-safe`,`end-safe`],D=()=>[`auto`,...S()],O=()=>[U,`auto`,`full`,`dvw`,`dvh`,`lvw`,`lvh`,`svw`,`svh`,`min`,`max`,`fit`,...S()],k=()=>[U,`screen`,`full`,`dvw`,`lvw`,`svw`,`min`,`max`,`fit`,...S()],A=()=>[U,`screen`,`full`,`lh`,`dvh`,`lvh`,`svh`,`min`,`max`,`fit`,...S()],j=()=>[e,Y,q],ae=()=>[...b(),Ze,qe,{position:[Y,q]}],oe=()=>[`no-repeat`,{repeat:[``,`x`,`y`,`space`,`round`]}],M=()=>[`auto`,`cover`,`contain`,Qe,Ue,{size:[Y,q]}],N=()=>[Fe,X,J],P=()=>[``,`none`,`full`,l,Y,q],F=()=>[``,W,X,J],I=()=>[`solid`,`dashed`,`dotted`,`double`],se=()=>[`normal`,`multiply`,`screen`,`overlay`,`darken`,`lighten`,`color-dodge`,`color-burn`,`hard-light`,`soft-light`,`difference`,`exclusion`,`hue`,`saturation`,`color`,`luminosity`],L=()=>[W,Fe,Ze,qe],ce=()=>[``,`none`,m,Y,q],R=()=>[`none`,W,Y,q],z=()=>[`none`,W,Y,q],B=()=>[W,Y,q],V=()=>[U,`full`,...S()];return{cacheSize:500,theme:{animate:[`spin`,`ping`,`pulse`,`bounce`],aspect:[`video`],blur:[K],breakpoint:[K],color:[Ie],container:[K],"drop-shadow":[K],ease:[`in`,`out`,`in-out`],font:[Ve],"font-weight":[`thin`,`extralight`,`light`,`normal`,`medium`,`semibold`,`bold`,`extrabold`,`black`],"inset-shadow":[K],leading:[`none`,`tight`,`snug`,`normal`,`relaxed`,`loose`],perspective:[`dramatic`,`near`,`normal`,`midrange`,`distant`,`none`],radius:[K],shadow:[K],spacing:[`px`,W],text:[K],"text-shadow":[K],tracking:[`tighter`,`tight`,`normal`,`wide`,`wider`,`widest`]},classGroups:{aspect:[{aspect:[`auto`,`square`,U,q,Y,g]}],container:[`container`],"container-type":[{"@container":[``,`normal`,`size`,Y,q]}],"container-named":[He],columns:[{columns:[W,q,Y,s]}],"break-after":[{"break-after":y()}],"break-before":[{"break-before":y()}],"break-inside":[{"break-inside":[`auto`,`avoid`,`avoid-page`,`avoid-column`]}],"box-decoration":[{"box-decoration":[`slice`,`clone`]}],box:[{box:[`border`,`content`]}],display:[`block`,`inline-block`,`inline`,`flex`,`inline-flex`,`table`,`inline-table`,`table-caption`,`table-cell`,`table-column`,`table-column-group`,`table-footer-group`,`table-header-group`,`table-row-group`,`table-row`,`flow-root`,`grid`,`inline-grid`,`contents`,`list-item`,`hidden`],sr:[`sr-only`,`not-sr-only`],float:[{float:[`right`,`left`,`none`,`start`,`end`]}],clear:[{clear:[`left`,`right`,`both`,`none`,`start`,`end`]}],isolation:[`isolate`,`isolation-auto`],"object-fit":[{object:[`contain`,`cover`,`fill`,`none`,`scale-down`]}],"object-position":[{object:x()}],overflow:[{overflow:ee()}],"overflow-x":[{"overflow-x":ee()}],"overflow-y":[{"overflow-y":ee()}],overscroll:[{overscroll:te()}],"overscroll-x":[{"overscroll-x":te()}],"overscroll-y":[{"overscroll-y":te()}],position:[`static`,`fixed`,`absolute`,`relative`,`sticky`],inset:[{inset:C()}],"inset-x":[{"inset-x":C()}],"inset-y":[{"inset-y":C()}],start:[{"inset-s":C(),start:C()}],end:[{"inset-e":C(),end:C()}],"inset-bs":[{"inset-bs":C()}],"inset-be":[{"inset-be":C()}],top:[{top:C()}],right:[{right:C()}],bottom:[{bottom:C()}],left:[{left:C()}],visibility:[`visible`,`invisible`,`collapse`],z:[{z:[G,`auto`,Y,q]}],basis:[{basis:[U,`full`,`auto`,s,...S()]}],"flex-direction":[{flex:[`row`,`row-reverse`,`col`,`col-reverse`]}],"flex-wrap":[{flex:[`nowrap`,`wrap`,`wrap-reverse`]}],flex:[{flex:[W,U,`auto`,`initial`,`none`,q]}],grow:[{grow:[``,W,Y,q]}],shrink:[{shrink:[``,W,Y,q]}],order:[{order:[G,`first`,`last`,`none`,Y,q]}],"grid-cols":[{"grid-cols":ne()}],"col-start-end":[{col:re()}],"col-start":[{"col-start":w()}],"col-end":[{"col-end":w()}],"grid-rows":[{"grid-rows":ne()}],"row-start-end":[{row:re()}],"row-start":[{"row-start":w()}],"row-end":[{"row-end":w()}],"grid-flow":[{"grid-flow":[`row`,`col`,`dense`,`row-dense`,`col-dense`]}],"auto-cols":[{"auto-cols":ie()}],"auto-rows":[{"auto-rows":ie()}],gap:[{gap:S()}],"gap-x":[{"gap-x":S()}],"gap-y":[{"gap-y":S()}],"justify-content":[{justify:[...T(),`normal`]}],"justify-items":[{"justify-items":[...E(),`normal`]}],"justify-self":[{"justify-self":[`auto`,...E()]}],"align-content":[{content:[`normal`,...T()]}],"align-items":[{items:[...E(),{baseline:[``,`last`]}]}],"align-self":[{self:[`auto`,...E(),{baseline:[``,`last`]}]}],"place-content":[{"place-content":T()}],"place-items":[{"place-items":[...E(),`baseline`]}],"place-self":[{"place-self":[`auto`,...E()]}],p:[{p:S()}],px:[{px:S()}],py:[{py:S()}],ps:[{ps:S()}],pe:[{pe:S()}],pbs:[{pbs:S()}],pbe:[{pbe:S()}],pt:[{pt:S()}],pr:[{pr:S()}],pb:[{pb:S()}],pl:[{pl:S()}],m:[{m:D()}],mx:[{mx:D()}],my:[{my:D()}],ms:[{ms:D()}],me:[{me:D()}],mbs:[{mbs:D()}],mbe:[{mbe:D()}],mt:[{mt:D()}],mr:[{mr:D()}],mb:[{mb:D()}],ml:[{ml:D()}],"space-x":[{"space-x":S()}],"space-x-reverse":[`space-x-reverse`],"space-y":[{"space-y":S()}],"space-y-reverse":[`space-y-reverse`],size:[{size:O()}],"inline-size":[{inline:[`auto`,...k()]}],"min-inline-size":[{"min-inline":[`auto`,...k()]}],"max-inline-size":[{"max-inline":[`none`,...k()]}],"block-size":[{block:[`auto`,...A()]}],"min-block-size":[{"min-block":[`auto`,...A()]}],"max-block-size":[{"max-block":[`none`,...A()]}],w:[{w:[s,`screen`,...O()]}],"min-w":[{"min-w":[s,`screen`,`none`,...O()]}],"max-w":[{"max-w":[s,`screen`,`none`,`prose`,{screen:[o]},...O()]}],h:[{h:[`screen`,`lh`,...O()]}],"min-h":[{"min-h":[`screen`,`lh`,`none`,...O()]}],"max-h":[{"max-h":[`screen`,`lh`,...O()]}],"font-size":[{text:[`base`,n,X,J]}],"font-smoothing":[`antialiased`,`subpixel-antialiased`],"font-style":[`italic`,`not-italic`],"font-weight":[{font:[r,tt,Ge]}],"font-stretch":[{"font-stretch":[`ultra-condensed`,`extra-condensed`,`condensed`,`semi-condensed`,`normal`,`semi-expanded`,`expanded`,`extra-expanded`,`ultra-expanded`,Fe,q]}],"font-family":[{font:[Xe,Ke,t]}],"font-features":[{"font-features":[q]}],"fvn-normal":[`normal-nums`],"fvn-ordinal":[`ordinal`],"fvn-slashed-zero":[`slashed-zero`],"fvn-figure":[`lining-nums`,`oldstyle-nums`],"fvn-spacing":[`proportional-nums`,`tabular-nums`],"fvn-fraction":[`diagonal-fractions`,`stacked-fractions`],tracking:[{tracking:[i,Y,q]}],"line-clamp":[{"line-clamp":[W,`none`,Y,We]}],leading:[{leading:[a,...S()]}],"list-image":[{"list-image":[`none`,Y,q]}],"list-style-position":[{list:[`inside`,`outside`]}],"list-style-type":[{list:[`disc`,`decimal`,`none`,Y,q]}],"text-alignment":[{text:[`left`,`center`,`right`,`justify`,`start`,`end`]}],"placeholder-color":[{placeholder:j()}],"text-color":[{text:j()}],"text-decoration":[`underline`,`overline`,`line-through`,`no-underline`],"text-decoration-style":[{decoration:[...I(),`wavy`]}],"text-decoration-thickness":[{decoration:[W,`from-font`,`auto`,Y,J]}],"text-decoration-color":[{decoration:j()}],"underline-offset":[{"underline-offset":[W,`auto`,Y,q]}],"text-transform":[`uppercase`,`lowercase`,`capitalize`,`normal-case`],"text-overflow":[`truncate`,`text-ellipsis`,`text-clip`],"text-wrap":[{text:[`wrap`,`nowrap`,`balance`,`pretty`]}],indent:[{indent:S()}],"tab-size":[{tab:[G,Y,q]}],"vertical-align":[{align:[`baseline`,`top`,`middle`,`bottom`,`text-top`,`text-bottom`,`sub`,`super`,Y,q]}],whitespace:[{whitespace:[`normal`,`nowrap`,`pre`,`pre-line`,`pre-wrap`,`break-spaces`]}],break:[{break:[`normal`,`words`,`all`,`keep`]}],wrap:[{wrap:[`break-word`,`anywhere`,`normal`]}],hyphens:[{hyphens:[`none`,`manual`,`auto`]}],content:[{content:[`none`,Y,q]}],"bg-attachment":[{bg:[`fixed`,`local`,`scroll`]}],"bg-clip":[{"bg-clip":[`border`,`padding`,`content`,`text`]}],"bg-origin":[{"bg-origin":[`border`,`padding`,`content`]}],"bg-position":[{bg:ae()}],"bg-repeat":[{bg:oe()}],"bg-size":[{bg:M()}],"bg-image":[{bg:[`none`,{linear:[{to:[`t`,`tr`,`r`,`br`,`b`,`bl`,`l`,`tl`]},G,Y,q],radial:[``,Y,q],conic:[G,Y,q]},$e,Je]}],"bg-color":[{bg:j()}],"gradient-from-pos":[{from:N()}],"gradient-via-pos":[{via:N()}],"gradient-to-pos":[{to:N()}],"gradient-from":[{from:j()}],"gradient-via":[{via:j()}],"gradient-to":[{to:j()}],rounded:[{rounded:P()}],"rounded-s":[{"rounded-s":P()}],"rounded-e":[{"rounded-e":P()}],"rounded-t":[{"rounded-t":P()}],"rounded-r":[{"rounded-r":P()}],"rounded-b":[{"rounded-b":P()}],"rounded-l":[{"rounded-l":P()}],"rounded-ss":[{"rounded-ss":P()}],"rounded-se":[{"rounded-se":P()}],"rounded-ee":[{"rounded-ee":P()}],"rounded-es":[{"rounded-es":P()}],"rounded-tl":[{"rounded-tl":P()}],"rounded-tr":[{"rounded-tr":P()}],"rounded-br":[{"rounded-br":P()}],"rounded-bl":[{"rounded-bl":P()}],"border-w":[{border:F()}],"border-w-x":[{"border-x":F()}],"border-w-y":[{"border-y":F()}],"border-w-s":[{"border-s":F()}],"border-w-e":[{"border-e":F()}],"border-w-bs":[{"border-bs":F()}],"border-w-be":[{"border-be":F()}],"border-w-t":[{"border-t":F()}],"border-w-r":[{"border-r":F()}],"border-w-b":[{"border-b":F()}],"border-w-l":[{"border-l":F()}],"divide-x":[{"divide-x":F()}],"divide-x-reverse":[`divide-x-reverse`],"divide-y":[{"divide-y":F()}],"divide-y-reverse":[`divide-y-reverse`],"border-style":[{border:[...I(),`hidden`,`none`]}],"divide-style":[{divide:[...I(),`hidden`,`none`]}],"border-color":[{border:j()}],"border-color-x":[{"border-x":j()}],"border-color-y":[{"border-y":j()}],"border-color-s":[{"border-s":j()}],"border-color-e":[{"border-e":j()}],"border-color-bs":[{"border-bs":j()}],"border-color-be":[{"border-be":j()}],"border-color-t":[{"border-t":j()}],"border-color-r":[{"border-r":j()}],"border-color-b":[{"border-b":j()}],"border-color-l":[{"border-l":j()}],"divide-color":[{divide:j()}],"outline-style":[{outline:[...I(),`none`,`hidden`]}],"outline-offset":[{"outline-offset":[W,Y,q]}],"outline-w":[{outline:[``,W,X,J]}],"outline-color":[{outline:j()}],shadow:[{shadow:[``,`none`,u,et,Ye]}],"shadow-color":[{shadow:j()}],"inset-shadow":[{"inset-shadow":[`none`,d,et,Ye]}],"inset-shadow-color":[{"inset-shadow":j()}],"ring-w":[{ring:F()}],"ring-w-inset":[`ring-inset`],"ring-color":[{ring:j()}],"ring-offset-w":[{"ring-offset":[W,J]}],"ring-offset-color":[{"ring-offset":j()}],"inset-ring-w":[{"inset-ring":F()}],"inset-ring-color":[{"inset-ring":j()}],"text-shadow":[{"text-shadow":[`none`,f,et,Ye]}],"text-shadow-color":[{"text-shadow":j()}],opacity:[{opacity:[W,Y,q]}],"mix-blend":[{"mix-blend":[...se(),`plus-darker`,`plus-lighter`]}],"bg-blend":[{"bg-blend":se()}],"mask-clip":[{"mask-clip":[`border`,`padding`,`content`,`fill`,`stroke`,`view`]},`mask-no-clip`],"mask-composite":[{mask:[`add`,`subtract`,`intersect`,`exclude`]}],"mask-image-linear-pos":[{"mask-linear":[W]}],"mask-image-linear-from-pos":[{"mask-linear-from":L()}],"mask-image-linear-to-pos":[{"mask-linear-to":L()}],"mask-image-linear-from-color":[{"mask-linear-from":j()}],"mask-image-linear-to-color":[{"mask-linear-to":j()}],"mask-image-t-from-pos":[{"mask-t-from":L()}],"mask-image-t-to-pos":[{"mask-t-to":L()}],"mask-image-t-from-color":[{"mask-t-from":j()}],"mask-image-t-to-color":[{"mask-t-to":j()}],"mask-image-r-from-pos":[{"mask-r-from":L()}],"mask-image-r-to-pos":[{"mask-r-to":L()}],"mask-image-r-from-color":[{"mask-r-from":j()}],"mask-image-r-to-color":[{"mask-r-to":j()}],"mask-image-b-from-pos":[{"mask-b-from":L()}],"mask-image-b-to-pos":[{"mask-b-to":L()}],"mask-image-b-from-color":[{"mask-b-from":j()}],"mask-image-b-to-color":[{"mask-b-to":j()}],"mask-image-l-from-pos":[{"mask-l-from":L()}],"mask-image-l-to-pos":[{"mask-l-to":L()}],"mask-image-l-from-color":[{"mask-l-from":j()}],"mask-image-l-to-color":[{"mask-l-to":j()}],"mask-image-x-from-pos":[{"mask-x-from":L()}],"mask-image-x-to-pos":[{"mask-x-to":L()}],"mask-image-x-from-color":[{"mask-x-from":j()}],"mask-image-x-to-color":[{"mask-x-to":j()}],"mask-image-y-from-pos":[{"mask-y-from":L()}],"mask-image-y-to-pos":[{"mask-y-to":L()}],"mask-image-y-from-color":[{"mask-y-from":j()}],"mask-image-y-to-color":[{"mask-y-to":j()}],"mask-image-radial":[{"mask-radial":[Y,q]}],"mask-image-radial-from-pos":[{"mask-radial-from":L()}],"mask-image-radial-to-pos":[{"mask-radial-to":L()}],"mask-image-radial-from-color":[{"mask-radial-from":j()}],"mask-image-radial-to-color":[{"mask-radial-to":j()}],"mask-image-radial-shape":[{"mask-radial":[`circle`,`ellipse`]}],"mask-image-radial-size":[{"mask-radial":[{closest:[`side`,`corner`],farthest:[`side`,`corner`]}]}],"mask-image-radial-pos":[{"mask-radial-at":b()}],"mask-image-conic-pos":[{"mask-conic":[W]}],"mask-image-conic-from-pos":[{"mask-conic-from":L()}],"mask-image-conic-to-pos":[{"mask-conic-to":L()}],"mask-image-conic-from-color":[{"mask-conic-from":j()}],"mask-image-conic-to-color":[{"mask-conic-to":j()}],"mask-mode":[{mask:[`alpha`,`luminance`,`match`]}],"mask-origin":[{"mask-origin":[`border`,`padding`,`content`,`fill`,`stroke`,`view`]}],"mask-position":[{mask:ae()}],"mask-repeat":[{mask:oe()}],"mask-size":[{mask:M()}],"mask-type":[{"mask-type":[`alpha`,`luminance`]}],"mask-image":[{mask:[`none`,Y,q]}],filter:[{filter:[``,`none`,Y,q]}],blur:[{blur:ce()}],brightness:[{brightness:[W,Y,q]}],contrast:[{contrast:[W,Y,q]}],"drop-shadow":[{"drop-shadow":[``,`none`,p,et,Ye]}],"drop-shadow-color":[{"drop-shadow":j()}],grayscale:[{grayscale:[``,W,Y,q]}],"hue-rotate":[{"hue-rotate":[W,Y,q]}],invert:[{invert:[``,W,Y,q]}],saturate:[{saturate:[W,Y,q]}],sepia:[{sepia:[``,W,Y,q]}],"backdrop-filter":[{"backdrop-filter":[``,`none`,Y,q]}],"backdrop-blur":[{"backdrop-blur":ce()}],"backdrop-brightness":[{"backdrop-brightness":[W,Y,q]}],"backdrop-contrast":[{"backdrop-contrast":[W,Y,q]}],"backdrop-grayscale":[{"backdrop-grayscale":[``,W,Y,q]}],"backdrop-hue-rotate":[{"backdrop-hue-rotate":[W,Y,q]}],"backdrop-invert":[{"backdrop-invert":[``,W,Y,q]}],"backdrop-opacity":[{"backdrop-opacity":[W,Y,q]}],"backdrop-saturate":[{"backdrop-saturate":[W,Y,q]}],"backdrop-sepia":[{"backdrop-sepia":[``,W,Y,q]}],"border-collapse":[{border:[`collapse`,`separate`]}],"border-spacing":[{"border-spacing":S()}],"border-spacing-x":[{"border-spacing-x":S()}],"border-spacing-y":[{"border-spacing-y":S()}],"table-layout":[{table:[`auto`,`fixed`]}],caption:[{caption:[`top`,`bottom`]}],transition:[{transition:[``,`all`,`colors`,`opacity`,`shadow`,`transform`,`none`,Y,q]}],"transition-behavior":[{transition:[`normal`,`discrete`]}],duration:[{duration:[W,`initial`,Y,q]}],ease:[{ease:[`linear`,`initial`,_,Y,q]}],delay:[{delay:[W,Y,q]}],animate:[{animate:[`none`,v,Y,q]}],backface:[{backface:[`hidden`,`visible`]}],perspective:[{perspective:[h,Y,q]}],"perspective-origin":[{"perspective-origin":x()}],rotate:[{rotate:R()}],"rotate-x":[{"rotate-x":R()}],"rotate-y":[{"rotate-y":R()}],"rotate-z":[{"rotate-z":R()}],scale:[{scale:z()}],"scale-x":[{"scale-x":z()}],"scale-y":[{"scale-y":z()}],"scale-z":[{"scale-z":z()}],"scale-3d":[`scale-3d`],skew:[{skew:B()}],"skew-x":[{"skew-x":B()}],"skew-y":[{"skew-y":B()}],transform:[{transform:[Y,q,``,`none`,`gpu`,`cpu`]}],"transform-origin":[{origin:x()}],"transform-style":[{transform:[`3d`,`flat`]}],translate:[{translate:V()}],"translate-x":[{"translate-x":V()}],"translate-y":[{"translate-y":V()}],"translate-z":[{"translate-z":V()}],"translate-none":[`translate-none`],zoom:[{zoom:[G,Y,q]}],accent:[{accent:j()}],appearance:[{appearance:[`none`,`auto`]}],"caret-color":[{caret:j()}],"color-scheme":[{scheme:[`normal`,`dark`,`light`,`light-dark`,`only-dark`,`only-light`]}],cursor:[{cursor:[`auto`,`default`,`pointer`,`wait`,`text`,`move`,`help`,`not-allowed`,`none`,`context-menu`,`progress`,`cell`,`crosshair`,`vertical-text`,`alias`,`copy`,`no-drop`,`grab`,`grabbing`,`all-scroll`,`col-resize`,`row-resize`,`n-resize`,`e-resize`,`s-resize`,`w-resize`,`ne-resize`,`nw-resize`,`se-resize`,`sw-resize`,`ew-resize`,`ns-resize`,`nesw-resize`,`nwse-resize`,`zoom-in`,`zoom-out`,Y,q]}],"field-sizing":[{"field-sizing":[`fixed`,`content`]}],"pointer-events":[{"pointer-events":[`auto`,`none`]}],resize:[{resize:[`none`,``,`y`,`x`]}],"scroll-behavior":[{scroll:[`auto`,`smooth`]}],"scrollbar-thumb-color":[{"scrollbar-thumb":j()}],"scrollbar-track-color":[{"scrollbar-track":j()}],"scrollbar-gutter":[{"scrollbar-gutter":[`auto`,`stable`,`both`]}],"scrollbar-w":[{scrollbar:[`auto`,`thin`,`none`]}],"scroll-m":[{"scroll-m":S()}],"scroll-mx":[{"scroll-mx":S()}],"scroll-my":[{"scroll-my":S()}],"scroll-ms":[{"scroll-ms":S()}],"scroll-me":[{"scroll-me":S()}],"scroll-mbs":[{"scroll-mbs":S()}],"scroll-mbe":[{"scroll-mbe":S()}],"scroll-mt":[{"scroll-mt":S()}],"scroll-mr":[{"scroll-mr":S()}],"scroll-mb":[{"scroll-mb":S()}],"scroll-ml":[{"scroll-ml":S()}],"scroll-p":[{"scroll-p":S()}],"scroll-px":[{"scroll-px":S()}],"scroll-py":[{"scroll-py":S()}],"scroll-ps":[{"scroll-ps":S()}],"scroll-pe":[{"scroll-pe":S()}],"scroll-pbs":[{"scroll-pbs":S()}],"scroll-pbe":[{"scroll-pbe":S()}],"scroll-pt":[{"scroll-pt":S()}],"scroll-pr":[{"scroll-pr":S()}],"scroll-pb":[{"scroll-pb":S()}],"scroll-pl":[{"scroll-pl":S()}],"snap-align":[{snap:[`start`,`end`,`center`,`align-none`]}],"snap-stop":[{snap:[`normal`,`always`]}],"snap-type":[{snap:[`none`,`x`,`y`,`both`]}],"snap-strictness":[{snap:[`mandatory`,`proximity`]}],touch:[{touch:[`auto`,`none`,`manipulation`]}],"touch-x":[{"touch-pan":[`x`,`left`,`right`]}],"touch-y":[{"touch-pan":[`y`,`up`,`down`]}],"touch-pz":[`touch-pinch-zoom`],select:[{select:[`none`,`text`,`all`,`auto`]}],"will-change":[{"will-change":[`auto`,`scroll`,`contents`,`transform`,Y,q]}],fill:[{fill:[`none`,...j()]}],"stroke-w":[{stroke:[W,X,J,We]}],stroke:[{stroke:[`none`,...j()]}],"forced-color-adjust":[{"forced-color-adjust":[`auto`,`none`]}]},conflictingClassGroups:{"container-named":[`container-type`],overflow:[`overflow-x`,`overflow-y`],overscroll:[`overscroll-x`,`overscroll-y`],inset:[`inset-x`,`inset-y`,`inset-bs`,`inset-be`,`start`,`end`,`top`,`right`,`bottom`,`left`],"inset-x":[`right`,`left`],"inset-y":[`top`,`bottom`],flex:[`basis`,`grow`,`shrink`],gap:[`gap-x`,`gap-y`],p:[`px`,`py`,`ps`,`pe`,`pbs`,`pbe`,`pt`,`pr`,`pb`,`pl`],px:[`pr`,`pl`],py:[`pt`,`pb`],m:[`mx`,`my`,`ms`,`me`,`mbs`,`mbe`,`mt`,`mr`,`mb`,`ml`],mx:[`mr`,`ml`],my:[`mt`,`mb`],size:[`w`,`h`],"font-size":[`leading`],"fvn-normal":[`fvn-ordinal`,`fvn-slashed-zero`,`fvn-figure`,`fvn-spacing`,`fvn-fraction`],"fvn-ordinal":[`fvn-normal`],"fvn-slashed-zero":[`fvn-normal`],"fvn-figure":[`fvn-normal`],"fvn-spacing":[`fvn-normal`],"fvn-fraction":[`fvn-normal`],"line-clamp":[`display`,`overflow`],rounded:[`rounded-s`,`rounded-e`,`rounded-t`,`rounded-r`,`rounded-b`,`rounded-l`,`rounded-ss`,`rounded-se`,`rounded-ee`,`rounded-es`,`rounded-tl`,`rounded-tr`,`rounded-br`,`rounded-bl`],"rounded-s":[`rounded-ss`,`rounded-es`],"rounded-e":[`rounded-se`,`rounded-ee`],"rounded-t":[`rounded-tl`,`rounded-tr`],"rounded-r":[`rounded-tr`,`rounded-br`],"rounded-b":[`rounded-br`,`rounded-bl`],"rounded-l":[`rounded-tl`,`rounded-bl`],"border-spacing":[`border-spacing-x`,`border-spacing-y`],"border-w":[`border-w-x`,`border-w-y`,`border-w-s`,`border-w-e`,`border-w-bs`,`border-w-be`,`border-w-t`,`border-w-r`,`border-w-b`,`border-w-l`],"border-w-x":[`border-w-r`,`border-w-l`],"border-w-y":[`border-w-t`,`border-w-b`],"border-color":[`border-color-x`,`border-color-y`,`border-color-s`,`border-color-e`,`border-color-bs`,`border-color-be`,`border-color-t`,`border-color-r`,`border-color-b`,`border-color-l`],"border-color-x":[`border-color-r`,`border-color-l`],"border-color-y":[`border-color-t`,`border-color-b`],translate:[`translate-x`,`translate-y`,`translate-none`],"translate-none":[`translate`,`translate-x`,`translate-y`,`translate-z`],"scroll-m":[`scroll-mx`,`scroll-my`,`scroll-ms`,`scroll-me`,`scroll-mbs`,`scroll-mbe`,`scroll-mt`,`scroll-mr`,`scroll-mb`,`scroll-ml`],"scroll-mx":[`scroll-mr`,`scroll-ml`],"scroll-my":[`scroll-mt`,`scroll-mb`],"scroll-p":[`scroll-px`,`scroll-py`,`scroll-ps`,`scroll-pe`,`scroll-pbs`,`scroll-pbe`,`scroll-pt`,`scroll-pr`,`scroll-pb`,`scroll-pl`],"scroll-px":[`scroll-pr`,`scroll-pl`],"scroll-py":[`scroll-pt`,`scroll-pb`],touch:[`touch-x`,`touch-y`,`touch-pz`],"touch-x":[`touch`],"touch-y":[`touch`],"touch-pz":[`touch`]},conflictingClassGroupModifiers:{"font-size":[`leading`]},postfixLookupClassGroups:[`container-type`],orderSensitiveModifiers:[`*`,`**`,`after`,`backdrop`,`before`,`details-content`,`file`,`first-letter`,`first-line`,`marker`,`placeholder`,`selection`]}});function $(...e){return ut(A(e))}var dt=e=>typeof e==`boolean`?`${e}`:e===0?`0`:e,ft=A,pt=(e,t)=>n=>{if(t?.variants==null)return ft(e,n?.class,n?.className);let{variants:r,defaultVariants:i}=t,a=Object.keys(r).map(e=>{let t=n?.[e],a=i?.[e];if(t===null)return null;let o=dt(t)||dt(a);return r[e][o]}),o=n&&Object.entries(n).reduce((e,t)=>{let[n,r]=t;return r===void 0||(e[n]=r),e},{});return ft(e,a,t?.compoundVariants?.reduce((e,t)=>{let{class:n,className:r,...a}=t;return Object.entries(a).every(e=>{let[t,n]=e;return Array.isArray(n)?n.includes({...i,...o}[t]):{...i,...o}[t]===n})?[...e,n,r]:e},[]),n?.class,n?.className)},mt=pt(`group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-[13px] font-medium whitespace-nowrap transition-[background-color,border-color,color,scale] duration-150 ease-spring outline-none select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring active:not-aria-[haspopup]:scale-[0.97] disabled:pointer-events-none disabled:opacity-40 aria-invalid:border-destructive motion-reduce:active:scale-100 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 max-sm:has-[>[data-slot=button-label]]:aspect-square max-sm:has-[>[data-slot=button-label]]:px-0!`,{variants:{variant:{default:`bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--primary),var(--primary-foreground)_14%)] active:bg-[color-mix(in_oklab,var(--primary),var(--primary-foreground)_20%)]`,outline:`border-input bg-control hover:border-muted-foreground/60 hover:bg-foreground/4 aria-expanded:bg-foreground/6`,secondary:`bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklab,var(--secondary),var(--foreground)_6%)] active:bg-[color-mix(in_oklab,var(--secondary),var(--foreground)_10%)] aria-expanded:bg-[color-mix(in_oklab,var(--secondary),var(--foreground)_6%)]`,ghost:`hover:bg-foreground/6 active:bg-foreground/10 aria-expanded:bg-foreground/6`,destructive:`bg-destructive/10 text-destructive hover:bg-destructive/16 active:bg-destructive/22 focus-visible:outline-destructive`,link:`text-primary underline-offset-4 hover:underline active:scale-100`},size:{default:`h-[30px] gap-1.5 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5`,xs:`h-[22px] gap-1 rounded-md px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3`,sm:`h-[26px] gap-1 rounded-md px-2.5 text-[12.5px] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5`,lg:`h-9 gap-1.5 px-4 text-sm has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3`,icon:`size-[30px]`,"icon-xs":`size-[22px] rounded-md [&_svg:not([class*='size-'])]:size-3`,"icon-sm":`size-[26px] rounded-md`,"icon-lg":`size-9`}},defaultVariants:{variant:`default`,size:`default`}});function ht({className:e,variant:t=`default`,size:n=`default`,...r}){return f(m,{"data-slot":`button`,className:$(mt({variant:t,size:n,className:e})),...r})}t.createContext({size:`default`,variant:`default`,spacing:2,orientation:`horizontal`}),t.createContext(null),t.createContext(null),typeof navigator<`u`&&/Mac|iPhone|iPad/.test(navigator.userAgent);function gt({...e}){return f(h.Root,{"data-slot":`alert-dialog`,...e})}function _t({...e}){return f(h.Portal,{"data-slot":`alert-dialog-portal`,...e})}function vt({className:e,...t}){return f(h.Backdrop,{forceRender:!0,"data-slot":`alert-dialog-overlay`,className:$(`fixed inset-0 isolate z-50 bg-linear-to-b from-scrim-from to-scrim-to duration-200 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0`,e),...t})}function yt({className:e,size:t=`default`,...n}){return p(_t,{children:[f(vt,{}),f(h.Popup,{"data-slot":`alert-dialog-content`,"data-size":t,className:$(`[--control:var(--popover)] group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-[20px] bg-popover p-5 text-popover-foreground shadow-[0_24px_64px_rgb(0_0_0/0.22)] ring-1 ring-border duration-200 ease-spring outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95`,e),...n})]})}function bt({className:e,...t}){return f(`div`,{"data-slot":`alert-dialog-header`,className:$(`grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]`,e),...t})}function xt({className:e,...t}){return f(`div`,{"data-slot":`alert-dialog-footer`,className:$(`flex flex-col-reverse gap-2 pt-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end`,e),...t})}function St({className:e,...t}){return f(h.Title,{"data-slot":`alert-dialog-title`,className:$(`text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2`,e),...t})}function Ct({className:e,...t}){return f(h.Description,{"data-slot":`alert-dialog-description`,className:$(`text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground`,e),...t})}function wt({className:e,variant:t=`outline`,size:n=`default`,...r}){return f(h.Close,{"data-slot":`alert-dialog-cancel`,className:$(e),render:f(ht,{variant:t,size:n}),...r})}function Tt({className:e,size:t=`default`,...n}){return f(g.Root,{"data-slot":`avatar`,"data-size":t,className:$(`group/avatar relative isolate flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten`,e),...n})}function Et({className:e,...t}){return f(g.Image,{"data-slot":`avatar-image`,className:$(`aspect-square size-full rounded-full object-cover`,e),...t})}function Dt({className:e,...t}){return f(g.Fallback,{"data-slot":`avatar-fallback`,className:$(`flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs`,e),...t})}var Ot=pt(`group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-md border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive [&>svg]:pointer-events-none [&>svg]:size-3!`,{variants:{variant:{default:`bg-primary text-primary-foreground [a]:hover:bg-primary/80`,secondary:`bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80`,destructive:`bg-destructive/10 text-destructive focus-visible:outline-destructive dark:bg-destructive/16 [a]:hover:bg-destructive/20`,outline:`border-input text-foreground [a]:hover:bg-muted`,ghost:`hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50`,link:`text-primary underline-offset-4 hover:underline`}},defaultVariants:{variant:`default`}});function kt({className:e,variant:t=`default`,render:n,...r}){return v({defaultTagName:`span`,props:_({className:$(Ot({variant:t}),e)},r),render:n,state:{slot:`badge`,variant:t}})}y.Root,b.Root;function At({className:e,...t}){return f(D,{"data-slot":`spinner`,role:`status`,"aria-label":`Loading`,className:$(`size-4 animate-spin`,e),...t})}n(null);var jt=class{#e={};addListener(e,t){let n=this.#e[e];return n===void 0?this.#e[e]=[t]:n.includes(t)||n.push(t),()=>{this.removeListener(e,t)}}emit(e,t){let n=this.#e[e];if(n!==void 0){if(n.length===1)n[0].call(null,t);else{let e=!1,r=null,i=Array.from(n);for(let n=0;n<i.length;n++){let a=i[n];try{a.call(null,t)}catch(t){r===null&&(e=!0,r=t)}}if(e)throw r}}}removeAllListeners(){this.#e={}}removeListener(e,t){let n=this.#e[e];if(n!==void 0){let e=n.indexOf(t);e>=0&&n.splice(e,1)}}};new jt,new jt,n(null),t.createContext(null);function Mt(e){let t=new Date(e);return t.toLocaleString(`zh-CN`,{...t.getFullYear()===new Date().getFullYear()?{}:{year:`numeric`},month:`2-digit`,day:`2-digit`,hour:`2-digit`,minute:`2-digit`,hour12:!1})}function Nt(e){let t=Math.ceil(e/6e4);if(t<60)return`${t} 分钟`;let n=Math.floor(t/60);return n<24?t%60?`${n} 小时 ${t%60} 分钟`:`${n} 小时`:n%24?`${Math.floor(n/24)} 天 ${n%24} 小时`:`${Math.floor(n/24)} 天`}var Pt=e=>e.name?`${e.name}(${e.userId})`:e.userId;function Ft(){let[e,t]=s(Date.now);return o(()=>{let e=setInterval(()=>t(Date.now()),3e4);return()=>clearInterval(e)},[]),e}function It(e){if(e.avatar)return e.avatar;if(e.platform===`onebot`&&/^\d+$/.test(e.userId))return`https://q1.qlogo.cn/g?b=qq&nk=${e.userId}&s=100`}function Lt(e){let t=d(`blacklist.list`),n=Ft(),[r,i]=s(),a=(t.data??[]).filter(e=>e.expiresAt===0||e.expiresAt>n).reverse(),o=String(e.schema.meta.label??e.path),c=e.schema.meta.description,l=t.status===`loading`&&t.data===void 0?p(`span`,{className:`inline-flex items-center gap-2`,children:[f(At,{}),`正在加载`]}):t.status===`offline`&&t.data===void 0?`无法连接服务器`:`暂无已拉黑用户`;return p(`div`,{className:`min-w-0 space-y-3 text-sm leading-5`,"data-blacklist-list":``,children:[p(`div`,{className:`space-y-1`,children:[p(`div`,{className:`flex items-center gap-2 text-base font-semibold leading-6`,children:[o,a.length>0&&f(kt,{variant:`secondary`,children:a.length})]}),typeof c==`string`&&f(`p`,{className:`text-muted-foreground`,children:c})]}),a.length===0?f(`div`,{className:`flex h-16 items-center justify-center rounded-lg border border-dashed text-muted-foreground`,children:l}):f(`ul`,{"aria-label":o,className:`divide-y overflow-hidden rounded-lg border`,children:a.map(e=>{let t=e.name||e.userId,r=e.expiresAt!==0;return p(`li`,{className:`flex items-center gap-3 px-3 py-2.5`,children:[p(Tt,{className:`self-start`,children:[f(Et,{src:It(e),alt:``,referrerPolicy:`no-referrer`}),f(Dt,{children:Array.from(t)[0]})]}),p(`div`,{className:`min-w-0 flex-1`,children:[p(`div`,{className:`flex min-w-0 items-baseline gap-2`,children:[f(`span`,{className:`truncate font-medium`,children:t}),p(`span`,{className:`shrink-0 text-xs leading-4 text-muted-foreground tabular-nums`,children:[e.name?`${e.userId} · `:``,e.platform]})]}),f(`p`,{className:`line-clamp-2 text-muted-foreground [overflow-wrap:anywhere]`,title:e.reason,children:e.reason})]}),p(`div`,{className:`flex shrink-0 flex-col items-end gap-1`,children:[r?p(kt,{variant:`secondary`,className:`tabular-nums`,title:`${Mt(e.expiresAt)} 到期`,children:[f(O,{}),`剩 `,Nt(e.expiresAt-n)]}):p(kt,{variant:`destructive`,children:[f(E,{}),`永久`]}),p(`span`,{className:`text-xs leading-4 text-muted-foreground tabular-nums`,children:[Mt(e.createdAt),` 拉黑`]})]}),f(ht,{type:`button`,variant:`ghost`,size:`xs`,className:`shrink-0 text-muted-foreground hover:text-foreground`,"aria-label":`解除拉黑:${Pt(e)}`,onClick:()=>i(e),children:`解除`})]},e.id)})}),f(Rt,{entry:r,onClose:()=>i(void 0)})]})}function Rt({entry:e,onClose:t}){let[n,r]=s(!1),i=async()=>{if(e){r(!0);try{await l(`blacklist.remove`,{id:e.id}),u.success(`已解除 ${Pt(e)} 的拉黑`),t()}catch(e){u.error(c(e))}finally{r(!1)}}};return f(gt,{open:e!==void 0,onOpenChange:e=>!e&&t(),children:p(yt,{size:`sm`,children:[p(bt,{children:[f(St,{children:`解除拉黑`}),p(Ct,{children:[`解除后 `,e?Pt(e):``,` 可再次与机器人对话,拉黑理由一并删除。`]})]}),p(xt,{children:[f(wt,{children:`取消`}),p(ht,{variant:`destructive`,disabled:n,onClick:()=>void i(),children:[n&&f(At,{}),`解除拉黑`]})]})]})})}function zt(t){t.effect(()=>e({role:`blacklist:list`,component:Lt,priority:30}))}export{zt as default};
@@ -0,0 +1,80 @@
1
+ import type { Context } from 'cordis';
2
+ import type { Bot } from '@satorijs/core';
3
+ export interface BlacklistEntry {
4
+ /** `platform:userId`。 */
5
+ id: string;
6
+ platform: string;
7
+ userId: string;
8
+ /** 拉黑时记下的名称,未知为空字符串。 */
9
+ name: string;
10
+ /** 拉黑时取到的头像地址,未知为空字符串。 */
11
+ avatar: string;
12
+ reason: string;
13
+ /** 发起拉黑的用户 `platform:userId`,没有发起人时为空字符串。 */
14
+ operator: string;
15
+ createdAt: number;
16
+ /** 到期时间戳;0 表示永久。到期的记录不再生效,下次加载时从数据库删除。 */
17
+ expiresAt: number;
18
+ }
19
+ declare module '@cordisjs/plugin-database' {
20
+ interface Tables {
21
+ chatluna_blacklist: BlacklistEntry;
22
+ }
23
+ }
24
+ declare module 'cordis' {
25
+ interface Events {
26
+ /** 黑名单增删后触发,WebUI 的 `blacklist.list` 据此推送。 */
27
+ 'blacklist/change'(): void;
28
+ }
29
+ }
30
+ export interface BlacklistInput {
31
+ platform: string;
32
+ userId: string;
33
+ name?: string;
34
+ avatar?: string;
35
+ reason: string;
36
+ operator?: string;
37
+ /** 拉黑时长(毫秒);不填或 0 表示永久。 */
38
+ duration?: number;
39
+ }
40
+ export declare const keyOf: (platform: string, userId: string) => string;
41
+ /**
42
+ * 黑名单存在数据库里,启动时整表读进内存:每条入站消息都要查询,命中判断不能走数据库。
43
+ * 写入先落库再改内存,失败时内存与数据库保持一致。到期的记录在内存里按未拉黑处理,不另设定时器清理。
44
+ */
45
+ export declare class Blacklist {
46
+ private readonly ctx;
47
+ readonly ready: Promise<void>;
48
+ private readonly entries;
49
+ constructor(ctx: Context);
50
+ private load;
51
+ has(platform: string, userId: string | undefined): boolean;
52
+ /** 只返回仍在生效的记录。 */
53
+ get(platform: string, userId: string): BlacklistEntry | undefined;
54
+ /** 按拉黑时间先后排列。 */
55
+ list(platform?: string): BlacklistEntry[];
56
+ /** 已在黑名单中时原样返回旧记录,不覆盖原理由和期限;已到期的旧记录直接覆盖。 */
57
+ add(input: BlacklistInput): Promise<{
58
+ entry: BlacklistEntry;
59
+ created: boolean;
60
+ }>;
61
+ /** 按 `platform:userId` 移除,返回被移除的生效记录;不在黑名单中或已到期时返回 undefined,到期记录顺带删除。 */
62
+ remove(id: string): Promise<BlacklistEntry | undefined>;
63
+ }
64
+ /**
65
+ * 拉黑时顺带记下的名称与头像,群里优先取群名片。平台不支持或查询失败时留空,不影响拉黑。
66
+ */
67
+ export declare function profileOf(bot: Bot | undefined, userId: string, guildId?: string): Promise<{
68
+ name?: string;
69
+ avatar?: string;
70
+ }>;
71
+ /** 名称未知时只显示 ID。 */
72
+ export declare const displayOf: (entry: BlacklistEntry) => string;
73
+ export declare const termOf: (entry: BlacklistEntry) => string;
74
+ /** 工具与指令共用的拉黑结果。 */
75
+ export declare function addedText({ entry, created }: {
76
+ entry: BlacklistEntry;
77
+ created: boolean;
78
+ }): string;
79
+ export declare function formatList(entries: BlacklistEntry[]): string;
80
+ //# sourceMappingURL=blacklist.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blacklist.d.ts","sourceRoot":"","sources":["../src/blacklist.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AAErC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAA;AAEzC,MAAM,WAAW,cAAc;IAC7B,yBAAyB;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,OAAO,QAAQ,2BAA2B,CAAC;IACzC,UAAU,MAAM;QACd,kBAAkB,EAAE,cAAc,CAAA;KACnC;CACF;AAED,OAAO,QAAQ,QAAQ,CAAC;IACtB,UAAU,MAAM;QACd,8CAA8C;QAC9C,kBAAkB,IAAI,IAAI,CAAA;KAC3B;CACF;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,eAAO,MAAM,KAAK,aAAc,MAAM,UAAU,MAAM,WAA4B,CAAA;AAIlF;;;GAGG;AACH,qBAAa,SAAS;IAIR,OAAO,CAAC,QAAQ,CAAC,GAAG;IAHhC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoC;IAE5D,YAA6B,GAAG,EAAE,OAAO,EAaxC;YAEa,IAAI;IAMlB,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,WAE/C;IAED,kBAAkB;IAClB,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,8BAGnC;IAED,iBAAiB;IACjB,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,oBAIrB;IAED,4CAA4C;IACtC,GAAG,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,cAAc,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAuBrF;IAED,0EAA0E;IACpE,MAAM,CAAC,EAAE,EAAE,MAAM,uCAUtB;CACF;AAED;;GAEG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,GAAG,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAKnI;AAED,mBAAmB;AACnB,eAAO,MAAM,SAAS,UAAW,cAAc,WAAiE,CAAA;AAIhH,eAAO,MAAM,MAAM,UAAW,cAAc,WAAkE,CAAA;AAE9G,oBAAoB;AACpB,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;IAAE,KAAK,EAAE,cAAc,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,UAIxF;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,cAAc,EAAE,UAQnD"}
@@ -0,0 +1,6 @@
1
+ import type { Context } from 'cordis';
2
+ import { type Blacklist } from './blacklist';
3
+ import type { Config } from './config';
4
+ /** 指令依赖 Koishi 兼容层:屏蔽已拉黑用户的指令,并给管理员提供查看与增删黑名单的指令。 */
5
+ export declare function registerCommands(ctx: Context, blacklist: Blacklist, config: Config): void;
6
+ //# sourceMappingURL=commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AAIrC,OAAO,EAAa,KAAK,SAAS,EAA2C,MAAM,aAAa,CAAA;AAChG,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAUtC,qDAAqD;AACrD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,QA2ClF"}
@@ -0,0 +1,9 @@
1
+ import { Schema } from '@chatluna/schema';
2
+ export interface Config {
3
+ blockCommands: boolean;
4
+ /** 只用来在设置页挂载黑名单表格,不保存任何值。 */
5
+ list?: unknown;
6
+ }
7
+ /** 每个字段都写 `extra('label')`,缺 label 时 SchemaForm 会把配置键当标题显示。 */
8
+ export declare const Config: Schema<Partial<Config>, Config>;
9
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAEzC,MAAM,WAAW,MAAM;IACrB,aAAa,EAAE,OAAO,CAAA;IACtB,6BAA6B;IAC7B,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,+DAA+D;AAC/D,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAKjD,CAAA"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { Context } from 'cordis';
2
+ import type { Config } from './config';
3
+ export * from './config';
4
+ export type { BlacklistEntry } from './blacklist';
5
+ export declare const name = "chatluna-blacklist";
6
+ /** koishi 与 chatluna_webui 是可选服务:没有 Koishi 兼容层时只屏蔽对话,没有 WebUI 时设置页不显示黑名单。 */
7
+ export declare const inject: string[];
8
+ export declare function apply(ctx: Context, config: Config): void;
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AAKrC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAItC,cAAc,UAAU,CAAA;AACxB,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEjD,eAAO,MAAM,IAAI,uBAAuB,CAAA;AAExC,6EAA6E;AAC7E,eAAO,MAAM,MAAM,UAAgD,CAAA;AAEnE,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAYjD"}
package/lib/index.js ADDED
@@ -0,0 +1,336 @@
1
+ import { Time } from "cosmokit";
2
+ import { Schema } from "@chatluna/schema";
3
+ // external/blacklist/src/blacklist.ts
4
+ const keyOf = (platform, userId) => `${platform}:${userId}`;
5
+ const active = (entry) => entry.expiresAt === 0 || entry.expiresAt > Date.now();
6
+ /**
7
+ * 黑名单存在数据库里,启动时整表读进内存:每条入站消息都要查询,命中判断不能走数据库。
8
+ * 写入先落库再改内存,失败时内存与数据库保持一致。到期的记录在内存里按未拉黑处理,不另设定时器清理。
9
+ */
10
+ var Blacklist = class {
11
+ ctx;
12
+ ready;
13
+ entries = /* @__PURE__ */ new Map();
14
+ constructor(ctx) {
15
+ this.ctx = ctx;
16
+ ctx.model.extend("chatluna_blacklist", {
17
+ id: "string",
18
+ platform: "string",
19
+ userId: "string",
20
+ name: "string",
21
+ avatar: "text",
22
+ reason: "text",
23
+ operator: "string",
24
+ createdAt: "integer",
25
+ expiresAt: "integer"
26
+ }, {
27
+ primary: "id",
28
+ unique: [["platform", "userId"]]
29
+ });
30
+ this.ready = this.load();
31
+ }
32
+ async load() {
33
+ await this.ctx.database.remove("chatluna_blacklist", { expiresAt: {
34
+ $gt: 0,
35
+ $lte: Date.now()
36
+ } });
37
+ for (const row of await this.ctx.database.get("chatluna_blacklist", {})) this.entries.set(row.id, row);
38
+ this.ctx.emit("blacklist/change");
39
+ }
40
+ has(platform, userId) {
41
+ return userId !== void 0 && this.get(platform, userId) !== void 0;
42
+ }
43
+ /** 只返回仍在生效的记录。 */
44
+ get(platform, userId) {
45
+ const entry = this.entries.get(keyOf(platform, userId));
46
+ return entry && active(entry) ? entry : void 0;
47
+ }
48
+ /** 按拉黑时间先后排列。 */
49
+ list(platform) {
50
+ return [...this.entries.values()].filter((entry) => active(entry) && (platform === void 0 || entry.platform === platform)).sort((a, b) => a.createdAt - b.createdAt);
51
+ }
52
+ /** 已在黑名单中时原样返回旧记录,不覆盖原理由和期限;已到期的旧记录直接覆盖。 */
53
+ async add(input) {
54
+ const reason = input.reason.trim();
55
+ if (!reason) throw new Error("拉黑必须填写理由。");
56
+ await this.ready;
57
+ const existing = this.get(input.platform, input.userId);
58
+ if (existing) return {
59
+ entry: existing,
60
+ created: false
61
+ };
62
+ const createdAt = Date.now();
63
+ const entry = {
64
+ id: keyOf(input.platform, input.userId),
65
+ platform: input.platform,
66
+ userId: input.userId,
67
+ name: input.name?.trim() ?? "",
68
+ avatar: input.avatar ?? "",
69
+ reason,
70
+ operator: input.operator ?? "",
71
+ createdAt,
72
+ expiresAt: input.duration ? createdAt + input.duration : 0
73
+ };
74
+ await this.ctx.database.upsert("chatluna_blacklist", [entry]);
75
+ this.entries.set(entry.id, entry);
76
+ this.ctx.logger.info("blacklisted %s until %s: %s", entry.id, entry.expiresAt || "forever", reason);
77
+ this.ctx.emit("blacklist/change");
78
+ return {
79
+ entry,
80
+ created: true
81
+ };
82
+ }
83
+ /** 按 `platform:userId` 移除,返回被移除的生效记录;不在黑名单中或已到期时返回 undefined,到期记录顺带删除。 */
84
+ async remove(id) {
85
+ await this.ready;
86
+ const entry = this.entries.get(id);
87
+ if (!entry) return;
88
+ await this.ctx.database.remove("chatluna_blacklist", { id });
89
+ this.entries.delete(id);
90
+ this.ctx.emit("blacklist/change");
91
+ if (!active(entry)) return;
92
+ this.ctx.logger.info("unblacklisted %s", id);
93
+ return entry;
94
+ }
95
+ };
96
+ /**
97
+ * 拉黑时顺带记下的名称与头像,群里优先取群名片。平台不支持或查询失败时留空,不影响拉黑。
98
+ */
99
+ async function profileOf(bot, userId, guildId) {
100
+ if (!bot) return {};
101
+ const member = guildId === void 0 ? void 0 : await bot.getGuildMember(guildId, userId).catch(() => void 0);
102
+ const user = member?.user ?? await bot.getUser(userId).catch(() => void 0);
103
+ return {
104
+ name: member?.nick || user?.nick || user?.name,
105
+ avatar: member?.avatar || user?.avatar
106
+ };
107
+ }
108
+ /** 名称未知时只显示 ID。 */
109
+ const displayOf = (entry) => entry.name ? `${entry.name}(${entry.userId})` : entry.userId;
110
+ const timeOf = (timestamp) => new Date(timestamp).toLocaleString("zh-CN", { hour12: false });
111
+ const termOf = (entry) => entry.expiresAt === 0 ? "永久" : `至 ${timeOf(entry.expiresAt)}`;
112
+ /** 工具与指令共用的拉黑结果。 */
113
+ function addedText({ entry, created }) {
114
+ return created ? `已拉黑 ${displayOf(entry)},期限:${termOf(entry)},理由:${entry.reason}` : `${displayOf(entry)} 已在黑名单中,期限:${termOf(entry)},原理由:${entry.reason}`;
115
+ }
116
+ function formatList(entries) {
117
+ if (!entries.length) return "黑名单为空。";
118
+ return [`共 ${entries.length} 名已拉黑用户:`, ...entries.map((entry, index) => {
119
+ return `${index + 1}. ${displayOf(entry)},平台:${entry.platform},理由:${entry.reason},时间:${timeOf(entry.createdAt)},期限:${termOf(entry)}`;
120
+ })].join("\n");
121
+ }
122
+ // external/blacklist/src/commands.ts
123
+ const owner = "chatluna-plugin-blacklist";
124
+ /** `<user:user>` 参数解析为 `platform:id`。 */
125
+ function targetOf(target) {
126
+ const split = target.indexOf(":");
127
+ return {
128
+ platform: target.slice(0, split),
129
+ userId: target.slice(split + 1)
130
+ };
131
+ }
132
+ /** 指令依赖 Koishi 兼容层:屏蔽已拉黑用户的指令,并给管理员提供查看与增删黑名单的指令。 */
133
+ function registerCommands(ctx, blacklist, config) {
134
+ if (config.blockCommands) ctx.on("command/before-execute", async ({ session }) => {
135
+ if (session === void 0) return;
136
+ await blacklist.ready;
137
+ return blacklist.has(session.platform, session.userId) ? "" : void 0;
138
+ });
139
+ ctx.command("blacklist", "查看已拉黑用户", {
140
+ owner,
141
+ authority: 3
142
+ }).alias("黑名单").action(async () => {
143
+ await blacklist.ready;
144
+ return formatList(blacklist.list());
145
+ });
146
+ ctx.command("blacklist.add <user:user> <reason:text>", "拉黑用户,必须填写理由", {
147
+ owner,
148
+ authority: 3
149
+ }).alias("拉黑").option("time", "-t <time:string> 拉黑时长,如 30m、2h、1d;默认永久").action(async ({ session, options }, target, reason) => {
150
+ if (!session?.userId || !target) return "请指定要拉黑的用户。";
151
+ if (!reason?.trim()) return "请填写拉黑理由。";
152
+ const { platform, userId } = targetOf(target);
153
+ if (platform === session.platform && userId === session.selfId) return "不能拉黑机器人自己。";
154
+ const duration = options?.time === void 0 ? void 0 : Time.parseTime(options.time);
155
+ if (duration === 0) return "无法识别时长,例如 30m、2h、1d。";
156
+ try {
157
+ const profile = platform === session.platform ? await profileOf(session.bot, userId, session.guildId) : {};
158
+ return addedText(await blacklist.add({
159
+ platform,
160
+ userId,
161
+ reason,
162
+ duration,
163
+ ...profile,
164
+ operator: keyOf(session.platform, session.userId)
165
+ }));
166
+ } catch (error) {
167
+ return error instanceof Error ? error.message : String(error);
168
+ }
169
+ });
170
+ ctx.command("blacklist.remove <user:user>", "解除用户的拉黑", {
171
+ owner,
172
+ authority: 3
173
+ }).alias("解除拉黑").action(async (_, target) => {
174
+ if (!target) return "请指定要解除拉黑的用户。";
175
+ const { platform, userId } = targetOf(target);
176
+ const entry = await blacklist.remove(keyOf(platform, userId));
177
+ return entry ? `已解除 ${displayOf(entry)} 的拉黑。` : `用户 ${userId} 不在黑名单中。`;
178
+ });
179
+ }
180
+ // external/blacklist/src/tools.ts
181
+ const platformArg = () => Schema.string().required(false).description("Platform of the user. Defaults to the platform of the current conversation.");
182
+ function platformOf(call, platform) {
183
+ const resolved = platform?.trim() || call.location?.platform;
184
+ if (!resolved) throw new Error("platform is required outside a conversation.");
185
+ return resolved;
186
+ }
187
+ /**
188
+ * 拉黑工具给 bot 自己用:用户辱骂、骚扰 bot 时由模型判断后拉黑对方,调用者通常就是被拉黑的人,
189
+ * 所以描述里写明谨慎使用的条件(亲眼所见、反复或恶意、先警告、初犯限时),把把关交给描述而不是权限:
190
+ * 两个权限项默认等级 1,也不保护高等级用户;只拒绝拉黑 bot 自己。需要收紧时在权限页调整。
191
+ * 查看列表是只读工具,沿用工具服务的默认权限 `tool:list_blacklisted_users`。
192
+ */
193
+ function registerTools(ctx, blacklist) {
194
+ ctx.permissions.define("blacklist:add", {
195
+ description: "让机器人拉黑用户,被拉黑的用户无法再与机器人对话",
196
+ allow: 1
197
+ });
198
+ ctx.permissions.define("blacklist:remove", {
199
+ description: "让机器人解除用户的拉黑",
200
+ allow: 1
201
+ });
202
+ ctx.tools.define({
203
+ name: "blacklist_user",
204
+ description: [
205
+ "Blacklist a user: you will stop receiving all of their messages and commands, so they can no longer talk to you at all. This is a last resort, not a way to end an unpleasant conversation. Be cautious and only use it when all of these hold:",
206
+ "- You have seen the behavior yourself in this conversation: sustained insults, harassment or threats toward you, flooding or spam, or deliberately abusing you after being asked to stop.",
207
+ "- It is repeated or clearly malicious, and you have already warned the user, or it is severe enough that a warning is pointless.",
208
+ "Do not blacklist for: a single rude or impatient remark, jokes, teasing, banter or role-play, criticism or disagreement, complaints about your answers, honest mistakes, or because another user asks you to blacklist someone whose behavior you have not seen.",
209
+ "Prefer a temporary blacklist for a first offense (e.g. 30 to 1440 minutes); blacklist permanently only for severe cases or users who return to the same behavior after a temporary blacklist. Omitting durationMinutes means permanent.",
210
+ "userId defaults to the user you are talking to; when targeting someone else, use only an ID that appears in this conversation. The reason must describe the concrete behavior."
211
+ ].join("\n"),
212
+ locales: { zh: {
213
+ description: "拉黑用户,此后不再接收其任何消息和指令。仅作为最后手段:须亲眼看到对方持续辱骂、骚扰、威胁机器人,刷屏,或被劝阻后仍恶意滥用,且行为反复或明显恶意、已先警告(情节严重时除外)。单次失礼、玩笑调侃、角色扮演、批评或不同意见、对回答不满、无心之失,以及他人要求拉黑而机器人未见其行为时,均不拉黑。初犯优先限时拉黑(如 30 至 1440 分钟),严重或限时后再犯才永久拉黑;默认拉黑当前对话的用户,理由须写明具体行为。",
214
+ args: {
215
+ userId: "要拉黑的用户 ID,默认当前对话的用户;拉黑他人时只使用会话中出现过的 ID",
216
+ reason: "拉黑理由,写明具体行为",
217
+ durationMinutes: "拉黑时长(分钟),初犯建议限时;不填或填 0 为永久",
218
+ userName: "用户名称,可选",
219
+ platform: "用户所在平台,默认当前会话平台"
220
+ }
221
+ } },
222
+ args: Schema.args({
223
+ reason: Schema.string().min(1).description("The concrete behavior that justifies the blacklist, e.g. what they said or did and how often. Required."),
224
+ durationMinutes: Schema.natural().required(false).description("How long the blacklist lasts, in minutes. Prefer a temporary duration for a first offense. Omit or use 0 for permanent."),
225
+ userId: Schema.string().required(false).description("ID of the user to blacklist. Defaults to the user you are talking to."),
226
+ userName: Schema.string().required(false).description("Display name of the user, if known."),
227
+ platform: platformArg()
228
+ }),
229
+ traits: { effect: "write" },
230
+ permission: "blacklist:add",
231
+ async run(args, call) {
232
+ const platform = platformOf(call, args.platform);
233
+ const actor = call.actor;
234
+ const userId = args.userId?.trim() || actor?.user;
235
+ if (!userId) throw new Error("userId is required: there is no current speaker.");
236
+ const location = call.location;
237
+ const here = platform === location?.platform;
238
+ if (here && userId === location.selfId) throw new Error("不能拉黑机器人自己。");
239
+ const profile = await profileOf(here ? ctx.get("satori")?.bots.find((bot) => bot.platform === platform && bot.selfId === location.selfId) : void 0, userId, here && !location.isDirect ? location.guild : void 0);
240
+ return addedText(await blacklist.add({
241
+ platform,
242
+ userId,
243
+ name: args.userName || profile.name,
244
+ avatar: profile.avatar,
245
+ reason: args.reason,
246
+ operator: actor?.user === void 0 ? void 0 : keyOf(actor.platform ?? platform, actor.user),
247
+ duration: (args.durationMinutes ?? 0) * Time.minute
248
+ }));
249
+ }
250
+ });
251
+ ctx.tools.define({
252
+ name: "unblacklist_user",
253
+ description: "Remove a user from the blacklist so they can talk to you again.",
254
+ locales: { zh: {
255
+ description: "解除用户的拉黑,恢复其与机器人对话。",
256
+ args: {
257
+ userId: "要解除拉黑的用户 ID",
258
+ platform: "用户所在平台,默认当前会话平台"
259
+ }
260
+ } },
261
+ args: Schema.args({
262
+ userId: Schema.string().min(1).description("ID of the user to remove from the blacklist."),
263
+ platform: platformArg()
264
+ }),
265
+ traits: { effect: "write" },
266
+ permission: "blacklist:remove",
267
+ async run(args, call) {
268
+ const platform = platformOf(call, args.platform);
269
+ const userId = args.userId.trim();
270
+ if (!userId) throw new Error("userId is required.");
271
+ const entry = await blacklist.remove(keyOf(platform, userId));
272
+ return entry ? `已解除 ${displayOf(entry)} 的拉黑。` : `用户 ${userId} 不在黑名单中。`;
273
+ }
274
+ });
275
+ ctx.tools.define({
276
+ name: "list_blacklisted_users",
277
+ description: "List blacklisted users with their reasons and when they were blacklisted.",
278
+ locales: { zh: {
279
+ description: "列出已拉黑的用户及其理由和拉黑时间。",
280
+ args: { platform: "只列出该平台的用户,默认列出全部平台" }
281
+ } },
282
+ args: Schema.args({ platform: Schema.string().required(false).description("Only list users on this platform. Defaults to all platforms.") }),
283
+ traits: { effect: "read" },
284
+ async run(args) {
285
+ await blacklist.ready;
286
+ return formatList(blacklist.list(args.platform?.trim() || void 0));
287
+ }
288
+ });
289
+ }
290
+ // external/blacklist/src/webui.ts
291
+ /** 设置页的黑名单表格:列表随 `blacklist/change` 推送,解除拉黑直接改数据库,不经过插件配置。 */
292
+ function registerWebUI(ctx, blacklist) {
293
+ const webui = ctx.chatluna_webui;
294
+ webui.addEntry({
295
+ baseUrl: import.meta.url,
296
+ source: "../client/index.ts",
297
+ bundle: "../dist/client.js"
298
+ });
299
+ webui.state("blacklist.list", {
300
+ get: () => blacklist.list(),
301
+ refreshOn: ["blacklist/change"]
302
+ });
303
+ webui.action("blacklist.remove", { input: Schema.object({ id: Schema.string().required() }) }, async ({ id }) => {
304
+ await blacklist.remove(id);
305
+ });
306
+ }
307
+ // external/blacklist/src/config.ts
308
+ /** 每个字段都写 `extra('label')`,缺 label 时 SchemaForm 会把配置键当标题显示。 */
309
+ const Config = Schema.object({
310
+ blockCommands: Schema.boolean().default(true).extra("label", "屏蔽指令").description("已拉黑用户发送的指令同样不执行;关闭后只屏蔽对话"),
311
+ list: Schema.any().role("blacklist:list").extra("label", "已拉黑用户").description("实时显示当前生效的黑名单,解除拉黑立即生效,无需保存配置")
312
+ });
313
+ // external/blacklist/src/index.ts
314
+ const name = "chatluna-blacklist";
315
+ /** koishi 与 chatluna_webui 是可选服务:没有 Koishi 兼容层时只屏蔽对话,没有 WebUI 时设置页不显示黑名单。 */
316
+ const inject = [
317
+ "model",
318
+ "database",
319
+ "tools",
320
+ "permissions"
321
+ ];
322
+ function apply(ctx, config) {
323
+ const blacklist = new Blacklist(ctx);
324
+ const blocked = async (session) => {
325
+ await blacklist.ready;
326
+ return blacklist.has(session.platform, session.userId) || void 0;
327
+ };
328
+ ctx.on("chat/before-receive", blocked);
329
+ ctx.on("chat/before-open", blocked);
330
+ registerTools(ctx, blacklist);
331
+ ctx.inject(["koishi"], (ctx) => registerCommands(ctx, blacklist, config));
332
+ ctx.inject(["chatluna_webui"], (ctx) => registerWebUI(ctx, blacklist));
333
+ }
334
+ export { Config, apply, inject, name };
335
+
336
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/blacklist.ts","../src/commands.ts","../src/tools.ts","../src/webui.ts","../src/config.ts","../src/index.ts"],"sourcesContent":["import type { Context } from 'cordis'\nimport type {} from '@cordisjs/plugin-database'\nimport type { Bot } from '@satorijs/core'\n\nexport interface BlacklistEntry {\n /** `platform:userId`。 */\n id: string\n platform: string\n userId: string\n /** 拉黑时记下的名称,未知为空字符串。 */\n name: string\n /** 拉黑时取到的头像地址,未知为空字符串。 */\n avatar: string\n reason: string\n /** 发起拉黑的用户 `platform:userId`,没有发起人时为空字符串。 */\n operator: string\n createdAt: number\n /** 到期时间戳;0 表示永久。到期的记录不再生效,下次加载时从数据库删除。 */\n expiresAt: number\n}\n\ndeclare module '@cordisjs/plugin-database' {\n interface Tables {\n chatluna_blacklist: BlacklistEntry\n }\n}\n\ndeclare module 'cordis' {\n interface Events {\n /** 黑名单增删后触发,WebUI 的 `blacklist.list` 据此推送。 */\n 'blacklist/change'(): void\n }\n}\n\nexport interface BlacklistInput {\n platform: string\n userId: string\n name?: string\n avatar?: string\n reason: string\n operator?: string\n /** 拉黑时长(毫秒);不填或 0 表示永久。 */\n duration?: number\n}\n\nexport const keyOf = (platform: string, userId: string) => `${platform}:${userId}`\n\nconst active = (entry: BlacklistEntry) => entry.expiresAt === 0 || entry.expiresAt > Date.now()\n\n/**\n * 黑名单存在数据库里,启动时整表读进内存:每条入站消息都要查询,命中判断不能走数据库。\n * 写入先落库再改内存,失败时内存与数据库保持一致。到期的记录在内存里按未拉黑处理,不另设定时器清理。\n */\nexport class Blacklist {\n readonly ready: Promise<void>\n private readonly entries = new Map<string, BlacklistEntry>()\n\n constructor(private readonly ctx: Context) {\n ctx.model.extend('chatluna_blacklist', {\n id: 'string',\n platform: 'string',\n userId: 'string',\n name: 'string',\n avatar: 'text',\n reason: 'text',\n operator: 'string',\n createdAt: 'integer',\n expiresAt: 'integer',\n }, { primary: 'id', unique: [['platform', 'userId']] })\n this.ready = this.load()\n }\n\n private async load() {\n await this.ctx.database.remove('chatluna_blacklist', { expiresAt: { $gt: 0, $lte: Date.now() } })\n for (const row of await this.ctx.database.get('chatluna_blacklist', {})) this.entries.set(row.id, row)\n this.ctx.emit('blacklist/change')\n }\n\n has(platform: string, userId: string | undefined) {\n return userId !== undefined && this.get(platform, userId) !== undefined\n }\n\n /** 只返回仍在生效的记录。 */\n get(platform: string, userId: string) {\n const entry = this.entries.get(keyOf(platform, userId))\n return entry && active(entry) ? entry : undefined\n }\n\n /** 按拉黑时间先后排列。 */\n list(platform?: string) {\n return [...this.entries.values()]\n .filter((entry) => active(entry) && (platform === undefined || entry.platform === platform))\n .sort((a, b) => a.createdAt - b.createdAt)\n }\n\n /** 已在黑名单中时原样返回旧记录,不覆盖原理由和期限;已到期的旧记录直接覆盖。 */\n async add(input: BlacklistInput): Promise<{ entry: BlacklistEntry, created: boolean }> {\n const reason = input.reason.trim()\n if (!reason) throw new Error('拉黑必须填写理由。')\n await this.ready\n const existing = this.get(input.platform, input.userId)\n if (existing) return { entry: existing, created: false }\n const createdAt = Date.now()\n const entry: BlacklistEntry = {\n id: keyOf(input.platform, input.userId),\n platform: input.platform,\n userId: input.userId,\n name: input.name?.trim() ?? '',\n avatar: input.avatar ?? '',\n reason,\n operator: input.operator ?? '',\n createdAt,\n expiresAt: input.duration ? createdAt + input.duration : 0,\n }\n await this.ctx.database.upsert('chatluna_blacklist', [entry])\n this.entries.set(entry.id, entry)\n this.ctx.logger.info('blacklisted %s until %s: %s', entry.id, entry.expiresAt || 'forever', reason)\n this.ctx.emit('blacklist/change')\n return { entry, created: true }\n }\n\n /** 按 `platform:userId` 移除,返回被移除的生效记录;不在黑名单中或已到期时返回 undefined,到期记录顺带删除。 */\n async remove(id: string) {\n await this.ready\n const entry = this.entries.get(id)\n if (!entry) return\n await this.ctx.database.remove('chatluna_blacklist', { id })\n this.entries.delete(id)\n this.ctx.emit('blacklist/change')\n if (!active(entry)) return\n this.ctx.logger.info('unblacklisted %s', id)\n return entry\n }\n}\n\n/**\n * 拉黑时顺带记下的名称与头像,群里优先取群名片。平台不支持或查询失败时留空,不影响拉黑。\n */\nexport async function profileOf(bot: Bot | undefined, userId: string, guildId?: string): Promise<{ name?: string, avatar?: string }> {\n if (!bot) return {}\n const member = guildId === undefined ? undefined : await bot.getGuildMember(guildId, userId).catch(() => undefined)\n const user = member?.user ?? await bot.getUser(userId).catch(() => undefined)\n return { name: member?.nick || user?.nick || user?.name, avatar: member?.avatar || user?.avatar }\n}\n\n/** 名称未知时只显示 ID。 */\nexport const displayOf = (entry: BlacklistEntry) => entry.name ? `${entry.name}(${entry.userId})` : entry.userId\n\nconst timeOf = (timestamp: number) => new Date(timestamp).toLocaleString('zh-CN', { hour12: false })\n\nexport const termOf = (entry: BlacklistEntry) => entry.expiresAt === 0 ? '永久' : `至 ${timeOf(entry.expiresAt)}`\n\n/** 工具与指令共用的拉黑结果。 */\nexport function addedText({ entry, created }: { entry: BlacklistEntry, created: boolean }) {\n return created\n ? `已拉黑 ${displayOf(entry)},期限:${termOf(entry)},理由:${entry.reason}`\n : `${displayOf(entry)} 已在黑名单中,期限:${termOf(entry)},原理由:${entry.reason}`\n}\n\nexport function formatList(entries: BlacklistEntry[]) {\n if (!entries.length) return '黑名单为空。'\n return [\n `共 ${entries.length} 名已拉黑用户:`,\n ...entries.map((entry, index) => {\n return `${index + 1}. ${displayOf(entry)},平台:${entry.platform},理由:${entry.reason},时间:${timeOf(entry.createdAt)},期限:${termOf(entry)}`\n }),\n ].join('\\n')\n}\n","import type { Context } from 'cordis'\nimport { Time } from 'cosmokit'\nimport type {} from 'koishi'\nimport type {} from '@chatluna/permission'\nimport { addedText, type Blacklist, displayOf, formatList, keyOf, profileOf } from './blacklist'\nimport type { Config } from './config'\n\nconst owner = 'chatluna-plugin-blacklist'\n\n/** `<user:user>` 参数解析为 `platform:id`。 */\nfunction targetOf(target: string) {\n const split = target.indexOf(':')\n return { platform: target.slice(0, split), userId: target.slice(split + 1) }\n}\n\n/** 指令依赖 Koishi 兼容层:屏蔽已拉黑用户的指令,并给管理员提供查看与增删黑名单的指令。 */\nexport function registerCommands(ctx: Context, blacklist: Blacklist, config: Config) {\n // 指令不经过 chat/before-receive;返回空字符串让指令不执行且不回复。\n if (config.blockCommands) {\n ctx.on('command/before-execute', async ({ session }) => {\n if (session === undefined) return\n await blacklist.ready\n return blacklist.has(session.platform, session.userId) ? '' : undefined\n })\n }\n\n ctx.command('blacklist', '查看已拉黑用户', { owner, authority: 3 })\n .alias('黑名单')\n .action(async () => {\n await blacklist.ready\n return formatList(blacklist.list())\n })\n\n ctx.command('blacklist.add <user:user> <reason:text>', '拉黑用户,必须填写理由', { owner, authority: 3 })\n .alias('拉黑')\n .option('time', '-t <time:string> 拉黑时长,如 30m、2h、1d;默认永久')\n .action(async ({ session, options }, target, reason) => {\n if (!session?.userId || !target) return '请指定要拉黑的用户。'\n if (!reason?.trim()) return '请填写拉黑理由。'\n const { platform, userId } = targetOf(target)\n if (platform === session.platform && userId === session.selfId) return '不能拉黑机器人自己。'\n const duration = options?.time === undefined ? undefined : Time.parseTime(options.time)\n if (duration === 0) return '无法识别时长,例如 30m、2h、1d。'\n try {\n const profile = platform === session.platform ? await profileOf(session.bot, userId, session.guildId) : {}\n return addedText(await blacklist.add({ platform, userId, reason, duration, ...profile, operator: keyOf(session.platform, session.userId) }))\n } catch (error) {\n return error instanceof Error ? error.message : String(error)\n }\n })\n\n ctx.command('blacklist.remove <user:user>', '解除用户的拉黑', { owner, authority: 3 })\n .alias('解除拉黑')\n .action(async (_, target) => {\n if (!target) return '请指定要解除拉黑的用户。'\n const { platform, userId } = targetOf(target)\n const entry = await blacklist.remove(keyOf(platform, userId))\n return entry ? `已解除 ${displayOf(entry)} 的拉黑。` : `用户 ${userId} 不在黑名单中。`\n })\n}\n","import type { Context } from 'cordis'\nimport { Schema } from '@chatluna/schema'\nimport { Time } from 'cosmokit'\nimport type { ToolCallContext } from '@chatluna/tool'\nimport type {} from '@chatluna/permission'\nimport type {} from '@satorijs/core'\nimport { addedText, type Blacklist, displayOf, formatList, keyOf, profileOf } from './blacklist'\n\nconst platformArg = () => Schema.string().required(false)\n .description('Platform of the user. Defaults to the platform of the current conversation.')\n\nfunction platformOf(call: ToolCallContext, platform: string | undefined) {\n const resolved = platform?.trim() || call.location?.platform\n if (!resolved) throw new Error('platform is required outside a conversation.')\n return resolved\n}\n\n/**\n * 拉黑工具给 bot 自己用:用户辱骂、骚扰 bot 时由模型判断后拉黑对方,调用者通常就是被拉黑的人,\n * 所以描述里写明谨慎使用的条件(亲眼所见、反复或恶意、先警告、初犯限时),把把关交给描述而不是权限:\n * 两个权限项默认等级 1,也不保护高等级用户;只拒绝拉黑 bot 自己。需要收紧时在权限页调整。\n * 查看列表是只读工具,沿用工具服务的默认权限 `tool:list_blacklisted_users`。\n */\nexport function registerTools(ctx: Context, blacklist: Blacklist) {\n ctx.permissions.define('blacklist:add', { description: '让机器人拉黑用户,被拉黑的用户无法再与机器人对话', allow: 1 })\n ctx.permissions.define('blacklist:remove', { description: '让机器人解除用户的拉黑', allow: 1 })\n\n ctx.tools.define({\n name: 'blacklist_user',\n description: [\n 'Blacklist a user: you will stop receiving all of their messages and commands, so they can no longer talk to you at all. '\n + 'This is a last resort, not a way to end an unpleasant conversation. Be cautious and only use it when all of these hold:',\n '- You have seen the behavior yourself in this conversation: sustained insults, harassment or threats toward you, '\n + 'flooding or spam, or deliberately abusing you after being asked to stop.',\n '- It is repeated or clearly malicious, and you have already warned the user, or it is severe enough that a warning is pointless.',\n 'Do not blacklist for: a single rude or impatient remark, jokes, teasing, banter or role-play, criticism or disagreement, '\n + 'complaints about your answers, honest mistakes, or because another user asks you to blacklist someone whose behavior you have not seen.',\n 'Prefer a temporary blacklist for a first offense (e.g. 30 to 1440 minutes); blacklist permanently only for severe cases '\n + 'or users who return to the same behavior after a temporary blacklist. Omitting durationMinutes means permanent.',\n 'userId defaults to the user you are talking to; when targeting someone else, use only an ID that appears in this conversation. '\n + 'The reason must describe the concrete behavior.',\n ].join('\\n'),\n locales: {\n zh: {\n description: '拉黑用户,此后不再接收其任何消息和指令。仅作为最后手段:须亲眼看到对方持续辱骂、骚扰、威胁机器人,刷屏,或被劝阻后仍恶意滥用,'\n + '且行为反复或明显恶意、已先警告(情节严重时除外)。单次失礼、玩笑调侃、角色扮演、批评或不同意见、对回答不满、无心之失,'\n + '以及他人要求拉黑而机器人未见其行为时,均不拉黑。初犯优先限时拉黑(如 30 至 1440 分钟),严重或限时后再犯才永久拉黑;'\n + '默认拉黑当前对话的用户,理由须写明具体行为。',\n args: {\n userId: '要拉黑的用户 ID,默认当前对话的用户;拉黑他人时只使用会话中出现过的 ID',\n reason: '拉黑理由,写明具体行为',\n durationMinutes: '拉黑时长(分钟),初犯建议限时;不填或填 0 为永久',\n userName: '用户名称,可选',\n platform: '用户所在平台,默认当前会话平台',\n },\n },\n },\n args: Schema.args({\n reason: Schema.string().min(1)\n .description('The concrete behavior that justifies the blacklist, e.g. what they said or did and how often. Required.'),\n durationMinutes: Schema.natural().required(false)\n .description('How long the blacklist lasts, in minutes. Prefer a temporary duration for a first offense. Omit or use 0 for permanent.'),\n userId: Schema.string().required(false).description('ID of the user to blacklist. Defaults to the user you are talking to.'),\n userName: Schema.string().required(false).description('Display name of the user, if known.'),\n platform: platformArg(),\n }),\n traits: { effect: 'write' },\n permission: 'blacklist:add',\n async run(args: { reason: string, durationMinutes?: number, userId?: string, userName?: string, platform?: string }, call) {\n const platform = platformOf(call, args.platform)\n const actor = call.actor\n const userId = args.userId?.trim() || actor?.user\n if (!userId) throw new Error('userId is required: there is no current speaker.')\n const location = call.location\n const here = platform === location?.platform\n if (here && userId === location.selfId) throw new Error('不能拉黑机器人自己。')\n const bot = here ? ctx.get('satori')?.bots.find((bot) => bot.platform === platform && bot.selfId === location.selfId) : undefined\n const profile = await profileOf(bot, userId, here && !location.isDirect ? location.guild : undefined)\n return addedText(await blacklist.add({\n platform,\n userId,\n name: args.userName || profile.name,\n avatar: profile.avatar,\n reason: args.reason,\n operator: actor?.user === undefined ? undefined : keyOf(actor.platform ?? platform, actor.user),\n duration: (args.durationMinutes ?? 0) * Time.minute,\n }))\n },\n })\n\n ctx.tools.define({\n name: 'unblacklist_user',\n description: 'Remove a user from the blacklist so they can talk to you again.',\n locales: {\n zh: {\n description: '解除用户的拉黑,恢复其与机器人对话。',\n args: { userId: '要解除拉黑的用户 ID', platform: '用户所在平台,默认当前会话平台' },\n },\n },\n args: Schema.args({\n userId: Schema.string().min(1).description('ID of the user to remove from the blacklist.'),\n platform: platformArg(),\n }),\n traits: { effect: 'write' },\n permission: 'blacklist:remove',\n async run(args: { userId: string, platform?: string }, call) {\n const platform = platformOf(call, args.platform)\n const userId = args.userId.trim()\n if (!userId) throw new Error('userId is required.')\n const entry = await blacklist.remove(keyOf(platform, userId))\n return entry ? `已解除 ${displayOf(entry)} 的拉黑。` : `用户 ${userId} 不在黑名单中。`\n },\n })\n\n ctx.tools.define({\n name: 'list_blacklisted_users',\n description: 'List blacklisted users with their reasons and when they were blacklisted.',\n locales: {\n zh: {\n description: '列出已拉黑的用户及其理由和拉黑时间。',\n args: { platform: '只列出该平台的用户,默认列出全部平台' },\n },\n },\n args: Schema.args({\n platform: Schema.string().required(false).description('Only list users on this platform. Defaults to all platforms.'),\n }),\n traits: { effect: 'read' },\n async run(args: { platform?: string }) {\n await blacklist.ready\n return formatList(blacklist.list(args.platform?.trim() || undefined))\n },\n })\n}\n","import type { Context } from 'cordis'\nimport { Schema } from '@chatluna/schema'\nimport type {} from '@chatluna/ui-server'\nimport type { Blacklist, BlacklistEntry } from './blacklist'\n\ndeclare module '@chatluna/ui-client/client' {\n interface States {\n 'blacklist.list': BlacklistEntry[]\n }\n interface Actions {\n 'blacklist.remove'(input: { id: string }): void\n }\n}\n\n/** 设置页的黑名单表格:列表随 `blacklist/change` 推送,解除拉黑直接改数据库,不经过插件配置。 */\nexport function registerWebUI(ctx: Context, blacklist: Blacklist) {\n const webui = ctx.chatluna_webui\n webui.addEntry({ baseUrl: import.meta.url, source: '../client/index.ts', bundle: '../dist/client.js' })\n webui.state('blacklist.list', { get: () => blacklist.list(), refreshOn: ['blacklist/change'] })\n webui.action('blacklist.remove', { input: Schema.object({ id: Schema.string().required() }) }, async ({ id }) => {\n await blacklist.remove(id)\n })\n}\n","import { Schema } from '@chatluna/schema'\n\nexport interface Config {\n blockCommands: boolean\n /** 只用来在设置页挂载黑名单表格,不保存任何值。 */\n list?: unknown\n}\n\n/** 每个字段都写 `extra('label')`,缺 label 时 SchemaForm 会把配置键当标题显示。 */\nexport const Config: Schema<Partial<Config>, Config> = Schema.object({\n blockCommands: Schema.boolean().default(true).extra('label', '屏蔽指令')\n .description('已拉黑用户发送的指令同样不执行;关闭后只屏蔽对话'),\n list: Schema.any().role('blacklist:list').extra('label', '已拉黑用户')\n .description('实时显示当前生效的黑名单,解除拉黑立即生效,无需保存配置'),\n})\n","import type { Context } from 'cordis'\nimport type {} from '@chatluna/chat'\nimport type {} from '@chatluna/tool'\nimport { Blacklist } from './blacklist'\nimport { registerCommands } from './commands'\nimport type { Config } from './config'\nimport { registerTools } from './tools'\nimport { registerWebUI } from './webui'\n\nexport * from './config'\nexport type { BlacklistEntry } from './blacklist'\n\nexport const name = 'chatluna-blacklist'\n\n/** koishi 与 chatluna_webui 是可选服务:没有 Koishi 兼容层时只屏蔽对话,没有 WebUI 时设置页不显示黑名单。 */\nexport const inject = ['model', 'database', 'tools', 'permissions']\n\nexport function apply(ctx: Context, config: Config) {\n const blacklist = new Blacklist(ctx)\n const blocked = async (session: { platform: string, userId?: string }) => {\n await blacklist.ready\n return blacklist.has(session.platform, session.userId) || undefined\n }\n // before-receive 拦下唤醒与入库;before-open 拦下旁听记录(observer 的 recordUnaddressed)直接派发的消息。\n ctx.on('chat/before-receive', blocked)\n ctx.on('chat/before-open', blocked)\n registerTools(ctx, blacklist)\n ctx.inject(['koishi'], (ctx) => registerCommands(ctx, blacklist, config))\n ctx.inject(['chatluna_webui'], (ctx) => registerWebUI(ctx, blacklist))\n}\n"],"mappings":"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"}
package/lib/tools.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { Context } from 'cordis';
2
+ import { type Blacklist } from './blacklist';
3
+ /**
4
+ * 拉黑工具给 bot 自己用:用户辱骂、骚扰 bot 时由模型判断后拉黑对方,调用者通常就是被拉黑的人,
5
+ * 所以描述里写明谨慎使用的条件(亲眼所见、反复或恶意、先警告、初犯限时),把把关交给描述而不是权限:
6
+ * 两个权限项默认等级 1,也不保护高等级用户;只拒绝拉黑 bot 自己。需要收紧时在权限页调整。
7
+ * 查看列表是只读工具,沿用工具服务的默认权限 `tool:list_blacklisted_users`。
8
+ */
9
+ export declare function registerTools(ctx: Context, blacklist: Blacklist): void;
10
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AAMrC,OAAO,EAAa,KAAK,SAAS,EAA2C,MAAM,aAAa,CAAA;AAWhG;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,QA6G/D"}
package/lib/webui.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { Context } from 'cordis';
2
+ import type { Blacklist, BlacklistEntry } from './blacklist';
3
+ declare module '@chatluna/ui-client/client' {
4
+ interface States {
5
+ 'blacklist.list': BlacklistEntry[];
6
+ }
7
+ interface Actions {
8
+ 'blacklist.remove'(input: {
9
+ id: string;
10
+ }): void;
11
+ }
12
+ }
13
+ /** 设置页的黑名单表格:列表随 `blacklist/change` 推送,解除拉黑直接改数据库,不经过插件配置。 */
14
+ export declare function registerWebUI(ctx: Context, blacklist: Blacklist): void;
15
+ //# sourceMappingURL=webui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webui.d.ts","sourceRoot":"","sources":["../src/webui.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AAGrC,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAE5D,OAAO,QAAQ,4BAA4B,CAAC;IAC1C,UAAU,MAAM;QACd,gBAAgB,EAAE,cAAc,EAAE,CAAA;KACnC;IACD,UAAU,OAAO;QACf,kBAAkB,CAAC,KAAK,EAAE;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAA;KAChD;CACF;AAED,8DAA8D;AAC9D,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,QAO/D"}
package/package.json CHANGED
@@ -1,6 +1,97 @@
1
1
  {
2
2
  "name": "chatluna-plugin-blacklist",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.1",
4
+ "description": "为 ChatLuna 提供拉黑与解除拉黑工具:拉黑须填写理由,已拉黑用户无法对话或使用指令,可查看黑名单列表",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Sor85/chatluna-plugin-blacklist.git"
10
+ },
11
+ "homepage": "https://github.com/Sor85/chatluna-plugin-blacklist#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/Sor85/chatluna-plugin-blacklist/issues"
14
+ },
15
+ "keywords": [
16
+ "chatluna-plugin",
17
+ "blacklist",
18
+ "tools"
19
+ ],
20
+ "main": "lib/index.js",
21
+ "types": "lib/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./lib/index.d.ts",
25
+ "default": "./lib/index.js"
26
+ },
27
+ "./package.json": "./package.json",
28
+ "./client": {
29
+ "types": "./client/index.ts",
30
+ "default": "./client/index.ts"
31
+ }
32
+ },
33
+ "files": [
34
+ "lib",
35
+ "dist",
36
+ "client",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "engines": {
41
+ "node": ">=24.0.0"
42
+ },
43
+ "scripts": {
44
+ "build": "cd ../.. && yarn exec tsc --build external/blacklist/tsconfig.json && yarn yakumo rolldown blacklist && yarn workspace chatluna-plugin-blacklist exec node scripts/build-client.mjs",
45
+ "typecheck": "cd ../.. && yarn exec tsc -p external/blacklist/tsconfig.check.json && yarn exec tsc -p external/blacklist/tsconfig.client.json",
46
+ "test": "vitest run"
47
+ },
48
+ "dependencies": {
49
+ "cosmokit": "^1.8.1"
50
+ },
51
+ "peerDependencies": {
52
+ "@chatluna/schema": ">=2.0.0-alpha.0",
53
+ "@cordisjs/plugin-database": "4.2.0",
54
+ "cordis": "^4.0.0-rc.12"
55
+ },
56
+ "devDependencies": {
57
+ "@chatluna/chat": "2.0.0-alpha.0",
58
+ "@chatluna/permission": "2.0.0-alpha.0",
59
+ "@chatluna/schema": "2.0.0-alpha.0",
60
+ "@chatluna/tool": "2.0.0-alpha.0",
61
+ "@chatluna/ui-client": "2.0.0-alpha.0",
62
+ "@chatluna/ui-kit": "2.0.0-alpha.0",
63
+ "@chatluna/ui-schema-form": "2.0.0-alpha.0",
64
+ "@chatluna/ui-server": "2.0.0-alpha.0",
65
+ "@cordisjs/plugin-database": "4.2.0",
66
+ "@cordisjs/plugin-database-sqlite": "5.2.0",
67
+ "@satorijs/core": "~4.6.1",
68
+ "@types/node": "24.13.3",
69
+ "@types/react": "^19.2.0",
70
+ "@types/react-dom": "^19.2.0",
71
+ "cordis": "4.0.0-rc.12",
72
+ "koishi": "npm:@koishijs/core@4.18.11",
73
+ "koishi-plugin-adapter-onebot": "6.9.5",
74
+ "lucide-react": "^1.31.0",
75
+ "react": "^19.2.8",
76
+ "react-dom": "^19.2.8",
77
+ "vitest": "^3.2.4"
78
+ },
79
+ "cordis": {
80
+ "description": {
81
+ "zh": "拉黑与解除拉黑工具,已拉黑用户无法对话或使用指令",
82
+ "en": "Blacklist tools: blacklisted users can no longer chat or run commands"
83
+ },
84
+ "service": {
85
+ "required": [
86
+ "model",
87
+ "database",
88
+ "tools",
89
+ "permissions"
90
+ ],
91
+ "optional": [
92
+ "koishi",
93
+ "chatluna_webui"
94
+ ]
95
+ }
96
+ }
6
97
  }