mirror of
https://github.com/jackwener/OpenCLI.git
synced 2026-09-14 18:25:42 +08:00
d2974a9ff6
* refactor(adapters): convert adapter layer from TypeScript to JavaScript Core framework stays TypeScript; adapter layer moves to JS-first. Adapters are essentially "executable config + browser scripts" that barely use TS features — this simplifies the build/distribution pipeline by removing the dist/clis/ intermediate compilation step. Changes: - Convert all 753 adapter files in clis/ from .ts to .js - Update tsconfig to exclude clis/ from compilation - Simplify build-manifest to scan clis/*.js directly (no dist/clis/) - Update discovery, main, fetch-adapters to load JS adapters from clis/ - Update generate-verified to output .js artifacts - Update package.json files field: dist/clis/ → clis/ - Fix all test files for the .ts → .js transition * fix(main): use findPackageRoot for BUILTIN_CLIS path The previous relative path (../../clis from __dirname) only worked for dist/src/main.js but broke dev mode (tsx src/main.ts) where __dirname is <repo>/src — resolving to /clis instead of <repo>/clis. Use findPackageRoot() which works for both dev and prod paths.
169 lines
6.2 KiB
JavaScript
169 lines
6.2 KiB
JavaScript
import { cli, Strategy } from '@jackwener/opencli/registry';
|
||
// 将通知类型代码映射为中文标签
|
||
function resolveActionLabel(type) {
|
||
if (!type)
|
||
return '通知';
|
||
const upper = type.toUpperCase();
|
||
if (upper.includes('LIKE'))
|
||
return '赞了你';
|
||
if (upper.includes('COMMENT'))
|
||
return '评论了你';
|
||
if (upper.includes('FOLLOW'))
|
||
return '关注了你';
|
||
if (upper.includes('REPOST'))
|
||
return '转发了你';
|
||
if (upper.includes('MENTION'))
|
||
return '提到了你';
|
||
if (upper.includes('REPLY'))
|
||
return '回复了你';
|
||
return type;
|
||
}
|
||
cli({
|
||
site: 'jike',
|
||
name: 'notifications',
|
||
description: '即刻通知',
|
||
domain: 'web.okjike.com',
|
||
strategy: Strategy.COOKIE,
|
||
browser: true,
|
||
args: [
|
||
{ name: 'limit', type: 'int', default: 20 },
|
||
],
|
||
columns: ['type', 'user', 'content', 'time'],
|
||
func: async (page, kwargs) => {
|
||
const limit = kwargs.limit || 20;
|
||
// 1. 直接导航到通知页
|
||
await page.goto('https://web.okjike.com/notification');
|
||
// 3. 优先用 React fiber 提取通知数据
|
||
// 通知 fiber 数据结构与帖子不同,需查找含 type + user 字段的 props
|
||
const fiberResults = (await page.evaluate(`(() => {
|
||
// 从 React fiber 树中提取通知数据,向上最多走 15 层
|
||
function getNotificationData(element) {
|
||
for (const key of Object.keys(element)) {
|
||
if (key.startsWith('__reactFiber$') || key.startsWith('__reactInternalInstance$')) {
|
||
let fiber = element[key];
|
||
for (let i = 0; i < 15 && fiber; i++) {
|
||
const props = fiber.memoizedProps || fiber.pendingProps;
|
||
if (props && props.data) {
|
||
const d = props.data;
|
||
// 通知条目特征:含 type/actionType 字段,以及来源用户字段
|
||
if (d.type || d.actionType || d.notificationType) return d;
|
||
}
|
||
fiber = fiber.return;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
const results = [];
|
||
const seen = new Set();
|
||
|
||
// 通知页使用 _item_ 类前缀作为条目容器
|
||
const elements = Array.from(document.querySelectorAll('[class*="_item_"]'));
|
||
|
||
for (const el of elements) {
|
||
const data = getNotificationData(el);
|
||
if (!data) continue;
|
||
|
||
const actionType = data.actionType || data.type || data.notificationType || '';
|
||
const fromUser =
|
||
(data.sourceUser && data.sourceUser.screenName) ||
|
||
(data.user && data.user.screenName) ||
|
||
(data.actionUser && data.actionUser.screenName) ||
|
||
(data.actor && data.actor.screenName) ||
|
||
'';
|
||
const targetContent =
|
||
(data.targetPost && data.targetPost.content) ||
|
||
(data.post && data.post.content) ||
|
||
'';
|
||
const commentContent =
|
||
(data.comment && data.comment.content) ||
|
||
data.commentContent ||
|
||
'';
|
||
const content = commentContent || targetContent;
|
||
const time = data.createdAt || data.updatedAt || '';
|
||
|
||
// 用 user+time+type 去重,避免同一用户同一时间不同类型通知被合并
|
||
const key = fromUser + '\x00' + time + '\x00' + (data.type || data.actionType || '');
|
||
if (seen.has(key)) continue;
|
||
seen.add(key);
|
||
|
||
if (!fromUser && !content) continue;
|
||
|
||
results.push({ actionType, fromUser, content, time });
|
||
}
|
||
|
||
return results;
|
||
})()`));
|
||
// 4. fiber 提取成功,映射类型标签后返回
|
||
if (fiberResults.length > 0) {
|
||
const notifications = fiberResults.map((r) => ({
|
||
type: resolveActionLabel(r.actionType),
|
||
user: r.fromUser,
|
||
content: r.content.replace(/\n/g, ' ').slice(0, 100),
|
||
time: r.time,
|
||
}));
|
||
return notifications.slice(0, limit);
|
||
}
|
||
// 5. 回退:解析通知条目的 innerText(格式: 用户名\n操作描述\n日期)
|
||
await page.autoScroll({ times: Math.ceil(limit / 10), delayMs: 2000 });
|
||
const domResults = (await page.evaluate(`(() => {
|
||
const results = [];
|
||
const items = document.querySelectorAll('[class*="_item_"]');
|
||
|
||
// 动作关键词,用于定位通知类型行
|
||
// 长模式优先匹配,避免"赞了你"截断"赞了你的动态"
|
||
const actionPatterns = [
|
||
'赞了你的动态', '赞了你的评论', '赞了你的转发',
|
||
'评论了你的动态', '评论了你的转发',
|
||
'回复了你的评论', '回复了你',
|
||
'转发了你的动态',
|
||
'提到了你', '关注了你',
|
||
'赞了你', '评论了你', '转发了你',
|
||
];
|
||
|
||
for (const item of items) {
|
||
const text = item.innerText || '';
|
||
const lines = text.split('\\n').map(l => l.trim()).filter(Boolean);
|
||
if (lines.length < 2) continue;
|
||
|
||
// 策略:在全文中搜索动作关键词来定位类型
|
||
let type = '';
|
||
let user = '';
|
||
let time = '';
|
||
let content = '';
|
||
|
||
const fullText = lines.join(' ');
|
||
for (const pattern of actionPatterns) {
|
||
const idx = fullText.indexOf(pattern);
|
||
if (idx >= 0) {
|
||
// 关键词前面是用户名(可能多人用、分隔)
|
||
user = fullText.slice(0, idx).replace(/[、,]/g, ' ').trim();
|
||
type = pattern;
|
||
// 关键词后面可能是时间和原帖内容
|
||
const rest = fullText.slice(idx + pattern.length).trim();
|
||
// 时间通常以数字或"刚刚"/"分钟前"等开头
|
||
const timeMatch = rest.match(/^[\\S]*?(?:刚刚|\\d+.*?前|\\d{4}\\/\\d{2}\\/\\d{2})/);
|
||
time = timeMatch ? timeMatch[0] : '';
|
||
content = rest.slice(time.length).trim().slice(0, 100);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 没匹配到动作关键词,回退到简单行序
|
||
if (!type) {
|
||
user = lines[0] || '';
|
||
type = lines[1] || '';
|
||
time = lines[2] || '';
|
||
}
|
||
|
||
if (!user && !type) continue;
|
||
results.push({ type, user, content, time });
|
||
}
|
||
|
||
return results;
|
||
})()`));
|
||
return domResults.slice(0, limit);
|
||
},
|
||
});
|