diff --git a/docs/other.md b/docs/other.md
index b6842d9ff2..174456d1a5 100644
--- a/docs/other.md
+++ b/docs/other.md
@@ -163,6 +163,29 @@ pageClass: routes
该网站原始 RSS 数据源无人维护,故重新抓取数据并生成数据源。
+## LinkedIn 领英中国
+
+### Jobs
+
+
+
+另外,可以通过添加额外的以下 query 参数来输出满足特定要求的工作职位:
+
+| 参数 | 描述 | 举例 | 默认值 |
+| ---------- | -------------------------------- | ----------------------------------------- | ------- |
+| `geo` | geo 编码 | 102890883(中国)、102772228(上海)、103873152(北京) | 空 |
+| `remote` | 是否只显示远程工作 | `true/false` | `false` |
+| `location` | 工作地点 | `china/shanghai/beijing` | 空 |
+| `relevant` | 排序方式 (true: 按相关性排序,false: 按日期排序) | `true/false` | `false` |
+| `period` | 发布时间 | `1/7/30` | 空 |
+
+例如:
+[`/linkedin/cn/jobs/Software?location=shanghai&period=1`](https://rsshub.app/linkedin/cn/jobs/Software?location=shanghai\&period=1): 查找所有在上海的今日发布的所有 Software 工作
+
+**为了方便起见,建议您在 [LinkedIn.cn](https://www.linkedin.cn/incareer/jobs/search) 上进行搜索,并使用 [RSSHub Radar](https://github.com/DIYgod/RSSHub-Radar) 加载特定的 feed。**
+
+
+
## MiniFlux
因需设置 API Key,故请自行架设 RSSHub。API 密钥则请于 MiniFlux 实例中的 `设置` -> `API密钥` -> `创建一个新的API密钥` 处获取。
diff --git a/lib/v2/linkedin/cn/index.js b/lib/v2/linkedin/cn/index.js
new file mode 100644
index 0000000000..341c5f5d3b
--- /dev/null
+++ b/lib/v2/linkedin/cn/index.js
@@ -0,0 +1,13 @@
+const { parseSearchHit, parseJobPosting } = require('./utils');
+
+const siteUrl = 'https://www.linkedin.cn/incareer/jobs/search';
+
+module.exports = async (ctx) => {
+ const { title, jobs } = await parseSearchHit(ctx);
+ const items = await Promise.all(jobs.map((job) => parseJobPosting(job)));
+ ctx.state.data = {
+ title: `领英 - ${title}`,
+ link: siteUrl,
+ item: items,
+ };
+};
diff --git a/lib/v2/linkedin/cn/utils.js b/lib/v2/linkedin/cn/utils.js
new file mode 100644
index 0000000000..ec7f148cf5
--- /dev/null
+++ b/lib/v2/linkedin/cn/utils.js
@@ -0,0 +1,119 @@
+const crypto = require('crypto');
+const path = require('path');
+const { art } = require('@/utils/render');
+const got = require('@/utils/got');
+
+const apiUrl = 'https://www.linkedin.cn/karpos/api/graphql';
+const searchHitQueryId = 'searchSearchHitsByJob.be362cd720abd0ebf89b4bbc3253047f';
+const jobPostingQueryId = 'jobsJobPostingsById.3b9573e88687a86607ddb74ff013ef50';
+
+const makeHeader = () => {
+ const sessionId = crypto.randomBytes(8).toString('hex').slice(0, 8);
+ const headers = {
+ Accept: '*/*',
+ Cookie: `JSESSIONID="ajax:${sessionId}"`,
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ Referer: 'https://www.linkedin.cn/incareer/jobs/search',
+ 'csrf-token': `ajax:${sessionId}`,
+ 'x-http-method-override': 'GET',
+ 'x-restli-protocol-version': '2.0.0',
+ };
+ return headers;
+};
+
+const period = {
+ 1: 'r86400',
+ 7: 'r604800',
+ 30: 'r2592000',
+};
+
+const location = {
+ china: '102890883',
+ shanghai: '102772228',
+ beijing: '103873152',
+};
+
+const makeVariables = (variables) =>
+ '(' +
+ Object.entries(variables)
+ .filter(([, v]) => v)
+ .map(([k, v]) => `${k}:${encodeURIComponent(v)}`)
+ .join(',') +
+ ')';
+
+const makeBody = (body) => {
+ let output = '';
+ Object.entries(body).forEach(([key, value], index) => {
+ output += `${key}=${value}`;
+ if (index < Object.keys(body).length - 1) {
+ output += '&';
+ }
+ });
+ return output;
+};
+
+const ctxToTitle = (ctx) => {
+ const { keywords } = ctx.params;
+ const { geo, remote, location, period, relevant } = ctx.query;
+ const g = location || geo || 'China';
+ const r = remote ? '远程' : '';
+ const p = period ? `近${period}天` : '';
+ const o = relevant ? '相关' : '最新';
+ return `${o}${p}在${g}的${keywords || ''}${r}工作机会`;
+};
+
+const parseSearchHit = async (ctx) => {
+ const variables = {
+ origin: 'jserp',
+ isForRemoteJobsPage: !!ctx.query.remote,
+ isChinaMultiNationalCorporation: false,
+ count: ctx.query.limit || 20,
+ start: '0',
+ geoUrn: `urn:li:ks_geo:${location[ctx.query.location] || ctx.query.geo || location.china}`,
+ keywords: decodeURIComponent(ctx.params.keywords || ''),
+ f_TPR: period[ctx.query.period] || '',
+ sortType: ctx.query.relevant ? '' : 'DATE_DESCENDING',
+ };
+
+ const resp = await got.post(apiUrl, {
+ headers: makeHeader(),
+ body: makeBody({
+ operationName: 'searchSearchHitsByJob',
+ variables: makeVariables(variables),
+ queryId: searchHitQueryId,
+ }),
+ });
+
+ return {
+ jobs: resp.data.data.searchSearchHitsByJob.elements.map((e) => e.target.jobPosting),
+ title: ctxToTitle(ctx),
+ };
+};
+
+const parseJobPosting = async (jobPosting) => {
+ const entityUrn = jobPosting.entityUrn;
+ const variables = {
+ jobPostingUrn: entityUrn,
+ };
+ const resp = await got.post(apiUrl, {
+ headers: makeHeader(),
+ body: makeBody({
+ operationName: 'jobViewPage',
+ variables: makeVariables(variables),
+ queryId: jobPostingQueryId,
+ }),
+ });
+ const job = resp.data.data.jobsJobPostingsById;
+ return {
+ title: `${jobPosting.companyName} 正在找 ${jobPosting.title}`,
+ link: `https://www.linkedin.cn/incareer/jobs/view/${entityUrn.split(':').pop()}`,
+ guid: `linkedincn:${entityUrn}`,
+ description: art(path.join(__dirname, '../templates/cn/posting.art'), job),
+ pubDate: jobPosting.listedAt,
+ };
+};
+
+module.exports = {
+ parseSearchHit,
+ parseJobPosting,
+};
diff --git a/lib/v2/linkedin/maintainer.js b/lib/v2/linkedin/maintainer.js
index 6a246d058d..424a4a60e9 100644
--- a/lib/v2/linkedin/maintainer.js
+++ b/lib/v2/linkedin/maintainer.js
@@ -1,3 +1,4 @@
module.exports = {
+ '/cn/jobs/:keywords?': ['bigfei'],
'/jobs': ['BrandNewLifeJackie26'],
};
diff --git a/lib/v2/linkedin/radar.js b/lib/v2/linkedin/radar.js
index 9183332ff4..6422b73ab4 100644
--- a/lib/v2/linkedin/radar.js
+++ b/lib/v2/linkedin/radar.js
@@ -1,6 +1,6 @@
module.exports = {
'linkedin.com': {
- _name: 'linkedin',
+ _name: 'LinkedIn',
'.': [
{
title: 'Job Listing',
@@ -19,4 +19,18 @@ module.exports = {
},
],
},
+ 'linkedin.cn': {
+ _name: 'LinkedIn 领英中国',
+ '.': [
+ {
+ title: 'Jobs',
+ docs: 'https://docs.rsshub.app/other.html#linkedin-ling-ying-zhong-guo',
+ source: '/incareer/jobs/search',
+ target: (params, url) => {
+ const searchParams = new URL(url).searchParams;
+ return `/linkedin/cn/jobs/${searchParams.get('keywords') || ''}`;
+ },
+ },
+ ],
+ },
};
diff --git a/lib/v2/linkedin/router.js b/lib/v2/linkedin/router.js
index 78eecc8d28..a40b719708 100644
--- a/lib/v2/linkedin/router.js
+++ b/lib/v2/linkedin/router.js
@@ -1,3 +1,4 @@
module.exports = function (router) {
+ router.get('/cn/jobs/:keywords?', require('./cn/index.js'));
router.get('/jobs/:job_types/:exp_levels/:keywords?', require('./jobs.js'));
};
diff --git a/lib/v2/linkedin/templates/cn/posting.art b/lib/v2/linkedin/templates/cn/posting.art
new file mode 100644
index 0000000000..e406bfa998
--- /dev/null
+++ b/lib/v2/linkedin/templates/cn/posting.art
@@ -0,0 +1,25 @@
+
{{ title }}
+
+ {{ if (applyMethod.instantOffsiteApply)}}
+ 点击申请
+ {{ /if }}
+
+
+ {{ if (applyMethod.basicOffsiteApply)}}
+ 点击申请
+ {{ /if }}
+
+已有{{numApplies}}人申请此职位, {{numViews}}人查看此职位
+{{ if(compensationDescription) }}
+薪资:{{ compensationDescription || 'N/A' }}
+{{ /if }}
+工作地点: {{ geo.defaultLocalizedName }}
+
+{{ if (company) }}
+公司介绍
+{{company.name}}
+员工人数:{{company.employeeCount}}
+{{company.localizedDescription}}
+{{ /if }}
+职位介绍
+{{description.text}}