Skip to content

Commit

Permalink
perf(script and core): 优化软件辅助类型提示以及代码
Browse files Browse the repository at this point in the history
  • Loading branch information
enncy committed Dec 20, 2023
1 parent 21f12cf commit 3394ada
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 229 deletions.
2 changes: 1 addition & 1 deletion packages/core/src/core/utils/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function request<T extends 'json' | 'text'>(
method?: 'get' | 'post' | 'head';
responseType?: T;
headers?: Record<string, string>;
data?: Record<string, string>;
data?: Record<string, any>;
}
): Promise<T extends 'json' ? any : string> {
return new Promise((resolve, reject) => {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './string';
export * from './tampermonkey';
export * from './store';
export * from './const';
export * from './playwright';
192 changes: 192 additions & 0 deletions packages/core/src/utils/playwright.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import type { Page } from 'playwright-core';
import { request } from '../core/utils';
import { $ } from './common';

export interface RemotePage {
click: (
selectorOrElement: string | Element,
options?: {
/**
* Defaults to `left`.
*/
button?: 'left' | 'right' | 'middle';

/**
* defaults to 1. See [UIEvent.detail].
*/
clickCount?: number;

/**
* Time to wait between `mousedown` and `mouseup` in milliseconds. Defaults to 0.
*/
delay?: number;

/**
* Whether to bypass the [actionability](https://playwright.dev/docs/actionability) checks. Defaults to `false`.
*/
force?: boolean;

/**
* Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores
* current modifiers back. If not specified, currently pressed modifiers are used.
*/
modifiers?: Array<'Alt' | 'Control' | 'Meta' | 'Shift'>;

/**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You
* can opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as
* navigating to inaccessible pages. Defaults to `false`.
*/
noWaitAfter?: boolean;

/**
* A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of
* the element.
*/
position?: {
x: number;

y: number;
};

/**
* When true, the call requires selector to resolve to a single element. If given selector resolves to more than one
* element, the call throws an exception.
*/
strict?: boolean;

/**
* Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout`
* option in the config, or by using the
* [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout)
* or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods.
*/
timeout?: number;

/**
* When set, this method only performs the [actionability](https://playwright.dev/docs/actionability) checks and skips the action. Defaults
* to `false`. Useful to wait until the element is ready for the action without performing it.
*/
trial?: boolean;
}
) => Promise<void>;
check: Page['check'];
dblclick: Page['dblclick'];
bringToFront: Page['bringToFront'];
dragAndDrop: Page['dragAndDrop'];
fill: Page['fill'];
focus: Page['focus'];
hover: Page['hover'];
screenshot: Page['screenshot'];
selectOption: Page['selectOption'];
setInputFiles: Page['setInputFiles'];
tap: Page['tap'];
press: Page['press'];
reload: Page['reload'];
waitForRequest(...args: Parameters<Page['waitForRequest']>): Promise<string>;
waitForResponse(...args: Parameters<Page['waitForResponse']>): Promise<string>;
waitForSelector(...args: Parameters<Page['waitForSelector']>): Promise<void>;
}

const ListOfActions = [
'click',
'check',
'dblclick',
'bringToFront',
'dragAndDrop',
'fill',
'focus',
'hover',
'screenshot',
'selectOption',
'setInputFiles',
'tap',
'press',
'reload',
'waitForRequest',
'waitForResponse',
'waitForSelector'
];

export class RemotePlaywright {
private static authToken = '';
private static currentPage: RemotePage | undefined = undefined;

static async getCurrentPage(logger: (...args: any[]) => void = console.debug): Promise<RemotePage | undefined> {
if (this.currentPage) {
return this.currentPage;
}
/**
* OCS桌面端后端无法拦截 GM_xmlhttpRequest ,所以这里使用 fetch 请求动作执行,然后后端根据key判断是否允许执行
*/
if (!this.authToken) {
try {
this.authToken = await request('http://localhost:15319/get-actions-key', {
type: 'GM_xmlhttpRequest',
method: 'get',
responseType: 'text'
});
this.currentPage = this.createRemotePage(this.authToken, logger);
return this.currentPage;
} catch (e) {
console.log(e);
return undefined;
}
} else {
this.currentPage = this.createRemotePage(this.authToken, logger);
return this.currentPage;
}
}

private static createRemotePage(authToken: string, logger?: (...args: any[]) => void) {
const page = Object.create({});
for (const property of ListOfActions) {
Reflect.set(page, property, async (...args: any[]) => {
let data;
if (property === 'click' && args[0] instanceof Element) {
args[0].scrollIntoView();
await $.sleep(1000);
const rect = args[0].getBoundingClientRect();
data = {
page: window.location.href,
property: 'mouse.click',
args: [
rect.left + rect.width / 2,
rect.top + rect.height / 2,
{
button: args[1]?.button,
clickCount: args[1]?.clickCount,
delay: args[1]?.delay
}
]
};
}

if (!data) {
data = { page: window.location.href, property: property, args: args };
}

logger?.('remote-playwright', JSON.stringify(data));

try {
const res = await request('http://localhost:15319/ocs-script-actions', {
type: 'fetch',
method: 'post',
responseType: 'text',
headers: {
'auth-token': authToken
},
data: data
});
return res;
} catch (e) {
console.error(e);
return undefined;
}
});
}
console.log(page);

return page;
}
}
67 changes: 49 additions & 18 deletions packages/scripts/src/projects/icourse.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import { $, $creator, $message, OCSWorker, Project, Script, defaultAnswerWrapperHandler } from '@ocsjs/core';
import {
$,
$creator,
$message,
OCSWorker,
Project,
RemotePage,
RemotePlaywright,
Script,
defaultAnswerWrapperHandler
} from '@ocsjs/core';
import { CommonWorkOptions, playMedia } from '../utils';
import { CommonProject } from './common';
import { commonWork, optimizationElementWithImage, removeRedundantWords, simplifyWorkResult } from '../utils/work';
import { $console } from './background';
import { $app_actions } from '../utils/app';
import { $playwright } from '../utils/app';
import { waitForElement, waitForMedia } from '../utils/study';
import { playbackRate, volume, workNotes } from '../utils/configs';

Expand Down Expand Up @@ -131,9 +141,10 @@ export const ICourseProject = Project.create({
main: async (canRun: () => boolean) => {
CommonProject.scripts.render.methods.pin(this);

const remotePage = await RemotePlaywright.getCurrentPage();
// 检查是否为软件环境
if (!(await $app_actions.init())) {
return $app_actions.showError();
if (!remotePage) {
return $playwright.showError();
}

// 移动窗口到边缘
Expand All @@ -156,11 +167,11 @@ export const ICourseProject = Project.create({
$console.log('视频学习完成');
} else if (isJob('u-icon-doc')) {
await waitForElement('.ux-pdf-reader');
await readPPT(this.cfg.readSpeed);
await readPPT(remotePage, this.cfg.readSpeed);
$console.log('PPT完成');
} else if (isJob('u-icon-discuss')) {
await waitForElement('.j-reply-all');
await discussion(this.cfg.discussionStrategy);
await discussion(remotePage, this.cfg.discussionStrategy);
$console.log('讨论完成');
} else if (isJob('u-icon-test')) {
const replay = await waitForElement('.j-replay');
Expand Down Expand Up @@ -209,7 +220,7 @@ export const ICourseProject = Project.create({
for (const item of list) {
const el = typeof item === 'function' ? item() : item;
if (el) {
await $app_actions.mouseClick(el);
await remotePage?.click(el);
}
}
if (list.length === 0) {
Expand Down Expand Up @@ -320,8 +331,10 @@ export const ICourseProject = Project.create({
CommonProject.scripts.render.methods.moveToEdge();

// 检查是否为软件环境
if (!(await $app_actions.init())) {
return $app_actions.showError();
const remotePage = await RemotePlaywright.getCurrentPage();
// 检查是否为软件环境
if (!remotePage) {
return $playwright.showError();
}

// 等待加载题目
Expand All @@ -331,7 +344,7 @@ export const ICourseProject = Project.create({
CommonProject.scripts.render.methods.pin(this);
commonWork(this, {
workerProvider: (opts) => {
const worker = workAndExam(type, opts);
const worker = workAndExam(remotePage, type, opts);
worker.once('close', () => {
clearInterval(interval);
});
Expand Down Expand Up @@ -374,6 +387,7 @@ function waitForQuestion() {
}

function workAndExam(
remotePage: RemotePage,
type: 'chapter-test' | 'work-or-exam',
{ answererWrappers, period, thread, redundanceWordsText, upload, stopSecondWhenFinish }: CommonWorkOptions
) {
Expand Down Expand Up @@ -426,15 +440,15 @@ function workAndExam(
const text = option.querySelector('.f-richEditorText');

const input = option.querySelector('input');
if (input && !input?.checked) {
await $app_actions.mouseClick(text);
if (input && !input?.checked && text) {
await remotePage.click(text);
}
} else if (type === 'completion' && answer.trim()) {
const text = option.querySelector('textarea');

if (text) {
text.value = answer.trim();
await $app_actions.mouseClick(text);
await remotePage.click(text);
}
}
}
Expand Down Expand Up @@ -485,7 +499,12 @@ function workAndExam(
await $.sleep(3000);

if (uploadable) {
await $app_actions.mouseClick(document.querySelector('.j-submit'));
const sumbit = document.querySelector('.j-submit');
if (sumbit) {
await remotePage.click(sumbit);
} else {
$console.warn('没有找到提交按钮,将跳过提交。');
}
}
}
});
Expand Down Expand Up @@ -528,7 +547,7 @@ async function watchMedia(playbackRate: number, volume: number) {
});
}

async function readPPT(readSpeed: number) {
async function readPPT(remotePage: RemotePage, readSpeed: number) {
const reader = document.querySelector('.ux-pdf-reader');
if (reader) {
const total = parseInt(
Expand All @@ -541,13 +560,20 @@ async function readPPT(readSpeed: number) {
);
for (let index = start; index < total + 1; index++) {
const next = document.querySelector<HTMLElement>('.ux-h5pdfreader_container_footer_pages_next');
await $app_actions.mouseClick(next);
if (next) {
await remotePage.click(next);
} else {
$console.error('未找到PPT的下一页按钮!');
}
await $.sleep(readSpeed * 1000);
}
}
}

async function discussion(discussionStrategy: typeof ICourseProject.scripts.study.cfg.discussionStrategy) {
async function discussion(
remotePage: RemotePage,
discussionStrategy: typeof ICourseProject.scripts.study.cfg.discussionStrategy
) {
if (discussionStrategy === 'not-reply') {
return $console.warn('讨论自动回复功能已关闭(上方菜单栏-中国大学MOOC-学习脚本中开启)。');
}
Expand Down Expand Up @@ -595,7 +621,12 @@ async function discussion(discussionStrategy: typeof ICourseProject.scripts.stud
if (p) {
p.innerText = res;
await $.sleep(1000);
await $app_actions.mouseClick(document.querySelector('.ui-richEditor .u-btn-sm'));
const submit = document.querySelector('.ui-richEditor .u-btn-sm');
if (submit) {
await remotePage.click(submit);
} else {
$console.error('提交按钮获取失败!');
}
await $.sleep(2000);
} else {
$console.error('获取评论输入框失败!');
Expand Down
Loading

0 comments on commit 3394ada

Please sign in to comment.