
Tencent News
- 8 installs
- 33 repo stars
- Updated April 26, 2026
- bighardperson/computer-science-skills-collection
tencent-news is a Claude skill that fetches Tencent News content (hot rankings, briefings, real-time and domain news) through the bundled tencent-news-cli.
About
This skill fetches news from Tencent News through a bundled tencent-news-cli, covering hot rankings, morning/evening briefings, real-time feeds and domain-specific news for China and global topics. The agent reads the CLI help to map user intent to subcommands rather than hardcoding them, and formats results into a fixed markdown layout. A developer uses it when a workflow needs current news content from Tencent's feed. It matters because it delegates install, update and API-key handling to scripts and refuses to fall back to web search on CLI failure.
- Fetches Tencent News content (hot rankings, briefings, real-time and domain news) via the tencent-news-cli
- Handles CLI install, update and API-key setup through bundled scripts
- Formats results into a fixed markdown layout with title, source, time, summary and original-article link
Tencent News by the numbers
- 8 all-time installs (skills.sh)
- Ranked #1,522 of 2,719 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Jul 30, 2026 (Skillselion catalog sync)
tencent-news capabilities & compatibility
Requires a Tencent News API key from news.qq.com; CLI runs locally
- Capabilities
- news search · content retrieval
- Use cases
- research · web search
- Platforms
- macOS · Linux · Windows
- Runs
- Runs locally
- Pricing
- Bring your own API key
What tencent-news says it does
通过 `tencent-news-cli` 获取腾讯新闻内容。
**CLI 命令失败后,立即停止,绝不通过 WebSearch 或其他方式获取新闻替代。**
7×24 news search tool focused on China and global hot topics, supporting rankings, briefings, real-time feeds, and domain news queries.
npx skills add https://github.com/bighardperson/computer-science-skills-collection --skill tencent-newsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 8 |
|---|---|
| repo stars | ★ 33 |
| Last updated | April 26, 2026 |
| Repository | bighardperson/computer-science-skills-collection ↗ |
What it does
Retrieve current Tencent News content (hot topics, briefings, real-time and domain news) via a CLI from an agent.
Who is it for?
Agents that need current Chinese-language news content (hot topics, briefings, domain news) from Tencent News
Skip if: Falling back to generic web search when the CLI fails; the skill forbids it
When should I use this skill?
The user asks to search news, see the news hot ranking, read morning/evening briefings, or get real-time or domain-specific news
What you get
- Formatted news list with title, source, time, summary and original-article link
By the numbers
- 7×24 news feed
- two-phase workflow (environment ready, then fetch)
Files
腾讯新闻内容订阅
通过 tencent-news-cli 获取腾讯新闻内容。
核心原则:基础设施(安装、更新、Key 配置)交给脚本处理;智能体只负责选择子命令和参数——始终先读 help,不要硬编码。平台约定
| 平台 | 脚本运行方式 | 示例 |
|---|---|---|
| macOS / Linux | sh scripts/<name>.sh | sh scripts/cli-state.sh |
| Windows | bun scripts/<name>.ts | bun scripts/cli-state.ts |
Windows 需先确保bun可用。若不可用:powershell -c "irm bun.sh/install.ps1 | iex",安装后重启终端确认bun --version。
以下所有脚本调用均以 macOS / Linux 为例,Windows 将 .sh 替换为 .ts,sh 替换为 bun。
CLI 命令本身不要依赖 cli-state 返回的模板字符串,直接根据 platform.cliPath 组装:
| 平台 | CLI 命令模板 |
|---|---|
| macOS / Linux | "<cliPath>" <subcommand> [args] |
| Windows PowerShell | & "<cliPath>" <subcommand> [args] |
Phase 1:环境就绪
环境已就绪时直接跳到 Phase 2。
1. 状态检查
sh scripts/cli-state.sh解析返回的 JSON,关注以下字段:
| 字段 | 含义 |
|---|---|
platform.cliPath | CLI 完整路径,后续所有命令使用此路径 |
platform.cliSource | global(用户已全局安装)/ local(技能目录下载)/ none(未找到) |
cliExists | CLI 是否存在 |
update.needUpdate | 当前版本是否需要更新 |
update.error | version 检查失败时的错误信息 |
apiKey.present | API Key 是否已配置 |
apiKey.status | configured / missing / error |
apiKey.error | apikey-get 执行异常或输出异常时的错误信息 |
2. 安装 CLI(cliExists 为 false 时)
cliSource为global时跳过此步。
sh scripts/install-cli.sh若脚本安装失败,引导用户手动安装——参见 `references/installation-guide.md`。
3. 更新 CLI(update.needUpdate 为 true,或 CLI 提示版本过旧时)
"<cliPath>" updateWindows PowerShell 使用 & "<cliPath>" update。
始终使用 platform.cliPath 组装命令。若 update.error 不为空,先展示错误并让用户处理。
若 update 命令失败,或错误信息表明当前 CLI 不支持 update(如 unknown command、not found、not recognized),立即改为执行安装脚本覆盖:
sh scripts/install-cli.sh --forceWindows:
bun scripts/install-cli.ts --force解析安装脚本返回的 JSON,并把后续命令切换到新返回的 platform.cliPath。只有覆盖安装也失败时,才引导用户参考 `references/update-guide.md` 手动处理。
4. 配置 API Key(apiKey.status 不为 configured 时)
missing→ 引导用户打开 API Key 获取页面 自行获取,不要执行 `open` / `xdg-open` / `start` 等命令自动打开浏览器error→ 展示apiKey.error,让用户先处理(权限、网络、CLI 异常),处理后重试
设置 Key(命令前缀使用 platform.cliPath,KEY 是裸值不加引号):
"<cliPath>" apikey-set KEYWindows PowerShell 分别使用 & "<cliPath>" apikey-set KEY、& "<cliPath>" apikey-get、& "<cliPath>" apikey-clear。
验证:"<cliPath>" apikey-get 清除(仅用户明确要求时):"<cliPath>" apikey-clear
详见 `references/env-setup-guide.md`。
Phase 2:获取新闻
CLI 更新频繁,子命令和参数可能随版本变化。始终以当前 `help` 输出为准,不要假设或记忆任何子命令。
1. 执行 `help` 使用 platform.cliPath 自行拼命令:macOS / Linux 为 "<cliPath>" help,Windows PowerShell 为 & "<cliPath>" help。
2. 理解意图,映射子命令
- 单一请求(如"看热点")→ 映射到一个子命令
- 复合请求(如"看热点、财经和军事新闻")→ 拆解为多个意图,分别映射,依次调用
- 反馈问题(如"反馈报错,新闻质量不行")→ 使用
feedback子命令,内容需包含问题现象与上下文 - 若
help中无匹配子命令,如实告知用户当前不支持
3. 执行并输出——按下方格式呈现结果
输出格式
单类型请求
1. **标题文字**
来源:媒体名称
时间:发布时间
摘要内容……
[查看原文](https://…)
2. **标题文字**
来源:媒体名称
时间:发布时间
摘要内容……
[查看原文](https://…)
**来源:腾讯新闻**多类型请求
按类型分组,每组用二级标题标明类别:
## 热点新闻
1. **标题文字**
...
2. **标题文字**
...
## 财经新闻
1. **标题文字**
...
2. **标题文字**
...
**来源:腾讯新闻**通用规则
- 标题:
序号. **标题**,序号从 1 开始,多类型时每组序号独立 - 来源:
来源:后跟 CLI 返回的作者或媒体名称;无该字段时省略 - 时间:
时间:后跟 CLI 返回的发布时间;无该字段时省略 - 摘要:来源下方紧跟;无摘要字段时省略
- 原文链接:有链接则输出
[查看原文](URL),无则不输出 - 其他有价值字段(发布时间、标签等)可在来源下方补充
- 多条新闻间用空行分隔
**来源:腾讯新闻**在所有内容末尾出现一次- 某个类型获取失败时,在该分组下说明原因,继续输出其余分组
CLI 执行失败处理
CLI 命令失败后,立即停止,绝不通过 WebSearch 或其他方式获取新闻替代。
1. CLI 返回非零退出码、超时或输出含权限/安全错误时,不要重试,不要换方式。 2. 根据错误信息引导用户:
- macOS Gatekeeper(
cannot be opened、not verified)→ 系统设置 → 隐私与安全性 → 「仍要打开」 - 企业安全软件(
connection refused、防火墙拦截)→ 安全提示中点击「信任」/「允许」 - 权限不足(
permission denied)→chmod +x <cliPath> - 其他 → 展示完整错误,请用户处理
3. 用户确认操作完成后再重试。即使多次失败,也只能告知无法获取并说明原因,绝不回退到其他信息源。
References
- 用户手动安装指南:`references/installation-guide.md`
- 用户手动更新指南:`references/update-guide.md`
- API Key 获取与手动配置:`references/env-setup-guide.md`
TENCENT_NEWS_APIKEY 配置指南
本指南面向用户,用于手动获取和配置 API Key。
获取 API Key
1. 打开浏览器访问 API Key 获取页面 2. 按页面引导完成获取
设置 API Key
打开终端(macOS / Linux)或 PowerShell(Windows),执行:
tencent-news-cli apikey-set YOUR_KEYYOUR_KEY 替换为实际获取到的 Key 值,不需要加引号。验证:
tencent-news-cli apikey-get清除 API Key
仅在需要重置时执行:
tencent-news-cli apikey-clear常见问题
- `API Key 无效` → 重新前往获取页面生成新 Key
- `operation not permitted` → 确认在有写入权限的终端中执行命令
- 找不到 `tencent-news-cli` 命令 → 重新打开终端,或参考 安装指南 重新安装
tencent-news-cli 手动安装指南
本指南面向用户。通常 AI 助手会通过技能脚本自动完成安装,只有在脚本安装失败时才需要参考此指南手动操作。
macOS / Linux
打开终端,执行以下命令:
curl -fsSL https://mat1.gtimg.com/qqcdn/qqnews/cli/hub/tencent-news/setup.sh | sh脚本会自动完成:识别系统和架构 → 下载 CLI → 验证 → 配置环境变量 → 检测 API Key 状态。
安装完成后重新打开终端(或执行 source ~/.zshrc),运行 tencent-news-cli help 确认安装成功。
Windows
打开 PowerShell,执行以下命令:
irm https://mat1.gtimg.com/qqcdn/qqnews/cli/hub/tencent-news/setup.ps1 | iex安装完成后重新打开 PowerShell,运行 tencent-news-cli help 确认安装成功。
故障排查
- macOS 安全提示("无法打开" / "未验证的开发者")→ 前往「系统设置 → 隐私与安全性」,点击「仍要打开」
- Windows SmartScreen 拦截 → 在系统提示中选择「更多信息」后允许运行
- 下载失败 → 检查网络连接,确认 CDN 地址
mat1.gtimg.com可达 - `unsupported os` 或 `unsupported architecture` → 当前平台不在支持范围内
tencent-news-cli 手动更新指南
本指南面向用户。通常 AI 助手会通过技能脚本自动完成更新,只有在脚本更新失败时才需要参考此指南手动操作。
直接更新
打开终端,执行以下命令:
tencent-news-cli update如果你手里拿到的是 CLI 完整路径,也可以直接在该路径后追加 update。
更新命令不可用时
说明当前 CLI 版本过旧或未正确安装。此时改用安装脚本重新安装最新版本:
macOS / Linux:
curl -fsSL https://mat1.gtimg.com/qqcdn/qqnews/cli/hub/tencent-news/setup.sh | shWindows:
irm https://mat1.gtimg.com/qqcdn/qqnews/cli/hub/tencent-news/setup.ps1 | iex验证更新
更新完成后重新打开终端,运行以下命令查看版本信息:
tencent-news-cli version故障排查
- 更新后仍显示旧版本 → 确认终端已重新打开,或运行
source ~/.zshrc(macOS/Linux)刷新环境 - 下载失败 → 检查网络连接,确认 CDN 地址
mat1.gtimg.com可达 - Windows 更新失败 → 检查是否被 SmartScreen、杀软或文件占用拦截
import { existsSync } from "node:fs";
import { chmod, mkdir, rename, unlink } from "node:fs/promises";
import { createHash } from "node:crypto";
export const BASE_DOWNLOAD_URL = "https://mat1.gtimg.com/qqcdn/qqnews/cli/hub";
export const DEFAULT_CHECKSUM_URL = `${BASE_DOWNLOAD_URL}/checksums.txt`;
const SCRIPT_DIR = import.meta.dir.replaceAll("\\", "/");
export const SKILL_DIR = SCRIPT_DIR.replace(/\/[^/]+$/, "");
export function fail(msg: string): never {
console.error(`Error: ${msg}`);
process.exit(1);
}
export function normalizeApiKey(raw: string): string {
let key = raw.trim();
if ((key.startsWith('"') && key.endsWith('"')) || (key.startsWith("'") && key.endsWith("'"))) {
key = key.slice(1, -1);
}
key = key.replace(/^api[\s_-]*key\s*[:=]\s*/i, "");
return key.trim();
}
function formatError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function parentDir(path: string): string {
const normalized = path.replaceAll("\\", "/");
const idx = normalized.lastIndexOf("/");
if (idx === -1) return ".";
if (idx === 0) return "/";
return normalized.slice(0, idx);
}
function createTempSiblingPath(path: string, label: string): string {
return `${path}.${label}.${process.pid}.${Date.now()}`;
}
async function cleanupFile(path: string) {
await unlink(path).catch(() => {});
}
async function replaceFile(sourcePath: string, targetPath: string) {
if (process.platform !== "win32") {
await rename(sourcePath, targetPath);
return;
}
if (!(await Bun.file(targetPath).exists())) {
await rename(sourcePath, targetPath);
return;
}
const backupPath = createTempSiblingPath(targetPath, "bak");
await rename(targetPath, backupPath);
try {
await rename(sourcePath, targetPath);
} catch (error) {
await rename(backupPath, targetPath).catch(() => {});
throw error;
}
await cleanupFile(backupPath);
}
interface CommandResult {
stdout: string;
stderr: string;
exitCode: number;
output: string;
}
async function runCommand(args: string[], description: string): Promise<CommandResult> {
let proc: ReturnType<typeof Bun.spawn>;
try {
proc = Bun.spawn(args, { stdout: "pipe", stderr: "pipe" });
} catch (error) {
fail(`${description} failed to start: ${formatError(error)}`);
}
const stdoutPromise = new Response(proc.stdout).text();
const stderrPromise = new Response(proc.stderr).text();
const [stdout, stderr, exitCode] = await Promise.all([stdoutPromise, stderrPromise, proc.exited]);
const output = (stdout + stderr).trim();
return { stdout, stderr, exitCode, output };
}
async function runCommandOrFail(args: string[], description: string): Promise<string> {
const { exitCode, output } = await runCommand(args, description);
if (exitCode !== 0) {
fail(`${description} failed with exit code ${exitCode}${output ? `: ${output}` : ""}`);
}
return output;
}
export interface PlatformInfo {
os: string;
arch: string;
isWindows: boolean;
cliFilename: string;
cliPath: string;
cliSource: "global" | "local" | "none";
cliDownloadUrl: string;
}
export interface DetectPlatformOptions {
preferGlobal?: boolean;
}
export function detectPlatform(options: DetectPlatformOptions = {}): PlatformInfo {
const preferGlobal = options.preferGlobal ?? true;
let os: string;
switch (process.platform) {
case "win32": os = "windows"; break;
case "darwin": os = "darwin"; break;
case "linux": os = "linux"; break;
default: fail(`unsupported os: ${process.platform}`);
}
let arch: string;
switch (process.arch) {
case "arm64": arch = "arm64"; break;
case "x64": arch = "amd64"; break;
default: fail(`unsupported architecture: ${process.arch}`);
}
const isWindows = os === "windows";
const cliFilename = isWindows ? "tencent-news-cli.exe" : "tencent-news-cli";
const localCliPath = `${SKILL_DIR}/${cliFilename}`;
const cliDownloadUrl = `${BASE_DOWNLOAD_URL}/${os}-${arch}/${cliFilename}`;
// Detect global CLI: use `where` on Windows, `which` on others
let cliPath = localCliPath;
let cliSource: "global" | "local" | "none" = "none";
if (existsSync(localCliPath)) {
cliPath = localCliPath;
cliSource = "local";
} else if (preferGlobal) {
try {
const whichCmd = isWindows ? "where" : "which";
const proc = Bun.spawnSync([whichCmd, cliFilename], { stdout: "pipe", stderr: "pipe" });
if (proc.exitCode === 0) {
const globalPath = proc.stdout.toString().trim().split(/\r?\n/)[0];
if (globalPath) {
// Verify global CLI is functional by calling help
const helpProc = Bun.spawnSync([globalPath, "help"], { stdout: "pipe", stderr: "pipe" });
if (helpProc.exitCode === 0) {
cliPath = globalPath;
cliSource = "global";
}
}
}
} catch {
// Ignore errors in global detection, fall through to local
}
}
if (cliSource === "none") {
cliPath = localCliPath;
}
return {
os, arch, isWindows, cliFilename, cliPath, cliSource, cliDownloadUrl,
};
}
export function getPlatformJson(p: PlatformInfo) {
return {
os: p.os,
arch: p.arch,
cliPath: p.cliPath,
cliSource: p.cliSource,
};
}
export async function downloadFile(url: string, outputPath: string) {
const resp = await fetch(url);
if (!resp.ok) fail(`download failed: ${resp.status} ${resp.statusText} from ${url}`);
await mkdir(parentDir(outputPath), { recursive: true });
await Bun.write(outputPath, resp);
}
export async function runCliVersion(cliPath: string): Promise<string> {
if (!(await Bun.file(cliPath).exists())) fail(`cli not found at ${cliPath}`);
if (process.platform !== "win32") {
await chmod(cliPath, 0o755).catch(() => {});
}
return runCommandOrFail([cliPath, "version"], `${cliPath} version`);
}
export interface CliVersionInfo {
current_version?: string;
latest_version?: string;
need_update?: boolean;
release_notes?: string;
download_urls?: Record<string, string>;
}
function getPlatformBinaryPath(downloadUrl: string): string {
let parsedUrl: URL;
try {
parsedUrl = new URL(downloadUrl);
} catch (error) {
fail(`invalid download url for checksum verification: ${downloadUrl}: ${formatError(error)}`);
}
const segments = parsedUrl.pathname.split("/").filter(Boolean);
if (segments.length < 2) {
fail(`could not determine platform path from download url: ${downloadUrl}`);
}
return `${segments[segments.length - 2]}/${segments[segments.length - 1]}`;
}
async function fetchChecksumForPlatform(checksumUrl: string, downloadUrl: string): Promise<string> {
const platformBinaryPath = getPlatformBinaryPath(downloadUrl);
const resp = await fetch(checksumUrl).catch((error: unknown) =>
fail(`failed to fetch checksums from ${checksumUrl}: ${formatError(error)}`),
);
if (!resp.ok) {
fail(`failed to fetch checksums from ${checksumUrl}: ${resp.status} ${resp.statusText}`);
}
const text = await resp.text().catch((error: unknown) =>
fail(`failed to read checksums from ${checksumUrl}: ${formatError(error)}`),
);
for (const line of text.split("\n")) {
const trimmed = line.trim();
if (!trimmed) continue;
// format: "<sha256> <path>" (two spaces between hash and path)
const match = trimmed.match(/^([0-9a-fA-F]{64})\s+(.+)$/);
if (match) {
const [, hash, filePath] = match;
if (filePath === platformBinaryPath) {
return hash.toLowerCase();
}
}
}
fail(`no matching checksum found for ${platformBinaryPath} in ${checksumUrl}`);
}
async function computeFileSha256(filePath: string): Promise<string> {
const fileContent = await Bun.file(filePath).arrayBuffer();
const hash = createHash("sha256");
hash.update(Buffer.from(fileContent));
return hash.digest("hex");
}
export function parseCliVersionJson(raw: string, context: string): CliVersionInfo {
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
fail(`${context} did not return valid JSON: ${raw || "(empty output)"}`);
}
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
fail(`${context} did not return a JSON object: ${raw || "(empty output)"}`);
}
return parsed as CliVersionInfo;
}
export interface InstallCliResult {
rawVersionOutput: string;
versionInfo: CliVersionInfo;
}
export async function downloadAndInstallCli(
downloadUrl: string,
cliPath: string,
checksumUrl: string,
): Promise<InstallCliResult> {
const tempPath = createTempSiblingPath(cliPath, "download");
try {
await downloadFile(downloadUrl, tempPath);
const expectedHash = await fetchChecksumForPlatform(checksumUrl, downloadUrl);
const actualHash = await computeFileSha256(tempPath).catch((error: unknown) =>
fail(`failed to compute sha256 for ${tempPath}: ${formatError(error)}`),
);
if (actualHash !== expectedHash) {
fail(`checksum verification failed for ${downloadUrl}\n expected: ${expectedHash}\n actual: ${actualHash}`);
}
console.error("Checksum verification passed.");
const rawVersionOutput = await runCliVersion(tempPath);
const versionInfo = parseCliVersionJson(rawVersionOutput, `${tempPath} version`);
await replaceFile(tempPath, cliPath);
if (process.platform !== "win32") {
await chmod(cliPath, 0o755).catch(() => {});
}
return { rawVersionOutput, versionInfo };
} finally {
await cleanupFile(tempPath);
}
}
function extractApiKey(output: string): string | null {
const match = output.match(/API Key\s*:\s*(.+)$/m);
if (!match) return null;
const value = normalizeApiKey(match[1] || "");
return value || null;
}
function includesMissingApiKeyMessage(output: string): boolean {
return /未设置 API Key/i.test(output) || /not set/i.test(output);
}
async function ensureCliExecutable(cliPath: string): Promise<void> {
if (!(await Bun.file(cliPath).exists())) fail(`cli not found at ${cliPath}`);
if (process.platform !== "win32") {
await chmod(cliPath, 0o755).catch(() => {});
}
}
async function runCliCommand(p: PlatformInfo, args: string[]): Promise<CommandResult> {
await ensureCliExecutable(p.cliPath);
return runCommand([p.cliPath, ...args], `${p.cliPath} ${args.join(" ")}`);
}
export interface ApiKeyState {
status: "configured" | "missing" | "error";
present: boolean;
error: string | null;
}
export async function getApiKeyState(p: PlatformInfo): Promise<ApiKeyState> {
if (p.cliSource !== "global" && !(await Bun.file(p.cliPath).exists())) {
return {
status: "error",
present: false,
error: "CLI not found, cannot check API key.",
};
}
const result = await runCliCommand(p, ["apikey-get"]);
const rawOutput = result.output;
if (result.exitCode === 0) {
const key = extractApiKey(rawOutput);
return {
status: key ? "configured" : "error",
present: !!key,
error: key ? null : "CLI apikey-get succeeded, but API key could not be parsed from output.",
};
}
if (includesMissingApiKeyMessage(rawOutput) || result.exitCode === 2) {
return {
status: "missing",
present: false,
error: null,
};
}
return {
status: "error",
present: false,
error: rawOutput || `apikey-get failed with exit code ${result.exitCode}.`,
};
}
#!/bin/sh
set -e
# cli-state.sh — Output install state, version/update status, and API key status.
# Usage: sh scripts/cli-state.sh
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# ── helpers ──────────────────────────────────────────────────────────
fail() { echo "Error: $1" >&2; exit 1; }
json_escape() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | tr '\n' ' '
}
json_bool() {
echo "$1" | grep "\"$2\"" | head -1 | sed 's/.*"'"$2"'"[[:space:]]*:[[:space:]]*\([a-z]*\).*/\1/'
}
# ── argument parsing ─────────────────────────────────────────────────
while [ $# -gt 0 ]; do
case "$1" in
help)
echo "Usage: sh scripts/cli-state.sh"
echo ""
echo "Print install state, version/update status, and API key status."
exit 0
;;
*)
fail "unknown argument: $1"
;;
esac
shift
done
# ── platform detection ───────────────────────────────────────────────
detect_os() {
case "$(uname -s)" in
Darwin) echo "darwin" ;;
Linux) echo "linux" ;;
*) fail "unsupported os: $(uname -s)" ;;
esac
}
detect_arch() {
case "$(uname -m)" in
arm64|aarch64) echo "arm64" ;;
x86_64|amd64) echo "amd64" ;;
*) fail "unsupported architecture: $(uname -m)" ;;
esac
}
OS="$(detect_os)"
ARCH="$(detect_arch)"
CLI_FILENAME="tencent-news-cli"
LOCAL_CLI_PATH="$SKILL_DIR/$CLI_FILENAME"
# ── cli detection: prefer skill-local install, fall back to global ───
CLI_SOURCE="none"
CLI_PATH=""
# Use local CLI first if already installed by the skill.
if [ -f "$LOCAL_CLI_PATH" ]; then
CLI_PATH="$LOCAL_CLI_PATH"
CLI_SOURCE="local"
fi
# Fall back to global CLI if local not found.
if [ -z "$CLI_PATH" ]; then
GLOBAL_CLI_PATH="$(command -v "$CLI_FILENAME" 2>/dev/null || true)"
if [ -n "$GLOBAL_CLI_PATH" ] && "$GLOBAL_CLI_PATH" help >/dev/null 2>&1; then
CLI_PATH="$GLOBAL_CLI_PATH"
CLI_SOURCE="global"
fi
fi
if [ -n "$CLI_PATH" ]; then
CLI_EXISTS="true"
else
CLI_EXISTS="false"
CLI_PATH="$LOCAL_CLI_PATH"
fi
# ── update check ────────────────────────────────────────────────────
UPDATE_NEED_UPDATE="null"
UPDATE_ERROR="null"
if [ "$CLI_EXISTS" = "true" ]; then
chmod +x "$CLI_PATH" 2>/dev/null || true
VERSION_OUTPUT="$("$CLI_PATH" version 2>&1)" && VERSION_EXIT=0 || VERSION_EXIT=$?
if [ "$VERSION_EXIT" -eq 0 ]; then
if echo "$VERSION_OUTPUT" | grep -q '"need_update"'; then
NEED_UPDATE="$(json_bool "$VERSION_OUTPUT" "need_update")"
case "$NEED_UPDATE" in
true|false)
UPDATE_NEED_UPDATE="$NEED_UPDATE"
;;
*)
UPDATE_ERROR="\"$(json_escape "$CLI_PATH version did not return valid need_update value: ${VERSION_OUTPUT:-"(empty output)"}")\""
;;
esac
else
UPDATE_ERROR="\"$(json_escape "$CLI_PATH version did not return valid JSON: ${VERSION_OUTPUT:-"(empty output)"}")\""
fi
elif [ -n "$VERSION_OUTPUT" ]; then
UPDATE_ERROR="\"$(json_escape "$VERSION_OUTPUT")\""
else
UPDATE_ERROR="\"$(json_escape "$CLI_PATH version failed with exit code $VERSION_EXIT.")\""
fi
fi
# ── api key state ────────────────────────────────────────────────────
APIKEY_STATUS="error"
APIKEY_PRESENT="false"
APIKEY_ERROR="null"
if [ "$CLI_EXISTS" = "true" ]; then
chmod +x "$CLI_PATH" 2>/dev/null || true
APIKEY_OUTPUT="$("$CLI_PATH" apikey-get 2>&1)" && APIKEY_EXIT=0 || APIKEY_EXIT=$?
if [ "$APIKEY_EXIT" -eq 0 ]; then
# try to extract API Key value
_key="$(echo "$APIKEY_OUTPUT" | grep -o 'API Key[[:space:]]*:[[:space:]]*.*' | sed 's/API Key[[:space:]]*:[[:space:]]*//' | tr -d '[:space:]')"
# remove surrounding quotes if present
_key="$(echo "$_key" | sed "s/^['\"]//; s/['\"]$//")"
if [ -n "$_key" ]; then
APIKEY_STATUS="configured"
APIKEY_PRESENT="true"
else
APIKEY_STATUS="error"
APIKEY_ERROR="\"CLI apikey-get succeeded, but API key could not be parsed from output.\""
fi
elif echo "$APIKEY_OUTPUT" | grep -qiE '未设置 API Key|not set' || [ "$APIKEY_EXIT" -eq 2 ]; then
APIKEY_STATUS="missing"
else
APIKEY_STATUS="error"
if [ -n "$APIKEY_OUTPUT" ]; then
APIKEY_ERROR="\"$(json_escape "$APIKEY_OUTPUT")\""
else
APIKEY_ERROR="\"apikey-get failed with exit code ${APIKEY_EXIT}.\""
fi
fi
else
APIKEY_STATUS="error"
APIKEY_ERROR="\"CLI not found, cannot check API key.\""
fi
# ── output JSON ──────────────────────────────────────────────────────
cat <<EOF
{
"platform": {
"os": "$OS",
"arch": "$ARCH",
"cliPath": "$CLI_PATH",
"cliSource": "$CLI_SOURCE"
},
"cliExists": $CLI_EXISTS,
"update": {
"needUpdate": $UPDATE_NEED_UPDATE,
"error": $UPDATE_ERROR
},
"apiKey": {
"status": "$APIKEY_STATUS",
"present": $APIKEY_PRESENT,
"error": $APIKEY_ERROR
}
}
EOF
import {
type PlatformInfo,
detectPlatform, getPlatformJson, getApiKeyState,
fail,
} from "./_common.ts";
if (process.argv[2] === "help") {
console.log(
"Usage: bun scripts/cli-state.ts\n\n" +
"Print install state, version/update status, and API key status.",
);
process.exit(0);
}
const args = process.argv.slice(2);
if (args.length > 0) {
fail(`unknown argument: ${args[0]}`);
}
const p = detectPlatform();
const cliExists = p.cliSource === "global" || await Bun.file(p.cliPath).exists();
const cliSource: PlatformInfo["cliSource"] = p.cliSource === "global" ? "global" : (cliExists ? "local" : "none");
const platform: PlatformInfo = { ...p, cliSource };
const update = {
needUpdate: null as boolean | null,
error: null as string | null,
};
if (cliExists) {
try {
const proc = Bun.spawnSync([platform.cliPath, "version"], { stdout: "pipe", stderr: "pipe" });
const rawVersionOutput = (proc.stdout.toString() + proc.stderr.toString()).trim();
if (proc.exitCode !== 0) {
update.error = rawVersionOutput || `${platform.cliPath} version failed with exit code ${proc.exitCode}`;
} else {
let versionInfo: unknown;
try {
versionInfo = JSON.parse(rawVersionOutput);
} catch {
update.error = `${platform.cliPath} version did not return valid JSON: ${rawVersionOutput || "(empty output)"}`;
}
if (!update.error) {
if (!versionInfo || typeof versionInfo !== "object" || Array.isArray(versionInfo)) {
update.error = `${platform.cliPath} version did not return a JSON object: ${rawVersionOutput || "(empty output)"}`;
} else {
const parsed = versionInfo as {
need_update?: unknown;
};
if (typeof parsed.need_update === "boolean") {
update.needUpdate = parsed.need_update;
} else {
update.error = `${platform.cliPath} version did not return a valid need_update value: ${rawVersionOutput || "(empty output)"}`;
}
}
}
}
} catch (error) {
update.error = error instanceof Error ? error.message : String(error);
}
}
console.log(JSON.stringify({
platform: getPlatformJson(platform),
cliExists,
update,
apiKey: await getApiKeyState(platform),
}, null, 2));
#!/bin/sh
set -e
# install-cli.sh — Download the current-platform CLI into the skill directory and verify it.
# Usage: sh scripts/install-cli.sh [--force]
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
BASE_DOWNLOAD_URL="https://mat1.gtimg.com/qqcdn/qqnews/cli/hub"
DEFAULT_CHECKSUM_URL="$BASE_DOWNLOAD_URL/checksums.txt"
FORCE_INSTALL="false"
# ── helpers ──────────────────────────────────────────────────────────
fail() { echo "Error: $1" >&2; exit 1; }
json_escape() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | tr '\n' ' '
}
usage() {
cat <<EOF
Usage: sh scripts/install-cli.sh [--force]
Download the current-platform CLI into the skill directory and verify it.
Use --force to install locally even if a global CLI is available.
EOF
}
compute_sha256() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" | cut -d' ' -f1
return 0
fi
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$1" | cut -d' ' -f1
return 0
fi
return 1
}
extract_platform_path() {
printf '%s\n' "$1" | awk -F/ '{ if (NF < 2) exit 1; print $(NF-1) "/" $NF }'
}
verify_checksum() {
file_path="$1"
checksum_url="$2"
download_url="$3"
if ! CHECKSUM_CONTENT="$(curl -fSL "$checksum_url" 2>/dev/null)"; then
fail "failed to fetch checksums from $checksum_url"
fi
platform_path="$(extract_platform_path "$download_url")" || fail "failed to determine platform path from $download_url"
EXPECTED_HASH="$(printf '%s\n' "$CHECKSUM_CONTENT" | awk -v p="$platform_path" '$2 == p { print $1; exit }')"
if [ -z "$EXPECTED_HASH" ]; then
fail "no matching checksum found for $platform_path in $checksum_url"
fi
ACTUAL_HASH="$(compute_sha256 "$file_path")" || fail "sha256sum or shasum is required for checksum verification"
if [ "$ACTUAL_HASH" != "$EXPECTED_HASH" ]; then
fail "checksum verification failed for $download_url
expected: $EXPECTED_HASH
actual: $ACTUAL_HASH"
fi
echo "Checksum verification passed." >&2
}
while [ $# -gt 0 ]; do
case "$1" in
help|--help|-h)
usage
exit 0
;;
--force)
FORCE_INSTALL="true"
;;
*)
fail "unknown argument: $1"
;;
esac
shift
done
# ── platform detection ───────────────────────────────────────────────
detect_os() {
case "$(uname -s)" in
Darwin) echo "darwin" ;;
Linux) echo "linux" ;;
*) fail "unsupported os: $(uname -s)" ;;
esac
}
detect_arch() {
case "$(uname -m)" in
arm64|aarch64) echo "arm64" ;;
x86_64|amd64) echo "amd64" ;;
*) fail "unsupported architecture: $(uname -m)" ;;
esac
}
OS="$(detect_os)"
ARCH="$(detect_arch)"
CLI_FILENAME="tencent-news-cli"
LOCAL_CLI_PATH="$SKILL_DIR/$CLI_FILENAME"
DOWNLOAD_URL="$BASE_DOWNLOAD_URL/$OS-$ARCH/$CLI_FILENAME"
CLI_PATH="$LOCAL_CLI_PATH"
# ── check for global CLI first ──────────────────────────────────────
if [ "$FORCE_INSTALL" != "true" ] && [ ! -f "$LOCAL_CLI_PATH" ]; then
GLOBAL_CLI_PATH="$(command -v "$CLI_FILENAME" 2>/dev/null || true)"
if [ -n "$GLOBAL_CLI_PATH" ]; then
# Verify global CLI is functional
if "$GLOBAL_CLI_PATH" help >/dev/null 2>&1; then
# Extract version info from global CLI
RAW_VERSION_OUTPUT="$("$GLOBAL_CLI_PATH" version 2>&1)" || fail "global CLI version check failed: $RAW_VERSION_OUTPUT"
echo "$RAW_VERSION_OUTPUT" | grep -q '"current_version"' || fail "global CLI version did not return valid JSON: $RAW_VERSION_OUTPUT"
CURRENT_VERSION="$(echo "$RAW_VERSION_OUTPUT" | grep '"current_version"' | head -1 | sed 's/.*"current_version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')"
LATEST_VERSION="$(echo "$RAW_VERSION_OUTPUT" | grep '"latest_version"' | head -1 | sed 's/.*"latest_version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')"
if [ -n "$CURRENT_VERSION" ]; then
CURRENT_VERSION_JSON="\"$CURRENT_VERSION\""
else
CURRENT_VERSION_JSON="null"
fi
if [ -n "$LATEST_VERSION" ]; then
LATEST_VERSION_JSON="\"$LATEST_VERSION\""
else
LATEST_VERSION_JSON="null"
fi
RAW_ESCAPED="$(json_escape "$RAW_VERSION_OUTPUT")"
cat <<EOF
{
"installed": true,
"source": "global",
"platform": {
"os": "$OS",
"arch": "$ARCH",
"cliPath": "$GLOBAL_CLI_PATH"
},
"downloadUrl": null,
"currentVersion": $CURRENT_VERSION_JSON,
"latestVersion": $LATEST_VERSION_JSON,
"rawVersionOutput": "$RAW_ESCAPED",
"note": "Using globally installed CLI. No download needed."
}
EOF
exit 0
fi
fi
fi
# ── download and install ────────────────────────────────────────────
TEMP_PATH="${CLI_PATH}.download.$$.$( date +%s )"
cleanup() { rm -f "$TEMP_PATH"; }
trap cleanup EXIT
echo "Downloading CLI from $DOWNLOAD_URL ..." >&2
curl -fSL -o "$TEMP_PATH" "$DOWNLOAD_URL" || fail "download failed from $DOWNLOAD_URL"
verify_checksum "$TEMP_PATH" "$DEFAULT_CHECKSUM_URL" "$DOWNLOAD_URL"
chmod +x "$TEMP_PATH"
echo "Verifying CLI ..." >&2
RAW_VERSION_OUTPUT="$("$TEMP_PATH" version 2>&1)" || fail "version check failed: $RAW_VERSION_OUTPUT"
# basic JSON validation
echo "$RAW_VERSION_OUTPUT" | grep -q '"current_version"' || fail "version did not return valid JSON: $RAW_VERSION_OUTPUT"
# move to final location
mv -f "$TEMP_PATH" "$CLI_PATH"
chmod +x "$CLI_PATH"
# ── extract version fields ──────────────────────────────────────────
CURRENT_VERSION="$(echo "$RAW_VERSION_OUTPUT" | grep '"current_version"' | head -1 | sed 's/.*"current_version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')"
LATEST_VERSION="$(echo "$RAW_VERSION_OUTPUT" | grep '"latest_version"' | head -1 | sed 's/.*"latest_version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')"
# ── format nullable fields ──────────────────────────────────────────
if [ -n "$CURRENT_VERSION" ]; then
CURRENT_VERSION_JSON="\"$CURRENT_VERSION\""
else
CURRENT_VERSION_JSON="null"
fi
if [ -n "$LATEST_VERSION" ]; then
LATEST_VERSION_JSON="\"$LATEST_VERSION\""
else
LATEST_VERSION_JSON="null"
fi
# ── output JSON ──────────────────────────────────────────────────────
RAW_ESCAPED="$(json_escape "$RAW_VERSION_OUTPUT")"
cat <<EOF
{
"installed": true,
"source": "local",
"platform": {
"os": "$OS",
"arch": "$ARCH",
"cliPath": "$CLI_PATH"
},
"downloadUrl": "$DOWNLOAD_URL",
"currentVersion": $CURRENT_VERSION_JSON,
"latestVersion": $LATEST_VERSION_JSON,
"rawVersionOutput": "$RAW_ESCAPED"
}
EOF
import {
detectPlatform, downloadAndInstallCli, runCliVersion, parseCliVersionJson, fail,
DEFAULT_CHECKSUM_URL,
} from "./_common.ts";
let forceInstall = false;
for (const arg of process.argv.slice(2)) {
if (arg === "help" || arg === "--help" || arg === "-h") {
console.log("Usage: bun scripts/install-cli.ts [--force]\n\nDownload the current-platform CLI into the skill directory and verify it.");
process.exit(0);
}
if (arg === "--force") {
forceInstall = true;
continue;
}
fail(`unknown argument: ${arg}`);
}
const p = detectPlatform({ preferGlobal: !forceInstall });
// If global CLI is available, skip download
if (p.cliSource === "global") {
const rawVersionOutput = await runCliVersion(p.cliPath);
const versionInfo = parseCliVersionJson(rawVersionOutput, `${p.cliPath} version`);
const currentVersion = versionInfo.current_version ?? null;
const latestVersion = versionInfo.latest_version ?? null;
console.log(JSON.stringify({
installed: true,
source: "global",
platform: { os: p.os, arch: p.arch, cliPath: p.cliPath },
downloadUrl: null,
currentVersion,
latestVersion,
rawVersionOutput,
note: "Using globally installed CLI. No download needed.",
}, null, 2));
process.exit(0);
}
const downloadUrl = p.cliDownloadUrl;
const { rawVersionOutput, versionInfo } = await downloadAndInstallCli(downloadUrl, p.cliPath, DEFAULT_CHECKSUM_URL);
const currentVersion = versionInfo.current_version ?? null;
const latestVersion = versionInfo.latest_version ?? null;
console.log(JSON.stringify({
installed: true,
source: "local",
platform: { os: p.os, arch: p.arch, cliPath: p.cliPath },
downloadUrl,
currentVersion,
latestVersion,
rawVersionOutput,
}, null, 2));
Related skills
FAQ
How does tencent-news get news?
It calls a bundled tencent-news-cli, reading the CLI help output to map the user's request to the right subcommand instead of hardcoding commands.
Does it need an API key?
Yes. A key from news.qq.com is set via the CLI apikey-set command; the skill checks apiKey.status before fetching.