crawl.js 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. 'use strict';
  2. // 门店采集路由:店铺清单、抓取任务控制、任务/系统日志
  3. const fs = require('fs');
  4. const { sendJson, readBody } = require('../util.js');
  5. function handleShopsManual(ctx, body, res) {
  6. const lines = String((body && body.text) || '').split(/\r?\n/);
  7. const parsed = [];
  8. for (const line of lines) {
  9. const t = line.trim();
  10. if (!t) continue;
  11. const parts = t.split(/[\s,,]+/).filter(Boolean);
  12. const idPart = parts.find((p) => /^\d+$/.test(p));
  13. if (!idPart) continue;
  14. const shopId = Number(idPart);
  15. const shopName = parts.filter((p) => p !== idPart).join(' ') || String(shopId);
  16. parsed.push({ shopId, shopName, source: 'manual' });
  17. }
  18. const previousShops = ctx.shops.getList();
  19. const mergedShops = ctx.shops.mergeShops(parsed);
  20. try { ctx.shops.saveManualShops(mergedShops); ctx.shops.setList(mergedShops); }
  21. catch (e) { ctx.shops.setList(previousShops); throw e; }
  22. sendJson(res, 200, { shops: ctx.shops.getList(), addedCount: parsed.length });
  23. }
  24. function handleShopsList(ctx, root, res) {
  25. if (root && !ctx.workspace.isWorkspaceRoot(root)) return sendJson(res, 403, { error: '仅允许访问当前输出根目录' });
  26. const completed = root && fs.existsSync(root) ? ctx.crawl.readCompletionState(root) : {};
  27. sendJson(res, 200, {
  28. shops: ctx.shops.getList().map((shop) => ({ ...shop, completed: completed[String(shop.shopId)] || null })),
  29. });
  30. }
  31. function register(app, ctx) {
  32. app.get('/api/shops', async ({ res, query }) => {
  33. // 前端把 root 做 base64url 编码传入,避免中文路径在 URL 里出问题
  34. const encodedRoot = query.root;
  35. const root = encodedRoot ? Buffer.from(encodedRoot, 'base64url').toString('utf8') : '';
  36. return handleShopsList(ctx, root, res);
  37. });
  38. app.post('/api/shops/manual', async ({ req, res }) => {
  39. const body = await readBody(req);
  40. return handleShopsManual(ctx, body, res);
  41. });
  42. app.post('/api/crawl/start', async ({ req, res }) => {
  43. const body = await readBody(req);
  44. return ctx.crawl.startCrawl(body, res);
  45. });
  46. app.post('/api/crawl/stop', async ({ res }) => {
  47. return ctx.crawl.stopCrawl(res);
  48. });
  49. app.get('/api/crawl/status', async ({ res }) => {
  50. sendJson(res, 200, ctx.crawl.getState());
  51. });
  52. // 采集通道探测:客户端调试端口是否就绪 + Cookie 是否已配置(采集页提示用)
  53. app.get('/api/crawl/channel', async ({ res }) => {
  54. const kit = require('../../napos/kit.js');
  55. const p = await kit.cdp.probe().catch(() => ({ ok: false }));
  56. sendJson(res, 200, {
  57. channel: ctx.config.crawlChannel || 'auto',
  58. clientUsable: !!p.ok,
  59. clientBrowser: (p.ok && p.version && p.version.Browser) || null,
  60. cookieConfigured: !!ctx.session.cookie && !!ctx.session.getKsid(),
  61. });
  62. });
  63. app.get('/api/logs', async ({ res }) => {
  64. sendJson(res, 200, { logs: ctx.logs.mergeLogs(ctx.crawl.getLog()) });
  65. });
  66. app.post('/api/logs/clear', async ({ res }) => {
  67. ctx.logs.clearAll(ctx.crawl.getLog());
  68. ctx.logs.pushSystemLog('日志已清空', 'warn');
  69. return sendJson(res, 200, { ok: true });
  70. });
  71. }
  72. module.exports = { register };