weflow-cli 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/bin/weflow-cli-electron.cjs +59 -0
- package/bin/weflow-cli.ts +2311 -0
- package/cli.cjs +3 -0
- package/dist/bin/weflow-cli.d.ts +3 -0
- package/dist/bin/weflow-cli.d.ts.map +1 -0
- package/dist/bin/weflow-cli.js +2270 -0
- package/dist/bin/weflow-cli.js.map +1 -0
- package/dist/mcp-server/index.d.ts +3 -0
- package/dist/mcp-server/index.d.ts.map +1 -0
- package/dist/mcp-server/index.js +690 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/dist/src/core/dbPathService.d.ts +40 -0
- package/dist/src/core/dbPathService.d.ts.map +1 -0
- package/dist/src/core/dbPathService.js +324 -0
- package/dist/src/core/dbPathService.js.map +1 -0
- package/dist/src/core/keyService.d.ts +59 -0
- package/dist/src/core/keyService.d.ts.map +1 -0
- package/dist/src/core/keyService.js +368 -0
- package/dist/src/core/keyService.js.map +1 -0
- package/dist/src/core/ntCore.d.ts +62 -0
- package/dist/src/core/ntCore.d.ts.map +1 -0
- package/dist/src/core/ntCore.js +191 -0
- package/dist/src/core/ntCore.js.map +1 -0
- package/dist/src/core/sqlcipherCore.d.ts +119 -0
- package/dist/src/core/sqlcipherCore.d.ts.map +1 -0
- package/dist/src/core/sqlcipherCore.js +693 -0
- package/dist/src/core/sqlcipherCore.js.map +1 -0
- package/dist/src/core/wcdbCore.d.ts +738 -0
- package/dist/src/core/wcdbCore.d.ts.map +1 -0
- package/dist/src/core/wcdbCore.js +4545 -0
- package/dist/src/core/wcdbCore.js.map +1 -0
- package/dist/src/core/wechatClient.d.ts +23 -0
- package/dist/src/core/wechatClient.d.ts.map +1 -0
- package/dist/src/core/wechatClient.js +166 -0
- package/dist/src/core/wechatClient.js.map +1 -0
- package/dist/src/services/chatService.d.ts +25 -0
- package/dist/src/services/chatService.d.ts.map +1 -0
- package/dist/src/services/chatService.js +291 -0
- package/dist/src/services/chatService.js.map +1 -0
- package/dist/src/services/configService.d.ts +43 -0
- package/dist/src/services/configService.d.ts.map +1 -0
- package/dist/src/services/configService.js +156 -0
- package/dist/src/services/configService.js.map +1 -0
- package/dist/src/services/exportService.d.ts +30 -0
- package/dist/src/services/exportService.d.ts.map +1 -0
- package/dist/src/services/exportService.js +273 -0
- package/dist/src/services/exportService.js.map +1 -0
- package/dist/src/services/wechat-formatter.d.ts +69 -0
- package/dist/src/services/wechat-formatter.d.ts.map +1 -0
- package/dist/src/services/wechat-formatter.js +248 -0
- package/dist/src/services/wechat-formatter.js.map +1 -0
- package/dist/src/services/wechatMessageService.d.ts +28 -0
- package/dist/src/services/wechatMessageService.d.ts.map +1 -0
- package/dist/src/services/wechatMessageService.js +341 -0
- package/dist/src/services/wechatMessageService.js.map +1 -0
- package/dist/src/services/wereadService.d.ts +206 -0
- package/dist/src/services/wereadService.d.ts.map +1 -0
- package/dist/src/services/wereadService.js +145 -0
- package/dist/src/services/wereadService.js.map +1 -0
- package/dist/src/services/whitelistService.d.ts +20 -0
- package/dist/src/services/whitelistService.d.ts.map +1 -0
- package/dist/src/services/whitelistService.js +60 -0
- package/dist/src/services/whitelistService.js.map +1 -0
- package/dist/src/types.d.ts +140 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/errors.d.ts +25 -0
- package/dist/src/utils/errors.d.ts.map +1 -0
- package/dist/src/utils/errors.js +43 -0
- package/dist/src/utils/errors.js.map +1 -0
- package/dist/src/utils/pathUtils.d.ts +5 -0
- package/dist/src/utils/pathUtils.d.ts.map +1 -0
- package/dist/src/utils/pathUtils.js +16 -0
- package/dist/src/utils/pathUtils.js.map +1 -0
- package/dist/src/utils/pythonRunner.d.ts +27 -0
- package/dist/src/utils/pythonRunner.d.ts.map +1 -0
- package/dist/src/utils/pythonRunner.js +62 -0
- package/dist/src/utils/pythonRunner.js.map +1 -0
- package/dist/src/utils/talkerUtils.d.ts +14 -0
- package/dist/src/utils/talkerUtils.d.ts.map +1 -0
- package/dist/src/utils/talkerUtils.js +73 -0
- package/dist/src/utils/talkerUtils.js.map +1 -0
- package/mcp-server/index.ts +757 -0
- package/package.json +72 -0
- package/resources/key/win32/x64/wx_key.dll +0 -0
- package/resources/wcdb/win32/x64/SDL2.dll +0 -0
- package/resources/wcdb/win32/x64/WCDB.dll +0 -0
- package/resources/wcdb/win32/x64/msvcp140.dll +0 -0
- package/resources/wcdb/win32/x64/msvcp140_1.dll +0 -0
- package/resources/wcdb/win32/x64/vcruntime140.dll +0 -0
- package/resources/wcdb/win32/x64/vcruntime140_1.dll +0 -0
- package/resources/wcdb/win32/x64/wcdb_api.dll +0 -0
- package/scripts/_batch_link_sources.py +73 -0
- package/scripts/_utils.py +531 -0
- package/scripts/annual_report.py +796 -0
- package/scripts/auto_tag.py +128 -0
- package/scripts/biz_daily.py +896 -0
- package/scripts/chat_report.py +397 -0
- package/scripts/chat_stats.py +702 -0
- package/scripts/classify_daily.py +411 -0
- package/scripts/compile_wiki.py +258 -0
- package/scripts/create_reading_notes.py +370 -0
- package/scripts/enrich_backlinks.py +163 -0
- package/scripts/export_chat_html.py +766 -0
- package/scripts/extract_3x_key.py +86 -0
- package/scripts/extract_todos.py +404 -0
- package/scripts/fav_server.py +392 -0
- package/scripts/fix_topics.py +111 -0
- package/scripts/generate_ai_report.py +367 -0
- package/scripts/generate_html.py +1372 -0
- package/scripts/generate_review.py +170 -0
- package/scripts/mcp_bridge.py +398 -0
- package/scripts/nt_decrypt.py +562 -0
- package/scripts/pipeline.py +145 -0
- package/scripts/promote_all.py +310 -0
- package/scripts/promote_ideas.py +297 -0
- package/scripts/rag_chat.py +191 -0
- package/scripts/scan_decrypt_4x.py +329 -0
- package/scripts/semantic_search.py +431 -0
- package/scripts/sync_fav.py +146 -0
- package/scripts/sync_weread.py +192 -0
- package/scripts/vault_rag.py +139 -0
- package/scripts/vault_search.py +141 -0
- package/src/core/dbPathService.ts +348 -0
- package/src/core/keyService.ts +409 -0
- package/src/core/ntCore.ts +224 -0
- package/src/core/sqlcipherCore.ts +764 -0
- package/src/core/wcdbCore.ts +4568 -0
- package/src/core/wechatClient.ts +204 -0
- package/src/lz4.d.ts +10 -0
- package/src/services/chatService.ts +296 -0
- package/src/services/configService.ts +197 -0
- package/src/services/exportService.ts +305 -0
- package/src/services/wechat-formatter.ts +348 -0
- package/src/services/wechatMessageService.ts +383 -0
- package/src/services/wereadService.ts +308 -0
- package/src/services/whitelistService.ts +72 -0
- package/src/types.ts +141 -0
- package/src/utils/errors.ts +42 -0
- package/src/utils/pathUtils.ts +16 -0
- package/src/utils/pythonRunner.ts +81 -0
- package/src/utils/talkerUtils.ts +84 -0
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* WeFlow MCP Server — 让 Claude Code 等 AI Agent 直接查询知识库。
|
|
4
|
+
*
|
|
5
|
+
* 启动: npx tsx mcp-server/index.ts
|
|
6
|
+
* 在 CLAUDE.md 中注册后,AI 可直接搜索文章、概念、日报。
|
|
7
|
+
*/
|
|
8
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
9
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
11
|
+
import { readFileSync, readdirSync, existsSync, statSync, writeFileSync, mkdirSync } from 'fs';
|
|
12
|
+
import { join, resolve } from 'path';
|
|
13
|
+
import { deflateSync } from 'zlib';
|
|
14
|
+
// ---- 微信公众号文章抓取 ----
|
|
15
|
+
async function fetchWeChatArticle(url) {
|
|
16
|
+
const resp = await fetch(url, {
|
|
17
|
+
headers: {
|
|
18
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
19
|
+
'Accept': 'text/html,application/xhtml+xml',
|
|
20
|
+
'Accept-Language': 'zh-CN,zh;q=0.9',
|
|
21
|
+
},
|
|
22
|
+
redirect: 'follow',
|
|
23
|
+
});
|
|
24
|
+
if (!resp.ok)
|
|
25
|
+
throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
|
|
26
|
+
const html = await resp.text();
|
|
27
|
+
// 提取 meta 信息
|
|
28
|
+
const title = html.match(/<meta[^>]*property="og:title"[^>]*content="([^"]+)"/i)?.[1]
|
|
29
|
+
|| html.match(/<title>([^<]+)<\/title>/i)?.[1]?.trim()
|
|
30
|
+
|| '(未知标题)';
|
|
31
|
+
const author = html.match(/<meta[^>]*property="og:article:author"[^>]*content="([^"]+)"/i)?.[1]
|
|
32
|
+
|| html.match(/var\s+nickname\s*=\s*"([^"]+)"/i)?.[1]
|
|
33
|
+
|| '(未知作者)';
|
|
34
|
+
const description = html.match(/<meta[^>]*property="og:description"[^>]*content="([^"]+)"/i)?.[1]
|
|
35
|
+
|| '';
|
|
36
|
+
// 提取 js_content 文章正文
|
|
37
|
+
const contentMatch = html.match(/<div[^>]*id="js_content"[^>]*>([\s\S]*?)<\/div>\s*<script/i);
|
|
38
|
+
let content = contentMatch?.[1] || '';
|
|
39
|
+
// 替换图片 data-src → src
|
|
40
|
+
content = content.replace(/data-src="/g, 'src="');
|
|
41
|
+
// 简单的 HTML → Markdown 转换
|
|
42
|
+
content = htmlToMarkdown(content);
|
|
43
|
+
// 提取图片链接
|
|
44
|
+
const images = [];
|
|
45
|
+
const imgMatches = content.matchAll(/!\[.*?\]\((.*?)\)/g);
|
|
46
|
+
for (const m of imgMatches) {
|
|
47
|
+
if (m[1])
|
|
48
|
+
images.push(m[1]);
|
|
49
|
+
}
|
|
50
|
+
return { title, author, description, content, images };
|
|
51
|
+
}
|
|
52
|
+
/** 简单的 HTML → Markdown 转换(处理微信文章常见结构) */
|
|
53
|
+
function htmlToMarkdown(html) {
|
|
54
|
+
let md = html
|
|
55
|
+
// 移除 style/script 标签
|
|
56
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
57
|
+
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
58
|
+
// 标题
|
|
59
|
+
.replace(/<h1[^>]*>(.*?)<\/h1>/gi, '\n# $1\n')
|
|
60
|
+
.replace(/<h2[^>]*>(.*?)<\/h2>/gi, '\n## $1\n')
|
|
61
|
+
.replace(/<h3[^>]*>(.*?)<\/h3>/gi, '\n### $1\n')
|
|
62
|
+
.replace(/<h4[^>]*>(.*?)<\/h4>/gi, '\n#### $1\n')
|
|
63
|
+
// 粗体/斜体
|
|
64
|
+
.replace(/<strong[^>]*>(.*?)<\/strong>/gi, '**$1**')
|
|
65
|
+
.replace(/<b[^>]*>(.*?)<\/b>/gi, '**$1**')
|
|
66
|
+
.replace(/<em[^>]*>(.*?)<\/em>/gi, '*$1*')
|
|
67
|
+
.replace(/<i[^>]*>(.*?)<\/i>/gi, '*$1*')
|
|
68
|
+
// 段落和换行
|
|
69
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
70
|
+
.replace(/<p[^>]*>/gi, '\n\n')
|
|
71
|
+
.replace(/<\/p>/gi, '')
|
|
72
|
+
// 列表
|
|
73
|
+
.replace(/<li[^>]*>(.*?)<\/li>/gi, '- $1\n')
|
|
74
|
+
// 引用
|
|
75
|
+
.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, (_, content) => {
|
|
76
|
+
return '\n> ' + content.trim().replace(/\n/g, '\n> ') + '\n';
|
|
77
|
+
})
|
|
78
|
+
// 图片
|
|
79
|
+
.replace(/<img[^>]*src="([^"]+)"[^>]*>/gi, '\n')
|
|
80
|
+
// 链接
|
|
81
|
+
.replace(/<a[^>]*href="([^"]+)"[^>]*>(.*?)<\/a>/gi, '[$2]($1)')
|
|
82
|
+
// 移除剩余标签
|
|
83
|
+
.replace(/<[^>]+>/g, '')
|
|
84
|
+
// 解码 HTML 实体
|
|
85
|
+
.replace(/&/g, '&')
|
|
86
|
+
.replace(/</g, '<')
|
|
87
|
+
.replace(/>/g, '>')
|
|
88
|
+
.replace(/"/g, '"')
|
|
89
|
+
.replace(/'/g, "'")
|
|
90
|
+
.replace(/ /g, ' ')
|
|
91
|
+
// 清理多余空行
|
|
92
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
93
|
+
.trim();
|
|
94
|
+
return md;
|
|
95
|
+
}
|
|
96
|
+
import { formatWeChatArticle, listThemes } from '../src/services/wechat-formatter.js';
|
|
97
|
+
// ---- 微信公众号 API 辅助 ----
|
|
98
|
+
const WECHAT_APP_ID = process.env.WECHAT_APPID || '';
|
|
99
|
+
const WECHAT_APP_SECRET = process.env.WECHAT_APPSECRET || '';
|
|
100
|
+
let _tokenCache = null;
|
|
101
|
+
async function getWeChatToken() {
|
|
102
|
+
if (_tokenCache && Date.now() < _tokenCache.expiresAt - 60000) {
|
|
103
|
+
return _tokenCache.token;
|
|
104
|
+
}
|
|
105
|
+
if (!WECHAT_APP_ID || !WECHAT_APP_SECRET) {
|
|
106
|
+
throw new Error('请设置环境变量 WECHAT_APPID 和 WECHAT_APPSECRET');
|
|
107
|
+
}
|
|
108
|
+
const resp = await fetch(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${WECHAT_APP_ID}&secret=${WECHAT_APP_SECRET}`);
|
|
109
|
+
const data = await resp.json();
|
|
110
|
+
if (data.errcode) {
|
|
111
|
+
throw new Error(`微信 Token 获取失败: ${data.errmsg} (${data.errcode})`);
|
|
112
|
+
}
|
|
113
|
+
_tokenCache = {
|
|
114
|
+
token: data.access_token,
|
|
115
|
+
expiresAt: Date.now() + (data.expires_in - 60) * 1000,
|
|
116
|
+
};
|
|
117
|
+
return _tokenCache.token;
|
|
118
|
+
}
|
|
119
|
+
async function uploadWeChatImage(token, filePath) {
|
|
120
|
+
const buf = readFileSync(filePath);
|
|
121
|
+
const boundary = `----WeFlow${Date.now()}`;
|
|
122
|
+
const filename = filePath.replace(/^.*[\\/]/, '');
|
|
123
|
+
const ext = filename.split('.').pop()?.toLowerCase() || 'png';
|
|
124
|
+
const mimeType = ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : 'image/png';
|
|
125
|
+
const header = [
|
|
126
|
+
`--${boundary}`,
|
|
127
|
+
`Content-Disposition: form-data; name="media"; filename="${filename}"`,
|
|
128
|
+
`Content-Type: ${mimeType}`,
|
|
129
|
+
'', '',
|
|
130
|
+
].join('\r\n');
|
|
131
|
+
const trailer = `\r\n--${boundary}--\r\n`;
|
|
132
|
+
const headerBytes = new TextEncoder().encode(header);
|
|
133
|
+
const trailerBytes = new TextEncoder().encode(trailer);
|
|
134
|
+
const body = new Uint8Array(headerBytes.length + buf.length + trailerBytes.length);
|
|
135
|
+
body.set(headerBytes, 0);
|
|
136
|
+
body.set(buf, headerBytes.length);
|
|
137
|
+
body.set(trailerBytes, headerBytes.length + buf.length);
|
|
138
|
+
const resp = await fetch(`https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=${token}&type=image`, {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}` },
|
|
141
|
+
body,
|
|
142
|
+
});
|
|
143
|
+
const data = await resp.json();
|
|
144
|
+
if (data.errcode) {
|
|
145
|
+
throw new Error(`上传封面图失败: ${data.errmsg} (${data.errcode})`);
|
|
146
|
+
}
|
|
147
|
+
return data.media_id;
|
|
148
|
+
}
|
|
149
|
+
async function createWeChatDraft(token, articles) {
|
|
150
|
+
const resp = await fetch(`https://api.weixin.qq.com/cgi-bin/draft/add?access_token=${token}`, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: { 'Content-Type': 'application/json' },
|
|
153
|
+
body: JSON.stringify({ articles }),
|
|
154
|
+
});
|
|
155
|
+
const data = await resp.json();
|
|
156
|
+
if (data.errcode) {
|
|
157
|
+
throw new Error(`创建草稿失败: ${data.errmsg} (${data.errcode})`);
|
|
158
|
+
}
|
|
159
|
+
return data.media_id;
|
|
160
|
+
}
|
|
161
|
+
const PKG_ROOT = resolve(join(import.meta.dirname || '.', '..'));
|
|
162
|
+
const BIZ_DAILY = join(PKG_ROOT, 'output', 'biz-daily');
|
|
163
|
+
const VAULT_WIKI = join(PKG_ROOT, 'output', 'wechat-vault', 'Wiki', 'Concepts');
|
|
164
|
+
const VAULT_INDEX = join(PKG_ROOT, 'output', 'wechat-vault', 'Wiki', '00-Overview.md');
|
|
165
|
+
const REVIEWS = join(PKG_ROOT, 'output', 'reviews', 'Daily');
|
|
166
|
+
function parseFrontmatter(text) {
|
|
167
|
+
if (!text.startsWith('---'))
|
|
168
|
+
return {};
|
|
169
|
+
const end = text.indexOf('---', 3);
|
|
170
|
+
if (end === -1)
|
|
171
|
+
return {};
|
|
172
|
+
const fm = {};
|
|
173
|
+
for (const line of text.slice(3, end).split('\n')) {
|
|
174
|
+
const idx = line.indexOf(':');
|
|
175
|
+
if (idx === -1)
|
|
176
|
+
continue;
|
|
177
|
+
const key = line.slice(0, idx).trim();
|
|
178
|
+
let val = line.slice(idx + 1).trim();
|
|
179
|
+
if (val.startsWith('[') && val.endsWith(']')) {
|
|
180
|
+
fm[key] = val.slice(1, -1).split(',').map(v => v.trim().replace(/['"]/g, ''));
|
|
181
|
+
}
|
|
182
|
+
else if (val.startsWith('"') && val.endsWith('"')) {
|
|
183
|
+
fm[key] = val.slice(1, -1);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
fm[key] = val;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return fm;
|
|
190
|
+
}
|
|
191
|
+
function scanArticles() {
|
|
192
|
+
const results = [];
|
|
193
|
+
if (!existsSync(BIZ_DAILY))
|
|
194
|
+
return results;
|
|
195
|
+
for (const dateDir of readdirSync(BIZ_DAILY).sort().reverse()) {
|
|
196
|
+
const dp = join(BIZ_DAILY, dateDir);
|
|
197
|
+
if (!statSync(dp).isDirectory())
|
|
198
|
+
continue;
|
|
199
|
+
for (const topic of readdirSync(dp)) {
|
|
200
|
+
const tp = join(dp, topic);
|
|
201
|
+
if (!statSync(tp).isDirectory())
|
|
202
|
+
continue;
|
|
203
|
+
for (const file of readdirSync(tp)) {
|
|
204
|
+
if (!file.endsWith('.md') || file === 'README.md')
|
|
205
|
+
continue;
|
|
206
|
+
const content = readFileSync(join(tp, file), 'utf-8');
|
|
207
|
+
const fm = parseFrontmatter(content);
|
|
208
|
+
results.push({
|
|
209
|
+
...fm,
|
|
210
|
+
file: join(dateDir, topic, file),
|
|
211
|
+
dateDir,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return results;
|
|
217
|
+
}
|
|
218
|
+
function searchArticles(args) {
|
|
219
|
+
let articles = scanArticles();
|
|
220
|
+
if (args.keyword) {
|
|
221
|
+
const kw = String(args.keyword).toLowerCase();
|
|
222
|
+
articles = articles.filter(a => (a.title || '').toLowerCase().includes(kw) ||
|
|
223
|
+
(a.source || '').toLowerCase().includes(kw) ||
|
|
224
|
+
(a.tags || []).some((t) => t.toLowerCase().includes(kw)));
|
|
225
|
+
}
|
|
226
|
+
if (args.topic) {
|
|
227
|
+
articles = articles.filter(a => a.topic === args.topic);
|
|
228
|
+
}
|
|
229
|
+
if (args.date) {
|
|
230
|
+
articles = articles.filter(a => a.dateDir === args.date);
|
|
231
|
+
}
|
|
232
|
+
const limit = Number(args.limit) || 20;
|
|
233
|
+
const top = articles.slice(0, limit);
|
|
234
|
+
if (!top.length)
|
|
235
|
+
return '未找到匹配文章';
|
|
236
|
+
return top.map(a => `- [${a.date}] **${a.title || '(无标题)'}** — ${a.source || ''} [${a.topic || ''}] [${(a.tags || []).join(', ')}]\n ${(a.description || '').slice(0, 120)}`).join('\n\n');
|
|
237
|
+
}
|
|
238
|
+
/** 自动生成封面图(纯色主题渐变 PNG,无外部依赖) */
|
|
239
|
+
function generateCoverImage(title, theme) {
|
|
240
|
+
const colors = {
|
|
241
|
+
warm: [230, 126, 34],
|
|
242
|
+
default: [52, 152, 219],
|
|
243
|
+
minimal: [51, 51, 51],
|
|
244
|
+
green: [39, 174, 96],
|
|
245
|
+
};
|
|
246
|
+
const [r, g, b] = colors[theme] || colors.default;
|
|
247
|
+
const w = 900, h = 500;
|
|
248
|
+
function crc32(buf) {
|
|
249
|
+
let c = 0xffffffff;
|
|
250
|
+
for (let i = 0; i < buf.length; i++) {
|
|
251
|
+
c ^= buf[i];
|
|
252
|
+
for (let j = 0; j < 8; j++)
|
|
253
|
+
c = (c >>> 1) ^ (c & 1 ? 0xedb88320 : 0);
|
|
254
|
+
}
|
|
255
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
256
|
+
}
|
|
257
|
+
function chunk(type, data) {
|
|
258
|
+
const head = Buffer.alloc(8);
|
|
259
|
+
head.writeUInt32BE(data.length, 0);
|
|
260
|
+
head.write(type, 4);
|
|
261
|
+
const crcBuf = Buffer.alloc(4);
|
|
262
|
+
crcBuf.writeUInt32BE(crc32(Buffer.concat([head.slice(4, 8), data])), 0);
|
|
263
|
+
return Buffer.concat([head, data, crcBuf]);
|
|
264
|
+
}
|
|
265
|
+
const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
266
|
+
const ihdr = Buffer.alloc(13);
|
|
267
|
+
ihdr.writeUInt32BE(w, 0);
|
|
268
|
+
ihdr.writeUInt32BE(h, 4);
|
|
269
|
+
ihdr[8] = 8;
|
|
270
|
+
ihdr[9] = 2;
|
|
271
|
+
const raw = Buffer.alloc(h * (1 + w * 3));
|
|
272
|
+
for (let y = 0; y < h; y++) {
|
|
273
|
+
const offset = y * (1 + w * 3);
|
|
274
|
+
raw[offset] = 0;
|
|
275
|
+
for (let x = 0; x < w; x++) {
|
|
276
|
+
const px = offset + 1 + x * 3;
|
|
277
|
+
const darken = 0.9 + 0.1 * (y / h);
|
|
278
|
+
raw[px] = Math.floor(r * darken);
|
|
279
|
+
raw[px + 1] = Math.floor(g * darken);
|
|
280
|
+
raw[px + 2] = Math.floor(b * darken);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const png = Buffer.concat([
|
|
284
|
+
signature,
|
|
285
|
+
chunk('IHDR', ihdr),
|
|
286
|
+
chunk('IDAT', deflateSync(raw)),
|
|
287
|
+
chunk('IEND', Buffer.alloc(0)),
|
|
288
|
+
]);
|
|
289
|
+
const tmpDir = join(PKG_ROOT, 'output', '.tmp');
|
|
290
|
+
if (!existsSync(tmpDir))
|
|
291
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
292
|
+
const path = join(tmpDir, `cover-${Date.now()}.png`);
|
|
293
|
+
writeFileSync(path, png);
|
|
294
|
+
return path;
|
|
295
|
+
}
|
|
296
|
+
async function main() {
|
|
297
|
+
const server = new Server({ name: 'weflow-mcp', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
298
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
299
|
+
tools: [
|
|
300
|
+
{
|
|
301
|
+
name: 'wechat.search_articles',
|
|
302
|
+
description: '搜索知识库文章(可按关键词/主题/日期过滤)',
|
|
303
|
+
inputSchema: {
|
|
304
|
+
type: 'object',
|
|
305
|
+
properties: {
|
|
306
|
+
keyword: { type: 'string', description: '搜索关键词(标题/来源/标签)' },
|
|
307
|
+
topic: { type: 'string', description: '主题: AI | 学术 | 新闻 | 文学 | 投资' },
|
|
308
|
+
date: { type: 'string', description: '日期 YYYY-MM-DD' },
|
|
309
|
+
limit: { type: 'number', description: '返回数量,默认20' },
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
name: 'wechat.get_daily',
|
|
315
|
+
description: '获取指定日期的公众号日报',
|
|
316
|
+
inputSchema: {
|
|
317
|
+
type: 'object',
|
|
318
|
+
properties: {
|
|
319
|
+
date: { type: 'string', description: '日期 YYYY-MM-DD,默认最新' },
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: 'wechat.get_concepts',
|
|
325
|
+
description: '查看概念图谱索引',
|
|
326
|
+
inputSchema: { type: 'object', properties: {} },
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: 'wechat.get_concept',
|
|
330
|
+
description: '获取某个概念的详细 Wiki 页',
|
|
331
|
+
inputSchema: {
|
|
332
|
+
type: 'object',
|
|
333
|
+
properties: {
|
|
334
|
+
name: { type: 'string', description: '概念名,如 RAG、Agent' },
|
|
335
|
+
},
|
|
336
|
+
required: ['name'],
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
name: 'wechat.get_review',
|
|
341
|
+
description: '获取某日的 AI 学习日报',
|
|
342
|
+
inputSchema: {
|
|
343
|
+
type: 'object',
|
|
344
|
+
properties: {
|
|
345
|
+
date: { type: 'string', description: '日期 YYYY-MM-DD,默认最新' },
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
name: 'wechat.get_stats',
|
|
351
|
+
description: '知识库统计概览',
|
|
352
|
+
inputSchema: { type: 'object', properties: {} },
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
name: 'wechat.format_article',
|
|
356
|
+
description: '将 Markdown 文章转换为微信公众号排版 HTML(支持 4 种主题:default/warm/minimal/green)',
|
|
357
|
+
inputSchema: {
|
|
358
|
+
type: 'object',
|
|
359
|
+
properties: {
|
|
360
|
+
content: { type: 'string', description: 'Markdown 格式的文章内容' },
|
|
361
|
+
theme: { type: 'string', description: '排版主题: default | warm | minimal | green,默认 default' },
|
|
362
|
+
},
|
|
363
|
+
required: ['content'],
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: 'wechat.publish_article',
|
|
368
|
+
description: '排版并发布 Markdown 文章到微信公众号草稿箱。需要设置环境变量 WECHAT_APPID 和 WECHAT_APPSECRET',
|
|
369
|
+
inputSchema: {
|
|
370
|
+
type: 'object',
|
|
371
|
+
properties: {
|
|
372
|
+
title: { type: 'string', description: '文章标题(≤64字符)' },
|
|
373
|
+
content: { type: 'string', description: 'Markdown 格式的文章内容' },
|
|
374
|
+
author: { type: 'string', description: '作者名(≤8字符),默认 "AI Assistant"' },
|
|
375
|
+
theme: { type: 'string', description: '排版主题: default | warm | minimal | green,默认 default' },
|
|
376
|
+
cover_image: { type: 'string', description: '封面图本地路径(可选)' },
|
|
377
|
+
preview_only: { type: 'boolean', description: '仅排版预览不发布草稿,默认 false' },
|
|
378
|
+
},
|
|
379
|
+
required: ['title', 'content'],
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
name: 'wechat.list_themes',
|
|
384
|
+
description: '列出微信公众号排版可用的所有主题',
|
|
385
|
+
inputSchema: { type: 'object', properties: {} },
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
name: 'wechat.search_public',
|
|
389
|
+
description: '搜索全网微信公众号文章(通过搜索引擎 site:mp.weixin.qq.com)',
|
|
390
|
+
inputSchema: {
|
|
391
|
+
type: 'object',
|
|
392
|
+
properties: {
|
|
393
|
+
keyword: { type: 'string', description: '搜索关键词' },
|
|
394
|
+
limit: { type: 'number', description: '返回数量,默认10' },
|
|
395
|
+
},
|
|
396
|
+
required: ['keyword'],
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
name: 'wechat.fetch_article',
|
|
401
|
+
description: '抓取单篇微信公众号文章,转换为 Markdown',
|
|
402
|
+
inputSchema: {
|
|
403
|
+
type: 'object',
|
|
404
|
+
properties: {
|
|
405
|
+
url: { type: 'string', description: '文章链接(mp.weixin.qq.com)' },
|
|
406
|
+
},
|
|
407
|
+
required: ['url'],
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
}));
|
|
412
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
413
|
+
const { name, arguments: args = {} } = request.params;
|
|
414
|
+
try {
|
|
415
|
+
switch (name) {
|
|
416
|
+
case 'wechat.search_articles':
|
|
417
|
+
return { content: [{ type: 'text', text: searchArticles(args) }] };
|
|
418
|
+
case 'wechat.get_daily': {
|
|
419
|
+
const dates = existsSync(BIZ_DAILY) ? readdirSync(BIZ_DAILY).sort().reverse() : [];
|
|
420
|
+
const date = String(args.date || dates[0] || '');
|
|
421
|
+
const readme = join(BIZ_DAILY, date, 'README.md');
|
|
422
|
+
if (!existsSync(readme))
|
|
423
|
+
return { content: [{ type: 'text', text: `未找到 ${date} 的日报` }] };
|
|
424
|
+
return { content: [{ type: 'text', text: readFileSync(readme, 'utf-8') }] };
|
|
425
|
+
}
|
|
426
|
+
case 'wechat.get_concepts': {
|
|
427
|
+
if (!existsSync(VAULT_INDEX))
|
|
428
|
+
return { content: [{ type: 'text', text: '概念索引尚未生成,请先运行 wiki compile' }] };
|
|
429
|
+
return { content: [{ type: 'text', text: readFileSync(VAULT_INDEX, 'utf-8') }] };
|
|
430
|
+
}
|
|
431
|
+
case 'wechat.get_concept': {
|
|
432
|
+
const name = String(args.name);
|
|
433
|
+
for (const ext of ['', '.md']) {
|
|
434
|
+
const path = join(VAULT_WIKI, name + ext);
|
|
435
|
+
if (existsSync(path)) {
|
|
436
|
+
return { content: [{ type: 'text', text: readFileSync(path, 'utf-8') }] };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// Fuzzy search
|
|
440
|
+
if (existsSync(VAULT_WIKI)) {
|
|
441
|
+
for (const f of readdirSync(VAULT_WIKI)) {
|
|
442
|
+
if (f.includes(name)) {
|
|
443
|
+
return { content: [{ type: 'text', text: readFileSync(join(VAULT_WIKI, f), 'utf-8') }] };
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return { content: [{ type: 'text', text: `未找到概念: ${name}` }] };
|
|
448
|
+
}
|
|
449
|
+
case 'wechat.get_review': {
|
|
450
|
+
const files = existsSync(REVIEWS) ? readdirSync(REVIEWS).sort().reverse() : [];
|
|
451
|
+
const date = String(args.date || '');
|
|
452
|
+
let path;
|
|
453
|
+
if (date) {
|
|
454
|
+
path = join(REVIEWS, `Daily-${date}.md`);
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
path = files.length ? join(REVIEWS, files[0]) : '';
|
|
458
|
+
}
|
|
459
|
+
if (!path || !existsSync(path))
|
|
460
|
+
return { content: [{ type: 'text', text: `未找到 ${date || '最新'} 的学习日报` }] };
|
|
461
|
+
return { content: [{ type: 'text', text: readFileSync(path, 'utf-8') }] };
|
|
462
|
+
}
|
|
463
|
+
case 'wechat.format_article': {
|
|
464
|
+
const content = String(args.content || '');
|
|
465
|
+
if (!content)
|
|
466
|
+
return { content: [{ type: 'text', text: '错误: content 不能为空' }] };
|
|
467
|
+
const theme = String(args.theme || 'default');
|
|
468
|
+
const html = formatWeChatArticle(content, { theme: theme });
|
|
469
|
+
const preview = html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').slice(0, 200) + '...';
|
|
470
|
+
return {
|
|
471
|
+
content: [{
|
|
472
|
+
type: 'text',
|
|
473
|
+
text: [
|
|
474
|
+
`主题: ${theme}`,
|
|
475
|
+
`总长度: ${html.length} 字符`,
|
|
476
|
+
`正文预览: ${preview}`,
|
|
477
|
+
`---`,
|
|
478
|
+
html,
|
|
479
|
+
].join('\n')
|
|
480
|
+
}]
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
case 'wechat.list_themes': {
|
|
484
|
+
const themes = listThemes();
|
|
485
|
+
return {
|
|
486
|
+
content: [{
|
|
487
|
+
type: 'text',
|
|
488
|
+
text: themes.map(t => `- **${t.id}** — ${t.name}: ${t.description}`).join('\n')
|
|
489
|
+
}]
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
case 'wechat.publish_article': {
|
|
493
|
+
const title = String(args.title || '').slice(0, 64);
|
|
494
|
+
const content = String(args.content || '');
|
|
495
|
+
const author = String(args.author || 'AI Assistant').slice(0, 8);
|
|
496
|
+
const theme = String(args.theme || 'default');
|
|
497
|
+
const coverImage = args.cover_image ? String(args.cover_image) : '';
|
|
498
|
+
const previewOnly = args.preview_only === true || args.preview_only === 'true';
|
|
499
|
+
if (!title || !content) {
|
|
500
|
+
return { content: [{ type: 'text', text: '错误: title 和 content 不能为空' }] };
|
|
501
|
+
}
|
|
502
|
+
// 1. 排版
|
|
503
|
+
const html = formatWeChatArticle(content, { theme: theme });
|
|
504
|
+
const wordCount = html.replace(/<[^>]*>/g, '').replace(/\s+/g, '').length;
|
|
505
|
+
if (previewOnly) {
|
|
506
|
+
return {
|
|
507
|
+
content: [{
|
|
508
|
+
type: 'text',
|
|
509
|
+
text: [
|
|
510
|
+
`[仅预览] 标题: ${title}`,
|
|
511
|
+
`作者: ${author} | 主题: ${theme} | 字数: ${wordCount}`,
|
|
512
|
+
`---`,
|
|
513
|
+
html,
|
|
514
|
+
].join('\n')
|
|
515
|
+
}]
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
// 2. 发布到草稿箱
|
|
519
|
+
try {
|
|
520
|
+
const token = await getWeChatToken();
|
|
521
|
+
// 上传封面图(如果提供,否则自动生成)
|
|
522
|
+
let thumbMediaId = '';
|
|
523
|
+
if (coverImage && existsSync(coverImage)) {
|
|
524
|
+
thumbMediaId = await uploadWeChatImage(token, coverImage);
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
// 自动生成封面图
|
|
528
|
+
const autoCover = generateCoverImage(title, theme);
|
|
529
|
+
thumbMediaId = await uploadWeChatImage(token, autoCover);
|
|
530
|
+
}
|
|
531
|
+
// 创建草稿
|
|
532
|
+
const mediaId = await createWeChatDraft(token, [{
|
|
533
|
+
title,
|
|
534
|
+
author,
|
|
535
|
+
digest: html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').slice(0, 120),
|
|
536
|
+
content: html,
|
|
537
|
+
thumb_media_id: thumbMediaId || undefined,
|
|
538
|
+
}]);
|
|
539
|
+
return {
|
|
540
|
+
content: [{
|
|
541
|
+
type: 'text',
|
|
542
|
+
text: [
|
|
543
|
+
`✅ 已发布到公众号草稿箱`,
|
|
544
|
+
`media_id: ${mediaId}`,
|
|
545
|
+
`标题: ${title}`,
|
|
546
|
+
`作者: ${author} | 主题: ${theme} | 字数: ${wordCount}`,
|
|
547
|
+
`封面图: ${thumbMediaId ? '已上传' : '未提供(使用默认)'}`,
|
|
548
|
+
'',
|
|
549
|
+
`请登录 mp.weixin.qq.com 草稿箱查看和正式发布。`,
|
|
550
|
+
].join('\n')
|
|
551
|
+
}]
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
catch (e) {
|
|
555
|
+
return { content: [{ type: 'text', text: `发布失败: ${e.message}\n\n——以下为排版预览——\n\n${html}` }] };
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
case 'wechat.get_stats': {
|
|
559
|
+
const articles = scanArticles();
|
|
560
|
+
const dates = [...new Set(articles.map(a => a.dateDir))].sort();
|
|
561
|
+
const topics = {};
|
|
562
|
+
articles.forEach(a => { topics[a.topic || '未知'] = (topics[a.topic || '未知'] || 0) + 1; });
|
|
563
|
+
let stats = `## 知识库统计\n\n- 文章总数: ${articles.length}\n- 覆盖日期: ${dates.length} 天 (${dates[0]} ~ ${dates[dates.length - 1]})\n- 主题分布:\n`;
|
|
564
|
+
for (const [t, c] of Object.entries(topics).sort((a, b) => b[1] - a[1])) {
|
|
565
|
+
stats += ` - ${t}: ${c} 篇\n`;
|
|
566
|
+
}
|
|
567
|
+
if (existsSync(VAULT_INDEX)) {
|
|
568
|
+
const idx = readFileSync(VAULT_INDEX, 'utf-8');
|
|
569
|
+
const m = idx.match(/共 (\d+) 个概念/);
|
|
570
|
+
if (m)
|
|
571
|
+
stats += `- 概念页: ${m[1]} 个\n`;
|
|
572
|
+
}
|
|
573
|
+
return { content: [{ type: 'text', text: stats }] };
|
|
574
|
+
}
|
|
575
|
+
case 'wechat.search_public': {
|
|
576
|
+
const keyword = String(args.keyword || '').trim();
|
|
577
|
+
if (!keyword)
|
|
578
|
+
return { content: [{ type: 'text', text: '错误: keyword 不能为空' }] };
|
|
579
|
+
const limit = Number(args.limit) || 10;
|
|
580
|
+
try {
|
|
581
|
+
// 使用搜狗微信搜索
|
|
582
|
+
const resp = await fetch(`https://weixin.sogou.com/weixin?type=2&query=${encodeURIComponent(keyword)}`, {
|
|
583
|
+
headers: {
|
|
584
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
|
585
|
+
'Referer': 'https://weixin.sogou.com/',
|
|
586
|
+
'Accept': 'text/html',
|
|
587
|
+
'Accept-Language': 'zh-CN,zh;q=0.9',
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
const html = await resp.text();
|
|
591
|
+
// 从搜狗结果中提取标题、公众号名、摘要
|
|
592
|
+
// 搜狗将结果放在 li 标签中,每篇文章包含标题链接 + 摘要
|
|
593
|
+
const items = [];
|
|
594
|
+
// 提取文章标题(h3 标签内的文本)
|
|
595
|
+
const titlePattern = /<h3[^>]*>\s*<a[^>]*>(.*?)<\/a>\s*<\/h3>/g;
|
|
596
|
+
const titles = [];
|
|
597
|
+
let tm;
|
|
598
|
+
while ((tm = titlePattern.exec(html)) !== null) {
|
|
599
|
+
const t = tm[1].replace(/<em[^>]*>/g, '').replace(/<\/em>/g, '').replace(/<!--[^>]*-->/g, '').trim();
|
|
600
|
+
if (t)
|
|
601
|
+
titles.push(t);
|
|
602
|
+
}
|
|
603
|
+
// 提取公众号名(在 span.all-time-y2 中)
|
|
604
|
+
const acctPattern = /<span[^>]*class="[^"]*all-time-y2[^"]*"[^>]*>(.*?)<\/span>/g;
|
|
605
|
+
const accounts = [];
|
|
606
|
+
let am;
|
|
607
|
+
while ((am = acctPattern.exec(html)) !== null) {
|
|
608
|
+
const a = am[1].replace(/<[^>]+>/g, '').trim();
|
|
609
|
+
if (a)
|
|
610
|
+
accounts.push(a);
|
|
611
|
+
}
|
|
612
|
+
// 提取摘要(p 标签)
|
|
613
|
+
const descPattern = /<p[^>]*class="[^"]*txt-info[^"]*"[^>]*>(.*?)<\/p>/g;
|
|
614
|
+
const descs = [];
|
|
615
|
+
let dm;
|
|
616
|
+
while ((dm = descPattern.exec(html)) !== null) {
|
|
617
|
+
const d = dm[1].replace(/<[^>]+>/g, '').replace(/…/g, '…').replace(/→/g, '→').replace(/—/g, '—').trim();
|
|
618
|
+
if (d && d.length > 10)
|
|
619
|
+
descs.push(d);
|
|
620
|
+
}
|
|
621
|
+
// 组合结果
|
|
622
|
+
const count = Math.max(titles.length, accounts.length, descs.length);
|
|
623
|
+
for (let i = 0; i < Math.min(count, limit); i++) {
|
|
624
|
+
items.push({
|
|
625
|
+
title: titles[i] || '(未知标题)',
|
|
626
|
+
account: accounts[i] || '(未知公众号)',
|
|
627
|
+
desc: descs[i] || '',
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
if (!items.length) {
|
|
631
|
+
return { content: [{ type: 'text', text: `未找到与"${keyword}"相关的微信公众号文章。\n\n提示:\n1. 尝试更具体的关键词\n2. 加上作者/公众号名\n3. 直接提供文章链接使用 wechat.fetch_article 抓取` }] };
|
|
632
|
+
}
|
|
633
|
+
const results = items.map((item, i) => `${i + 1}. **${item.title}**\n 公众号: ${item.account}\n ${item.desc}`).join('\n\n');
|
|
634
|
+
return {
|
|
635
|
+
content: [{
|
|
636
|
+
type: 'text',
|
|
637
|
+
text: [
|
|
638
|
+
`搜索关键词: "${keyword}" | 找到 ${items.length} 篇:`,
|
|
639
|
+
'',
|
|
640
|
+
results,
|
|
641
|
+
'',
|
|
642
|
+
'---',
|
|
643
|
+
'提示: 在微信或搜狗微信搜索中打开看到文章后,复制链接使用 wechat.fetch_article 抓取全文。'
|
|
644
|
+
].join('\n')
|
|
645
|
+
}]
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
catch (e) {
|
|
649
|
+
return { content: [{ type: 'text', text: `搜索失败: ${e.message}` }] };
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
case 'wechat.fetch_article': {
|
|
653
|
+
const url = String(args.url || '').trim();
|
|
654
|
+
if (!url)
|
|
655
|
+
return { content: [{ type: 'text', text: '错误: url 不能为空' }] };
|
|
656
|
+
if (!url.includes('mp.weixin.qq.com')) {
|
|
657
|
+
return { content: [{ type: 'text', text: '错误: 仅支持微信公众号文章链接 (mp.weixin.qq.com)' }] };
|
|
658
|
+
}
|
|
659
|
+
try {
|
|
660
|
+
const article = await fetchWeChatArticle(url);
|
|
661
|
+
return {
|
|
662
|
+
content: [{
|
|
663
|
+
type: 'text',
|
|
664
|
+
text: [
|
|
665
|
+
`# ${article.title}`,
|
|
666
|
+
`> 作者: ${article.author}`,
|
|
667
|
+
article.description ? `> ${article.description}` : '',
|
|
668
|
+
'',
|
|
669
|
+
article.content,
|
|
670
|
+
].join('\n')
|
|
671
|
+
}]
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
catch (e) {
|
|
675
|
+
return { content: [{ type: 'text', text: `抓取失败: ${e.message}\n\n提示: 部分文章需要微信客户端环境才能访问,可尝试在微信中打开后复制链接。` }] };
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
default:
|
|
679
|
+
return { content: [{ type: 'text', text: `未知工具: ${name}` }] };
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
catch (e) {
|
|
683
|
+
return { content: [{ type: 'text', text: `错误: ${e.message}` }] };
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
const transport = new StdioServerTransport();
|
|
687
|
+
await server.connect(transport);
|
|
688
|
+
}
|
|
689
|
+
main().catch(console.error);
|
|
690
|
+
//# sourceMappingURL=index.js.map
|