Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
N
News Hub Skills
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Back End
News Hub Skills
Commits
71c850ae
Commit
71c850ae
authored
Aug 14, 2026
by
谢宇轩
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: edit bloomberg bundles
parent
7dfdb0e2
Changes
7
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
1371 additions
and
12 deletions
+1371
-12
.gitignore
.gitignore
+4
-0
SKILL.md
skills/news-hub-feed/SKILL.md
+184
-12
api-client.js
skills/news-hub-feed/scripts/api-client.js
+45
-0
digest.js
skills/news-hub-feed/scripts/digest.js
+516
-0
profile.js
skills/news-hub-feed/scripts/profile.js
+344
-0
report.js
skills/news-hub-feed/scripts/report.js
+262
-0
bloomberg-technology.nhsource.json
...s/news-source-analyzer/bloomberg-technology.nhsource.json
+16
-0
No files found.
.gitignore
View file @
71c850ae
...
...
@@ -17,6 +17,10 @@ node_modules/
# authored locally — the bundle is the committed unit, not the helper.
helpers/
# User preferences and research reports (news-hub-feed)
skills/news-hub-feed/profile.json
skills/news-hub-feed/reports/
# IDE / agent workspace files
.zcode/
AGENTS.md
skills/news-hub-feed/SKILL.md
View file @
71c850ae
This diff is collapsed.
Click to expand it.
skills/news-hub-feed/scripts/api-client.js
View file @
71c850ae
...
...
@@ -20,6 +20,8 @@ const path = require('path');
const
SKILL_DIR
=
path
.
dirname
(
__dirname
);
// .../skills/news-hub-feed
const
CONFIG_PATH
=
path
.
join
(
SKILL_DIR
,
'config.json'
);
const
TEMPLATE_PATH
=
path
.
join
(
SKILL_DIR
,
'references'
,
'config-template.json'
);
const
PROFILE_PATH
=
path
.
join
(
SKILL_DIR
,
'profile.json'
);
const
REPORTS_DIR
=
path
.
join
(
SKILL_DIR
,
'reports'
);
// Scopes this skill needs for full functionality.
const
REQUIRED_SCOPES
=
[
'articles:read'
,
'marks:read'
,
'marks:write'
];
...
...
@@ -307,10 +309,50 @@ function truncate(s, maxLen) {
return
s
.
slice
(
0
,
maxLen
-
1
)
+
'…'
;
}
/**
* Ensure a directory exists (recursive mkdir).
* @param {string} dir
*/
function
ensureDir
(
dir
)
{
if
(
!
fs
.
existsSync
(
dir
))
{
fs
.
mkdirSync
(
dir
,
{
recursive
:
true
});
}
}
/**
* Return today's date as YYYY-MM-DD (local timezone).
* @returns {string}
*/
function
todayStr
()
{
const
d
=
new
Date
();
const
y
=
d
.
getFullYear
();
const
m
=
String
(
d
.
getMonth
()
+
1
).
padStart
(
2
,
'0'
);
const
day
=
String
(
d
.
getDate
()).
padStart
(
2
,
'0'
);
return
`
${
y
}
-
${
m
}
-
${
day
}
`
;
}
/**
* Convert a string into a safe filename component.
* Replaces spaces and special chars with underscores, keeps CJK chars.
* @param {string} s
* @returns {string}
*/
function
sanitizeFilename
(
s
)
{
if
(
!
s
)
return
'untitled'
;
return
s
.
replace
(
/
[\/\\
:*?"<>|
]
/g
,
'_'
)
.
replace
(
/
\s
+/g
,
'_'
)
.
replace
(
/_+/g
,
'_'
)
.
replace
(
/^_|_$/g
,
''
)
.
slice
(
0
,
60
)
||
'untitled'
;
}
module
.
exports
=
{
SKILL_DIR
,
CONFIG_PATH
,
TEMPLATE_PATH
,
PROFILE_PATH
,
REPORTS_DIR
,
REQUIRED_SCOPES
,
loadConfig
,
requireApi
,
...
...
@@ -324,4 +366,7 @@ module.exports = {
parseMarksCsv
,
fmtTime
,
truncate
,
ensureDir
,
todayStr
,
sanitizeFilename
,
};
skills/news-hub-feed/scripts/digest.js
0 → 100644
View file @
71c850ae
This diff is collapsed.
Click to expand it.
skills/news-hub-feed/scripts/profile.js
0 → 100644
View file @
71c850ae
This diff is collapsed.
Click to expand it.
skills/news-hub-feed/scripts/report.js
0 → 100644
View file @
71c850ae
'use strict'
;
/**
* report.js — Research report helper for news-hub-feed.
*
* Two sub-commands:
*
* fetch — batch-fetch article details for the agent to synthesize into a report.
* node scripts/report.js fetch <csv-of-ids> [--fields summary,original,translation] [--json]
*
* save — save a finished Markdown report to reports/ with frontmatter.
* node scripts/report.js save --title <title> [--articles <csv-ids>] --stdin
* node scripts/report.js save --title <title> [--articles <csv-ids>] --content <path>
*
* The agent writes the report body; this script handles the API fetching and
* the file I/O (ensuring frontmatter, directory, filename).
*/
const
fs
=
require
(
'fs'
);
const
path
=
require
(
'path'
);
const
{
loadConfig
,
requireApi
,
apiRequest
,
parseArgs
,
csvSplit
,
fmtTime
,
ensureDir
,
todayStr
,
sanitizeFilename
,
REPORTS_DIR
,
}
=
require
(
'./api-client'
);
const
HELP
=
`\
用法:
node scripts/report.js fetch <id1,id2,...> [--fields summary,original,translation] [--json]
node scripts/report.js save --title <title> [--articles <csv-ids>] --stdin
node scripts/report.js save --title <title> [--articles <csv-ids>] --content <file-path>
`
;
// ---------------------------------------------------------------------------
// fetch sub-command
// ---------------------------------------------------------------------------
/**
* Batch-fetch article details.
* @param {object} cfg
* @param {string[]} ids — array of numeric string IDs
* @param {string} fields — comma-separated fields param
* @returns {Promise<Array>}
*/
async
function
fetchArticles
(
cfg
,
ids
,
fields
)
{
const
results
=
[];
for
(
const
id
of
ids
)
{
const
query
=
{};
if
(
fields
)
query
.
fields
=
fields
;
try
{
const
article
=
await
apiRequest
(
cfg
,
'GET'
,
`/articles/
${
id
}
`
,
{
query
});
results
.
push
(
article
);
}
catch
(
err
)
{
results
.
push
({
articleId
:
id
,
error
:
err
.
message
,
code
:
err
.
code
||
'ERROR'
,
});
}
}
return
results
;
}
async
function
cmdFetch
(
cfg
,
positional
,
flags
)
{
if
(
positional
.
length
<
1
)
{
console
.
error
(
'用法: node scripts/report.js fetch <id1,id2,...> [--fields ...] [--json]'
);
process
.
exit
(
1
);
}
// Accept both CSV and space-separated IDs
let
ids
;
if
(
positional
[
0
].
includes
(
','
))
{
ids
=
csvSplit
(
positional
[
0
]);
}
else
{
ids
=
positional
.
slice
();
}
// Validate all IDs are numeric
for
(
const
id
of
ids
)
{
if
(
!
/^
\d
+$/
.
test
(
id
))
{
console
.
error
(
`✗ 文章 ID 必须是正整数,得到:
${
id
}
`
);
process
.
exit
(
1
);
}
}
const
fields
=
flags
.
fields
||
'summary,translation'
;
console
.
error
(
`正在拉取
${
ids
.
length
}
篇文章 (fields=
${
fields
}
)…`
);
const
articles
=
await
fetchArticles
(
cfg
,
ids
,
fields
);
// Check for errors
const
errors
=
articles
.
filter
((
a
)
=>
a
.
error
);
if
(
errors
.
length
>
0
)
{
console
.
error
(
`⚠️
${
errors
.
length
}
篇拉取失败:`
);
for
(
const
e
of
errors
)
{
console
.
error
(
` #
${
e
.
articleId
}
:
${
e
.
error
}
`
);
}
}
if
(
flags
.
json
)
{
process
.
stdout
.
write
(
JSON
.
stringify
(
articles
,
null
,
2
)
+
'
\
n'
);
}
else
{
// Print a compact summary for agent reference
console
.
log
(
`\n已拉取
${
articles
.
length
-
errors
.
length
}
/
${
ids
.
length
}
篇文章:\n`
);
for
(
const
a
of
articles
)
{
if
(
a
.
error
)
{
console
.
log
(
` #
${
a
.
articleId
}
❌
${
a
.
error
}
`
);
continue
;
}
console
.
log
(
` #
${
a
.
articleId
}
${
a
.
titleZh
||
a
.
titleEn
||
'(无标题)'
}
`
);
console
.
log
(
`
${
a
.
source
||
''
}
|
${
a
.
category
||
''
}
|
${
fmtTime
(
a
.
publishedAt
)}
`
);
if
(
a
.
tags
&&
a
.
tags
.
length
>
0
)
{
console
.
log
(
` tags:
${
a
.
tags
.
join
(
', '
)}
`
);
}
if
(
a
.
summaryOverview
)
{
console
.
log
(
` 概述:
${
a
.
summaryOverview
}
`
);
}
console
.
log
();
}
}
}
// ---------------------------------------------------------------------------
// save sub-command
// ---------------------------------------------------------------------------
async
function
cmdSave
(
positional
,
flags
)
{
if
(
!
flags
.
title
)
{
console
.
error
(
'✗ --title 是必需的'
);
console
.
error
(
HELP
);
process
.
exit
(
1
);
}
let
content
;
if
(
flags
.
stdin
)
{
// Read from stdin
content
=
fs
.
readFileSync
(
0
,
'utf8'
);
}
else
if
(
flags
.
content
)
{
if
(
!
fs
.
existsSync
(
flags
.
content
))
{
console
.
error
(
`✗ 文件不存在:
${
flags
.
content
}
`
);
process
.
exit
(
1
);
}
content
=
fs
.
readFileSync
(
flags
.
content
,
'utf8'
);
}
else
{
console
.
error
(
'✗ 需要 --stdin 或 --content <path> 之一'
);
console
.
error
(
HELP
);
process
.
exit
(
1
);
}
// Parse article IDs
const
articleIds
=
flags
.
articles
?
csvSplit
(
flags
.
articles
)
:
[];
// Extract categories from content? No — let the agent provide them.
// Build frontmatter
const
date
=
todayStr
();
const
now
=
new
Date
().
toISOString
().
replace
(
/
\.\d{3}
Z$/
,
'Z'
);
const
frontmatter
=
{
date
,
title
:
flags
.
title
,
articles
:
articleIds
,
generatedAt
:
now
,
};
const
frontmatterYaml
=
formatFrontmatter
(
frontmatter
);
// Combine: frontmatter + content (strip existing frontmatter from content if present)
const
bodyContent
=
stripFrontmatter
(
content
);
const
fullReport
=
`---\n
${
frontmatterYaml
}
---\n\n
${
bodyContent
.
trim
()}
\n`
;
// Ensure reports directory exists
ensureDir
(
REPORTS_DIR
);
// Generate filename
const
filename
=
`
${
date
}
_
${
sanitizeFilename
(
flags
.
title
)}
.md`
;
const
filepath
=
path
.
join
(
REPORTS_DIR
,
filename
);
fs
.
writeFileSync
(
filepath
,
fullReport
);
console
.
log
(
`✓ 报告已保存:
${
filepath
}
`
);
// Update briefing history if there's a matching entry for today
try
{
const
{
loadProfile
,
saveProfile
}
=
require
(
'./profile'
);
const
profile
=
loadProfile
();
const
todayEntry
=
profile
.
briefingHistory
.
find
((
h
)
=>
h
.
date
===
date
);
if
(
todayEntry
)
{
todayEntry
.
reportGenerated
=
`reports/
${
filename
}
`
;
saveProfile
(
profile
);
}
}
catch
{
// Profile may not exist yet — non-fatal
}
}
function
formatFrontmatter
(
fm
)
{
const
lines
=
[];
lines
.
push
(
`date:
${
fm
.
date
}
`
);
lines
.
push
(
`title: "
${
fm
.
title
.
replace
(
/"/g
,
'
\\
"'
)}
"`
);
if
(
fm
.
articles
.
length
>
0
)
{
lines
.
push
(
`articles: [
${
fm
.
articles
.
join
(
', '
)}
]`
);
}
else
{
lines
.
push
(
`articles: []`
);
}
lines
.
push
(
`generatedAt:
${
fm
.
generatedAt
}
`
);
return
lines
.
join
(
'
\
n'
)
+
'
\
n'
;
}
function
stripFrontmatter
(
text
)
{
if
(
text
.
startsWith
(
'---
\
n'
))
{
const
end
=
text
.
indexOf
(
'
\
n---
\
n'
,
4
);
if
(
end
!==
-
1
)
{
return
text
.
slice
(
end
+
5
);
}
}
return
text
;
}
// ---------------------------------------------------------------------------
// Main dispatch
// ---------------------------------------------------------------------------
async
function
main
()
{
const
{
flags
,
positional
}
=
parseArgs
(
process
.
argv
.
slice
(
2
));
const
subcommand
=
positional
[
0
];
const
rest
=
positional
.
slice
(
1
);
if
(
!
subcommand
||
subcommand
===
'help'
||
subcommand
===
'--help'
||
subcommand
===
'-h'
)
{
console
.
log
(
HELP
);
return
;
}
switch
(
subcommand
)
{
case
'fetch'
:
{
const
cfg
=
loadConfig
();
requireApi
(
cfg
);
await
cmdFetch
(
cfg
,
rest
,
flags
);
break
;
}
case
'save'
:
await
cmdSave
(
rest
,
flags
);
break
;
default
:
console
.
error
(
`✗ 未知子命令:
${
subcommand
}
\n`
);
console
.
log
(
HELP
);
process
.
exit
(
1
);
}
}
module
.
exports
=
{
fetchArticles
,
formatFrontmatter
,
stripFrontmatter
};
main
().
catch
((
err
)
=>
{
console
.
error
(
`\n✗
${
err
.
message
}
`
);
process
.
exit
(
1
);
});
skills/news-source-analyzer/bloomberg-technology.nhsource.json
0 → 100644
View file @
71c850ae
{
"format"
:
"news-harvester-source"
,
"version"
:
1
,
"exportedAt"
:
"2026-08-14T02:47:27.378Z"
,
"source"
:
{
"id"
:
"bloomberg-technology"
,
"name"
:
"彭博社科技"
,
"homepageUrl"
:
"https://www.bloomberg.com/technology"
,
"method"
:
"browser"
,
"vaultFolder"
:
"Bloomberg/Technology"
},
"files"
:
{
"helpers/bloomberg-technology.md"
:
"---
\n
source: bloomberg-technology
\n\n
# ── 发现阶段 ──
\n
# Bloomberg Technology 首页文章分散在多个 LineupContent* 卡片容器 +
\n
# styles_itemTextContainer 列表中,用复合 listSelector 一次性圈中。
\n
listSelector: '[class*=
\"
LineupContent
\"
], [class*=
\"
itemTextContainer
\"
]'
\n
linkSelector: 'a[href]'
\n\n
# 文章 URL 形如 /news/articles/YYYY-MM-DD/<slug> 或 /news/features/YYYY-MM-DD/<slug>
\n
# 视频 /news/videos/ 用 excludeUrlPattern 排除。
\n
urlPattern: 'bloomberg
\\
.com/news/(articles|features)/.+'
\n
excludeUrlPattern: 'bloomberg
\\
.com/news/videos/'
\n\n
scrollSteps: 3
\n
scrollWaitMs: 2000
\n
waitMs: 20000
\n
maxLinks: 30
\n\n
# ── 提取阶段 ──
\n
# Bloomberg 正文段落 class 为 ArticleBodyText_articleBodyContent__xxxxx,
\n
# 直接命中正文 <p>,避免误抓 newsletter / MostRead 等侧边栏内容。
\n
# body-content p 作为 fallback(整个正文容器内的所有 <p>)。
\n
bodySelectors:
\n
- '[class*=
\"
ArticleBodyText
\"
]'
\n
- '[data-testid=
\"
paragraph
\"
]'
\n
- '[class*=
\"
body-content
\"
] p'
\n
- '[class*=
\"
articleBodyContent
\"
] p'
\n
- '[class*=
\"
ArticleBody
\"
] p'
\n
- '[class*=
\"
article-body
\"
] p'
\n
- 'article p'
\n
bodyMinParagraphs: 3
\n\n
dateSelector: 'time[datetime]'
\n
# Bloomberg 作者署名在 byline 容器中,JSON-LD 作为最终 fallback。
\n
authorSelector: '[rel=
\"
author
\"
], [class*=
\"
author
\"
] a, [class*=
\"
byline
\"
] a, [class*=
\"
Byline
\"
] a'
\n\n
imageSelector: 'img'
\n
imageMinWidth: 200
\n
excludeImage:
\n
- logo
\n
- icon
\n
- avatar
\n
- profile
\n\n
# Bloomberg 文末常见署名/订阅推广噪声 + 推荐文章标题噪声。
\n
noisePrefixes:
\n
- 'Sign up for'
\n
- 'Subscribe to'
\n
- 'Read more:'
\n
- 'Read More:'
\n
- 'Most Read from Bloomberg'
\n
- 'This article was produced'
\n
- '©2026 Bloomberg'
\n
- 'Bloomberg may send me'
\n
- 'By submitting my information'
\n
- 'By continuing, I agree'
\n\n
titleStripSuffix: ' - Bloomberg$'
\n
maxBodyChars: 15000
\n
---
\n
# bloomberg-technology 抓取备忘
\n\n
- 首页 `https://www.bloomberg.com/technology` 文章卡片分布在多个 `LineupContent*`
\n
容器(4Up/3Up/2Up/Basic)以及 `styles_itemTextContainer` 列表中,inspect 实测
\n
首屏约 22 篇文章链接 + 4 个视频。用复合 `listSelector` 圈中所有卡片。
\n
- `urlPattern` 限定 `/news/(articles|features)/.+`,天然排除 `/technology/ai` 等
\n
section 页与 `/markets` 等导航;`excludeUrlPattern` 排除 `/news/videos/`。
\n
- 反爬/付费墙:Bloomberg 对 headless 有反爬,正文可能因未登录被截断。
\n
如 preview 出现 charCount<500 或 CAPTCHA(exit 70),需
\n
`node scripts/ensure-chromium.js --login --url https://www.bloomberg.com/technology`
\n
在窗口浏览器登录后退出,持久化 cookie 到 userDataDir。
\n
- 正文段落 class 为 `ArticleBodyText_articleBodyContent__xxxxx`(哈希后缀会变),
\n
用 `[class*=
\"
ArticleBodyText
\"
]` 精确命中,避免误抓 newsletter / MostRead 推荐区块。
\n
waitMs=20000 给足页面渲染时间,防止正文容器加载不全时 fallback 兜底抓到推荐标题。
\n
- 作者署名在 byline 容器中,authorSelector 已覆盖 `[class*=
\"
byline
\"
] a`;
\n
若 CSS 选择器全部未命中,fetcher-helper 会自动 fallback 到 JSON-LD `author` 字段。
\n
- 验证: node scripts/preview.js --url https://www.bloomberg.com/technology
\\\n
--recipe helpers/bloomberg-technology.md --count 3
\n
"
,
"helpers/bloomberg-technology.fixtures.json"
:
"{
\n
\"
articles
\"
: [
\n
\"
https://www.bloomberg.com/news/articles/2026-08-13/trump-enlists-private-sector-to-boost-cyber-offensive-arsenal
\"
,
\n
\"
https://www.bloomberg.com/news/articles/2026-08-13/anthropic-said-in-talks-to-buy-ai-startup-decart-for-6-billion
\"
,
\n
\"
https://www.bloomberg.com/news/features/2026-08-12/thousands-of-india-workers-are-helping-ai-firms-train-robots-to-replace-them
\"\n
],
\n
\"
sections
\"
: [
\n
\"
https://www.bloomberg.com/technology/ai
\"
,
\n
\"
https://www.bloomberg.com/technology/big-tech
\"\n
],
\n
\"
minLinks
\"
: 10
\n
}
\n
"
}
}
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment