job-pro 1.0.16 → 1.0.17
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/dist/index.js +46 -0
- package/extension/README.md +79 -0
- package/extension/background.js +177 -0
- package/extension/manifest.json +55 -0
- package/extension/popup.html +37 -0
- package/extension/popup.js +54 -0
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -55,6 +55,7 @@ import { createInterface } from "node:readline";
|
|
|
55
55
|
import { memoryList, memoryGet, memorySet, memoryEvent, memoryClear, } from "./memory.js";
|
|
56
56
|
import { writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
57
57
|
import { dirname, join } from "node:path";
|
|
58
|
+
import { fileURLToPath } from "node:url";
|
|
58
59
|
import { homedir } from "node:os";
|
|
59
60
|
import { createRequire as require_createRequire } from "node:module";
|
|
60
61
|
function require_module() {
|
|
@@ -131,6 +132,8 @@ USAGE
|
|
|
131
132
|
[--limit N] [--companies a,b,c]
|
|
132
133
|
[--timeout ms] [--apply-ready]
|
|
133
134
|
[--compact | --text]
|
|
135
|
+
job-pro extension print extension/ path + install steps
|
|
136
|
+
job-pro extension path just the absolute path (scriptable)
|
|
134
137
|
job-pro --version
|
|
135
138
|
job-pro help
|
|
136
139
|
|
|
@@ -1095,6 +1098,49 @@ async function main() {
|
|
|
1095
1098
|
printStatus(compact);
|
|
1096
1099
|
return;
|
|
1097
1100
|
}
|
|
1101
|
+
if (cmd === "extension") {
|
|
1102
|
+
// Locate the extension/ directory. The package ships it as a sibling of
|
|
1103
|
+
// dist/, so __dirname is cli/dist and the extension lives at ../extension.
|
|
1104
|
+
// For a `npx job-pro` run, that lands in the npm cache; for a global
|
|
1105
|
+
// install, in the prefix. For local dev, the repo's top-level extension/.
|
|
1106
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
1107
|
+
const candidates = [
|
|
1108
|
+
join(here, "..", "extension"),
|
|
1109
|
+
join(here, "..", "..", "extension"),
|
|
1110
|
+
];
|
|
1111
|
+
const extPath = candidates.find((p) => existsSync(join(p, "manifest.json"))) ?? null;
|
|
1112
|
+
const sub = args[1];
|
|
1113
|
+
if (sub === "path") {
|
|
1114
|
+
if (!extPath)
|
|
1115
|
+
die("extension/ not found — please reinstall job-pro@latest");
|
|
1116
|
+
console.log(extPath);
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
// Default: print install walkthrough.
|
|
1120
|
+
if (!extPath)
|
|
1121
|
+
die("extension/ not found — please reinstall job-pro@latest");
|
|
1122
|
+
console.log(`
|
|
1123
|
+
job-pro session-capture extension
|
|
1124
|
+
=================================
|
|
1125
|
+
|
|
1126
|
+
Path: ${extPath}
|
|
1127
|
+
|
|
1128
|
+
Install (Chrome / Edge / Brave):
|
|
1129
|
+
1. Open chrome://extensions
|
|
1130
|
+
2. Enable "Developer mode" (top-right toggle)
|
|
1131
|
+
3. Click "Load unpacked"
|
|
1132
|
+
4. Pick the path above
|
|
1133
|
+
5. Browse a careers site (e.g. jobs.bytedance.com), log in, then click
|
|
1134
|
+
the extension's popup → "Export session" to drop
|
|
1135
|
+
~/Downloads/jobpro/<adapter>.session.json
|
|
1136
|
+
6. Move it under ~/.jobpro/<adapter>.session.json — \`job-pro <co> apply\`
|
|
1137
|
+
will pick it up automatically.
|
|
1138
|
+
|
|
1139
|
+
Or copy the path to clipboard (macOS):
|
|
1140
|
+
echo "${extPath}" | pbcopy
|
|
1141
|
+
`);
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1098
1144
|
if (cmd === "find") {
|
|
1099
1145
|
const compact = args.includes("--compact");
|
|
1100
1146
|
const textMode = args.includes("--text");
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# job-pro session bridge — Chrome extension
|
|
2
|
+
|
|
3
|
+
Manifest v3 extension that captures careers-site session cookies + CSRF/XSRF
|
|
4
|
+
headers for use by the CLI's auto-apply (Phase 2.1). Unlike the simple
|
|
5
|
+
Greenhouse / Lever boards (where the apply form is open-access and the CLI
|
|
6
|
+
can submit anonymously via `--debug-submit-to`), most Chinese ATS tenants
|
|
7
|
+
gate `apply` behind a logged-in candidate session. This extension lets
|
|
8
|
+
users log in once in their normal browser, then export the captured
|
|
9
|
+
session into `~/.jobpro/<adapter>.session.json` for the CLI to re-use.
|
|
10
|
+
|
|
11
|
+
## Install (developer mode, local)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Repo root
|
|
15
|
+
cd extension/
|
|
16
|
+
# Optional: generate placeholder icons (just colored squares).
|
|
17
|
+
# The extension still loads without them; popup just won't have an icon.
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
1. Open Chrome → `chrome://extensions/` → enable **Developer mode**.
|
|
21
|
+
2. Click **Load unpacked** → select the `extension/` directory.
|
|
22
|
+
3. Pin the puzzle-piece icon to the toolbar for quick access.
|
|
23
|
+
|
|
24
|
+
## Capture a session
|
|
25
|
+
|
|
26
|
+
1. Log into any supported careers site (e.g. `talent.antgroup.com`,
|
|
27
|
+
`iflytek.zhiye.com`, `app.mokahr.com/.../<org>/<siteId>`).
|
|
28
|
+
2. Browse around — view a job, open the apply modal — so the SPA fires
|
|
29
|
+
its auth-bearing XHRs. The extension listens for `Cookie`,
|
|
30
|
+
`X-Xsrf-Token`, `Authorization`, and Feishu/Beisen-style headers
|
|
31
|
+
(`X-Fscp-Std-Info`, `langtype`, etc.) and caches them by adapter key.
|
|
32
|
+
3. Click the toolbar icon → **Export** on the captured row. The
|
|
33
|
+
extension downloads `jobpro/<adapter>.session.json` via Chrome's
|
|
34
|
+
download manager.
|
|
35
|
+
4. Move the file:
|
|
36
|
+
```bash
|
|
37
|
+
mkdir -p ~/.jobpro
|
|
38
|
+
mv ~/Downloads/jobpro/<adapter>.session.json ~/.jobpro/
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## What's in the JSON
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"adapter": "antgroup",
|
|
46
|
+
"host": "talent.antgroup.com",
|
|
47
|
+
"exported_at": "2026-05-16T08:00:00.000Z",
|
|
48
|
+
"headers": {
|
|
49
|
+
"x-xsrf-token": "VSQK2wSZQC-DRAZxaQevxQ",
|
|
50
|
+
"x-fscp-std-info": "{\"client_id\": \"40108\"}",
|
|
51
|
+
"cookie": "<full cookie header>",
|
|
52
|
+
"...": "..."
|
|
53
|
+
},
|
|
54
|
+
"cookies": [
|
|
55
|
+
{ "name": "XSRF-TOKEN", "value": "…", "domain": ".liepin.com", "path": "/", … }
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The CLI doesn't read this file yet — Phase 2.1 wires that in.
|
|
61
|
+
Today the file is the deliverable; future iterations land the
|
|
62
|
+
`<adapter>.applyWithSession(sessionPath, postId)` flow.
|
|
63
|
+
|
|
64
|
+
## Why MV3, not a content script injection
|
|
65
|
+
|
|
66
|
+
We need `chrome.cookies` to dump HttpOnly cookies (used by every Chinese
|
|
67
|
+
ATS we've probed). Only background service workers can call
|
|
68
|
+
`chrome.cookies.getAll()`. The popup just talks to the worker via
|
|
69
|
+
`chrome.runtime.sendMessage`.
|
|
70
|
+
|
|
71
|
+
## Scope (privacy)
|
|
72
|
+
|
|
73
|
+
* Only captures headers from hosts explicitly listed in
|
|
74
|
+
`manifest.json#host_permissions`. Browsing anywhere else is invisible
|
|
75
|
+
to the extension.
|
|
76
|
+
* Storage is `chrome.storage.local` — never synced, never sent to any
|
|
77
|
+
remote.
|
|
78
|
+
* Exports are user-triggered downloads to `~/Downloads/jobpro/`. No
|
|
79
|
+
network egress.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// job-pro session bridge — background service worker.
|
|
2
|
+
//
|
|
3
|
+
// Captures cookies + recent CSRF / XSRF-Token headers from supported
|
|
4
|
+
// careers sites and stores them in chrome.storage so the popup can hand
|
|
5
|
+
// them off as a downloadable session.json file. The CLI's auto-apply
|
|
6
|
+
// then loads `~/.jobpro/<co>.session.json` and re-uses the captured
|
|
7
|
+
// credentials to fire the actual submission POST.
|
|
8
|
+
//
|
|
9
|
+
// Design constraint: this is a manifest-v3 service worker, so it
|
|
10
|
+
// shouldn't hold state in module-scope (workers can be evicted at any
|
|
11
|
+
// time). All state goes through chrome.storage.local.
|
|
12
|
+
|
|
13
|
+
// Map of careers-site host → adapter key. Used both to identify which
|
|
14
|
+
// "company" a session belongs to and to scope what we export.
|
|
15
|
+
const HOST_TO_KEY = {
|
|
16
|
+
"join.qq.com": "tencent",
|
|
17
|
+
"jobs.bytedance.com": "bytedance",
|
|
18
|
+
"campus-talent.alibaba.com": "alibaba",
|
|
19
|
+
"zhaopin.meituan.com": "meituan",
|
|
20
|
+
"job.xiaohongshu.com": "xiaohongshu",
|
|
21
|
+
"campus.jd.com": "jd",
|
|
22
|
+
"campus.kuaishou.cn": "kuaishou",
|
|
23
|
+
"xiaomi.jobs.f.mioffice.cn": "xiaomi",
|
|
24
|
+
"talent.baidu.com": "baidu",
|
|
25
|
+
"hr.163.com": "netease",
|
|
26
|
+
"talent.didiglobal.com": "didi",
|
|
27
|
+
"jobs.bilibili.com": "bilibili",
|
|
28
|
+
"careers.pinduoduo.com": "pdd",
|
|
29
|
+
"career.huawei.com": "huawei",
|
|
30
|
+
"campus.pingan.com": "pingan",
|
|
31
|
+
"careers.ctrip.com": "trip",
|
|
32
|
+
"www.unitree.com": "unitree",
|
|
33
|
+
"job.byd.com": "byd",
|
|
34
|
+
"talent.antgroup.com": "antgroup",
|
|
35
|
+
"hrcareersweb.antgroup.com": "antgroup",
|
|
36
|
+
"hr.sensetime.com": "sensetime",
|
|
37
|
+
"wecruit.hotjob.cn": "horizonrobotics",
|
|
38
|
+
"app.mokahr.com": "moka",
|
|
39
|
+
"careers.oppo.com": "oppo",
|
|
40
|
+
"hr.vivo.com": "vivo",
|
|
41
|
+
"vivo.zhiye.com": "vivo",
|
|
42
|
+
"iflytek.zhiye.com": "iflytek",
|
|
43
|
+
"campus.sf-express.com": "sf",
|
|
44
|
+
"www.lixiang.com": "liauto",
|
|
45
|
+
"lilithgames.jobs.feishu.cn": "lilith",
|
|
46
|
+
"www.liepin.com": "liepin",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function adapterKeyForHost(host) {
|
|
50
|
+
if (HOST_TO_KEY[host]) return HOST_TO_KEY[host];
|
|
51
|
+
// .jobs.feishu.cn / .zhiye.com wildcard fallback — store by subdomain.
|
|
52
|
+
if (host.endsWith(".jobs.feishu.cn")) return `feishu:${host}`;
|
|
53
|
+
if (host.endsWith(".zhiye.com")) return `zhiye:${host}`;
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Cache the latest auth-related request headers per adapter key. We
|
|
58
|
+
// don't keep XHR bodies — only `Cookie` / `X-Xsrf-Token` / `Authorization`
|
|
59
|
+
// / `X-Csrf-Token` / `X-Fscp-Std-Info` etc.
|
|
60
|
+
const AUTH_HEADER_NAMES = new Set([
|
|
61
|
+
"authorization",
|
|
62
|
+
"cookie",
|
|
63
|
+
"x-xsrf-token",
|
|
64
|
+
"x-csrf-token",
|
|
65
|
+
"x-csrftoken",
|
|
66
|
+
"x-requested-with",
|
|
67
|
+
"x-fscp-std-info",
|
|
68
|
+
"x-fscp-version",
|
|
69
|
+
"x-fscp-trace-id",
|
|
70
|
+
"x-client-type",
|
|
71
|
+
"langtype",
|
|
72
|
+
"x-token",
|
|
73
|
+
"x-auth-token",
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
chrome.webRequest?.onSendHeaders?.addListener?.(
|
|
77
|
+
// Note: in MV3 you can't *modify* requests without `declarativeNetRequest`,
|
|
78
|
+
// but reading via webRequest is still allowed for hosts in host_permissions.
|
|
79
|
+
// If the user is on a network where webRequest isn't available we fall
|
|
80
|
+
// back to cookies-only capture (see chrome.cookies below).
|
|
81
|
+
(details) => {
|
|
82
|
+
try {
|
|
83
|
+
const u = new URL(details.url);
|
|
84
|
+
const key = adapterKeyForHost(u.hostname);
|
|
85
|
+
if (!key) return;
|
|
86
|
+
const captured = {};
|
|
87
|
+
for (const h of details.requestHeaders ?? []) {
|
|
88
|
+
const name = (h.name ?? "").toLowerCase();
|
|
89
|
+
if (AUTH_HEADER_NAMES.has(name)) captured[name] = h.value ?? "";
|
|
90
|
+
}
|
|
91
|
+
if (Object.keys(captured).length === 0) return;
|
|
92
|
+
const storageKey = `auth_headers:${key}`;
|
|
93
|
+
chrome.storage.local.get([storageKey]).then((existing) => {
|
|
94
|
+
chrome.storage.local.set({
|
|
95
|
+
[storageKey]: {
|
|
96
|
+
adapter: key,
|
|
97
|
+
host: u.hostname,
|
|
98
|
+
url: details.url,
|
|
99
|
+
captured_at: new Date().toISOString(),
|
|
100
|
+
headers: { ...(existing[storageKey]?.headers ?? {}), ...captured },
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
} catch (err) {
|
|
105
|
+
console.warn("[job-pro] header capture err:", err);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
{ urls: ["<all_urls>"] },
|
|
109
|
+
["requestHeaders", "extraHeaders"]
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Expose a message API for the popup: "give me everything you have for
|
|
113
|
+
// the active tab", and a "clear" command.
|
|
114
|
+
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
|
115
|
+
(async () => {
|
|
116
|
+
if (msg?.type === "list_sessions") {
|
|
117
|
+
const all = await chrome.storage.local.get(null);
|
|
118
|
+
const out = Object.entries(all)
|
|
119
|
+
.filter(([k]) => k.startsWith("auth_headers:"))
|
|
120
|
+
.map(([k, v]) => ({ key: k.slice("auth_headers:".length), ...v }));
|
|
121
|
+
sendResponse({ ok: true, sessions: out });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (msg?.type === "export_session" && typeof msg.key === "string") {
|
|
125
|
+
const stored = (await chrome.storage.local.get([`auth_headers:${msg.key}`]))[
|
|
126
|
+
`auth_headers:${msg.key}`
|
|
127
|
+
];
|
|
128
|
+
if (!stored) {
|
|
129
|
+
sendResponse({ ok: false, message: `no session captured for ${msg.key}` });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const cookies = await chrome.cookies.getAll({ url: `https://${stored.host}/` });
|
|
133
|
+
const session = {
|
|
134
|
+
adapter: msg.key,
|
|
135
|
+
host: stored.host,
|
|
136
|
+
exported_at: new Date().toISOString(),
|
|
137
|
+
headers: stored.headers,
|
|
138
|
+
cookies: cookies.map((c) => ({
|
|
139
|
+
name: c.name,
|
|
140
|
+
value: c.value,
|
|
141
|
+
domain: c.domain,
|
|
142
|
+
path: c.path,
|
|
143
|
+
expiresAt: c.expirationDate,
|
|
144
|
+
httpOnly: c.httpOnly,
|
|
145
|
+
secure: c.secure,
|
|
146
|
+
sameSite: c.sameSite,
|
|
147
|
+
})),
|
|
148
|
+
};
|
|
149
|
+
const blob = new Blob([JSON.stringify(session, null, 2)], { type: "application/json" });
|
|
150
|
+
const dataUrl = await new Promise((resolve) => {
|
|
151
|
+
const reader = new FileReader();
|
|
152
|
+
reader.onloadend = () => resolve(reader.result);
|
|
153
|
+
reader.readAsDataURL(blob);
|
|
154
|
+
});
|
|
155
|
+
try {
|
|
156
|
+
const dlId = await chrome.downloads.download({
|
|
157
|
+
url: dataUrl,
|
|
158
|
+
filename: `jobpro/${msg.key}.session.json`,
|
|
159
|
+
saveAs: false,
|
|
160
|
+
});
|
|
161
|
+
sendResponse({ ok: true, downloadId: dlId, host: stored.host, cookieCount: cookies.length });
|
|
162
|
+
} catch (err) {
|
|
163
|
+
sendResponse({ ok: false, message: `download failed: ${err?.message ?? String(err)}` });
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (msg?.type === "clear_sessions") {
|
|
168
|
+
const all = await chrome.storage.local.get(null);
|
|
169
|
+
const keys = Object.keys(all).filter((k) => k.startsWith("auth_headers:"));
|
|
170
|
+
await chrome.storage.local.remove(keys);
|
|
171
|
+
sendResponse({ ok: true, cleared: keys.length });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
sendResponse({ ok: false, message: `unknown message type: ${msg?.type}` });
|
|
175
|
+
})().catch((err) => sendResponse({ ok: false, message: String(err) }));
|
|
176
|
+
return true; // async sendResponse
|
|
177
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "job-pro session bridge",
|
|
4
|
+
"short_name": "job-pro",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"description": "Capture careers-site session cookies + CSRF headers for use by the job-pro CLI auto-apply.",
|
|
7
|
+
"permissions": [
|
|
8
|
+
"cookies",
|
|
9
|
+
"storage",
|
|
10
|
+
"activeTab",
|
|
11
|
+
"scripting",
|
|
12
|
+
"downloads"
|
|
13
|
+
],
|
|
14
|
+
"host_permissions": [
|
|
15
|
+
"https://join.qq.com/*",
|
|
16
|
+
"https://jobs.bytedance.com/*",
|
|
17
|
+
"https://campus-talent.alibaba.com/*",
|
|
18
|
+
"https://zhaopin.meituan.com/*",
|
|
19
|
+
"https://job.xiaohongshu.com/*",
|
|
20
|
+
"https://campus.jd.com/*",
|
|
21
|
+
"https://campus.kuaishou.cn/*",
|
|
22
|
+
"https://xiaomi.jobs.f.mioffice.cn/*",
|
|
23
|
+
"https://talent.baidu.com/*",
|
|
24
|
+
"https://hr.163.com/*",
|
|
25
|
+
"https://talent.didiglobal.com/*",
|
|
26
|
+
"https://jobs.bilibili.com/*",
|
|
27
|
+
"https://careers.pinduoduo.com/*",
|
|
28
|
+
"https://career.huawei.com/*",
|
|
29
|
+
"https://campus.pingan.com/*",
|
|
30
|
+
"https://careers.ctrip.com/*",
|
|
31
|
+
"https://www.unitree.com/*",
|
|
32
|
+
"https://job.byd.com/*",
|
|
33
|
+
"https://talent.antgroup.com/*",
|
|
34
|
+
"https://hrcareersweb.antgroup.com/*",
|
|
35
|
+
"https://*.jobs.feishu.cn/*",
|
|
36
|
+
"https://*.zhiye.com/*",
|
|
37
|
+
"https://hr.sensetime.com/*",
|
|
38
|
+
"https://wecruit.hotjob.cn/*",
|
|
39
|
+
"https://app.mokahr.com/*",
|
|
40
|
+
"https://careers.oppo.com/*",
|
|
41
|
+
"https://hr.vivo.com/*",
|
|
42
|
+
"https://campus.sf-express.com/*",
|
|
43
|
+
"https://www.lixiang.com/*",
|
|
44
|
+
"https://lilithgames.jobs.feishu.cn/*",
|
|
45
|
+
"https://www.liepin.com/*"
|
|
46
|
+
],
|
|
47
|
+
"background": {
|
|
48
|
+
"service_worker": "background.js",
|
|
49
|
+
"type": "module"
|
|
50
|
+
},
|
|
51
|
+
"action": {
|
|
52
|
+
"default_popup": "popup.html",
|
|
53
|
+
"default_title": "job-pro session bridge"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>job-pro session bridge</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font: 13px/1.4 -apple-system, system-ui, sans-serif; margin: 12px; min-width: 320px; }
|
|
8
|
+
h1 { font-size: 14px; margin: 0 0 8px; }
|
|
9
|
+
p.lede { color: #555; margin: 0 0 12px; }
|
|
10
|
+
ul { margin: 0; padding: 0; list-style: none; }
|
|
11
|
+
li { display: flex; align-items: center; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #eee; }
|
|
12
|
+
li:last-child { border-bottom: 0; }
|
|
13
|
+
.key { font-weight: 600; }
|
|
14
|
+
.meta { color: #888; font-size: 11px; }
|
|
15
|
+
button { font: inherit; padding: 4px 8px; border: 1px solid #ccc; background: #f5f5f5; border-radius: 3px; cursor: pointer; }
|
|
16
|
+
button.danger { color: #c00; }
|
|
17
|
+
#status { margin-top: 12px; padding: 8px; background: #f5f9f0; border-left: 3px solid #5a5; font-size: 11px; white-space: pre-wrap; }
|
|
18
|
+
#status.error { background: #f9eef0; border-color: #c55; }
|
|
19
|
+
#empty { color: #888; padding: 16px 0; text-align: center; }
|
|
20
|
+
</style>
|
|
21
|
+
</head>
|
|
22
|
+
<body>
|
|
23
|
+
<h1>job-pro session bridge</h1>
|
|
24
|
+
<p class="lede">
|
|
25
|
+
Captured careers-site sessions. Click <strong>Export</strong> on a row
|
|
26
|
+
to download <code><adapter>.session.json</code>, then move it
|
|
27
|
+
into <code>~/.jobpro/</code> for the CLI's <code>apply</code> verb.
|
|
28
|
+
</p>
|
|
29
|
+
<ul id="sessions"></ul>
|
|
30
|
+
<div id="empty" hidden>No sessions captured yet. Browse a supported careers site (see <code>manifest.json</code>) while logged in, then re-open this popup.</div>
|
|
31
|
+
<div id="status" hidden></div>
|
|
32
|
+
<p style="margin-top:14px;text-align:right">
|
|
33
|
+
<button id="clear" class="danger">Clear all</button>
|
|
34
|
+
</p>
|
|
35
|
+
<script src="popup.js"></script>
|
|
36
|
+
</body>
|
|
37
|
+
</html>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const sessionsEl = document.getElementById("sessions");
|
|
2
|
+
const emptyEl = document.getElementById("empty");
|
|
3
|
+
const statusEl = document.getElementById("status");
|
|
4
|
+
const clearBtn = document.getElementById("clear");
|
|
5
|
+
|
|
6
|
+
function showStatus(text, isError) {
|
|
7
|
+
statusEl.textContent = text;
|
|
8
|
+
statusEl.hidden = false;
|
|
9
|
+
statusEl.classList.toggle("error", !!isError);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function send(msg) {
|
|
13
|
+
return new Promise((resolve) => chrome.runtime.sendMessage(msg, resolve));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function render() {
|
|
17
|
+
const r = await send({ type: "list_sessions" });
|
|
18
|
+
if (!r?.ok || !r.sessions?.length) {
|
|
19
|
+
sessionsEl.innerHTML = "";
|
|
20
|
+
emptyEl.hidden = false;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
emptyEl.hidden = true;
|
|
24
|
+
sessionsEl.innerHTML = "";
|
|
25
|
+
for (const s of r.sessions) {
|
|
26
|
+
const li = document.createElement("li");
|
|
27
|
+
const left = document.createElement("div");
|
|
28
|
+
const right = document.createElement("button");
|
|
29
|
+
left.innerHTML = `<span class="key">${s.key}</span> <span class="meta">${s.host} · ${s.captured_at?.slice(0, 19) ?? "?"}</span>`;
|
|
30
|
+
right.textContent = "Export";
|
|
31
|
+
right.addEventListener("click", async () => {
|
|
32
|
+
const exp = await send({ type: "export_session", key: s.key });
|
|
33
|
+
if (exp?.ok) {
|
|
34
|
+
showStatus(
|
|
35
|
+
`Saved jobpro/${s.key}.session.json — ${exp.cookieCount} cookies + headers from ${exp.host}.\n` +
|
|
36
|
+
`Move it into ~/.jobpro/${s.key}.session.json for the CLI.`
|
|
37
|
+
);
|
|
38
|
+
} else {
|
|
39
|
+
showStatus(`Export failed: ${exp?.message ?? "unknown error"}`, true);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
li.appendChild(left);
|
|
43
|
+
li.appendChild(right);
|
|
44
|
+
sessionsEl.appendChild(li);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
clearBtn.addEventListener("click", async () => {
|
|
49
|
+
const r = await send({ type: "clear_sessions" });
|
|
50
|
+
showStatus(r?.ok ? `Cleared ${r.cleared} session(s).` : `Failed: ${r?.message}`, !r?.ok);
|
|
51
|
+
render();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
render();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "job-pro",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
4
4
|
"description": "Query Chinese big-tech campus recruiting from your terminal. 50 companies, all 50 live. 46 via each company's own API; the 4 with no public canonical feed (Hikvision, CICC, Cainiao, WeBank) surfaced via Liepin as a clearly-labeled third-party fallback. No signup, no token, no server.",
|
|
5
5
|
"homepage": "https://job.ha7ch.com",
|
|
6
6
|
"repository": "https://github.com/HA7CH/job-pro",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"job-pro": "./dist/index.js"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
|
-
"dist"
|
|
13
|
+
"dist",
|
|
14
|
+
"extension"
|
|
14
15
|
],
|
|
15
16
|
"keywords": [
|
|
16
17
|
"campus-recruiting",
|
|
@@ -29,7 +30,7 @@
|
|
|
29
30
|
"dev": "tsx src/index.ts",
|
|
30
31
|
"test": "tsx test/smoke.ts",
|
|
31
32
|
"test:apply": "tsx test/apply-smoke.ts",
|
|
32
|
-
"prepublishOnly": "npm run build"
|
|
33
|
+
"prepublishOnly": "npm run build && rm -rf extension && cp -R ../extension extension"
|
|
33
34
|
},
|
|
34
35
|
"dependencies": {
|
|
35
36
|
"puppeteer-core": "^25.0.2"
|