feat: 投资资讯日报查询页 + 文档更新 + 拉取服务器最新内容

- 新增 charts/news_reports.php 报告查询导航页(列表+详情一体,fetch API 渲染)
- 新增 js/newsReports.js:AI 摘要/新闻联播/财经新闻/公告调研/数据总览固定模块,stats 归一化兼容多种格式
- index-2.php 研究报告卡片改为导航链接,移除 PHP 目录下拉框
- 更新 AGENTS.md/CLAUDE.md(路径、页面清单、数据模式等核实修正)
- 同步服务器拉取的文档:quant 使用手册、podcast-docs
- 生成 continuation.md 会话检查点
This commit is contained in:
2026-08-05 20:28:57 +08:00
parent 60fb1b95e1
commit 8ea9b48a9a
10 changed files with 1717 additions and 128 deletions
+32 -13
View File
@@ -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()` 注入 `<script>` 标签作为 JS 全局变量 → 页面专属的 JS 文件读取全局变量并用 `echarts.init()` 渲染
- **图表数据传递** —— 当前活跃页面有三种模式(见下),新页面优先复用现有模式,不要自创第四种
### 三种数据传递模式(实测现状)
1. **模式 Bfetch 外部 API**:页面 JS 用 `fetch()``https://api.doorcome.cn/api/*`(如 `stockparam``indexDatas``stockinfo``getdiv``stockep``stockmargin``stockbasic`)。代表页面:`stock_trend.php``stockdiv.php``stockep.php``stockmargin.php``hkholdbycode.php``index_trend.php``index_mv_all.php``index_margin.php`
2. **模式 CAJAX → `inc/ajax.inc.php`**:页面 JS 用 `$.ajax``../inc/ajax.inc.php`,通过 `$_REQUEST['t']` 路由(if 链,不是 switch/case)。`t` 取值:`stockList``financeData``esfTBD``esfListDaily``newTBD``moneyflowData``estateData`。代表页面:`moneyflow.php``realestate.php``estateTradeDaily.php``estateListDaily.php``estateNewTradeDaily.php``stockkeydata.php``stockTradeRecord.php`
3. **模式 A(服务端注入)**PHP 查询后 `json_encode()` 注入 `<script>` 标签中的 JS 变量。`window.chartConfig` 全局变量**仅剩 `deprecated/` 在用**;活跃页面 `charts/chartStDetail.php` 是变体(注入命名变量 `data1``data5``legend` 等)。
## 代码风格约定
@@ -27,12 +33,13 @@
如果要新增一个数据可视化页面,按以下步骤:
1.`inc/` 中写数据获取函数(如果数据源有复用价值),或在页面内直接写查询
2.根目录创建 `newpage.php`include 需要的 `inc/*.php`
2. `charts/` 创建 `newpage.php`include 需要的 `inc/*.php`
3. 通过 `$_REQUEST` 接收参数(如 `ts_code``t_start``t_end`
4. 查询数据库,`json_encode` 结果注入到页面
5. `js/` 中创建对应的 JS 文件,负责 ECharts 初始化和图表渲染
6. 对于样式较新的页面,使用 `js/tailwindcss_3.4.17.js` 并参考 `index-2.php` 的布局风格
7. `index-2.php` 中添加导航入口
4. 数据获取:优先走模式 Bfetch `api.doorcome.cn`)或模式 C`ajax.inc.php` 加一个 `t=` 分支)
5. 图表 JS 放 `js/` 目录,新式页面复用 `js/renderCharts.js``doubleLineChart()` 等)和 `js/pubfunc.js``getRows()``hideSwitch()` 等)
6. 公共库统一从 `/lib/js/` 引用:`jquery-3.6.0.min.js``echarts-5.4.2.js``tailwindcss-3.4.17.js`(注意是连字符、在 `lib/js/` 下,不是 `js/`
7. 对于样式较新的页面,使用 `/lib/js/tailwindcss-3.4.17.js` 并参考 `index-2.php` 的布局风格
8.`index-2.php` 中添加导航入口
## 常用代码片段
@@ -63,16 +70,24 @@ $data = callTushareApi('daily_basic', $params);
### 前端 AJAX 调用模式
```php
// 请求参数中必须包含 t 字段,用于路由
$_REQUEST['t'] = 'stockList'; // ajax.inc.php 中的 case
// 请求参数中必须包含 t 字段,用于 ajax.inc.php 路由
$_REQUEST['t'] = 'stockList';
```
## 数据库表名约定
完整清单见 `DB_REFERENCE.md`(16 张表、在用/未用/已被 API 替代状态一目了然)。要点:
- **已被 API 替代、代码不再直接引用**:`index_hist_pro`(指数行情)、`stock_his_pro`(个股行情)、`stock_all_pro`(代码↔名称)——对应数据走 `api.doorcome.cn``indexDatas` / `stockbasic` / `stockinfo` 等端点。
- **在用**`moneyflow_hsgt_pro`(沪深港通原始值)、`moneyflow_hsgt_pro_ext`(累积值)、`estate_json` / `estate_json_ext` / `estate_listing`(房地产)、`trade_record` / `trade_record_cj`(方正/长江证券交易记录)、`mac_report`(宏观研究,quant/ 用)、`pv_log` / `pv_counter`(访问日志)。
- **机构持仓在 `ih_by_ts_code_ext`**,但消费它的页面(`chartSix.php``chartThree.php``stock_ih.php``index_ih.php`)已全部移入 `deprecated/`,当前无活跃调用者;`getIhData()` / `getIhDataByStock()` 保留在 `inc/getData.inc.php`
## 注意事项
- **TuShare API token 硬编码在代码中** —— 不要提交到公开仓库。
- **TuShare API token 硬编码在 `inc/config.php` 中** —— 不要提交到公开仓库。
- **没有鉴权机制** —— 这是内网或个人使用的系统,不需要添加登录/权限功能。
- **数据库表名约定** —— 指数数据在 `index_hist_pro`,个股历史在 `stock_his_pro`,基本面在 `stock_his_basic_pro`,机构持仓在 `ih_by_ts_code_ext`,房地产数据在 `estate_json` 系列,交易记录在 `trade_record`
- **`deprecated/` 目录** —— 存放已废弃的旧版页面,直接忽略,不要修改或引用。
- **`deprecated/` 目录** —— 存放已废弃的旧版页面(`chartOne`~`chartSix``stock_ih``index_ih`、旧版 `stock_trend`/`index_trend` 等,部分仍用 `window.chartConfig` 模式),直接忽略,不要修改或引用
- **`class/basic_info.class.php`** —— 独立的小类,未被核心流程引用。
- **`news/``research/`** —— 独立的子模块,有自己的 JS/CSS 和设计文档,修改前先看对应的 `design.md``outline.md`
## Checkpoint
@@ -82,3 +97,7 @@ $_REQUEST['t'] = 'stockList'; // ajax.inc.php 中的 case
- **当前状态**:刚刚完成了什么、改了哪些文件、结果如何
- **后续步骤**:具体的、有序的下一步行动
- **待解决问题**:未解决的疑问、已知限制或需要决策的事项
## Notes
(待补充:部署 `sync-echart` 命令细节见 CLAUDE.mdPHP 语法检查 `php -l` 修改后必做。)
+28 -22
View File
@@ -32,26 +32,30 @@
- `tradeRec.inc.php` — 交易记录查询(`trade_record` / `trade_record_cj` 表)。
- `excelOperate.inc.php` — 通过 PhpSpreadsheet 读取 Excel/CSV 文件。
- `postJson.inc.php` — 使用 cURL 发送 JSON HTTP POST 请求。
- `getEstate.inc.php` — 房地产相关数据查询(`estate_listing` 表)及二手房成交/挂牌数据函数
- `getEstate.inc.php` — 房地产相关数据查询(`estate_json``estate_json_ext``estate_listing` 表)及二手房成交/挂牌数据函数(`esfTradeDaily``esfListDaily`
- `upload.php` — 文件上传处理。
- `plansetup.php` — 用于 Composer autoload 的快捷入口(`__DIR__."/../vendor/autoload.php"`)。
> 数据库表全量清单(16 张表、在用/未用/已由 API 替代状态)见根目录 `DB_REFERENCE.md`。
### 页面结构
根目录及 `charts/` 下的每个 `.php` 文件渲染一个独立的数据视图,存在种数据获取模式:
根目录及 `charts/` 下的每个 `.php` 文件渲染一个独立的数据视图,存在种数据获取模式:
**模式 A传统——服务端注入):**
1. 引入所需的 `inc/*.php`
2. 通过 `$_REQUEST` 接收查询参数
3. PHP 查询数据库,将结果通过 `json_encode()` 注入 `<script>` 标签内的 `window.chartConfig`
4. 引入对应的 `js/*.js`JS 读取 `window.chartConfig` 后用 `echarts.init()` 渲染
5. 引入 `html/head.php``html/footer.php` 组成页面布局
**模式 A(服务端注入):**
1. 引入所需的 `inc/*.php`,通过 `$_REQUEST` 接收查询参数
2. PHP 查询数据库,将结果通过 `json_encode()` 注入 `<script>` 标签内的 JS 变量
3. 引入对应的 `js/*.js` 渲染
4. 注意:`window.chartConfig` 全局变量**仅剩 `deprecated/` 在用**;活跃页面中 `charts/chartStDetail.php` 是变体(注入命名变量 `data1``data5`
**模式 B(较新——JS fetch 调用外部 API):**
1. 页面直接加载 JS,不注入数据
2. JS 在 `$(document).ready` 中通过 `fetch()` 调用 `https://api.doorcome.cn/api/*` 获取数据
3. 渲染逻辑与模式 A 相同
4. 代表页面:`stock_trend.php``stockdiv.php``stockep.php``stockmargin.php``hkholdbycode.php`
2. JS 在 `$(document).ready` 中通过 `fetch()` 调用 `https://api.doorcome.cn/api/*`(如 `stockparam``indexDatas``stockinfo``getdiv``stockep``stockmargin``stockbasic`获取数据
3. 代表页面:`stock_trend.php``stockdiv.php``stockep.php``stockmargin.php``hkholdbycode.php``index_trend.php``index_mv_all.php``index_margin.php`
**模式 CAJAX → `inc/ajax.inc.php`):**
1. 页面 JS 用 `$.ajax``../inc/ajax.inc.php`,通过 `$_REQUEST['t']` 路由(if 链):`stockList``financeData``esfTBD``esfListDaily``newTBD``moneyflowData``estateData`
2. 代表页面:`moneyflow.php``realestate.php``estateTradeDaily.php``estateListDaily.php``estateNewTradeDaily.php``stockkeydata.php``stockTradeRecord.php`
### 页面目录
@@ -66,10 +70,8 @@
| `charts/stockdiv.php` | 股价 VS 股息率 |
| `charts/stockmargin.php` | 股价 VS 融资融券余额 |
| `charts/hkholdbycode.php` | 股价 VS 北向资金持股 |
| `charts/stock_ih.php` | 股价 VS 机构持仓趋势 |
| `charts/stockkeydata.php` | 个股基本面数据 |
| `charts/index_trend.php` | 指数 VS PE/PB/PS/市值 |
| `charts/index_ih.php` | 指数 VS 机构持仓趋势 |
| `charts/index_mv_all.php` | 指数 VS 两市总市值 |
| `charts/index_margin.php` | 指数 VS 融资余额 |
| `charts/moneyflow.php` | 指数 VS 沪深港通资金流向 |
@@ -78,13 +80,14 @@
| `charts/estateTradeDaily.php` | 二手房每日成交量(宁波) |
| `charts/estateListDaily.php` | 二手房每日挂牌量(宁波) |
| `charts/stockTradeRecord.php` | 个人股票交易记录 |
| `charts/chartStDetail.php` | 个股详细图表 |
| `charts/chartSix.php` | 个股 VS 机构持仓(持股数/金额/比例 |
| `charts/chartThree.php` | 指数 VS 机构持仓(分类型:基金/QFII/社保等) |
| `charts/myexcel.php` | 交易记录 Excel/CSV 导入处理(配合上传流程) |
| `charts/chartStDetail.php` | 个股详细图表(服务端注入命名变量 data1…data5 |
> 注:`stock_ih.php`、`index_ih.php`、`chartSix.php`、`chartThree.php` 及旧版 `stock_trend.php`/`index_trend.php`/`index_mv_all.php` 已移入 `deprecated/`,勿引用。
### JavaScript 约定
每个页面加载 `js/` 中对应的 JS 文件(如 `chartStDetail.js``renderCharts.js``estate.js`)。新版页面通过 `window.chartConfig` 对象传递数据,旧版页面正逐步迁移至此模式。ECharts 库统一使用 `lib/echarts/5.4.2/echarts.js`v5)。
每个页面加载 `js/` 中对应的 JS 文件(如 `chartStDetail.js``renderCharts.js``pubfunc.js``adj.ajax.js`)。新式图表页面复用 `js/renderCharts.js``doubleLineChart()` 等)和 `js/pubfunc.js``getRows()``hideSwitch()` 等)。ECharts 库统一 `/lib/js/echarts-5.4.2.js`v5引用。`window.chartConfig` 传递模式仅存于 `deprecated/`
### 子模块
@@ -96,9 +99,10 @@
### 前端库位置
- `lib/` — jQuery、DataTables、ECharts 5.4.2
- `libai/` — Chart.js、anime.js、shader-park-coreAI/研究页面使用)
- `js/`页面专属图表逻辑、Tailwind CSS 3.4.17、ECharts GL、ecStat
- `lib/js/` — jQuery 3.6.0、DataTables 1.13.4、ECharts 5.4.2`echarts-5.4.2.js`)、ECharts GL、ecStat、Tailwind CSS 3.4.17`tailwindcss-3.4.17.js`)、Chart.js、anime.js、marked、mermaid 等第三方库
- `lib/css/``lib/webfonts/` — 第三方 CSS 与字体
- `libai/`shader-park-coreAI/研究页面使用)
- `js/` — 页面专属图表逻辑(不含第三方库)
- `css/` — 自定义样式(`style.css``css2.css`)、Font Awesome
## 常用命令
@@ -112,9 +116,11 @@ php -l charts/stock_trend.php
composer update # 更新依赖
composer dump-autoload # 更新 autoload
# 部署到远程服务器(~/.bashrc 中定义
# 部署到远程服务器(脚本位于 ~/bin/sync-echartrsync 单向推送本地 → 服务器
sync-echart -n # 预览(dry-run
sync-echart # 执行同步部署到 www.doorcome.cn
sync-echart # 执行部署到 simon@www.doorcome.cn:/var/www/html/echart/
# 排除项: .git/.vscode/.claude/.serena/.mcp.json/reasonix.toml
# 以及数据/产物目录 uploads/xls/files/research/podcast/podcast-docs(服务器为权威,不覆盖)
# 批量 PHP 语法检查(修改多个文件后)
for f in charts/*.php; do php -l "$f"; done
+198
View File
@@ -0,0 +1,198 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>投资资讯日报</title>
<script src="/lib/js/tailwindcss-3.4.17.js"></script>
<script type="text/javascript" src="/lib/js/jquery-3.6.0.min.js"></script>
<link href="/lib/css/fontawesome-6.4.all.min.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#165DFF',
secondary: '#36D399',
neutral: '#F8FAFC',
dark: '#1E293B'
},
fontFamily: {
inter: ['Inter', 'system-ui', 'sans-serif'],
},
}
}
}
</script>
<style type="text/tailwindcss">
@layer components {
.badge {
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold whitespace-nowrap;
}
.badge-finance { @apply bg-blue-100 text-blue-700; }
.badge-intl { @apply bg-violet-100 text-violet-700; }
.badge-pos { @apply bg-emerald-100 text-emerald-700; }
.badge-neg { @apply bg-red-100 text-red-700; }
.badge-neu { @apply bg-gray-200 text-gray-600; }
.type-tab {
@apply px-4 py-2 rounded-lg text-sm font-medium text-gray-500 hover:text-primary hover:bg-primary/5 transition-colors cursor-pointer;
}
.tab-active { @apply bg-primary text-white hover:text-white hover:bg-primary shadow-sm; }
.btn-view {
@apply inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-semibold text-primary bg-primary/10 hover:bg-primary hover:text-white transition-colors whitespace-nowrap;
}
.report-card {
@apply bg-white rounded-xl shadow-sm border border-gray-100 p-5 hover:shadow-md hover:border-primary/30 transition-all cursor-pointer;
}
.stat-bit {
@apply inline-flex items-center gap-1 px-2 py-1 rounded-md bg-gray-50 border border-gray-100 text-xs text-gray-500;
}
.module-card {
@apply bg-white rounded-xl shadow-sm border border-gray-100 p-5 md:p-6 mb-6;
}
.module-title {
@apply flex items-center gap-2 text-xl font-bold text-dark mb-4;
}
.module-sub { @apply ml-2 text-sm font-normal text-gray-400; }
.ai-summary-list {
@apply space-y-2.5 text-base leading-relaxed text-gray-700;
}
.ai-summary-list li {
@apply pl-4 relative;
}
.ai-summary-list li::before {
content: "";
@apply absolute left-0 text-primary;
}
.event-row { @apply flex gap-3 py-3.5 px-2 hover:bg-gray-50 transition-colors rounded-lg; }
.event-left { @apply flex flex-col items-center gap-1 pt-0.5 shrink-0 w-9; }
.event-rank { @apply text-sm text-gray-400 font-mono; }
.event-body { @apply flex-1 min-w-0; }
.event-title-line { @apply flex items-start gap-2.5; }
.imp-badge { @apply shrink-0 mt-0.5 inline-flex items-center justify-center w-7 h-7 rounded-md text-sm font-bold bg-gray-100 text-gray-500; }
.imp-5 { @apply bg-red-100 text-red-700; }
.imp-4 { @apply bg-orange-100 text-orange-600; }
.event-title { @apply text-base font-medium text-gray-800 leading-snug; }
.event-link { @apply text-gray-800 hover:text-primary transition-colors; }
.event-summary { @apply mt-1.5 text-sm text-gray-500 leading-relaxed line-clamp-3; }
.event-meta { @apply mt-2.5 flex flex-wrap gap-x-4 gap-y-1 text-sm text-gray-400; }
.meta-item { @apply inline-flex items-center; }
.stats-grid { @apply grid grid-cols-3 md:grid-cols-6 gap-3 mb-4; }
.stat-card {
@apply bg-gray-50 border border-gray-100 rounded-xl p-3 text-center;
}
.stat-num { @apply text-2xl font-bold text-dark; }
.stat-label { @apply mt-1.5 text-sm text-gray-500; }
.stats-sub { @apply grid grid-cols-2 md:grid-cols-5 gap-3 mb-3; }
.stats-sub-title { @apply text-base font-semibold text-gray-600 mt-2 mb-2; }
.kv-table-wrap { @apply mb-4; }
.kv-title { @apply text-base font-semibold text-gray-600 mb-1.5; }
.kv-table { @apply w-full text-base border-collapse; }
.kv-table th { @apply text-left text-sm font-semibold text-gray-500 bg-gray-50 px-3 py-2 border border-gray-100; }
.kv-table td { @apply px-3 py-2 border border-gray-100 text-gray-700; }
.sentiment-wrap { @apply mb-4; }
.sentiment-bar { @apply flex h-4 rounded-full overflow-hidden mb-2; }
.s-pos { @apply bg-emerald-500; }
.s-neg { @apply bg-red-500; }
.s-neu { @apply bg-gray-400; }
.sentiment-legend { @apply flex flex-wrap gap-4 text-xs text-gray-500; }
.s-dot { @apply inline-block w-2.5 h-2.5 rounded-full mr-1; }
.s-dot.s-pos { @apply bg-emerald-500; }
.s-dot.s-neg { @apply bg-red-500; }
.s-dot.s-neu { @apply bg-gray-400; }
.source-grid { @apply grid grid-cols-3 md:grid-cols-6 gap-3; }
.source-item { @apply bg-gray-50 border border-gray-100 rounded-lg p-2.5 text-center; }
.s-count { @apply text-xl font-bold text-dark; }
.s-name { @apply mt-1 text-sm text-gray-500 truncate; }
.empty-box {
@apply bg-white rounded-xl shadow-sm border border-gray-100 py-16 text-center text-gray-400;
}
.empty-box i { @apply text-4xl mb-3 block; }
.empty-box p { @apply text-sm; }
.filter-input {
@apply px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary/50;
}
.btn-primary {
@apply inline-flex items-center px-4 py-2 bg-primary text-white rounded-lg hover:bg-blue-600 transition-colors text-sm font-medium;
}
}
</style>
</head>
<body class="bg-gray-50 font-inter text-dark min-h-screen">
<header class="bg-white shadow-sm sticky top-0 z-50">
<div class="container mx-auto px-4 py-4 flex justify-between items-center">
<div class="flex items-center space-x-2">
<i class="fa-solid fa-newspaper text-primary text-2xl"></i>
<h1 class="text-xl font-bold">投资资讯日报</h1>
<span class="text-xs text-gray-400 hidden md:inline ml-1">国内 / 国际 · AI 深度研究日报</span>
</div>
<a href="/index-2.php" class="inline-flex items-center text-sm text-gray-500 hover:text-primary transition-colors">
<i class="fa-solid fa-house mr-1"></i>返回门户
</a>
</div>
</header>
<main class="container mx-auto px-4 py-6 max-w-5xl">
<!-- 筛选栏 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-4 mb-6">
<div class="flex flex-wrap items-center gap-3">
<div class="flex bg-gray-100 rounded-lg p-1">
<span class="type-tab tab-active" data-type="">全部</span>
<span class="type-tab" data-type="finance">国内</span>
<span class="type-tab" data-type="intl">国际</span>
</div>
<div class="flex items-center gap-2 ml-auto flex-wrap">
<label class="text-sm text-gray-500">开始</label>
<input type="date" id="startDate" class="filter-input">
<label class="text-sm text-gray-500">结束</label>
<input type="date" id="endDate" class="filter-input">
<button id="btnQuery" class="btn-primary"><i class="fa-solid fa-magnifying-glass mr-1"></i>查询</button>
</div>
</div>
<div class="mt-3 text-sm text-gray-400" id="rangeInfo"></div>
</div>
<!-- 列表视图 -->
<div id="listView">
<div id="reportList" class="space-y-4"></div>
</div>
<!-- 详情视图 -->
<div id="detailView" class="hidden">
<div class="flex items-center justify-between mb-4 flex-wrap gap-2">
<button id="btnBack" class="inline-flex items-center text-sm text-gray-500 hover:text-primary transition-colors">
<i class="fa-solid fa-arrow-left mr-1"></i>返回列表
</button>
<div id="detailHeader" class="flex items-center gap-3 flex-wrap"></div>
</div>
<div id="detailContent"></div>
</div>
<!-- 加载遮罩 -->
<div id="loadingOverlay" class="hidden fixed inset-0 bg-white/70 backdrop-blur-sm z-40 flex items-center justify-center">
<div class="text-center">
<div class="inline-block w-10 h-10 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
<p class="mt-3 text-sm text-gray-500" id="loadingMsg">加载中...</p>
</div>
</div>
</main>
<footer class="text-center text-gray-400 text-sm py-6">
数据来源: api.doorcome.cn 日报接口 · 仅供个人研究参考
</footer>
<script src="/js/newsReports.js"></script>
</body>
</html>
+33 -35
View File
@@ -1,46 +1,44 @@
# Continuation
# continuation.md — 会话检查点
> 生成时间:2026-08-04(本会话)
> 工作区:/Users/summer/Downloads/py/wwwechart 平台,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 文件链接(用 `<title>` 元素作为链接名)
- 部署到 `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/` 每日报告目录(2026060820260804)。`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→cninfointl: 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/` 替换
- **本机无 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` 被拒(后来明确要求后成功保存);如后续会话需重新保存,注意用户偏好。
+5 -58
View File
@@ -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();
+476
View File
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
$(function () {
NewsReports.init();
});
+120
View File
@@ -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(开发用) |
+181
View File
@@ -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>播客处理流水线 — 使用说明</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; line-height: 1.6; color: #333; }
h1 { border-bottom: 2px solid #2563eb; padding-bottom: 8px; }
h2 { margin-top: 32px; border-bottom: 1px solid #e5e7eb; padding-bottom: 4px; }
pre { background: #f3f4f6; padding: 12px; border-radius: 8px; overflow-x: auto; font-size: 14px; }
code { background: #f3f4f6; padding: 2px 6px; border-radius: 4px; font-size: 14px; }
table { border-collapse: collapse; width: 100%; margin: 12px 0; }
th, td { border: 1px solid #e5e7eb; padding: 8px 12px; text-align: left; }
th { background: #f9fafb; }
.tip { background: #eff6ff; border-left: 4px solid #2563eb; padding: 12px; margin: 16px 0; border-radius: 4px; }
.warn { background: #fef3c7; border-left: 4px solid #f59e0b; padding: 12px; margin: 16px 0; border-radius: 4px; }
</style>
</head>
<body>
<h1>播客处理流水线 · 使用说明</h1>
<p>从 Apple Podcast URL 自动获取播客 → 语音转文字 → 中文翻译 → AI 要点分析</p>
<h2>一、地址总览</h2>
<table>
<tr><th>用途</th><th>地址</th></tr>
<tr><td>前端首页(时间线)</td><td><a href="https://doorcome.cn/podcast/">https://doorcome.cn/podcast/</a></td></tr>
<tr><td>添加播客</td><td><a href="https://doorcome.cn/podcast/new">https://doorcome.cn/podcast/new</a></td></tr>
<tr><td>播客管理</td><td><a href="https://doorcome.cn/podcast/podcasts">https://doorcome.cn/podcast/podcasts</a></td></tr>
<tr><td>API 文档 (Swagger)</td><td><a href="https://doorcome.cn/api/docs/">https://doorcome.cn/api/docs/</a></td></tr>
<tr><td>管理后台</td><td><a href="https://doorcome.cn/admin/">https://doorcome.cn/admin/</a></td></tr>
</table>
<h2>二、前端使用</h2>
<h3>2.1 添加播客</h3>
<ol>
<li>打开「添加」页面</li>
<li>粘贴 Apple Podcast 链接,格式如:<br>
<code>https://podcasts.apple.com/cn/podcast/lex-fridman-podcast/id1434243584</code></li>
<li>点击「添加」按钮</li>
<li>系统自动获取播客信息、节目列表,后台 AI 开始处理</li>
</ol>
<div class="tip">
✅ 支持所有 Apple Podcast URL。系统会通过 iTunes API 获取元数据,
然后解析 RSS Feed 获取所有节目。
</div>
<h3>2.2 查看要点</h3>
<ul>
<li>首页按日期分组展示已处理的要点卡片</li>
<li>可按播客来源或日期过滤</li>
<li>卡片包含:封面、标题、一句话摘要、标签</li>
<li>点击「详情」查看完整内容</li>
<li>点击「播放」在线听音频</li>
</ul>
<h3>2.3 单集详情</h3>
<ul>
<li>音频播放器(顶部)</li>
<li>要点导航(点击展开/收起各话题)</li>
<li>全文展示:原文 / 中文翻译一键切换</li>
</ul>
<h3>2.4 播客管理</h3>
<ul>
<li>查看已添加的所有播客</li>
<li>删除播客及其所有节目</li>
</ul>
<h2>三、后台管理</h2>
<p>访问 <code>/admin/</code> 使用管理员账号登录。</p>
<table>
<tr><th>模型</th><th>功能</th></tr>
<tr><td>Podcast(播客)</td><td>查看、搜索所有播客</td></tr>
<tr><td>Episode(节目)</td><td>查看处理进度、过滤状态、重试失败节目</td></tr>
<tr><td>Transcript(转写)</td><td>查看 ASR 结果和翻译内容</td></tr>
<tr><td>Highlight(要点)</td><td>查看 AI 分析结果</td></tr>
</table>
<div class="tip">
处理状态说明:
<code>pending</code><code>downloading</code><code>downloaded</code>
<code>transcribing</code><code>transcribed</code>
<code>translating</code><code>translated</code>
<code>analyzing</code><code>completed</code><br>
任意步骤失败 → <code>failed</code>Admin 可选重试)
</div>
<h2>四、AI 处理流程</h2>
<p>添加播客后,Celery Worker 自动处理每个节目:</p>
<pre>
Step 1: 下载音频文件到服务器
Step 2: 千问 DashScope ASR → 语音转文字(带时间戳分段)
Step 3: 检测语种:
· 非中文 → DeepSeek 翻译成中文
· 中文 → 跳过翻译
Step 4: DeepSeek 要点分析 → JSON 结构化结果
· 一句话摘要
· 详细摘要(3-5段)
· 要点列表(话题 + 观点 + 时间戳)
· 标签
最终状态 → completed。
失败自动重试 3 次,每次间隔 60 秒。
</pre>
<h2>五、命令行操作</h2>
<p>SSH 到服务器后使用:</p>
<pre>
# 添加播客
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
</pre>
<h2>六、API 参考</h2>
<p>完整 API 文档在 <a href="https://doorcome.cn/api/docs/">Swagger UI</a></p>
<table>
<tr><th>方法</th><th>路径</th><th>说明</th></tr>
<tr><td>POST</td><td>/api/podcasts/add/</td><td>通过 Apple URL 添加播客</td></tr>
<tr><td>GET</td><td>/api/podcasts/</td><td>播客列表</td></tr>
<tr><td>GET</td><td>/api/podcasts/{id}/</td><td>播客详情(含节目列表)</td></tr>
<tr><td>DELETE</td><td>/api/podcasts/{id}/</td><td>删除播客</td></tr>
<tr><td>GET</td><td>/api/episodes/</td><td>节目列表(支持 status/podcast_id 过滤)</td></tr>
<tr><td>GET</td><td>/api/episodes/{id}/</td><td>单集详情(含转写+要点)</td></tr>
<tr><td>POST</td><td>/api/episodes/{id}/retry/</td><td>重试失败的节目</td></tr>
<tr><td>GET</td><td>/api/highlights/</td><td>要点列表(支持 date/podcast_id 过滤)</td></tr>
<tr><td>GET</td><td>/api/highlights/today/</td><td>今日要点</td></tr>
</table>
<h2>七、管理员账号</h2>
<table>
<tr><th>地址</th><th>用户名</th><th>密码</th></tr>
<tr><td><a href="https://doorcome.cn/admin/">https://doorcome.cn/admin/</a></td><td>admin</td><td>admin123</td></tr>
</table>
<div class="warn">
⚠️ 首次使用建议修改默认密码。
</div>
<h2>八、配置说明</h2>
<p>服务器配置文件:<code>/home/simon/myquant/django/podcast/.env</code></p>
<table>
<tr><th>配置项</th><th>说明</th></tr>
<tr><td>DASHSCOPE_API_KEY</td><td>千问 DashScope API KeyASR 语音转文字)</td></tr>
<tr><td>DEEPSEEK_API_KEY</td><td>DeepSeek API Key(翻译 + 要点分析)</td></tr>
<tr><td>DASHSCOPE_ASR_MODEL</td><td>ASR 模型名(默认 paraformer-realtime-v2</td></tr>
<tr><td>DEEPSEEK_MODEL</td><td>DeepSeek 模型名(默认 deepseek-chat</td></tr>
<tr><td>DEV_MOCK_ASR</td><td>开发模式:1=跳过真实 ASR,0=调用真实 API</td></tr>
</table>
</body>
</html>
+390
View File
@@ -0,0 +1,390 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IntlNews 操作手册</title>
<style>
:root {
--bg: #1a1a2e;
--card: #16213e;
--accent: #0f3460;
--green: #00c853;
--yellow: #ffd600;
--red: #ff1744;
--text: #e0e0e0;
--muted: #8892b0;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--text); line-height: 1.7; padding: 20px; }
.container { max-width: 960px; margin: 0 auto; }
h1 { font-size: 2em; margin: 1em 0 0.3em; color: #fff; border-bottom: 2px solid var(--accent); padding-bottom: 0.3em; }
h2 { font-size: 1.4em; margin: 1.5em 0 0.5em; color: #64ffda; }
h3 { font-size: 1.1em; margin: 1em 0 0.3em; color: #80cbc4; }
p, li { margin: 0.4em 0; }
code { background: #0d2137; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; color: #a8d8ea; }
pre { background: #0d2137; padding: 12px 16px; border-radius: 6px; overflow-x: auto; border: 1px solid #1a3a5c; margin: 0.8em 0; }
table { width: 100%; border-collapse: collapse; margin: 1em 0; }
th, td { padding: 8px 12px; text-align: left; border-bottom: 1px solid #1a3a5c; }
th { background: var(--accent); color: #fff; font-weight: 600; }
tr:hover { background: #1a2a4a; }
.tag { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 0.8em; font-weight: 600; }
.tag-ok { background: #1b5e20; color: #a5d6a7; }
.tag-warn { background: #e65100; color: #ffcc80; }
.tag-err { background: #b71c1c; color: #ef9a9a; }
.tag-new { background: #0d47a1; color: #90caf9; }
.info-box { background: #0d2137; border-left: 4px solid #64ffda; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 1em 0; }
.warn-box { background: #1e1a0d; border-left: 4px solid #ffd600; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 1em 0; }
.cmd { display: block; padding: 4px 0; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 0.9em; }
.cmd::before { content: '$ '; color: #64ffda; }
.footer { margin-top: 3em; padding-top: 1em; border-top: 1px solid #1a3a5c; color: var(--muted); font-size: 0.85em; text-align: center; }
</style>
</head>
<body>
<div class="container">
<h1>📰 IntlNews — 国际财经新闻 Deep Research</h1>
<p style="color:var(--muted);">最后更新:2026-07-23 | 运行服务器:pi@192.168.1.160</p>
<div class="info-box">
<strong>项目定位:</strong>面向国际财经新闻的私有化 Deep Research 平台。抓取英文财经新闻 → 英译中 → 事件抽取 → 向量入库 → MCP Agent 深度研究。
</div>
<!-- ==================== 一、架构 ==================== -->
<h1>一、系统架构</h1>
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 250'%3E%3Crect width='400' height='250' fill='%231a1a2e'/%3E%3Ctext x='20' y='30' fill='%2364ffda' font-size='14'%3EPi 4 (8GB) — 全链路独立运行%3C/text%3E%3C/text%3E%3C/svg%3E" alt="Arch" style="display:none;">
<pre>
┌──────────────────────────────────────────────────────┐
│ 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 │ │
│ └────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
</pre>
<div class="info-box">
<strong>关键变更:</strong>海外服务器已于 2026-07-14 停用。全链路在 Pi 独立运行,通过 <code>Privoxy → SOCKS5 (ss-local)</code> 代理访问海外新闻网站。<br>
<strong>反爬策略:</strong>2026-07-23 更新 — 启用 Crawl4AI <code>magic</code> 全自动反检测 + Playwright <code>stealth</code> 模式。investing.com 改用 Google News RSS 代理。
</div>
<!-- ==================== 二、部署 ==================== -->
<h1>二、部署指南</h1>
<h2>2.1 环境要求</h2>
<ul>
<li>Python 3.11 + uv</li>
<li>Xvfbheadful Playwright 虚拟显示器)</li>
<li>Shadowsocks-libev + v2ray-pluginSOCKS5 代理)</li>
<li>PrivoxyHTTP → SOCKS5 转换)</li>
</ul>
<h2>2.2 初始化安装</h2>
<pre>
# 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 &amp;
# 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
</pre>
<h2>2.3 定时调度(crontab</h2>
<pre>
# 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 &amp;
</pre>
<!-- ==================== 三、操作命令 ==================== -->
<h1>三、操作命令</h1>
<h2>3.1 全流程运行</h2>
<pre>
# 一键全流程(M1→M6→日报)
bash scripts/domestic_full.sh
</pre>
<h2>3.2 M1 抓取</h2>
<pre>
# 抓取全部 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
</pre>
<h2>3.3 M2→M6 管道</h2>
<pre>
# 完整管道(需要先有 raw 数据)
bash scripts/pipeline.sh
# 或逐步执行:
PYTHONPATH=. .venv/bin/python3 -c "from scheduler.pipeline import run_pipeline; run_pipeline('$(date +%Y%m%d)')"
</pre>
<h2>3.4 日报</h2>
<pre>
# 生成当日日报
PYTHONPATH=. .venv/bin/python3 -c "from scheduler.reporter import generate_report; print(generate_report())"
# 查看日报列表
ls -lt data/reports/
</pre>
<h2>3.5 日志查看</h2>
<pre>
# 全量日志
tail -100 logs/full.log
# 筛选指定源
grep 'investing\|investinglive' logs/full.log | tail -20
</pre>
<!-- ==================== 四、新闻源状态 ==================== -->
<h1>四、新闻源状态</h1>
<table>
<tr><th></th><th>ID</th><th>策略</th><th>日产量</th><th>状态</th></tr>
<tr>
<td>Reuters</td><td><code>reuters</code></td><td>Google News RSS</td><td>~30 篇</td>
<td><span class="tag tag-warn">摘要</span> DataDome</td>
</tr>
<tr>
<td>CNBC</td><td><code>cnbc</code></td><td>CNBC RSS</td><td>~25 篇</td>
<td><span class="tag tag-ok">正常</span></td>
</tr>
<tr>
<td>MarketWatch</td><td><code>marketwatch</code></td><td>RSS + stealth</td><td>~25 篇</td>
<td><span class="tag tag-ok">正常</span></td>
</tr>
<tr>
<td>Financial Times</td><td><code>ft</code></td><td>FT RSS</td><td>~20 篇</td>
<td><span class="tag tag-warn">摘要</span> 付费墙</td>
</tr>
<tr>
<td>Yahoo Finance</td><td><code>yahoo_finance</code></td><td>Yahoo RSS</td><td>~30 篇</td>
<td><span class="tag tag-ok">正常</span></td>
</tr>
<tr>
<td>Investing.com</td><td><code>investing</code></td><td><strong>Google News RSS</strong> + stealth</td><td>25 篇</td>
<td><span class="tag tag-new">已修复</span> RSS 代理</td>
</tr>
<tr>
<td>Seeking Alpha</td><td><code>seekingalpha</code></td><td>RSS + stealth</td><td>~20 篇</td>
<td><span class="tag tag-ok">正常</span></td>
</tr>
<tr>
<td>Barron's</td><td><code>barrons</code></td><td>Dow Jones RSS + stealth</td><td>~20 篇</td>
<td><span class="tag tag-ok">正常</span></td>
</tr>
<tr>
<td>WSJ</td><td><code>wsj</code></td><td>Dow Jones RSS + stealth</td><td>~20 篇</td>
<td><span class="tag tag-ok">正常</span></td>
</tr>
<tr>
<td>Economist</td><td><code>economist</code></td><td>RSS + stealth</td><td>~15 篇</td>
<td><span class="tag tag-ok">正常</span></td>
</tr>
<tr>
<td style="background:#0d47a1;">InvestingLive</td><td><code>investinglive</code></td><td><strong>RSS(全文)</strong></td><td><strong>25 篇</strong></td>
<td><span class="tag tag-new">2026-07-23 新增</span></td>
</tr>
<tr>
<td>ZeroHedge</td><td><code>zerohedge</code></td><td>FeedBurner RSS</td><td>~15 篇</td>
<td><span class="tag tag-ok">正常</span></td>
</tr>
<tr style="color:var(--muted); text-decoration:line-through;">
<td>ForexLive</td><td><del><code>forexlive</code></del></td><td></td><td></td>
<td><span class="tag tag-err">已移除</span> 301→investinglive</td>
</tr>
</table>
<div class="warn-box">
<strong>⚠️ 注意:</strong>RSS 摘要源的全文依赖 headful Playwright 回退抓取,成功率取决于代理线路质量。investing.com 因 Cloudflare 反爬,全文抓取间歇性失败。
</div>
<!-- ==================== 五、数据存储 ==================== -->
<h1>五、数据存储</h1>
<pre>
/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 模板
</pre>
<!-- ==================== 六、MCP 服务 ==================== -->
<h1>六、MCP 服务</h1>
<p>M8 模块提供 MCPModel Context Protocol)服务,供 Cherry Studio / Claude Code / Cursor 等客户端接入。</p>
<h2>6.1 启动</h2>
<pre>
cd /home/pi/intlnews
PYTHONPATH=. .venv/bin/python3 -m mcp_server.server
</pre>
<h2>6.2 可用工具</h2>
<table>
<tr><th>Tool</th><th>参数</th><th>功能</th></tr>
<tr><td><code>en_news_search</code></td><td>query, top_k</td><td>语义搜索知识库</td></tr>
<tr><td><code>en_news_filter</code></td><td>source_id, date, sentiment</td><td>按条件筛选事件</td></tr>
<tr><td><code>en_news_trending</code></td><td>days</td><td>获取近期热点</td></tr>
<tr><td><code>en_news_report</code></td><td></td><td>获取最新日报</td></tr>
<tr><td><code>en_news_daily_brief</code></td><td></td><td>一键生成简报</td></tr>
</table>
<!-- ==================== 七、配置参考 ==================== -->
<h1>七、配置参考</h1>
<h2>7.1 环境变量(.env</h2>
<table>
<tr><th>变量</th><th>用途</th></tr>
<tr><td><code>DEEPSEEK_API_KEY</code></td><td>DeepSeek LLM(翻译+事件抽取)</td></tr>
<tr><td><code>DASHSCOPE_API_KEY</code></td><td>DashScope Embedding(向量生成)</td></tr>
<tr><td><code>QDRANT_URL</code></td><td>Qdrant 服务地址</td></tr>
</table>
<h2>7.2 新闻源配置(sources.yaml</h2>
<p>每个源包含:id, name, homepage, rss_url, article_url_pattern, anti_bot_mode 等。</p>
<p>新增源时参考现有格式,启用/禁用通过 <code>enabled: true/false</code></p>
<h2>7.3 Profile 配置</h2>
<p>Pi 服务器使用 <code>8g_headful</code> profile(环境变量 <code>EN_NEWS_PROFILE</code>),通过 Privoxy HTTP 代理访问海外网站。</p>
<ul>
<li><code>proxy.enabled: true</code> — 启用 HTTP 代理</li>
<li><code>crawler.headful: true</code> — 有头浏览器 + Xvfb</li>
<li><code>crawler.magic: true</code> — Crawl4AI 全自动反检测</li>
<li><code>crawler.enable_stealth: true</code> — Playwright stealth</li>
</ul>
<!-- ==================== 八、排障 ==================== -->
<h1>八、常见排障</h1>
<table>
<tr><th style="width:200px;">现象</th><th>排查步骤</th></tr>
<tr>
<td>抓取 0 篇</td>
<td>
1. 检查 <code>logs/full.log</code> 看 RSS/Web 错误<br>
2. <code>bash scripts/domestic_crawl_8g.sh &lt;source_id&gt;</code> 单源测试<br>
3. 检查代理是否正常:<code>curl -I --proxy 127.0.0.1:3128 https://example.com</code>
</td>
</tr>
<tr>
<td>RSS 超时</td>
<td>
1. 确认 Shadowsocks 和 Privoxy 在运行<br>
2. <code>systemctl status shadowsocks-libev-local privoxy</code><br>
3. 环境变量 <code>HTTP_PROXY</code> 是否设置
</td>
</tr>
<tr>
<td>Cloudflare 拦截</td>
<td>
1. 已启用 <code>magic</code> + <code>stealth</code> 自动反检测<br>
2. 仍失败则需更换代理 IP(数据中心 IP 被标记)<br>
3. 改用 Google News RSS 代理(已为 investing/reuters 启用)
</td>
</tr>
<tr>
<td>日报无数据</td>
<td>
1. 检查 <code>data/translations/{date}/</code> 是否有 JSONL<br>
2. 检查 <code>data/embeddings/{date}/</code> 是否有向量
</td>
</tr>
<tr>
<td>MCP 连接失败</td>
<td>
1. 确认 MCP 服务进程在运行<br>
2. 检查端口监听:<code>ss -tlnp | grep 8080</code>
</td>
</tr>
</table>
<!-- ==================== 九、变更记录 ==================== -->
<h1>九、变更记录</h1>
<table>
<tr><th>日期</th><th>变更</th></tr>
<tr>
<td>2026-07-23</td>
<td>
<strong>反爬策略增强:</strong><br>
- crawler.py: magic/stealth/--disable-webrtc/remove_consent_popups<br>
- rss_crawler.py: httpx 代理支持<br>
- investing.com: Google News RSS 代理(绕过 Cloudflare<br>
- <strong>forexlive → investinglive 迁移</strong>(域名已 301 重定向)
</td>
</tr>
<tr>
<td>2026-07-14</td>
<td>
- 海外服务器停用,Pi 全链路独立运行<br>
- 8g_headful profile 新增(headful + HTTP 代理)<br>
- Shadowsocks + Privoxy 部署
</td>
</tr>
</table>
<div class="footer">
IntlNews — 国际财经新闻私有化 Deep Research 平台<br>
运行于 pi@192.168.1.160 | 调度 06:00 / 12:00 / 18:00 / 22:00
</div>
</div>
</body>
</html>
+254
View File
@@ -0,0 +1,254 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>A 股 Deep Research 用户手册 v1.1</title>
<style>
:root { --bg: #f8f9fa; --card: #fff; --text: #212529; --muted: #6c757d;
--accent: #2563eb; --border: #dee2e6; --code-bg: #f1f3f5; --radius: 8px; }
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: var(--bg); color: var(--text); line-height: 1.75; padding-bottom: 4rem; }
.container { max-width: 900px; margin: 0 auto; padding: 1.5rem; }
header { background: linear-gradient(135deg, #1e293b, #334155); color: #fff; padding: 3rem 0 2rem; text-align: center; }
header h1 { font-size: 2rem; }
header p { color: #94a3b8; margin-top: .5rem; }
nav { background: var(--card); border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 10; padding: .75rem 0; }
nav a { color: var(--accent); text-decoration: none; margin-right: 1rem; font-size: .9rem; }
h2 { font-size: 1.5rem; margin: 2.5rem 0 1rem; padding-bottom: .5rem; border-bottom: 2px solid var(--accent); }
h3 { font-size: 1.2rem; margin: 1.8rem 0 .8rem; color: #1e293b; }
h4 { font-size: 1rem; margin: 1.2rem 0 .5rem; }
p { margin: .6rem 0; }
a { color: var(--accent); }
code { background: var(--code-bg); padding: .15em .4em; border-radius: 4px; font-family: "SF Mono", "Fira Code", monospace; font-size: .88em; }
pre { background: #1e293b; color: #e2e8f0; padding: 1rem 1.2rem; border-radius: var(--radius); overflow-x: auto; font-size: .85em; line-height: 1.6; margin: .8rem 0; }
pre code { background: none; padding: 0; color: inherit; }
table { width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: .93em; }
th, td { border: 1px solid var(--border); padding: .5rem .7rem; text-align: left; }
th { background: #f1f5f9; font-weight: 600; }
tr:nth-child(even) { background: #fafbfc; }
ul, ol { padding-left: 1.5rem; margin: .6rem 0; }
li { margin: .25rem 0; }
.card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem 1.5rem; margin: 1rem 0; }
.badge-ok { background: #d1fae5; color: #065f46; display: inline-block; padding: .1em .5em; border-radius: 10px; font-size: .8em; font-weight: 600; }
.badge-warn { background: #fef3c7; color: #92400e; display: inline-block; padding: .1em .5em; border-radius: 10px; font-size: .8em; }
.badge-err { background: #fee2e2; color: #991b1b; display: inline-block; padding: .1em .5em; border-radius: 10px; font-size: .8em; }
footer { text-align: center; color: var(--muted); font-size: .85em; margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--border); }
@media (max-width: 640px) { .container { padding: .8rem; } header h1 { font-size: 1.5rem; } }
</style>
</head>
<body>
<header><div class="container"><h1>A 股 Deep Research 私有投研平台</h1><p>用户手册 v1.1 · 2026-06-17</p></div></header>
<nav><div class="container">
<a href="#概述">概述</a> <a href="#快速开始">快速开始</a> <a href="#cli">CLI 速查</a>
<a href="#全链路">全链路</a> <a href="#站点管理">站点管理</a> <a href="#检索">检索</a>
<a href="#定时任务">定时任务</a> <a href="#mcp">MCP</a> <a href="#投研">投研</a> <a href="#附录">附录</a>
</div></nav>
<main class="container">
<h2 id="概述">1. 项目概述</h2>
<p><strong>A 股 Deep Research</strong> 是私有化投研平台,自动完成财经新闻抓取 → 正文提取 → 事件抽取 → 向量化 → 语义检索,通过 MCP 协议接入 Cherry Studio / Claude Code,实现「自然语言提问 → 结构化研究报告」。</p>
<p><strong>技术栈</strong>: Python 3.11 · Crawl4AI · GNE · DeepSeek/Qwen · DashScope · Qdrant · APScheduler · MCP</p>
<p><strong>部署</strong>: 树莓派 5 (ARM64) <code>/home/pi/news/</code></p>
<h2 id="快速开始">2. 快速开始</h2>
<h3>2.1 安装</h3>
<pre><code>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 抓取需要</code></pre>
<h3>2.2 验证</h3>
<pre><code>uv run pytest -m "not integration" # 应显示 186 passed</code></pre>
<h2 id="cli">3. 统一 CLI 速查</h2>
<p>所有操作通过 <code>a-share</code> 命令完成。</p>
<table>
<tr><th>命令</th><th>功能</th><th>常用参数</th></tr>
<tr><td><code>a-share crawl</code></td><td>M1 抓取新闻</td><td><code>--source cls</code></td></tr>
<tr><td><code>a-share extract</code></td><td>M2 正文提取</td><td><code>--date 20260616</code></td></tr>
<tr><td><code>a-share dedup</code></td><td>M3 三层去重</td><td><code>--date 20260616 --reset</code></td></tr>
<tr><td><code>a-share events</code></td><td>M4 LLM 事件抽取</td><td><code>--provider qwen --limit 5</code></td></tr>
<tr><td><code>a-share embed</code></td><td>M5 向量化</td><td><code>--model text-embedding-v3</code></td></tr>
<tr><td><code>a-share ingest</code></td><td>M6 Qdrant 入库</td><td><code>--date 20260616 --recreate</code></td></tr>
<tr><td><code>a-share pipeline</code></td><td>全链路 (M1→M6)</td><td><code>--once --report</code></td></tr>
<tr><td><code>a-share discover</code></td><td>分析站点, 生成配置</td><td><code>--name 中文名 --add --extra URL</code></td></tr>
<tr><td><code>a-share add-entry</code></td><td>追加入口到已有源</td><td><code>source_id url1 url2 ...</code></td></tr>
<tr><td><code>a-share report</code></td><td>生成/上传日报</td><td><code>--date 20260616 --no-upload</code></td></tr>
<tr><td><code>a-share search</code></td><td>终端检索知识库</td><td><code>--source --stock --sentiment --top</code></td></tr>
<tr><td><code>a-share status</code></td><td>数据总览</td><td>无参数</td></tr>
</table>
<h2 id="全链路">4. 全链路一键运行</h2>
<pre><code>uv run a-share pipeline --once # 一次跑通 M1→M6
uv run a-share pipeline --once --report # 末尾自动生成日报并上传</code></pre>
<p>耗时约 4-5 分钟 (100 篇): crawler ~3min / extractor ~24s / dedup ~2s / llm ~45s / embedding ~10s / qdrant ~4s</p>
<p>只执行部分步骤: <code>uv run a-share pipeline --once --steps crawler,llm</code></p>
<p><strong>产物路径</strong>: <code>data/raw/</code> (M1) → <code>data/processed/</code> (M2) → <code>data/deduped/</code> (M3) → <code>data/events/</code> (M4) → <code>data/embeddings/</code> (M5) → <code>data/qdrant_storage/</code> (M6)</p>
<h2 id="站点管理">5. 新闻源管理</h2>
<h3>5.1 查看已有源</h3>
<p>配置文件: <code>configs/sources.yaml</code>。内置财联社 / 东方财富 / 新浪财经 / 证券时报 / 第一财经。</p>
<h3>5.2 新增站点 (一条命令)</h3>
<pre><code># 自动分析 + 写入 sources.yaml
uv run a-share discover https://example.com/news/ --name 中文名 --add
# 仅预览
uv run a-share discover https://example.com/news/</code></pre>
<p>自动完成: JS 抓取 → 链接提取 → 导航过滤 → 模式聚类 → 正则生成 → yaml 输出。写入时自动空行分隔、序号递增、<code># ---- N. 名称 ----</code> 注释头。</p>
<h3>5.3 同站多频道</h3>
<pre><code># 一次性分析多入口
uv run a-share discover https://example.com/news/global \
--extra https://example.com/news/china \
--extra https://example.com/news/tech \
--name 中文名 --add</code></pre>
<p>生成的 yaml 自动包含 <code>extra_homepages</code> 字段,抓取时依次访问所有入口,自动去重。</p>
<h3>5.4 为已有源追加入口</h3>
<pre><code>uv run a-share add-entry wallstreetcn https://xxx.com/news/新频道</code></pre>
<p>自动去重 — 已存在的 URL 跳过。</p>
<h3>5.5 验证</h3>
<pre><code>uv run a-share crawl --source wallstreetcn</code></pre>
<h2 id="cninfo">6. cninfo 公告抓取</h2>
<p>cninfo(巨潮资讯网)是独立的 A 股公告抓取管道,与新闻抓取分开调度(每天 08:00)。</p>
<pre><code>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 # 个股日报</code></pre>
<p>全链路 9 步: 公告→调研→IRM→提取→PDF→去重→LLM→向量→Qdrant。</p>
<h4>公告关注列表</h4>
<pre><code>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 # 只抓关注公司</code></pre>
<p>关注公司公告在日报中置顶并 ⭐ 高亮。配置: <code>configs/watchlist.yaml</code></p>
<p><strong>配置</strong>: <code>CNINFO_ENABLED</code> <code>CNINFO_DAYS_BACK</code> <code>CNINFO_MAX_PAGES</code> <code>CNINFO_API_BASE</code> <code>CNINFO_PDF_BASE</code></p>
<div class="card"><strong>⚠ .env 注意</strong>: 值后面不能跟 <code>#</code> 注释。python-dotenv 会把 <code>#</code> 后内容当成值。注释必须独占一行。</div>
<h2 id="检索">7. 知识库检索</h2>
<h3>6.1 终端直接检索</h3>
<pre><code>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</code></pre>
<h3>6.2 Python 检索 (带过滤)</h3>
<pre><code>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()</code></pre>
<p>支持 8 种过滤: source_id / stock_codes / company_names / industries / sentiment / importance_min / event_types / date_range。</p>
<h3>6.3 日报</h3>
<pre><code>uv run a-share report # 生成今日日报并上传
uv run a-share report --date 20260616 # 指定日期
uv run a-share report --no-upload # 仅生成不上传</code></pre>
<p>HTML 日报含: AI 摘要 (LLM 生成 400-500 字要点) + 数据总览 + 情绪分布 + 事件类型 + 高重要度事件列表。</p>
<h2 id="定时任务">8. 定时任务部署</h2>
<h3>7.1 安装 systemd 服务 (一次性)</h3>
<pre><code>sudo cp scripts/a-share-research.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable a-share-research</code></pre>
<h3>7.2 日常操作</h3>
<table>
<tr><td>启动</td><td><code>sudo systemctl start a-share-research</code></td></tr>
<tr><td>停止</td><td><code>sudo systemctl stop a-share-research</code></td></tr>
<tr><td>状态</td><td><code>sudo systemctl status a-share-research</code></td></tr>
<tr><td>实时日志</td><td><code>journalctl -u a-share-research -f</code></td></tr>
</table>
<p>默认执行时间: 每天 07:00 / 12:00 / 18:00 / 22:00。7:00 的日报自动覆盖过去 24 小时新闻。</p>
<h4>修改定时配置</h4>
<pre><code># 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</code></pre>
<h2 id="mcp">8. MCP 服务与 AI Agent</h2>
<h3>8.1 连接 Cherry Studio</h3>
<p>设置 → MCP 服务器 → 添加:</p>
<pre><code>{"mcpServers":{"a-share-research":{"command":"uv","args":["run","python","-m","scripts.run_mcp_server"],"cwd":"/home/pi/news"}}}</code></pre>
<h3>8.2 5 个 MCP 工具</h3>
<table>
<tr><th>工具</th><th>功能</th><th>示例</th></tr>
<tr><td><code>search_news</code></td><td>通用语义检索</td><td><code>search_news("宁德时代固态电池")</code></td></tr>
<tr><td><code>search_company_news</code></td><td>按公司检索</td><td><code>search_company_news("价格", company="茅台")</code></td></tr>
<tr><td><code>search_industry_news</code></td><td>按行业检索</td><td><code>search_industry_news("政策", industry="半导体")</code></td></tr>
<tr><td><code>search_stock_events</code></td><td>按股票代码检索</td><td><code>search_stock_events("事件", stock_code="300750")</code></td></tr>
<tr><td><code>search_sentiment_trend</code></td><td>情绪检索+统计</td><td><code>search_sentiment_trend("AI算力", sentiment="all")</code></td></tr>
</table>
<h2 id="投研">9. 投研分析模板</h2>
<p>在 Cherry Studio 中粘贴 <code>docs/agent_prompt.md</code> 作为 System Prompt:</p>
<div class="card"><strong>公司分析</strong>: 分析宁德时代 (300750) 近 30 天的利好利空变化</div>
<div class="card"><strong>行业分析</strong>: 分析动力电池行业最近的政策动向和竞争格局</div>
<div class="card"><strong>风险排查</strong>: 梳理中旗新材 (001212) 近期面临的主要风险</div>
<div class="card"><strong>事件追踪</strong>: 过去一周 A 股有哪些重大监管处罚事件?</div>
<div class="card"><strong>情绪趋势</strong>: 近一个月 AI 算力概念的情绪变化趋势如何?</div>
<h2 id="附录">10. 附录</h2>
<h3>A. Qdrant 部署模式</h3>
<table>
<tr><th></th><th>本地文件模式 (默认)</th><th>Docker Server 模式</th></tr>
<tr><td>类比</td><td>SQLite</td><td>PostgreSQL</td></tr>
<tr><td>进程</td><td>无独立进程</td><td>容器 a_share_qdrant</td></tr>
<tr><td>ARM64</td><td><span class="badge-ok"></span></td><td><span class="badge-err"></span> jemalloc 16K 页崩溃</td></tr>
<tr><td>适用</td><td>树莓派 / 单机</td><td>x86 生产 / 远程</td></tr>
</table>
<h3>B. 环境变量</h3>
<table>
<tr><td><code>LLM_PROVIDER</code></td><td><code>deepseek</code></td><td>M4 LLM</td></tr>
<tr><td><code>DEEPSEEK_API_KEY</code></td><td></td><td>DeepSeek Key</td></tr>
<tr><td><code>DASHSCOPE_API_KEY</code></td><td></td><td>百炼 Key (M5/M8)</td></tr>
<tr><td><code>EMBEDDING_PROVIDER</code></td><td><code>dashscope</code></td><td>M5 嵌入</td></tr>
<tr><td><code>SCHEDULE_TIMES</code></td><td><code>07:00,12:00,18:00,22:00</code></td><td>M7 定时</td></tr>
</table>
<h3>C. 日志文件</h3>
<p><code>logs/crawler.log</code> · <code>logs/extractor.log</code> · <code>logs/dedup.log</code> · <code>logs/llm.log</code> · <code>logs/embedding.log</code> · <code>logs/qdrant.log</code> · <code>logs/scheduler.log</code> · <code>logs/mcp_server.log</code> (自动轮转 10MB×5)</p>
<h3>D. 常见问题</h3>
<div class="card"><h4>Q: 某源抓取 0 篇?</h4><p>A: 编辑 <code>configs/sources.yaml</code>,检查 <code>article_url_pattern</code> 正则。或重新 <code>discover</code> 分析。</p></div>
<div class="card"><h4>Q: LLM 报错?</h4><p>A: 检查 <code>.env</code><code>DEEPSEEK_API_KEY</code>。可切换 <code>--provider qwen</code> 测试。</p></div>
<div class="card"><h4>Q: Qdrant 搜索无结果?</h4><p>A: <code>uv run a-share status</code> 检查数据量。若 Qdrant 为 0,执行 <code>uv run a-share ingest --date &lt;date&gt; --recreate</code></p></div>
<div class="card"><h4>Q: 从零重建知识库?</h4><pre><code>rm -rf data/raw data/processed data/dedup data/deduped data/events data/embeddings data/qdrant_storage
uv run a-share pipeline --once</code></pre></div>
</main>
<footer><div class="container"><p>A 股 Deep Research 私有投研平台 · 用户手册 v1.1 · 2026-06-17</p></div></footer>
</body>
</html>