diff --git a/src/gep/candidates.js b/src/gep/candidates.js index c6309116..d7a56f73 100644 --- a/src/gep/candidates.js +++ b/src/gep/candidates.js @@ -95,7 +95,7 @@ function extractCapabilityCandidates({ recentSessionTranscript, signals }) { ]; for (const sc of signalCandidates) { - if (!signalList.includes(sc.signal)) continue; + if (!signalList.some(s => s === sc.signal || s.startsWith(sc.signal + ':'))) continue; const evidence = `Signal present: ${sc.signal}`; const shape = buildFiveQuestionsShape({ title: sc.title, signals, evidence }); candidates.push({ diff --git a/src/gep/mutation.js b/src/gep/mutation.js index 84605cc4..a7c6bed5 100644 --- a/src/gep/mutation.js +++ b/src/gep/mutation.js @@ -45,7 +45,9 @@ var OPPORTUNITY_SIGNALS = [ function hasOpportunitySignal(signals) { var list = Array.isArray(signals) ? signals.map(function (s) { return String(s || ''); }) : []; for (var i = 0; i < OPPORTUNITY_SIGNALS.length; i++) { - if (list.includes(OPPORTUNITY_SIGNALS[i])) return true; + var name = OPPORTUNITY_SIGNALS[i]; + if (list.includes(name)) return true; + if (list.some(function (s) { return s.startsWith(name + ':'); })) return true; } return false; } diff --git a/src/gep/questionGenerator.js b/src/gep/questionGenerator.js index 965588bc..fa4dcbd0 100644 --- a/src/gep/questionGenerator.js +++ b/src/gep/questionGenerator.js @@ -145,7 +145,7 @@ function generateQuestions(opts) { } // --- Strategy 5: User feature requests the agent can amplify --- - if (signalSet.has('user_feature_request')) { + if (signalSet.has('user_feature_request') || signals.some(function (s) { return String(s).startsWith('user_feature_request:'); })) { var featureLines = transcript.split('\n').filter(function(l) { return /\b(add|implement|create|build|i want|i need|please add)\b/i.test(l); }); diff --git a/src/gep/signals.js b/src/gep/signals.js index 1d79e490..e17f527b 100644 --- a/src/gep/signals.js +++ b/src/gep/signals.js @@ -16,7 +16,10 @@ var OPPORTUNITY_SIGNALS = [ function hasOpportunitySignal(signals) { var list = Array.isArray(signals) ? signals : []; for (var i = 0; i < OPPORTUNITY_SIGNALS.length; i++) { - if (list.includes(OPPORTUNITY_SIGNALS[i])) return true; + var name = OPPORTUNITY_SIGNALS[i]; + if (list.includes(name)) return true; + // Signals may carry extra as "name:snippet" + if (list.some(function (s) { return String(s).startsWith(name + ':'); })) return true; } return false; } @@ -49,8 +52,12 @@ function analyzeRecentHistory(recentEvents) { var sigs = Array.isArray(evt.signals) ? evt.signals : []; for (var k = 0; k < sigs.length; k++) { var s = String(sigs[k]); - // Normalize: ignore errsig details for frequency counting - var key = s.startsWith('errsig:') ? 'errsig' : s.startsWith('recurring_errsig') ? 'recurring_errsig' : s; + // Normalize: strip details suffix so frequency keys match dedup filter keys + var key = s.startsWith('errsig:') ? 'errsig' + : s.startsWith('recurring_errsig') ? 'recurring_errsig' + : s.startsWith('user_feature_request:') ? 'user_feature_request' + : s.startsWith('user_improvement_suggestion:') ? 'user_improvement_suggestion' + : s; signalFreq[key] = (signalFreq[key] || 0) + 1; } var genes = Array.isArray(evt.genes_used) ? evt.genes_used : []; @@ -137,7 +144,6 @@ function extractSignals({ recentSessionTranscript, todayLog, memorySnippet, user String(userSnippet || ''), ].join('\n'); var lower = corpus.toLowerCase(); - // Analyze recent evolution history for de-duplication var history = analyzeRecentHistory(recentEvents || []); @@ -145,7 +151,8 @@ function extractSignals({ recentSessionTranscript, todayLog, memorySnippet, user // Refined error detection regex to avoid false positives on "fail"/"failed" in normal text. // We prioritize structured error markers ([error], error:, exception:) and specific JSON patterns. - var errorHit = /\[error\]|error:|exception:|iserror":true|"status":\s*"error"|"status":\s*"failed"/.test(lower); + // Chinese: 错误、异常、失败、报错 — all require contextual colon [::] so improvement text (e.g. "改进一下错误处理") is not treated as log_error. + var errorHit = /\[error\]|error:|exception:|iserror":true|"status":\s*"error"|"status":\s*"failed"|错误\s*[::]|异常\s*[::]|报错\s*[::]|失败\s*[::]/.test(lower); if (errorHit) signals.push('log_error'); // Error signature (more reproducible than a coarse "log_error" tag). @@ -156,7 +163,7 @@ function extractSignals({ recentSessionTranscript, todayLog, memorySnippet, user .filter(Boolean); var errLine = - lines.find(function (l) { return /\b(typeerror|referenceerror|syntaxerror)\b\s*:|error\s*:|exception\s*:|\[error/i.test(l); }) || + lines.find(function (l) { return /\b(typeerror|referenceerror|syntaxerror)\b\s*:|error\s*:|exception\s*:|\[error|错误\s*[::]|异常\s*[::]|报错\s*[::]|失败\s*[::]/i.test(l); }) || null; if (errLine) { @@ -200,27 +207,77 @@ function extractSignals({ recentSessionTranscript, todayLog, memorySnippet, user } // --- Opportunity signals (innovation / feature requests) --- - - // user_feature_request: user explicitly asks for a new capability - // Look for action verbs + object patterns that indicate a feature request - if (/\b(add|implement|create|build|make|develop|write|design)\b[^.?!\n]{3,60}\b(feature|function|module|capability|tool|support|endpoint|command|option|mode)\b/i.test(corpus)) { - signals.push('user_feature_request'); + // Support 4 languages: 简中、繁中、英、日. Attach extra info (snippet) for selector/prompt use. + + var featureRequestSnippet = ''; + // English + var featEn = corpus.match(/\b(add|implement|create|build|make|develop|write|design)\b[^.?!\n]{3,120}\b(feature|function|module|capability|tool|support|endpoint|command|option|mode)\b/i); + if (featEn) featureRequestSnippet = featEn[0].replace(/\s+/g, ' ').trim().slice(0, 200); + if (!featureRequestSnippet && /\b(i want|i need|we need|please add|can you add|could you add|let'?s add)\b/i.test(lower)) { + var featWant = corpus.match(/.{0,80}\b(i want|i need|we need|please add|can you add|could you add|let'?s add)\b.{0,80}/i); + featureRequestSnippet = featWant ? featWant[0].replace(/\s+/g, ' ').trim().slice(0, 200) : 'feature request'; + } + // 简中(含「我想……」:截取描述至 200 字) + if (!featureRequestSnippet && /加个|实现一下|做个|想要\s*一个|需要\s*一个|帮我加|帮我开发|加一下|新增一个|加个功能|做个功能|我想/.test(corpus)) { + var featZh = corpus.match(/.{0,100}(加个|实现一下|做个|想要\s*一个|需要\s*一个|帮我加|帮我开发|加一下|新增一个|加个功能|做个功能).{0,100}/); + if (featZh) featureRequestSnippet = featZh[0].replace(/\s+/g, ' ').trim().slice(0, 200); + if (!featureRequestSnippet && /我想/.test(corpus)) { + var featWantZh = corpus.match(/我想\s*[,,\.。、\s]*([\s\S]{0,400})/); + featureRequestSnippet = featWantZh ? (featWantZh[1].replace(/\s+/g, ' ').trim().slice(0, 200) || '功能需求') : '功能需求'; + } + if (!featureRequestSnippet) featureRequestSnippet = '功能需求'; } - // Also catch direct "I want/need X" patterns - if (/\b(i want|i need|we need|please add|can you add|could you add|let'?s add)\b/i.test(lower)) { - signals.push('user_feature_request'); + // 繁中 + if (!featureRequestSnippet && /加個|實現一下|做個|想要一個|請加|新增一個|加個功能|做個功能|幫我加/.test(corpus)) { + var featTw = corpus.match(/.{0,100}(加個|實現一下|做個|想要一個|請加|新增一個|加個功能|做個功能|幫我加).{0,100}/); + featureRequestSnippet = featTw ? featTw[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '功能需求'; + } + // 日 + if (!featureRequestSnippet && /追加|実装|作って|機能を|追加して|が欲しい|を追加|してほしい/.test(corpus)) { + var featJa = corpus.match(/.{0,100}(追加|実装|作って|機能を|追加して|が欲しい|を追加|してほしい).{0,100}/); + featureRequestSnippet = featJa ? featJa[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '機能要望'; + } + if (featureRequestSnippet || /\b(add|implement|create|build|make|develop|write|design)\b[^.?!\n]{3,60}\b(feature|function|module|capability|tool|support|endpoint|command|option|mode)\b/i.test(corpus) || + /\b(i want|i need|we need|please add|can you add|could you add|let'?s add)\b/i.test(lower) || + /加个|实现一下|做个|想要\s*一个|需要\s*一个|帮我加|帮我开发|加一下|新增一个|加个功能|做个功能|我想/.test(corpus) || + /加個|實現一下|做個|想要一個|請加|新增一個|加個功能|做個功能|幫我加/.test(corpus) || + /追加|実装|作って|機能を|追加して|が欲しい|を追加|してほしい/.test(corpus)) { + signals.push('user_feature_request:' + (featureRequestSnippet || '')); } - // user_improvement_suggestion: user suggests making something better - if (/\b(should be|could be better|improve|enhance|upgrade|refactor|clean up|simplify|streamline)\b/i.test(lower)) { - // Only fire if there is no active error (to distinguish from repair requests) - if (!errorHit) signals.push('user_improvement_suggestion'); + // user_improvement_suggestion: 4 languages + extra + var improvementSnippet = ''; + if (!errorHit) { + var impEn = corpus.match(/.{0,80}\b(should be|could be better|improve|enhance|upgrade|refactor|clean up|simplify|streamline)\b.{0,80}/i); + if (impEn) improvementSnippet = impEn[0].replace(/\s+/g, ' ').trim().slice(0, 200); + if (!improvementSnippet && /改进一下|优化一下|简化|重构|整理一下|弄得更好/.test(corpus)) { + var impZh = corpus.match(/.{0,100}(改进一下|优化一下|简化|重构|整理一下|弄得更好).{0,100}/); + improvementSnippet = impZh ? impZh[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '改进建议'; + } + if (!improvementSnippet && /改進一下|優化一下|簡化|重構|整理一下|弄得更好/.test(corpus)) { + var impTw = corpus.match(/.{0,100}(改進一下|優化一下|簡化|重構|整理一下|弄得更好).{0,100}/); + improvementSnippet = impTw ? impTw[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '改進建議'; + } + if (!improvementSnippet && /改善|最適化|簡素化|リファクタ|良くして|改良/.test(corpus)) { + var impJa = corpus.match(/.{0,100}(改善|最適化|簡素化|リファクタ|良くして|改良).{0,100}/); + improvementSnippet = impJa ? impJa[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '改善要望'; + } + var hasImprovement = improvementSnippet || + /\b(should be|could be better|improve|enhance|upgrade|refactor|clean up|simplify|streamline)\b/i.test(lower) || + /改进一下|优化一下|简化|重构|整理一下|弄得更好/.test(corpus) || + /改進一下|優化一下|簡化|重構|整理一下|弄得更好/.test(corpus) || + /改善|最適化|簡素化|リファクタ|良くして|改良/.test(corpus); + if (hasImprovement) signals.push('user_improvement_suggestion:' + (improvementSnippet || '')); } // perf_bottleneck: performance issues detected if (/\b(slow|timeout|timed?\s*out|latency|bottleneck|took too long|performance issue|high cpu|high memory|oom|out of memory)\b/i.test(lower)) { signals.push('perf_bottleneck'); } + // Chinese: 慢/超时/卡顿/性能/内存溢出 + if (/太慢|超时|卡顿|性能问题|内存溢出|跑不动|很慢/.test(corpus)) { + signals.push('perf_bottleneck'); + } // capability_gap: something is explicitly unsupported or missing if (/\b(not supported|cannot|doesn'?t support|no way to|missing feature|unsupported|not available|not implemented|no support for)\b/i.test(lower)) { @@ -229,6 +286,12 @@ function extractSignals({ recentSessionTranscript, todayLog, memorySnippet, user signals.push('capability_gap'); } } + // Chinese: 不支持/没法/无法/没有这个功能 + if (/不支持|没法|无法\s*实现|没有这个功能|还不支持/.test(corpus)) { + if (!signals.includes('memory_missing') && !signals.includes('user_missing') && !signals.includes('session_logs_missing')) { + signals.push('capability_gap'); + } + } // --- Tool Usage Analytics --- var toolUsage = {}; @@ -273,7 +336,9 @@ function extractSignals({ recentSessionTranscript, todayLog, memorySnippet, user var beforeDedup = signals.length; signals = signals.filter(function (s) { // Normalize signal key for comparison - var key = s.startsWith('errsig:') ? 'errsig' : s.startsWith('recurring_errsig') ? 'recurring_errsig' : s; + var key = s.startsWith('errsig:') ? 'errsig' : s.startsWith('recurring_errsig') ? 'recurring_errsig' + : s.startsWith('user_feature_request:') ? 'user_feature_request' : s.startsWith('user_improvement_suggestion:') ? 'user_improvement_suggestion' + : s; return !history.suppressedSignals.has(key); }); if (beforeDedup > 0 && signals.length === 0) { diff --git a/test/selector.test.js b/test/selector.test.js index dbd82690..28d350fb 100644 --- a/test/selector.test.js +++ b/test/selector.test.js @@ -23,7 +23,7 @@ const GENES = [ type: 'Gene', id: 'gene_innovate', category: 'innovate', - signals_match: ['user_feature_request', 'capability_gap', 'stable_success_plateau'], + signals_match: ['user_feature_request', 'user_improvement_suggestion', 'capability_gap', 'stable_success_plateau'], strategy: ['build it'], validation: ['node -e "true"'], }, @@ -81,6 +81,19 @@ describe('selectGene', () => { // With preference, it should be selected even if gene_repair scores higher assert.equal(result.selected.id, 'gene_optimize'); }); + + it('matches gene when signal carries extra info (user_feature_request:snippet)', () => { + // Signal format: "user_feature_request:加个支付模块" — selector matches by substring includes(pattern) + const result = selectGene(GENES, ['user_feature_request:加个支付模块,要支持微信和支付宝'], {}); + assert.ok(result.selected, 'should select a gene'); + assert.equal(result.selected.id, 'gene_innovate', 'innovate gene has signals_match user_feature_request'); + }); + + it('matches gene when signal carries extra info (user_improvement_suggestion:snippet)', () => { + const result = selectGene(GENES, ['user_improvement_suggestion:refactor the payment module and simplify the API'], {}); + assert.ok(result.selected); + assert.equal(result.selected.id, 'gene_innovate', 'innovate gene has signals_match user_improvement_suggestion'); + }); }); describe('selectCapsule', () => { diff --git a/test/signals.test.js b/test/signals.test.js new file mode 100644 index 00000000..926b8e4d --- /dev/null +++ b/test/signals.test.js @@ -0,0 +1,218 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const { extractSignals } = require('../src/gep/signals'); + +const emptyInput = { + recentSessionTranscript: '', + todayLog: '', + memorySnippet: '', + userSnippet: '', + recentEvents: [], +}; + +function hasSignal(signals, name) { + return Array.isArray(signals) && signals.some(s => String(s).startsWith(name)); +} + +function getSignalExtra(signals, name) { + const s = Array.isArray(signals) ? signals.find(x => String(x).startsWith(name + ':')) : undefined; + if (!s) return undefined; + const i = String(s).indexOf(':'); + return i === -1 ? '' : String(s).slice(i + 1).trim(); +} + +describe('extractSignals — user_feature_request (4 languages)', () => { + it('recognizes English feature request', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: 'Please add a dark mode toggle to the settings page.', + }); + assert.ok(hasSignal(r, 'user_feature_request'), 'expected user_feature_request in ' + JSON.stringify(r)); + }); + + it('recognizes Simplified Chinese (简中) feature request', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: '加个支付模块,要支持微信和支付宝。', + }); + assert.ok(hasSignal(r, 'user_feature_request'), 'expected user_feature_request in ' + JSON.stringify(r)); + }); + + it('recognizes Traditional Chinese (繁中) feature request', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: '請加一個匯出報表的功能,要支援 PDF。', + }); + assert.ok(hasSignal(r, 'user_feature_request'), 'expected user_feature_request in ' + JSON.stringify(r)); + }); + + it('recognizes Japanese (日) feature request', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: 'ダークモードのトグルを追加してほしいです。', + }); + assert.ok(hasSignal(r, 'user_feature_request'), 'expected user_feature_request in ' + JSON.stringify(r)); + }); + + it('user_feature_request signal carries extra info when present', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: 'Please add a dark mode toggle to the settings page.', + }); + const extra = getSignalExtra(r, 'user_feature_request'); + assert.ok(extra !== undefined, 'expected user_feature_request:extra form'); + assert.ok(extra.length > 0, 'extra should not be empty'); + assert.ok(extra.toLowerCase().includes('dark') || extra.includes('toggle') || extra.includes('add'), 'extra should reflect request content'); + }); +}); + +describe('extractSignals — user_improvement_suggestion (4 languages)', () => { + it('recognizes English improvement suggestion', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: 'The UI could be better; we should simplify the onboarding flow.', + }); + assert.ok(hasSignal(r, 'user_improvement_suggestion'), 'expected user_improvement_suggestion in ' + JSON.stringify(r)); + }); + + it('recognizes Simplified Chinese (简中) improvement suggestion', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: '改进一下登录流程,优化一下性能。', + }); + assert.ok(hasSignal(r, 'user_improvement_suggestion'), 'expected user_improvement_suggestion in ' + JSON.stringify(r)); + }); + + it('recognizes Traditional Chinese (繁中) improvement suggestion', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: '建議改進匯出速度,優化一下介面。', + }); + assert.ok(hasSignal(r, 'user_improvement_suggestion'), 'expected user_improvement_suggestion in ' + JSON.stringify(r)); + }); + + it('recognizes Japanese (日) improvement suggestion', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: 'ログインの流れを改善してほしい。', + }); + assert.ok(hasSignal(r, 'user_improvement_suggestion'), 'expected user_improvement_suggestion in ' + JSON.stringify(r)); + }); + + it('user_improvement_suggestion signal carries extra info when present', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: 'We should refactor the payment module and simplify the API.', + }); + const extra = getSignalExtra(r, 'user_improvement_suggestion'); + assert.ok(extra !== undefined, 'expected user_improvement_suggestion:extra form'); + assert.ok(extra.length > 0, 'extra should not be empty'); + }); +}); + +describe('extractSignals — edge cases (snippet length, 我想, empty, punctuation)', () => { + it('「我想」+ 超长描述:snippet 截断至 200 字以内', () => { + const long = '我想让系统支持批量导入用户、导出报表、自定义工作流、多语言切换、主题切换、权限组、审计日志、Webhook 通知、API 限流、缓存策略配置、数据库备份恢复、灰度发布、A/B 测试、埋点统计、性能监控、告警规则、工单流转、知识库搜索、智能推荐、以及一大堆其他功能以便我们能够更好地管理业务。'; + const r = extractSignals({ ...emptyInput, userSnippet: long }); + assert.ok(hasSignal(r, 'user_feature_request'), 'expected user_feature_request'); + const extra = getSignalExtra(r, 'user_feature_request'); + assert.ok(extra !== undefined && extra.length > 0, 'extra should be present'); + assert.ok(extra.length <= 200, 'snippet must be truncated to 200 chars, got ' + extra.length); + }); + + it('「我想」+ 短描述:能识别且带 snippet', () => { + const r = extractSignals({ ...emptyInput, userSnippet: '我想加一个导出 Excel 的功能。' }); + assert.ok(hasSignal(r, 'user_feature_request')); + const extra = getSignalExtra(r, 'user_feature_request'); + assert.ok(extra !== undefined && extra.length > 0); + }); + + it('「我想。」后无内容:仍识别为 feature request,snippet 可为默认或空', () => { + const r = extractSignals({ ...emptyInput, userSnippet: '我想。' }); + assert.ok(hasSignal(r, 'user_feature_request'), 'expected user_feature_request for 我想。'); + }); + + it('仅「我想」无标点无后续:仍识别', () => { + const r = extractSignals({ ...emptyInput, userSnippet: '我想' }); + assert.ok(hasSignal(r, 'user_feature_request')); + }); + + it('空 userSnippet:不产生 user_feature_request / user_improvement_suggestion(仅来自 user)', () => { + const r = extractSignals({ ...emptyInput, userSnippet: '' }); + const hasFeat = hasSignal(r, 'user_feature_request'); + const hasImp = hasSignal(r, 'user_improvement_suggestion'); + assert.ok(!hasFeat && !hasImp, 'empty userSnippet should not yield feature/improvement from user input'); + }); + + it('仅空格与标点:不匹配为功能/改进', () => { + const r = extractSignals({ ...emptyInput, userSnippet: ' \n\t 。,、 \n' }); + assert.ok(!hasSignal(r, 'user_feature_request'), 'whitespace/punctuation only should not match'); + assert.ok(!hasSignal(r, 'user_improvement_suggestion')); + }); + + it('I want + 超长英文描述:snippet 截断', () => { + const long = 'I want to add a feature that allows users to export data in CSV and Excel formats, with custom column mapping, date range filters, scheduled exports, email delivery, and integration with our analytics pipeline so that we can reduce manual reporting work. This is critical for Q2.'; + const r = extractSignals({ ...emptyInput, userSnippet: long }); + assert.ok(hasSignal(r, 'user_feature_request')); + const extra = getSignalExtra(r, 'user_feature_request'); + assert.ok(extra === undefined || extra.length <= 200, 'snippet if present should be <= 200'); + }); + + it('改进一下 + 超长描述:user_improvement_suggestion snippet 截断至 200', () => { + // 避免含「错误/失败/异常/报错」以免触发 errorHit 压制 improvement + const long = '改进一下登录流程:首先支持扫码登录、然后记住设备、然后支持多因素认证、然后审计日志、然后限流防刷、然后国际化提示、然后无障碍优化、然后性能优化、然后安全加固、然后文档补全。'; + const r = extractSignals({ ...emptyInput, userSnippet: long }); + assert.ok(hasSignal(r, 'user_improvement_suggestion')); + const extra = getSignalExtra(r, 'user_improvement_suggestion'); + assert.ok(extra !== undefined && extra.length > 0); + assert.ok(extra.length <= 200, 'improvement snippet <= 200, got ' + extra.length); + }); + + it('多句混合:首句为功能需求时仍识别并带 snippet', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: '加个支付模块,要支持微信和支付宝。另外昨天那个 bug 修了吗?', + }); + assert.ok(hasSignal(r, 'user_feature_request')); + const extra = getSignalExtra(r, 'user_feature_request'); + assert.ok(extra !== undefined && extra.length > 0); + }); + + it('描述中含换行与制表符:正则能匹配并归一化空格', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: '我想\n加一个\t导出\n报表的功能。', + }); + assert.ok(hasSignal(r, 'user_feature_request')); + const extra = getSignalExtra(r, 'user_feature_request'); + assert.ok(extra !== undefined); + assert.ok(!/\n/.test(extra) || extra.length <= 200, 'snippet should be normalized (no newlines in stored form or truncated)'); + }); + + it('「我想」出现在段落中间:仍能识别', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: '前面是一些背景说明。我想加一个暗色模式开关,方便夜间使用。', + }); + assert.ok(hasSignal(r, 'user_feature_request')); + const extra = getSignalExtra(r, 'user_feature_request'); + assert.ok(extra !== undefined && extra.length > 0); + }); + + it('仅标点句「。。。。」不触发功能/改进', () => { + const r = extractSignals({ ...emptyInput, userSnippet: '。。。。' }); + assert.ok(!hasSignal(r, 'user_feature_request')); + assert.ok(!hasSignal(r, 'user_improvement_suggestion')); + }); + + it('user_feature_request 与 user_improvement_suggestion 均带描述时两条都有 extra', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: '加个支付模块。另外改进一下登录流程,简化步骤。', + }); + assert.ok(hasSignal(r, 'user_feature_request')); + assert.ok(hasSignal(r, 'user_improvement_suggestion')); + assert.ok(getSignalExtra(r, 'user_feature_request')); + assert.ok(getSignalExtra(r, 'user_improvement_suggestion')); + }); +});