Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions src/clis/cnki/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { cli, Strategy } from '../../registry.js';

cli({
site: 'cnki',
name: 'search',
description: '中国知网论文搜索(海外版)',
domain: 'oversea.cnki.net',
strategy: Strategy.COOKIE,
args: [
{ name: 'query', positional: true, required: true, help: '搜索关键词' },
{ name: 'limit', type: 'int', default: 10, help: '返回结果数量 (max 20)' },
],
columns: ['rank', 'title', 'authors', 'journal', 'date', 'url'],
navigateBefore: false,
func: async (page, kwargs) => {
const limit = Math.min(kwargs.limit || 10, 20);
const query = encodeURIComponent(kwargs.query);

await page.goto(`https://oversea.cnki.net/kns/search?dbcode=CFLS&kw=${query}&korder=SU`);
await page.wait(8);

const data = await page.evaluate(`
(async () => {
const normalize = v => (v || '').replace(/\\s+/g, ' ').trim();
for (let i = 0; i < 40; i++) {
if (document.querySelector('.result-table-list tbody tr, #gridTable tbody tr')) break;
await new Promise(r => setTimeout(r, 500));
}
const rows = document.querySelectorAll('.result-table-list tbody tr, #gridTable tbody tr');
const results = [];
for (const row of rows) {
// CNKI table columns: checkbox | seq | title | authors | journal | date | source_db
const tds = row.querySelectorAll('td');
if (tds.length < 5) continue;

// Find the title — it's in td.name or the td with an <a> linking to article
const nameCell = row.querySelector('td.name') || tds[2];
const titleEl = nameCell?.querySelector('a');
const title = normalize(titleEl?.textContent).replace(/免费$/, '');
if (!title) continue;

let url = titleEl?.getAttribute('href') || '';
if (url && !url.startsWith('http')) url = 'https://oversea.cnki.net' + url;

// Authors and journal: find by class or positional
const authorCell = row.querySelector('td.author') || tds[3];
const journalCell = row.querySelector('td.source') || tds[4];
const dateCell = row.querySelector('td.date') || tds[5];

const authors = normalize(authorCell?.textContent);
const journal = normalize(journalCell?.textContent);
const date = normalize(dateCell?.textContent);

results.push({ rank: results.length + 1, title, authors, journal, date, url });
if (results.length >= ${limit}) break;
}
return results;
})()
`);
return Array.isArray(data) ? data : [];
},
});
64 changes: 64 additions & 0 deletions src/clis/jd/add-cart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { cli, Strategy } from '../../registry.js';

cli({
site: 'jd',
name: 'add-cart',
description: '京东加入购物车',
domain: 'item.jd.com',
strategy: Strategy.COOKIE,
args: [
{ name: 'sku', positional: true, required: true, help: '商品 SKU ID' },
{ name: 'num', type: 'int', default: 1, help: '数量' },
],
columns: ['status', 'title', 'price', 'sku'],
navigateBefore: false,
func: async (page, kwargs) => {
const sku = kwargs.sku;
const num = kwargs.num || 1;

await page.goto(`https://item.jd.com/${sku}.html`);
await page.wait(4);

// Get product info
const info = await page.evaluate(`
(() => {
const text = document.body?.innerText || '';
const titleMatch = document.title.match(/^【[^】]*】(.+?)【/);
const title = titleMatch ? titleMatch[1].trim() : document.title.split('-')[0].trim();
const priceMatch = text.match(/¥([\\d,.]+)/);
const price = priceMatch ? '¥' + priceMatch[1] : '';
return { title, price };
})()
`);

// Navigate to cart domain and use gate.action to add item
await page.goto(`https://cart.jd.com/gate.action?pid=${sku}&pcount=${num}&ptype=1`);
await page.wait(4);

const result = await page.evaluate(`
(() => {
const url = location.href;
const text = document.body?.innerText || '';
if (text.includes('已成功加入') || text.includes('商品已成功') || url.includes('addtocart')) {
return 'success';
}
if (text.includes('请登录') || text.includes('login') || url.includes('login')) {
return 'login_required';
}
return 'page:' + url.substring(0, 60) + ' | ' + text.substring(0, 100);
})()
`);

let status = '? 未知';
if (result === 'success') status = '✓ 已加入购物车';
else if (result === 'login_required') status = '✗ 需要登录京东';
else status = '? ' + result;

return [{
status,
title: (info?.title || '').slice(0, 80),
price: info?.price || '',
sku,
}];
},
});
76 changes: 76 additions & 0 deletions src/clis/jd/cart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { cli, Strategy } from '../../registry.js';

cli({
site: 'jd',
name: 'cart',
description: '查看京东购物车',
domain: 'cart.jd.com',
strategy: Strategy.COOKIE,
args: [],
columns: ['index', 'title', 'price', 'quantity', 'sku'],
navigateBefore: false,
func: async (page) => {
await page.goto('https://cart.jd.com/cart_index');
await page.wait(5);

const data = await page.evaluate(`
(async () => {
const normalize = v => (v || '').replace(/\\s+/g, ' ').trim();
for (let i = 0; i < 20; i++) {
if (document.body?.innerText?.length > 500) break;
await new Promise(r => setTimeout(r, 500));
}
const text = document.body?.innerText || '';

// Try API approach: fetch cart data via JD's cart API
try {
const resp = await fetch('https://api.m.jd.com/api?appid=JDC_mall_cart&functionId=pcCart_jc_getCurrentCart&body=%7B%22serInfo%22%3A%7B%22area%22%3A%2222_1930_50948_52157%22%7D%7D', {
credentials: 'include',
headers: { 'referer': 'https://cart.jd.com/' },
});
const json = await resp.json();
const cartData = json?.resultData?.cartInfo?.vendors || [];
const items = [];
for (const vendor of cartData) {
const sorted = vendor.sorted || [];
for (const item of sorted) {
const product = item.item || item;
if (!product.Id && !product.skuId) continue;
items.push({
index: items.length + 1,
title: normalize(product.name || product.Name || '').slice(0, 80),
price: product.price ? '¥' + product.price : '',
quantity: String(product.num || product.Num || 1),
sku: String(product.Id || product.skuId || ''),
});
}
}
if (items.length > 0) return items;
} catch {}

// Fallback: parse from page text
const lines = text.split('\\n').map(l => l.trim()).filter(Boolean);
const items = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const priceMatch = line.match(/¥([\\d,.]+)/);
if (priceMatch && i > 0) {
// Previous line might be the product title
const title = lines[i-1];
if (title && title.length > 5 && title.length < 200 && !title.startsWith('¥')) {
items.push({
index: items.length + 1,
title: title.slice(0, 80),
price: '¥' + priceMatch[1],
quantity: '',
sku: '',
});
}
}
}
return items;
})()
`);
return Array.isArray(data) ? data : [];
},
});
67 changes: 67 additions & 0 deletions src/clis/jd/detail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { cli, Strategy } from '../../registry.js';

cli({
site: 'jd',
name: 'detail',
description: '京东商品详情',
domain: 'item.jd.com',
strategy: Strategy.COOKIE,
args: [
{ name: 'sku', positional: true, required: true, help: '商品 SKU ID' },
],
columns: ['field', 'value'],
navigateBefore: false,
func: async (page, kwargs) => {
await page.goto(`https://item.jd.com/${kwargs.sku}.html`);
await page.wait(5);

const data = await page.evaluate(`
(() => {
const normalize = v => (v || '').replace(/\\s+/g, ' ').trim();
const text = document.body?.innerText || '';

// Title from <title> tag
const titleMatch = document.title.match(/^【[^】]*】(.+?)【/);
const title = titleMatch ? titleMatch[1].trim() : normalize(document.title.split('【')[0]);

// Price
const priceMatch = text.match(/¥([\\d,.]+)/);
const price = priceMatch ? '¥' + priceMatch[1] : '';

// Rating summary - find "超XX%买家赞不绝口" or similar
const ratingMatch = text.match(/(超\\d+%[^\\n]{2,20})/);
const rating = ratingMatch ? ratingMatch[1] : '';

// Total reviews
const reviewMatch = text.match(/买家评价\\(([\\d万+]+)\\)/);
const reviews = reviewMatch ? reviewMatch[1] : '';

// Shop
const shopMatch = text.match(/(\\S{2,15}(?:京东自营旗舰店|旗舰店|专卖店|自营店))/);
const shop = shopMatch ? shopMatch[1] : '';

// Tags - extract "触感超舒适 163" patterns
const tagPattern = /([\u4e00-\u9fa5]{2,8})\\s+(\\d+)/g;
const tags = [];
let m;
const tagSection = text.substring(text.indexOf('买家评价'), text.indexOf('买家评价') + 500);
while ((m = tagPattern.exec(tagSection)) && tags.length < 6) {
if (parseInt(m[2]) > 1) tags.push(m[1] + '(' + m[2] + ')');
}

const results = [
{ field: '商品名称', value: title },
{ field: '价格', value: price },
{ field: 'SKU', value: '${kwargs.sku}' },
{ field: '店铺', value: shop },
{ field: '评价数量', value: reviews },
{ field: '好评率', value: rating },
{ field: '评价标签', value: tags.join(' | ') },
{ field: '链接', value: location.href },
];
return results.filter(r => r.value);
})()
`);
return Array.isArray(data) ? data : [];
},
});
64 changes: 64 additions & 0 deletions src/clis/jd/reviews.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { cli, Strategy } from '../../registry.js';

cli({
site: 'jd',
name: 'reviews',
description: '京东商品评价',
domain: 'item.jd.com',
strategy: Strategy.COOKIE,
args: [
{ name: 'sku', positional: true, required: true, help: '商品 SKU ID' },
{ name: 'limit', type: 'int', default: 10, help: '返回评价数量 (max 20)' },
],
columns: ['rank', 'user', 'content', 'date'],
navigateBefore: false,
func: async (page, kwargs) => {
const limit = Math.min(kwargs.limit || 10, 20);
await page.goto(`https://item.jd.com/${kwargs.sku}.html`);
await page.wait(5);
// Scroll to load reviews section
await page.autoScroll({ times: 2, delayMs: 1500 });

const data = await page.evaluate(`
(async () => {
const normalize = v => (v || '').replace(/\\s+/g, ' ').trim();
const text = document.body?.innerText || '';

// JD new version: reviews are inline in page text
// Pattern: username \\n review_text \\n [date or next username]
// Find the review section after "买家评价"
const reviewStart = text.indexOf('买家评价');
const reviewEnd = text.indexOf('全部评价');
if (reviewStart < 0) return [];

const reviewSection = text.substring(reviewStart, reviewEnd > reviewStart ? reviewEnd : reviewStart + 3000);
const lines = reviewSection.split('\\n').map(l => l.trim()).filter(Boolean);

const results = [];
// Skip header lines, look for user-review pairs
// Users are like "c***4", "3***a", "A***7" or "jd_xxx"
// JD usernames contain * (masked), like "c***4", "3***a", "jd_xxx"
const userPattern = /^[a-zA-Z0-9*_]{3,15}$/;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (userPattern.test(line) && line.includes('*') && i + 1 < lines.length) {
const user = line;
const content = lines[i + 1];
// Skip if content looks like a header/tag
if (content.length < 5 || content.match(/^(全部评价|问大家|查看更多)/)) continue;
results.push({
rank: results.length + 1,
user,
content: content.slice(0, 150),
date: '',
});
i++; // skip the content line
if (results.length >= ${limit}) break;
}
}
return results;
})()
`);
return Array.isArray(data) ? data : [];
},
});
Loading