@jackwener/opencli 1.0.1 → 1.0.4
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/.github/workflows/build-extension.yml +80 -0
- package/.github/workflows/ci.yml +6 -6
- package/.github/workflows/docs.yml +52 -0
- package/.github/workflows/e2e-headed.yml +2 -2
- package/.github/workflows/pkg-pr-new.yml +2 -2
- package/.github/workflows/release.yml +2 -5
- package/.github/workflows/security.yml +2 -2
- package/CDP.md +1 -1
- package/CDP.zh-CN.md +1 -1
- package/README.md +42 -34
- package/README.zh-CN.md +42 -34
- package/SKILL.md +3 -5
- package/dist/browser/cdp.d.ts +42 -0
- package/dist/browser/cdp.js +339 -0
- package/dist/browser/daemon-client.d.ts +3 -1
- package/dist/browser/daemon-client.js +4 -0
- package/dist/browser/dom-helpers.d.ts +20 -0
- package/dist/browser/dom-helpers.js +109 -0
- package/dist/browser/index.d.ts +3 -0
- package/dist/browser/index.js +4 -0
- package/dist/browser/mcp.d.ts +1 -0
- package/dist/browser/mcp.js +10 -5
- package/dist/browser/page.d.ts +7 -0
- package/dist/browser/page.js +39 -123
- package/dist/browser/utils.d.ts +10 -0
- package/dist/browser/utils.js +27 -0
- package/dist/browser.test.js +49 -1
- package/dist/build-manifest.js +3 -1
- package/dist/build-manifest.test.js +34 -0
- package/dist/capabilityRouting.d.ts +2 -0
- package/dist/capabilityRouting.js +30 -0
- package/dist/capabilityRouting.test.d.ts +1 -0
- package/dist/capabilityRouting.test.js +42 -0
- package/dist/chaoxing.d.ts +58 -0
- package/dist/chaoxing.js +225 -0
- package/dist/chaoxing.test.d.ts +1 -0
- package/dist/chaoxing.test.js +45 -0
- package/dist/cli-manifest.json +885 -48
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +234 -0
- package/dist/clis/antigravity/serve.d.ts +14 -0
- package/dist/clis/antigravity/serve.js +263 -0
- package/dist/clis/bilibili/download.js +4 -14
- package/dist/clis/boss/chatlist.d.ts +1 -0
- package/dist/clis/boss/chatlist.js +50 -0
- package/dist/clis/boss/chatmsg.d.ts +1 -0
- package/dist/clis/boss/chatmsg.js +73 -0
- package/dist/clis/boss/resume.d.ts +1 -0
- package/dist/clis/boss/resume.js +249 -0
- package/dist/clis/boss/send.d.ts +1 -0
- package/dist/clis/boss/send.js +176 -0
- package/dist/clis/chaoxing/assignments.d.ts +1 -0
- package/dist/clis/chaoxing/assignments.js +74 -0
- package/dist/clis/chaoxing/exams.d.ts +1 -0
- package/dist/clis/chaoxing/exams.js +74 -0
- package/dist/clis/chatgpt/ask.js +15 -14
- package/dist/clis/chatgpt/ax.d.ts +1 -0
- package/dist/clis/chatgpt/ax.js +78 -0
- package/dist/clis/chatgpt/read.js +5 -6
- package/dist/clis/hf/top.d.ts +1 -0
- package/dist/clis/hf/top.js +119 -0
- package/dist/clis/jike/comment.d.ts +1 -0
- package/dist/clis/jike/comment.js +107 -0
- package/dist/clis/jike/create.d.ts +1 -0
- package/dist/clis/jike/create.js +106 -0
- package/dist/clis/jike/feed.d.ts +1 -0
- package/dist/clis/jike/feed.js +67 -0
- package/dist/clis/jike/like.d.ts +1 -0
- package/dist/clis/jike/like.js +61 -0
- package/dist/clis/jike/notifications.d.ts +1 -0
- package/dist/clis/jike/notifications.js +169 -0
- package/dist/clis/jike/post.yaml +58 -0
- package/dist/clis/jike/repost.d.ts +1 -0
- package/dist/clis/jike/repost.js +103 -0
- package/dist/clis/jike/search.d.ts +1 -0
- package/dist/clis/jike/search.js +67 -0
- package/dist/clis/jike/shared.d.ts +19 -0
- package/dist/clis/jike/shared.js +25 -0
- package/dist/clis/jike/topic.yaml +52 -0
- package/dist/clis/jike/user.yaml +51 -0
- package/dist/clis/smzdm/search.js +28 -39
- package/dist/clis/stackoverflow/bounties.yaml +29 -0
- package/dist/clis/stackoverflow/hot.yaml +28 -0
- package/dist/clis/stackoverflow/search.yaml +32 -0
- package/dist/clis/stackoverflow/unanswered.yaml +28 -0
- package/dist/clis/twitter/download.js +6 -16
- package/dist/clis/twitter/post.js +9 -2
- package/dist/clis/twitter/search.js +14 -33
- package/dist/clis/xiaohongshu/download.d.ts +1 -1
- package/dist/clis/xiaohongshu/download.js +4 -4
- package/dist/clis/zhihu/download.js +3 -3
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +16 -0
- package/dist/download/index.d.ts +12 -8
- package/dist/download/index.js +11 -3
- package/dist/download/index.test.d.ts +1 -0
- package/dist/download/index.test.js +14 -0
- package/dist/engine.js +25 -14
- package/dist/explore.d.ts +1 -0
- package/dist/explore.js +48 -103
- package/dist/generate.js +1 -0
- package/dist/interceptor.js +3 -2
- package/dist/main.js +4 -193
- package/dist/output.d.ts +2 -1
- package/dist/output.js +3 -1
- package/dist/pipeline/executor.test.js +1 -0
- package/dist/pipeline/steps/download.js +14 -18
- package/dist/registry.d.ts +4 -3
- package/dist/registry.js +5 -2
- package/dist/runtime.d.ts +4 -1
- package/dist/runtime.js +2 -2
- package/dist/scripts/framework.d.ts +4 -0
- package/dist/scripts/framework.js +21 -0
- package/dist/scripts/interact.d.ts +4 -0
- package/dist/scripts/interact.js +20 -0
- package/dist/scripts/store.d.ts +9 -0
- package/dist/scripts/store.js +44 -0
- package/dist/synthesize.js +1 -1
- package/dist/types.d.ts +12 -0
- package/dist/verify.d.ts +6 -1
- package/dist/verify.js +54 -2
- package/docs/.vitepress/config.mts +193 -0
- package/docs/adapters/browser/apple-podcasts.md +28 -0
- package/docs/adapters/browser/bbc.md +26 -0
- package/docs/adapters/browser/bilibili.md +38 -0
- package/docs/adapters/browser/boss.md +28 -0
- package/docs/adapters/browser/coupang.md +28 -0
- package/docs/adapters/browser/ctrip.md +27 -0
- package/docs/adapters/browser/github.md +26 -0
- package/docs/adapters/browser/hackernews.md +26 -0
- package/docs/adapters/browser/linkedin.md +27 -0
- package/docs/adapters/browser/reddit.md +41 -0
- package/docs/adapters/browser/reuters.md +27 -0
- package/docs/adapters/browser/smzdm.md +27 -0
- package/docs/adapters/browser/twitter.md +47 -0
- package/docs/adapters/browser/v2ex.md +32 -0
- package/docs/adapters/browser/weibo.md +27 -0
- package/docs/adapters/browser/xiaohongshu.md +32 -0
- package/docs/adapters/browser/xiaoyuzhou.md +28 -0
- package/docs/adapters/browser/xueqiu.md +32 -0
- package/docs/adapters/browser/yahoo-finance.md +26 -0
- package/docs/adapters/browser/youtube.md +29 -0
- package/docs/adapters/browser/zhihu.md +30 -0
- package/docs/adapters/desktop/antigravity.md +46 -0
- package/docs/adapters/desktop/chatgpt.md +43 -0
- package/docs/adapters/desktop/chatwise.md +38 -0
- package/docs/adapters/desktop/codex.md +32 -0
- package/docs/adapters/desktop/cursor.md +33 -0
- package/docs/adapters/desktop/discord.md +28 -0
- package/docs/adapters/desktop/feishu.md +20 -0
- package/docs/adapters/desktop/neteasemusic.md +31 -0
- package/docs/adapters/desktop/notion.md +29 -0
- package/docs/adapters/desktop/wechat.md +28 -0
- package/docs/adapters/index.md +49 -0
- package/docs/advanced/cdp.md +103 -0
- package/docs/advanced/download.md +63 -0
- package/docs/advanced/electron.md +125 -0
- package/docs/advanced/remote-chrome.md +72 -0
- package/docs/developer/ai-workflow.md +66 -0
- package/docs/developer/architecture.md +90 -0
- package/docs/developer/contributing.md +136 -0
- package/docs/developer/testing.md +237 -0
- package/docs/developer/ts-adapter.md +87 -0
- package/docs/developer/yaml-adapter.md +108 -0
- package/docs/guide/browser-bridge.md +38 -0
- package/docs/guide/getting-started.md +56 -0
- package/docs/guide/installation.md +37 -0
- package/docs/guide/troubleshooting.md +56 -0
- package/docs/index.md +35 -0
- package/docs/zh/adapters/index.md +5 -0
- package/docs/zh/advanced/cdp.md +3 -0
- package/docs/zh/developer/contributing.md +24 -0
- package/docs/zh/guide/browser-bridge.md +25 -0
- package/docs/zh/guide/getting-started.md +40 -0
- package/docs/zh/guide/installation.md +37 -0
- package/docs/zh/index.md +29 -0
- package/extension/dist/background.js +386 -438
- package/extension/manifest.json +2 -2
- package/extension/package-lock.json +1156 -0
- package/extension/src/background.test.ts +151 -0
- package/extension/src/background.ts +124 -53
- package/extension/src/protocol.ts +3 -1
- package/package.json +7 -3
- package/src/browser/cdp.ts +367 -0
- package/src/browser/daemon-client.ts +7 -1
- package/src/browser/dom-helpers.ts +116 -0
- package/src/browser/index.ts +4 -0
- package/src/browser/mcp.ts +14 -6
- package/src/browser/page.ts +47 -124
- package/src/browser/utils.ts +27 -0
- package/src/browser.test.ts +56 -0
- package/src/build-manifest.test.ts +36 -0
- package/src/build-manifest.ts +2 -1
- package/src/capabilityRouting.test.ts +47 -0
- package/src/capabilityRouting.ts +28 -0
- package/src/chaoxing.test.ts +53 -0
- package/src/chaoxing.ts +268 -0
- package/src/cli.ts +205 -0
- package/src/clis/antigravity/SKILL.md +5 -0
- package/src/clis/antigravity/serve.ts +329 -0
- package/src/clis/bilibili/download.ts +4 -15
- package/src/clis/boss/chatlist.ts +50 -0
- package/src/clis/boss/chatmsg.ts +70 -0
- package/src/clis/boss/resume.ts +262 -0
- package/src/clis/boss/send.ts +193 -0
- package/src/clis/chaoxing/README.md +36 -0
- package/src/clis/chaoxing/README.zh-CN.md +35 -0
- package/src/clis/chaoxing/assignments.ts +88 -0
- package/src/clis/chaoxing/exams.ts +88 -0
- package/src/clis/chatgpt/ask.ts +14 -15
- package/src/clis/chatgpt/ax.ts +81 -0
- package/src/clis/chatgpt/read.ts +5 -7
- package/src/clis/hf/top.ts +141 -0
- package/src/clis/jike/comment.ts +113 -0
- package/src/clis/jike/create.ts +113 -0
- package/src/clis/jike/feed.ts +74 -0
- package/src/clis/jike/like.ts +65 -0
- package/src/clis/jike/notifications.ts +185 -0
- package/src/clis/jike/post.yaml +58 -0
- package/src/clis/jike/repost.ts +114 -0
- package/src/clis/jike/search.ts +74 -0
- package/src/clis/jike/shared.ts +36 -0
- package/src/clis/jike/topic.yaml +52 -0
- package/src/clis/jike/user.yaml +51 -0
- package/src/clis/smzdm/search.ts +30 -39
- package/src/clis/stackoverflow/bounties.yaml +29 -0
- package/src/clis/stackoverflow/hot.yaml +28 -0
- package/src/clis/stackoverflow/search.yaml +32 -0
- package/src/clis/stackoverflow/unanswered.yaml +28 -0
- package/src/clis/twitter/download.ts +6 -17
- package/src/clis/twitter/post.ts +9 -2
- package/src/clis/twitter/search.ts +15 -33
- package/src/clis/xiaohongshu/download.ts +4 -4
- package/src/clis/zhihu/download.ts +3 -3
- package/src/doctor.ts +18 -2
- package/src/download/index.test.ts +16 -0
- package/src/download/index.ts +22 -4
- package/src/engine.ts +20 -13
- package/src/explore.ts +54 -103
- package/src/generate.ts +1 -0
- package/src/interceptor.ts +3 -2
- package/src/main.ts +4 -180
- package/src/output.ts +15 -13
- package/src/pipeline/executor.test.ts +1 -0
- package/src/pipeline/steps/download.ts +14 -17
- package/src/registry.ts +9 -5
- package/src/runtime.ts +3 -2
- package/src/scripts/framework.ts +20 -0
- package/src/scripts/interact.ts +22 -0
- package/src/scripts/store.ts +40 -0
- package/src/synthesize.ts +1 -1
- package/src/types.ts +9 -0
- package/src/verify.ts +64 -3
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import {
|
|
3
|
+
getCourses, initSession, enterCourse, getTabIframeUrl,
|
|
4
|
+
parseExamsFromDom, sleep,
|
|
5
|
+
type ExamRow,
|
|
6
|
+
} from '../../chaoxing.js';
|
|
7
|
+
|
|
8
|
+
cli({
|
|
9
|
+
site: 'chaoxing',
|
|
10
|
+
name: 'exams',
|
|
11
|
+
description: '学习通考试列表',
|
|
12
|
+
domain: 'mooc2-ans.chaoxing.com',
|
|
13
|
+
strategy: Strategy.COOKIE,
|
|
14
|
+
timeoutSeconds: 90,
|
|
15
|
+
args: [
|
|
16
|
+
{ name: 'course', type: 'string', help: '按课程名过滤(模糊匹配)' },
|
|
17
|
+
{
|
|
18
|
+
name: 'status',
|
|
19
|
+
type: 'string',
|
|
20
|
+
default: 'all',
|
|
21
|
+
choices: ['all', 'upcoming', 'ongoing', 'finished'],
|
|
22
|
+
help: '按状态过滤',
|
|
23
|
+
},
|
|
24
|
+
{ name: 'limit', type: 'int', default: 20, help: '最大返回数量' },
|
|
25
|
+
],
|
|
26
|
+
columns: ['rank', 'course', 'title', 'start', 'end', 'status', 'score'],
|
|
27
|
+
|
|
28
|
+
func: async (page, kwargs) => {
|
|
29
|
+
const { course: courseFilter, status: statusFilter = 'all', limit = 20 } = kwargs;
|
|
30
|
+
|
|
31
|
+
// 1. Establish session
|
|
32
|
+
await initSession(page);
|
|
33
|
+
|
|
34
|
+
// 2. Get courses
|
|
35
|
+
const courses = await getCourses(page);
|
|
36
|
+
if (!courses.length) throw new Error('未获取到课程列表,请确认已登录学习通');
|
|
37
|
+
|
|
38
|
+
const filtered = courseFilter
|
|
39
|
+
? courses.filter(c => c.title.includes(courseFilter))
|
|
40
|
+
: courses;
|
|
41
|
+
if (courseFilter && !filtered.length) {
|
|
42
|
+
throw new Error(`未找到匹配「${courseFilter}」的课程`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 3. Per-course: enter → click 考试 tab → navigate to iframe → parse
|
|
46
|
+
const allRows: ExamRow[] = [];
|
|
47
|
+
|
|
48
|
+
for (const c of filtered) {
|
|
49
|
+
try {
|
|
50
|
+
await enterCourse(page, c);
|
|
51
|
+
const iframeUrl = await getTabIframeUrl(page, '考试');
|
|
52
|
+
if (!iframeUrl) continue;
|
|
53
|
+
|
|
54
|
+
await page.goto(iframeUrl);
|
|
55
|
+
await page.wait(2);
|
|
56
|
+
|
|
57
|
+
const rows = await parseExamsFromDom(page, c.title);
|
|
58
|
+
allRows.push(...rows);
|
|
59
|
+
} catch {
|
|
60
|
+
// Single course failure: skip, continue
|
|
61
|
+
}
|
|
62
|
+
if (filtered.length > 1) await sleep(600);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 4. Sort: upcoming first
|
|
66
|
+
allRows.sort((a, b) => {
|
|
67
|
+
const order = (s: string) =>
|
|
68
|
+
s === '未开始' ? 0 : s === '进行中' ? 1 : s === '已结束' ? 2 : s === '已完成' ? 3 : 4;
|
|
69
|
+
return order(a.status) - order(b.status);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// 5. Filter by status
|
|
73
|
+
const statusMap: Record<string, string[]> = {
|
|
74
|
+
upcoming: ['未开始'],
|
|
75
|
+
ongoing: ['进行中'],
|
|
76
|
+
finished: ['已结束', '已完成'],
|
|
77
|
+
};
|
|
78
|
+
const finalRows =
|
|
79
|
+
statusFilter === 'all'
|
|
80
|
+
? allRows
|
|
81
|
+
: allRows.filter(r => statusMap[statusFilter]?.includes(r.status));
|
|
82
|
+
|
|
83
|
+
return finalRows.slice(0, Number(limit)).map((item, i) => ({
|
|
84
|
+
rank: i + 1,
|
|
85
|
+
...item,
|
|
86
|
+
}));
|
|
87
|
+
},
|
|
88
|
+
});
|
package/src/clis/chatgpt/ask.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execSync, spawnSync } from 'node:child_process';
|
|
2
2
|
import { cli, Strategy } from '../../registry.js';
|
|
3
3
|
import type { IPage } from '../../types.js';
|
|
4
|
+
import { getVisibleChatMessages } from './ax.js';
|
|
4
5
|
|
|
5
6
|
export const askCommand = cli({
|
|
6
7
|
site: 'chatgpt',
|
|
@@ -21,6 +22,7 @@ export const askCommand = cli({
|
|
|
21
22
|
// Backup clipboard
|
|
22
23
|
let clipBackup = '';
|
|
23
24
|
try { clipBackup = execSync('pbpaste', { encoding: 'utf-8' }); } catch {}
|
|
25
|
+
const messagesBefore = getVisibleChatMessages();
|
|
24
26
|
|
|
25
27
|
// Send the message
|
|
26
28
|
spawnSync('pbcopy', { input: text });
|
|
@@ -35,33 +37,30 @@ export const askCommand = cli({
|
|
|
35
37
|
"-e 'end tell'";
|
|
36
38
|
execSync(cmd);
|
|
37
39
|
|
|
38
|
-
//
|
|
39
|
-
spawnSync('pbcopy', { input:
|
|
40
|
+
// Restore clipboard after the prompt is sent.
|
|
41
|
+
if (clipBackup) spawnSync('pbcopy', { input: clipBackup });
|
|
40
42
|
|
|
41
|
-
// Wait for response, then read
|
|
42
|
-
const pollInterval =
|
|
43
|
+
// Wait for response, then read the latest visible assistant message from the AX tree.
|
|
44
|
+
const pollInterval = 1;
|
|
43
45
|
const maxPolls = Math.ceil(timeout / pollInterval);
|
|
44
46
|
let response = '';
|
|
45
47
|
|
|
46
48
|
for (let i = 0; i < maxPolls; i++) {
|
|
47
|
-
// Wait
|
|
48
49
|
execSync(`sleep ${pollInterval}`);
|
|
49
|
-
|
|
50
|
-
// Try Cmd+Shift+C to copy the latest response
|
|
51
50
|
execSync("osascript -e 'tell application \"ChatGPT\" to activate'");
|
|
52
|
-
execSync("osascript -e '
|
|
53
|
-
|
|
51
|
+
execSync("osascript -e 'delay 0.2'");
|
|
52
|
+
|
|
53
|
+
const messagesNow = getVisibleChatMessages();
|
|
54
|
+
if (messagesNow.length <= messagesBefore.length) continue;
|
|
54
55
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
const newMessages = messagesNow.slice(messagesBefore.length);
|
|
57
|
+
const candidate = [...newMessages].reverse().find((message) => message !== text);
|
|
58
|
+
if (candidate) {
|
|
59
|
+
response = candidate;
|
|
58
60
|
break;
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
// Restore clipboard
|
|
63
|
-
if (clipBackup) spawnSync('pbcopy', { input: clipBackup });
|
|
64
|
-
|
|
65
64
|
if (!response) {
|
|
66
65
|
return [
|
|
67
66
|
{ Role: 'User', Text: text },
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
const AX_READ_SCRIPT = `
|
|
4
|
+
import Cocoa
|
|
5
|
+
import ApplicationServices
|
|
6
|
+
|
|
7
|
+
func attr(_ el: AXUIElement, _ name: String) -> AnyObject? {
|
|
8
|
+
var value: CFTypeRef?
|
|
9
|
+
guard AXUIElementCopyAttributeValue(el, name as CFString, &value) == .success else { return nil }
|
|
10
|
+
return value as AnyObject?
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
func s(_ el: AXUIElement, _ name: String) -> String? {
|
|
14
|
+
if let v = attr(el, name) as? String, !v.isEmpty { return v }
|
|
15
|
+
return nil
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func children(_ el: AXUIElement) -> [AXUIElement] {
|
|
19
|
+
(attr(el, kAXChildrenAttribute as String) as? [AnyObject] ?? []).map { $0 as! AXUIElement }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func collectLists(_ el: AXUIElement, into out: inout [AXUIElement]) {
|
|
23
|
+
let role = s(el, kAXRoleAttribute as String) ?? ""
|
|
24
|
+
if role == kAXListRole as String { out.append(el) }
|
|
25
|
+
for c in children(el) { collectLists(c, into: &out) }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func collectTexts(_ el: AXUIElement, into out: inout [String]) {
|
|
29
|
+
let role = s(el, kAXRoleAttribute as String) ?? ""
|
|
30
|
+
if role == kAXStaticTextRole as String {
|
|
31
|
+
if let text = s(el, kAXDescriptionAttribute as String), !text.isEmpty {
|
|
32
|
+
out.append(text)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for c in children(el) { collectTexts(c, into: &out) }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
guard let app = NSRunningApplication.runningApplications(withBundleIdentifier: "com.openai.chat").first else {
|
|
39
|
+
fputs("ChatGPT not running\\n", stderr)
|
|
40
|
+
exit(1)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let axApp = AXUIElementCreateApplication(app.processIdentifier)
|
|
44
|
+
guard let win = attr(axApp, kAXFocusedWindowAttribute as String) as! AXUIElement? else {
|
|
45
|
+
fputs("No focused ChatGPT window\\n", stderr)
|
|
46
|
+
exit(1)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
var lists: [AXUIElement] = []
|
|
50
|
+
collectLists(win, into: &lists)
|
|
51
|
+
|
|
52
|
+
var best: [String] = []
|
|
53
|
+
for list in lists {
|
|
54
|
+
var texts: [String] = []
|
|
55
|
+
collectTexts(list, into: &texts)
|
|
56
|
+
if texts.count > best.count {
|
|
57
|
+
best = texts
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let data = try! JSONSerialization.data(withJSONObject: best, options: [])
|
|
62
|
+
print(String(data: data, encoding: .utf8)!)
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
export function getVisibleChatMessages(): string[] {
|
|
66
|
+
const output = execFileSync('swift', ['-'], {
|
|
67
|
+
input: AX_READ_SCRIPT,
|
|
68
|
+
encoding: 'utf-8',
|
|
69
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
70
|
+
}).trim();
|
|
71
|
+
|
|
72
|
+
if (!output) return [];
|
|
73
|
+
|
|
74
|
+
const parsed = JSON.parse(output);
|
|
75
|
+
if (!Array.isArray(parsed)) return [];
|
|
76
|
+
|
|
77
|
+
return parsed
|
|
78
|
+
.filter((item): item is string => typeof item === 'string')
|
|
79
|
+
.map((item) => item.replace(/[\uFFFC\u200B-\u200D\uFEFF]/g, '').trim())
|
|
80
|
+
.filter((item) => item.length > 0);
|
|
81
|
+
}
|
package/src/clis/chatgpt/read.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
2
|
import { cli, Strategy } from '../../registry.js';
|
|
3
3
|
import type { IPage } from '../../types.js';
|
|
4
|
+
import { getVisibleChatMessages } from './ax.js';
|
|
4
5
|
|
|
5
6
|
export const readCommand = cli({
|
|
6
7
|
site: 'chatgpt',
|
|
@@ -14,17 +15,14 @@ export const readCommand = cli({
|
|
|
14
15
|
func: async (page: IPage | null) => {
|
|
15
16
|
try {
|
|
16
17
|
execSync("osascript -e 'tell application \"ChatGPT\" to activate'");
|
|
17
|
-
execSync("osascript -e 'delay 0.5'");
|
|
18
|
-
execSync("osascript -e 'tell application \"System Events\" to keystroke \"c\" using {command down, shift down}'");
|
|
19
18
|
execSync("osascript -e 'delay 0.3'");
|
|
19
|
+
const messages = getVisibleChatMessages();
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (!result) {
|
|
24
|
-
return [{ Role: 'System', Text: 'No text was copied. Is there a response in the chat?' }];
|
|
21
|
+
if (!messages.length) {
|
|
22
|
+
return [{ Role: 'System', Text: 'No visible chat messages were found in the current ChatGPT window.' }];
|
|
25
23
|
}
|
|
26
24
|
|
|
27
|
-
return [{ Role: 'Assistant', Text:
|
|
25
|
+
return [{ Role: 'Assistant', Text: messages[messages.length - 1] }];
|
|
28
26
|
} catch (err: any) {
|
|
29
27
|
throw new Error("Failed to read from ChatGPT: " + err.message);
|
|
30
28
|
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { CliError } from '../../errors.js';
|
|
3
|
+
|
|
4
|
+
interface PaperAuthor {
|
|
5
|
+
name: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface DailyPaper {
|
|
9
|
+
paper: {
|
|
10
|
+
id: string;
|
|
11
|
+
upvotes: number;
|
|
12
|
+
authors: PaperAuthor[];
|
|
13
|
+
};
|
|
14
|
+
title: string;
|
|
15
|
+
numComments: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface PeriodPaper {
|
|
19
|
+
id: string;
|
|
20
|
+
title: string;
|
|
21
|
+
upvotes: number;
|
|
22
|
+
publishedAt: string;
|
|
23
|
+
authors: PaperAuthor[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function truncate(str: string, max = 60): string {
|
|
27
|
+
return str.length > max ? str.slice(0, max - 3) + '...' : str;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatAuthors(authors: PaperAuthor[], max = 3): string {
|
|
31
|
+
const names = authors.map((a) => a.name);
|
|
32
|
+
if (names.length <= max) return names.join(', ');
|
|
33
|
+
return names.slice(0, max).join(', ') + ' et al.';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const MONTH_ABBR = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
37
|
+
|
|
38
|
+
function getMonthRange(): string {
|
|
39
|
+
const now = new Date();
|
|
40
|
+
return `${MONTH_ABBR[now.getUTCMonth()]} ${now.getUTCFullYear()}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getWeekRange(): string {
|
|
44
|
+
const now = new Date();
|
|
45
|
+
const day = now.getUTCDay(); // 0=Sun, 6=Sat
|
|
46
|
+
const daysToSat = day === 6 ? 0 : 6 - day;
|
|
47
|
+
const end = new Date(now);
|
|
48
|
+
end.setUTCDate(now.getUTCDate() + daysToSat);
|
|
49
|
+
const start = new Date(end);
|
|
50
|
+
start.setUTCDate(end.getUTCDate() - 6);
|
|
51
|
+
|
|
52
|
+
const sm = MONTH_ABBR[start.getUTCMonth()];
|
|
53
|
+
const em = MONTH_ABBR[end.getUTCMonth()];
|
|
54
|
+
const sd = start.getUTCDate();
|
|
55
|
+
const ed = end.getUTCDate();
|
|
56
|
+
return sm === em ? `${sm} ${sd}-${ed}` : `${sm} ${sd}-${em} ${ed}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
cli({
|
|
60
|
+
site: 'hf',
|
|
61
|
+
name: 'top',
|
|
62
|
+
description: 'Top upvoted Hugging Face papers',
|
|
63
|
+
domain: 'huggingface.co',
|
|
64
|
+
strategy: Strategy.PUBLIC,
|
|
65
|
+
browser: false,
|
|
66
|
+
args: [
|
|
67
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of papers' },
|
|
68
|
+
{ name: 'all', type: 'bool', default: false, help: 'Return all papers (ignore limit)' },
|
|
69
|
+
{ name: 'date', type: 'str', required: false, help: 'Date (YYYY-MM-DD), defaults to most recent' },
|
|
70
|
+
{ name: 'period', type: 'str', default: 'daily', choices: ['daily', 'weekly', 'monthly'], help: 'Time period: daily, weekly, or monthly' },
|
|
71
|
+
],
|
|
72
|
+
footerExtra: (kwargs) => {
|
|
73
|
+
if (kwargs._footerDate) return kwargs._footerDate;
|
|
74
|
+
if (kwargs.period === 'monthly') return getMonthRange();
|
|
75
|
+
if (kwargs.period === 'weekly') return getWeekRange();
|
|
76
|
+
return kwargs.date ?? new Date().toISOString().slice(0, 10);
|
|
77
|
+
},
|
|
78
|
+
func: async (_page, kwargs) => {
|
|
79
|
+
const period = String(kwargs.period ?? 'daily');
|
|
80
|
+
const all = Boolean(kwargs.all);
|
|
81
|
+
const endpoint = process.env.HF_ENDPOINT?.replace(/\/+$/, '') || 'https://huggingface.co';
|
|
82
|
+
|
|
83
|
+
if (period === 'weekly' || period === 'monthly') {
|
|
84
|
+
if (kwargs.date) {
|
|
85
|
+
throw new CliError('INVALID_ARG', `--date is not supported for ${period} period`, `Omit --date when using --period ${period}`);
|
|
86
|
+
}
|
|
87
|
+
const url = `${endpoint}/api/papers?period=${period}`;
|
|
88
|
+
const res = await fetch(url);
|
|
89
|
+
if (!res.ok) throw new CliError('FETCH_ERROR', `HF API error: ${res.status} ${res.statusText}`, 'Check HF_ENDPOINT or try again later');
|
|
90
|
+
const body = await res.json();
|
|
91
|
+
if (!Array.isArray(body)) throw new CliError('FETCH_ERROR', 'Unexpected HF API response', 'Check endpoint');
|
|
92
|
+
const data: PeriodPaper[] = body;
|
|
93
|
+
const dates = data.map((d) => d.publishedAt).filter(Boolean).sort();
|
|
94
|
+
if (dates.length > 0) {
|
|
95
|
+
if (period === 'monthly') {
|
|
96
|
+
const d = new Date(dates[0]);
|
|
97
|
+
kwargs._footerDate = `${MONTH_ABBR[d.getUTCMonth()]} ${d.getUTCFullYear()}`;
|
|
98
|
+
} else {
|
|
99
|
+
const start = new Date(dates[0]);
|
|
100
|
+
const end = new Date(dates[dates.length - 1]);
|
|
101
|
+
const sm = MONTH_ABBR[start.getUTCMonth()];
|
|
102
|
+
const em = MONTH_ABBR[end.getUTCMonth()];
|
|
103
|
+
const sd = start.getUTCDate();
|
|
104
|
+
const ed = end.getUTCDate();
|
|
105
|
+
kwargs._footerDate = sm === em ? `${sm} ${sd}-${ed}` : `${sm} ${sd}-${em} ${ed}`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const sorted = [...data].sort((a, b) => (b.upvotes ?? 0) - (a.upvotes ?? 0));
|
|
109
|
+
const items = all ? sorted : sorted.slice(0, Number(kwargs.limit));
|
|
110
|
+
return items.map((item, i) => ({
|
|
111
|
+
rank: i + 1,
|
|
112
|
+
id: item.id ?? '',
|
|
113
|
+
title: truncate(item.title ?? ''),
|
|
114
|
+
upvotes: item.upvotes ?? 0,
|
|
115
|
+
authors: formatAuthors(item.authors ?? []),
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// daily
|
|
120
|
+
if (kwargs.date && !/^\d{4}-\d{2}-\d{2}$/.test(String(kwargs.date))) {
|
|
121
|
+
throw new CliError('INVALID_ARG', `Invalid date format: ${kwargs.date}`, 'Use YYYY-MM-DD');
|
|
122
|
+
}
|
|
123
|
+
const url = kwargs.date
|
|
124
|
+
? `${endpoint}/api/daily_papers?date=${kwargs.date}`
|
|
125
|
+
: `${endpoint}/api/daily_papers`;
|
|
126
|
+
const res = await fetch(url);
|
|
127
|
+
if (!res.ok) throw new CliError('FETCH_ERROR', `HF API error: ${res.status} ${res.statusText}`, 'Check HF_ENDPOINT or try again later');
|
|
128
|
+
const body = await res.json();
|
|
129
|
+
if (!Array.isArray(body)) throw new CliError('FETCH_ERROR', 'Unexpected HF API response', 'Check date format or endpoint');
|
|
130
|
+
const data: DailyPaper[] = body;
|
|
131
|
+
const sorted = [...data].sort((a, b) => (b.paper?.upvotes ?? 0) - (a.paper?.upvotes ?? 0));
|
|
132
|
+
const items = all ? sorted : sorted.slice(0, Number(kwargs.limit));
|
|
133
|
+
return items.map((item, i) => ({
|
|
134
|
+
rank: i + 1,
|
|
135
|
+
id: item.paper?.id ?? '',
|
|
136
|
+
title: truncate(item.title ?? ''),
|
|
137
|
+
upvotes: item.paper?.upvotes ?? 0,
|
|
138
|
+
authors: formatAuthors(item.paper?.authors ?? []),
|
|
139
|
+
}));
|
|
140
|
+
},
|
|
141
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 评论即刻帖子
|
|
5
|
+
*
|
|
6
|
+
* 帖子详情页有评论输入框(contenteditable 或 textarea),
|
|
7
|
+
* 填入文本后点击"回复"或"发布"按钮提交。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
cli({
|
|
11
|
+
site: 'jike',
|
|
12
|
+
name: 'comment',
|
|
13
|
+
description: '评论即刻帖子',
|
|
14
|
+
domain: 'web.okjike.com',
|
|
15
|
+
strategy: Strategy.UI,
|
|
16
|
+
browser: true,
|
|
17
|
+
args: [
|
|
18
|
+
{ name: 'id', type: 'string', required: true, help: '帖子 ID' },
|
|
19
|
+
{ name: 'text', type: 'string', required: true, help: '评论内容' },
|
|
20
|
+
],
|
|
21
|
+
columns: ['status', 'message'],
|
|
22
|
+
func: async (page, kwargs) => {
|
|
23
|
+
await page.goto(`https://web.okjike.com/originalPost/${kwargs.id}`);
|
|
24
|
+
await page.wait(5);
|
|
25
|
+
|
|
26
|
+
// 1. 找到评论输入框并填入文本
|
|
27
|
+
const inputResult = await page.evaluate(`(async () => {
|
|
28
|
+
try {
|
|
29
|
+
const textToInsert = ${JSON.stringify(kwargs.text)};
|
|
30
|
+
|
|
31
|
+
// 优先在评论区容器内找 contenteditable,避免误选页面其他编辑器;
|
|
32
|
+
// 若评论区 class 名变更则回退到全页查找
|
|
33
|
+
const editor =
|
|
34
|
+
document.querySelector('[class*="_comment_"] [contenteditable="true"]') ||
|
|
35
|
+
document.querySelector('[contenteditable="true"]');
|
|
36
|
+
if (editor) {
|
|
37
|
+
editor.focus();
|
|
38
|
+
const dt = new DataTransfer();
|
|
39
|
+
dt.setData('text/plain', textToInsert);
|
|
40
|
+
editor.dispatchEvent(new ClipboardEvent('paste', {
|
|
41
|
+
clipboardData: dt, bubbles: true, cancelable: true,
|
|
42
|
+
}));
|
|
43
|
+
await new Promise(r => setTimeout(r, 800));
|
|
44
|
+
if (editor.textContent?.length > 0) {
|
|
45
|
+
return { ok: true, message: 'contenteditable' };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 回退:textarea(带评论相关 placeholder)
|
|
50
|
+
const textareas = document.querySelectorAll('textarea');
|
|
51
|
+
for (const ta of textareas) {
|
|
52
|
+
const ph = ta.getAttribute('placeholder') || '';
|
|
53
|
+
if (ph.includes('评论') || ph.includes('回复') || ph.includes('说点什么')) {
|
|
54
|
+
ta.focus();
|
|
55
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
56
|
+
HTMLTextAreaElement.prototype, 'value'
|
|
57
|
+
)?.set;
|
|
58
|
+
setter?.call(ta, textToInsert);
|
|
59
|
+
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
60
|
+
await new Promise(r => setTimeout(r, 500));
|
|
61
|
+
return { ok: true, message: 'textarea' };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 兜底:任意 textarea
|
|
66
|
+
if (textareas.length > 0) {
|
|
67
|
+
const ta = textareas[0];
|
|
68
|
+
ta.focus();
|
|
69
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
70
|
+
HTMLTextAreaElement.prototype, 'value'
|
|
71
|
+
)?.set;
|
|
72
|
+
setter?.call(ta, textToInsert);
|
|
73
|
+
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
74
|
+
await new Promise(r => setTimeout(r, 500));
|
|
75
|
+
return { ok: true, message: 'textarea-fallback' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { ok: false, message: '未找到评论输入框' };
|
|
79
|
+
} catch (e) {
|
|
80
|
+
return { ok: false, message: e.toString() };
|
|
81
|
+
}
|
|
82
|
+
})()`);
|
|
83
|
+
|
|
84
|
+
if (!inputResult.ok) {
|
|
85
|
+
return [{ status: 'failed', message: inputResult.message }];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 2. 点击"回复"或"发布"按钮
|
|
89
|
+
const submitResult = await page.evaluate(`(async () => {
|
|
90
|
+
try {
|
|
91
|
+
await new Promise(r => setTimeout(r, 500));
|
|
92
|
+
const btns = Array.from(document.querySelectorAll('button')).filter(btn => {
|
|
93
|
+
const text = btn.textContent?.trim() || '';
|
|
94
|
+
return (text === '回复' || text === '发布' || text === '发送' || text === '评论') && !btn.disabled;
|
|
95
|
+
});
|
|
96
|
+
if (btns.length === 0) {
|
|
97
|
+
return { ok: false, message: '未找到可用的回复按钮(可能因内容为空而禁用)' };
|
|
98
|
+
}
|
|
99
|
+
btns[0].click();
|
|
100
|
+
return { ok: true, message: '评论发布成功' };
|
|
101
|
+
} catch (e) {
|
|
102
|
+
return { ok: false, message: e.toString() };
|
|
103
|
+
}
|
|
104
|
+
})()`);
|
|
105
|
+
|
|
106
|
+
if (submitResult.ok) await page.wait(3);
|
|
107
|
+
|
|
108
|
+
return [{
|
|
109
|
+
status: submitResult.ok ? 'success' : 'failed',
|
|
110
|
+
message: submitResult.message,
|
|
111
|
+
}];
|
|
112
|
+
},
|
|
113
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 发布即刻动态
|
|
5
|
+
*
|
|
6
|
+
* 即刻首页 /following 顶部有内联发帖框("分享你的想法..."),
|
|
7
|
+
* 直接在其中输入文本,点击"发送"按钮即可发布。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
cli({
|
|
11
|
+
site: 'jike',
|
|
12
|
+
name: 'create',
|
|
13
|
+
description: '发布即刻动态',
|
|
14
|
+
domain: 'web.okjike.com',
|
|
15
|
+
strategy: Strategy.UI,
|
|
16
|
+
browser: true,
|
|
17
|
+
args: [
|
|
18
|
+
{ name: 'text', type: 'string', required: true, help: '动态正文内容' },
|
|
19
|
+
],
|
|
20
|
+
columns: ['status', 'message'],
|
|
21
|
+
func: async (page, kwargs) => {
|
|
22
|
+
// 1. 导航到首页(有内联发帖框)
|
|
23
|
+
await page.goto('https://web.okjike.com');
|
|
24
|
+
await page.wait(5);
|
|
25
|
+
|
|
26
|
+
// 2. 在发帖框中输入文本
|
|
27
|
+
const textResult = await page.evaluate(`(async () => {
|
|
28
|
+
try {
|
|
29
|
+
const textToInsert = ${JSON.stringify(kwargs.text)};
|
|
30
|
+
|
|
31
|
+
// 首页发帖框在 _postForm_ 容器内,查找其中的 contenteditable
|
|
32
|
+
const form = document.querySelector('[class*="_postForm_"]');
|
|
33
|
+
const editor = form
|
|
34
|
+
? form.querySelector('[contenteditable="true"]')
|
|
35
|
+
: document.querySelector('[contenteditable="true"]');
|
|
36
|
+
|
|
37
|
+
if (editor) {
|
|
38
|
+
editor.focus();
|
|
39
|
+
// 用 ClipboardEvent paste 触发 React 状态更新
|
|
40
|
+
const dt = new DataTransfer();
|
|
41
|
+
dt.setData('text/plain', textToInsert);
|
|
42
|
+
editor.dispatchEvent(new ClipboardEvent('paste', {
|
|
43
|
+
clipboardData: dt, bubbles: true, cancelable: true,
|
|
44
|
+
}));
|
|
45
|
+
await new Promise(r => setTimeout(r, 800));
|
|
46
|
+
|
|
47
|
+
// 检查是否成功插入
|
|
48
|
+
const inserted = editor.textContent || '';
|
|
49
|
+
if (inserted.length > 0) {
|
|
50
|
+
return { ok: true, message: 'contenteditable' };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 回退:textarea
|
|
55
|
+
const textarea = form
|
|
56
|
+
? form.querySelector('textarea')
|
|
57
|
+
: document.querySelector('textarea');
|
|
58
|
+
|
|
59
|
+
if (textarea) {
|
|
60
|
+
textarea.focus();
|
|
61
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
62
|
+
HTMLTextAreaElement.prototype, 'value'
|
|
63
|
+
)?.set;
|
|
64
|
+
setter?.call(textarea, textToInsert);
|
|
65
|
+
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
66
|
+
await new Promise(r => setTimeout(r, 500));
|
|
67
|
+
return { ok: true, message: 'textarea' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { ok: false, message: '未找到发帖输入框' };
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return { ok: false, message: e.toString() };
|
|
73
|
+
}
|
|
74
|
+
})()`);
|
|
75
|
+
|
|
76
|
+
if (!textResult.ok) {
|
|
77
|
+
return [{ status: 'failed', message: textResult.message }];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 3. 点击"发送"按钮
|
|
81
|
+
const submitResult = await page.evaluate(`(async () => {
|
|
82
|
+
try {
|
|
83
|
+
await new Promise(r => setTimeout(r, 500));
|
|
84
|
+
|
|
85
|
+
// 即刻首页发帖框的按钮文字为"发送"
|
|
86
|
+
const candidates = [
|
|
87
|
+
...Array.from(document.querySelectorAll('button')).filter(btn => {
|
|
88
|
+
const text = btn.textContent?.trim() || '';
|
|
89
|
+
return text === '发送' || text === '发布';
|
|
90
|
+
}),
|
|
91
|
+
].filter(el => el && !el.disabled);
|
|
92
|
+
|
|
93
|
+
if (candidates.length === 0) {
|
|
94
|
+
return { ok: false, message: '未找到可用的发送按钮(按钮可能因内容为空而禁用)' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
candidates[0].click();
|
|
98
|
+
return { ok: true, message: '动态发布成功' };
|
|
99
|
+
} catch (e) {
|
|
100
|
+
return { ok: false, message: e.toString() };
|
|
101
|
+
}
|
|
102
|
+
})()`);
|
|
103
|
+
|
|
104
|
+
if (submitResult.ok) {
|
|
105
|
+
await page.wait(3);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return [{
|
|
109
|
+
status: submitResult.ok ? 'success' : 'failed',
|
|
110
|
+
message: submitResult.message,
|
|
111
|
+
}];
|
|
112
|
+
},
|
|
113
|
+
});
|