speccore 6.5.1 → 6.10.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/.agents/skills/spec-ask/SKILL.md +6 -0
- package/.agents/skills/spec-reindex/SKILL.md +40 -0
- package/.agents/skills/speccore-router/SKILL.md +9 -0
- package/dist/cli.js +9 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/about.d.ts.map +1 -1
- package/dist/commands/about.js +10 -3
- package/dist/commands/about.js.map +1 -1
- package/dist/commands/analyze.d.ts +2 -0
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +141 -0
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/change.d.ts.map +1 -1
- package/dist/commands/change.js +3 -0
- package/dist/commands/change.js.map +1 -1
- package/dist/commands/doc2spec.d.ts +1 -0
- package/dist/commands/doc2spec.d.ts.map +1 -1
- package/dist/commands/doc2spec.js +175 -0
- package/dist/commands/doc2spec.js.map +1 -1
- package/dist/commands/execute.d.ts.map +1 -1
- package/dist/commands/execute.js +20 -6
- package/dist/commands/execute.js.map +1 -1
- package/dist/commands/help.js +32 -5
- package/dist/commands/help.js.map +1 -1
- package/dist/commands/iteration/create.d.ts.map +1 -1
- package/dist/commands/iteration/create.js +38 -10
- package/dist/commands/iteration/create.js.map +1 -1
- package/dist/commands/iteration/split.d.ts.map +1 -1
- package/dist/commands/iteration/split.js +125 -9
- package/dist/commands/iteration/split.js.map +1 -1
- package/dist/commands/rag-index.d.ts +19 -0
- package/dist/commands/rag-index.d.ts.map +1 -0
- package/dist/commands/rag-index.js +272 -0
- package/dist/commands/rag-index.js.map +1 -0
- package/dist/commands/refresh.d.ts +21 -0
- package/dist/commands/refresh.d.ts.map +1 -0
- package/dist/commands/refresh.js +185 -0
- package/dist/commands/refresh.js.map +1 -0
- package/dist/commands/sync-global.js +41 -0
- package/dist/commands/sync-global.js.map +1 -1
- package/dist/core/ai-context-generator.d.ts +2 -0
- package/dist/core/ai-context-generator.d.ts.map +1 -1
- package/dist/core/ai-context-generator.js +8 -6
- package/dist/core/ai-context-generator.js.map +1 -1
- package/dist/core/analyze-engine.d.ts +19 -0
- package/dist/core/analyze-engine.d.ts.map +1 -1
- package/dist/core/analyze-engine.js +326 -9
- package/dist/core/analyze-engine.js.map +1 -1
- package/dist/core/ask-engine.d.ts.map +1 -1
- package/dist/core/ask-engine.js +113 -4
- package/dist/core/ask-engine.js.map +1 -1
- package/dist/core/ask-host-ai.d.ts.map +1 -1
- package/dist/core/ask-host-ai.js +9 -6
- package/dist/core/ask-host-ai.js.map +1 -1
- package/dist/core/code-scanner.d.ts +24 -2
- package/dist/core/code-scanner.d.ts.map +1 -1
- package/dist/core/code-scanner.js +235 -5
- package/dist/core/code-scanner.js.map +1 -1
- package/dist/core/context-builder.d.ts.map +1 -1
- package/dist/core/context-builder.js +29 -7
- package/dist/core/context-builder.js.map +1 -1
- package/dist/core/decay-detector.d.ts +2 -2
- package/dist/core/decay-detector.d.ts.map +1 -1
- package/dist/core/decay-detector.js +149 -8
- package/dist/core/decay-detector.js.map +1 -1
- package/dist/core/git-integration.d.ts +50 -2
- package/dist/core/git-integration.d.ts.map +1 -1
- package/dist/core/git-integration.js +217 -27
- package/dist/core/git-integration.js.map +1 -1
- package/dist/core/global-knowledge.d.ts +25 -0
- package/dist/core/global-knowledge.d.ts.map +1 -0
- package/dist/core/global-knowledge.js +228 -0
- package/dist/core/global-knowledge.js.map +1 -0
- package/dist/core/index-guard.d.ts +57 -0
- package/dist/core/index-guard.d.ts.map +1 -0
- package/dist/core/index-guard.js +202 -0
- package/dist/core/index-guard.js.map +1 -0
- package/dist/core/intent-cache.d.ts +3 -1
- package/dist/core/intent-cache.d.ts.map +1 -1
- package/dist/core/intent-cache.js +33 -3
- package/dist/core/intent-cache.js.map +1 -1
- package/dist/core/knowledge-graph.d.ts +7 -1
- package/dist/core/knowledge-graph.d.ts.map +1 -1
- package/dist/core/knowledge-graph.js +265 -32
- package/dist/core/knowledge-graph.js.map +1 -1
- package/dist/core/platform-registry.d.ts +7 -2
- package/dist/core/platform-registry.d.ts.map +1 -1
- package/dist/core/platform-registry.js +57 -4
- package/dist/core/platform-registry.js.map +1 -1
- package/dist/core/prompt-builder.d.ts +3 -2
- package/dist/core/prompt-builder.d.ts.map +1 -1
- package/dist/core/prompt-builder.js +231 -71
- package/dist/core/prompt-builder.js.map +1 -1
- package/dist/core/rag-engine.d.ts +152 -0
- package/dist/core/rag-engine.d.ts.map +1 -0
- package/dist/core/rag-engine.js +816 -0
- package/dist/core/rag-engine.js.map +1 -0
- package/dist/core/reindex-engine.js +5 -5
- package/dist/core/reindex-engine.js.map +1 -1
- package/dist/core/task-paths.d.ts +2 -2
- package/dist/core/task-paths.d.ts.map +1 -1
- package/dist/core/task-paths.js +6 -4
- package/dist/core/task-paths.js.map +1 -1
- package/dist/core/unified-retrieval.d.ts +140 -0
- package/dist/core/unified-retrieval.d.ts.map +1 -0
- package/dist/core/unified-retrieval.js +466 -0
- package/dist/core/unified-retrieval.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* RAG Engine — 轻量级检索增强生成
|
|
4
|
+
*
|
|
5
|
+
* 设计目标:不引入向量数据库,用规则分块 + 关键词匹配实现文档检索
|
|
6
|
+
* 适用场景:SpecCore CLI 在构建 Prompt 时,从长文档中提取最相关的片段
|
|
7
|
+
*
|
|
8
|
+
* 流程:
|
|
9
|
+
* 分析阶段: 读取文档 → 按标题分块 → 提取摘要/关键词 → 存入 rag-index.json
|
|
10
|
+
* 执行阶段: 根据 task/需求关键词 → 检索相关块 → 按分数排序 → 组装进 Prompt
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.chunkByHeaders = chunkByHeaders;
|
|
14
|
+
exports.buildRagIndex = buildRagIndex;
|
|
15
|
+
exports.getRagIndexPath = getRagIndexPath;
|
|
16
|
+
exports.saveRagIndex = saveRagIndex;
|
|
17
|
+
exports.loadRagIndex = loadRagIndex;
|
|
18
|
+
exports.isRagIndexStale = isRagIndexStale;
|
|
19
|
+
exports.retrieveRelevantChunks = retrieveRelevantChunks;
|
|
20
|
+
exports.assembleChunksForPrompt = assembleChunksForPrompt;
|
|
21
|
+
exports.getFileSummaries = getFileSummaries;
|
|
22
|
+
exports.indexTaskDocuments = indexTaskDocuments;
|
|
23
|
+
exports.checkRagIndexFreshness = checkRagIndexFreshness;
|
|
24
|
+
exports.indexDirectoryDocuments = indexDirectoryDocuments;
|
|
25
|
+
exports.refreshRagIndex = refreshRagIndex;
|
|
26
|
+
exports.readChunkPrecise = readChunkPrecise;
|
|
27
|
+
exports.readChunksPrecise = readChunksPrecise;
|
|
28
|
+
const fs_extra_1 = require("fs-extra");
|
|
29
|
+
const path_1 = require("path");
|
|
30
|
+
const crypto_1 = require("crypto");
|
|
31
|
+
// ═══════════════════════════════════════════════════════════
|
|
32
|
+
// 常量
|
|
33
|
+
// ═══════════════════════════════════════════════════════════
|
|
34
|
+
const RAG_INDEX_PATH = (0, path_1.join)('.speccore', 'cache', 'rag-index.json');
|
|
35
|
+
const RAG_VERSION = '1.0';
|
|
36
|
+
/** 停用词 */
|
|
37
|
+
const STOP_WORDS = new Set([
|
|
38
|
+
'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'had', 'her', 'was', 'one', 'our', 'out', 'day', 'get', 'has', 'him', 'his', 'how', 'man', 'new', 'now', 'old', 'see', 'two', 'way', 'who', 'boy', 'did', 'its', 'let', 'put', 'say', 'she', 'too', 'use',
|
|
39
|
+
'功能', '实现', '需要', '进行', '根据', '通过', '可以', '如下', '包括', '相关', '对应', '采用', '基于', '完成', '确保', '提供', '支持', '包含', '用于', '其中', '以下', '上述', '所示', '为例',
|
|
40
|
+
]);
|
|
41
|
+
/** 语义映射(轻量版,可扩展) */
|
|
42
|
+
/**
|
|
43
|
+
* 语义同义词映射表 — 轻量级语义扩展
|
|
44
|
+
*
|
|
45
|
+
* 设计原则:
|
|
46
|
+
* - 覆盖常见业务领域(电商、金融、社交、企业应用)
|
|
47
|
+
* - 中英双语映射,支持跨语言检索
|
|
48
|
+
* - 每个词条 3-6 个同义词,避免过度扩展导致噪声
|
|
49
|
+
* - 反向查找自动建立(expandKeywords 中已实现)
|
|
50
|
+
*
|
|
51
|
+
* 维护建议:项目初始化后可按业务领域补充行业术语
|
|
52
|
+
*/
|
|
53
|
+
const SEMANTIC_MAP = {
|
|
54
|
+
// ═══════════════════════════════════════════════════════════
|
|
55
|
+
// 1. 用户与认证 (14组)
|
|
56
|
+
// ═══════════════════════════════════════════════════════════
|
|
57
|
+
login: ['auth', 'session', 'token', 'signin', 'authenticate', 'sso'],
|
|
58
|
+
auth: ['login', 'session', 'token', 'permission', 'authenticate', 'authorization'],
|
|
59
|
+
permission: ['rbac', 'acl', 'role', 'authorize', 'access', 'privilege'],
|
|
60
|
+
rbac: ['permission', 'role', 'access', 'authorize', 'acl'],
|
|
61
|
+
user: ['account', 'member', 'profile', 'person', 'customer', 'client'],
|
|
62
|
+
account: ['user', 'member', 'profile', 'registration', 'signup'],
|
|
63
|
+
register: ['signup', 'enrollment', 'createAccount', 'join'],
|
|
64
|
+
logout: ['signout', 'exit', 'quit', 'sessionEnd'],
|
|
65
|
+
password: ['credential', 'passphrase', 'pwd', 'secret'],
|
|
66
|
+
session: ['token', 'cookie', 'jwt', 'state', 'connection'],
|
|
67
|
+
jwt: ['token', 'session', 'auth', 'bearer', 'claim'],
|
|
68
|
+
sso: ['singleSignOn', 'federated', 'oauth', 'ldap', 'cas'],
|
|
69
|
+
oauth: ['sso', 'openid', 'authorization', 'token', 'grant'],
|
|
70
|
+
captcha: ['verification', 'challenge', 'validateCode'],
|
|
71
|
+
// ═══════════════════════════════════════════════════════════
|
|
72
|
+
// 2. 电商与交易 (18组)
|
|
73
|
+
// ═══════════════════════════════════════════════════════════
|
|
74
|
+
order: ['purchase', 'transaction', 'payment', 'buy', 'checkout', 'cart'],
|
|
75
|
+
payment: ['pay', 'order', 'transaction', 'checkout', 'billing', 'settlement'],
|
|
76
|
+
cart: ['basket', 'trolley', 'shoppingCart', 'itemList'],
|
|
77
|
+
checkout: ['payment', 'order', 'settlement', 'pay', 'purchase'],
|
|
78
|
+
refund: ['return', 'reimbursement', 'cashback', 'chargeback'],
|
|
79
|
+
invoice: ['receipt', 'bill', 'ticket', 'statement'],
|
|
80
|
+
coupon: ['voucher', 'discount', 'promo', 'code'],
|
|
81
|
+
discount: ['coupon', 'voucher', 'promo', 'reduction', 'sale'],
|
|
82
|
+
promotion: ['campaign', 'marketing', 'sale', 'activity'],
|
|
83
|
+
price: ['cost', 'amount', 'fee', 'charge', 'rate'],
|
|
84
|
+
sku: ['stockKeepingUnit', 'item', 'product', 'variant', 'spec'],
|
|
85
|
+
inventory: ['stock', 'warehouse', 'storage', 'reserve'],
|
|
86
|
+
stock: ['inventory', 'warehouse', 'storage', 'supply'],
|
|
87
|
+
shipping: ['delivery', 'logistics', 'transport', 'freight'],
|
|
88
|
+
delivery: ['shipping', 'logistics', 'transport', 'courier'],
|
|
89
|
+
warehouse: ['storage', 'depot', 'inventory', 'stock'],
|
|
90
|
+
supplier: ['vendor', 'provider', 'merchant', 'seller'],
|
|
91
|
+
merchant: ['seller', 'vendor', 'shop', 'store', 'retailer'],
|
|
92
|
+
// ═══════════════════════════════════════════════════════════
|
|
93
|
+
// 3. 商品与内容 (12组)
|
|
94
|
+
// ═══════════════════════════════════════════════════════════
|
|
95
|
+
product: ['item', 'goods', 'merchandise', 'sku', 'commodity'],
|
|
96
|
+
category: ['classification', 'type', 'group', 'taxonomy'],
|
|
97
|
+
brand: ['trademark', 'label', 'make', 'manufacturer'],
|
|
98
|
+
review: ['comment', 'rating', 'feedback', 'evaluation'],
|
|
99
|
+
search: ['query', 'lookup', 'find', 'retrieve', 'discover'],
|
|
100
|
+
filter: ['sort', 'condition', 'criteria', 'refine', 'screen'],
|
|
101
|
+
recommend: ['suggest', 'propose', 'advise', 'personalized'],
|
|
102
|
+
favorite: ['bookmark', 'like', 'wishlist', 'collect'],
|
|
103
|
+
bookmark: ['favorite', 'save', 'mark'],
|
|
104
|
+
tag: ['label', 'keyword', 'mark', 'category'],
|
|
105
|
+
catalog: ['directory', 'listing', 'index', 'inventory'],
|
|
106
|
+
content: ['article', 'post', 'material', 'document', 'media'],
|
|
107
|
+
// ═══════════════════════════════════════════════════════════
|
|
108
|
+
// 4. 通知与消息 (8组)
|
|
109
|
+
// ═══════════════════════════════════════════════════════════
|
|
110
|
+
notification: ['alert', 'reminder', 'message', 'notice', 'push'],
|
|
111
|
+
message: ['msg', 'notification', 'email', 'sms', 'chat'],
|
|
112
|
+
email: ['mail', 'message', 'newsletter', 'electronicMail'],
|
|
113
|
+
sms: ['message', 'text', 'mobile', 'shortMessage'],
|
|
114
|
+
push: ['notification', 'alert', 'broadcast', 'realtime'],
|
|
115
|
+
subscribe: ['follow', 'register', 'enrollment'],
|
|
116
|
+
unsubscribe: ['cancel', 'optout'],
|
|
117
|
+
template: ['pattern', 'model', 'format', 'layout'],
|
|
118
|
+
// ═══════════════════════════════════════════════════════════
|
|
119
|
+
// 5. 数据与分析 (10组)
|
|
120
|
+
// ═══════════════════════════════════════════════════════════
|
|
121
|
+
analytics: ['analysis', 'statistics', 'metrics', 'reporting'],
|
|
122
|
+
metric: ['indicator', 'kpi', 'measurement', 'stat'],
|
|
123
|
+
report: ['dashboard', 'summary', 'analytics', 'chart'],
|
|
124
|
+
dashboard: ['panel', 'overview', 'board', 'console'],
|
|
125
|
+
chart: ['graph', 'diagram', 'visualization', 'plot'],
|
|
126
|
+
export: ['download', 'output', 'backup', 'extract'],
|
|
127
|
+
import: ['upload', 'input', 'load', 'ingest'],
|
|
128
|
+
sync: ['synchronize', 'replicate', 'refresh', 'update'],
|
|
129
|
+
backup: ['snapshot', 'copy', 'archive', 'restore'],
|
|
130
|
+
cache: ['buffer', 'store', 'memo', 'redis'],
|
|
131
|
+
// ═══════════════════════════════════════════════════════════
|
|
132
|
+
// 6. 系统与架构 (16组)
|
|
133
|
+
// ═══════════════════════════════════════════════════════════
|
|
134
|
+
api: ['interface', 'endpoint', 'rest', 'graphql', 'contract'],
|
|
135
|
+
endpoint: ['api', 'route', 'url', 'path'],
|
|
136
|
+
service: ['microservice', 'module', 'component', 'business'],
|
|
137
|
+
module: ['component', 'package', 'library', 'plugin'],
|
|
138
|
+
component: ['widget', 'element', 'part', 'module'],
|
|
139
|
+
config: ['configuration', 'setting', 'preference', 'option'],
|
|
140
|
+
deploy: ['release', 'publish', 'rollout', 'ship'],
|
|
141
|
+
release: ['deploy', 'version', 'publish', 'launch'],
|
|
142
|
+
rollback: ['revert', 'undo', 'fallback', 'restore'],
|
|
143
|
+
environment: ['env', 'stage', 'context', 'runtime'],
|
|
144
|
+
production: ['prod', 'live', 'online'],
|
|
145
|
+
staging: ['test', 'preview', 'uat', 'preprod'],
|
|
146
|
+
monitor: ['observe', 'track', 'watch', 'supervise'],
|
|
147
|
+
log: ['record', 'trace', 'audit', 'journal'],
|
|
148
|
+
error: ['exception', 'fault', 'bug', 'failure', 'issue'],
|
|
149
|
+
exception: ['error', 'fault', 'throw', 'catch'],
|
|
150
|
+
// ═══════════════════════════════════════════════════════════
|
|
151
|
+
// 7. 任务与调度 (6组)
|
|
152
|
+
// ═══════════════════════════════════════════════════════════
|
|
153
|
+
retry: ['attempt', 'replay', 'resend'],
|
|
154
|
+
timeout: ['deadline', 'expiration', 'limit'],
|
|
155
|
+
queue: ['buffer', 'pipeline', 'backlog'],
|
|
156
|
+
schedule: ['cron', 'timer', 'job', 'task'],
|
|
157
|
+
job: ['task', 'cron', 'worker', 'batch'],
|
|
158
|
+
worker: ['processor', 'handler', 'executor', 'consumer'],
|
|
159
|
+
// ═══════════════════════════════════════════════════════════
|
|
160
|
+
// 8. 安全与合规 (6组)
|
|
161
|
+
// ═══════════════════════════════════════════════════════════
|
|
162
|
+
security: ['safety', 'protection', 'defense', 'secure'],
|
|
163
|
+
encrypt: ['encode', 'cipher', 'crypt'],
|
|
164
|
+
decrypt: ['decode', 'decipher'],
|
|
165
|
+
hash: ['digest', 'checksum', 'fingerprint'],
|
|
166
|
+
signature: ['sign', 'sig', 'verification'],
|
|
167
|
+
verify: ['validate', 'confirm', 'check', 'authenticate'],
|
|
168
|
+
validate: ['verify', 'check', 'confirm'],
|
|
169
|
+
audit: ['review', 'inspect', 'examine'],
|
|
170
|
+
compliance: ['regulation', 'policy', 'standard'],
|
|
171
|
+
privacy: ['confidentiality', 'dataProtection', 'gdpr'],
|
|
172
|
+
// ═══════════════════════════════════════════════════════════
|
|
173
|
+
// 9. 前端与UI (8组)
|
|
174
|
+
// ═══════════════════════════════════════════════════════════
|
|
175
|
+
page: ['screen', 'view', 'route', 'interface'],
|
|
176
|
+
form: ['input', 'field', 'entry'],
|
|
177
|
+
table: ['grid', 'list', 'datatable'],
|
|
178
|
+
modal: ['dialog', 'popup', 'overlay'],
|
|
179
|
+
toast: ['notification', 'snackbar', 'message'],
|
|
180
|
+
menu: ['navigation', 'nav', 'sidebar'],
|
|
181
|
+
theme: ['style', 'skin', 'appearance'],
|
|
182
|
+
responsive: ['adaptive', 'mobile', 'fluid'],
|
|
183
|
+
// ═══════════════════════════════════════════════════════════
|
|
184
|
+
// 10. 中文业务术语 (16组)
|
|
185
|
+
// ═══════════════════════════════════════════════════════════
|
|
186
|
+
登录: ['认证', '鉴权', '会话', 'token', '密码', '登陆'],
|
|
187
|
+
注册: ['开户', '创建账号', '新用户', '加入', 'signup'],
|
|
188
|
+
权限: ['RBAC', '角色', '访问控制', '授权', '特权'],
|
|
189
|
+
用户: ['客户', '会员', '账号', '账户', '使用者'],
|
|
190
|
+
密码: ['口令', '凭证', '密钥', '密文'],
|
|
191
|
+
订单: ['购买', '交易', '支付', '下单', '采购'],
|
|
192
|
+
支付: ['付款', '结算', '收银台', '扣款', 'billing'],
|
|
193
|
+
购物车: ['购物篮', '采购车', '商品列表'],
|
|
194
|
+
退款: ['退货', '返现', '撤销', '冲正'],
|
|
195
|
+
优惠券: ['代金券', '折扣码', '促销码', '红包'],
|
|
196
|
+
库存: ['存货', '仓储', '备货', '储备'],
|
|
197
|
+
物流: ['配送', '快递', '运输', '发货'],
|
|
198
|
+
价格: ['金额', '费用', '单价', '定价', '报价'],
|
|
199
|
+
商品: ['产品', '货品', 'SKU', '单品'],
|
|
200
|
+
分类: ['类目', '类别', '类型', '分组'],
|
|
201
|
+
搜索: ['查询', '查找', '检索', '发现'],
|
|
202
|
+
推荐: ['建议', '个性化', '猜你喜欢', '智能推荐'],
|
|
203
|
+
评价: ['评论', '评分', '反馈', '口碑'],
|
|
204
|
+
通知: ['提醒', '告警', '消息', '公告', '推送'],
|
|
205
|
+
消息: ['信息', '通知', '邮件', '短信', '站内信'],
|
|
206
|
+
邮件: ['email', '信箱', '电子信'],
|
|
207
|
+
短信: ['SMS', '短消息', '文本消息'],
|
|
208
|
+
数据: ['信息', '资料', '数值', '记录'],
|
|
209
|
+
分析: ['统计', '报表', '洞察', '解析'],
|
|
210
|
+
报表: ['报告', '看板', '图表', '统计表'],
|
|
211
|
+
导出: ['下载', '输出', '备份', '提取'],
|
|
212
|
+
导入: ['上传', '输入', '加载', '录入'],
|
|
213
|
+
同步: ['一致', '复制', '刷新', '更新'],
|
|
214
|
+
接口: ['API', '端点', '路由', '契约'],
|
|
215
|
+
服务: ['微服务', '模块', '组件', '业务'],
|
|
216
|
+
部署: ['发布', '上线', '投产', '发版'],
|
|
217
|
+
环境: ['运行环境', '上下文', '阶段', '场景'],
|
|
218
|
+
监控: ['观测', '追踪', '告警', '监管'],
|
|
219
|
+
日志: ['记录', '追踪', '审计', '日记'],
|
|
220
|
+
错误: ['异常', '故障', '缺陷', '问题'],
|
|
221
|
+
队列: ['缓冲', '管道', '任务队列', '消息队列'],
|
|
222
|
+
调度: ['定时', '计划', '任务', '排程'],
|
|
223
|
+
安全: ['防护', '保护', '加密', '防御'],
|
|
224
|
+
加密: ['编码', '混淆', '密文', '加密算法'],
|
|
225
|
+
验证: ['校验', '确认', '核实', '认证'],
|
|
226
|
+
审计: ['审查', '检查', '日志审计', '合规检查'],
|
|
227
|
+
页面: ['视图', '路由', '屏幕', '界面'],
|
|
228
|
+
组件: ['元件', '控件', '部件', '模块'],
|
|
229
|
+
表单: ['输入框', '字段', '录入'],
|
|
230
|
+
弹窗: ['对话框', '浮层', '模态框'],
|
|
231
|
+
};
|
|
232
|
+
// ═══════════════════════════════════════════════════════════
|
|
233
|
+
// 1. 文档分块(按 Markdown 标题)
|
|
234
|
+
// ═══════════════════════════════════════════════════════════
|
|
235
|
+
/**
|
|
236
|
+
* 按 Markdown 标题层级分块
|
|
237
|
+
* 规则:## / ### / #### 作为分块边界,一级标题 # 作为文档开头(不切分)
|
|
238
|
+
*/
|
|
239
|
+
function chunkByHeaders(content, filePath) {
|
|
240
|
+
const lines = content.split('\n');
|
|
241
|
+
const chunks = [];
|
|
242
|
+
let current = null;
|
|
243
|
+
const fileName = (0, path_1.basename)(filePath);
|
|
244
|
+
// 文档开头(# 标题之前的内容)作为第一个隐式块
|
|
245
|
+
let preambleLines = [];
|
|
246
|
+
let foundFirstHeader = false;
|
|
247
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
248
|
+
const line = lines[lineIdx];
|
|
249
|
+
const match = line.match(/^(#{2,4})\s+(.+)$/);
|
|
250
|
+
if (match) {
|
|
251
|
+
foundFirstHeader = true;
|
|
252
|
+
// 保存上一个块
|
|
253
|
+
if (current) {
|
|
254
|
+
chunks.push(finalizeChunk(current, filePath, fileName));
|
|
255
|
+
}
|
|
256
|
+
else if (preambleLines.length > 0) {
|
|
257
|
+
// 保存序言块
|
|
258
|
+
const preamble = preambleLines.join('\n').trim();
|
|
259
|
+
if (preamble.length > 20) {
|
|
260
|
+
chunks.push(createChunk(filePath, fileName, '文档概述', 1, preamble, undefined, undefined, 1, lineIdx));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// 开始新块(记录起始行号,1-based)
|
|
264
|
+
current = { title: match[2].trim(), level: match[1].length, lines: [], startLine: lineIdx + 1 };
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
if (!foundFirstHeader) {
|
|
268
|
+
preambleLines.push(line);
|
|
269
|
+
}
|
|
270
|
+
else if (current) {
|
|
271
|
+
current.lines.push(line);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// 保存最后一个块
|
|
276
|
+
if (current) {
|
|
277
|
+
chunks.push(finalizeChunk(current, filePath, fileName));
|
|
278
|
+
}
|
|
279
|
+
return chunks;
|
|
280
|
+
}
|
|
281
|
+
function finalizeChunk(current, filePath, fileName) {
|
|
282
|
+
const content = current.lines.join('\n').trim();
|
|
283
|
+
const summary = extractSummary(content);
|
|
284
|
+
const keywords = extractKeywords(current.title + ' ' + content);
|
|
285
|
+
// endLine = startLine + headerLine(1) + contentLines
|
|
286
|
+
const endLine = current.startLine + current.lines.length;
|
|
287
|
+
return createChunk(filePath, fileName, current.title, current.level, content, summary, keywords, current.startLine, endLine);
|
|
288
|
+
}
|
|
289
|
+
function createChunk(filePath, fileName, title, level, content, summary, keywords, startLine, endLine) {
|
|
290
|
+
const id = (0, crypto_1.createHash)('md5').update(`${filePath}::${title}`).digest('hex').slice(0, 12);
|
|
291
|
+
return {
|
|
292
|
+
id,
|
|
293
|
+
filePath,
|
|
294
|
+
fileName,
|
|
295
|
+
title,
|
|
296
|
+
level,
|
|
297
|
+
content,
|
|
298
|
+
summary: summary || extractSummary(content),
|
|
299
|
+
keywords: keywords || extractKeywords(title + ' ' + content),
|
|
300
|
+
charCount: content.length,
|
|
301
|
+
startLine,
|
|
302
|
+
endLine,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
// ═══════════════════════════════════════════════════════════
|
|
306
|
+
// 2. 摘要提取(规则驱动)
|
|
307
|
+
// ═══════════════════════════════════════════════════════════
|
|
308
|
+
/**
|
|
309
|
+
* 从块内容中提取结构化摘要
|
|
310
|
+
* - 表格 → 提取表头 + 前 3 行
|
|
311
|
+
* - 列表 → 提取前 5 项
|
|
312
|
+
* - 段落 → 提取前 2 句
|
|
313
|
+
* - 代码块 → 提取第一行注释或函数签名
|
|
314
|
+
*/
|
|
315
|
+
function extractSummary(content) {
|
|
316
|
+
const trimmed = content.trim();
|
|
317
|
+
if (trimmed.length === 0)
|
|
318
|
+
return '';
|
|
319
|
+
if (trimmed.length < 150)
|
|
320
|
+
return trimmed; // 短内容直接保留
|
|
321
|
+
// 尝试提取表格(用 RegExp 构造函数避免换行符解析问题)
|
|
322
|
+
const tablePattern = new RegExp('(\\|[^\\n]+\\|\\n\\|[-:| ]+\\|\\n(?:\\|[^\\n]+\\|\\n?){1,3})');
|
|
323
|
+
const tableMatch = trimmed.match(tablePattern);
|
|
324
|
+
if (tableMatch) {
|
|
325
|
+
return `【表格】\n${tableMatch[0]}`;
|
|
326
|
+
}
|
|
327
|
+
// 尝试提取列表
|
|
328
|
+
const listMatch = trimmed.match(/^([-*]\s+.+\n?){1,5}/m);
|
|
329
|
+
if (listMatch) {
|
|
330
|
+
return `【要点】\n${listMatch[0].trim()}`;
|
|
331
|
+
}
|
|
332
|
+
// 尝试提取代码块签名
|
|
333
|
+
const codeMatch = trimmed.match(/```\w*\n([^\n]+)/);
|
|
334
|
+
if (codeMatch) {
|
|
335
|
+
return `【代码】${codeMatch[1].slice(0, 80)}`;
|
|
336
|
+
}
|
|
337
|
+
// 默认:提取前 2 句(中文按句号,英文按句点+空格)
|
|
338
|
+
const sentences = trimmed.split(/(?<=[。!?.!?])\s+/);
|
|
339
|
+
if (sentences.length >= 2) {
|
|
340
|
+
return sentences.slice(0, 2).join(' ').slice(0, 200);
|
|
341
|
+
}
|
|
342
|
+
return trimmed.slice(0, 200);
|
|
343
|
+
}
|
|
344
|
+
// ═══════════════════════════════════════════════════════════
|
|
345
|
+
// 3. 关键词提取(复用 code-scanner 逻辑)
|
|
346
|
+
// ═══════════════════════════════════════════════════════════
|
|
347
|
+
function extractKeywords(text) {
|
|
348
|
+
const keywords = [];
|
|
349
|
+
// 中文关键词(2-4 字)
|
|
350
|
+
const cn = text.match(/[\u4e00-\u9fa5]{2,4}/g) || [];
|
|
351
|
+
keywords.push(...cn);
|
|
352
|
+
// 英文标识符(3+ 字母)
|
|
353
|
+
const en = text.match(/\b[a-zA-Z]{3,}\b/g) || [];
|
|
354
|
+
keywords.push(...en);
|
|
355
|
+
// CamelCase / PascalCase 拆分
|
|
356
|
+
const camel = text.match(/\b[a-z]+[A-Z][a-zA-Z]+\b/g) || [];
|
|
357
|
+
for (const c of camel) {
|
|
358
|
+
const parts = c.split(/(?=[A-Z])/);
|
|
359
|
+
keywords.push(...parts.filter(p => p.length >= 3));
|
|
360
|
+
}
|
|
361
|
+
// 去重 + 停用词过滤
|
|
362
|
+
const filtered = [...new Set(keywords)].filter(k => !STOP_WORDS.has(k.toLowerCase()));
|
|
363
|
+
// 语义扩展
|
|
364
|
+
const expanded = expandKeywords(filtered);
|
|
365
|
+
return expanded.slice(0, 15);
|
|
366
|
+
}
|
|
367
|
+
function expandKeywords(keywords) {
|
|
368
|
+
const expanded = new Set(keywords);
|
|
369
|
+
for (const kw of keywords) {
|
|
370
|
+
const lower = kw.toLowerCase();
|
|
371
|
+
if (SEMANTIC_MAP[lower]) {
|
|
372
|
+
for (const syn of SEMANTIC_MAP[lower])
|
|
373
|
+
expanded.add(syn);
|
|
374
|
+
}
|
|
375
|
+
// 反向查找
|
|
376
|
+
for (const [key, syns] of Object.entries(SEMANTIC_MAP)) {
|
|
377
|
+
if (syns.includes(lower) && !expanded.has(key))
|
|
378
|
+
expanded.add(key);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return [...expanded];
|
|
382
|
+
}
|
|
383
|
+
// ═══════════════════════════════════════════════════════════
|
|
384
|
+
// 4. 索引构建与存取
|
|
385
|
+
// ═══════════════════════════════════════════════════════════
|
|
386
|
+
/**
|
|
387
|
+
* 为多个文档构建 RAG 索引
|
|
388
|
+
*/
|
|
389
|
+
async function buildRagIndex(documents, scope) {
|
|
390
|
+
const chunks = [];
|
|
391
|
+
const fileSummaries = {};
|
|
392
|
+
const fileMtimes = {};
|
|
393
|
+
for (const doc of documents) {
|
|
394
|
+
const docChunks = chunkByHeaders(doc.content, doc.filePath);
|
|
395
|
+
chunks.push(...docChunks);
|
|
396
|
+
// 文件级摘要:取前 3 个最高级别块的摘要
|
|
397
|
+
const topLevelChunks = docChunks
|
|
398
|
+
.filter(c => c.level <= 3)
|
|
399
|
+
.sort((a, b) => a.level - b.level || b.charCount - a.charCount)
|
|
400
|
+
.slice(0, 3);
|
|
401
|
+
fileSummaries[doc.filePath] = topLevelChunks.map(c => `• ${c.title}: ${c.summary.slice(0, 80)}`).join('\n');
|
|
402
|
+
// 记录文件修改时间
|
|
403
|
+
if (doc.mtime) {
|
|
404
|
+
fileMtimes[doc.filePath] = doc.mtime;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
version: RAG_VERSION,
|
|
409
|
+
updatedAt: new Date().toISOString(),
|
|
410
|
+
scope,
|
|
411
|
+
chunks,
|
|
412
|
+
fileSummaries,
|
|
413
|
+
fileMtimes,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* 获取 RAG 索引文件路径
|
|
418
|
+
* 支持按 scope 分文件存储,避免 task/iteration/global 互相覆盖
|
|
419
|
+
*/
|
|
420
|
+
function getRagIndexPath(cwd, fileName = 'rag-index.json') {
|
|
421
|
+
return (0, path_1.join)(cwd, '.speccore', 'cache', fileName);
|
|
422
|
+
}
|
|
423
|
+
async function saveRagIndex(cwd, index, fileName) {
|
|
424
|
+
const filePath = getRagIndexPath(cwd, fileName);
|
|
425
|
+
await (0, fs_extra_1.ensureDir)((0, path_1.join)(cwd, '.speccore', 'cache'));
|
|
426
|
+
await (0, fs_extra_1.writeFile)(filePath, JSON.stringify(index, null, 2));
|
|
427
|
+
}
|
|
428
|
+
async function loadRagIndex(cwd, fileName) {
|
|
429
|
+
const filePath = getRagIndexPath(cwd, fileName);
|
|
430
|
+
if (!(await (0, fs_extra_1.pathExists)(filePath)))
|
|
431
|
+
return null;
|
|
432
|
+
try {
|
|
433
|
+
const content = await (0, fs_extra_1.readFile)(filePath, 'utf-8');
|
|
434
|
+
return JSON.parse(content);
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* 检查索引是否匹配当前 scope(迭代/任务变更后需重建)
|
|
442
|
+
*/
|
|
443
|
+
function isRagIndexStale(index, currentScope) {
|
|
444
|
+
return index.scope !== currentScope;
|
|
445
|
+
}
|
|
446
|
+
// ═══════════════════════════════════════════════════════════
|
|
447
|
+
// 5. 相关性检索
|
|
448
|
+
// ═══════════════════════════════════════════════════════════
|
|
449
|
+
/**
|
|
450
|
+
* 根据查询语句检索最相关的文档块
|
|
451
|
+
*
|
|
452
|
+
* 评分逻辑:
|
|
453
|
+
* - 标题关键词命中:+3 分/词
|
|
454
|
+
* - 内容关键词命中:+1 分/词
|
|
455
|
+
* - 摘要关键词命中:+2 分/词
|
|
456
|
+
* - 高级别标题(##)bonus:+0.5 分
|
|
457
|
+
* - 文件路径关键词命中:+1 分/词
|
|
458
|
+
*/
|
|
459
|
+
function retrieveRelevantChunks(index, options) {
|
|
460
|
+
const { query, topK = 5, minScore = 0.5, maxChunkChars = 1500, maxTotalChars = 6000, platforms } = options;
|
|
461
|
+
const queryKeywords = extractKeywords(query);
|
|
462
|
+
// 按端过滤:如果指定了 platforms,只保留路径中包含对应端名的 chunk
|
|
463
|
+
const platformFilter = (c) => {
|
|
464
|
+
if (!platforms || platforms.length === 0)
|
|
465
|
+
return true;
|
|
466
|
+
const fp = c.filePath.toLowerCase();
|
|
467
|
+
return platforms.some(p => fp.includes(p.toLowerCase()));
|
|
468
|
+
};
|
|
469
|
+
if (queryKeywords.length === 0) {
|
|
470
|
+
// 无关键词时返回最高级别的块
|
|
471
|
+
return index.chunks
|
|
472
|
+
.filter(c => c.level <= 3)
|
|
473
|
+
.filter(platformFilter)
|
|
474
|
+
.sort((a, b) => a.level - b.level || b.charCount - a.charCount)
|
|
475
|
+
.slice(0, topK);
|
|
476
|
+
}
|
|
477
|
+
const scored = index.chunks.filter(platformFilter).map(chunk => {
|
|
478
|
+
let score = 0;
|
|
479
|
+
// 标题匹配(权重最高)
|
|
480
|
+
for (const kw of queryKeywords) {
|
|
481
|
+
const lowerKw = kw.toLowerCase();
|
|
482
|
+
if (chunk.title.toLowerCase().includes(lowerKw))
|
|
483
|
+
score += 3;
|
|
484
|
+
}
|
|
485
|
+
// 内容匹配
|
|
486
|
+
for (const kw of queryKeywords) {
|
|
487
|
+
const lowerKw = kw.toLowerCase();
|
|
488
|
+
if (chunk.content.toLowerCase().includes(lowerKw))
|
|
489
|
+
score += 1;
|
|
490
|
+
}
|
|
491
|
+
// 摘要匹配
|
|
492
|
+
for (const kw of queryKeywords) {
|
|
493
|
+
const lowerKw = kw.toLowerCase();
|
|
494
|
+
if (chunk.summary.toLowerCase().includes(lowerKw))
|
|
495
|
+
score += 2;
|
|
496
|
+
}
|
|
497
|
+
// 预提取关键词匹配
|
|
498
|
+
for (const kw of queryKeywords) {
|
|
499
|
+
const lowerKw = kw.toLowerCase();
|
|
500
|
+
if (chunk.keywords.some(k => k.toLowerCase() === lowerKw))
|
|
501
|
+
score += 2.5;
|
|
502
|
+
}
|
|
503
|
+
// 文件路径匹配
|
|
504
|
+
for (const kw of queryKeywords) {
|
|
505
|
+
const lowerKw = kw.toLowerCase();
|
|
506
|
+
if (chunk.filePath.toLowerCase().includes(lowerKw))
|
|
507
|
+
score += 1;
|
|
508
|
+
}
|
|
509
|
+
// 高级别标题 bonus
|
|
510
|
+
if (chunk.level === 2)
|
|
511
|
+
score += 0.5;
|
|
512
|
+
// 归一化到 0~1(假设最大可能分约 20)
|
|
513
|
+
const normalizedScore = Math.min(score / 10, 1);
|
|
514
|
+
return { ...chunk, relevanceScore: normalizedScore };
|
|
515
|
+
});
|
|
516
|
+
// 过滤 + 排序
|
|
517
|
+
const filtered = scored
|
|
518
|
+
.filter(c => (c.relevanceScore || 0) >= minScore)
|
|
519
|
+
.sort((a, b) => (b.relevanceScore || 0) - (a.relevanceScore || 0));
|
|
520
|
+
// 按 topK + maxTotalChars 截断
|
|
521
|
+
const result = [];
|
|
522
|
+
let totalChars = 0;
|
|
523
|
+
for (const chunk of filtered) {
|
|
524
|
+
if (result.length >= topK)
|
|
525
|
+
break;
|
|
526
|
+
const chunkText = formatChunkForPrompt(chunk, maxChunkChars);
|
|
527
|
+
if (totalChars + chunkText.length > maxTotalChars) {
|
|
528
|
+
// 尝试用更短的摘要版本
|
|
529
|
+
const slimText = formatChunkForPrompt({ ...chunk, content: chunk.summary }, maxChunkChars);
|
|
530
|
+
if (totalChars + slimText.length <= maxTotalChars) {
|
|
531
|
+
result.push({ ...chunk, content: chunk.summary });
|
|
532
|
+
totalChars += slimText.length;
|
|
533
|
+
}
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
result.push(chunk);
|
|
537
|
+
totalChars += chunkText.length;
|
|
538
|
+
}
|
|
539
|
+
return result;
|
|
540
|
+
}
|
|
541
|
+
// ═══════════════════════════════════════════════════════════
|
|
542
|
+
// 6. Prompt 组装
|
|
543
|
+
// ═══════════════════════════════════════════════════════════
|
|
544
|
+
/**
|
|
545
|
+
* 将 chunk 格式化为 Prompt 可用的文本
|
|
546
|
+
*/
|
|
547
|
+
function formatChunkForPrompt(chunk, maxChars) {
|
|
548
|
+
const content = chunk.content.length > maxChars
|
|
549
|
+
? chunk.content.slice(0, maxChars) + '\n> ... (已截断)'
|
|
550
|
+
: chunk.content;
|
|
551
|
+
return `### ${chunk.title}(${chunk.fileName})\n\n${content}\n\n`;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* 将检索结果组装为 extraSpecs 格式(兼容现有 prompt-builder 接口)
|
|
555
|
+
*/
|
|
556
|
+
function assembleChunksForPrompt(chunks, options) {
|
|
557
|
+
const maxChunk = options?.maxCharsPerChunk ?? 1500;
|
|
558
|
+
const maxTotal = options?.maxTotalChars ?? 6000;
|
|
559
|
+
const result = [];
|
|
560
|
+
let totalChars = 0;
|
|
561
|
+
for (const chunk of chunks) {
|
|
562
|
+
let content = chunk.content;
|
|
563
|
+
if (content.length > maxChunk) {
|
|
564
|
+
content = content.slice(0, maxChunk) + `\n\n> ... (已截断,原块 ${chunk.content.length} 字)`;
|
|
565
|
+
}
|
|
566
|
+
const text = `## ${chunk.title}\n\n${content}`;
|
|
567
|
+
if (totalChars + text.length > maxTotal && result.length > 0)
|
|
568
|
+
break;
|
|
569
|
+
result.push({
|
|
570
|
+
name: `${chunk.fileName} › ${chunk.title}`,
|
|
571
|
+
path: chunk.filePath,
|
|
572
|
+
content: text,
|
|
573
|
+
});
|
|
574
|
+
totalChars += text.length;
|
|
575
|
+
}
|
|
576
|
+
return result;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* 获取文件级摘要(用于快速了解文档全貌)
|
|
580
|
+
*/
|
|
581
|
+
function getFileSummaries(index) {
|
|
582
|
+
const lines = ['## 参考文档概览\n'];
|
|
583
|
+
for (const [filePath, summary] of Object.entries(index.fileSummaries)) {
|
|
584
|
+
lines.push(`**${(0, path_1.basename)(filePath)}**`);
|
|
585
|
+
lines.push(summary);
|
|
586
|
+
lines.push('');
|
|
587
|
+
}
|
|
588
|
+
return lines.join('\n');
|
|
589
|
+
}
|
|
590
|
+
// ═══════════════════════════════════════════════════════════
|
|
591
|
+
// 7. 便捷函数:一键为任务目录构建索引
|
|
592
|
+
// ═══════════════════════════════════════════════════════════
|
|
593
|
+
/**
|
|
594
|
+
* 扫描任务目录下的所有参考文档,构建 RAG 索引
|
|
595
|
+
*/
|
|
596
|
+
async function indexTaskDocuments(cwd, taskDir, iteration, platform, fileName) {
|
|
597
|
+
const filesToIndex = [];
|
|
598
|
+
const candidates = [
|
|
599
|
+
(0, path_1.join)(cwd, taskDir, '_shared', 'CONTEXT.md'),
|
|
600
|
+
(0, path_1.join)(cwd, taskDir, '_shared', 'TECH.md'),
|
|
601
|
+
(0, path_1.join)(cwd, taskDir, '_shared', 'REQ.md'),
|
|
602
|
+
(0, path_1.join)(cwd, taskDir, '_shared', 'SCHEMA.md'),
|
|
603
|
+
(0, path_1.join)(cwd, taskDir, '_shared', 'API_CONTRACT.yaml'),
|
|
604
|
+
(0, path_1.join)(cwd, taskDir, '00-specs', 'TECH.md'),
|
|
605
|
+
(0, path_1.join)(cwd, taskDir, '00-specs', 'TASK.md'),
|
|
606
|
+
(0, path_1.join)(cwd, taskDir, '99-artifacts', 'TEST.md'),
|
|
607
|
+
(0, path_1.join)(cwd, taskDir, '99-artifacts', 'REVIEW.md'),
|
|
608
|
+
(0, path_1.join)(cwd, taskDir, '99-artifacts', 'RISK.md'),
|
|
609
|
+
(0, path_1.join)(cwd, taskDir, '.issues.md'),
|
|
610
|
+
];
|
|
611
|
+
if (platform) {
|
|
612
|
+
candidates.push((0, path_1.join)(cwd, taskDir, `${platform}`, 'TASK.md'), (0, path_1.join)(cwd, taskDir, `${platform}`, 'COMPONENT_TREE.md'), (0, path_1.join)(cwd, taskDir, `${platform}`, 'ROUTES.md'), (0, path_1.join)(cwd, taskDir, `${platform}`, 'STATE.md'));
|
|
613
|
+
}
|
|
614
|
+
if (iteration) {
|
|
615
|
+
candidates.push((0, path_1.join)(cwd, `Iteration-${iteration}`, '020-specs', 'DESIGN.md'));
|
|
616
|
+
if (platform) {
|
|
617
|
+
candidates.push((0, path_1.join)(cwd, `Iteration-${iteration}`, '020-specs', 'platforms', platform, 'SPEC.md'));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
for (const fp of candidates) {
|
|
621
|
+
if (await (0, fs_extra_1.pathExists)(fp)) {
|
|
622
|
+
const [content, st] = await Promise.all([
|
|
623
|
+
(0, fs_extra_1.readFile)(fp, 'utf-8'),
|
|
624
|
+
(0, fs_extra_1.stat)(fp),
|
|
625
|
+
]);
|
|
626
|
+
if (content.trim().length > 50 && !content.trim().match(/^#+\s*待填充|^<!--\s*AI-FILL\s*-->$/m)) {
|
|
627
|
+
filesToIndex.push({ filePath: fp, content, mtime: st.mtimeMs });
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const scope = `${iteration || 'global'}_${taskDir.replace(/\//g, '_')}_${platform || 'all'}`;
|
|
632
|
+
const index = await buildRagIndex(filesToIndex, scope);
|
|
633
|
+
await saveRagIndex(cwd, index, fileName);
|
|
634
|
+
return index;
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* 检查 RAG 索引是否新鲜(对比源文件 mtime)
|
|
638
|
+
* 返回: { fresh: boolean; staleFiles: string[] }
|
|
639
|
+
*/
|
|
640
|
+
async function checkRagIndexFreshness(cwd, fileName) {
|
|
641
|
+
const index = await loadRagIndex(cwd, fileName);
|
|
642
|
+
if (!index)
|
|
643
|
+
return { fresh: false, staleFiles: [], newFiles: [] };
|
|
644
|
+
const staleFiles = [];
|
|
645
|
+
const indexedPaths = new Set(Object.keys(index.fileMtimes));
|
|
646
|
+
for (const [filePath, cachedMtime] of Object.entries(index.fileMtimes)) {
|
|
647
|
+
try {
|
|
648
|
+
const st = await (0, fs_extra_1.stat)(filePath);
|
|
649
|
+
if (st.mtimeMs > cachedMtime + 1000) { // 1秒容差
|
|
650
|
+
staleFiles.push(filePath);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
// 文件被删除也算过期
|
|
655
|
+
staleFiles.push(filePath);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// 检测新增文件:扫描索引目录中未记录的文件
|
|
659
|
+
const newFiles = [];
|
|
660
|
+
try {
|
|
661
|
+
const scopeDir = index.scope.includes('_030-tasks_')
|
|
662
|
+
? (0, path_1.join)(cwd, index.scope.split('_').slice(1, -1).join('/').replace(/_/g, '/'))
|
|
663
|
+
: null;
|
|
664
|
+
if (scopeDir && await (0, fs_extra_1.pathExists)(scopeDir)) {
|
|
665
|
+
await scanForNewFiles(scopeDir, indexedPaths, newFiles);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
// 非关键,忽略
|
|
670
|
+
}
|
|
671
|
+
return {
|
|
672
|
+
fresh: staleFiles.length === 0 && newFiles.length === 0,
|
|
673
|
+
staleFiles,
|
|
674
|
+
newFiles,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
/** 递归扫描目录,找出未在索引中的新增 .md 文件 */
|
|
678
|
+
async function scanForNewFiles(dir, indexedPaths, newFiles) {
|
|
679
|
+
const items = await (0, fs_extra_1.readdir)(dir, { withFileTypes: true });
|
|
680
|
+
for (const item of items) {
|
|
681
|
+
const fullPath = (0, path_1.join)(dir, item.name);
|
|
682
|
+
if (item.isDirectory() && !item.name.startsWith('.') && item.name !== 'node_modules') {
|
|
683
|
+
await scanForNewFiles(fullPath, indexedPaths, newFiles);
|
|
684
|
+
}
|
|
685
|
+
else if (item.isFile() && item.name.endsWith('.md') && !indexedPaths.has(fullPath)) {
|
|
686
|
+
newFiles.push(fullPath);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* 为任意目录构建 RAG 索引(通用版本,不限于任务目录)
|
|
692
|
+
* 扫描目录下所有 .md 文件,自动分块建索引
|
|
693
|
+
*/
|
|
694
|
+
async function indexDirectoryDocuments(cwd, dirPath, scope, fileName) {
|
|
695
|
+
const filesToIndex = [];
|
|
696
|
+
async function scanDir(dir) {
|
|
697
|
+
if (!(await (0, fs_extra_1.pathExists)(dir)))
|
|
698
|
+
return;
|
|
699
|
+
const items = await (0, fs_extra_1.readdir)(dir, { withFileTypes: true });
|
|
700
|
+
for (const item of items) {
|
|
701
|
+
const fullPath = (0, path_1.join)(dir, item.name);
|
|
702
|
+
if (item.isDirectory() && !item.name.startsWith('.') && item.name !== 'node_modules') {
|
|
703
|
+
await scanDir(fullPath);
|
|
704
|
+
}
|
|
705
|
+
else if (item.isFile() && item.name.endsWith('.md') && !item.name.startsWith('README')) {
|
|
706
|
+
const [content, st] = await Promise.all([
|
|
707
|
+
(0, fs_extra_1.readFile)(fullPath, 'utf-8'),
|
|
708
|
+
(0, fs_extra_1.stat)(fullPath),
|
|
709
|
+
]);
|
|
710
|
+
if (content.trim().length > 50 && !content.trim().match(/^#+\s*待填充|^<!--\s*AI-FILL\s*-->$/m)) {
|
|
711
|
+
filesToIndex.push({ filePath: fullPath, content, mtime: st.mtimeMs });
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
await scanDir(dirPath);
|
|
717
|
+
const index = await buildRagIndex(filesToIndex, scope);
|
|
718
|
+
await saveRagIndex(cwd, index, fileName);
|
|
719
|
+
return index;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* 增量刷新 RAG 索引:只重建有变更的文件
|
|
723
|
+
*/
|
|
724
|
+
async function refreshRagIndex(cwd, taskDir, iteration, platform, fileName) {
|
|
725
|
+
const existing = await loadRagIndex(cwd, fileName);
|
|
726
|
+
if (!existing) {
|
|
727
|
+
return indexTaskDocuments(cwd, taskDir, iteration, platform, fileName);
|
|
728
|
+
}
|
|
729
|
+
const { staleFiles, newFiles } = await checkRagIndexFreshness(cwd, fileName);
|
|
730
|
+
if (staleFiles.length === 0 && newFiles.length === 0) {
|
|
731
|
+
return existing;
|
|
732
|
+
}
|
|
733
|
+
// 保留未变更的 chunk,替换变更文件的 chunk
|
|
734
|
+
const freshChunks = existing.chunks.filter(c => !staleFiles.includes(c.filePath));
|
|
735
|
+
const freshSummaries = { ...existing.fileSummaries };
|
|
736
|
+
for (const fp of staleFiles) {
|
|
737
|
+
if (await (0, fs_extra_1.pathExists)(fp)) {
|
|
738
|
+
const content = await (0, fs_extra_1.readFile)(fp, 'utf-8');
|
|
739
|
+
const docChunks = chunkByHeaders(content, fp);
|
|
740
|
+
freshChunks.push(...docChunks);
|
|
741
|
+
const topLevel = docChunks
|
|
742
|
+
.filter(c => c.level <= 3)
|
|
743
|
+
.sort((a, b) => a.level - b.level || b.charCount - a.charCount)
|
|
744
|
+
.slice(0, 3);
|
|
745
|
+
freshSummaries[fp] = topLevel.map(c => `• ${c.title}: ${c.summary.slice(0, 80)}`).join('\n');
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
delete freshSummaries[fp];
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
// 更新 mtime
|
|
752
|
+
const freshMtimes = { ...existing.fileMtimes };
|
|
753
|
+
for (const fp of staleFiles) {
|
|
754
|
+
if (await (0, fs_extra_1.pathExists)(fp)) {
|
|
755
|
+
const st = await (0, fs_extra_1.stat)(fp);
|
|
756
|
+
freshMtimes[fp] = st.mtimeMs;
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
delete freshMtimes[fp];
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
const scope = `${iteration || 'global'}_${taskDir.replace(/\//g, '_')}_${platform || 'all'}`;
|
|
763
|
+
const refreshed = {
|
|
764
|
+
...existing,
|
|
765
|
+
updatedAt: new Date().toISOString(),
|
|
766
|
+
scope,
|
|
767
|
+
chunks: freshChunks,
|
|
768
|
+
fileSummaries: freshSummaries,
|
|
769
|
+
fileMtimes: freshMtimes,
|
|
770
|
+
};
|
|
771
|
+
await saveRagIndex(cwd, refreshed, fileName);
|
|
772
|
+
return refreshed;
|
|
773
|
+
}
|
|
774
|
+
// ═══════════════════════════════════════════════════════════════
|
|
775
|
+
// 11. 精确读取(按行号范围)
|
|
776
|
+
// ═══════════════════════════════════════════════════════════════
|
|
777
|
+
/**
|
|
778
|
+
* 按 chunk 的行号范围精确读取源文件对应章节
|
|
779
|
+
*
|
|
780
|
+
* 用途:AI 拿到 chunk 的 startLine/endLine 后,用 Read 工具的
|
|
781
|
+
* offset/limit 只读目标章节,节省 80-95% Token
|
|
782
|
+
*
|
|
783
|
+
* @param chunk 带有 startLine/endLine 的 DocumentChunk
|
|
784
|
+
* @returns 精确读取的文本,若无行号则回退到 chunk.content
|
|
785
|
+
*/
|
|
786
|
+
async function readChunkPrecise(chunk) {
|
|
787
|
+
if (!chunk.startLine || !chunk.endLine) {
|
|
788
|
+
return chunk.content;
|
|
789
|
+
}
|
|
790
|
+
if (!(await (0, fs_extra_1.pathExists)(chunk.filePath))) {
|
|
791
|
+
return chunk.content;
|
|
792
|
+
}
|
|
793
|
+
try {
|
|
794
|
+
const content = await (0, fs_extra_1.readFile)(chunk.filePath, 'utf-8');
|
|
795
|
+
const lines = content.split('\n');
|
|
796
|
+
// startLine/endLine 是 1-based
|
|
797
|
+
const start = Math.max(0, chunk.startLine - 1);
|
|
798
|
+
const end = Math.min(lines.length, chunk.endLine);
|
|
799
|
+
return lines.slice(start, end).join('\n');
|
|
800
|
+
}
|
|
801
|
+
catch {
|
|
802
|
+
return chunk.content;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* 批量精确读取多个 chunk
|
|
807
|
+
*/
|
|
808
|
+
async function readChunksPrecise(chunks) {
|
|
809
|
+
const results = [];
|
|
810
|
+
for (const chunk of chunks) {
|
|
811
|
+
const content = await readChunkPrecise(chunk);
|
|
812
|
+
results.push({ chunk, content });
|
|
813
|
+
}
|
|
814
|
+
return results;
|
|
815
|
+
}
|
|
816
|
+
//# sourceMappingURL=rag-engine.js.map
|