项目:XSearch | 版本:v1.1 | 生成日期:2026-10-06 | 依据:源码分析
本篇是"改代码之前先读"的操作手册:给出六个高频扩展场景的逐步 SOP,全部以当前源码结构为锚点。每个场景都遵循同一格式:目标 → 触点清单(要动哪些文件)→ 步骤 → 验证方法 → 红线。开工前建议先读《07-模块详解》的"模块间依赖的三条铁律",再对照本篇执行。
这是最常见的扩展,触点最多,也是唯一需要先过合规关的。
触点清单:新建 XSearch/crawler/spiders/<name>.py;XSearch/search/crawl_manager.py 的 SPIDERS 字典;XSearch/search/api_views.py 的来源分组常量(若属新闻/AI/榜单分类);frontend/src/views/(若需新页面或 Tab)与 CrawlView.vue 内联爬虫列表;XSearch/search/tests.py。
步骤:
User-agent: * 未禁目标路径;确认无需登录、无验证码;在 crawler/settings.py 的 FINGERPRINT_HOSTS 保持不动(注释原话:"新增条目 = 扩大风险面,动手前先确认该站真的拒绝常规 UA"——只有诚实 UA 拿 444/412 的站才有资格进白名单,且要先在《08-安全与权限设计》第九节的立场上过一遍)。CrawlerComplianceTests.test_spoofing_scoped_to_douyin_only 会因白名单变化而失败,这是有意的提醒。aihot_hot.py 的骨架——custom_settings 设 DOWNLOAD_DELAY(≥1,官方 API 建议 2)与必要的 HANDLE_HTTPSTATUS_LIST;Scrapy 2.13+ 用 async def start() yield 首批请求;解析回调产出标准化 item dict,字段契约见《15-接口全量参考》第六节,其中 url_object_id(稳定唯一 id,决定幂等)与 source(新来源 key,建议 <站名>_<类型> 风格)两个键决定下游一切行为;正文过 5000 字符上限。crawl_manager.SPIDERS 加 "<key>": {"scrapy_name": "<scrapy名>", "label": "<中文标签>"}——不加这行,Web 界面启动会被"未知爬虫"拒绝(这是防任意命令执行的闸门,不是繁文缛节)。api_views.NEWS_SOURCES;AI 源加入 AI_SOURCES;榜单源加入 RANKING_SOURCES(会被搜索与要闻接口自动排除)。若都不是,只需保证 source 字段正确,分面聚合会自动展示新来源。RankingsView.vue 的 CRAWL_SPIDER 映射加 Tab(该映射就是为此预留的);CrawlView.vue 第 210 行的内联 spiders 数组同步补一条(后端离线时的兜底展示)。CrawlerComplianceTests 的断言。验证:cd XSearch && scrapy crawl <name> 本机跑通并检查 ES(curl "localhost:9200/<索引>/_search?q=...");python manage.py test search 全绿;登录采集管理页确认新爬虫出现在列表并能启动、状态/历史正常回写。
以"给文档加 lang(语种)字段"为例。
触点清单:crawler/pipelines.py(QuoteDocument 声明 + index_item 赋值);各爬虫 item(提供数据);search/api_views.py(读接口透传:_extract/_list_item);frontend/src/api.ts(DocItem interface)与展示组件。
步骤:
QuoteDocument 加字段声明(如 lang = Keyword())。注意分析器选择:参与全文检索的中文文本用 Text(analyzer="ik_max_word", search_analyzer=ik_search_analyzer),精确过滤/聚合维度用 Keyword。index_item() 里加 lang=item.get("lang"),并按需经 _to_int/_to_float 清洗。_news_item/_parse_item 等产出函数补键。QuoteDocument.init() 只在索引不存在时建 mapping,已存在的索引不会自动加字段(见《07-模块详解》2.4)。两种姿势二选一:开发期直接删索引重采(curl -XDELETE localhost:9200/quotes 后重跑爬虫);生产期对存量索引 PUT /quotes/_mapping 增量加字段(ES 支持加字段、不支持改类型)。_extract(搜索结果)与 _list_item(列表/详情)的返回 dict 各加一行;前端 DocItem 加可选字段,npm run build 验证类型。post_filter(保分面计数不失真,见 api_search 的 source/days 先例);排序加进 _SORTS 字典(missing: "_last" 兜底空值)。验证:删索引重采后 curl "localhost:9200/quotes/_mapping" 确认字段在场;跑一次搜索确认新字段出现在响应;python manage.py test search 全绿(TzAwareWriteTests 的源码扫描只管日期字段,新字段不受影响)。
以"按作者精确筛选(author 参数)"为例,这是对既有查询骨架的扩展,触点最少。
步骤:
api_search:读参数(request.GET.get("author", "").strip()),参照 source 的处理把它追加进 post_filters 列表——务必走 post_filter 而非 must:分面计数才能保持"其他筛选条件下"的口径,用户切换筛选不互相污染(api_search.py 285-289 行的注释是这条规则的原文)。_FACET_AGGS 加一个 terms 聚合(如 "facet_authors": {"terms": {"field": "author", "size": 20}}),_extract 里对应取桶。注意 author 是 Keyword 字段,terms 聚合天然可用。SearchView.vue 的 QUERY_KEYS 数组加 'author';serialize/路由 watch 的还原逻辑自动生效(这是 URL 状态机的好处:五个键变六个键只改一处常量);界面控件仿照来源分面的 el-tag 组。ApiSearchTests.test_ranking_source_includes_keyword,断言新参数落在 post_filter 且不影响 must 子句。验证:构造带/不带 author 的请求对比分面计数;粘贴带 author 的 URL 深链打开,确认状态还原。
以"新增 /about 页"为例。
步骤:
frontend/src/views/AboutView.vue,从 StatsView.vue(70 行,全站最薄)抄结构:<script setup lang="ts"> + onMounted 拉数据 + el-alert 就地报错。main.ts 路由表加一条:{ path: '/about', component: () => import('./views/AboutView.vue'), meta: { title: '关于', desc: '...' } }——必须保持懒加载(CI 体积守门就是防这里退化成静态 import);meta.title/desc 供 afterEach 维护标题与描述,管理性质页面加 noIndex: true。App.vue 的 el-menu 加 <el-menu-item index="/about">;管理页必须同时满足两个条件——路由加入 auth.ts 的 ADMIN_PATHS(守卫拦截 + 401 处置共用),菜单项加 v-if="isAdmin"。api.ts 加 interface 与 api.<method> 封装(走统一的 request<T>,401 处置/错误提取免费获得);不要在组件里裸写 fetch。DbAdminView 的处理方式;仓库附带 frontend/scripts/mobile-audit.py 可做粗检。验证:npm run build(vue-tsc + 体积守门双闸);dev 态 npm run dev 手测深链/刷新/前进后退(history 模式由 nginx try_files 兜底,生产需确认 nginx.conf 无需改动——静态路径天然被 SPA 兜底规则覆盖)。
以"新增 GET /api/tags/(高频标签统计)"为例。
步骤:
search/api_views.py 写视图函数:遵循文件内既有范式——@require_http_methods(["GET"]);ES 查询包 try-except,故障返回 _es_error(e)(结构化 503);响应用 JsonResponse,错误体 {error, code?}。@require_admin(写操作另加 @csrf_exempt,POST/PUT 皆然)。xsearch/urls.py 注册 path('api/tags/', api_tags, name="api-tags")——注意 Django 路由无方法回退匹配,若同路径多方法必须单视图内部分流(api_db_doc 的注释解释过这个坑)。api.ts 加封装与 interface;ServiceInfo(urls.py)的 endpoints 样例列表可顺手补一条(该列表本就不全,见《13-技术债务与演进建议》)。验证:python manage.py test search;curl 实测含错误路径(空参、ES 停机)。
比 SOP-1 轻一个量级——爬虫与管道全部复用,只扩配置。
步骤:
crawler/spiders/news_rss.py 的 FEEDS 字典加条目:key 即写入 ES 的 source(必须 news_ 前缀风格,前端 SOURCE_NAMES 映射与后端 NEWS_SOURCES 都依赖这个命名约定);label、url、follow_links(feed 无摘要需跟进文章页抓正文时为 True,并可能需要仿照 CNS_ARTICLE_RE 加站点专属的文章链接正则与正文解析分支)。api_views.NEWS_SOURCES 加同一个 key(test_dead_people_feed_removed_everywhere 断言爬虫 FEEDS 与后端 NEWS_SOURCES 集合一致,两处必须同步改,否则该测试红灯——这是刻意设计的一致性防漂移)。SOURCE_NAMES 映射(HomeView/SearchView/NewsView 等各自持有一份)加中文名,否则界面显示原始 key。验证:scrapy crawl news_rss -a sources=<新key> 单源试跑(sources 参数就是为此设计的);确认新闻页新 Tab/来源标签正常;停用源时参照 news_people 先例——后端常量、爬虫 FEEDS、前端映射三处同步移除并留注释,存量文档不删(可用数据管理页按来源清理)。
所有场景共享四条红线:第一,不动 FINGERPRINT_HOSTS 白名单与 ROBOTSTXT_OBEY,除非完成合规预检并同步更新 CrawlerComplianceTests;第二,不把任何新状态放进进程内单例(token/互斥/调度的前车之鉴——gunicorn 必须单 worker 的原因),新状态一律 ES/Redis/JSON 文件;第三,不改 _id 生成规则与 source 既有取值(幂等与来源分面全压在这两个键上,改了等于换库);第四,每个 SOP 收尾必须跑 python manage.py test search(110 个方法)与 npm run build(类型+体积双闸),并把新增行为补成测试——补测优先级参考《09-测试与质量保障》第七节的缺口排序。
六个 SOP 各有清单,但有一组横切问题是任何扩展开工前都值得过一遍的,逐条自问:
JOB_NAME_RE 的 basename + 正则 + 存在性三重校验)_display_date)?.env.example、README、《14-配置与环境详解》三处同步,并说明默认值与 fail-closed 属性).gitignore/.dockerignore 覆盖)这十条里最容易跳过也最不该跳过的是第 10 条。这个项目的每一处防御性代码背后都有一个"曾在生产坏过"的故事(《05-核心流程》末节),扩展代码默认没有故事——它的失败是安静的。把"坏掉时在哪被发现"想清楚并落实成一个具体的信号(日志行、告警键、测试断言),新代码才算真正接入了这套系统的免疫系统。