diff --git a/AGENTS.md b/AGENTS.md index 9ca1b12..b75eb31 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,10 +9,16 @@ ## 技术约束 - **不要引入框架或构建工具** —— 项目是纯 PHP + jQuery + ECharts,没有 webpack、vite、composer autoload 之外的依赖管理。新增功能沿用现有模式即可。 -- **不要动 `inc/config.php`** —— 该文件包含数据库连接逻辑且部分编码加密,手工修改可能破坏现有功能。 -- **PHP 版本** —— 代码兼容 PHP 7.x/8.x,使用了 `mysqli`、cURL、Composer autoload。不要使用 PHP 8.1+ 独有的特性(如枚举、readonly 等)。 +- **不要动 `inc/config.php`** —— 该文件含**明文**数据库凭据和 `TUSHARE_API_TOKEN`,是敏感文件;手工修改可能破坏连接逻辑。也**不要提交到公开仓库**。 +- **PHP 版本** —— 代码兼容 PHP 7.x/8.x(本地为 PHP 8.4),使用了 `mysqli`、cURL、Composer autoload。不要使用 PHP 8.1+ 独有的特性(如枚举、readonly 等)。 - **所有用户输入走 `$_REQUEST`** —— GET 和 POST 统一处理,不需要区分。 -- **图表数据传递模式** —— 后端 PHP 查库 → `json_encode()` 注入 ` + + + + + + +
+
+
+ +

投资资讯日报

+ +
+ + 返回门户 + +
+
+ +
+ +
+
+
+ 全部 + 国内 + 国际 +
+
+ + + + + +
+
+
+
+ + +
+
+
+ + + + + + +
+ + + + + + diff --git a/continuation.md b/continuation.md index b32317f..cfce2de 100644 --- a/continuation.md +++ b/continuation.md @@ -1,46 +1,44 @@ -# Continuation +# continuation.md — 会话检查点 + +> 生成时间:2026-08-04(本会话) +> 工作区:/Users/summer/Downloads/py/www(echart 平台,PHP + jQuery + ECharts,无框架) ## 当前状态 -index-2.php 研究报告卡片重构完成并已部署,getBasic.inc.php 解析错误已修复。GitHub 认证未配置,推送被阻塞。 +本会话完成四件事,均已部署上线: -## 已完成 +### 1. 项目文档刷新(init) +- 重写 `AGENTS.md`,外科手术式修正 `CLAUDE.md`:Tailwind/ECharts 真实路径(`/lib/js/tailwindcss-3.4.17.js`、`/lib/js/echarts-5.4.2.js`)、已移入 `deprecated/` 的页面清单、三种数据模式(fetch api.doorcome.cn / ajax.inc.php t= 路由 / 服务端注入)、表名约定(index_hist_pro 等已由 API 替代)、config.php 明文凭据说明。 -1. **index-2.php 重构** - - 删除 PDF报告卡片(6 个硬编码 PDF 链接) - - 研究报告卡片改为动态加载:扫描 `research/yyyymmdd/` 格式子目录,生成下拉选择框 - - 选中日期后显示该子目录下所有 HTML 文件链接(用 `` 元素作为链接名) - - 部署到 `simon@www.doorcome.cn:/var/www/html/echart/` +### 2. 服务器 → 本地全量拉取 +- rsync(不带 `--delete`)从 `simon@www.doorcome.cn:/var/www/html/echart/` 拉取 148M、405 文件。新增 `podcast-docs/`(deploy.md、usage.html)、`quant/intlnews_usage.html`、`quant/user_guide.html`、`research/` 每日报告目录(20260608–20260804)。`podcast/` 服务器上为空目录。 +- 文档按用户选择恢复为本地新版(备份在 `/tmp/echart-doc-backup/`)。 -2. **getBasic.inc.php 修复** - - API 替换后残留的多余 `}` 导致部署后 PHP 解析错误(Unmatched `}`) - - 已删除并重新部署 +### 3. 投资资讯日报前端(核心交付) +- 新建 `charts/news_reports.php`:报告查询导航页(类型 Tabs + 日期筛选 + 列表 + 详情一体,Tailwind 本地库,零外部链接)。 +- 新建 `js/newsReports.js`:fetch `https://api.doorcome.cn/api/news/reports/` 渲染。详情固定模块:AI 摘要 → 事件(finance: xwlb→news→cninfo;intl: intl)→ 数据总览。 +- 修改 `index-2.php`:研究报告下拉框卡片 → 链接 `/charts/news_reports.php`,删除 `showReportGroup`。 +- **关键坑**:真实 API `stats` 结构与文档示例差异大且随日期变化(8/1 前后两套:新版 `pipeline(raw_total)+news/cninfo/xwlb`,旧版 `pipeline(M1中文键)+sources/sentiment/importance/event_types`;intl 的 sentiment 是字符串数组、importance 值是字符串、还有合并格式 `{"重要度":"数量","等级 N":"..."}`)。已实现 `normalizeKV()` 归一化,59 份日报全部渲染通过。 +- **Bug 修复**:点击卡片"无法弹出"——根因 `openDetail` 未隐藏 `listView`(详情渲染在列表下方,滚动回顶部后用户看不到)。新增 `showDetail()` 隐藏列表,已部署并用 CDP 真实点击验证(`listView class: hidden` ✓)。 -3. **Chart 页面数据源审计** - - 7 个页面已使用 api.doorcome.cn(无需改动) - - 5 个房地产/资金流向页面使用本地 DB 但无 API 可替 - - 1 个(stockkeydata.php)可用 `/api/finance/` 替代 - - 1 个(stockTradeRecord.php)私人交易数据不可替代 +### 4. 部署工具 sync-echart +- 创建 `~/bin/sync-echart`(rsync 单向推送本地 → `simon@www.doorcome.cn:/var/www/html/echart/`,排除本地环境文件 + 数据目录 `uploads/xls/files/research/podcast/podcast-docs`)。`-n` 预览。 +- PATH 已配:`~/.bash_profile`、`~/.zshrc`(原注释行改为启用)。 +- 部署方式已记录:`CLAUDE.md` 常用命令节 + 项目记忆 `project/sync-echart-deploy.md`。 +- 已执行 3 次部署(页面+JS+index-2 → CLAUDE.md → JS 修复),服务器 md5 全部验证一致。 -4. **Git 提交** - - 本地 commit: `742bed2`(修复多余括号 & 重构研究报告卡片) - - **推送失败**:GitHub 未配置认证 +## 后续步骤 -## 当前工作 +1. 让用户在浏览器**硬刷新**(Cmd+Shift+R)验证 `https://echart.doorcome.cn/charts/news_reports.php`:列表 → 点击卡片 → 详情各模块。 +2. (建议)把 `charts/news_reports.php` 补进 `CLAUDE.md` 的页面目录表,`sync-echart` 部署。 +3. (可选)导航页加"重要事件聚合"视图:`/api/news/events/`(days/importance/report_type/section/limit 参数,已预留 API_EVENTS 常量未用)。 +4. (可选)数据总览改 ECharts 图表版(当前是数字卡 + 表格)。 +5. (可选)`research/` 静态日报 HTML 与 API 页面并存,旧文件仍可直接访问;确认是否需要归档清理。 -- `stockkeydata.php` 可改由 `/api/finance/` 替代,待用户确认后执行 +## 待解决问题 -## 未完成 - -- GitHub 推送阻塞,需先 `gh auth login` 完成认证 -- `moneyflow_hsgt_pro/ext`、`estate_*` 数据陈旧但无 API 可替,维持现状 - -## 关键设计决策 - -- 研究报告卡片改为按日期子目录动态扫描,不再硬编码文件列表 - -## 下一步 - -1. 执行 `gh auth login` 完成 GitHub 认证 -2. 推送 commit 到 remote -3. 确认是否执行 stockkeydata.php 的 `/api/finance/` 替换 \ No newline at end of file +- **本机无 PHP**:`php -l` 语法检查需 scp 到服务器执行(已建立流程)。 +- **数据目录不同步**:`sync-echart` 排除 uploads/xls/files/research/podcast;服务器 ↔ 本地数据同步需手动 rsync(不带 `--delete`)。 +- **API stats 结构不稳定**:随生成日期变化,前端依赖 `normalizeKV()` 防御性兼容;若 API 侧统一格式可简化前端(需与 djapi 后端协调)。 +- **API 文档滞后**:`/Users/summer/Downloads/cc-cursor/docs/news_report_api.md` 中 finance stats 的 `sources` key 实测不存在(新版为 `pipeline.raw_by_source`),文档与实测不一致。 +- **记忆**:曾有一次 `remember` 被拒(后来明确要求后成功保存);如后续会话需重新保存,注意用户偏好。 diff --git a/index-2.php b/index-2.php index afb98f1..4cf04fc 100644 --- a/index-2.php +++ b/index-2.php @@ -144,55 +144,10 @@ </div> <h3 class="text-lg font-semibold">研究报告</h3> </div> - <p class="text-gray-600 text-sm mb-4">按日期查看研究报告。</p> - <?php - $researchDir = __DIR__ . '/research'; - $dateDirs = []; - foreach (glob($researchDir . '/*', GLOB_ONLYDIR) as $d) { - $bn = basename($d); - if (preg_match('/^\d{8}$/', $bn)) { - $dateDirs[] = $bn; - } - } - rsort($dateDirs); - if (empty($dateDirs)): ?> - <div class="p-4 border border-gray-200 rounded-lg text-gray-500 text-center"> - <i class="fa-solid fa-inbox mr-2"></i>暂无日期归档的研究报告 - </div> - <?php else: ?> - <div class="mb-4"> - <select id="reportDate" name="reportDate" - class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50" - onchange="showReportGroup(this.value)"> - <option value="">-- 选择日期 --</option> - <?php foreach ($dateDirs as $d): ?> - <option value="<?= $d ?>"><?= substr($d,0,4).'-'.substr($d,4,2).'-'.substr($d,6,2) ?></option> - <?php endforeach; ?> - </select> - </div> - <?php foreach ($dateDirs as $d): - $htmls = glob("$researchDir/$d/*.html"); - if (empty($htmls)) continue; - ?> - <div class="report-group" id="report-<?= $d ?>" style="display:none;"> - <div class="text-xs text-gray-400 mb-2 font-medium"><?= substr($d,0,4).'-'.substr($d,4,2).'-'.substr($d,6,2) ?></div> - <div class="space-y-2"> - <?php foreach ($htmls as $f): - $content = file_get_contents($f); - preg_match('/<title>(.*?)<\/title>/i', $content, $m); - $title = !empty($m[1]) ? $m[1] : basename($f); - $relPath = ltrim(str_replace(__DIR__, '', $f), '/'); - ?> - <a href="/<?= $relPath ?>" target="_blank" - class="flex items-center p-2 border border-gray-200 rounded-lg hover:bg-gray-50 transition-all text-sm"> - <i class="fa-solid fa-file-lines text-indigo-500 w-5 mr-2"></i> - <span><?= htmlspecialchars($title) ?></span> - </a> - <?php endforeach; ?> - </div> - </div> - <?php endforeach; ?> - <?php endif; ?> + <p class="text-gray-600 text-sm mb-4">国内 / 国际投资资讯日报,按日期分模块查看,含 AI 摘要、新闻联播要闻、重要事件与数据总览。</p> + <a href="/charts/news_reports.php" class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors text-sm font-medium"> + 查看报告 <i class="fa-solid fa-arrow-right ml-2"></i> + </a> </div> <!-- 宏观研究数据卡片 --> <div class="bg-white rounded-xl shadow-md p-6 card-hover"> @@ -278,21 +233,13 @@ <script> // 模拟打开链接的函数 - function openurl(url) { + function openurl(url) { let ts_code = $("#ts_code").val(); url = url + '?ts_code='+ ts_code; console.log(url); window.open(url); } - function showReportGroup(date) { - document.querySelectorAll('.report-group').forEach(el => el.style.display = 'none'); - if (date) { - const target = document.getElementById('report-' + date); - if (target) target.style.display = 'block'; - } - } - // 股票搜索功能 document.getElementById('search-stock').addEventListener('click', function() { const stockCode = document.getElementById('ts_code').value.trim(); diff --git a/js/newsReports.js b/js/newsReports.js new file mode 100644 index 0000000..e93dc41 --- /dev/null +++ b/js/newsReports.js @@ -0,0 +1,476 @@ +/** + * 投资资讯日报 前端逻辑 + * 数据源: https://api.doorcome.cn/api/news/reports/ (日报列表/详情) + * https://api.doorcome.cn/api/news/events/ (重要事件聚合, 预留) + * 模式 B: 页面加载后由 JS fetch 获取数据并渲染, 无服务端注入 + */ +var NewsReports = { + API_REPORTS: 'https://api.doorcome.cn/api/news/reports/', + API_EVENTS: 'https://api.doorcome.cn/api/news/events/', + + // 类型/section 中文映射 + TYPE_NAMES: { finance: '国内投资资讯', intl: '国际投资资讯' }, + SECTION_NAMES: { xwlb: '新闻联播要闻', news: '重要事件 · 财经新闻', cninfo: '重要事件 · 公告/调研/互动', intl: '国际要闻' }, + SECTION_ICONS: { xwlb: 'fa-tower-broadcast', news: 'fa-fire', cninfo: 'fa-clipboard-list', intl: 'fa-earth-asia' }, + SENTIMENT: { positive: { icon: '🟢', cls: 'badge-pos', txt: '利好' }, negative: { icon: '🔴', cls: 'badge-neg', txt: '利空' }, neutral: { icon: '⚪', cls: 'badge-neu', txt: '中性' } }, + + state: { reportType: '', startDate: '', endDate: '', currentId: null }, + + init: function () { + var now = new Date(); + var start = new Date(now.getTime() - 30 * 86400000); + this.state.startDate = this.fmtDate(start); + this.state.endDate = this.fmtDate(now); + + this.bindEvents(); + this.loadReports(); + }, + + fmtDate: function (d) { + return d.getFullYear() + '-' + this.pad2(d.getMonth() + 1) + '-' + this.pad2(d.getDate()); + }, + pad2: function (n) { return n < 10 ? '0' + n : '' + n; }, + + // 时间显示: 2026-08-03T07:00:00 -> 2026-08-03 07:00 + fmtTime: function (iso) { + if (!iso) return ''; + return iso.replace('T', ' ').substring(0, 16); + }, + + bindEvents: function () { + var self = this; + // 类型 Tabs + $('.type-tab').on('click', function () { + $('.type-tab').removeClass('tab-active'); + $(this).addClass('tab-active'); + self.state.reportType = $(this).data('type'); + self.loadReports(); + }); + // 查询按钮 + $('#btnQuery').on('click', function () { + self.state.startDate = $('#startDate').val() || self.state.startDate; + self.state.endDate = $('#endDate').val() || self.state.endDate; + self.loadReports(); + }); + // 返回列表 + $('#btnBack').on('click', function () { + self.showList(); + }); + }, + + // ==================== 列表 ==================== + loadReports: function () { + var self = this; + this.showLoading('正在加载日报列表...'); + var url = this.API_REPORTS + '?start_date=' + this.state.startDate + '&end_date=' + this.state.endDate; + if (this.state.reportType) url += '&report_type=' + this.state.reportType; + + $.getJSON(url) + .done(function (data) { + if (!Array.isArray(data) || data.length === 0) { + self.renderEmpty(); + return; + } + self.state.reports = data; + self.renderList(data); + }) + .fail(function (xhr) { + self.renderError('列表加载失败', xhr); + }); + }, + + renderList: function (reports) { + var self = this; + var html = ''; + reports.forEach(function (r) { + var typeName = self.TYPE_NAMES[r.report_type] || r.report_type; + var typeCls = r.report_type === 'finance' ? 'badge-finance' : 'badge-intl'; + var summary = self.truncate(r.ai_summary || '', 140); + var statBits = self.listStatBits(r); + html += '' + + '<div class="report-card aos-init" data-id="' + r.id + '" onclick="NewsReports.openDetail(' + r.id + ')">' + + '<div class="flex items-start justify-between gap-3">' + + '<div class="flex items-center gap-2 flex-wrap">' + + '<span class="badge ' + typeCls + '">' + typeName + '</span>' + + '<span class="text-lg font-bold text-dark">' + r.report_date + '</span>' + + '<span class="text-sm text-gray-400"><i class="fa-regular fa-clock mr-1"></i>' + self.fmtTime(r.generated_at) + ' 生成</span>' + + '</div>' + + '<button class="btn-view"><i class="fa-solid fa-book-open mr-1"></i>查看</button>' + + '</div>' + + '<p class="mt-2.5 text-base text-gray-600 leading-relaxed line-clamp-3">' + summary + '</p>' + + (statBits ? '<div class="mt-3 flex flex-wrap gap-2">' + statBits + '</div>' : '') + + '</div>'; + }); + $('#reportList').html(html); + this.hideLoading(); + this.updateRangeInfo(reports); + }, + + // 列表卡片上的统计数据小徽章 (防御性读取) + listStatBits: function (r) { + var bits = []; + var s = r.stats || {}; + if (s.xwlb && s.xwlb.total) bits.push('<span class="stat-bit"><i class="fa-solid fa-tower-broadcast text-sky-600"></i>新闻联播 ' + s.xwlb.total + '</span>'); + if (s.news && s.news.total) bits.push('<span class="stat-bit"><i class="fa-solid fa-newspaper text-indigo-600"></i>财经新闻 ' + s.news.total + '</span>'); + if (s.cninfo && s.cninfo.total) bits.push('<span class="stat-bit"><i class="fa-solid fa-clipboard-list text-emerald-600"></i>公告调研 ' + s.cninfo.total + '</span>'); + if (s.source_dist && s.source_dist.length) bits.push('<span class="stat-bit"><i class="fa-solid fa-globe text-teal-600"></i>来源 ' + s.source_dist.length + '</span>'); + if (s.sources) bits.push('<span class="stat-bit"><i class="fa-solid fa-globe text-teal-600"></i>来源 ' + Object.keys(s.sources).length + '</span>'); + if (s.pipeline) { + var p = s.pipeline; + var pKeys = Object.keys(p); + for (var i = pKeys.length - 1; i >= 0; i--) { + var pv = p[pKeys[i]]; + if (pv == null || typeof pv === 'object') continue; + bits.push('<span class="stat-bit"><i class="fa-solid fa-database text-slate-600"></i>' + pKeys[i] + ' ' + pv + '</span>'); + break; + } + } + return bits.join(''); + }, + + truncate: function (txt, n) { + txt = (txt || '').replace(/\s+/g, ' ').trim(); + if (!txt) return '(无摘要)'; + return txt.length > n ? txt.substring(0, n) + '…' : txt; + }, + + updateRangeInfo: function (reports) { + var dates = reports.map(function (r) { return r.report_date; }); + $('#rangeInfo').text('共 ' + reports.length + ' 份日报' + (dates.length ? ' · ' + dates[0] + ' ~ ' + dates[dates.length - 1] : '')); + }, + + renderEmpty: function () { + $('#reportList').html('<div class="empty-box"><i class="fa-solid fa-inbox"></i><p>该时间段暂无日报</p></div>'); + $('#rangeInfo').text(''); + this.hideLoading(); + }, + + renderError: function (msg, xhr) { + var detail = ''; + if (xhr && xhr.responseJSON && xhr.responseJSON.error) detail = xhr.responseJSON.error; + $('#reportList').html('<div class="empty-box"><i class="fa-solid fa-triangle-exclamation text-red-500"></i><p>' + msg + '</p>' + (detail ? '<p class="text-sm text-gray-400">' + detail + '</p>' : '') + '</div>'); + this.hideLoading(); + }, + + // ==================== 详情 ==================== + openDetail: function (id) { + var self = this; + this.state.currentId = id; + this.showDetail(); + this.showLoading('正在加载日报详情...'); + $('#detailContent').html(''); + + $.getJSON(this.API_REPORTS + '?id=' + id) + .done(function (r) { + self.renderDetail(r); + self.hideLoading(); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }) + .fail(function (xhr) { + self.renderError('日报详情加载失败', xhr); + self.hideLoading(); + }); + }, + + renderDetail: function (r) { + var self = this; + var typeName = this.TYPE_NAMES[r.report_type] || r.report_type; + var typeCls = r.report_type === 'finance' ? 'badge-finance' : 'badge-intl'; + + // 头部 + $('#detailHeader').html( + '<span class="badge ' + typeCls + '">' + typeName + '</span>' + + '<span class="text-xl font-bold text-dark">' + r.report_date + ' 日报</span>' + + '<span class="text-sm text-gray-400"><i class="fa-regular fa-clock mr-1"></i>' + this.fmtTime(r.generated_at) + ' 生成</span>' + ); + + var html = ''; + // ① AI 摘要 + html += this.renderSummary(r.ai_summary); + // ② 事件模块 (固定顺序: 国内 xwlb→news→cninfo, 国际 intl) + var sections = r.report_type === 'finance' ? ['xwlb', 'news', 'cninfo'] : ['intl']; + var events = r.events || []; + sections.forEach(function (sec) { + var secEvents = events.filter(function (e) { return e.section === sec; }); + if (secEvents.length === 0) return; + html += self.renderEventModule(sec, secEvents); + }); + // ③ 数据总览 + html += this.renderStats(r.stats); + $('#detailContent').html(html); + this.bindEventHover(); + }, + + // AI 摘要: 按条目分行渲染 + renderSummary: function (summary) { + if (!summary) return ''; + var items = summary.split('\n').map(function (s) { return s.replace(/^[-*\s]+/, '').trim(); }).filter(Boolean); + if (items.length === 0) return ''; + var lis = items.map(function (s) { return '<li>' + selfEscape(s) + '</li>'; }).join(''); + return '' + + '<div class="module-card">' + + '<div class="module-title"><i class="fa-solid fa-wand-magic-sparkles text-indigo-600"></i>AI 摘要</div>' + + '<ul class="ai-summary-list">' + lis + '</ul>' + + '</div>'; + }, + + // 事件模块 + renderEventModule: function (sec, events) { + var self = this; + var name = this.SECTION_NAMES[sec] || sec; + var icon = this.SECTION_ICONS[sec] || 'fa-list'; + var imp = events.filter(function (e) { return e.importance >= 4; }).length; + var rows = events.map(function (e, i) { return self.renderEventRow(e, i + 1); }).join(''); + return '' + + '<div class="module-card">' + + '<div class="module-title"><i class="fa-solid ' + icon + ' text-indigo-600"></i>' + name + + '<span class="module-sub">共 ' + events.length + ' 条' + (imp ? ' · 重要(≥4) ' + imp + ' 条' : '') + '</span>' + + '</div>' + + '<div class="divide-y divide-gray-100">' + rows + '</div>' + + '</div>'; + }, + + renderEventRow: function (e, idx) { + var sen = this.SENTIMENT[e.sentiment] || this.SENTIMENT.neutral; + var impCls = e.importance >= 5 ? 'imp-5' : (e.importance >= 4 ? 'imp-4' : ''); + var title = selfEscape(e.title || ''); + if (e.url) title = '<a href="' + selfEscape(e.url) + '" target="_blank" rel="noopener" class="event-link">' + title + '<i class="fa-solid fa-up-right-from-square ml-1 text-xs"></i></a>'; + var summary = e.summary ? '<p class="event-summary">' + selfEscape(e.summary) + '</p>' : ''; + var meta = []; + if (e.event_type) meta.push('<span class="meta-item"><i class="fa-regular fa-tag mr-1"></i>' + selfEscape(e.event_type) + '</span>'); + if (e.source) meta.push('<span class="meta-item"><i class="fa-regular fa-newspaper mr-1"></i>' + selfEscape(e.source) + '</span>'); + return '' + + '<div class="event-row">' + + '<div class="event-left">' + + '<span class="event-rank">' + idx + '</span>' + + '<span class="badge ' + sen.cls + '" title="' + sen.txt + '">' + sen.icon + '</span>' + + '</div>' + + '<div class="event-body">' + + '<div class="event-title-line">' + + '<span class="imp-badge ' + impCls + '">' + (e.importance != null ? e.importance : '-') + '</span>' + + '<div class="event-title">' + title + '</div>' + + '</div>' + + summary + + (meta.length ? '<div class="event-meta">' + meta.join('') + '</div>' : '') + + '</div>' + + '</div>'; + }, + + // ==================== 数据总览 ==================== + // stats 格式随生成日期变化(2026-08 前后两套结构), 统一归一化后渲染: + // A. 字符串数组 ["🟢 利好 5 (11%)", ...] (intl / finance旧版 sentiment) + // B. 对象 {"1": 81, "2": 265} / {"东方财富": 16} (importances / sources) + // C. 对象数组 [{"重要度":"等级 2","数量":"4"}] 或合并格式 [{"重要度":"数量","等级 1":"46"}] + normalizeKV: function (data) { + var out = []; + if (!data) return out; + if (Array.isArray(data)) { + data.forEach(function (item) { + if (item == null) return; + if (typeof item === 'string' || typeof item === 'number') { + out.push({ label: String(item), value: '' }); + } else if (typeof item === 'object') { + if (item['重要度'] === '数量') { + // 合并格式: {"重要度":"数量","等级 1":"46",...} → 展开各等级键 + Object.keys(item).forEach(function (k) { + if (k === '重要度') return; + out.push({ label: k, value: item[k] }); + }); + return; + } + if ('数量' in item) { + if (item['数量'] === '数量') { + // 合并格式: {"重要度":"数量","等级 1":"46",...} → 展开各等级键 + Object.keys(item).forEach(function (k) { + if (k === '重要度') return; + out.push({ label: k, value: item[k] }); + }); + } else { + var lk = Object.keys(item).filter(function (k) { return k !== '数量'; })[0]; + out.push({ label: lk ? item[lk] : '—', value: item['数量'] }); + } + return; + } + var keys = Object.keys(item); + if (keys.length >= 2) out.push({ label: item[keys[0]], value: item[keys[1]] }); + else if (keys.length === 1) out.push({ label: keys[0], value: item[keys[0]] }); + } + }); + } else if (typeof data === 'object') { + Object.keys(data).forEach(function (k) { + var v = data[k]; + if (v == null || typeof v === 'object') return; // 嵌套对象跳过, 单独处理 + out.push({ label: k, value: v }); + }); + } + return out; + }, + + renderStats: function (s) { + if (!s) return ''; + var html = '<div class="module-card">' + + '<div class="module-title"><i class="fa-solid fa-chart-simple text-indigo-600"></i>数据总览</div>'; + + // pipeline 管道 (M1→M6 / raw_total 两种键名, 嵌套对象为来源明细) + if (s.pipeline) { + var keys = Object.keys(s.pipeline); + var nums = keys.filter(function (k) { return typeof s.pipeline[k] !== 'object'; }); + if (nums.length) { + html += '<div class="stats-grid">'; + nums.forEach(function (k) { + var v = s.pipeline[k]; + if (v == null) return; + html += '<div class="stat-card"><div class="stat-num">' + v + '</div><div class="stat-label">' + selfEscape(String(k).replace(/^M\d+\s*/, 'M')) + '</div></div>'; + }); + html += '</div>'; + } + keys.forEach(function (k) { + var v = s.pipeline[k]; + if (v == null || typeof v !== 'object') return; + var label = String(k).replace(/^M\d+\s*/, ''); + html += '<div class="stats-sub-title">' + selfEscape(label) + '</div><div class="source-grid">'; + Object.keys(v).forEach(function (sk) { + html += '<div class="source-item"><div class="s-count">' + v[sk] + '</div><div class="s-name">' + selfEscape(sk) + '</div></div>'; + }); + html += '</div>'; + }); + } + + // finance: news / cninfo / xwlb 统计 + if (s.news) html += this.renderFinanceNewsStats(s.news); + if (s.cninfo) html += this.renderCninfoStats(s.cninfo); + if (s.xwlb) html += this.renderXwlbStats(s.xwlb); + + // 顶层: sentiment / importance / event_types / source_dist / sources (intl & finance旧版) + if (s.sentiment && s.sentiment.length) html += this.renderSentiment(s.sentiment); + if (s.importance && s.importance.length) html += this.renderKVTable('重要度分布', s.importance); + if (s.event_types && s.event_types.length) html += this.renderKVTable('事件类型分布', s.event_types); + if (s.source_dist && s.source_dist.length) html += this.renderKVTable('来源分布', s.source_dist); + if (s.sources) html += this.renderSources(s.sources); + + html += '</div>'; + return html; + }, + + renderFinanceNewsStats: function (news) { + var html = '<div class="stats-sub">'; + if (news.total != null) html += '<div class="stat-card"><div class="stat-num">' + news.total + '</div><div class="stat-label">新闻总量</div></div>'; + if (news.hi_threshold != null) html += '<div class="stat-card"><div class="stat-num">' + news.hi_threshold + '</div><div class="stat-label">重要(≥阈值)</div></div>'; + html += '</div>'; + if (news.sentiments) html += this.renderSentimentBar(news.sentiments); + if (news.importances) html += this.renderKVTable('新闻重要度', news.importances); + if (news.event_types && news.event_types.length) html += this.renderKVTable('新闻事件类型', news.event_types); + return html; + }, + + // sentiment: 字符串数组(已含emoji/百分比, intl & finance旧版) 或 对象(finance新版 news.sentiments) + renderSentiment: function (data) { + if (data.length && typeof data[0] === 'string') { + var items = data.map(function (s) { return '<span class="badge badge-neu">' + selfEscape(s) + '</span>'; }).join(''); + return '<div class="stats-sub-title">情感分布</div><div class="flex flex-wrap gap-2 mb-3">' + items + '</div>'; + } + return this.renderSentimentBar(data); + }, + + renderCninfoStats: function (cn) { + var html = '<div class="stats-sub">'; + if (cn.total != null) html += '<div class="stat-card"><div class="stat-num">' + cn.total + '</div><div class="stat-label">公告调研总量</div></div>'; + if (cn.hi_threshold != null) html += '<div class="stat-card"><div class="stat-num">' + cn.hi_threshold + '</div><div class="stat-label">重要(≥阈值)</div></div>'; + if (cn.announcement != null) html += '<div class="stat-card"><div class="stat-num">' + cn.announcement + '</div><div class="stat-label">公司公告</div></div>'; + if (cn.research != null) html += '<div class="stat-card"><div class="stat-num">' + cn.research + '</div><div class="stat-label">调研报告</div></div>'; + if (cn.irm != null) html += '<div class="stat-card"><div class="stat-num">' + cn.irm + '</div><div class="stat-label">互动易</div></div>'; + html += '</div>'; + if (cn.by_day) { + var days = Object.keys(cn.by_day).sort(); + if (days.length) { + html += '<div class="kv-table-wrap"><table class="kv-table"><thead><tr><th>日期</th><th>数量</th></tr></thead><tbody>'; + days.forEach(function (d) { html += '<tr><td>' + selfEscape(d) + '</td><td>' + cn.by_day[d] + '</td></tr>'; }); + html += '</tbody></table></div>'; + } + } + return html; + }, + + renderXwlbStats: function (xw) { + var html = '<div class="stats-sub">'; + if (xw.total != null) html += '<div class="stat-card"><div class="stat-num">' + xw.total + '</div><div class="stat-label">新闻联播条数</div></div>'; + if (xw.date) html += '<div class="stat-card"><div class="stat-num text-sm">' + selfEscape(xw.date) + '</div><div class="stat-label">联播日期</div></div>'; + html += '</div>'; + return html; + }, + + renderKVTable: function (title, data) { + var items = this.normalizeKV(data); + if (!items.length) return ''; + var rows = items.map(function (it) { + return '<tr><td>' + selfEscape(it.label) + '</td><td>' + (it.value === '' ? '' : selfEscape(it.value)) + '</td></tr>'; + }).join(''); + return '<div class="kv-table-wrap"><div class="kv-title">' + selfEscape(title) + '</div>' + + '<table class="kv-table"><thead><tr><th>类别</th><th>数量</th></tr></thead><tbody>' + rows + '</tbody></table></div>'; + }, + + renderSentimentBar: function (sent) { + var pos = sent.positive || sent.pos || 0; + var neg = sent.negative || sent.neg || 0; + var neu = sent.neutral || sent.neu || 0; + var total = pos + neg + neu; + if (!total) return ''; + var pct = function (v) { return (v / total * 100).toFixed(1) + '%'; }; + return '' + + '<div class="sentiment-wrap">' + + '<div class="sentiment-bar"><div class="s-pos" style="width:' + pct(pos) + '"></div><div class="s-neg" style="width:' + pct(neg) + '"></div><div class="s-neu" style="width:' + pct(neu) + '"></div></div>' + + '<div class="sentiment-legend">' + + '<span><i class="s-dot s-pos"></i>利好 ' + pos + ' (' + pct(pos) + ')</span>' + + '<span><i class="s-dot s-neg"></i>利空 ' + neg + ' (' + pct(neg) + ')</span>' + + '<span><i class="s-dot s-neu"></i>中性 ' + neu + ' (' + pct(neu) + ')</span>' + + '</div>' + + '</div>'; + }, + + renderSources: function (sources) { + var self = this; + var keys = Object.keys(sources); + if (!keys.length) return ''; + var items = keys.map(function (k) { + return '<div class="source-item"><div class="s-count">' + sources[k] + '</div><div class="s-name">' + selfEscape(k) + '</div></div>'; + }).join(''); + return '<div class="stats-sub-title">各源数据</div><div class="source-grid">' + items + '</div>'; + }, + + // ==================== UI 控制 ==================== + showList: function () { + $('#listView').removeClass('hidden'); + $('#detailView').addClass('hidden'); + $('#detailHeader').html(''); + $('#detailContent').html(''); + }, + + // 打开详情: 隐藏列表, 独占视口 + showDetail: function () { + $('#listView').addClass('hidden'); + $('#detailView').removeClass('hidden'); + }, + + showLoading: function (msg) { + $('#loadingOverlay').removeClass('hidden'); + $('#loadingMsg').text(msg || '加载中...'); + }, + hideLoading: function () { + $('#loadingOverlay').addClass('hidden'); + }, + + bindEventHover: function () { + // 事件行悬停样式已由 CSS 处理, 无需额外逻辑 (保留钩子) + } +}; + +// HTML 转义 (防 XSS, 数据来自外部 API) +function selfEscape(str) { + return String(str == null ? '' : str) + .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') + .replace(/"/g, '"').replace(/'/g, '''); +} + +$(function () { + NewsReports.init(); +}); diff --git a/podcast-docs/deploy.md b/podcast-docs/deploy.md new file mode 100644 index 0000000..15837e7 --- /dev/null +++ b/podcast-docs/deploy.md @@ -0,0 +1,120 @@ +# 部署文档 + +> 当前部署环境: doorcome.cn (Debian 12) + +## 服务器要求 + +| 组件 | 版本 | 用途 | +|------|------|------| +| Python | 3.11+ | Django + Celery | +| MySQL/MariaDB | 10.11+ | 数据存储 | +| Redis | 6+ | Celery Broker | +| Nginx | — | 反向代理 | +| Node.js | 18+ | 前端 | + +## 目录结构 + +``` +/home/simon/myquant/django/podcast/ # 后端代码 + ├── venv/ # Python 虚拟环境 + ├── staticfiles/ # Django 静态文件 + ├── data/audios/ # 音频文件 + ├── logs/ # 日志 + └── frontend/ # 前端代码 + └── .next/ # 前端构建产物 +``` + +## systemd 服务 + +| 服务 | 端口 | 说明 | +|------|------|------| +| `podcast-gunicorn` | 8001 | Django API | +| `podcast-celery` | — | AI 处理 Worker | +| `podcast-frontend` | 3000 | Next.js 前端 | + +### 管理命令 + +```bash +sudo systemctl status podcast-gunicorn +sudo systemctl restart podcast-celery +sudo journalctl -u podcast-frontend -n 50 -f +``` + +## Nginx 路由 + +| 路径 | 后端 | +|------|------| +| `/api/` | `127.0.0.1:8001` (Django) | +| `/admin/` | `127.0.0.1:8001` (Django) | +| `/static/` | Django 静态文件 | +| `/media/` | 音频文件 | +| `/podcast/` | `127.0.0.1:3000` (Next.js) | + +## 首次部署 + +```bash +# 1. 同步代码 +rsync -avz --delete --exclude='node_modules' --exclude='__pycache__' \ + --exclude='.env' --exclude='venv' --exclude='.next' \ + ./ doorcome:/home/simon/myquant/django/podcast/ + +# 2. 安装依赖 +ssh doorcome +cd /home/simon/myquant/django/podcast +source venv/bin/activate +pip install -r requirements.txt + +# 3. 配置环境变量 +vi .env # 填入数据库连接、API Key 等 + +# 4. 数据库迁移 +python manage.py migrate +python manage.py collectstatic --noinput +python manage.py createsuperuser + +# 5. 启动服务 +sudo systemctl start podcast-gunicorn +sudo systemctl start podcast-celery +sudo systemctl start podcast-frontend + +# 6. 配置 Nginx +# 编辑 /etc/nginx/sites-enabled/default 添加反向代理配置 +sudo nginx -t && sudo systemctl reload nginx +``` + +## 更新部署 + +```bash +# 本地同步代码 +rsync -avz --delete --exclude={node_modules,__pycache__,.env,venv,.next,db.sqlite3} \ + ./ doorcome:/home/simon/myquant/django/podcast/ + +# 远程执行 +ssh doorcome " +cd /home/simon/myquant/django/podcast +source venv/bin/activate +pip install -r requirements.txt -q +python manage.py migrate +python manage.py collectstatic --noinput +sudo systemctl restart podcast-gunicorn podcast-celery + +cd frontend +npm install -q +npm run build +sudo systemctl restart podcast-frontend +" +``` + +## 配置项 + +参见 `/home/simon/myquant/django/podcast/.env`: + +| 配置项 | 说明 | +|--------|------| +| `DEBUG` | 生产环境设为 False | +| `ALLOWED_HOSTS` | 允许的域名 | +| `DB_*` | MySQL 连接信息 | +| `REDIS_URL` | Redis 连接 | +| `DASHSCOPE_API_KEY` | 千问 ASR API Key | +| `DEEPSEEK_API_KEY` | DeepSeek API Key | +| `DEV_MOCK_ASR` | 0=真实 ASR, 1=Mock(开发用) | diff --git a/podcast-docs/usage.html b/podcast-docs/usage.html new file mode 100644 index 0000000..a66653c --- /dev/null +++ b/podcast-docs/usage.html @@ -0,0 +1,181 @@ +<!DOCTYPE html> +<html lang="zh-CN"> +<head> +<meta charset="UTF-8"> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +<title>播客处理流水线 — 使用说明 + + + + +

播客处理流水线 · 使用说明

+

从 Apple Podcast URL 自动获取播客 → 语音转文字 → 中文翻译 → AI 要点分析

+ +

一、地址总览

+ + + + + + + +
用途地址
前端首页(时间线)https://doorcome.cn/podcast/
添加播客https://doorcome.cn/podcast/new
播客管理https://doorcome.cn/podcast/podcasts
API 文档 (Swagger)https://doorcome.cn/api/docs/
管理后台https://doorcome.cn/admin/
+ +

二、前端使用

+ +

2.1 添加播客

+
    +
  1. 打开「添加」页面
  2. +
  3. 粘贴 Apple Podcast 链接,格式如:
    + https://podcasts.apple.com/cn/podcast/lex-fridman-podcast/id1434243584
  4. +
  5. 点击「添加」按钮
  6. +
  7. 系统自动获取播客信息、节目列表,后台 AI 开始处理
  8. +
+ +
+ ✅ 支持所有 Apple Podcast URL。系统会通过 iTunes API 获取元数据, + 然后解析 RSS Feed 获取所有节目。 +
+ +

2.2 查看要点

+ + +

2.3 单集详情

+ + +

2.4 播客管理

+ + +

三、后台管理

+ +

访问 /admin/ 使用管理员账号登录。

+ + + + + + + +
模型功能
Podcast(播客)查看、搜索所有播客
Episode(节目)查看处理进度、过滤状态、重试失败节目
Transcript(转写)查看 ASR 结果和翻译内容
Highlight(要点)查看 AI 分析结果
+ +
+ 处理状态说明: + pendingdownloadingdownloaded → + transcribingtranscribed → + translatingtranslated → + analyzingcompleted
+ 任意步骤失败 → failed(Admin 可选重试) +
+ +

四、AI 处理流程

+ +

添加播客后,Celery Worker 自动处理每个节目:

+ +
+Step 1: 下载音频文件到服务器
+Step 2: 千问 DashScope ASR → 语音转文字(带时间戳分段)
+Step 3: 检测语种:
+  · 非中文 → DeepSeek 翻译成中文
+  · 中文 → 跳过翻译
+Step 4: DeepSeek 要点分析 → JSON 结构化结果
+  · 一句话摘要
+  · 详细摘要(3-5段)
+  · 要点列表(话题 + 观点 + 时间戳)
+  · 标签
+
+最终状态 → completed。
+失败自动重试 3 次,每次间隔 60 秒。
+
+ +

五、命令行操作

+ +

SSH 到服务器后使用:

+ +
+# 添加播客
+cd /home/simon/myquant/django/podcast
+source venv/bin/activate
+python manage.py init_podcast "https://podcasts.apple.com/cn/podcast/.../idXXXXXX"
+
+# 添加并触发 AI 处理
+python manage.py init_podcast "https://..." --trigger-tasks
+
+# 查看服务状态
+sudo systemctl status podcast-gunicorn   # Django API
+sudo systemctl status podcast-celery      # AI 处理 Worker
+sudo systemctl status podcast-frontend    # 前端
+
+# 查看日志
+sudo journalctl -u podcast-celery -n 50 -f
+
+# 重启某个服务
+sudo systemctl restart podcast-celery
+
+ +

六、API 参考

+ +

完整 API 文档在 Swagger UI

+ + + + + + + + + + + + +
方法路径说明
POST/api/podcasts/add/通过 Apple URL 添加播客
GET/api/podcasts/播客列表
GET/api/podcasts/{id}/播客详情(含节目列表)
DELETE/api/podcasts/{id}/删除播客
GET/api/episodes/节目列表(支持 status/podcast_id 过滤)
GET/api/episodes/{id}/单集详情(含转写+要点)
POST/api/episodes/{id}/retry/重试失败的节目
GET/api/highlights/要点列表(支持 date/podcast_id 过滤)
GET/api/highlights/today/今日要点
+ +

七、管理员账号

+ + + + +
地址用户名密码
https://doorcome.cn/admin/adminadmin123
+ +
+ ⚠️ 首次使用建议修改默认密码。 +
+ +

八、配置说明

+ +

服务器配置文件:/home/simon/myquant/django/podcast/.env

+ + + + + + + + +
配置项说明
DASHSCOPE_API_KEY千问 DashScope API Key(ASR 语音转文字)
DEEPSEEK_API_KEYDeepSeek API Key(翻译 + 要点分析)
DASHSCOPE_ASR_MODELASR 模型名(默认 paraformer-realtime-v2)
DEEPSEEK_MODELDeepSeek 模型名(默认 deepseek-chat)
DEV_MOCK_ASR开发模式:1=跳过真实 ASR,0=调用真实 API
+ + + diff --git a/quant/intlnews_usage.html b/quant/intlnews_usage.html new file mode 100644 index 0000000..5720f14 --- /dev/null +++ b/quant/intlnews_usage.html @@ -0,0 +1,390 @@ + + + + + +IntlNews 操作手册 + + + +
+ +

📰 IntlNews — 国际财经新闻 Deep Research

+

最后更新:2026-07-23 | 运行服务器:pi@192.168.1.160

+ +
+项目定位:面向国际财经新闻的私有化 Deep Research 平台。抓取英文财经新闻 → 英译中 → 事件抽取 → 向量入库 → MCP Agent 深度研究。 +
+ + +

一、系统架构

+Arch + +
+┌──────────────────────────────────────────────────────┐
+│                   Pi 4 (8GB)                          │
+│  ┌──────────┐   ┌──────────┐   ┌──────────────────┐  │
+│  │ M1 抓取   │──▶│ M2 正文  │──▶│ M3 三层去重      │  │
+│  │ headful   │   │ trafilat.│   │ URL/Content/SimH │  │
+│  │ Playwright│   │          │   │                  │  │
+│  │ + HTTP 代 │   │          │   │                  │  │
+│  │ 理(海外)  │   │          │   │                  │  │
+│  └──────────┘   └──────────┘   └────────┬─────────┘  │
+│                                         ▼            │
+│  ┌──────────┐   ┌──────────┐   ┌──────────────────┐  │
+│  │ M6 Qdrant│◀──│ M5 向量  │◀──│ M4 翻译+事件抽取  │  │
+│  │ 入库     │   │ DashScope│   │ DeepSeek v4      │  │
+│  └────┬─────┘   └──────────┘   └──────────────────┘  │
+│       │                                               │
+│       ▼                                               │
+│  ┌──────────┐   ┌──────────┐                          │
+│  │ M7 日报  │──▶│ doorcome │                          │
+│  │ 生成     │   │ 上传     │                          │
+│  └──────────┘   └──────────┘                          │
+│       │                                               │
+│       ▼                                               │
+│  ┌────────────────────────────────────────────────┐   │
+│  │ M8 MCP 服务(Cherry Studio / Claude Code)      │   │
+│  └────────────────────────────────────────────────┘   │
+└──────────────────────────────────────────────────────┘
+
+ +
+关键变更:海外服务器已于 2026-07-14 停用。全链路在 Pi 独立运行,通过 Privoxy → SOCKS5 (ss-local) 代理访问海外新闻网站。
+反爬策略:2026-07-23 更新 — 启用 Crawl4AI magic 全自动反检测 + Playwright stealth 模式。investing.com 改用 Google News RSS 代理。 +
+ + +

二、部署指南

+ +

2.1 环境要求

+ + +

2.2 初始化安装

+
+# 1. 克隆仓库
+git clone ssh://gitea:2222/simon/intl_news /home/pi/intlnews
+cd /home/pi/intlnews
+
+# 2. Python 环境
+uv sync
+
+# 3. 环境变量
+cp .env.example .env
+# 填入: DEEPSEEK_API_KEY, DASHSCOPE_API_KEY, QDRANT_URL
+
+# 4. Xvfb 虚拟显示器
+sudo apt install xvfb
+Xvfb :99 -screen 0 1280x1024x24 -ac +extension RANDR &
+
+# 5. Shadowsocks + Privoxy(海外代理)
+sudo apt install shadowsocks-libev privoxy
+# 配置 /etc/shadowsocks-libev/config.json
+# 配置 /etc/privoxy/config: forward-socks5t / 127.0.0.1:1088 .
+sudo systemctl enable --now shadowsocks-libev-local
+sudo systemctl enable --now privoxy
+
+ +

2.3 定时调度(crontab)

+
+# crontab -e 添加:
+0 6  * * * cd /home/pi/intlnews && bash scripts/domestic_full.sh >> logs/full.log 2>&1
+0 12 * * * cd /home/pi/intlnews && bash scripts/domestic_full.sh >> logs/full.log 2>&1
+0 18 * * * cd /home/pi/intlnews && bash scripts/domestic_full.sh >> logs/full.log 2>&1
+0 22 * * * cd /home/pi/intlnews && bash scripts/domestic_full.sh >> logs/full.log 2>&1
+@reboot Xvfb :99 -screen 0 1280x1024x24 -ac +extension RANDR &
+
+ + +

三、操作命令

+ +

3.1 全流程运行

+
+# 一键全流程(M1→M6→日报)
+bash scripts/domestic_full.sh
+
+ +

3.2 M1 抓取

+
+# 抓取全部 13 个源
+bash scripts/domestic_crawl_8g.sh
+
+# 单源调试
+bash scripts/domestic_crawl_8g.sh investing
+bash scripts/domestic_crawl_8g.sh investinglive
+bash scripts/domestic_crawl_8g.sh reuters
+
+ +

3.3 M2→M6 管道

+
+# 完整管道(需要先有 raw 数据)
+bash scripts/pipeline.sh
+
+# 或逐步执行:
+PYTHONPATH=. .venv/bin/python3 -c "from scheduler.pipeline import run_pipeline; run_pipeline('$(date +%Y%m%d)')"
+
+ +

3.4 日报

+
+# 生成当日日报
+PYTHONPATH=. .venv/bin/python3 -c "from scheduler.reporter import generate_report; print(generate_report())"
+
+# 查看日报列表
+ls -lt data/reports/
+
+ +

3.5 日志查看

+
+# 全量日志
+tail -100 logs/full.log
+
+# 筛选指定源
+grep 'investing\|investinglive' logs/full.log | tail -20
+
+ + +

四、新闻源状态

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID策略日产量状态
ReutersreutersGoogle News RSS~30 篇摘要 DataDome
CNBCcnbcCNBC RSS~25 篇正常
MarketWatchmarketwatchRSS + stealth~25 篇正常
Financial TimesftFT RSS~20 篇摘要 付费墙
Yahoo Financeyahoo_financeYahoo RSS~30 篇正常
Investing.cominvestingGoogle News RSS + stealth25 篇已修复 RSS 代理
Seeking AlphaseekingalphaRSS + stealth~20 篇正常
Barron'sbarronsDow Jones RSS + stealth~20 篇正常
WSJwsjDow Jones RSS + stealth~20 篇正常
EconomisteconomistRSS + stealth~15 篇正常
InvestingLiveinvestingliveRSS(全文)25 篇2026-07-23 新增
ZeroHedgezerohedgeFeedBurner RSS~15 篇正常
ForexLiveforexlive已移除 301→investinglive
+ +
+⚠️ 注意:RSS 摘要源的全文依赖 headful Playwright 回退抓取,成功率取决于代理线路质量。investing.com 因 Cloudflare 反爬,全文抓取间歇性失败。 +
+ + +

五、数据存储

+ +
+/home/pi/intlnews/
+├── data/
+│   ├── raw/{source_id}/{yyyymmdd}/     M1 原始抓取(.html + .md + index.jsonl)
+│   ├── processed/{source_id}/           M2 正文提取结果
+│   ├── dedup/                            M3 去重指纹库(SQLite)
+│   ├── translations/{yyyymmdd}/         M4 翻译+事件抽取(JSONL)
+│   ├── embeddings/{yyyymmdd}/           M5 向量文件(JSONL)
+│   ├── reports/                          M7 日报(HTML)
+│   └── vectorstore/                      M6 Qdrant 本地数据
+├── configs/
+│   ├── sources.yaml                     新闻源定义(13 个源)
+│   ├── system.yaml                      系统参数
+│   └── profiles/8g_headful.yaml         Pi 抓取配置
+├── logs/full.log                        运行日志
+└── prompts/                             LLM Prompt 模板
+
+ + +

六、MCP 服务

+ +

M8 模块提供 MCP(Model Context Protocol)服务,供 Cherry Studio / Claude Code / Cursor 等客户端接入。

+ +

6.1 启动

+
+cd /home/pi/intlnews
+PYTHONPATH=. .venv/bin/python3 -m mcp_server.server
+
+ +

6.2 可用工具

+ + + + + + + +
Tool参数功能
en_news_searchquery, top_k语义搜索知识库
en_news_filtersource_id, date, sentiment按条件筛选事件
en_news_trendingdays获取近期热点
en_news_report获取最新日报
en_news_daily_brief一键生成简报
+ + +

七、配置参考

+ +

7.1 环境变量(.env)

+ + + + + +
变量用途
DEEPSEEK_API_KEYDeepSeek LLM(翻译+事件抽取)
DASHSCOPE_API_KEYDashScope Embedding(向量生成)
QDRANT_URLQdrant 服务地址
+ +

7.2 新闻源配置(sources.yaml)

+

每个源包含:id, name, homepage, rss_url, article_url_pattern, anti_bot_mode 等。

+

新增源时参考现有格式,启用/禁用通过 enabled: true/false

+ +

7.3 Profile 配置

+

Pi 服务器使用 8g_headful profile(环境变量 EN_NEWS_PROFILE),通过 Privoxy HTTP 代理访问海外网站。

+ + + +

八、常见排障

+ + + + + + + + + + + + + + + + + + + + + + + +
现象排查步骤
抓取 0 篇 + 1. 检查 logs/full.log 看 RSS/Web 错误
+ 2. bash scripts/domestic_crawl_8g.sh <source_id> 单源测试
+ 3. 检查代理是否正常:curl -I --proxy 127.0.0.1:3128 https://example.com +
RSS 超时 + 1. 确认 Shadowsocks 和 Privoxy 在运行
+ 2. systemctl status shadowsocks-libev-local privoxy
+ 3. 环境变量 HTTP_PROXY 是否设置 +
Cloudflare 拦截 + 1. 已启用 magic + stealth 自动反检测
+ 2. 仍失败则需更换代理 IP(数据中心 IP 被标记)
+ 3. 改用 Google News RSS 代理(已为 investing/reuters 启用) +
日报无数据 + 1. 检查 data/translations/{date}/ 是否有 JSONL
+ 2. 检查 data/embeddings/{date}/ 是否有向量 +
MCP 连接失败 + 1. 确认 MCP 服务进程在运行
+ 2. 检查端口监听:ss -tlnp | grep 8080 +
+ + +

九、变更记录

+ + + + + + + + + + + +
日期变更
2026-07-23 + 反爬策略增强:
+ - crawler.py: magic/stealth/--disable-webrtc/remove_consent_popups
+ - rss_crawler.py: httpx 代理支持
+ - investing.com: Google News RSS 代理(绕过 Cloudflare)
+ - forexlive → investinglive 迁移(域名已 301 重定向) +
2026-07-14 + - 海外服务器停用,Pi 全链路独立运行
+ - 8g_headful profile 新增(headful + HTTP 代理)
+ - Shadowsocks + Privoxy 部署 +
+ + + +
+ + diff --git a/quant/user_guide.html b/quant/user_guide.html new file mode 100644 index 0000000..079fb9a --- /dev/null +++ b/quant/user_guide.html @@ -0,0 +1,254 @@ + + + + + +A 股 Deep Research 用户手册 v1.1 + + + +

A 股 Deep Research 私有投研平台

用户手册 v1.1 · 2026-06-17

+ + + +
+ +

1. 项目概述

+

A 股 Deep Research 是私有化投研平台,自动完成财经新闻抓取 → 正文提取 → 事件抽取 → 向量化 → 语义检索,通过 MCP 协议接入 Cherry Studio / Claude Code,实现「自然语言提问 → 结构化研究报告」。

+

技术栈: Python 3.11 · Crawl4AI · GNE · DeepSeek/Qwen · DashScope · Qdrant · APScheduler · MCP

+

部署: 树莓派 5 (ARM64) /home/pi/news/

+ +

2. 快速开始

+ +

2.1 安装

+
cd /home/pi/news
+uv sync
+cp .env.example .env
+# 编辑 .env, 填入 DEEPSEEK_API_KEY 和 DASHSCOPE_API_KEY
+uv run python -m playwright install chromium   # 仅 M1 抓取需要
+ +

2.2 验证

+
uv run pytest -m "not integration"   # 应显示 186 passed
+ +

3. 统一 CLI 速查

+

所有操作通过 a-share 命令完成。

+ + + + + + + + + + + + + + + +
命令功能常用参数
a-share crawlM1 抓取新闻--source cls
a-share extractM2 正文提取--date 20260616
a-share dedupM3 三层去重--date 20260616 --reset
a-share eventsM4 LLM 事件抽取--provider qwen --limit 5
a-share embedM5 向量化--model text-embedding-v3
a-share ingestM6 Qdrant 入库--date 20260616 --recreate
a-share pipeline全链路 (M1→M6)--once --report
a-share discover分析站点, 生成配置--name 中文名 --add --extra URL
a-share add-entry追加入口到已有源source_id url1 url2 ...
a-share report生成/上传日报--date 20260616 --no-upload
a-share search终端检索知识库--source --stock --sentiment --top
a-share status数据总览无参数
+ +

4. 全链路一键运行

+
uv run a-share pipeline --once           # 一次跑通 M1→M6
+uv run a-share pipeline --once --report  # 末尾自动生成日报并上传
+ +

耗时约 4-5 分钟 (100 篇): crawler ~3min / extractor ~24s / dedup ~2s / llm ~45s / embedding ~10s / qdrant ~4s

+

只执行部分步骤: uv run a-share pipeline --once --steps crawler,llm

+ +

产物路径: data/raw/ (M1) → data/processed/ (M2) → data/deduped/ (M3) → data/events/ (M4) → data/embeddings/ (M5) → data/qdrant_storage/ (M6)

+ +

5. 新闻源管理

+ +

5.1 查看已有源

+

配置文件: configs/sources.yaml。内置财联社 / 东方财富 / 新浪财经 / 证券时报 / 第一财经。

+ +

5.2 新增站点 (一条命令)

+
# 自动分析 + 写入 sources.yaml
+uv run a-share discover https://example.com/news/ --name 中文名 --add
+
+# 仅预览
+uv run a-share discover https://example.com/news/
+

自动完成: JS 抓取 → 链接提取 → 导航过滤 → 模式聚类 → 正则生成 → yaml 输出。写入时自动空行分隔、序号递增、# ---- N. 名称 ---- 注释头。

+ +

5.3 同站多频道

+
# 一次性分析多入口
+uv run a-share discover https://example.com/news/global \
+  --extra https://example.com/news/china \
+  --extra https://example.com/news/tech \
+  --name 中文名 --add
+

生成的 yaml 自动包含 extra_homepages 字段,抓取时依次访问所有入口,自动去重。

+ +

5.4 为已有源追加入口

+
uv run a-share add-entry wallstreetcn https://xxx.com/news/新频道
+

自动去重 — 已存在的 URL 跳过。

+ +

5.5 验证

+
uv run a-share crawl --source wallstreetcn
+ +

6. cninfo 公告抓取

+

cninfo(巨潮资讯网)是独立的 A 股公告抓取管道,与新闻抓取分开调度(每天 08:00)。

+ +
uv run a-share pipeline --cninfo-once           # watchlist 全链路 (公告+调研+IRM)
+uv run a-share cninfo --watchlist               # 仅公告(关注公司)
+uv run a-share cninfo --research                # 仅调研
+uv run a-share cninfo --irm                     # 仅互动问答
+uv run a-share stock-report                     # 个股日报
+ +

全链路 9 步: 公告→调研→IRM→提取→PDF→去重→LLM→向量→Qdrant。

+ +

公告关注列表

+
uv run a-share watchlist add 300750 宁德时代 --note "动力电池龙头"
+uv run a-share watchlist list
+uv run a-share watchlist remove 000001
+uv run a-share cninfo --watchlist                        # 只抓关注公司
+

关注公司公告在日报中置顶并 ⭐ 高亮。配置: configs/watchlist.yaml

+ +

配置: CNINFO_ENABLED CNINFO_DAYS_BACK CNINFO_MAX_PAGES CNINFO_API_BASE CNINFO_PDF_BASE

+ +
⚠ .env 注意: 值后面不能跟 # 注释。python-dotenv 会把 # 后内容当成值。注释必须独占一行。
+ +

7. 知识库检索

+ +

6.1 终端直接检索

+
uv run a-share search "宁德时代固态电池" --top 5
+uv run a-share search "政策" --source cls --sentiment positive
+uv run a-share search "风险" --stock 001212 --min-importance 3
+ +

6.2 Python 检索 (带过滤)

+
from vectorstore import VectorStore, SearchFilter, make_qdrant_client
+c = make_qdrant_client()
+s = VectorStore(c)
+hits = s.query(vector=probe["vector"], top_k=10,
+    filter=SearchFilter(source_id="cls", importance_min=3, sentiment="positive"))
+for h in hits: print(h.short_summary())
+s.close()
+

支持 8 种过滤: source_id / stock_codes / company_names / industries / sentiment / importance_min / event_types / date_range。

+ +

6.3 日报

+
uv run a-share report                     # 生成今日日报并上传
+uv run a-share report --date 20260616     # 指定日期
+uv run a-share report --no-upload         # 仅生成不上传
+

HTML 日报含: AI 摘要 (LLM 生成 400-500 字要点) + 数据总览 + 情绪分布 + 事件类型 + 高重要度事件列表。

+ +

8. 定时任务部署

+ +

7.1 安装 systemd 服务 (一次性)

+
sudo cp scripts/a-share-research.service /etc/systemd/system/
+sudo systemctl daemon-reload
+sudo systemctl enable a-share-research
+ +

7.2 日常操作

+ + + + + +
启动sudo systemctl start a-share-research
停止sudo systemctl stop a-share-research
状态sudo systemctl status a-share-research
实时日志journalctl -u a-share-research -f
+

默认执行时间: 每天 07:00 / 12:00 / 18:00 / 22:00。7:00 的日报自动覆盖过去 24 小时新闻。

+ +

修改定时配置

+
# 1. 编辑 .env 中的 SCHEDULE_TIMES
+nano /home/pi/news/.env
+# 格式: HH:MM,HH:MM,... (24 小时制, 逗号分隔)
+# 例: SCHEDULE_TIMES=08:00,14:00,20:00
+
+# 2. 重启服务
+sudo systemctl restart a-share-research
+
+# 3. 验证
+grep "已注册定时任务" /home/pi/news/logs/scheduler.log | tail -4
+ +

8. MCP 服务与 AI Agent

+ +

8.1 连接 Cherry Studio

+

设置 → MCP 服务器 → 添加:

+
{"mcpServers":{"a-share-research":{"command":"uv","args":["run","python","-m","scripts.run_mcp_server"],"cwd":"/home/pi/news"}}}
+ +

8.2 5 个 MCP 工具

+ + + + + + + +
工具功能示例
search_news通用语义检索search_news("宁德时代固态电池")
search_company_news按公司检索search_company_news("价格", company="茅台")
search_industry_news按行业检索search_industry_news("政策", industry="半导体")
search_stock_events按股票代码检索search_stock_events("事件", stock_code="300750")
search_sentiment_trend情绪检索+统计search_sentiment_trend("AI算力", sentiment="all")
+ +

9. 投研分析模板

+

在 Cherry Studio 中粘贴 docs/agent_prompt.md 作为 System Prompt:

+
公司分析: 分析宁德时代 (300750) 近 30 天的利好利空变化
+
行业分析: 分析动力电池行业最近的政策动向和竞争格局
+
风险排查: 梳理中旗新材 (001212) 近期面临的主要风险
+
事件追踪: 过去一周 A 股有哪些重大监管处罚事件?
+
情绪趋势: 近一个月 AI 算力概念的情绪变化趋势如何?
+ +

10. 附录

+ +

A. Qdrant 部署模式

+ + + + + + +
本地文件模式 (默认)Docker Server 模式
类比SQLitePostgreSQL
进程无独立进程容器 a_share_qdrant
ARM64 jemalloc 16K 页崩溃
适用树莓派 / 单机x86 生产 / 远程
+ +

B. 环境变量

+ + + + + + +
LLM_PROVIDERdeepseekM4 LLM
DEEPSEEK_API_KEYDeepSeek Key
DASHSCOPE_API_KEY百炼 Key (M5/M8)
EMBEDDING_PROVIDERdashscopeM5 嵌入
SCHEDULE_TIMES07:00,12:00,18:00,22:00M7 定时
+ +

C. 日志文件

+

logs/crawler.log · logs/extractor.log · logs/dedup.log · logs/llm.log · logs/embedding.log · logs/qdrant.log · logs/scheduler.log · logs/mcp_server.log (自动轮转 10MB×5)

+ +

D. 常见问题

+

Q: 某源抓取 0 篇?

A: 编辑 configs/sources.yaml,检查 article_url_pattern 正则。或重新 discover 分析。

+

Q: LLM 报错?

A: 检查 .envDEEPSEEK_API_KEY。可切换 --provider qwen 测试。

+

Q: Qdrant 搜索无结果?

A: uv run a-share status 检查数据量。若 Qdrant 为 0,执行 uv run a-share ingest --date <date> --recreate

+

Q: 从零重建知识库?

rm -rf data/raw data/processed data/dedup data/deduped data/events data/embeddings data/qdrant_storage
+uv run a-share pipeline --once
+ +
+ + +