dr-claw 1.1.2
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 +1380 -0
- package/NOTICE +17 -0
- package/README.md +875 -0
- package/README.zh-CN.md +857 -0
- package/dist/api-docs.html +878 -0
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/_basePickBy-C0n1uo4u.js +1 -0
- package/dist/assets/_baseUniq-BRNmhFkc.js +1 -0
- package/dist/assets/arc-DOMBmC_A.js +1 -0
- package/dist/assets/architectureDiagram-VXUJARFQ-CtMWglbD.js +36 -0
- package/dist/assets/blockDiagram-VD42YOAC-DK_gTkJy.js +122 -0
- package/dist/assets/c4Diagram-YG6GDRKO-DrDAvsn7.js +10 -0
- package/dist/assets/channel-BFOE7OyC.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-CGbhdZcN.js +1 -0
- package/dist/assets/chunk-55IACEB6-sGeFSxen.js +1 -0
- package/dist/assets/chunk-B4BG7PRW-RxgHAU_P.js +165 -0
- package/dist/assets/chunk-DI55MBZ5-CvHUXZrx.js +220 -0
- package/dist/assets/chunk-FMBD7UC4-Sn7cXVNU.js +15 -0
- package/dist/assets/chunk-QN33PNHL-bDKOHppN.js +1 -0
- package/dist/assets/chunk-QZHKN3VN-B9kN5LGm.js +1 -0
- package/dist/assets/chunk-TZMSLE5B-CCBe6len.js +1 -0
- package/dist/assets/classDiagram-2ON5EDUG-DQoMNYlY.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-DQoMNYlY.js +1 -0
- package/dist/assets/clone-BulQSblH.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-r6sLiHBW.js +1 -0
- package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
- package/dist/assets/dagre-6UL2VRFP-AbsaR1Oa.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-PSM6KHXK-DjCvdUyy.js +24 -0
- package/dist/assets/diagram-QEK2KX5R-CqiocXkH.js +43 -0
- package/dist/assets/diagram-S2PKOQOG-D5ALeoII.js +24 -0
- package/dist/assets/erDiagram-Q2GNP2WA-C1AeOsiz.js +60 -0
- package/dist/assets/flowDiagram-NV44I4VS-C98ve_DO.js +162 -0
- package/dist/assets/ganttDiagram-JELNMOA3-3zdLvcxD.js +267 -0
- package/dist/assets/gitGraphDiagram-V2S2FVAM-nDuDMsHh.js +65 -0
- package/dist/assets/graph-Blm-emkR.js +1 -0
- package/dist/assets/index-B8Yutsnm.css +1 -0
- package/dist/assets/index-Cf6gyhc3.js +654 -0
- package/dist/assets/infoDiagram-HS3SLOUP-CmzE3q0R.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/journeyDiagram-XKPGCS4Q-CYaW7LrP.js +139 -0
- package/dist/assets/kanban-definition-3W4ZIXB7-mkmW7p0j.js +89 -0
- package/dist/assets/layout-Ct2kFIId.js +1 -0
- package/dist/assets/linear-CKG9EwIJ.js +1 -0
- package/dist/assets/mermaid.core-CaKcUnnd.js +250 -0
- package/dist/assets/mindmap-definition-VGOIOE7T-Bct-pYin.js +68 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pieDiagram-ADFJNKIX-C8wFD4Jr.js +30 -0
- package/dist/assets/quadrantDiagram-AYHSOK5B-CuiE_f46.js +7 -0
- package/dist/assets/requirementDiagram-UZGBJVZJ-C4v1CqU8.js +64 -0
- package/dist/assets/sankeyDiagram-TZEHDZUN-keAqqs-Y.js +10 -0
- package/dist/assets/sequenceDiagram-WL72ISMW-ClNbvKTB.js +145 -0
- package/dist/assets/stateDiagram-FKZM4ZOC-D8We-2vq.js +1 -0
- package/dist/assets/stateDiagram-v2-4FDKWEC3-BUURfjrE.js +1 -0
- package/dist/assets/timeline-definition-IT6M3QCI-DsvEMT5T.js +61 -0
- package/dist/assets/treemap-GDKQZRPO-TJrzN7Bj.js +162 -0
- package/dist/assets/vendor-codemirror-ansM-eig.js +33 -0
- package/dist/assets/vendor-react-BWF6YQQ3.js +8 -0
- package/dist/assets/vendor-xterm-DfcmCpbH.js +66 -0
- package/dist/assets/xychartDiagram-PRI3JC2R-BdATmY5F.js +7 -0
- package/dist/clear-cache.html +85 -0
- package/dist/convert-icons.md +53 -0
- package/dist/dr-claw.png +0 -0
- package/dist/favicon.png +0 -0
- package/dist/generate-icons.js +49 -0
- package/dist/icons/claude-ai-icon.svg +1 -0
- package/dist/icons/codex-white.svg +3 -0
- package/dist/icons/codex.svg +3 -0
- package/dist/icons/cursor-white.svg +12 -0
- package/dist/icons/cursor.svg +1 -0
- package/dist/icons/file.svg +16997 -0
- package/dist/icons/generate-icons.md +19 -0
- package/dist/icons/icon-128x128.png +0 -0
- package/dist/icons/icon-144x144.png +0 -0
- package/dist/icons/icon-152x152.png +0 -0
- package/dist/icons/icon-192x192.png +0 -0
- package/dist/icons/icon-384x384.png +0 -0
- package/dist/icons/icon-512x512.png +0 -0
- package/dist/icons/icon-72x72.png +0 -0
- package/dist/icons/icon-96x96.png +0 -0
- package/dist/icons/news/arxiv.svg +1 -0
- package/dist/icons/news/huggingface.svg +8 -0
- package/dist/icons/news/x-black.png +0 -0
- package/dist/icons/news/x-white.png +0 -0
- package/dist/icons/news/xiaohongshu.png +0 -0
- package/dist/index.html +51 -0
- package/dist/leveraged-cognition.png +0 -0
- package/dist/manifest.json +61 -0
- package/dist/screenshots/chat.png +0 -0
- package/dist/screenshots/new_claw.png +0 -0
- package/dist/screenshots/news_dashboard.png +0 -0
- package/dist/screenshots/project_dashboard.png +0 -0
- package/dist/screenshots/research_lab.png +0 -0
- package/dist/screenshots/skill_library.png +0 -0
- package/dist/screenshots/task_list.png +0 -0
- package/dist/screenshots/terminal_example.png +0 -0
- package/dist/screenshots/terminal_example1.png +0 -0
- package/dist/sw.js +58 -0
- package/dist/wechat-group-qr.jpg +0 -0
- package/package.json +133 -0
- package/public/api-docs.html +878 -0
- package/public/clear-cache.html +85 -0
- package/public/convert-icons.md +53 -0
- package/public/dr-claw.png +0 -0
- package/public/favicon.png +0 -0
- package/public/generate-icons.js +49 -0
- package/public/icons/claude-ai-icon.svg +1 -0
- package/public/icons/codex-white.svg +3 -0
- package/public/icons/codex.svg +3 -0
- package/public/icons/cursor-white.svg +12 -0
- package/public/icons/cursor.svg +1 -0
- package/public/icons/file.svg +16997 -0
- package/public/icons/generate-icons.md +19 -0
- package/public/icons/icon-128x128.png +0 -0
- package/public/icons/icon-144x144.png +0 -0
- package/public/icons/icon-152x152.png +0 -0
- package/public/icons/icon-192x192.png +0 -0
- package/public/icons/icon-384x384.png +0 -0
- package/public/icons/icon-512x512.png +0 -0
- package/public/icons/icon-72x72.png +0 -0
- package/public/icons/icon-96x96.png +0 -0
- package/public/icons/news/arxiv.svg +1 -0
- package/public/icons/news/huggingface.svg +8 -0
- package/public/icons/news/x-black.png +0 -0
- package/public/icons/news/x-white.png +0 -0
- package/public/icons/news/xiaohongshu.png +0 -0
- package/public/leveraged-cognition.png +0 -0
- package/public/manifest.json +61 -0
- package/public/screenshots/chat.png +0 -0
- package/public/screenshots/new_claw.png +0 -0
- package/public/screenshots/news_dashboard.png +0 -0
- package/public/screenshots/project_dashboard.png +0 -0
- package/public/screenshots/research_lab.png +0 -0
- package/public/screenshots/skill_library.png +0 -0
- package/public/screenshots/task_list.png +0 -0
- package/public/screenshots/terminal_example.png +0 -0
- package/public/screenshots/terminal_example1.png +0 -0
- package/public/sw.js +58 -0
- package/public/wechat-group-qr.jpg +0 -0
- package/scripts/export-skills-catalog-v2.mjs +746 -0
- package/scripts/fix-node-pty.js +45 -0
- package/scripts/gemini_image_core.py +470 -0
- package/scripts/install-python-tools.js +64 -0
- package/server/AGENTS.md +73 -0
- package/server/__tests__/gemini-legacy-thought.test.mjs +88 -0
- package/server/__tests__/runtimePorts.test.mjs +115 -0
- package/server/__tests__/session-delete.test.mjs +166 -0
- package/server/bin/compute-node +70 -0
- package/server/claude-sdk.js +799 -0
- package/server/cli-chat.js +488 -0
- package/server/cli.js +354 -0
- package/server/compute-node.js +439 -0
- package/server/constants/config.js +5 -0
- package/server/cursor-cli.js +340 -0
- package/server/database/auth.db +0 -0
- package/server/database/db.js +1852 -0
- package/server/database/init.sql +194 -0
- package/server/gemini-cli.js +1352 -0
- package/server/index.js +3154 -0
- package/server/load-env.js +71 -0
- package/server/local-gpu.js +860 -0
- package/server/middleware/auth.js +100 -0
- package/server/openai-codex.js +724 -0
- package/server/openrouter.js +890 -0
- package/server/project-token-usage.js +407 -0
- package/server/projects.js +3973 -0
- package/server/routes/agent.js +1272 -0
- package/server/routes/auth.js +128 -0
- package/server/routes/auto-research.js +558 -0
- package/server/routes/cli-auth.js +958 -0
- package/server/routes/codex.js +344 -0
- package/server/routes/commands.js +601 -0
- package/server/routes/compute.js +358 -0
- package/server/routes/cursor.js +795 -0
- package/server/routes/git.js +1348 -0
- package/server/routes/mcp-utils.js +48 -0
- package/server/routes/mcp.js +618 -0
- package/server/routes/news.js +711 -0
- package/server/routes/projects.js +633 -0
- package/server/routes/references.js +338 -0
- package/server/routes/settings.js +285 -0
- package/server/routes/skills.js +914 -0
- package/server/routes/taskmaster.js +2566 -0
- package/server/routes/telemetry.js +89 -0
- package/server/routes/user.js +155 -0
- package/server/scripts/fix_session_names.js +57 -0
- package/server/scripts/research-news/common_words.py +19 -0
- package/server/scripts/research-news/link_keywords.py +298 -0
- package/server/scripts/research-news/scan_existing_notes.py +266 -0
- package/server/scripts/research-news/scoring_utils.py +227 -0
- package/server/scripts/research-news/search_arxiv.py +842 -0
- package/server/scripts/research-news/search_huggingface.py +438 -0
- package/server/scripts/research-news/search_x.py +490 -0
- package/server/scripts/research-news/search_xiaohongshu.py +610 -0
- package/server/taskmaster-templates/README.md +150 -0
- package/server/taskmaster-templates/README.zh-CN.md +150 -0
- package/server/taskmaster-templates/ai-research-dataset.json +230 -0
- package/server/taskmaster-templates/ai-research-method-model.json +230 -0
- package/server/taskmaster-templates/ai-research-position-paper.json +230 -0
- package/server/telemetry.js +121 -0
- package/server/templates/AGENTS.md +103 -0
- package/server/templates/CLAUDE.md +104 -0
- package/server/templates/GEMINI.md +103 -0
- package/server/templates/cursor-project.md +108 -0
- package/server/templates/index.js +37 -0
- package/server/utils/__tests__/sessionTokenUsage.test.js +82 -0
- package/server/utils/auto-research-mailer.js +57 -0
- package/server/utils/cliResolution.js +170 -0
- package/server/utils/commandParser.js +303 -0
- package/server/utils/cursorCommand.js +61 -0
- package/server/utils/geminiApiKey.js +28 -0
- package/server/utils/gitConfig.js +24 -0
- package/server/utils/imageAttachmentFiles.js +35 -0
- package/server/utils/mcp-detector.js +198 -0
- package/server/utils/parsers/bibtex-parser.js +207 -0
- package/server/utils/permissions.js +100 -0
- package/server/utils/runtimePorts.js +212 -0
- package/server/utils/sessionFormatting.js +68 -0
- package/server/utils/sessionIndex.js +100 -0
- package/server/utils/sessionMode.js +95 -0
- package/server/utils/sessionTokenUsage.js +105 -0
- package/server/utils/taskmaster-websocket.js +129 -0
- package/server/utils/zotero-client.js +136 -0
- package/shared/errorClassifier.js +102 -0
- package/shared/geminiThinkingSupport.d.ts +22 -0
- package/shared/geminiThinkingSupport.js +96 -0
- package/shared/geminiThoughtParser.d.ts +8 -0
- package/shared/geminiThoughtParser.js +30 -0
- package/shared/modelConstants.js +139 -0
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
import { credentialsDb } from '../database/db.js';
|
|
7
|
+
import { checkCommandAvailable } from '../utils/cliResolution.js';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
const router = express.Router();
|
|
13
|
+
|
|
14
|
+
// Data directory for news config & results
|
|
15
|
+
const DATA_DIR = path.join(__dirname, '..', 'data');
|
|
16
|
+
const SCRIPTS_DIR = path.join(__dirname, '..', 'scripts');
|
|
17
|
+
const PYTHON_RUNTIME_CACHE_TTL_MS = 30_000;
|
|
18
|
+
const PYTHON_RUNTIME_INSPECTION_CODE = [
|
|
19
|
+
'import importlib.util, json, os, ssl, sys',
|
|
20
|
+
'paths = ssl.get_default_verify_paths()',
|
|
21
|
+
'data = {',
|
|
22
|
+
' "executable": sys.executable,',
|
|
23
|
+
' "version": sys.version.split()[0],',
|
|
24
|
+
' "has_certifi": importlib.util.find_spec("certifi") is not None,',
|
|
25
|
+
' "cafile": paths.cafile,',
|
|
26
|
+
' "cafile_exists": bool(paths.cafile and os.path.exists(paths.cafile)),',
|
|
27
|
+
' "openssl_cafile": paths.openssl_cafile,',
|
|
28
|
+
' "openssl_cafile_exists": bool(paths.openssl_cafile and os.path.exists(paths.openssl_cafile))',
|
|
29
|
+
'}',
|
|
30
|
+
'print(json.dumps(data))',
|
|
31
|
+
].join('\n');
|
|
32
|
+
|
|
33
|
+
let cachedPythonRuntime = null;
|
|
34
|
+
let pythonRuntimeCacheExpiry = 0;
|
|
35
|
+
|
|
36
|
+
function getPythonRuntimeCandidates() {
|
|
37
|
+
if (process.platform === 'win32') {
|
|
38
|
+
return [
|
|
39
|
+
{ command: 'python', args: [], label: 'python' },
|
|
40
|
+
{ command: 'py', args: ['-3'], label: 'py -3' },
|
|
41
|
+
{ command: 'python3', args: [], label: 'python3' },
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return [
|
|
46
|
+
{ command: 'python3', args: [], label: 'python3' },
|
|
47
|
+
{ command: 'python', args: [], label: 'python' },
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function inspectPythonRuntime(candidate) {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
const child = spawn(candidate.command, [...candidate.args, '-c', PYTHON_RUNTIME_INSPECTION_CODE], {
|
|
54
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
let stdout = '';
|
|
58
|
+
let stderr = '';
|
|
59
|
+
let settled = false;
|
|
60
|
+
|
|
61
|
+
const finish = (value) => {
|
|
62
|
+
if (settled) return;
|
|
63
|
+
settled = true;
|
|
64
|
+
resolve(value);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const timeout = setTimeout(() => {
|
|
68
|
+
try { child.kill(); } catch {}
|
|
69
|
+
finish(null);
|
|
70
|
+
}, 3000);
|
|
71
|
+
|
|
72
|
+
child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
73
|
+
child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
74
|
+
child.on('error', () => {
|
|
75
|
+
clearTimeout(timeout);
|
|
76
|
+
finish(null);
|
|
77
|
+
});
|
|
78
|
+
child.on('close', (code) => {
|
|
79
|
+
clearTimeout(timeout);
|
|
80
|
+
if (code !== 0) {
|
|
81
|
+
finish(null);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const inspected = JSON.parse(stdout.trim());
|
|
86
|
+
finish({
|
|
87
|
+
...candidate,
|
|
88
|
+
...inspected,
|
|
89
|
+
hasUsableCertificates: Boolean(
|
|
90
|
+
inspected.has_certifi ||
|
|
91
|
+
inspected.cafile_exists ||
|
|
92
|
+
inspected.openssl_cafile_exists
|
|
93
|
+
),
|
|
94
|
+
stderr: stderr.trim(),
|
|
95
|
+
});
|
|
96
|
+
} catch {
|
|
97
|
+
finish(null);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function resolvePythonRuntime(options = {}) {
|
|
104
|
+
const { refresh = false } = options;
|
|
105
|
+
|
|
106
|
+
if (!refresh && cachedPythonRuntime && Date.now() < pythonRuntimeCacheExpiry) {
|
|
107
|
+
return cachedPythonRuntime;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const inspectedCandidates = [];
|
|
111
|
+
for (const candidate of getPythonRuntimeCandidates()) {
|
|
112
|
+
const probeArgs = [...candidate.args, '--version'];
|
|
113
|
+
if (await checkCommandAvailable(candidate.command, probeArgs, { platform: process.platform })) {
|
|
114
|
+
const inspected = await inspectPythonRuntime(candidate);
|
|
115
|
+
inspectedCandidates.push(inspected || {
|
|
116
|
+
...candidate,
|
|
117
|
+
executable: null,
|
|
118
|
+
version: null,
|
|
119
|
+
has_certifi: false,
|
|
120
|
+
cafile: null,
|
|
121
|
+
cafile_exists: false,
|
|
122
|
+
openssl_cafile: null,
|
|
123
|
+
openssl_cafile_exists: false,
|
|
124
|
+
hasUsableCertificates: false,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const selectedRuntime = inspectedCandidates.find((candidate) => candidate?.hasUsableCertificates)
|
|
130
|
+
|| inspectedCandidates[0]
|
|
131
|
+
|| null;
|
|
132
|
+
|
|
133
|
+
if (selectedRuntime) {
|
|
134
|
+
cachedPythonRuntime = selectedRuntime;
|
|
135
|
+
pythonRuntimeCacheExpiry = Date.now() + PYTHON_RUNTIME_CACHE_TTL_MS;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return selectedRuntime;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Source Registry
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
const SOURCE_REGISTRY = {
|
|
145
|
+
arxiv: {
|
|
146
|
+
label: 'arXiv',
|
|
147
|
+
script: 'research-news/search_arxiv.py',
|
|
148
|
+
configFile: 'news-config-arxiv.json',
|
|
149
|
+
resultsFile: 'news-results-arxiv.json',
|
|
150
|
+
defaultConfig: {
|
|
151
|
+
research_domains: {
|
|
152
|
+
'Large Language Models': {
|
|
153
|
+
keywords: ['large language model', 'LLM', 'transformer', 'foundation model'],
|
|
154
|
+
arxiv_categories: ['cs.AI', 'cs.LG', 'cs.CL'],
|
|
155
|
+
priority: 5,
|
|
156
|
+
},
|
|
157
|
+
'Multimodal': {
|
|
158
|
+
keywords: ['vision-language', 'multimodal', 'image-text', 'visual'],
|
|
159
|
+
arxiv_categories: ['cs.CV', 'cs.MM', 'cs.CL'],
|
|
160
|
+
priority: 4,
|
|
161
|
+
},
|
|
162
|
+
'AI Agents': {
|
|
163
|
+
keywords: ['agent', 'multi-agent', 'orchestration', 'autonomous', 'planning'],
|
|
164
|
+
arxiv_categories: ['cs.AI', 'cs.MA', 'cs.RO'],
|
|
165
|
+
priority: 4,
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
top_n: 10,
|
|
169
|
+
max_results: 200,
|
|
170
|
+
categories: 'cs.AI,cs.LG,cs.CL,cs.CV,cs.MM,cs.MA,cs.RO',
|
|
171
|
+
},
|
|
172
|
+
requiresCredentials: false,
|
|
173
|
+
},
|
|
174
|
+
huggingface: {
|
|
175
|
+
label: 'HuggingFace Daily Papers',
|
|
176
|
+
script: 'research-news/search_huggingface.py',
|
|
177
|
+
configFile: 'news-config-huggingface.json',
|
|
178
|
+
resultsFile: 'news-results-huggingface.json',
|
|
179
|
+
defaultConfig: {
|
|
180
|
+
research_domains: {},
|
|
181
|
+
top_n: 30,
|
|
182
|
+
},
|
|
183
|
+
requiresCredentials: false,
|
|
184
|
+
},
|
|
185
|
+
x: {
|
|
186
|
+
label: 'X (Twitter)',
|
|
187
|
+
script: 'research-news/search_x.py',
|
|
188
|
+
configFile: 'news-config-x.json',
|
|
189
|
+
resultsFile: 'news-results-x.json',
|
|
190
|
+
defaultConfig: {
|
|
191
|
+
research_domains: {
|
|
192
|
+
'Large Language Models': {
|
|
193
|
+
keywords: ['large language model', 'LLM', 'transformer', 'foundation model'],
|
|
194
|
+
arxiv_categories: [],
|
|
195
|
+
priority: 5,
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
top_n: 10,
|
|
199
|
+
queries: 'LLM,AI agents,foundation model',
|
|
200
|
+
accounts: '',
|
|
201
|
+
},
|
|
202
|
+
requiresCredentials: false,
|
|
203
|
+
},
|
|
204
|
+
xiaohongshu: {
|
|
205
|
+
label: 'Xiaohongshu',
|
|
206
|
+
script: 'research-news/search_xiaohongshu.py',
|
|
207
|
+
configFile: 'news-config-xiaohongshu.json',
|
|
208
|
+
resultsFile: 'news-results-xiaohongshu.json',
|
|
209
|
+
defaultConfig: {
|
|
210
|
+
research_domains: {
|
|
211
|
+
'Large Language Models': {
|
|
212
|
+
keywords: ['大模型', 'LLM', 'AI', '人工智能'],
|
|
213
|
+
arxiv_categories: [],
|
|
214
|
+
priority: 5,
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
top_n: 10,
|
|
218
|
+
keywords: '大模型,AI论文,人工智能',
|
|
219
|
+
},
|
|
220
|
+
requiresCredentials: false,
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
async function ensureDataDir() {
|
|
225
|
+
await fs.mkdir(DATA_DIR, { recursive: true });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function getSourceEntry(source) {
|
|
229
|
+
return SOURCE_REGISTRY[source] || null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// GET /api/news/sources — list all sources with status
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
router.get('/sources', async (req, res) => {
|
|
236
|
+
try {
|
|
237
|
+
await ensureDataDir();
|
|
238
|
+
const sources = [];
|
|
239
|
+
for (const [key, entry] of Object.entries(SOURCE_REGISTRY)) {
|
|
240
|
+
// Check if results file exists
|
|
241
|
+
let hasResults = false;
|
|
242
|
+
let lastSearchDate = null;
|
|
243
|
+
try {
|
|
244
|
+
const resultsPath = path.join(DATA_DIR, entry.resultsFile);
|
|
245
|
+
const data = JSON.parse(await fs.readFile(resultsPath, 'utf8'));
|
|
246
|
+
hasResults = (data.top_papers?.length ?? 0) > 0;
|
|
247
|
+
lastSearchDate = data.search_date || null;
|
|
248
|
+
} catch { /* no results yet */ }
|
|
249
|
+
|
|
250
|
+
// Check credentials status for sources that need them
|
|
251
|
+
let credentialStatus = 'not_required';
|
|
252
|
+
if (entry.requiresCredentials) {
|
|
253
|
+
try {
|
|
254
|
+
const cred = credentialsDb.getActiveCredential(req.user.id, entry.credentialType);
|
|
255
|
+
credentialStatus = cred ? 'configured' : 'missing';
|
|
256
|
+
} catch {
|
|
257
|
+
credentialStatus = 'missing';
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
sources.push({
|
|
262
|
+
key,
|
|
263
|
+
label: entry.label,
|
|
264
|
+
hasResults,
|
|
265
|
+
lastSearchDate,
|
|
266
|
+
requiresCredentials: entry.requiresCredentials,
|
|
267
|
+
credentialType: entry.credentialType || null,
|
|
268
|
+
credentialStatus,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
res.json({ sources });
|
|
272
|
+
} catch (err) {
|
|
273
|
+
res.status(500).json({ error: 'Failed to list sources', details: err.message });
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
// GET /api/news/config/:source — per-source config
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
router.get('/config/:source', async (req, res) => {
|
|
281
|
+
try {
|
|
282
|
+
const entry = getSourceEntry(req.params.source);
|
|
283
|
+
if (!entry) return res.status(404).json({ error: `Unknown source: ${req.params.source}` });
|
|
284
|
+
|
|
285
|
+
await ensureDataDir();
|
|
286
|
+
const configPath = path.join(DATA_DIR, entry.configFile);
|
|
287
|
+
const data = await fs.readFile(configPath, 'utf8');
|
|
288
|
+
res.json(JSON.parse(data));
|
|
289
|
+
} catch (err) {
|
|
290
|
+
if (err.code === 'ENOENT') {
|
|
291
|
+
const entry = getSourceEntry(req.params.source);
|
|
292
|
+
res.json(entry.defaultConfig);
|
|
293
|
+
} else {
|
|
294
|
+
res.status(500).json({ error: 'Failed to read config', details: err.message });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// PUT /api/news/config/:source — save per-source config
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
router.put('/config/:source', async (req, res) => {
|
|
303
|
+
try {
|
|
304
|
+
const entry = getSourceEntry(req.params.source);
|
|
305
|
+
if (!entry) return res.status(404).json({ error: `Unknown source: ${req.params.source}` });
|
|
306
|
+
|
|
307
|
+
await ensureDataDir();
|
|
308
|
+
const configPath = path.join(DATA_DIR, entry.configFile);
|
|
309
|
+
await fs.writeFile(configPath, JSON.stringify(req.body, null, 2), 'utf8');
|
|
310
|
+
res.json({ success: true });
|
|
311
|
+
} catch (err) {
|
|
312
|
+
res.status(500).json({ error: 'Failed to save config', details: err.message });
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
// Search handler — streams progress via Server-Sent Events (SSE)
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
async function handleSearch(sourceName, req, res) {
|
|
320
|
+
try {
|
|
321
|
+
const entry = getSourceEntry(sourceName);
|
|
322
|
+
if (!entry) return res.status(404).json({ error: `Unknown source: ${sourceName}` });
|
|
323
|
+
|
|
324
|
+
await ensureDataDir();
|
|
325
|
+
|
|
326
|
+
// Read current config
|
|
327
|
+
const configPath = path.join(DATA_DIR, entry.configFile);
|
|
328
|
+
let config;
|
|
329
|
+
try {
|
|
330
|
+
config = JSON.parse(await fs.readFile(configPath, 'utf8'));
|
|
331
|
+
} catch {
|
|
332
|
+
config = entry.defaultConfig;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Write JSON config for the Python script
|
|
336
|
+
const tmpConfigPath = path.join(DATA_DIR, `research_interests_${sourceName}.json`);
|
|
337
|
+
await fs.writeFile(tmpConfigPath, JSON.stringify(config, null, 2), 'utf8');
|
|
338
|
+
|
|
339
|
+
const scriptPath = path.join(SCRIPTS_DIR, entry.script);
|
|
340
|
+
|
|
341
|
+
// Check if script exists
|
|
342
|
+
try {
|
|
343
|
+
await fs.access(scriptPath);
|
|
344
|
+
} catch {
|
|
345
|
+
return res.status(404).json({ error: `Search script not found for source: ${sourceName}` });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const resultsPath = path.join(DATA_DIR, entry.resultsFile);
|
|
349
|
+
const topN = config.top_n || 10;
|
|
350
|
+
|
|
351
|
+
// Build args based on source
|
|
352
|
+
// HuggingFace: config is optional (fetches all daily papers without filtering)
|
|
353
|
+
const args = [scriptPath];
|
|
354
|
+
const hasDomains = config.research_domains && Object.keys(config.research_domains).length > 0;
|
|
355
|
+
if (sourceName !== 'huggingface' || hasDomains) {
|
|
356
|
+
args.push('--config', tmpConfigPath);
|
|
357
|
+
}
|
|
358
|
+
args.push('--output', resultsPath, '--top-n', String(topN));
|
|
359
|
+
|
|
360
|
+
if (sourceName === 'arxiv') {
|
|
361
|
+
const maxResults = config.max_results || 200;
|
|
362
|
+
const categories = config.categories || 'cs.AI,cs.LG,cs.CL,cs.CV,cs.MM,cs.MA,cs.RO';
|
|
363
|
+
args.push('--max-results', String(maxResults), '--categories', categories);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (sourceName === 'x' && config.queries) {
|
|
367
|
+
args.push('--queries', config.queries);
|
|
368
|
+
}
|
|
369
|
+
if (sourceName === 'x' && config.accounts) {
|
|
370
|
+
args.push('--accounts', config.accounts);
|
|
371
|
+
}
|
|
372
|
+
if (sourceName === 'xiaohongshu' && config.keywords) {
|
|
373
|
+
args.push('--keywords', config.keywords);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Build env — pass credentials if required.
|
|
377
|
+
// Strip __PYVENV_LAUNCHER__ so uv-installed Python CLIs invoked by the
|
|
378
|
+
// search scripts find the correct stdlib (macOS Python framework sets this
|
|
379
|
+
// variable and it confuses child interpreters with a different version).
|
|
380
|
+
const env = { ...process.env };
|
|
381
|
+
delete env.__PYVENV_LAUNCHER__;
|
|
382
|
+
if (entry.requiresCredentials) {
|
|
383
|
+
try {
|
|
384
|
+
const credValue = credentialsDb.getActiveCredential(req.user.id, entry.credentialType);
|
|
385
|
+
if (!credValue) {
|
|
386
|
+
return res.status(400).json({
|
|
387
|
+
error: `No active credential found for ${entry.label}. Please add your ${entry.credentialType} in settings.`,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
// Map credential types to environment variables
|
|
391
|
+
const credEnvMap = {
|
|
392
|
+
// Add future credential mappings here
|
|
393
|
+
};
|
|
394
|
+
const envVar = credEnvMap[entry.credentialType];
|
|
395
|
+
if (envVar) {
|
|
396
|
+
env[envVar] = credValue;
|
|
397
|
+
}
|
|
398
|
+
} catch (credErr) {
|
|
399
|
+
return res.status(400).json({ error: 'Failed to retrieve credentials', details: credErr.message });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Write search logs to a file so they can be polled by the frontend
|
|
404
|
+
const logPath = path.join(DATA_DIR, `news-log-${sourceName}.json`);
|
|
405
|
+
await fs.writeFile(logPath, JSON.stringify([]), 'utf8');
|
|
406
|
+
const logs = [];
|
|
407
|
+
|
|
408
|
+
const pythonRuntime = await resolvePythonRuntime();
|
|
409
|
+
if (!pythonRuntime) {
|
|
410
|
+
const details = process.platform === 'win32'
|
|
411
|
+
? 'No usable Python runtime found. Tried: python, py -3, python3.'
|
|
412
|
+
: 'No usable Python runtime found. Tried: python3, python.';
|
|
413
|
+
logs.push(details);
|
|
414
|
+
try { await fs.writeFile(logPath, JSON.stringify(logs), 'utf8'); } catch {}
|
|
415
|
+
console.error(`[news][${sourceName}] ${details}`);
|
|
416
|
+
return res.status(503).json({
|
|
417
|
+
error: `Search failed for ${entry.label}`,
|
|
418
|
+
details,
|
|
419
|
+
logs,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const runtimeDetails = [pythonRuntime.label];
|
|
424
|
+
if (pythonRuntime.version) runtimeDetails.push(`Python ${pythonRuntime.version}`);
|
|
425
|
+
if (pythonRuntime.executable) runtimeDetails.push(pythonRuntime.executable);
|
|
426
|
+
logs.push(`[runtime] Using ${runtimeDetails.join(' | ')}`);
|
|
427
|
+
if (!pythonRuntime.hasUsableCertificates) {
|
|
428
|
+
logs.push('[runtime] Warning: selected Python runtime has no detectable CA bundle; HTTPS requests may fail.');
|
|
429
|
+
}
|
|
430
|
+
try { await fs.writeFile(logPath, JSON.stringify(logs), 'utf8'); } catch {}
|
|
431
|
+
|
|
432
|
+
const child = spawn(pythonRuntime.command, [...pythonRuntime.args, ...args], {
|
|
433
|
+
cwd: path.join(SCRIPTS_DIR, 'research-news'),
|
|
434
|
+
env,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
let stdout = '';
|
|
438
|
+
let stderrBuf = '';
|
|
439
|
+
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
440
|
+
child.stderr.on('data', async (data) => {
|
|
441
|
+
const chunk = data.toString();
|
|
442
|
+
stderrBuf += chunk;
|
|
443
|
+
const lines = stderrBuf.split('\n');
|
|
444
|
+
stderrBuf = lines.pop();
|
|
445
|
+
for (const line of lines) {
|
|
446
|
+
const trimmed = line.trim();
|
|
447
|
+
if (trimmed) logs.push(trimmed);
|
|
448
|
+
}
|
|
449
|
+
// Update log file for polling
|
|
450
|
+
try { await fs.writeFile(logPath, JSON.stringify(logs), 'utf8'); } catch {}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
child.on('close', async (code) => {
|
|
454
|
+
if (stderrBuf.trim()) logs.push(stderrBuf.trim());
|
|
455
|
+
try { await fs.writeFile(logPath, JSON.stringify(logs), 'utf8'); } catch {}
|
|
456
|
+
|
|
457
|
+
if (code !== 0) {
|
|
458
|
+
console.error(`[news][${sourceName}] script failed (exit ${code})`);
|
|
459
|
+
return res.status(500).json({
|
|
460
|
+
error: `Search failed for ${entry.label}`,
|
|
461
|
+
details: logs.join('\n'),
|
|
462
|
+
logs,
|
|
463
|
+
exitCode: code,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
const results = JSON.parse(await fs.readFile(resultsPath, 'utf8'));
|
|
469
|
+
results.logs = logs;
|
|
470
|
+
res.json(results);
|
|
471
|
+
} catch (readErr) {
|
|
472
|
+
res.status(500).json({ error: 'Failed to read search results', details: readErr.message });
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
child.on('error', (err) => {
|
|
477
|
+
console.error(`[news][${sourceName}] Failed to spawn script:`, err);
|
|
478
|
+
res.status(500).json({ error: 'Failed to execute search script', details: err.message });
|
|
479
|
+
});
|
|
480
|
+
} catch (err) {
|
|
481
|
+
res.status(500).json({ error: 'Search failed', details: err.message });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// POST /api/news/search/:source — trigger search for one source
|
|
486
|
+
router.post('/search/:source', (req, res) => handleSearch(req.params.source, req, res));
|
|
487
|
+
|
|
488
|
+
// GET /api/news/logs/:source — poll search progress logs
|
|
489
|
+
router.get('/logs/:source', async (req, res) => {
|
|
490
|
+
try {
|
|
491
|
+
const logPath = path.join(DATA_DIR, `news-log-${req.params.source}.json`);
|
|
492
|
+
const data = await fs.readFile(logPath, 'utf8');
|
|
493
|
+
res.json({ logs: JSON.parse(data) });
|
|
494
|
+
} catch {
|
|
495
|
+
res.json({ logs: [] });
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
// POST /api/news/xhs-login — trigger xiaohongshu-cli login
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
router.post('/xhs-login', (req, res) => {
|
|
503
|
+
const requestedMethod = req.body?.method === 'qrcode' ? 'qrcode' : 'browser';
|
|
504
|
+
const requestedCookieSource = typeof req.body?.cookieSource === 'string'
|
|
505
|
+
? req.body.cookieSource.trim().toLowerCase()
|
|
506
|
+
: 'auto';
|
|
507
|
+
const allowedCookieSources = new Set([
|
|
508
|
+
'auto', 'arc', 'brave', 'chrome', 'chromium', 'edge', 'firefox', 'librewolf', 'opera', 'opera_gx', 'safari', 'vivaldi',
|
|
509
|
+
]);
|
|
510
|
+
const cookieSource = allowedCookieSources.has(requestedCookieSource) ? requestedCookieSource : 'auto';
|
|
511
|
+
const commandArgs = ['login'];
|
|
512
|
+
if (requestedMethod === 'qrcode') {
|
|
513
|
+
commandArgs.push('--qrcode');
|
|
514
|
+
} else if (cookieSource !== 'auto') {
|
|
515
|
+
commandArgs.push('--cookie-source', cookieSource);
|
|
516
|
+
}
|
|
517
|
+
commandArgs.push('--json');
|
|
518
|
+
|
|
519
|
+
const xhsEnv = { ...process.env };
|
|
520
|
+
delete xhsEnv.__PYVENV_LAUNCHER__;
|
|
521
|
+
const child = spawn('xhs', commandArgs, {
|
|
522
|
+
env: xhsEnv,
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
let stdoutBuf = '';
|
|
526
|
+
let stderrBuf = '';
|
|
527
|
+
const logs = [];
|
|
528
|
+
let responded = false;
|
|
529
|
+
|
|
530
|
+
const sendOnce = (status, payload) => {
|
|
531
|
+
if (responded || res.headersSent) return;
|
|
532
|
+
responded = true;
|
|
533
|
+
res.status(status).json(payload);
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
child.stdout.on('data', (data) => { stdoutBuf += data.toString(); });
|
|
537
|
+
child.stderr.on('data', (data) => {
|
|
538
|
+
stderrBuf += data.toString();
|
|
539
|
+
const lines = stderrBuf.split('\n');
|
|
540
|
+
stderrBuf = lines.pop() || '';
|
|
541
|
+
for (const line of lines) {
|
|
542
|
+
const trimmed = line.trim();
|
|
543
|
+
if (trimmed) logs.push(trimmed);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
child.on('close', (code) => {
|
|
548
|
+
if (stderrBuf.trim()) logs.push(stderrBuf.trim());
|
|
549
|
+
|
|
550
|
+
let authenticated = false;
|
|
551
|
+
let nickname = '';
|
|
552
|
+
let error = '';
|
|
553
|
+
let contextHint = '';
|
|
554
|
+
try {
|
|
555
|
+
const result = JSON.parse(stdoutBuf);
|
|
556
|
+
authenticated = !!(result?.ok && result?.data?.authenticated);
|
|
557
|
+
nickname = result?.data?.user?.nickname || '';
|
|
558
|
+
if (!authenticated) {
|
|
559
|
+
error = result?.error?.message || result?.message || '';
|
|
560
|
+
}
|
|
561
|
+
} catch {
|
|
562
|
+
authenticated = code === 0;
|
|
563
|
+
if (!authenticated) {
|
|
564
|
+
error = stdoutBuf.trim();
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (!authenticated && !error) {
|
|
569
|
+
error = requestedMethod === 'qrcode'
|
|
570
|
+
? 'QR login failed or timed out.'
|
|
571
|
+
: 'Browser cookie extraction failed.';
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (!authenticated) {
|
|
575
|
+
contextHint = requestedMethod === 'qrcode'
|
|
576
|
+
? 'QR login is recommended for remote deployments and Linux browser-cookie issues.'
|
|
577
|
+
: 'Browser cookie extraction runs on the machine hosting the dr-claw service, not on the device where this page is open.';
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
sendOnce(200, {
|
|
581
|
+
success: authenticated,
|
|
582
|
+
nickname,
|
|
583
|
+
logs,
|
|
584
|
+
exitCode: code,
|
|
585
|
+
method: requestedMethod,
|
|
586
|
+
cookieSource,
|
|
587
|
+
error,
|
|
588
|
+
contextHint: contextHint || undefined,
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
child.on('error', (err) => {
|
|
593
|
+
const contextHint = requestedMethod === 'qrcode'
|
|
594
|
+
? 'QR login is recommended for remote deployments and Linux browser-cookie issues.'
|
|
595
|
+
: 'Browser cookie extraction runs on the machine hosting the dr-claw service, not on the device where this page is open.';
|
|
596
|
+
|
|
597
|
+
sendOnce(500, {
|
|
598
|
+
success: false,
|
|
599
|
+
error: `Failed to run xhs login: ${err.message}`,
|
|
600
|
+
logs,
|
|
601
|
+
method: requestedMethod,
|
|
602
|
+
cookieSource,
|
|
603
|
+
contextHint,
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// ---------------------------------------------------------------------------
|
|
609
|
+
// GET /api/news/results/:source — cached results for one source
|
|
610
|
+
// ---------------------------------------------------------------------------
|
|
611
|
+
router.get('/results/:source', async (req, res) => {
|
|
612
|
+
try {
|
|
613
|
+
const entry = getSourceEntry(req.params.source);
|
|
614
|
+
if (!entry) return res.status(404).json({ error: `Unknown source: ${req.params.source}` });
|
|
615
|
+
|
|
616
|
+
const resultsPath = path.join(DATA_DIR, entry.resultsFile);
|
|
617
|
+
const data = await fs.readFile(resultsPath, 'utf8');
|
|
618
|
+
res.json(JSON.parse(data));
|
|
619
|
+
} catch (err) {
|
|
620
|
+
if (err.code === 'ENOENT') {
|
|
621
|
+
res.json({ top_papers: [], total_found: 0, total_filtered: 0 });
|
|
622
|
+
} else {
|
|
623
|
+
res.status(500).json({ error: 'Failed to read results', details: err.message });
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// ---------------------------------------------------------------------------
|
|
629
|
+
// Backward-compatible aliases (old routes → arxiv source)
|
|
630
|
+
// ---------------------------------------------------------------------------
|
|
631
|
+
router.get('/config', async (req, res) => {
|
|
632
|
+
try {
|
|
633
|
+
await ensureDataDir();
|
|
634
|
+
const entry = SOURCE_REGISTRY.arxiv;
|
|
635
|
+
const configPath = path.join(DATA_DIR, entry.configFile);
|
|
636
|
+
const data = await fs.readFile(configPath, 'utf8');
|
|
637
|
+
res.json(JSON.parse(data));
|
|
638
|
+
} catch (err) {
|
|
639
|
+
if (err.code === 'ENOENT') {
|
|
640
|
+
res.json(SOURCE_REGISTRY.arxiv.defaultConfig);
|
|
641
|
+
} else {
|
|
642
|
+
res.status(500).json({ error: 'Failed to read config', details: err.message });
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
router.put('/config', async (req, res) => {
|
|
648
|
+
try {
|
|
649
|
+
const entry = SOURCE_REGISTRY.arxiv;
|
|
650
|
+
await ensureDataDir();
|
|
651
|
+
const configPath = path.join(DATA_DIR, entry.configFile);
|
|
652
|
+
await fs.writeFile(configPath, JSON.stringify(req.body, null, 2), 'utf8');
|
|
653
|
+
res.json({ success: true });
|
|
654
|
+
} catch (err) {
|
|
655
|
+
res.status(500).json({ error: 'Failed to save config', details: err.message });
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
router.post('/search', (req, res) => handleSearch('arxiv', req, res));
|
|
660
|
+
|
|
661
|
+
router.get('/results', async (req, res) => {
|
|
662
|
+
try {
|
|
663
|
+
const entry = SOURCE_REGISTRY.arxiv;
|
|
664
|
+
const resultsPath = path.join(DATA_DIR, entry.resultsFile);
|
|
665
|
+
const data = await fs.readFile(resultsPath, 'utf8');
|
|
666
|
+
res.json(JSON.parse(data));
|
|
667
|
+
} catch (err) {
|
|
668
|
+
if (err.code === 'ENOENT') {
|
|
669
|
+
// Also try the legacy path for backward compat
|
|
670
|
+
try {
|
|
671
|
+
const legacyPath = path.join(DATA_DIR, 'news-results.json');
|
|
672
|
+
const data = await fs.readFile(legacyPath, 'utf8');
|
|
673
|
+
res.json(JSON.parse(data));
|
|
674
|
+
} catch {
|
|
675
|
+
res.json({ top_papers: [], total_found: 0, total_filtered: 0 });
|
|
676
|
+
}
|
|
677
|
+
} else {
|
|
678
|
+
res.status(500).json({ error: 'Failed to read results', details: err.message });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
// ---------------------------------------------------------------------------
|
|
684
|
+
// Helpers
|
|
685
|
+
// ---------------------------------------------------------------------------
|
|
686
|
+
function buildYamlConfig(config) {
|
|
687
|
+
let yaml = '# Auto-generated from Dr. Claw News Dashboard config\n\n';
|
|
688
|
+
yaml += 'research_domains:\n';
|
|
689
|
+
|
|
690
|
+
const domains = config.research_domains || {};
|
|
691
|
+
for (const [name, domain] of Object.entries(domains)) {
|
|
692
|
+
yaml += ` "${name}":\n`;
|
|
693
|
+
yaml += ` keywords:\n`;
|
|
694
|
+
for (const kw of domain.keywords || []) {
|
|
695
|
+
yaml += ` - "${kw}"\n`;
|
|
696
|
+
}
|
|
697
|
+
if (domain.arxiv_categories?.length) {
|
|
698
|
+
yaml += ` arxiv_categories:\n`;
|
|
699
|
+
for (const cat of domain.arxiv_categories) {
|
|
700
|
+
yaml += ` - "${cat}"\n`;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (domain.priority) {
|
|
704
|
+
yaml += ` priority: ${domain.priority}\n`;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return yaml;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
export default router;
|