feat: add sinafinance 7x24 news adapter (#131)
Based on PR #131 by larria, with fixes applied: - Renamed command 724 → news for clarity - Fixed indentation to 2-space project standard - Added SinaNewsItem type (removed item: any) - Added res.ok check + CliError for HTTP failures - Added stripHtml() for rich_text content - Updated README, README.zh-CN, SKILL.md Co-authored-by: larria <1115524+larria@users.noreply.github.com>
This commit is contained in:
1 parent
03f067d907
commit
02793e990e
5 files changed
+107
-4
No files matched your search
@@ -145,6 +145,7 @@ Run `opencli list` for the live registry.
|
||||
| **smzdm** | `search` | Browser |
|
||||
| **weibo** | `hot` | Browser |
|
||||
| **yahoo-finance** | `quote` | Browser |
|
||||
| **sinafinance** | `news` | 🌐 Public |
|
||||
|
||||
### Desktop App Adapters
|
||||
|
||||
|
||||
@@ -146,6 +146,7 @@ npm install -g @jackwener/opencli@latest
|
||||
| **smzdm** | `search` | 浏览器 |
|
||||
| **weibo** | `hot` | 浏览器 |
|
||||
| **yahoo-finance** | `quote` | 浏览器 |
|
||||
| **sinafinance** | `news` | 🌐 公开 |
|
||||
|
||||
### 桌面应用适配器
|
||||
|
||||
|
||||
@@ -143,6 +143,9 @@ opencli youtube transcript --url "xxx" --lang zh-Hans --mode raw # 指定语言
|
||||
# Yahoo Finance (browser)
|
||||
opencli yahoo-finance quote --symbol AAPL # 股票行情
|
||||
|
||||
# Sina Finance
|
||||
opencli sinafinance news --limit 10 --type 1 # 7x24实时快讯 (0=全部 1=A股 2=宏观 3=公司 4=数据 5=市场 6=国际 7=观点 8=央行 9=其它)
|
||||
|
||||
# Reuters (browser)
|
||||
opencli reuters search --query "AI" # 路透社搜索
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ async function getConversationText(page: IPage): Promise<string> {
|
||||
return String(text ?? '');
|
||||
}
|
||||
|
||||
async function sendMessage(page: IPage, message: string): Promise<void> {
|
||||
async function sendMessage(page: IPage, message: string, bridge?: CDPBridge): Promise<void> {
|
||||
await page.evaluate(`
|
||||
(async () => {
|
||||
const container = document.getElementById('antigravity.agentSidePanelInputBox');
|
||||
@@ -110,7 +110,28 @@ async function sendMessage(page: IPage, message: string): Promise<void> {
|
||||
})()
|
||||
`);
|
||||
await sleep(500);
|
||||
await page.pressKey('Enter');
|
||||
|
||||
// Use CDP-level key event (native) instead of JS KeyboardEvent (synthetic).
|
||||
// Lexical/React ignores synthetic KeyboardEvents but responds to native ones.
|
||||
if (bridge) {
|
||||
await bridge.send('Input.dispatchKeyEvent', {
|
||||
type: 'keyDown',
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
windowsVirtualKeyCode: 13,
|
||||
nativeVirtualKeyCode: 13,
|
||||
});
|
||||
await sleep(50);
|
||||
await bridge.send('Input.dispatchKeyEvent', {
|
||||
type: 'keyUp',
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
windowsVirtualKeyCode: 13,
|
||||
nativeVirtualKeyCode: 13,
|
||||
});
|
||||
} else {
|
||||
await page.pressKey('Enter');
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForReply(
|
||||
@@ -196,6 +217,7 @@ function extractLastReply(text: string): string {
|
||||
async function handleMessages(
|
||||
body: AnthropicRequest,
|
||||
page: IPage,
|
||||
bridge?: CDPBridge,
|
||||
): Promise<AnthropicResponse> {
|
||||
// Extract the last user message
|
||||
const userMessages = body.messages.filter(m => m.role === 'user');
|
||||
@@ -214,7 +236,7 @@ async function handleMessages(
|
||||
|
||||
// Send the message
|
||||
console.error(`[serve] Sending: "${userText.slice(0, 80)}${userText.length > 80 ? '...' : ''}"`);
|
||||
await sendMessage(page, userText);
|
||||
await sendMessage(page, userText, bridge);
|
||||
|
||||
// Poll for reply
|
||||
console.error('[serve] Waiting for reply...');
|
||||
@@ -372,7 +394,7 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
|
||||
|
||||
// Lazy connect on first request
|
||||
const activePage = await ensureConnected();
|
||||
const response = await handleMessages(body, activePage);
|
||||
const response = await handleMessages(body, activePage, cdp ?? undefined);
|
||||
jsonResponse(res, 200, response);
|
||||
} finally {
|
||||
requestInFlight = false;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Sina Finance 7x24 live news feed.
|
||||
*
|
||||
* Uses the public CJ API — no key or browser required.
|
||||
* https://app.cj.sina.com.cn/api/news/pc
|
||||
*/
|
||||
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import { CliError } from '../../errors.js';
|
||||
|
||||
// User-facing type (0-9) → Sina API tag ID
|
||||
const TYPE_MAP = [
|
||||
0, // 0: 全部
|
||||
10, // 1: A股
|
||||
1, // 2: 宏观
|
||||
3, // 3: 公司
|
||||
4, // 4: 数据
|
||||
5, // 5: 市场
|
||||
102, // 6: 国际
|
||||
6, // 7: 观点
|
||||
6, // 8: 央行
|
||||
8, // 9: 其它
|
||||
] as const;
|
||||
|
||||
interface SinaNewsItem {
|
||||
id?: string;
|
||||
create_time?: string;
|
||||
rich_text?: string;
|
||||
view_num?: number;
|
||||
}
|
||||
|
||||
function stripHtml(html: string): string {
|
||||
return html.replace(/<[^>]+>/g, '').trim();
|
||||
}
|
||||
|
||||
cli({
|
||||
site: 'sinafinance',
|
||||
name: 'news',
|
||||
description: '新浪财经 7x24 小时实时快讯',
|
||||
domain: 'app.cj.sina.com.cn',
|
||||
strategy: Strategy.PUBLIC,
|
||||
browser: false,
|
||||
args: [
|
||||
{ name: 'limit', type: 'int', default: 20, help: 'Max results (max 50)' },
|
||||
{ name: 'type', type: 'int', default: 0, help: 'News type: 0=全部 1=A股 2=宏观 3=公司 4=数据 5=市场 6=国际 7=观点 8=央行 9=其它' },
|
||||
],
|
||||
columns: ['id', 'time', 'content', 'views'],
|
||||
func: async (_page, args) => {
|
||||
const limit = Math.max(1, Math.min(Number(args.limit), 50));
|
||||
const apiTag = TYPE_MAP[args.type as number] ?? 0;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
page: '1',
|
||||
size: String(limit),
|
||||
tag: String(apiTag),
|
||||
});
|
||||
|
||||
const res = await fetch(`https://app.cj.sina.com.cn/api/news/pc?${params}`);
|
||||
if (!res.ok) {
|
||||
throw new CliError('FETCH_ERROR', `Sina Finance API HTTP ${res.status}`, 'Check your network connection');
|
||||
}
|
||||
const json = await res.json() as { result?: { data?: { feed?: { list?: SinaNewsItem[] } } } };
|
||||
const list = json?.result?.data?.feed?.list ?? [];
|
||||
|
||||
if (!list.length) {
|
||||
throw new CliError('NOT_FOUND', 'No news found', 'Try a different type or increase limit');
|
||||
}
|
||||
|
||||
return list.map((item) => ({
|
||||
id: item.id ?? '',
|
||||
time: item.create_time ?? '',
|
||||
content: stripHtml(item.rich_text ?? ''),
|
||||
views: item.view_num ?? 0,
|
||||
}));
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user