diff --git a/scripts/updateSubs.ts b/scripts/updateSubs.ts index 5a5235b9..898dee90 100644 --- a/scripts/updateSubs.ts +++ b/scripts/updateSubs.ts @@ -1,6 +1,6 @@ import fs from 'node:fs/promises'; import url from 'node:url'; -import type { RawApp } from '../src/types'; +import type { RawApp } from '@gkd-kit/api'; import { tryRun } from '../src/utils'; // 使用命令更新内存订阅 diff --git a/src/categories.ts b/src/categories.ts index a483d6dc..bf8b7a28 100644 --- a/src/categories.ts +++ b/src/categories.ts @@ -1,4 +1,4 @@ -import type { RawCategory } from './types'; +import type { RawCategory } from '@gkd-kit/api'; const categories: RawCategory[] = [ { diff --git a/src/config.ts b/src/config.ts index feaf45c4..37858889 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,7 +1,7 @@ import categories from './categories'; import globalGroups from './globalGroups'; import apps from './rawApps'; -import type { RawSubscription } from './types'; +import type { RawSubscription } from '@gkd-kit/api'; const subsConfig: RawSubscription = { id: 666, diff --git a/src/file.ts b/src/file.ts index ed1c7041..743e734b 100644 --- a/src/file.ts +++ b/src/file.ts @@ -3,13 +3,13 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import type PkgT from '../package.json'; import { parseSelector } from './selector'; +import type { RawAppAddProp } from './types'; import type { - RawApp, RawAppGroup, RawGlobalGroup, IArray, RawSubscription, -} from './types'; +} from '@gkd-kit/api'; import JSON5 from 'json5'; // 定义一个将 IArray 类型转换为 T[] 类型的函数 @@ -206,7 +206,7 @@ export const checkConfig = (newConfig: RawSubscription) => { // 检查组和规则的重复键 const apps = newConfig.apps || []; - apps.forEach((app) => { + apps.forEach((app: RawAppAddProp) => { const deprecatedKeys = app.deprecatedKeys || []; const keys = new Set(); const oldGroups = oldConfig.apps?.find((a) => a.id == app.id)?.groups || []; @@ -377,7 +377,7 @@ export const checkAndDeleteFiles = async (): Promise => { }; // 导出一个异步函数,用于更新应用的 Markdown 文件 -export const updateAppMd = async (app: RawApp) => { +export const updateAppMd = async (app: RawAppAddProp) => { // 生成应用的 Markdown 文本内容 const appHeadMdText = [ `# ${app.name}`, @@ -509,7 +509,7 @@ const getGlobalDiffLog = ( // 定义一个类型,表示应用的变更日志 type AppDiff = { - app: RawApp; + app: RawAppAddProp; addGroups: RawAppGroup[]; changeGroups: RawAppGroup[]; removeGroups: RawAppGroup[]; diff --git a/src/globalGroups.ts b/src/globalGroups.ts index 9d4d5237..3f64b4e0 100644 --- a/src/globalGroups.ts +++ b/src/globalGroups.ts @@ -1,5 +1,5 @@ import apps from './rawApps'; -import type { RawGlobalGroup } from './types'; +import type { RawGlobalGroup } from '@gkd-kit/api'; import { OPEN_AD_ORDER } from './utils'; const diabledAppIds = [ @@ -13,6 +13,14 @@ const diabledAppIds = [ 'com.android.phone.recorder', // 录音 'com.android.soundrecorder', // 录音机 'com.android.server.telecom', // 来电拒接短信服务 + + // 排除软件包安装程序 + 'com.android.packageinstaller', + 'com.google.android.packageinstaller', // Google Play + 'com.miui.packageinstaller', // 小米系 + 'com.samsung.android.packageinstaller', // 三星系 + 'com.oplus.appdetail', // 一加系 + // 在一些常见的应用中禁用 'com.tencent.mm', // 微信 'li.songe.gkd', // GKD diff --git a/src/rawApps.ts b/src/rawApps.ts index e9ec41f4..2340cb40 100644 --- a/src/rawApps.ts +++ b/src/rawApps.ts @@ -4,15 +4,17 @@ import url from 'node:url'; import picocolors from 'picocolors'; import { pinyin } from 'pinyin-pro'; import { walk } from './file'; -import type { RawApp } from './types'; +import type { RawAppAddProp } from './types'; import { OPEN_AD_ORDER } from './utils'; -const rawApps: RawApp[] = []; +const rawApps: RawAppAddProp[] = []; for await (const tsFp of walk(process.cwd() + '/src/apps')) { if (!tsFp.endsWith('.ts')) { throw new Error('invalid typescript app config file: ' + tsFp); } - const mod: { default: RawApp } = await import(url.pathToFileURL(tsFp).href); + const mod: { default: RawAppAddProp } = await import( + url.pathToFileURL(tsFp).href + ); const appConfig = mod.default; if (path.basename(tsFp, '.ts') != appConfig.id) { throw new Error( diff --git a/src/types.ts b/src/types.ts index 705fadd7..7cd20322 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,626 +1,16 @@ -/** - * 一个或者多个值类型 - * @example - * const n1: IArray = 1; // ✅ - * const n2: IArray = [1]; // ✅ - * const s1: IArray = 'hello'; // ✅ - * const a2: IArray = ['hello']; // ✅ - */ -export type IArray = T | T[]; - -/** - * 此类型表示一个整数 - * - * @example - * 114514 // ✅ - * 2.5 // ❌ - */ -export type Integer = number; - -type RawCommonProps = { - /** - * 单位: 毫秒 - * - * 当前规则的冷却时间, 或者执行 action 最小间隔 - * - * @default 1000 - */ - actionCd?: Integer; - - /** - * 单位: 毫秒 - * - * 延迟执行: 查询到节点->等待一段时间->再次查询到节点则执行对应 action - * - */ - actionDelay?: Integer; - - /** - * - * 如果开启, 此规则下的所有 `末尾属性选择器`的`第一个属性选择表达式`符合下面的结构之一的选择器 将使用快速查找 - * - * - [id='abc'] - * - [vid='abc'] - * - [text='abc'] - * - [text^='abc'] - * - [text*='abc'] - * - [text$='abc'] - * - * 比如 `A > B + C[id='x'][childCount=2]` 符合, 但 `A > B + C[childCount=2][id='x']` 不符合 - * - * 它的底层原理是 跳过手动遍历所有节点 直接调用 [findAccessibilityNodeInfosByViewId](https://developer.android.google.cn/reference/android/view/accessibility/AccessibilityNodeInfo#findAccessibilityNodeInfosByViewId(java.lang.String)) / [findAccessibilityNodeInfosByText](https://developer.android.google.cn/reference/android/view/accessibility/AccessibilityNodeInfo#findAccessibilityNodeInfosByText(java.lang.String)) 得到可匹配节点 - * - * 大多数情况下都能查询到, 在少数某些复杂结构下, 即使目标节点存在, 快速查询也不一定查询到 - * - * 比如 [Image < @View + View >2 [text*='广告']](https://github.com/gkd-kit/subscription/blob/1ae87452d287b558f58f9c4e4448a3190e212ca1/src/apps/com.zidongdianji.ts#L26) 虽然符合快速查询的条件但是使用 `findAccessibilityNodeInfosByText("广告")` 并不能查询到节点 - * - * 它是优点是快速, 因为遍历所有节点是一个耗时行为, 虽然多数情况下这种耗时较低 - * - * 但是在某些软件比如 哔哩哔哩 的开屏广告在这种耗时下延迟可达 1-2s, 这也是导致 [gkd-kit/gkd#60](https://github.com/gkd-kit/gkd/issues/60) 的原因 - * - * --- - * - * v1.4.1 版本后生成的快照时将标识每个节点是否可 quickFind, 网页审查工具属性面板顶部会注明这个标识 - * - * [![image](https://github.com/gkd-kit/subscription/assets/38517192/80cdbad1-fa8d-4415-81a1-cecaa7d69e33)](https://i.gkd.li/i/13316168) - * - */ - quickFind?: boolean; - - /** - * 单位: 毫秒 - * - * 匹配延迟 - * - * 规则准备匹配/或被唤醒时, 等待一段时间, 使此规则参与查询屏幕节点 - * - */ - matchDelay?: Integer; - - /** - * 单位: 毫秒 - * - * 规则匹配时间, 此规则参与查询屏幕节点时, 等待一段时间, 休眠此规则 - * - * 例如某些应用的 开屏广告 的 activityId 容易误触/太广泛, 而开屏广告几乎只在应用切出来时出现, 设置一个有限匹配时间能避免后续的误触 - * - */ - matchTime?: Integer; - - /** - * 最大执行次数 - * - * 规则的 action 被执行的最大次数, 达到最大次数时, 休眠此规则 - * - * 功能类似 matchTime, 适用于只需要执行一次的: 开屏广告/更新弹窗/青少年弹窗 一类规则 - * - * 当规则准备匹配/或被唤醒时, 将重新计算次数 - * - */ - actionMaximum?: Integer; - - /** - * 当规则因为 matchTime/actionMaximum 而休眠时, 如何唤醒此规则 - * - * @default 'activity' - * - * @example - * 'activity' - * // 当 activity 刷新时, 唤醒规则 - * // 刷新 activity 并不代表 activityId 变化 - * // 如 哔哩哔哩视频播放页 底部点击推荐视频 进入另一个 视频播放页, 进入了新 activity 但是 activityId 并没有变化 - * - * @example - * 'app' - * // 重新进入 app 时, 唤醒规则 - */ - resetMatch?: 'activity' | 'app'; - - /** - * 与这个 key 的 rule 共享 cd - * - * 比如开屏广告可能需要多个 rule 去匹配, 当一个 rule 触发时, 其它 rule 的触发是无意义的 - * - * 如果你对这个 key 的 rule 设置 actionCd=3000, 那么当这个 rule 和 本 rule 触发任意一个时, 在 3000毫秒 内两个 rule 都将进入 cd - */ - actionCdKey?: Integer; - - /** - * 与这个 key 的 rule 共享次数 - * - * 比如开屏广告可能需要多个 rule 去匹配, 当一个 rule 触发时, 其它 rule 的触发是无意义的 - * - * 如果你对这个 key 的 rule 设置 actionMaximum=1, 那么当这个 rule 和 本 rule 触发任意一个时, 两个 rule 都将进入休眠 - */ - actionMaximumKey?: Integer; - - /** - * 规则参与匹配的顺序, 数字越小越先匹配 - * - * 如果两个规则 order 相同, 按照 groups 中的数组顺序匹配, app 类型规则顺序优先于 global 类型规则 - * - * 属于不同订阅的规则按照订阅列表中顺序匹配, 长按订阅卡片可以拖动排序 - * - * @default 0 - * - * @version 1.7.0 - */ - order?: Integer; - - /** - * 单位: 毫秒 - * - * 在开始匹配后的一段时间内, 不管界面没有通知变化, 主动使此规则参与屏幕查询 - * - * GKD 借助 [onAccessibilityEvent](https://developer.android.com/reference/android/accessibilityservice/AccessibilityService#onAccessibilityEvent(android.view.accessibility.AccessibilityEvent)) 感知界面变化 - * - * 但是某些基于 flutter/webview 开发的应用/页面在变化时并不会通知系统去触发 onAccessibilityEvent, 但是屏幕上的节点信息确实产生变化 - * - * 唯一的办法是在开始匹配的一定时间内主动查询屏幕节点 - * - * @version 1.7.0 - */ - forcedTime?: Integer; - /** - * 当前 规则/规则组 的快照链接, 增强订阅可维护性 - */ - snapshotUrls?: IArray; - - /** - * 当前 规则/规则组 的规则在手机上的运行示例, 支持 jpg/png/webp/gif - * - * 如果规则是多个规则组合起来的, 可以更好看懂规则到底在干啥, 比如 点击关闭按钮-选择关闭原因-确认关闭 这种广告用 gif 看着更清楚在干啥 - */ - exampleUrls?: IArray; -}; - -type RawRuleProps = RawCommonProps & { - /** - * 当前规则在列表中的唯一标识 - * - * key 没有顺序大小之分, 可以是任意数字 - * - * 设置后不可更改, 否则造成点击记录错乱 - */ - key?: Integer; - - /** - * 规则组名称 - */ - name?: string; - - /** - * 要求当前列表里某个 key 刚刚执行 - * - * 比如点击关闭按钮-选择关闭原因-确认关闭, key 分别是 1,2,3, preKeys 分别是 [],[1],[2] - * - * 那么 选择关闭原因 必须要求 比如点击关闭按钮 刚刚点击过才能执行, 确认关闭 也要求 选择关闭原因 刚刚点击过才执行 - * - * 否则后面的规则不会触发, 也就是要求规则按顺序执行, 这是为了防止规则匹配范围太过广泛而误触 - * - */ - preKeys?: IArray; - - /** - * @example - * `click` - * // 为默认值, 如果目标节点是 clickable 的, 则使用 `clickNode`, 反之使用 `clickCenter` - * // 并且当 `clickNode` 事件没有被应用接收时, 则使用 `clickCenter` - * - * @example - * `clickNode` - * // 向系统发起一个点击无障碍节点事件. 即使节点在屏幕外部/或者被其它节点遮挡,也依然能够正确触发点击目标节点 - * // 但是如果目标节点不是 clickable 的, 目标 APP 通常不响应这个点击事件, 也就是点击无效果 - * // 在极少数情况下, 即使节点是 clickable 的, APP 显示接收但是不响应节点点击事件, 此时需要手动设置 `clickCenter` - * - * @example - * `clickCenter` - * // 计算出此控件的中心的坐标并且如果这个坐标在屏幕内部,那么就向系统发起一个点击屏幕坐标事件 - * // 如果这个坐标不在屏幕内部, 当作未匹配 - * // 另外如果目标节点的位置被其它节点遮挡覆盖, 则会点击触发最上层的节点(可能不是目标节点) - * - * @example - * `back` - * // 向系统发起一个返回事件, 相当于按下返回键 - * - * @example - * `longClick` - * // 如果目标节点是 longClickable 的, 则使用 `longClickNode`, 反之使用 `longClickCenter` - * // 并且当 `longClickNode` 事件没有被应用接收时, 则使用 `longClickCenter` - * - * @example - * `longClickNode` - * // 向系统发起一个长按无障碍节点事件,与 clickNode 类似 - * - * @example - * `longClickCenter` - * // 与 clickCenter 类似, 长按时间为 400 毫秒 - */ - action?: - | 'click' - | 'clickNode' - | 'clickCenter' - | 'back' - | 'longClick' - | 'longClickNode' - | 'longClickCenter'; - - /** - * 在使用 clickCenter/longClickCenter 时的自定义点击位置 - * - * 默认坐标为节点中心 - * - * 如果计算出的坐标不在屏幕内部, 当作未匹配 - * - * @version 1.7.0 - */ - position?: Position; - - /** - * 一个或者多个合法的 GKD 选择器, 如果每个选择器都能匹配上节点, 那么点击最后一个选择器的目标节点 - */ - matches?: IArray; - - /** - * 一个或者多个合法的 GKD 选择器, 如果存在一个选择器匹配上节点, 则停止匹配此规则 - */ - excludeMatches?: IArray; -}; - -type RawGroupProps = RawCommonProps & { - /** - * 当前规则组在列表中的唯一标识 - * - * 也是客户端禁用/启用此规则组的依据 - * - * 设置后不可更改, 否则造成客户端启用/禁用错乱 - * - * key 没有顺序大小之分, 可以是任意数字 - */ - key: Integer; - - /** - * 规则组名称 - */ - name: string; - - /** - * 规则组描述 - */ - desc?: string; - - /** - * 控制规则默认情况下是启用还是禁用, 默认启用 - * - * 仅对于本仓库的规则而言, 除开屏广告外, 其它规则默认禁用 - */ - enable?: boolean; - - /** - * 其它 group 的 key, 允许将目标组的所有 rule 添加到当前组的作用域 - * - * 假设 group1{key=1} 有一个 rule1{key=11}, group2{key=2} 有 rule2{key=22}, rule3{key=23} - * - * 如果 group1 的 scopeKeys=[2] 并且 group2 没有被禁用, 那么 rule1 的 preKeys/actionCdKey/actionMaximumKey 可以是 11/22/23 - * - * 如果存在相同 key 的 rule, 优先使用本组的 rule, 其次按 scopeKeys 的顺序查找其它组的 rule - * - */ - scopeKeys?: IArray; - - // rules: RawRuleProps[]; -}; - -type RawAppRuleProps = { - /** - * 如果 设备界面Id startWith activityIds 的任意一项, 则匹配 - * - * 如果要匹配所有界面: `undefined` (不填写) 或者 `[]` (避免使用上级属性) - */ - activityIds?: IArray; - - /** - * 如果 设备界面Id startWith excludeActivityIds 的任意一项, 则排除匹配 - * - * 优先级高于 activityIds - */ - excludeActivityIds?: IArray; - - /** - * 如果应用版本名称包含在此列表中, 则匹配 - * - * @version 1.7.0 - */ - versionNames?: IArray; - - /** - * 如果应用版本名称包含在此列表中, 则排除匹配, 优先级高于 versionNames - * - * @version 1.7.0 - */ - excludeVersionNames?: IArray; - - /** - * 如果应用版本代码包含在此列表中, 则匹配 - * - * @version 1.7.0 - */ - versionCodes?: IArray; - - /** - * 如果应用版本代码包含在此列表中, 则排除匹配, 优先级高于 versionCodes - * - * @version 1.7.0 - */ - excludeVersionCodes?: IArray; -}; - -/** - * 位置类型, 用以描述自定义点击位置 - * - * 使用 left/top/right/bottom 实现定位, 此对象只能有两个属性 - * - * 合法的定位组合为: left-top, left-bottom, right-top, right-bottom - * - * 示例1-点击目标节点的中心 - * ```json5 - * { - * left: 'width/2', - * top: 'height/2', - * } - * ``` - * - * 示例2-点击目标节点的左上顶点 - * ```json5 - * { - * left: 0, - * top: 0, - * } - * ``` - * - * 示例2-点击目标节点的右上区域 - * - https://i.gkd.li/i/14112390 - * - https://i.gkd.li/i/14319672 - * - https://github.com/gkd-kit/gkd/assets/38517192/2cac0614-5eba-48a1-9149-4e564cb79945 - * ```json5 - * { - * right: 'width*0.1352', - * top: 'width*0.0852', - * } - * ``` - * - * 相对坐标计算公式,以width属性为例(选择目标节点某一个不变的属性即可,推荐选择width或height) - * K为系数,需要计算出具体数值或使用字符串类型填写数学表达式 - * U/D/L/R 分别为鼠标悬浮在快照截图上时预览图左下角上下左右的值 - - * right : 'width * K', - * K = R / 目标节点width - - * left : 'width * K', - * K = L / 目标节点width - - * top : 'width * K', - * K = U / 目标节点width - - * bottom : 'width * K', - * K = D / 目标节点width - */ -export type Position = { - /** - * 距离目标节点左边的距离 - * - * 方向: 边 -> 节点中心, 负数表示反方向(也可点击节点外部区域) - * - * 支持两种值类型, 字符串和数字, 数字等价于相同内容的字符串, 如 2.5 等价于 '2.5' - * - * 字符串类型支持来自快照属性面板上的 left/top/right/bottom/width/height 的数学计算表达式 - * - * @example - * 2.5 // ✅ - * '2.5' // ✅ - * '2.5 + 1 - 2 * 3 / 4 ^ 5 % 6' // ✅ - * '(right + left) / 2' // ✅ - */ - left?: string | number; - - /** - * 距离目标节点上边的距离 - */ - top?: string | number; - - /** - * 距离目标节点右边的距离 - */ - right?: string | number; - - /** - * 距离目标节点下边的距离 - */ - bottom?: string | number; -}; - -// <--全局规则相关-- -type RawGlobalApp = RawAppRuleProps & { - id: string; - /** - * 默认值: `true` - * - * true => 在此 APP 启用此规则 - * - * false => 在此 APP 禁用此规则 - */ - enable?: boolean; -}; -type RawGlobalRuleProps = { - /** - * 默认值: `true` - * - * true => 匹配任意 APP - * - * false => 仅匹配 apps 里面的 app - */ - matchAnyApp?: boolean; - - /** - * 默认值: `false` - * - * 是否匹配桌面, 仅全局规则可用 - * - * 如果你切换了桌面, 你需要打开 GKD 的界面触发识别新桌面 - */ - matchLauncher?: boolean; - - /** - * 默认值: `false` - * - * 是否匹配系统应用, 仅全局规则可用 - */ - matchSystemApp?: boolean; - - apps?: RawGlobalApp[]; -}; - -type RawGlobalRule = RawRuleProps & RawGlobalRuleProps; - -export type RawGlobalGroup = RawGroupProps & - RawGlobalRuleProps & { - apps?: RawGlobalApp[]; - rules: RawGlobalRule[]; - }; -// --全局规则相关--> - -// <--APP规则相关-- -export type RawCategory = { - /** - * 当前分类在列表中的唯一标识 - * - * 也是客户端禁用/启用此分类组的依据 - */ - key: Integer; - - /** - * 分类名称 - * - * 同时也是分类的依据, 捕获以 name 开头的所有 APP 规则组, 不捕获全局规则组 - * - * 示例: `开屏广告` 将捕获 `开屏广告-1` `开屏广告-2` `开屏广告-233` 这类 APP 规则组 - */ - name: string; - - /** - * null => 跟随捕获的规则组的 enable 的默认值 - * - * true => 全部启用捕获的规则组 - * - * false => 全部禁用捕获的规则组 - */ - enable?: boolean; -}; - -type RawAppRule = RawRuleProps & RawAppRuleProps; -export type RawAppGroup = RawGroupProps & - RawAppRuleProps & { - /** - * string => { matches: string } - * - * string[] => { matches: string }[] - */ - rules: IArray; - }; - -export type RawApp = { - /** - * 应用包名 - */ - id: string; - - /** - * 如果设备没有安装这个 APP, 则使用这个 name 显示 - */ - name?: string; - - /** - * 此应用的规则组列表 - */ - groups: RawAppGroup[]; +import * as api from '@gkd-kit/api'; +export type RawAppAddProp = api.RawApp & { /** * 某些规则组被移除不使用时, 为了避免 key 在后续被复用, 需要将已经删除的规则组的 key 填入此数组做校验使用 */ deprecatedKeys?: number[]; }; -// --APP规则相关--> -export type RawSubscription = { - /** - * 当前订阅文件的标识, 如果新旧订阅文件id不一致则更新失败\ - * 范围: `[0, Number.MAX_SAFE_INTEGER]`\ - * 建议值: `new Date().getTime()` - * - * GKD默认订阅是 0, 负数 id APP 自己内部使用, APP 不允许用户添加负数 id 的订阅 - * - * 负数订阅由 APP 内部使用, 如本地订阅是 -2, 内存订阅是 -1 - */ - id: Integer; - - /** - * 订阅的名称 - */ - name: string; - - /** - * 订阅的版本号, 用于检测更新 - * - * 只有当新订阅的 version 大于本地旧订阅的 version 才执行更新替换本地 - */ - version: Integer; - - /** - * 作者名称 - */ - author?: string; - - /** - * GKD 会定时或者用户手动刷新请求这个链接, 如果返回的订阅的 version 大于 APP 订阅当前的 version , 则更新 - * - * 如果这个字段不存在, 则使用添加订阅时填写的链接 - */ - updateUrl?: string; - - /** - * 一个自定义 uri 链接, 用户点击[用户反馈]时, 打开此链接 - * - * 可以是一个网页链接, 也可以是一个 APP 内部的 uri 链接 - */ - supportUri?: string; - - /** - * 一个只需要 id 和 version 的 json 文件链接, 检测更新时, 优先检测此链接, 如果 id 相等并且 version 增加, 则再去请求 updateUrl - * - * 目的是防止订阅文件过大而消耗过多的流量 - */ - checkUpdateUrl?: string; - - /** - * 此订阅的全局规则组列表 - */ - globalGroups?: RawGlobalGroup[]; - - /** - * 此订阅的应用规则分类列表 - */ - categories?: RawCategory[]; - - /** - * 此订阅的应用列表 - */ - apps?: RawApp[]; -}; - -export const defineSubsConfig = (config: RawSubscription) => { +export const defineSubsConfig = (config: api.RawSubscription) => { return JSON.stringify(config, undefined, 2); }; -export const defineAppConfig = (config: RawApp) => { +export const defineAppConfig = (config: RawAppAddProp) => { return config; };