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.
- package/dashboard/dist/assets/index-B5dbY-cS.js +143 -0
- package/dashboard/dist/assets/{index-BDmJqEkA.css → index-Bun3ld_J.css} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/lib/external/ai/providers/GoogleGeminiProvider.js +7 -2
- package/lib/external/mcp/handlers/bootstrap.js +144 -27
- package/lib/http/HttpServer.js +3 -2
- package/lib/http/routes/ai.js +132 -0
- package/lib/http/routes/candidates.js +369 -78
- package/lib/http/routes/spm.js +143 -0
- package/lib/http/utils/sse-sessions.js +114 -0
- package/lib/http/utils/sse.js +128 -0
- package/lib/service/chat/ChatAgent.js +37 -1
- package/lib/service/chat/tools.js +10 -3
- package/lib/service/spm/SpmService.js +14 -1
- package/package.json +1 -1
- package/dashboard/dist/assets/index-D8dCXLzr.js +0 -129
|
@@ -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
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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 (
|
|
715
|
-
updateData.description =
|
|
797
|
+
if (normalized.description != null && normalized.description !== before.description) {
|
|
798
|
+
updateData.description = normalized.description;
|
|
716
799
|
changed = true;
|
|
717
800
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
|
726
|
-
updateData.reasoning = { ...(entry.reasoning || {}), 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 (
|
|
730
|
-
|
|
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
|
|
package/lib/http/HttpServer.js
CHANGED
|
@@ -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.
|
|
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
|
}
|
package/lib/http/routes/ai.js
CHANGED
|
@@ -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 消耗报告(按日 + 按来源 + 总计)
|