Initialize project structure with Bun, Elysia, and AI-powered article summarization

This commit is contained in:
李东云
2025-02-10 18:30:16 +08:00
commit 50ab2cf69c
16 changed files with 588 additions and 0 deletions

175
.gitignore vendored Normal file
View File

@@ -0,0 +1,175 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

13
2025-2-10.md Normal file
View File

@@ -0,0 +1,13 @@
# 总结
**核心观点:**
* **指数层面:** 虽然今天收小阳线但存在隐患主要问题是成交量大幅萎缩即便突破3322点60日线问题可能更大。下午的上攻伴随量能萎缩呈现量价背离需要警惕。
* **操作建议:** 如果明天高开个股出现进攻衰竭信号,可适当减仓,以防量能持续萎缩导致市场降温。但由于今天只是第一天量价不匹配,若未来量能能再次放大,则个股仍有操作空间。
* **仓位:** 个人维持5-5.5成仓位,消费和医药持仓为主,不考虑加仓。目前位置不适合中线布局,除非是长线布局。
* **DS题材可能指数字经济或算力概念** 目前已进入深度博傻阶段等待接盘侠。三大运营商电信、联通必须持续冲新高否则可能面临负反馈。资金不会立刻撤出DS仍会在科技股中流动直到更大的利好出现。
* **风险提示:** 不建议追高DS题材现在入场已经太晚如果要参与只能小仓位短线投机并设置止损。不要盲目相信科技信仰避免高位接盘。
**总结:**
作者认为当前市场存在量价背离的隐患建议谨慎操作注意控制仓位。对于近期火热的DS题材作者持谨慎态度认为已进入博傻阶段不建议追高。

15
README.md Normal file
View File

@@ -0,0 +1,15 @@
# info_flow
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.js
```
This project was created using `bun init` in bun v1.1.37. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

15
bak/ai.js Normal file
View File

@@ -0,0 +1,15 @@
// ai.js
export async function analyzeContent(content) {
// 假设DeepSeek API的调用方式
const response = await fetch("https://api.deepseek.com/v1/analyze", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer sk-75972dd6431e4440a0428fa8922ed6b1",
},
body: JSON.stringify({ content }),
});
const result = await response.json();
console.log('Result:', result);
return result.summary; // 假设返回的是Markdown格式的总结
}

21
bak/rss.js Normal file
View File

@@ -0,0 +1,21 @@
// ... existing code ...
export async function fetchRSS(url, maxRetries = 3) {
let retries = 0;
while (retries < maxRetries) {
try {
const response = await fetch(url, { verbose: true, keepalive: true, timeout: 10000, compress: false });
// console.log('Response:', response);
const text = await response.text();
// console.log('Response Text:', text);
return text;
} catch (error) {
console.error(`Fetch Error (Attempt ${retries + 1}):`, error);
retries++;
// 等待一段时间再重试,避免过于频繁的请求
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
console.error('Fetch failed after multiple retries.');
return null;
}
// ... existing code ...

29
bak/server.js Normal file
View File

@@ -0,0 +1,29 @@
// server.js
import { serve } from "bun";
import { fetchRSS } from "./rss.js";
import { analyzeContent } from "./ai.js";
import { getWeChatArticleLinks } from "./wechat.js";
const PORT = 3000;
async function handleRequest(request) {
// 从环境变量中获取 RSS URL如果没有则使用默认值
const rssUrl = "https://mp.weixin.qq.com/s/vQgsMuxXffpFZkNFj89wUQ";
try {
const rssContent = await fetchRSS(rssUrl);
const analyzedContent = await analyzeContent(rssContent);
return new Response(analyzedContent, {
headers: { "Content-Type": "text/markdown" },
});
} catch (error) {
console.error("Error processing request:", error);
return new Response("Error processing request", { status: 500 });
}
}
serve({
port: PORT,
fetch: handleRequest,
});
console.log(`Server running at http://localhost:${PORT}`);

24
bak/wechat.js Normal file
View File

@@ -0,0 +1,24 @@
import { JSDOM } from 'jsdom';
async function getWeChatArticleLinks(publicAccountName) {
// const searchUrl = `https://weixin.sogou.com/weixin?p=01030402&query=${encodeURIComponent(publicAccountName)}&type=2&ie=utf8`;
// const response = await fetch(searchUrl);
console.log(`https://mp.weixin.qq.com/mp/profile_ext?action=getmsg&__biz=${publicAccountName}`);
const response = await fetch(`https://mp.weixin.qq.com/mp/profile_ext?action=getmsg&__biz=${publicAccountName}`, {
headers: {
'Referer': `https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=${publicAccountName}`
}
});
const html = await response.text();
const { document } = new JSDOM(html).window;
const articleLinks = [];
const articleElements = document.querySelectorAll('.news-box .news-list li a');
articleElements.forEach((element) => {
const link = element.href;
articleLinks.push(link);
});
return articleLinks;
}
export { getWeChatArticleLinks };

BIN
bun.lockb Executable file

Binary file not shown.

0
config.json Normal file
View File

1
global.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module '@elysiajs/html';

78
history/2025/2/10.md Normal file
View File

@@ -0,0 +1,78 @@
好的,我已经收到了你提供的三篇文章。现在我将对它们进行分析,并为你设计短、中、长线的投资策略。
**一、文章核心内容总结:**
* **文章12.10日收盘总结):**
* **市场隐患:** 量能大幅缩量,下午出现量价背离,进攻衰竭信号。
* **操作建议:** 尾盘/明早高开出现进攻衰竭信号的个股减仓。如果后续量能放大或维持常态,则个股仍可操作,不轻易大减仓。
* **仓位建议:** 维持5-5.5成仓位。消费和医药持仓为主,不加仓。
* **投资策略:** 中线布局意义不大,除非是半年以上不动的大长线布局。
* **Deepseek (DS) 风险:** 深度博傻阶段,等接盘侠。三大运营商必须继续冲新高,否则负反馈可能开始。不建议追高,手痒玩玩投机仓短线带好止损。资金仍在科技股里流动,等待更大层面的重磅利好。
* **文章2 (阳气乍露,日之初升)**
* **市场定性:** 情绪周期为上升。
* **操作建议:** 关注拓维信息、华胜天成,给机会可考虑上车。
* **Deepseek (DS) 策略:** DS是市场最强主线等待DS大分歧时上车补票。前排给机会大胆上车。
* **文章3 (重要提示)**
* **应对策略:** 在节奏感不好的时候,耐心等待一个看得懂的极致最票的模式内机会。
* **龙头战法:** 行情超预期走强后,重新改变行情定性,出现龙头强分机会。
* **案例:** 每日互动 (DS概念),行情级别在确定性扩大加强,作为市场最强票,绕不过去。
* **强趋势股梳理:** 列举了云服务、DS、哪吒、AI医疗、端侧、智驾、机器人等方向的个股作为一段时间内的观察标的。
* **主要题材梳理:** DS概念、中兵系重组。
**二、投资策略设计:**
综合以上三篇文章的信息,我可以为你设计一个短中长线的投资策略,请注意,这只是根据提供的资料做出的建议,实际投资需要结合你的风险承受能力、资金情况和市场变化灵活调整。
**1. 短线策略1-3周**
* **核心:** 把握Deepseek (DS) 的短线机会,但控制风险。
* **选股:**
* **激进型:** 关注DS概念中的龙头个股如文章3提到的每日互动或者其他文中提及的DS概念股。
* **稳健型:** 等待DS概念出现大分歧时选择调整充分、基本面较好的个股逢低介入。关注文章2提到的拓维信息、华胜天成若调整到位可考虑。
* **操作:**
* 严格止损因为DS概念处于博傻阶段一旦出现亏损立即止损。文章1建议手痒玩玩投机仓短线带好止损。
* 快进快出:不恋战,赚取快钱,避免成为接盘侠。
* 关注量价关系文章1提到量价背离是风险信号要警惕。
* **仓位:** 短线仓位控制在总仓位的10-20%。
**2. 中线策略3个月-1年**
* **核心:** 围绕科技主线,选择有业绩支撑、成长性较好的个股,适当关注消费和医药。
* **选股:**
* **科技:** 从云服务、AI医疗、端侧、智驾、机器人等方向中选择细分行业的龙头企业。
* **消费/医药:** 持有原有仓位,不加仓。
* **操作:**
* 逢低布局:选择在回调时逐步建仓。
* 价值投资:关注公司的基本面,长期持有。
* **仓位:** 中线仓位控制在总仓位的30-40%。
**3. 长线策略1年以上**
* **核心:** 低位布局,长期持有,耐心等待。
* **选股:**
* **低估值潜力股:** 寻找被市场低估的、有长期发展潜力的个股。
* **行业龙头:** 选择具有长期竞争优势的行业龙头企业。
* **操作:**
* 分批建仓:在低位分批买入,降低成本。
* 长期持有:不轻易交易,享受企业成长带来的收益。
* **仓位:** 长线仓位控制在总仓位的30-40%。
**4. 整体仓位控制:**
* 根据你的风险承受能力将总仓位控制在50-70%。
* 短、中、长线仓位的比例可以根据市场情况进行调整。
* 保留一定的现金,以备不时之需。
**5. 风险提示:**
* 股市有风险,投资需谨慎。
* 密切关注市场动态,及时调整投资策略。
* 不要盲目跟风,要有自己的判断。
* 控制好仓位,避免过度投资。
**6. 需要进一步考虑的问题:**
* **文章1中提到“其他更大更高层面的重磅利好”是什么** 这个利好出现时可能会打破DS的局面需要密切关注。
* **中兵系重组的机会如何?** 文章3提到了中兵系重组可以进一步研究相关标的。
希望这个策略能给你一些参考。记住,投资需要不断学习和实践,才能找到最适合自己的方法。祝你投资顺利!

143
index.ts Normal file
View File

@@ -0,0 +1,143 @@
import { Elysia } from "elysia";
import { html } from '@elysiajs/html'
import { JSDOM } from 'jsdom';
import { GoogleGenerativeAI } from "@google/generative-ai";
const genAI = new GoogleGenerativeAI(process.env.API_KEY || "");
const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash-lite-preview-02-05" });
async function callGeminiAPI(articleContent: string): Promise<any> {
// articleContent = `You are a helpful assistant that summarizes WeChat articles.use Chinese: ${articleContent}`;
// const response = (await model.generateContent(articleContent)).response;
// return response.text();
const {
GoogleGenerativeAI,
HarmCategory,
HarmBlockThreshold,
} = require("@google/generative-ai");
const apiKey = process.env.API_KEY;
const genAI = new GoogleGenerativeAI(apiKey);
const model = genAI.getGenerativeModel({
model: "gemini-2.0-flash",
});
const generationConfig = {
temperature: 1,
topP: 0.95,
topK: 40,
maxOutputTokens: 8192,
responseMimeType: "text/plain",
};
async function run(articleContent: string) {
const chatSession = model.startChat({
generationConfig,
history: [
{
role: "user",
parts: [
{ text: "接下来我会给你提供几篇文章,用---和换行分割。如果是投资主题,我希望你能帮我总结和设计出一个短中长线的投资策略:" },
],
},
{
role: "model",
parts: [
{ text: "请提供你说的文章,我会根据内容总结并设计投资策略。你需要将几篇文章用`---`和换行分割,像你例子里那样。" },
],
},
{
role: "user",
parts: [
{ text: articleContent },
],
}
],
});
const result = await chatSession.sendMessage("INSERT_INPUT_HERE");
console.log(result.response.text());
return result.response.text();
}
return run(articleContent);
}
const app = new Elysia()
.use(html())
.get("/", () => `
<!DOCTYPE html>
<html>
<head>
<title>文章总结</title>
</head>
<body>
<h1>输入微信文章链接</h1>
<form action="/summarize" method="post">
<label for="articleUrl">文章链接:</label><br>
<textarea id="articleUrl" name="articleUrl" rows="4" cols="50"></textarea><br><br>
<input type="submit" value="提交">
</form>
</body>
</html>
`)
.post("/summarize", async ({ body }) => {
try {
const { articleUrl } = body as { articleUrl: string };
const urls = articleUrl.split('\n').filter(url => url.trim() !== '');
async function fetchArticle(url: string): Promise<string> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch article: Status Code ${response.status}`);
}
return await response.text();
}
let articleText = '';
for (const url of urls) {
try {
const articleHTML = await fetchArticle(url);
const dom = new JSDOM(articleHTML);
const jsContent = dom.window.document.querySelector("#js_content");
const content = jsContent ? jsContent.textContent : "";
articleText += content + '\n---\n';
} catch (error) {
console.error(`Failed to process article from ${url}:`, error);
// Optionally, you might want to handle the error more gracefully,
// such as skipping the article or returning a default value.
}
}
// return articleText;
const geminiResponse = await callGeminiAPI(articleText);
const today = new Date();
const fs = require('fs');
const path = require('path');
const fileName = `history/${today.getFullYear()}/${today.getMonth() + 1}/${today.getDate()}.md`;
const dir = path.dirname(fileName);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(fileName, geminiResponse);
return new Response(geminiResponse, {
headers: {
"Content-Type": "text/markdown; charset=utf-8",
},
});
} catch (error: any) {
console.error("Error:", error);
return { error: error.message };
}
})
.listen({ port: 3000 });
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);

27
jsconfig.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

20
package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "info_flow",
"module": "index.js",
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"type": "module",
"dependencies": {
"@elysiajs/html": "^1.2.0",
"@google/generative-ai": "^0.21.0",
"cheerio": "^1.0.0",
"dotenv": "^16.4.7",
"elysia": "^1.2.12",
"jsdom": "^26.0.0",
"openai": "^4.83.0"
}
}

11
style.css Normal file
View File

@@ -0,0 +1,11 @@
/* style.css */
body {
max-width: 768px;
margin: 0 auto;
font-family: system-ui;
line-height: 1.6;
}
.timestamp {
color: #666;
font-size: 0.8em
}

16
wechat.js Normal file
View File

@@ -0,0 +1,16 @@
import { JSDOM } from 'jsdom';
async function getWeChatArticleLinks(publicAccountName) {
const html = await response.text();
const { document } = new JSDOM(html).window;
const articleLinks = [];
const articleElements = document.querySelectorAll('.news-box .news-list li a');
articleElements.forEach((element) => {
const link = element.href;
articleLinks.push(link);
});
return articleLinks;
}
export { getWeChatArticleLinks };