diff --git a/scripts/updateSubs.ts b/scripts/updateSubs.ts index 6bc5764e..5a5235b9 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 { AppConfigMudule } from '../src/types'; +import type { RawApp } from '../src/types'; import { tryRun } from '../src/utils'; // 使用命令更新内存订阅 @@ -36,7 +36,7 @@ if (!(await fs.stat(tsFp).catch(() => false))) { } const getAppConfig = async () => { - const mod: AppConfigMudule = await import(url.pathToFileURL(tsFp).href); + const mod: { default: RawApp } = await import(url.pathToFileURL(tsFp).href); return mod.default; }; diff --git a/src/categories.ts b/src/categories.ts new file mode 100644 index 00000000..96f66d87 --- /dev/null +++ b/src/categories.ts @@ -0,0 +1,30 @@ +import type { RawCategory } from './types'; + +const categories: RawCategory[] = [ + { + key: 0, + name: '开屏广告', + }, + { + key: 1, + name: '青少年模式', + }, + { + key: 2, + name: '更新提示', + }, + { + key: 3, + name: '评价提示', + }, + { + key: 4, + name: '通知提示', + }, + { + key: 5, + name: '定位提示', + }, +]; + +export default categories; diff --git a/src/config.ts b/src/config.ts index 759ff319..31575dc8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,13 +2,15 @@ import path from 'node:path'; import url from 'node:url'; import picocolors from 'picocolors'; import { walk } from './file'; -import type { AppConfig, AppConfigMudule, SubscriptionConfig } from './types'; +import type { RawApp, RawSubscription } from './types'; import _ from 'lodash'; import { pinyin } from 'pinyin-pro'; +import globalGroups from './globalGroups'; +import categories from './categories'; -const apps: AppConfig[] = []; +const apps: RawApp[] = []; for await (const tsFp of walk(process.cwd() + '/src/apps')) { - const mod: AppConfigMudule = await import(url.pathToFileURL(tsFp).href); + const mod: { default: RawApp } = await import(url.pathToFileURL(tsFp).href); const appConfig = mod.default; if (path.basename(tsFp, '.ts') != appConfig.id) { throw new Error( @@ -27,16 +29,23 @@ for await (const tsFp of walk(process.cwd() + '/src/apps')) { apps.push(appConfig); } -const subsConfig: SubscriptionConfig = { +const subsConfig: RawSubscription = { id: 0, + version: 0, name: '默认订阅', author: 'lisonge', supportUri: 'https://github.com/gkd-kit/subscription', checkUpdateUrl: 'https://registry.npmmirror.com/@gkd-kit/subscription/latest/files/dist/gkd.version.json', + globalGroups, + categories, apps: _.sortBy(apps, (a) => { - const pyName = pinyin(a.name, { separator: '', toneType: 'none' }); - if (pyName === a.name) return a.name; + const showName = a.name || a.id; + const pyName = pinyin(showName, { + separator: '', + toneType: 'none', + }); + if (pyName === showName) return showName; return '\uFFFF' + pyName; // 让带拼音的全排在后面 }), }; diff --git a/src/file.ts b/src/file.ts index c6d377e1..ced5978a 100644 --- a/src/file.ts +++ b/src/file.ts @@ -3,19 +3,14 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import type PkgT from '../package.json'; import { parseSelector } from './selector'; -import type { - AppConfig, - GroupConfig, - IArray, - SubscriptionConfig, -} from './types'; +import type { RawApp, RawAppGroup, IArray, RawSubscription } from './types'; import JSON5 from 'json5'; const iArrayToArray = (array: IArray = []): T[] => { return Array().concat(array); }; -const sortKeys: (keyof SubscriptionConfig)[] = [ +const sortKeys: (keyof RawSubscription)[] = [ 'id', 'name', 'version', @@ -61,14 +56,14 @@ const pkg: typeof PkgT = JSON.parse( ); const pkgKeys = Object.keys(pkg); -export const writeConfig = async (config: SubscriptionConfig) => { +export const writeConfig = async (config: RawSubscription) => { const gkdFp = process.cwd() + '/dist/gkd.json5'; const versionFp = process.cwd() + '/dist/gkd.version.json'; - const oldConfig: SubscriptionConfig = JSON5.parse( + const oldConfig: RawSubscription = JSON5.parse( await fs.readFile(gkdFp, 'utf-8').catch(() => '{}'), ); - const newConfig: SubscriptionConfig = { + const newConfig: RawSubscription = { ...config, version: oldConfig.version || 0, }; @@ -137,7 +132,7 @@ export const validSnapshotUrl = (s: string) => { return u.pathname.startsWith('/import/'); }; -export const checkConfig = (newConfig: SubscriptionConfig) => { +export const checkConfig = (newConfig: RawSubscription) => { // check duplicated group key newConfig.apps?.forEach((app) => { const deprecatedKeys = app.deprecatedKeys || []; @@ -246,7 +241,7 @@ export const checkConfig = (newConfig: SubscriptionConfig) => { }); }); }); - const newKeys = Object.keys(newConfig) as (keyof SubscriptionConfig)[]; + const newKeys = Object.keys(newConfig) as (keyof RawSubscription)[]; if (newKeys.some((s) => !sortKeys.includes(s))) { console.log({ sortKeys, @@ -256,7 +251,7 @@ export const checkConfig = (newConfig: SubscriptionConfig) => { } }; -export const updateAppMd = async (app: AppConfig) => { +export const updateAppMd = async (app: RawApp) => { const appHeadMdText = [ `# ${app.name}`, `存在 ${app.groups?.length || 0} 规则组 - [${app.id}](/src/apps/${ @@ -322,14 +317,14 @@ export const updateAppMd = async (app: AppConfig) => { }; const getAppDiffLog = ( - oldGroups: GroupConfig[] = [], - newGroups: GroupConfig[] = [], + oldGroups: RawAppGroup[] = [], + newGroups: RawAppGroup[] = [], ) => { const removeGroups = oldGroups.filter( (og) => !newGroups.find((ng) => ng.key == og.key), ); - const addGroups: GroupConfig[] = []; - const changeGroups: GroupConfig[] = []; + const addGroups: RawAppGroup[] = []; + const changeGroups: RawAppGroup[] = []; newGroups.forEach((ng) => { const oldGroup = oldGroups.find((og) => og.key == ng.key); if (oldGroup) { @@ -348,21 +343,21 @@ const getAppDiffLog = ( }; type AppDiff = { - app: AppConfig; - addGroups: GroupConfig[]; - changeGroups: GroupConfig[]; - removeGroups: GroupConfig[]; + app: RawApp; + addGroups: RawAppGroup[]; + changeGroups: RawAppGroup[]; + removeGroups: RawAppGroup[]; }; export const updateReadMeMd = async ( - newConfig: SubscriptionConfig, - oldConfig: SubscriptionConfig, + newConfig: RawSubscription, + oldConfig: RawSubscription, ) => { let changeCount = 0; const appDiffs: AppDiff[] = []; await Promise.all( - newConfig.apps.map(async (app) => { - const oldApp = oldConfig.apps.find((a) => a.id == app.id); + newConfig.apps!.map(async (app) => { + const oldApp = oldConfig.apps!.find((a) => a.id == app.id); if (!_.isEqual(oldApp, app)) { changeCount++; await updateAppMd(app); @@ -423,19 +418,19 @@ export const updateReadMeMd = async ( const appListText = '| 名称 | ID | 规则组 |\n| - | - | - |\n' + - newConfig.apps - .map((app) => { + newConfig + .apps!.map((app) => { const groups = app.groups || []; return `| ${app.name} | [${app.id}](/docs/${app.id}.md) | ${groups.length} |`; }) .join('\n'); const mdTemplate = await fs.readFile(process.cwd() + '/Template.md', 'utf-8'); const readMeMdText = mdTemplate - .replaceAll('--APP_SIZE--', newConfig.apps.length.toString()) + .replaceAll('--APP_SIZE--', newConfig.apps!.length.toString()) .replaceAll( '--GROUP_SIZE--', - newConfig.apps - .reduce((p, c) => p + (c.groups?.length || 0), 0) + newConfig + .apps!.reduce((p, c) => p + (c.groups?.length || 0), 0) .toString(), ) .replaceAll('--VERSION--', (newConfig.version || 0).toString()); diff --git a/src/globalGroups.ts b/src/globalGroups.ts new file mode 100644 index 00000000..80e02c2e --- /dev/null +++ b/src/globalGroups.ts @@ -0,0 +1,42 @@ +import type { RawGlobalGroup } from './types'; + +const globalGroups: RawGlobalGroup[] = [ + { + key: 0, + name: '开屏广告', + actionMaximum: 1, + matchTime: 10000, + resetMatch: 'app', + actionCdKey: 0, + actionMaximumKey: 0, + rules: [ + { + key: 0, + quickFind: true, + matches: '[text*="跳过"][text.length<10]', + action: 'clickCenter', + }, + { + key: 1, + matches: + '[id*="skip"||((text*="跳过"||text*="skip")&&text.length<10)||desc*="skip"||desc*="跳过"][editable=false]', + action: 'clickCenter', + }, + ], + apps: [ + { + id: 'com.android.systemui', + enable: false, + }, + { + id: 'com.miui.aod', + enable: false, + }, + { + id: 'com.miui.home', + enable: false, + }, + ], + }, +]; +export default globalGroups; diff --git a/src/types.ts b/src/types.ts index 282696f7..d87ae084 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,45 +1,13 @@ export type IArray = T | T[]; -/** - * 此类型任意属性如果是 undefined 则使用上级属性, 例如 rule.cd 是 undefined, 则 rule.cd 使用 group.cd - */ -type CommonProps = { - /** - * 如果 设备界面Id startWith activityIds 的任意一项, 则匹配 - * - * 如果要匹配所有界面: `undefined` (不填写) 或者 `[]` (避免使用上级属性) - */ - activityIds?: IArray; - - /** - * @deprecated 从 v1.5.0 已弃用 - * - * 匹配桌面的 activityId, 因为 activityId 在某些机器/应用上获取概率不准确 - * - * 有时当出现 开屏广告 时, activityId 还是桌面的 - * - */ - matchLauncher?: boolean; - - /** - * 如果 设备界面Id startWith excludeActivityIds 的任意一项, 则排除匹配, 这个优先级更高 - */ - excludeActivityIds?: IArray; - +type RawCommonProps = { /** * 单位: 毫秒 * * 当前规则的冷却时间, 或者执行 action 最小间隔 - * */ actionCd?: number; - /** - * @deprecated - * 使用 actionCd - */ - cd?: number; - /** * 单位: 毫秒 * @@ -48,17 +16,12 @@ type CommonProps = { */ actionDelay?: number; - /** - * @deprecated - * 使用 actionDelay - */ - delay?: number; - /** * * 如果开启, 此规则下的所有 `末尾属性选择器`的`第一个属性选择表达式`符合下面的结构之一的选择器 将使用快速查找 * * - [id='abc'] + * - [vid='abc'] * - [text='abc'] * - [text^='abc'] * - [text*='abc'] @@ -134,74 +97,38 @@ type CommonProps = { */ resetMatch?: 'activity' | 'app'; - // 暂未支持 - filter?: { - /** - * 某些应用使用框架生成控件id, 如QQ/微信, 这些id只在相邻几个版本可使用 - */ - appVersionCode?: unknown; - screenHeight?: unknown; - screenWidth?: unknown; - isLandscape?: boolean; - }; -}; - -export type AppConfig = { - id: string; /** - * 如果设备没有安装这个 APP, 则使用这个 name 显示 - */ - name: string; - groups?: GroupConfig[]; - - /** - * 某些规则组被移除不使用时, 为了避免 key 在后续被复用, 需要将已经删除的规则组的 key 填入此数组做校验使用 - */ - deprecatedKeys?: number[]; -} & CommonProps; - -export type AppConfigMudule = { - default: AppConfig; -}; - -export type GroupConfig = { - /** - * 当前规则组在列表中的唯一标识\ - * 也是客户端禁用/启用此规则组的依据 - */ - key: number; - - name: string; - desc?: string; - - /** - * 控制规则默认情况下是启用还是禁用, 默认启用 + * 与这个 key 的 rule 共享 cd * - * 仅对于本仓库的规则而言, 除开屏广告外, 其它规则默认禁用 - */ - enable?: boolean; - - /** - * string => { matches: string } + * 比如开屏广告可能需要多个 rule 去匹配, 当一个 rule 触发时, 其它 rule 的触发是无意义的 * - * string[] => { matches: string }[] + * 如果你对这个 key 的 rule 设置 actionCd=3000, 那么当这个 rule 和 本 rule 触发任意一个时, 在 3000毫秒 内两个 rule 都将进入 cd */ - rules?: IArray; + actionCdKey?: number; /** - * 当前 规则/规则组 的快照链接, 最好填上, 增强订阅可维护性 + * 与这个 key 的 rule 共享次数 + * + * 比如开屏广告可能需要多个 rule 去匹配, 当一个 rule 触发时, 其它 rule 的触发是无意义的 + * + * 如果你对这个 key 的 rule 设置 actionMaximum=0, 那么当这个 rule 和 本 rule 触发任意一个时, 两个 rule 都将进入休眠 + */ + actionMaximumKey?: number; + + /** + * 当前 规则/规则组 的快照链接, 增强订阅可维护性 */ snapshotUrls?: IArray; /** - * 当前 规则/规则组 的规则在手机上的运行示例, gif/mp4 都行 + * 当前 规则/规则组 的规则在手机上的运行示例, 支持 jpg/png/webp/gif * * 如果规则是多个规则组合起来的, 可以更好看懂规则到底在干啥, 比如 点击关闭按钮-选择关闭原因-确认关闭 这种广告用 gif 看着更清楚在干啥 */ exampleUrls?: IArray; -} & CommonProps; +}; -type RuleConfig = { +type RawRuleProps = RawCommonProps & { /** * 当前规则在列表中的唯一标识 */ @@ -209,16 +136,6 @@ type RuleConfig = { name?: string; - /** - * 一个或者多个合法的 GKD 选择器, 如果每个选择器都能匹配上节点, 那么点击最后一个选择器的目标节点 - */ - matches?: IArray; - - /** - * 一个或者多个合法的 GKD 选择器, 如果存在一个选择器匹配上节点, 则停止匹配此规则 - */ - excludeMatches?: IArray; - /** * 要求当前列表里某个 key 刚刚执行 * @@ -228,8 +145,6 @@ type RuleConfig = { * * 否则后面的规则不会触发, 也就是要求规则按顺序执行, 这是为了防止规则匹配范围太过广泛而误触 * - * 多数情况下 不需要设置 - * */ preKeys?: IArray; @@ -276,60 +191,182 @@ type RuleConfig = { | 'longClickCenter'; /** - * 与这个 key 的 rule 共享次数 - * - * 比如开屏广告可能需要多个 rule 去匹配, 当一个 rule 触发时, 其它 rule 的触发是无意义的 - * - * 如果你对这个 key 的 rule 设置 actionMaximum=1, 那么当这个 rule 和 本 rule 触发任意一个时, 两个 rule 都将进入休眠 + * 一个或者多个合法的 GKD 选择器, 如果每个选择器都能匹配上节点, 那么点击最后一个选择器的目标节点 */ - actionMaximumKey?: number; + matches?: IArray; /** - * 与这个 key 的 rule 共享 cd - * - * 比如开屏广告可能需要多个 rule 去匹配, 当一个 rule 触发时, 其它 rule 的触发是无意义的 - * - * 如果你对这个 key 的 rule 设置 actionCd=3000, 那么当这个 rule 和 本 rule 触发任意一个时, 在 3000毫秒 内两个 rule 都将进入 cd + * 一个或者多个合法的 GKD 选择器, 如果存在一个选择器匹配上节点, 则停止匹配此规则 */ - actionCdKey?: number; + excludeMatches?: IArray; +}; - snapshotUrls?: IArray; - exampleUrls?: IArray; -} & CommonProps; +type RawGroupProps = RawCommonProps & { + /** + * 当前规则组在列表中的唯一标识 + * + * 也是客户端禁用/启用此规则组的依据 + */ + key: number; -export type SubscriptionConfig = { + name: string; + desc?: string; + + /** + * 控制规则默认情况下是启用还是禁用, 默认启用 + * + * 仅对于本仓库的规则而言, 除开屏广告外, 其它规则默认禁用 + */ + enable?: boolean; + + // rules: RawRuleProps[]; +}; + +type RawAppRuleProps = { + /** + * 如果 设备界面Id startWith activityIds 的任意一项, 则匹配 + * + * 如果要匹配所有界面: `undefined` (不填写) 或者 `[]` (避免使用上级属性) + */ + activityIds?: IArray; + + /** + * 如果 设备界面Id startWith excludeActivityIds 的任意一项, 则排除匹配 + * + * 优先级高于 activityIds + */ + excludeActivityIds?: IArray; +}; + +// <--全局规则相关-- +type RawGlobalApp = RawAppRuleProps & { + id: string; + /** + * 默认值: `true` + * + * true => 在此 APP 启用此规则 + * + * false => 在此 APP 禁用此规则 + */ + enable?: boolean; +}; +type RawGlobalRuleProps = { + /** + * 默认值: `true` + * + * true => 匹配任意 APP + * + * false => 仅匹配 apps 里面的 app + */ + matchAnyApp?: boolean; + apps?: RawGlobalApp[]; +}; + +type RawGlobalRule = RawRuleProps & RawGlobalRuleProps; + +export type RawGlobalGroup = RawGroupProps & + RawGlobalRuleProps & { + apps: RawGlobalApp[]; + rules: RawGlobalRule[]; + }; +// --全局规则相关--> + +// <--APP规则相关-- +export type RawCategory = { + /** + * 当前分类在列表中的唯一标识 + * + * 也是客户端禁用/启用此分类组的依据 + */ + key: number; + + /** + * 分类名称 + * + * 同时也是分类的依据, 捕获以 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[]; + + /** + * 某些规则组被移除不使用时, 为了避免 key 在后续被复用, 需要将已经删除的规则组的 key 填入此数组做校验使用 + */ + deprecatedKeys?: number[]; +}; +// --APP规则相关--> + +export type RawSubscription = { /** * 当前订阅文件的标识, 如果新旧订阅文件id不一致则更新失败\ * 范围: `[0, Number.MAX_SAFE_INTEGER]`\ * 建议值: `new Date().getTime()` * - * 官方默认订阅是 0, 负数 id APP 自己内部使用, APP 不允许用户添加负数 id 的订阅 + * GKD默认订阅是 0, 负数 id APP 自己内部使用, APP 不允许用户添加负数 id 的订阅 * * 负数订阅由 APP 内部使用, 如本地订阅是 -2, 内存订阅是 -1 */ id: number; /** - * 规则的名称 + * 订阅的名称 */ name: string; /** - * 必填, 此处有 ? 是因为本项目的 version 由 ts 校验自动生成 + * 订阅的版本号, 用于检测更新 * * 只有当新订阅的 version 大于本地旧订阅的 version 才执行更新替换本地 */ - version?: number; + version: number; author?: string; /** - * APP 会定时或者用户手动请求这个链接, 如果返回的订阅的 version 大于 APP 订阅当前的 version , 则更新 + * GKD 会定时或者用户手动刷新请求这个链接, 如果返回的订阅的 version 大于 APP 订阅当前的 version , 则更新 * * 如果这个字段不存在, 则使用添加订阅时填写的链接 */ updateUrl?: string; + /** + * 一个自定义 uri 链接, 用户点击[用户反馈]时, 打开此链接 + * + * 可以是一个网页链接, 也可以是一个 APP 内部的 uri 链接 + */ + supportUri?: string; + /** * 一个只需要 id 和 version 的 json 文件链接, 检测更新时, 优先检测此链接, 如果 id 相等并且 version 增加, 则再去请求 updateUrl * @@ -337,18 +374,15 @@ export type SubscriptionConfig = { */ checkUpdateUrl?: string; - /** - * https url, custom android schema url - */ - supportUri?: string; - - apps: AppConfig[]; + apps?: RawApp[]; + categories?: RawCategory[]; + globalGroups?: RawGlobalGroup[]; }; -export const defineSubsConfig = (config: SubscriptionConfig) => { +export const defineSubsConfig = (config: RawSubscription) => { return JSON.stringify(config, undefined, 2); }; -export const defineAppConfig = (config: AppConfig) => { +export const defineAppConfig = (config: RawApp) => { return config; };