* Infer candidate domain services from issue title and body text. * * @param {string} title * @param {string} body * @returns {string[]}
(title, body)
| 141 | * @returns {string[]} |
| 142 | */ |
| 143 | function collectDomainsFromText(title, body) { |
| 144 | const normalizedBody = String(body || "") |
| 145 | .replace(/(["'])(?:(?=(\\?))\2.)*?\1/gs, (segment) => { |
| 146 | return /lark-cli\s+/i.test(segment) && segment.length > 80 ? '""' : segment; |
| 147 | }); |
| 148 | const text = normalizeText(title, normalizedBody); |
| 149 | const titleText = String(title || "").toLowerCase(); |
| 150 | |
| 151 | const hits = new Set(); |
| 152 | |
| 153 | function normalizeService(svc) { |
| 154 | const s = String(svc || "").toLowerCase(); |
| 155 | if (s === "docs") return "doc"; |
| 156 | return s; |
| 157 | } |
| 158 | |
| 159 | // 1) Explicit domain labels in text: domain/<service> |
| 160 | const explicit = new RegExp(`\\bdomain\\/(${DOMAIN_REGEX_ALTERNATION})\\b`, "gi"); |
| 161 | for (const match of text.matchAll(explicit)) { |
| 162 | const svc = match && match[1] ? normalizeService(match[1]) : ""; |
| 163 | if (DOMAIN_SERVICES.includes(svc)) hits.add(svc); |
| 164 | } |
| 165 | |
| 166 | // 2) Command mention: lark-cli <service> / lark cli <service> |
| 167 | const cmd = new RegExp(`\\blark[-\\s]?cli\\s+(${DOMAIN_REGEX_ALTERNATION})\\b`, "gi"); |
| 168 | for (const match of text.matchAll(cmd)) { |
| 169 | const svc = match && match[1] ? normalizeService(match[1]) : ""; |
| 170 | if (DOMAIN_SERVICES.includes(svc)) hits.add(svc); |
| 171 | } |
| 172 | |
| 173 | // 3) Loose title match: if title contains a standalone service word. |
| 174 | // This is intentionally limited to TITLE to reduce false positives. |
| 175 | // NOTE: exclude `im` here because it's too common in English text (e.g. "im stuck"). |
| 176 | const looseServices = DOMAIN_SERVICES.filter((s) => s !== "im"); |
| 177 | for (const svc of looseServices) { |
| 178 | const pattern = svc === "doc" ? "\\bdocs?\\b" : `\\b${svc}\\b`; |
| 179 | const re = new RegExp(pattern, "i"); |
| 180 | if (re.test(titleText)) hits.add(svc); |
| 181 | } |
| 182 | |
| 183 | // 4) Keyword heuristics (for users who don't paste the exact command) |
| 184 | // Keep this conservative; add keywords only when they are strongly tied to a domain. |
| 185 | const keywordMap = { |
| 186 | base: [/\bbase\s*\+/i, /\bbase-token\b/i, /open-apis\/bitable\//i, /\brecords?\/(search|list)\b/i, /多维表格/], |
| 187 | doc: [/\bdocx\b/i, /\bfeishu document\b/i, /\blark document\b/i, /\bdocument comments?\b/i, /飞书文档|云文档|文档/], |
| 188 | drive: [/\bdrive\b/i, /\bfolder token\b/i, /create_folder/i, /drive\/v1\/files/i, /\bdrive\s*\+/i], |
| 189 | sheets: [/电子表格/, /\bsheets\s*\+/i], |
| 190 | calendar: [/日历/, /\bcalendar\s*\+/i], |
| 191 | mail: [/邮件/, /\bmail\s*\+/i], |
| 192 | task: [/任务清单/, /飞书任务/, /\btask\s*\+/i], |
| 193 | wiki: [/知识库/, /\bwiki\s*\+/i], |
| 194 | minutes: [/妙记/, /\bminutes\s*\+/i], |
| 195 | vc: [/\bvc\s*\+/i, /飞书会议|视频会议|创建会议/], |
| 196 | im: [/消息|群聊|私聊/, /\bim\s*\+/i, /im\/v1/i], |
| 197 | auth: [/\bauth\s+(login|status|check|logout)\b/i, /\bkeychain\b/i, /\buser_access_token\b/i, /\buser token\b/i, /\bconsent\b/i, /授权|登录|scope authorization/], |
| 198 | core: [/\bpostinstall\b/i, /\bconfig(\.json)?\b/i, /\bconfig\s+(init|show|remove)\b/i, /\bpackage\.json\b/i, /\bscripts\/install\.js\b/i, /\bbun\b/i, /\bskills?\b/i, /\btrae\b/i, /\bprofile\b/i, /\bmulti-account\b/i, /\bprivate deployment\b/i, /\bbinary release\b/i, /\bbinary fails?\b/i, /\bunsupported platform\b/i, /\bebadplatform\b/i, /\bwindows\b.*\bbinary\b|\bbinary\b.*\bwindows\b/i, /\briscv64\b.*\bsupport/i, /私有化|安装脚本|配置文件|多账号|多个应用|多用户|持久化连接|服务器端/], |
| 199 | }; |
| 200 | for (const [svc, patterns] of Object.entries(keywordMap)) { |
no test coverage detected