@clawhub-jiuwei2-006916483b
Route natural-language requests about today's news, market news, TradeAlpha news, or TradeAlpha login into the bundled TradeAlpha plugin tools. Prefer the si...
---
name: tradealpha-open-platform
description: Route natural-language requests about today's news, market news, TradeAlpha news, or TradeAlpha login into the bundled TradeAlpha plugin tools. Prefer the single router tool `tradealpha_open_platform`, and only fall back to helper tools when needed.
homepage: https://quantaccess.lxaa.top
version: 0.4.0
metadata:
{
"openclaw":
{
"emoji": "📰",
"requires": { "bins": ["node"] },
"primaryEnv": "TRADEALPHA_API_KEY",
},
}
---
# TradeAlpha开放平台
TradeAlpha开放平台:路透、彭博、川普 Truth、国内主流消息源,一网打尽。
TradeAlpha Open Platform: Reuters, Bloomberg, Trump's Truth Social, and major Chinese news sources, all in one place.
这个 skill 只负责自然语言召回和登录门控,真正执行依赖同名插件里的真实工具。不要把 `tradealpha-open-platform` 当成 tool 名调用;应优先调用插件总入口 `tradealpha_open_platform`。
## First Rule
每次用户想使用 TradeAlpha 新闻能力时,都遵守下面的固定顺序:
1. 用户想登录、获取 token、刷新 token 时,先向用户索要账号和密码
2. 调用 `tradealpha_open_platform`,并传 `intent: "login"`
3. 用户想拉新闻时,优先调用 `tradealpha_open_platform`
4. 如果 `tradealpha_open_platform` 返回 `auth_required: true`
5. 立即向用户索要账号和密码
6. 再次调用 `tradealpha_open_platform`,补上账号和密码
7. 登录成功后再重试新闻请求
如果用户提到以下任一意图,应优先触发本技能:
- 今天的新闻
- 今日新闻
- 现在的新闻
- 市场新闻
- 宏观新闻
- 路透新闻
- 彭博新闻
- Truth 新闻
- 国内新闻快讯
- 登录 TradeAlpha
- 获取 token
- 初始化或刷新 token
- 配置 TradeAlpha 权限
- 拉取实时新闻
- 按来源、分类、重要程度筛选新闻
不要说“没有 tradealpha 这个工具”。当前应优先使用的真实工具是:
- `tradealpha_open_platform`
- `tradealpha_login`(辅助)
- `tradealpha_news`(辅助)
- `tradealpha_realtime_news`(兼容别名,优先仍用 `tradealpha_news`)
## When To Use
在这些场景使用本技能:
- 用户直接说“我要今天的新闻”“帮我拉今天新闻”
- 用户直接说“帮我看市场新闻”“帮我拉彭博/路透新闻”
- 用户要先登录或初始化 token
- 用户要更新、刷新、重新获取 token
- 用户要抓取实时新闻
- 用户要按来源、重要程度、分类筛选新闻
- 用户要对比彭博、路透、Truth、国内源口径
- 用户要获取近 24 小时或指定时间段内的市场新闻
## Routing Rules
### 登录场景
如果用户要登录、初始化 token、刷新 token:
1. 向用户索要 `account` 和 `password`
2. 调用 `tradealpha_open_platform`,传 `intent: "login"`、`account`、`password`
3. 登录成功后再继续后续新闻请求
### 拉新闻场景
如果用户要新闻:
1. 直接调用 `tradealpha_open_platform`
2. 如果返回 `auth_required: false`,继续整理新闻结果
3. 如果返回 `auth_required: true`,向用户索要 `account` 和 `password`
4. 再次调用 `tradealpha_open_platform`,携带 `account` 和 `password`
5. 如果用户只想单独登录,也可以调用 `tradealpha_open_platform` 并传 `intent: "login"`
### 新闻工具常用参数
- `intent`
- `timeframe`
- `start_time`
- `end_time`
- `source`
- `category`
- `level`
- `page`
- `page_size`
## Runtime Rules
- 先走插件总入口 `tradealpha_open_platform`,不要回退到 shell 脚本
- 对“今天新闻”“今日新闻”“拉新闻”这类自然语言,默认视为要用本 skill
- 如果工具返回 `auth_required: true`,必须先登录,不能跳过
- 登录前不要假设用户已经有 token
- 返回结果是 JSON,先读 `details` / JSON 再总结给用户
- 不要在回复里回显用户密码或 token
- 新闻通常存在 `0-5` 分钟客观延迟
FILE:dist/src/auth.d.ts
export interface StoredTradeAlphaConfig {
apiToken?: string;
accessToken?: string | null;
tokenType?: string | null;
account?: string;
user?: unknown;
savedAt?: string;
}
export declare function getTradeAlphaConfigPath(): string;
export declare function readStoredTradeAlphaConfig(): StoredTradeAlphaConfig | null;
export declare function getTradeAlphaApiToken(): string | null;
FILE:dist/src/auth.js
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getTradeAlphaConfigPath = getTradeAlphaConfigPath;
exports.readStoredTradeAlphaConfig = readStoredTradeAlphaConfig;
exports.getTradeAlphaApiToken = getTradeAlphaApiToken;
const node_fs_1 = __importDefault(require("node:fs"));
const node_os_1 = __importDefault(require("node:os"));
const node_path_1 = __importDefault(require("node:path"));
const CONFIG_PATH = node_path_1.default.join(node_os_1.default.homedir(), ".config", "tradealpha-open-platform", "config.json");
function getTradeAlphaConfigPath() {
return CONFIG_PATH;
}
function readStoredTradeAlphaConfig() {
if (!node_fs_1.default.existsSync(CONFIG_PATH)) {
return null;
}
try {
const raw = node_fs_1.default.readFileSync(CONFIG_PATH, "utf8");
return JSON.parse(raw);
}
catch {
return null;
}
}
function getTradeAlphaApiToken() {
const envToken = process.env.TRADEALPHA_API_KEY?.trim();
if (envToken) {
return envToken;
}
const storedConfig = readStoredTradeAlphaConfig();
const storedToken = storedConfig?.apiToken?.trim();
return storedToken || null;
}
//# sourceMappingURL=auth.js.map
FILE:dist/src/index.d.ts
/**
* TradeAlpha Open Platform - OpenClaw Skill
*
* Aggregates major global and Chinese news sources for market intelligence.
*/
export interface ToolResult {
success: boolean;
data?: unknown;
error?: string;
}
export interface SkillTool {
name: string;
description: string;
execute: (args: Record<string, unknown>) => Promise<ToolResult>;
}
export declare const tools: SkillTool[];
declare const _default: {
metadata: {
name: string;
title: string;
description: string;
descriptionZh: string;
descriptionEn: string;
version: string;
};
tools: SkillTool[];
};
export default _default;
FILE:dist/src/index.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.tools = void 0;
const news_1 = require("./news");
const SKILL_NAME = "TradeAlpha开放平台";
const SKILL_DESCRIPTION_ZH = "路透、彭博、川普Truth、国内主流消息源,一网打尽";
const SKILL_DESCRIPTION_EN = "Reuters, Bloomberg, Trump's Truth Social, and major Chinese news sources, all in one place.";
const getRealtimeNewsTool = {
name: "get-realtime-news",
description: "Fetches real-time news from Reuters, Bloomberg, Truth Social, research reports, and Chinese mainstream sources.",
execute: async (args) => {
const result = await (0, news_1.fetchRealtimeNews)(args);
if (!result.success) {
return result;
}
return {
success: true,
data: {
skill: SKILL_NAME,
...(result.data ?? {}),
filters: {
sources: news_1.realtimeNewsEnums.sources,
categories: news_1.realtimeNewsEnums.categories,
levels: news_1.realtimeNewsEnums.levels,
minNewsTime: news_1.realtimeNewsEnums.minNewsTime,
},
},
};
},
};
exports.tools = [getRealtimeNewsTool];
exports.default = {
metadata: {
name: "tradealpha-open-platform",
title: SKILL_NAME,
description: `SKILL_NAME:SKILL_DESCRIPTION_ZH SKILL_DESCRIPTION_EN`,
descriptionZh: `SKILL_NAME:SKILL_DESCRIPTION_ZH`,
descriptionEn: `TradeAlpha Open Platform: SKILL_DESCRIPTION_EN`,
version: "0.3.0",
},
tools: exports.tools,
};
//# sourceMappingURL=index.js.map
FILE:dist/src/news.d.ts
import type { ToolResult } from "./index";
export declare const REALTIME_NEWS_URL = "https://quantaccess.lxaa.top/api/v1/news/realtime_news";
declare const NEWS_SOURCES: readonly ["domestic", "truth", "bloomberg", "rtrs", "research_report"];
declare const NEWS_CATEGORIES: readonly ["政治军事", "社会", "娱乐体育", "公司", "超大型公司", "政策", "市场与货币"];
declare const NEWS_LEVELS: readonly ["很重要", "重要", "一般"];
type NewsSource = (typeof NEWS_SOURCES)[number];
type NewsCategory = (typeof NEWS_CATEGORIES)[number];
type NewsLevel = (typeof NEWS_LEVELS)[number];
export interface RealtimeNewsRequest {
start_time?: string;
end_time?: string;
source?: NewsSource;
category?: NewsCategory;
level?: NewsLevel;
page?: number;
page_size?: number;
}
export declare function buildRealtimeNewsRequest(rawArgs: Record<string, unknown>): RealtimeNewsRequest;
export declare function fetchRealtimeNews(rawArgs: Record<string, unknown>): Promise<ToolResult>;
export declare const realtimeNewsEnums: {
sources: readonly ["domestic", "truth", "bloomberg", "rtrs", "research_report"];
categories: readonly ["政治军事", "社会", "娱乐体育", "公司", "超大型公司", "政策", "市场与货币"];
levels: readonly ["很重要", "重要", "一般"];
minNewsTime: string;
};
export {};
FILE:dist/src/news.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.realtimeNewsEnums = exports.REALTIME_NEWS_URL = void 0;
exports.buildRealtimeNewsRequest = buildRealtimeNewsRequest;
exports.fetchRealtimeNews = fetchRealtimeNews;
const auth_1 = require("./auth");
exports.REALTIME_NEWS_URL = "https://quantaccess.lxaa.top/api/v1/news/realtime_news";
const MIN_NEWS_TIME = "2025-04-01 00:00:00";
const DATE_ONLY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
const DATE_TIME_PATTERN = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
const NEWS_SOURCES = [
"domestic",
"truth",
"bloomberg",
"rtrs",
"research_report",
];
const NEWS_CATEGORIES = [
"政治军事",
"社会",
"娱乐体育",
"公司",
"超大型公司",
"政策",
"市场与货币",
];
const NEWS_LEVELS = ["很重要", "重要", "一般"];
function isRecord(value) {
return typeof value === "object" && value !== null;
}
function parseOptionalString(args, key) {
const value = args[key];
if (value == null) {
return undefined;
}
if (typeof value !== "string") {
throw new Error(`参数 \`key\` 必须是字符串。`);
}
const trimmed = value.trim();
return trimmed || undefined;
}
function parseOptionalInteger(args, key) {
const value = args[key];
if (value == null) {
return undefined;
}
if (typeof value === "number" && Number.isInteger(value)) {
return value;
}
if (typeof value === "string" && value.trim() !== "") {
const parsed = Number(value);
if (Number.isInteger(parsed)) {
return parsed;
}
}
throw new Error(`参数 \`key\` 必须是整数。`);
}
function validateEnum(value, key, allowedValues) {
if (!value) {
return undefined;
}
if (!allowedValues.includes(value)) {
throw new Error(`参数 \`key\` 取值无效,可选值为:allowedValues.join("、")。`);
}
return value;
}
function normalizeComparableTime(value) {
return DATE_ONLY_PATTERN.test(value) ? `value 00:00:00` : value;
}
function validateTimeFormat(value, key) {
if (!DATE_ONLY_PATTERN.test(value) && !DATE_TIME_PATTERN.test(value)) {
throw new Error(`参数 \`key\` 格式无效,必须为 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss。`);
}
const comparable = normalizeComparableTime(value);
if (comparable < MIN_NEWS_TIME) {
throw new Error(`参数 \`key\` 不能早于 2025-04-01 00:00:00(北京时间)。`);
}
}
function validateTimeRange(startTime, endTime) {
if (!startTime || !endTime) {
return;
}
if (normalizeComparableTime(startTime) > normalizeComparableTime(endTime)) {
throw new Error("`start_time` 不能晚于 `end_time`。");
}
}
function buildRealtimeNewsRequest(rawArgs) {
const start_time = parseOptionalString(rawArgs, "start_time");
const end_time = parseOptionalString(rawArgs, "end_time");
const source = validateEnum(parseOptionalString(rawArgs, "source"), "source", NEWS_SOURCES);
const category = validateEnum(parseOptionalString(rawArgs, "category"), "category", NEWS_CATEGORIES);
const level = validateEnum(parseOptionalString(rawArgs, "level"), "level", NEWS_LEVELS);
const page = parseOptionalInteger(rawArgs, "page") ?? 1;
const page_size = parseOptionalInteger(rawArgs, "page_size") ?? 20;
if (start_time) {
validateTimeFormat(start_time, "start_time");
}
if (end_time) {
validateTimeFormat(end_time, "end_time");
}
validateTimeRange(start_time, end_time);
if (page < 1) {
throw new Error("参数 `page` 必须大于或等于 1。");
}
if (page_size < 1 || page_size > 100) {
throw new Error("参数 `page_size` 必须在 1 到 100 之间。");
}
return {
start_time,
end_time,
source,
category,
level,
page,
page_size,
};
}
function getTokenOrThrow() {
const token = (0, auth_1.getTradeAlphaApiToken)();
if (!token) {
throw new Error(`未找到 TradeAlpha token。请先运行 \`npm run login\`,或设置 \`TRADEALPHA_API_KEY\`。本地配置路径:(0, auth_1.getTradeAlphaConfigPath)()`);
}
return token;
}
async function fetchRealtimeNews(rawArgs) {
try {
const request = buildRealtimeNewsRequest(rawArgs);
const token = getTokenOrThrow();
const response = await fetch(exports.REALTIME_NEWS_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer token`,
},
body: JSON.stringify({
...request,
token,
}),
});
let payload;
try {
payload = await response.json();
}
catch {
return {
success: false,
error: `新闻接口返回了非 JSON 响应,HTTP response.status。`,
};
}
if (!isRecord(payload)) {
return {
success: false,
error: "新闻接口返回了无法识别的响应结构。",
};
}
const apiResponse = payload;
if (!response.ok || apiResponse.code !== 0 || !apiResponse.data) {
const detail = apiResponse.message || `HTTP response.status`;
return {
success: false,
error: typeof apiResponse.code === "number"
? `获取新闻失败(code: apiResponse.code):detail`
: `获取新闻失败:detail`,
};
}
return {
success: true,
data: {
request,
total: apiResponse.data.total,
page: apiResponse.data.page,
pageSize: apiResponse.data.page_size,
items: apiResponse.data.list,
note: "新闻数据通常存在 0-5 分钟客观延迟。",
},
};
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
exports.realtimeNewsEnums = {
sources: NEWS_SOURCES,
categories: NEWS_CATEGORIES,
levels: NEWS_LEVELS,
minNewsTime: MIN_NEWS_TIME,
};
//# sourceMappingURL=news.js.map
FILE:openclaw.plugin.json
{
"id": "tradealpha-open-platform",
"name": "TradeAlpha Open Platform",
"description": "Login-first TradeAlpha bundle plugin with a single router tool plus bundled skill for OpenClaw.",
"enabledByDefault": true,
"contracts": {
"tools": [
"tradealpha_open_platform",
"tradealpha_login",
"tradealpha_news",
"tradealpha_realtime_news"
],
"skills": [
"./skills"
]
},
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}
FILE:package-lock.json
{
"name": "tradealpha-open-platform",
"version": "0.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "tradealpha-open-platform",
"version": "0.3.0",
"license": "MIT",
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.4.0"
}
},
"node_modules/@types/node": {
"version": "20.19.39",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
"integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}
FILE:package.json
{
"name": "tradealpha-open-platform",
"version": "0.4.0",
"description": "TradeAlpha login-first OpenClaw bundle plugin and skill: a single router tool handles login, token refresh, and realtime news from Reuters, Bloomberg, Truth Social, research alerts, and major Chinese news sources.",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
"openclaw": {
"extensions": [
"./plugin/index.mjs"
]
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"typecheck": "tsc --noEmit",
"login": "node scripts/login.js",
"news": "node scripts/get-realtime-news.js"
},
"keywords": [
"openclaw",
"clawhub",
"ai-skill",
"tradealpha",
"news",
"markets"
],
"license": "MIT",
"devDependencies": {
"typescript": "^5.4.0",
"@types/node": "^20.0.0"
}
}
FILE:plugin/index.mjs
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import path from "node:path";
import { fileURLToPath } from "node:url";
const execFileAsync = promisify(execFile);
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const packageRoot = path.resolve(__dirname, "..");
const LOGIN_TOOL_SCHEMA = {
type: "object",
additionalProperties: false,
properties: {
account: {
type: "string",
description: "TradeAlpha 登录账号,通常是手机号或用户名。",
},
password: {
type: "string",
description: "TradeAlpha 登录密码。",
},
},
};
const NEWS_TOOL_SCHEMA = {
type: "object",
additionalProperties: false,
properties: {
intent: {
type: "string",
enum: ["login", "news"],
description: "要执行的动作。默认是 news;显式传 login 时执行登录。",
},
timeframe: {
type: "string",
enum: ["today", "latest"],
description: "快捷时间范围。today 表示今天,latest 表示默认近 24 小时。",
},
account: {
type: "string",
description: "TradeAlpha 登录账号。用于首次登录或 token 失效后自动补登录。",
},
password: {
type: "string",
description: "TradeAlpha 登录密码。用于首次登录或 token 失效后自动补登录。",
},
start_time: {
type: "string",
description: "开始时间,格式 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss。",
},
end_time: {
type: "string",
description: "结束时间,格式 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss。",
},
source: {
type: "string",
enum: ["domestic", "truth", "bloomberg", "rtrs", "research_report"],
description: "新闻源。",
},
category: {
type: "string",
enum: ["政治军事", "社会", "娱乐体育", "公司", "超大型公司", "政策", "市场与货币"],
description: "新闻分类。",
},
level: {
type: "string",
enum: ["很重要", "重要", "一般"],
description: "重要程度。",
},
page: {
type: "integer",
minimum: 1,
description: "页码,默认 1。",
},
page_size: {
type: "integer",
minimum: 1,
maximum: 100,
description: "每页条数,默认 20,最大 100。",
},
},
};
function jsonResult(payload) {
return {
content: [
{
type: "text",
text: JSON.stringify(payload, null, 2),
},
],
details: payload,
};
}
function resolvePackageRoot(api) {
return api?.rootDir || packageRoot;
}
function resolveScriptPath(api, relativePath) {
return path.join(resolvePackageRoot(api), relativePath);
}
function pickNewsArgs(rawParams) {
if (!rawParams || typeof rawParams !== "object" || Array.isArray(rawParams)) {
return {};
}
const picked = {};
for (const key of [
"timeframe",
"start_time",
"end_time",
"source",
"category",
"level",
"page",
"page_size",
]) {
if (Object.hasOwn(rawParams, key)) {
picked[key] = rawParams[key];
}
}
return picked;
}
function resolveIntent(rawParams) {
if (!rawParams || typeof rawParams !== "object" || Array.isArray(rawParams)) {
return "news";
}
if (rawParams.intent === "login") {
return "login";
}
return "news";
}
function hasCredentials(rawParams) {
return Boolean(
rawParams &&
typeof rawParams === "object" &&
!Array.isArray(rawParams) &&
typeof rawParams.account === "string" &&
rawParams.account.trim() !== "" &&
typeof rawParams.password === "string" &&
rawParams.password.trim() !== "",
);
}
function normalizeTodayTimeRange(newsArgs) {
if (newsArgs.timeframe !== "today") {
const { timeframe, ...rest } = newsArgs;
return rest;
}
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
const date = `year-month-day`;
const time = `String(now.getHours()).padStart(2, "0"):String(now.getMinutes()).padStart(2, "0"):String(now.getSeconds()).padStart(2, "0")`;
const { timeframe, start_time, end_time, ...rest } = newsArgs;
return {
...rest,
start_time: start_time ?? `date 00:00:00`,
end_time: end_time ?? `date time`,
};
}
async function runJsonNodeScript(scriptPath, scriptArgs) {
try {
const { stdout, stderr } = await execFileAsync(process.execPath, [scriptPath, ...scriptArgs], {
cwd: packageRoot,
env: process.env,
maxBuffer: 2 * 1024 * 1024,
});
return parseScriptJson(stdout || stderr);
} catch (error) {
const stdout = typeof error?.stdout === "string" ? error.stdout : "";
const stderr = typeof error?.stderr === "string" ? error.stderr : "";
const combined = [stdout, stderr].filter(Boolean).join("\n").trim();
try {
return parseScriptJson(combined);
} catch {
return {
success: false,
error: combined || (error instanceof Error ? error.message : String(error)),
};
}
}
}
function parseScriptJson(rawOutput) {
const trimmed = rawOutput.trim();
if (!trimmed) {
throw new Error("脚本没有返回内容。");
}
return JSON.parse(trimmed);
}
async function runTradeAlphaLogin(api, rawParams) {
const missingFields = [];
const rawAccount = rawParams?.account;
const rawPassword = rawParams?.password;
const account =
typeof rawAccount === "string" && rawAccount.trim() !== ""
? rawAccount.trim()
: null;
const password =
typeof rawPassword === "string" && rawPassword.trim() !== ""
? rawPassword.trim()
: null;
if (!account) {
missingFields.push("account");
}
if (!password) {
missingFields.push("password");
}
if (missingFields.length > 0) {
return {
success: false,
auth_required: true,
next_action: "provide_credentials",
token_source: "none",
missing_fields: missingFields,
message: "请先向用户索要 TradeAlpha 账号和密码,再调用登录。",
};
}
const scriptPath = resolveScriptPath(api, "scripts/login.js");
return await runJsonNodeScript(scriptPath, [
"--account",
account,
"--password",
password,
"--json",
]);
}
async function runTradeAlphaNews(api, rawParams) {
const scriptPath = resolveScriptPath(api, "scripts/get-realtime-news.js");
const normalizedArgs = normalizeTodayTimeRange(pickNewsArgs(rawParams));
return await runJsonNodeScript(scriptPath, [JSON.stringify(normalizedArgs)]);
}
function createTradeAlphaLoginTool(api) {
return {
name: "tradealpha_login",
label: "TradeAlpha Login",
description:
"Log in to TradeAlpha Open Platform with account and password, then persist the returned user.api_token locally. Use this first whenever TradeAlpha token status is unknown, missing, or expired.",
parameters: LOGIN_TOOL_SCHEMA,
execute: async (_toolCallId, rawParams) => {
return jsonResult(await runTradeAlphaLogin(api, rawParams));
},
};
}
function createTradeAlphaOpenPlatformTool(api) {
return {
name: "tradealpha_open_platform",
label: "TradeAlpha Open Platform",
description:
"Single entrypoint for TradeAlpha. Use this for today's news, market news, or login. It auto-checks auth, asks for credentials when missing, and can log in before retrying the news request.",
parameters: NEWS_TOOL_SCHEMA,
execute: async (_toolCallId, rawParams) => {
const intent = resolveIntent(rawParams);
if (intent === "login") {
return jsonResult(await runTradeAlphaLogin(api, rawParams));
}
let newsPayload = await runTradeAlphaNews(api, rawParams);
if (newsPayload?.auth_required === true && hasCredentials(rawParams)) {
const loginPayload = await runTradeAlphaLogin(api, rawParams);
if (loginPayload?.success !== true) {
return jsonResult(loginPayload);
}
newsPayload = await runTradeAlphaNews(api, rawParams);
if (newsPayload && typeof newsPayload === "object") {
newsPayload.login = {
success: true,
message: "已自动完成登录并重试新闻请求。",
};
}
}
return jsonResult(newsPayload);
},
};
}
function createTradeAlphaNewsTool(api, name, label, description) {
return {
name,
label,
description,
parameters: NEWS_TOOL_SCHEMA,
execute: async (_toolCallId, rawParams) => {
return jsonResult(await runTradeAlphaNews(api, rawParams));
},
};
}
export default {
id: "tradealpha-open-platform",
name: "TradeAlpha Open Platform Plugin",
description:
"Registers TradeAlpha login-first news tools for OpenClaw.",
version: "0.4.0",
register(api) {
api.registerTool(createTradeAlphaOpenPlatformTool(api));
api.registerTool(createTradeAlphaLoginTool(api));
api.registerTool(
createTradeAlphaNewsTool(
api,
"tradealpha_news",
"TradeAlpha News",
"Fetch TradeAlpha news. If token is missing or expired, this tool returns auth_required=true and the agent must ask the user for credentials, call tradealpha_login, then retry tradealpha_news.",
),
);
api.registerTool(
createTradeAlphaNewsTool(
api,
"tradealpha_realtime_news",
"TradeAlpha Realtime News",
"Fetch real-time TradeAlpha news after login. Use tradealpha_news as the preferred alias. If token is missing or expired, call tradealpha_login first.",
),
);
},
};
FILE:references/login-flow.md
# TradeAlpha Login Flow
TradeAlpha 采用单 token 模式。
1. 使用账号密码调用登录接口
2. 从响应中的 `user.api_token` 提取唯一 token
3. 将 token 存到本地配置,供后续新闻接口复用
4. 如果新闻接口返回认证失效,重新登录并重试
当前实现优先读取:
1. 环境变量 `TRADEALPHA_API_KEY`
2. 本地配置文件中的已保存 token
登录脚本:
- `scripts/login.js`
新闻脚本:
- `scripts/get-realtime-news.js`
FILE:references/news-api.md
# TradeAlpha News API Notes
TradeAlpha 实时新闻支持以下常用筛选参数:
- `start_time`
- `end_time`
- `source`
- `category`
- `level`
- `page`
- `page_size`
插件总入口 `tradealpha_open_platform` 额外提供两个便捷参数:
- `intent`: `login` 或 `news`
- `timeframe`: `today` 或 `latest`
推荐约定:
- 用户说“今天新闻”时,优先用 `timeframe: "today"`
- 用户未给时间范围时,走接口默认近 24 小时逻辑
- 认证失败时,不直接结束;先引导登录,再重试
FILE:scripts/get-realtime-news.js
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const REALTIME_NEWS_URL =
"https://quantaccess.lxaa.top/api/v1/news/realtime_news";
const CONFIG_PATH = path.join(
os.homedir(),
".config",
"tradealpha-open-platform",
"config.json",
);
const MIN_NEWS_TIME = "2025-04-01 00:00:00";
const DATE_ONLY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
const DATE_TIME_PATTERN = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
const NEWS_SOURCES = [
"domestic",
"truth",
"bloomberg",
"rtrs",
"research_report",
];
const NEWS_CATEGORIES = [
"政治军事",
"社会",
"娱乐体育",
"公司",
"超大型公司",
"政策",
"市场与货币",
];
const NEWS_LEVELS = ["很重要", "重要", "一般"];
function printHelp() {
console.log("TradeAlpha 实时新闻脚本");
console.log("");
console.log("用法:");
console.log(" node scripts/get-realtime-news.js");
console.log(
' node scripts/get-realtime-news.js \'{"source":"bloomberg","level":"重要","page_size":5}\'',
);
console.log("");
console.log("支持字段:");
console.log(" start_time, end_time, source, category, level, page, page_size");
}
function printJson(payload, error = false) {
const encoded = JSON.stringify(payload, null, 2);
if (error) {
console.error(encoded);
return;
}
console.log(encoded);
}
function readStoredToken() {
const envToken = process.env.TRADEALPHA_API_KEY?.trim();
if (envToken) {
return {
token: envToken,
token_source: "env",
};
}
if (!fs.existsSync(CONFIG_PATH)) {
return {
token: null,
token_source: "none",
};
}
try {
const raw = fs.readFileSync(CONFIG_PATH, "utf8");
const parsed = JSON.parse(raw);
const storedToken = typeof parsed?.apiToken === "string" ? parsed.apiToken.trim() : null;
return {
token: storedToken,
token_source: storedToken ? "local-config" : "none",
};
} catch {
return {
token: null,
token_source: "none",
};
}
}
function parseInput(argv) {
const firstArg = argv[0];
if (!firstArg) {
return {};
}
if (firstArg === "--help" || firstArg === "-h") {
printHelp();
process.exit(0);
}
try {
const parsed = JSON.parse(firstArg);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("输入必须是 JSON 对象。");
}
return parsed;
} catch (error) {
throw new Error(
`参数必须是单个 JSON 对象字符串,例如 '{"source":"bloomberg","page_size":5}'。原始错误:String(error)`,
);
}
}
function parseOptionalString(args, key) {
const value = args[key];
if (value == null) {
return undefined;
}
if (typeof value !== "string") {
throw new Error(`参数 \`key\` 必须是字符串。`);
}
const trimmed = value.trim();
return trimmed || undefined;
}
function parseOptionalInteger(args, key) {
const value = args[key];
if (value == null) {
return undefined;
}
if (typeof value === "number" && Number.isInteger(value)) {
return value;
}
if (typeof value === "string" && value.trim() !== "") {
const parsed = Number(value);
if (Number.isInteger(parsed)) {
return parsed;
}
}
throw new Error(`参数 \`key\` 必须是整数。`);
}
function validateEnum(value, key, allowedValues) {
if (!value) {
return undefined;
}
if (!allowedValues.includes(value)) {
throw new Error(
`参数 \`key\` 取值无效,可选值为:allowedValues.join("、")。`,
);
}
return value;
}
function normalizeComparableTime(value) {
return DATE_ONLY_PATTERN.test(value) ? `value 00:00:00` : value;
}
function validateTimeFormat(value, key) {
if (!DATE_ONLY_PATTERN.test(value) && !DATE_TIME_PATTERN.test(value)) {
throw new Error(
`参数 \`key\` 格式无效,必须为 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss。`,
);
}
if (normalizeComparableTime(value) < MIN_NEWS_TIME) {
throw new Error(
`参数 \`key\` 不能早于 2025-04-01 00:00:00(北京时间)。`,
);
}
}
function buildRequest(rawArgs) {
const start_time = parseOptionalString(rawArgs, "start_time");
const end_time = parseOptionalString(rawArgs, "end_time");
const source = validateEnum(
parseOptionalString(rawArgs, "source"),
"source",
NEWS_SOURCES,
);
const category = validateEnum(
parseOptionalString(rawArgs, "category"),
"category",
NEWS_CATEGORIES,
);
const level = validateEnum(
parseOptionalString(rawArgs, "level"),
"level",
NEWS_LEVELS,
);
const page = parseOptionalInteger(rawArgs, "page") ?? 1;
const page_size = parseOptionalInteger(rawArgs, "page_size") ?? 20;
if (start_time) {
validateTimeFormat(start_time, "start_time");
}
if (end_time) {
validateTimeFormat(end_time, "end_time");
}
if (
start_time &&
end_time &&
normalizeComparableTime(start_time) > normalizeComparableTime(end_time)
) {
throw new Error("`start_time` 不能晚于 `end_time`。");
}
if (page < 1) {
throw new Error("参数 `page` 必须大于或等于 1。");
}
if (page_size < 1 || page_size > 100) {
throw new Error("参数 `page_size` 必须在 1 到 100 之间。");
}
return {
start_time,
end_time,
source,
category,
level,
page,
page_size,
};
}
async function fetchRealtimeNews(request, token) {
const response = await fetch(REALTIME_NEWS_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer token`,
},
body: JSON.stringify({
...request,
token,
}),
});
let payload;
try {
payload = await response.json();
} catch {
throw new Error(`新闻接口返回了非 JSON 响应,HTTP response.status。`);
}
if (!response.ok || payload?.code !== 0 || !payload?.data) {
const detail = typeof payload?.message === "string" ? payload.message : `HTTP response.status`;
const codeText =
typeof payload?.code === "number" ? `(code: payload.code)` : "";
const error = new Error(`获取新闻失败codeText:detail`);
error.tradealpha_code = typeof payload?.code === "number" ? payload.code : null;
throw error;
}
return payload.data;
}
async function main() {
const args = parseInput(process.argv.slice(2));
const request = buildRequest(args);
const auth = readStoredToken();
if (!auth.token) {
printJson(
{
success: false,
auth_required: true,
next_action: "provide_credentials",
token_source: auth.token_source,
error: `未找到 TradeAlpha token。请先运行 \`node scripts/login.js\` 或设置 \`TRADEALPHA_API_KEY\`。配置路径:CONFIG_PATH`,
message: "当前还没有可用 token,请先登录。",
},
true,
);
process.exitCode = 1;
return;
}
const data = await fetchRealtimeNews(request, auth.token);
printJson({
success: true,
auth_required: false,
next_action: "none",
token_source: auth.token_source,
request,
total: data.total,
page: data.page,
page_size: data.page_size,
list: data.list,
note: "新闻数据通常存在 0-5 分钟客观延迟。",
});
}
main().catch((error) => {
const message = error instanceof Error ? error.message : String(error);
const tradealphaCode =
typeof error?.tradealpha_code === "number" ? error.tradealpha_code : null;
const authRequired = tradealphaCode === 1001;
printJson(
{
success: false,
auth_required: authRequired,
next_action: authRequired ? "provide_credentials" : "fetch_news",
token_source: "none",
error: message,
error_code: tradealphaCode,
message: authRequired
? "token 无效或已过期,请重新登录。"
: "新闻拉取失败,请检查参数或稍后重试。",
},
true,
);
process.exitCode = 1;
});
FILE:scripts/login.js
const fs = require("node:fs/promises");
const os = require("node:os");
const path = require("node:path");
const readline = require("node:readline/promises");
const { stdin, stdout } = require("node:process");
const LOGIN_URL = "https://quantaccess.lxaa.top/api/v1/auth/login/password";
const CONFIG_DIR = path.join(os.homedir(), ".config", "tradealpha-open-platform");
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
function parseArgs(argv) {
const options = {
account: null,
password: null,
json: false,
help: false,
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--account") {
options.account = argv[index + 1] ?? null;
index += 1;
continue;
}
if (arg === "--password") {
options.password = argv[index + 1] ?? null;
index += 1;
continue;
}
if (arg === "--json") {
options.json = true;
continue;
}
if (arg === "--help" || arg === "-h") {
options.help = true;
continue;
}
}
return options;
}
function printHelp() {
console.log("TradeAlpha 登录脚本");
console.log("");
console.log("用法:");
console.log(" node scripts/login.js");
console.log(" node scripts/login.js --account 15600000000 --password 123456");
console.log(" node scripts/login.js --json");
}
function printJson(payload, error = false) {
const encoded = JSON.stringify(payload, null, 2);
if (error) {
console.error(encoded);
return;
}
console.log(encoded);
}
async function promptCredentials() {
const rl = readline.createInterface({
input: stdin,
output: stdout,
});
try {
const account = (await rl.question("TradeAlpha 账号: ")).trim();
rl.close();
const password = (await questionHidden("TradeAlpha 密码: ")).trim();
if (!account || !password) {
throw new Error("账号和密码都不能为空。");
}
return { account, password };
} finally {
rl.close();
}
}
function questionHidden(prompt) {
return new Promise((resolve, reject) => {
if (!stdin.isTTY || typeof stdin.setRawMode !== "function") {
const fallback = readline.createInterface({
input: stdin,
output: stdout,
});
fallback.question(prompt).then(resolve).catch(reject).finally(() => {
fallback.close();
});
return;
}
const wasRaw = stdin.isRaw;
const chars = [];
const cleanup = () => {
stdin.removeListener("data", onData);
stdin.setRawMode(Boolean(wasRaw));
stdout.write("\n");
};
const onData = (buffer) => {
const text = buffer.toString("utf8");
if (text === "\r" || text === "\n") {
cleanup();
resolve(chars.join(""));
return;
}
if (text === "\u0003") {
cleanup();
reject(new Error("登录已取消。"));
return;
}
if (text === "\u007f") {
chars.pop();
return;
}
chars.push(text);
};
stdout.write(prompt);
stdin.setRawMode(true);
stdin.resume();
stdin.on("data", onData);
});
}
async function login(account, password) {
const response = await fetch(LOGIN_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ account, password }),
});
let payload;
try {
payload = await response.json();
} catch {
throw new Error(`登录失败,接口返回了非 JSON 响应,HTTP response.status`);
}
if (!response.ok) {
const detail =
typeof payload?.detail === "string"
? payload.detail
: typeof payload?.message === "string"
? payload.message
: `HTTP response.status`;
throw new Error(`登录失败:detail`);
}
const apiToken = payload?.user?.api_token;
if (typeof apiToken !== "string" || apiToken.length === 0) {
throw new Error("登录成功,但响应里没有 user.api_token。");
}
return {
success: true,
auth_required: false,
next_action: "fetch_news",
token_source: "local-config",
apiToken,
accessToken:
typeof payload?.access_token === "string" ? payload.access_token : null,
tokenType: typeof payload?.token_type === "string" ? payload.token_type : null,
user: payload?.user ?? null,
};
}
async function saveConfig(config) {
await fs.mkdir(CONFIG_DIR, { recursive: true });
await fs.writeFile(CONFIG_PATH, `JSON.stringify(config, null, 2)\n`, {
encoding: "utf8",
mode: 0o600,
});
await fs.chmod(CONFIG_PATH, 0o600);
}
async function main() {
const options = parseArgs(process.argv.slice(2));
if (options.help) {
printHelp();
return;
}
if (!options.json) {
console.log("TradeAlpha 登录");
console.log(`登录地址: LOGIN_URL`);
}
const account = typeof options.account === "string" ? options.account.trim() : "";
const password =
typeof options.password === "string" ? options.password.trim() : "";
const credentials =
account && password ? { account, password } : await promptCredentials();
const result = await login(credentials.account, credentials.password);
await saveConfig({
apiToken: result.apiToken,
accessToken: result.accessToken,
tokenType: result.tokenType,
account: credentials.account,
user: result.user,
savedAt: new Date().toISOString(),
});
if (options.json) {
printJson({
success: true,
auth_required: false,
next_action: "fetch_news",
token_source: "local-config",
account: credentials.account,
configPath: CONFIG_PATH,
user: result.user,
message: "登录成功,已保存唯一 token。",
});
return;
}
console.log("");
console.log("登录成功,已保存唯一 token。");
console.log(`配置文件: CONFIG_PATH`);
console.log("后续 Skill 可直接读取本地配置,无需重复输入。");
}
main().catch((error) => {
const message = error instanceof Error ? error.message : String(error);
printJson(
{
success: false,
auth_required: true,
next_action: "provide_credentials",
token_source: "none",
error: message,
message: "登录失败,请检查账号密码后重试。",
},
true,
);
process.exitCode = 1;
});
FILE:skills/tradealpha-open-platform/SKILL.md
---
name: tradealpha-open-platform
description: Bundled TradeAlpha router skill for OpenClaw. Use for today's news, market news, Reuters, Bloomberg, Truth Social, Chinese mainstream headlines, or when the user needs to log in or refresh a TradeAlpha token. Prefer the single tool `tradealpha_open_platform`.
homepage: https://quantaccess.lxaa.top
version: 0.4.0
---
# TradeAlpha开放平台
这是插件内置 skill,用来把自然语言请求稳定路由到 `tradealpha_open_platform`。
## First Rule
只要用户要新闻、要登录、要 token、要刷新 token,就优先调用 `tradealpha_open_platform`。
## Routing Rules
### 登录
如果用户想登录、初始化 token、刷新 token:
1. 先索要 `account` 和 `password`
2. 调用 `tradealpha_open_platform`,传:
- `intent: "login"`
- `account`
- `password`
### 新闻
如果用户想获取今天新闻、市场新闻、路透、彭博、Truth 或国内主流消息:
1. 先调用 `tradealpha_open_platform`
2. 对“今天新闻”优先传 `timeframe: "today"`
3. 如果返回 `auth_required: true`:
- 先索要 `account` 和 `password`
- 再次调用 `tradealpha_open_platform`,补上 `account` 和 `password`
## Common Parameters
- `intent`
- `timeframe`
- `start_time`
- `end_time`
- `source`
- `category`
- `level`
- `page`
- `page_size`
## Runtime Rules
- 不要把 `tradealpha-open-platform` 当成 tool 名;真正的 tool 名是 `tradealpha_open_platform`
- 只有当总入口不适合时,才回退到 `tradealpha_login` / `tradealpha_news`
- 不要在回复中回显用户密码或 token
FILE:src/auth.ts
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const CONFIG_PATH = path.join(
os.homedir(),
".config",
"tradealpha-open-platform",
"config.json",
);
export interface StoredTradeAlphaConfig {
apiToken?: string;
accessToken?: string | null;
tokenType?: string | null;
account?: string;
user?: unknown;
savedAt?: string;
}
export function getTradeAlphaConfigPath(): string {
return CONFIG_PATH;
}
export function readStoredTradeAlphaConfig(): StoredTradeAlphaConfig | null {
if (!fs.existsSync(CONFIG_PATH)) {
return null;
}
try {
const raw = fs.readFileSync(CONFIG_PATH, "utf8");
return JSON.parse(raw) as StoredTradeAlphaConfig;
} catch {
return null;
}
}
export function getTradeAlphaApiToken(): string | null {
const envToken = process.env.TRADEALPHA_API_KEY?.trim();
if (envToken) {
return envToken;
}
const storedConfig = readStoredTradeAlphaConfig();
const storedToken = storedConfig?.apiToken?.trim();
return storedToken || null;
}
FILE:src/index.ts
import { fetchRealtimeNews, realtimeNewsEnums } from "./news";
/**
* TradeAlpha Open Platform - OpenClaw Skill
*
* Aggregates major global and Chinese news sources for market intelligence.
*/
export interface ToolResult {
success: boolean;
data?: unknown;
error?: string;
}
export interface SkillTool {
name: string;
description: string;
execute: (args: Record<string, unknown>) => Promise<ToolResult>;
}
const SKILL_NAME = "TradeAlpha开放平台";
const SKILL_DESCRIPTION_ZH =
"路透、彭博、川普Truth、国内主流消息源,一网打尽";
const SKILL_DESCRIPTION_EN =
"Reuters, Bloomberg, Trump's Truth Social, and major Chinese news sources, all in one place.";
const getRealtimeNewsTool: SkillTool = {
name: "get-realtime-news",
description:
"Fetches real-time news from Reuters, Bloomberg, Truth Social, research reports, and Chinese mainstream sources.",
execute: async (args: Record<string, unknown>): Promise<ToolResult> => {
const result = await fetchRealtimeNews(args);
if (!result.success) {
return result;
}
return {
success: true,
data: {
skill: SKILL_NAME,
...((result.data as Record<string, unknown>) ?? {}),
filters: {
sources: realtimeNewsEnums.sources,
categories: realtimeNewsEnums.categories,
levels: realtimeNewsEnums.levels,
minNewsTime: realtimeNewsEnums.minNewsTime,
},
},
};
},
};
export const tools: SkillTool[] = [getRealtimeNewsTool];
export default {
metadata: {
name: "tradealpha-open-platform",
title: SKILL_NAME,
description: `SKILL_NAME:SKILL_DESCRIPTION_ZH SKILL_DESCRIPTION_EN`,
descriptionZh: `SKILL_NAME:SKILL_DESCRIPTION_ZH`,
descriptionEn: `TradeAlpha Open Platform: SKILL_DESCRIPTION_EN`,
version: "0.4.0",
},
tools,
};
FILE:src/news.ts
import { getTradeAlphaApiToken, getTradeAlphaConfigPath } from "./auth";
import type { ToolResult } from "./index";
export const REALTIME_NEWS_URL =
"https://quantaccess.lxaa.top/api/v1/news/realtime_news";
const MIN_NEWS_TIME = "2025-04-01 00:00:00";
const DATE_ONLY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
const DATE_TIME_PATTERN = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
const NEWS_SOURCES = [
"domestic",
"truth",
"bloomberg",
"rtrs",
"research_report",
] as const;
const NEWS_CATEGORIES = [
"政治军事",
"社会",
"娱乐体育",
"公司",
"超大型公司",
"政策",
"市场与货币",
] as const;
const NEWS_LEVELS = ["很重要", "重要", "一般"] as const;
type NewsSource = (typeof NEWS_SOURCES)[number];
type NewsCategory = (typeof NEWS_CATEGORIES)[number];
type NewsLevel = (typeof NEWS_LEVELS)[number];
export interface RealtimeNewsRequest {
start_time?: string;
end_time?: string;
source?: NewsSource;
category?: NewsCategory;
level?: NewsLevel;
page?: number;
page_size?: number;
}
interface RealtimeNewsItem {
id: number;
datetime: string;
content: string;
source: string;
category: string;
level: string;
}
interface RealtimeNewsApiResponse {
code: number;
message: string;
data?: {
total: number;
page: number;
page_size: number;
list: RealtimeNewsItem[];
};
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function parseOptionalString(
args: Record<string, unknown>,
key: string,
): string | undefined {
const value = args[key];
if (value == null) {
return undefined;
}
if (typeof value !== "string") {
throw new Error(`参数 \`key\` 必须是字符串。`);
}
const trimmed = value.trim();
return trimmed || undefined;
}
function parseOptionalInteger(
args: Record<string, unknown>,
key: string,
): number | undefined {
const value = args[key];
if (value == null) {
return undefined;
}
if (typeof value === "number" && Number.isInteger(value)) {
return value;
}
if (typeof value === "string" && value.trim() !== "") {
const parsed = Number(value);
if (Number.isInteger(parsed)) {
return parsed;
}
}
throw new Error(`参数 \`key\` 必须是整数。`);
}
function validateEnum<T extends readonly string[]>(
value: string | undefined,
key: string,
allowedValues: T,
): T[number] | undefined {
if (!value) {
return undefined;
}
if (!allowedValues.includes(value as T[number])) {
throw new Error(
`参数 \`key\` 取值无效,可选值为:allowedValues.join("、")。`,
);
}
return value as T[number];
}
function normalizeComparableTime(value: string): string {
return DATE_ONLY_PATTERN.test(value) ? `value 00:00:00` : value;
}
function validateTimeFormat(value: string, key: string): void {
if (!DATE_ONLY_PATTERN.test(value) && !DATE_TIME_PATTERN.test(value)) {
throw new Error(
`参数 \`key\` 格式无效,必须为 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss。`,
);
}
const comparable = normalizeComparableTime(value);
if (comparable < MIN_NEWS_TIME) {
throw new Error(
`参数 \`key\` 不能早于 2025-04-01 00:00:00(北京时间)。`,
);
}
}
function validateTimeRange(
startTime: string | undefined,
endTime: string | undefined,
): void {
if (!startTime || !endTime) {
return;
}
if (normalizeComparableTime(startTime) > normalizeComparableTime(endTime)) {
throw new Error("`start_time` 不能晚于 `end_time`。");
}
}
export function buildRealtimeNewsRequest(
rawArgs: Record<string, unknown>,
): RealtimeNewsRequest {
const start_time = parseOptionalString(rawArgs, "start_time");
const end_time = parseOptionalString(rawArgs, "end_time");
const source = validateEnum(
parseOptionalString(rawArgs, "source"),
"source",
NEWS_SOURCES,
);
const category = validateEnum(
parseOptionalString(rawArgs, "category"),
"category",
NEWS_CATEGORIES,
);
const level = validateEnum(
parseOptionalString(rawArgs, "level"),
"level",
NEWS_LEVELS,
);
const page = parseOptionalInteger(rawArgs, "page") ?? 1;
const page_size = parseOptionalInteger(rawArgs, "page_size") ?? 20;
if (start_time) {
validateTimeFormat(start_time, "start_time");
}
if (end_time) {
validateTimeFormat(end_time, "end_time");
}
validateTimeRange(start_time, end_time);
if (page < 1) {
throw new Error("参数 `page` 必须大于或等于 1。");
}
if (page_size < 1 || page_size > 100) {
throw new Error("参数 `page_size` 必须在 1 到 100 之间。");
}
return {
start_time,
end_time,
source,
category,
level,
page,
page_size,
};
}
function getTokenOrThrow(): string {
const token = getTradeAlphaApiToken();
if (!token) {
throw new Error(
`未找到 TradeAlpha token。请先运行 \`npm run login\`,或设置 \`TRADEALPHA_API_KEY\`。本地配置路径:getTradeAlphaConfigPath()`,
);
}
return token;
}
export async function fetchRealtimeNews(
rawArgs: Record<string, unknown>,
): Promise<ToolResult> {
try {
const request = buildRealtimeNewsRequest(rawArgs);
const token = getTokenOrThrow();
const response = await fetch(REALTIME_NEWS_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer token`,
},
body: JSON.stringify({
...request,
token,
}),
});
let payload: unknown;
try {
payload = await response.json();
} catch {
return {
success: false,
error: `新闻接口返回了非 JSON 响应,HTTP response.status。`,
};
}
if (!isRecord(payload)) {
return {
success: false,
error: "新闻接口返回了无法识别的响应结构。",
};
}
const apiResponse = payload as unknown as RealtimeNewsApiResponse;
if (!response.ok || apiResponse.code !== 0 || !apiResponse.data) {
const detail = apiResponse.message || `HTTP response.status`;
return {
success: false,
error:
typeof apiResponse.code === "number"
? `获取新闻失败(code: apiResponse.code):detail`
: `获取新闻失败:detail`,
};
}
return {
success: true,
data: {
request,
total: apiResponse.data.total,
page: apiResponse.data.page,
pageSize: apiResponse.data.page_size,
items: apiResponse.data.list,
note: "新闻数据通常存在 0-5 分钟客观延迟。",
},
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
export const realtimeNewsEnums = {
sources: NEWS_SOURCES,
categories: NEWS_CATEGORIES,
levels: NEWS_LEVELS,
minNewsTime: MIN_NEWS_TIME,
};
FILE:tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "dist",
"rootDir": ".",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}