autosnippet 2.19.0 → 2.19.2

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.
@@ -670,28 +670,86 @@ export async function bootstrapRefine(ctx, args) {
670
670
  onProgress?.('refine:item-started', { candidateId: entry.id, title: entry.title, current: processed, total: entries.length, progress: Math.round(((processed - 1) / entries.length) * 100) });
671
671
 
672
672
  try {
673
- const prompt = `你是一位高级代码知识管理专家。请改进以下知识条目:
673
+ const before = {
674
+ title: entry.title || '',
675
+ description: entry.description || '',
676
+ pattern: entry.content?.pattern || '',
677
+ markdown: entry.content?.markdown || '',
678
+ rationale: entry.content?.rationale || '',
679
+ tags: entry.tags || [],
680
+ confidence: entry.reasoning?.confidence ?? 0.6,
681
+ relations: entry.relations || {},
682
+ aiInsight: entry.aiInsight || null,
683
+ agentNotes: entry.agentNotes || null,
684
+ };
685
+
686
+ const refineInstruction = args.userPrompt
687
+ ? args.userPrompt
688
+ : '请改善描述使其更专业简洁,补充高阶架构洞察,如缺少关联关系请推断补充';
689
+
690
+ const prompt = `你是一位高级代码知识管理专家。请改进以下知识条目。
691
+
692
+ ## ⭐ JSON key 规范(最高优先级)
693
+
694
+ 返回的 JSON 必须且只能使用以下 9 个 key,大小写必须完全一致:
695
+
696
+ description → 摘要(string)
697
+ pattern → 代码/标准用法(string)
698
+ markdown → Markdown 文档(string)
699
+ rationale → 设计原理(string)
700
+ tags → 标签(string[])
701
+ confidence → 置信度(number 0.0–1.0)
702
+ aiInsight → AI 洞察(string | null)
703
+ agentNotes → Agent 笔记(string[] | null)
704
+ relations → 关联关系(object)
705
+
706
+ ## 当前条目信息
707
+
708
+ 标题: ${before.title}
709
+ 类型: ${entry.knowledgeType || '未知'}
710
+ 语言: ${entry.language || '未知'}
711
+
712
+ 【description】摘要
713
+ ${before.description || '(空)'}
714
+
715
+ 【pattern】代码/标准用法
716
+ ${(before.pattern || '(空)').substring(0, 2000)}
674
717
 
675
- 标题: ${entry.title}
676
- 类型: ${entry.knowledgeType}
677
- 语言: ${entry.language}
678
- 描述: ${entry.description || ''}
679
- 代码:
680
- \`\`\`
681
- ${(entry.content?.pattern || '').substring(0, 2000)}
682
- \`\`\`
718
+ 【markdown】Markdown 文档
719
+ ${(before.markdown || '(空)').substring(0, 2000)}
720
+
721
+ 【rationale】设计原理
722
+ ${before.rationale || '(空)'}
723
+
724
+ 【tags】标签
725
+ ${JSON.stringify(before.tags)}
726
+
727
+ 【confidence】置信度
728
+ ${before.confidence}
729
+
730
+ 【relations】关联关系
731
+ ${JSON.stringify(before.relations)}
732
+
733
+ 【aiInsight】AI 洞察
734
+ ${before.aiInsight || '(空)'}
735
+
736
+ 【agentNotes】Agent 笔记
737
+ ${JSON.stringify(before.agentNotes || [])}
683
738
 
684
739
  同批次知识: ${allTitles.slice(0, 20).join(', ')}
685
- ${args.userPrompt ? `用户补充: ${args.userPrompt}` : ''}
686
740
 
687
- 请返回 JSON:
688
- {
689
- "summary": "改进后的中文摘要",
690
- "tags": ["建议标签"],
691
- "relations": [{"target":"相关条目标题","relation":"depends_on|extends|related_to"}],
692
- "confidence": 0.0-1.0,
693
- "insight": "架构洞察(一句话)"
694
- }`;
741
+ ## 润色指令
742
+
743
+ ${refineInstruction}
744
+
745
+ ## 约束
746
+
747
+ 1. 只修改需要改进的字段,未涉及的必须原样返回。
748
+ 2. tags 采用合并策略(保留原有 + 补充新建议),不要删除已有标签。
749
+ 3. relations 为 object 格式,key 为关系类型(如 inherits/implements/calls/depends_on/extends/related),value 为 string[]。
750
+ 4. 每个 key 都必须存在,key 名称必须与上述完全一致。
751
+
752
+ 仅返回 JSON,不要添加任何其他文字或代码块标记。`;
695
753
 
696
754
  const parsed = await aiProvider.chatWithStructuredOutput(prompt, { temperature: 0.3 });
697
755
 
@@ -707,27 +765,86 @@ ${args.userPrompt ? `用户补充: ${args.userPrompt}` : ''}
707
765
  continue;
708
766
  }
709
767
 
768
+ // ─── key 别名归一化(与 candidates.js 保持一致) ───
769
+ const KEY_ALIASES = {
770
+ summary: 'description', desc: 'description',
771
+ content: 'pattern', design: 'rationale', designRationale: 'rationale',
772
+ markdownDoc: 'markdown', doc: 'markdown',
773
+ tag: 'tags', label: 'tags', labels: 'tags',
774
+ score: 'confidence',
775
+ ai_insight: 'aiInsight', insight: 'aiInsight', aiinsight: 'aiInsight',
776
+ agent_notes: 'agentNotes', notes: 'agentNotes', agentnotes: 'agentNotes',
777
+ relation: 'relations',
778
+ };
779
+ const VALID_KEYS = new Set(['description', 'pattern', 'markdown', 'rationale', 'tags', 'confidence', 'aiInsight', 'agentNotes', 'relations']);
780
+ const normalized = {};
781
+ for (const [key, value] of Object.entries(parsed)) {
782
+ if (VALID_KEYS.has(key)) {
783
+ normalized[key] = value;
784
+ } else {
785
+ const mapped = KEY_ALIASES[key] || KEY_ALIASES[key.toLowerCase?.()];
786
+ if (mapped && !(mapped in normalized)) normalized[mapped] = value;
787
+ }
788
+ }
789
+ for (const k of VALID_KEYS) {
790
+ if (!(k in normalized)) normalized[k] = before[k];
791
+ }
792
+
710
793
  // 构建更新数据
711
794
  const updateData = {};
712
795
  let changed = false;
713
796
 
714
- if (parsed.summary && parsed.summary !== entry.description) {
715
- updateData.description = parsed.summary;
797
+ if (normalized.description != null && normalized.description !== before.description) {
798
+ updateData.description = normalized.description;
716
799
  changed = true;
717
800
  }
718
- if (parsed.tags && Array.isArray(parsed.tags)) {
719
- const merged = [...new Set([...(entry.tags || []), ...parsed.tags])];
720
- if (merged.length > (entry.tags || []).length) {
801
+ // tags 采用合并策略
802
+ if (normalized.tags != null && Array.isArray(normalized.tags)) {
803
+ const merged = [...new Set([...(before.tags || []), ...normalized.tags])];
804
+ if (JSON.stringify(merged) !== JSON.stringify(before.tags)) {
721
805
  updateData.tags = merged;
722
806
  changed = true;
723
807
  }
724
808
  }
725
- if (typeof parsed.confidence === 'number' && parsed.confidence !== 0.6) {
726
- updateData.reasoning = { ...(entry.reasoning || {}), confidence: parsed.confidence };
809
+ if (typeof normalized.confidence === 'number' && normalized.confidence !== before.confidence) {
810
+ updateData.reasoning = { ...(entry.reasoning || {}), confidence: normalized.confidence };
811
+ changed = true;
812
+ }
813
+ if (normalized.aiInsight != null && normalized.aiInsight !== before.aiInsight) {
814
+ updateData.aiInsight = normalized.aiInsight;
727
815
  changed = true;
728
816
  }
729
- if (parsed.insight) {
730
- updateData.description = `${entry.description || ''}\n\n**AI Insight**: ${parsed.insight}`.trim();
817
+ if (normalized.agentNotes !== undefined) {
818
+ const newNotes = JSON.stringify(normalized.agentNotes);
819
+ if (newNotes !== JSON.stringify(before.agentNotes)) {
820
+ updateData.agentNotes = normalized.agentNotes;
821
+ changed = true;
822
+ }
823
+ }
824
+ if (normalized.relations !== undefined) {
825
+ const newRels = JSON.stringify(normalized.relations);
826
+ if (newRels !== JSON.stringify(before.relations)) {
827
+ updateData.relations = normalized.relations;
828
+ changed = true;
829
+ }
830
+ }
831
+ // content 嵌套写入
832
+ const contentPatch = { ...(entry.content || {}) };
833
+ let contentChanged = false;
834
+ if (normalized.pattern != null && normalized.pattern !== before.pattern) {
835
+ contentPatch.pattern = normalized.pattern;
836
+ contentChanged = true;
837
+ }
838
+ if (normalized.markdown != null && normalized.markdown !== before.markdown) {
839
+ contentPatch.markdown = normalized.markdown;
840
+ contentChanged = true;
841
+ }
842
+ if (normalized.rationale != null && normalized.rationale !== before.rationale) {
843
+ contentPatch.rationale = normalized.rationale;
844
+ contentChanged = true;
845
+ }
846
+ if (contentChanged) {
847
+ updateData.content = contentPatch;
731
848
  changed = true;
732
849
  }
733
850
 
@@ -178,10 +178,11 @@ export class HttpServer {
178
178
  // Gateway 中间件 (注入 req.gw)
179
179
  this.app.use(gatewayMiddleware());
180
180
 
181
- // 请求超时设置(AI 扫描类路由需要更长时间)
181
+ // 请求超时设置(AI 扫描类路由需要更长时间,SSE 流式路由需要更长时间)
182
182
  this.app.use((req, res, next) => {
183
183
  const isLongRunning = req.path.includes('/spm/scan') || req.path.includes('/spm/bootstrap') || req.path.includes('/extract/');
184
- req.setTimeout(isLongRunning ? 600000 : 60000); // AI 扫描/冷启动 10分钟,其他 60秒
184
+ const isStreaming = req.path.includes('/stream') || req.path.includes('/events/');
185
+ req.setTimeout(isLongRunning ? 600000 : isStreaming ? 300000 : 60000); // AI 扫描 10分钟, SSE/EventSource 5分钟, 其他 60秒
185
186
  next();
186
187
  });
187
188
  }
@@ -11,6 +11,7 @@ import { getServiceContainer } from '../../injection/ServiceContainer.js';
11
11
  import { createProvider } from '../../external/ai/AiFactory.js';
12
12
  import { ValidationError } from '../../shared/errors/index.js';
13
13
  import Logger from '../../infrastructure/logging/Logger.js';
14
+ import { createStreamSession, getStreamSession } from '../utils/sse-sessions.js';
14
15
 
15
16
  const router = express.Router();
16
17
  const logger = Logger.getInstance();
@@ -399,6 +400,137 @@ router.post('/env-config', asyncHandler(async (req, res) => {
399
400
  res.json({ success: true, data: result });
400
401
  }));
401
402
 
403
+ // ═══════════════════════════════════════════════════════
404
+ // SSE Streaming — 流式对话(Session + EventSource 架构)
405
+ // ═══════════════════════════════════════════════════════
406
+
407
+ /**
408
+ * POST /api/v1/ai/chat/stream
409
+ * 启动 AI 对话流 — 创建 session,后台执行 ChatAgent,立即返回 sessionId
410
+ *
411
+ * 客户端拿到 sessionId 后通过 GET /chat/events/:sessionId (EventSource) 消费事件
412
+ *
413
+ * 协议事件(通过 session 缓冲 + EventSource 交付):
414
+ * step:start — 新推理步骤开始 {step, maxSteps, phase}
415
+ * step:end — 推理步骤结束 {step}
416
+ * tool:start — 工具调用开始 {id, tool, args}
417
+ * tool:end — 工具调用结束 {tool, status, resultSize?, duration?, error?}
418
+ * text:start — 文本流开始 {id, role}
419
+ * text:delta — 文本分块 {id, delta}
420
+ * text:end — 文本流结束 {id}
421
+ * stream:done — 会话完成 {text, toolCalls, hasContext}
422
+ * stream:error — 会话错误 {message}
423
+ *
424
+ * Body: { prompt: string, history?: Array<{role,content}> }
425
+ * Response: { success: true, sessionId: string }
426
+ */
427
+ router.post('/chat/stream', asyncHandler(async (req, res) => {
428
+ const { prompt, history = [] } = req.body;
429
+ if (!prompt) throw new ValidationError('prompt is required');
430
+
431
+ const chatAgent = getChatAgent();
432
+ const session = createStreamSession('chat');
433
+
434
+ logger.debug('SSE session created', { sessionId: session.sessionId });
435
+
436
+ // 立即返回 sessionId(不等待 ChatAgent 执行)
437
+ res.json({ success: true, sessionId: session.sessionId });
438
+
439
+ // 后台执行 ChatAgent — 事件通过 session.send() 缓冲
440
+ chatAgent.execute(prompt, {
441
+ history,
442
+ onProgress: (event) => session.send(event),
443
+ }).then(result => {
444
+ // text:start/delta/end 已由 ChatAgent.execute() 内部通过 onProgress 发送
445
+ session.end({
446
+ text: result.reply,
447
+ toolCalls: result.toolCalls || [],
448
+ hasContext: result.hasContext || false,
449
+ });
450
+ logger.debug('SSE session completed', { sessionId: session.sessionId, events: session.buffer.length });
451
+ }).catch(err => {
452
+ logger.warn('SSE session error', { sessionId: session.sessionId, error: err.message });
453
+ session.error(err.message);
454
+ });
455
+ }));
456
+
457
+ /**
458
+ * GET /api/v1/ai/chat/events/:sessionId
459
+ * EventSource SSE 端点 — 消费指定 session 的实时事件
460
+ *
461
+ * 流程:
462
+ * 1. 回放 session 缓冲区中已积累的所有事件
463
+ * 2. 如果 session 已完成 → 直接结束流
464
+ * 3. 否则订阅实时事件,直到 stream:done / stream:error
465
+ *
466
+ * 使用原生 EventSource API 消费(浏览器内置 SSE 支持,无缓冲问题)
467
+ */
468
+ router.get('/chat/events/:sessionId', (req, res) => {
469
+ const session = getStreamSession(req.params.sessionId);
470
+ if (!session) {
471
+ return res.status(404).json({ success: false, error: 'Session not found or expired' });
472
+ }
473
+
474
+ // ─── SSE Headers ───
475
+ res.setHeader('Content-Type', 'text/event-stream');
476
+ res.setHeader('Cache-Control', 'no-cache');
477
+ res.setHeader('Connection', 'keep-alive');
478
+ res.setHeader('X-Accel-Buffering', 'no');
479
+ res.flushHeaders();
480
+
481
+ if (res.socket) {
482
+ res.socket.setNoDelay(true);
483
+ res.socket.setTimeout(0);
484
+ }
485
+
486
+ /** 写入一个 SSE data 行 */
487
+ function writeEvent(event) {
488
+ if (res.writableEnded) return;
489
+ const line = `data: ${JSON.stringify(event)}\n\n`;
490
+ res.write(line);
491
+ }
492
+
493
+ // 1) 回放缓冲区
494
+ let isDone = false;
495
+ for (const event of session.buffer) {
496
+ writeEvent(event);
497
+ if (event.type === 'stream:done' || event.type === 'stream:error') {
498
+ isDone = true;
499
+ }
500
+ }
501
+
502
+ // 2) 如果已完成,直接关闭
503
+ if (isDone || session.completed) {
504
+ res.end();
505
+ return;
506
+ }
507
+
508
+ // 3) 订阅实时事件
509
+ const unsubscribe = session.on((event) => {
510
+ writeEvent(event);
511
+ if (event.type === 'stream:done' || event.type === 'stream:error') {
512
+ unsubscribe();
513
+ clearInterval(heartbeat);
514
+ res.end();
515
+ }
516
+ });
517
+
518
+ // 心跳保活 (每 15 秒)
519
+ const heartbeat = setInterval(() => {
520
+ if (res.writableEnded) {
521
+ clearInterval(heartbeat);
522
+ return;
523
+ }
524
+ res.write(`: ping ${Date.now()}\n\n`);
525
+ }, 15_000);
526
+
527
+ // 客户端断开连接时清理
528
+ res.on('close', () => {
529
+ unsubscribe();
530
+ clearInterval(heartbeat);
531
+ });
532
+ });
533
+
402
534
  /**
403
535
  * GET /api/v1/ai/token-usage
404
536
  * 近 7 日 Token 消耗报告(按日 + 按来源 + 总计)