diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index eed4fc77..56e6b604 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -37,9 +37,9 @@ jobs:
run: sed -i 's/\/usr\/bin\/env node/node/g' C:/Users/runneradmin/setup-pnpm/node_modules/.pnpm/pnpm@9.4.0/node_modules/pnpm/bin/pnpm.cjs
shell: bash
- - run: npx changelogithub
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # - run: npx changelogithub
+ # env:
+ # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: pnpm install
- run: pnpm run build
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 9996410d..72890f8e 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -31,7 +31,7 @@ export default withMermaid({
nav: [
{ text: '指南', link: '/desktop/' },
{ text: '规则', link: '/rule/' },
- { text: '规则测试', link: '/play/' }
+ { text: '在线工具', link: '/play/' }
],
search: {
provider: 'local'
diff --git a/docs/play/index.md b/docs/play/index.md
index 3566762c..91dbf5d7 100644
--- a/docs/play/index.md
+++ b/docs/play/index.md
@@ -1,4 +1,9 @@
-> 开发中, 目前规则部分不支持 `@filter` `@js`, 暂不支持解析流程测试
+# 取文本测试
+
+> 开发中, 目前在线工具规则部分不支持 `@filter` `@js`, 暂不支持解析流程测试
+---
+
+
diff --git a/docs/rule/expression.md b/docs/rule/expression.md
new file mode 100644
index 00000000..fd56d6e0
--- /dev/null
+++ b/docs/rule/expression.md
@@ -0,0 +1,23 @@
+| 特性 | 说明 | 示例 |
+| ---------- | :---------------------------------------- | :--------------------------------- |
+| `@css` | 使用 css 选择器查找内容 [规则测试][@css] | `@css:.box1 .box2@text` |
+| `@json` | 使用 jsonpath 查找内容 [规则测试][@json] | `@json:$.list[:1].title` |
+| `@xpath` | 使用 xpath 查找内容 [规则测试][@xpath] | `@xpath://*[@class="box3"]/text()` |
+| `@js` | 使用 js 脚本 | `@js:1+1` |
+| `@filter` | 模拟浏览器加载地址后匹配指定链接 | `@filter:(?:m3u8\|mp4)` |
+| `@replace` | 替换匹配到的内容为空 [规则测试][@replace] | `@replace:\d` |
+| `##` | 正则替换 [规则测试][##] | `$.a##2##替换文本` |
+| `{{}}` | 拼接 [规则测试][拼接] | `http://www.aaa.com/{{$.id}}` |
+| `\|\|` | 或, 直到规则匹配成功 [规则测试][或] | `$.a\|\|$.b` |
+| `&&` | | |
+| 嵌套组合 | | `$.info.body@css:.box1 .box2@text` |
+
+规则可以省略开头的,**@css**、**@xpath**、**@json**, 因为解析器会尝试自动识别。
+
+[@css]: /play/?_t=1#eyJpbnB1dFRleHQiOiI8ZGl2IGNsYXNzPVwiYm94MVwiPjxkaXYgY2xhc3M9XCJib3gyXCI+Y29udGVudDwvZGl2PjwvZGl2PiIsInJ1bGUiOiIuYm94MSAuYm94MkB0ZXh0IiwiaXNMaXN0IjpmYWxzZX0=
+[@json]: /play/?_t=2#eyJpbnB1dFRleHQiOiJ7XCJhXCI6IDF9IiwicnVsZSI6IiQuYSIsImlzTGlzdCI6ZmFsc2V9
+[@xpath]: /play/?_t=3#eyJpbnB1dFRleHQiOiI8ZGl2IGNsYXNzPVwiYm94MVwiPjxkaXYgY2xhc3M9XCJib3gyXCI+Y29udGVudDI8L2Rpdj48ZGl2IGNsYXNzPVwiYm94M1wiPmNvbnRlbnQzPC9kaXY+PC9kaXY+IiwicnVsZSI6Ii8vKltAY2xhc3M9XCJib3gzXCJdL3RleHQoKSIsImlzTGlzdCI6ZmFsc2V9
+[@replace]: /play/?_t=6#eyJpbnB1dFRleHQiOiJhMTJiM2MiLCJydWxlIjoiQHJlcGxhY2U6XFxkIiwiaXNMaXN0IjpmYWxzZX0=
+[##]: /play/?_t=7#eyJpbnB1dFRleHQiOiJ7XCJhXCI6IFwiMTIzXCJ9IiwicnVsZSI6IiQuYSMjMiMj5pu/5o2i5paH5pysIiwiaXNMaXN0IjpmYWxzZX0=
+[拼接]: /play/?_t=8#eyJpbnB1dFRleHQiOiJ7XCJhXCI6IDF9IiwicnVsZSI6InF7eyQueHx8JC5hfX13IiwiaXNMaXN0IjpmYWxzZX0=
+[或]: /play/?_t=9#eyJpbnB1dFRleHQiOiJ7XCJhXCI6IFwiMVwifSIsInJ1bGUiOiIkLmJ8fCQuYSIsImlzTGlzdCI6ZmFsc2V9
diff --git a/docs/rule/index.md b/docs/rule/index.md
index ac17283d..3799d1e1 100644
--- a/docs/rule/index.md
+++ b/docs/rule/index.md
@@ -14,53 +14,6 @@ outline: deep
::: code-group
-```json
-{
- "id": "xxx-xxx-xxx-xxx-xxx",
- "author": "",
- "name": "",
- "host": "",
- "icon": "",
- "contentType": 1,
- "sort": 0,
- "userAgent": "",
- "enableDiscover": false,
- "discoverUrl": "",
- "discoverNextUrl": "",
- "discoverList": "",
- "discoverTags": "",
- "discoverName": "",
- "discoverCover": "",
- "discoverChapter": "",
- "discoverDescription": "",
- "discoverResult": "",
- "enableSearch": false,
- "searchUrl": "",
- "searchAuthor": "",
- "chapterCover": "",
- "chapterTime": "",
- "discoverAuthor": "",
- "searchList": "",
- "searchTags": "",
- "searchName": "",
- "searchCover": "",
- "searchChapter": "",
- "searchDescription": "",
- "searchResult": "",
- "enableMultiRoads": false,
- "chapterRoads": "",
- "chapterRoadName": "",
- "chapterUrl": "",
- "chapterNextUrl": "",
- "chapterList": "",
- "chapterName": "",
- "chapterResult": "",
- "contentUrl": "",
- "contentNextUrl": "",
- "contentItems": ""
-}
-```
-
```typescript
export interface Rule {
// ===== 通用字段 =====
@@ -142,12 +95,59 @@ enum ContentType {
}
```
+```json
+{
+ "id": "xxx-xxx-xxx-xxx-xxx",
+ "author": "",
+ "name": "",
+ "host": "",
+ "icon": "",
+ "contentType": 1,
+ "sort": 0,
+ "userAgent": "",
+ "enableDiscover": false,
+ "discoverUrl": "",
+ "discoverNextUrl": "",
+ "discoverList": "",
+ "discoverTags": "",
+ "discoverName": "",
+ "discoverCover": "",
+ "discoverChapter": "",
+ "discoverDescription": "",
+ "discoverResult": "",
+ "enableSearch": false,
+ "searchUrl": "",
+ "searchAuthor": "",
+ "chapterCover": "",
+ "chapterTime": "",
+ "discoverAuthor": "",
+ "searchList": "",
+ "searchTags": "",
+ "searchName": "",
+ "searchCover": "",
+ "searchChapter": "",
+ "searchDescription": "",
+ "searchResult": "",
+ "enableMultiRoads": false,
+ "chapterRoads": "",
+ "chapterRoadName": "",
+ "chapterUrl": "",
+ "chapterNextUrl": "",
+ "chapterList": "",
+ "chapterName": "",
+ "chapterResult": "",
+ "contentUrl": "",
+ "contentNextUrl": "",
+ "contentItems": ""
+}
+```
+
+:::
+
> 格式 `eso://:xxxxx` 是压缩后的规则, 软件也会自动识别, 也可以使用 [在线规则编解码工具](/play/comparess) 还原成json
>
> 并不是每个字段都是必填的, 按需填写既可。
-:::
-
## 规则字段类型
### URL地址规则
@@ -405,39 +405,13 @@ Content-Type: application/json
> `discoverUrl` 虽然规则看起来像 `URL地址规则`, 但是用法截然不同, 所以这里单独说明
-## 规则支持情况
-
-- ✅ 理论支持
-- ⚠️ 支持部分
-- ❌ 暂不支持
+## 规则表达式
-### URL地址规则
+[在线练习](/play/)
-| 特性 | 支持情况 | 示例 |
-| ---- | :------: | ---------------------------------------------------------------------------------------------------------- |
-| URL | ✅ | `https://xxx.com/search?q=$keyword&pageSize=10` |
-| JSON | ✅ | `{"url":"https://xxx.com/search","method":"post","headers":{"token":"111"},"body":{"keyword":"$keyword"}}` |
-| @js | ✅ | `@js:(() => { return {url, method, body, headers}; })();` |
-
-### 规则表达式
-
-| 特性 | 支持情况 | 说明 | 示例 |
-| ---------- | :------: | -------------------------------- | --------------------------------------- |
-| `@css` | ✅ | | `@css:.box1 .box2@text` |
-| `@json` | ✅ | | `@json:$.list[:1].title` |
-| `@xpath` | ✅ | | `@xpath://*[@class="box3"]/text()` |
-| `@js` | ✅ | | |
-| `@filter` | ✅ | 模拟浏览器加载地址后匹配指定链接 | `@filter:(?:m3u8\|mp4)(?:$\|/\|\\?\|&)` |
-| `@replace` | ✅ | | `@replace:.*?url=\|.*?v=` |
-| `##` | ✅ | 正则替换 | `@css:.c2 a@href##\\d+\\.html` |
-| `{{}}` | ✅ | 使用变量 | `http://www.aaa.com/{{$.id}}` |
-| 嵌套组合 | ✅ | | `$.info.body@css:.box1 .box2@text` |
-| `\|\|` | ✅ | | |
-| `&&` | ✅ | | |
-
-规则可以省略开头的,**@css**、**@xpath**、**@json**, 因为解析器会尝试自动识别。
+
-## 规则表达式
+---
### CSS
@@ -474,7 +448,7 @@ Content-Type: application/json
>
> 如果上一个流程`结果规则`拿到的结果是 `456`, 那么在`取列表规则`字段中 `@js:lastResult` 将输出 `456`, 在`URL地址规则`字段中 `@js:result` 将输出 `456`
-内置方法: `CryptoJS`、`fetch`、`xpath`
+内置方法: `CryptoJS`、`fetch`、`xpath`、`cheerio`
#### CryptoJS
diff --git a/packages/core/__test__/analyzer.test.ts b/packages/core/__test__/analyzer.test.ts
index c5593f2b..2a779019 100644
--- a/packages/core/__test__/analyzer.test.ts
+++ b/packages/core/__test__/analyzer.test.ts
@@ -16,6 +16,36 @@ describe('analyzer', () => {
expect(content).toEqual('iPhone');
});
+ it('|| getString', async () => {
+ const body = '{"a": "1"}';
+ const content = await analyzerManager.getString('$.b||$.a', body);
+ expect(content).toEqual('1');
+ });
+
+ it('|| getStringList', async () => {
+ const body = '{"a": "1"}';
+ const content = await analyzerManager.getStringList('$.b||$.a', body);
+ expect(content).toEqual(['1']);
+ });
+
+ it('##', async () => {
+ const body = '{"a": "123"}';
+ const content = await analyzerManager.getString('$.a##2##替换文本', body);
+ expect(content).toEqual('1替换文本3');
+ });
+
+ it('{{}}', async () => {
+ const body = '{"a": 1}';
+ const content = await analyzerManager.getString('q{{$.x||$.a}}w', body);
+ expect(content).toEqual('q1w');
+ });
+
+ it('@replace', async () => {
+ const body = '123';
+ const content = await analyzerManager.getString('@replace:2', body);
+ expect(content).toEqual('13');
+ });
+
it('XPath', async () => {
const body = '
';
const content = await analyzerManager.getString('//*[@class="box3"]/text()', body);
diff --git a/packages/core/src/analyzer/AnalyzerManager.ts b/packages/core/src/analyzer/AnalyzerManager.ts
index 65017c90..ab5316ec 100644
--- a/packages/core/src/analyzer/AnalyzerManager.ts
+++ b/packages/core/src/analyzer/AnalyzerManager.ts
@@ -158,13 +158,54 @@ export class AnalyzerManager {
}
async _getString(r: SingleRule, rule?: string): Promise {
- const _rule = rule || r.rule;
- try {
- const res = await r.analyzer.getString(_rule);
- return Array.isArray(res) ? res.join('').trim() : res;
- } catch (error: any) {
- throw new AnalyzerException(error?.message);
+ if (rule === undefined) {
+ rule = r.rule;
}
+
+ if (r.analyzer instanceof AnalyzerJS) {
+ const temp = await r.analyzer.getString(rule);
+ if (temp === null) {
+ return '';
+ } else if (Array.isArray(temp)) {
+ return temp
+ .map((s) => typeof s !== 'undefined' && String(s).trim())
+ .filter((s) => s)
+ .join(' ');
+ }
+ return String(temp).trim();
+ }
+
+ let result = '';
+
+ if (rule.includes('&&')) {
+ const rs = [];
+ for (const rSimple of rule.split('&&')) {
+ const temp = await this._getString(r, rSimple);
+ if (temp.length > 0) {
+ rs.push(temp);
+ }
+ }
+ return rs.join(' ');
+ } else if (rule.includes('||')) {
+ for (const rSimple of rule.split('||')) {
+ const temp = await this._getString(r, rSimple);
+ if (temp.length > 0) {
+ return temp;
+ }
+ }
+ } else {
+ const temp = await r.analyzer.getString(rule);
+ if (Array.isArray(temp)) {
+ result = temp
+ .map((s) => typeof s !== 'undefined' && String(s).trim())
+ .filter((s) => s)
+ .join(' ');
+ } else if (temp !== null) {
+ result = String(temp).trim();
+ }
+ }
+
+ return result.length === 0 ? '' : this.replaceSmart(r.replace)(result);
}
public async getString(rule: string, body: string): Promise {
@@ -197,13 +238,44 @@ export class AnalyzerManager {
return temp;
}
- async _getStringList(r: SingleRule, rule?: string): Promise {
- const _rule = rule || r.rule;
- try {
- return await r.analyzer.getStringList(rule || r.rule);
- } catch (error: any) {
- throw new AnalyzerException(error?.message);
+ async _getStringList(r: SingleRule, rule?: string): Promise {
+ if (rule === undefined) {
+ rule = r.rule;
+ }
+
+ if (r.analyzer instanceof AnalyzerJS) {
+ return await r.analyzer.getStringList(rule);
+ }
+
+ if (rule.includes('&&')) {
+ const result = [];
+ for (const rSimple of rule.split('&&')) {
+ const temp = await this._getStringList(r, rSimple);
+ if (Array.isArray(temp)) {
+ result.push(...temp.map((s) => s?.trim()).filter((s) => s));
+ } else if (temp && temp.trim()) {
+ result.push(temp.trim());
+ }
+ }
+ return result;
+ } else if (rule.includes('||')) {
+ let result: string[] = [];
+ for (const rSimple of rule.split('||')) {
+ const temp = await this._getStringList(r, rSimple);
+ if (Array.isArray(temp)) {
+ result = temp.map((s) => s?.trim()).filter((s) => s);
+ } else if (temp && temp.trim()) {
+ result = [temp.trim()];
+ }
+ if (result.length > 0) {
+ return result;
+ }
+ }
+ } else {
+ return await r.analyzer.getStringList(rule);
}
+
+ return [];
}
public async getStringList(rule: string, body: string): Promise {