@bolloon/bolloon-agent 0.1.34 → 0.1.36
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/.auto-evolve-calls +1 -0
- package/.last-auto-evolve-baseline +1 -0
- package/.playwright-mcp/page-2026-06-16T07-51-45-706Z.yml +130 -0
- package/.playwright-mcp/page-2026-06-16T07-56-44-116Z.yml +131 -0
- package/Bolloon.md +144 -0
- package/Dive-into/CITATION.cff +17 -0
- package/Dive-into/LICENSE +25 -0
- package/Dive-into/README.md +598 -0
- package/Dive-into/README_zh.md +599 -0
- package/Dive-into/assets/context.png +0 -0
- package/Dive-into/assets/extensibility.png +0 -0
- package/Dive-into/assets/iteration.png +0 -0
- package/Dive-into/assets/layered_architecture.png +0 -0
- package/Dive-into/assets/main_structure.png +0 -0
- package/Dive-into/assets/permission.png +0 -0
- package/Dive-into/assets/session_compact.png +0 -0
- package/Dive-into/assets/subagent.png +0 -0
- package/Dive-into/paper/Dive_into_Claude_Code.pdf +0 -0
- package/README.md +1 -1
- package/dist/agents/p2p-chat-tools.js +6 -6
- package/dist/agents/permission-mode.js +115 -0
- package/dist/agents/pi-sdk.js +687 -25
- package/dist/agents/pre-tool-validator.js +194 -0
- package/dist/agents/workflow-pivot-loop.js +113 -12
- package/dist/bollharness/src/scripts/checks/check_doc_freshness.js +1 -1
- package/dist/bollharness/src/scripts/checks/check_doc_links.js +1 -1
- package/dist/bollharness/src/scripts/context_router.js +1 -1
- package/dist/bollharness/src/scripts/deploy-guard.js +1 -1
- package/dist/bollharness/src/scripts/guard_router.js +1 -1
- package/dist/bollharness/src/scripts/hooks/_hook_output.js +1 -1
- package/dist/bollharness/src/scripts/hooks/risk-tracker.js +1 -1
- package/dist/bollharness-integration/context-router.js +2 -2
- package/dist/bollharness-integration/guard-checker.js +1 -1
- package/dist/bootstrap/bootstrap.js +117 -0
- package/dist/bootstrap/context-collector.js +312 -0
- package/dist/bootstrap/context-hierarchy.js +218 -0
- package/dist/bootstrap/lifecycle-hooks.js +171 -0
- package/dist/bootstrap/project-context.js +158 -0
- package/dist/context-compaction/auto-compact.js +144 -0
- package/dist/context-compaction/budget-gate.js +28 -0
- package/dist/context-compaction/budget-reduce.js +35 -0
- package/dist/context-compaction/context-collapse.js +66 -0
- package/dist/context-compaction/index.js +21 -0
- package/dist/context-compaction/microcompact.js +51 -0
- package/dist/context-compaction/pipeline.js +123 -0
- package/dist/context-compaction/snip.js +45 -0
- package/dist/context-compaction/token-estimator.js +35 -0
- package/dist/context-compaction/types.js +19 -0
- package/dist/heartbeat/HealthMonitor.js +3 -2
- package/dist/index.js +49 -2
- package/dist/llm/llm-judgment-client.js +32 -30
- package/dist/llm/pi-ai.js +125 -28
- package/dist/llm/system-prompt/health.js +129 -0
- package/dist/llm/system-prompt/registry.js +246 -0
- package/dist/llm/system-prompt/strip-hibsml.js +51 -0
- package/dist/llm/tool-manifest/ask_user_input.js +35 -0
- package/dist/llm/tool-manifest/bash.js +23 -0
- package/dist/llm/tool-manifest/create_file.js +24 -0
- package/dist/llm/tool-manifest/fetch_sports_data.js +26 -0
- package/dist/llm/tool-manifest/image_search.js +24 -0
- package/dist/llm/tool-manifest/index.js +69 -0
- package/dist/llm/tool-manifest/mcp.js +43 -0
- package/dist/llm/tool-manifest/message_compose.js +31 -0
- package/dist/llm/tool-manifest/places.js +83 -0
- package/dist/llm/tool-manifest/present_files.js +21 -0
- package/dist/llm/tool-manifest/recipe.js +40 -0
- package/dist/llm/tool-manifest/recommend_apps.js +23 -0
- package/dist/llm/tool-manifest/str_replace.js +27 -0
- package/dist/llm/tool-manifest/types.js +7 -0
- package/dist/llm/tool-manifest/view.js +24 -0
- package/dist/llm/tool-manifest/weather.js +27 -0
- package/dist/llm/tool-manifest/web.js +51 -0
- package/dist/network/p2p-direct.js +23 -0
- package/dist/network/source-intent-broadcaster.js +203 -0
- package/dist/network/source-intent.js +100 -0
- package/dist/pi-ecosystem-judgment/adaptive-scan.js +279 -0
- package/dist/pi-ecosystem-judgment/causal-judge.js +449 -0
- package/dist/pi-ecosystem-judgment/detect-hook.js +168 -0
- package/dist/pi-ecosystem-judgment/distill-prompt.js +226 -0
- package/dist/pi-ecosystem-judgment/evolve-judgment.js +170 -0
- package/dist/pi-ecosystem-judgment/human-value-pipeline.js +21 -0
- package/dist/pi-ecosystem-judgment/human-value-store.js +283 -22
- package/dist/pi-ecosystem-judgment/injection-gate.js +208 -0
- package/dist/pi-ecosystem-judgment/monitor-gate.js +188 -0
- package/dist/pi-ecosystem-judgment/value-injection.js +8 -2
- package/dist/security/builtin-guards.js +124 -0
- package/dist/security/context-router-tool.js +106 -0
- package/dist/security/input-scanner.js +223 -0
- package/dist/security/react-harness.js +143 -0
- package/dist/security/tool-gate.js +235 -0
- package/dist/utils/auto-evolve-policy.js +117 -0
- package/dist/utils/clamp.js +7 -0
- package/dist/utils/double.js +6 -0
- package/dist/web/client.js +3810 -3830
- package/dist/web/components/p2p/P2PModal.js +188 -0
- package/dist/web/components/p2p/index.js +264 -226
- package/dist/web/components/p2p/p2p-modal.js +657 -0
- package/dist/web/components/p2p/p2p-tools.js +248 -0
- package/dist/web/index.html +60 -49
- package/dist/web/server.js +827 -124
- package/dist/web/style.css +531 -249
- package/dist/web/ui/message-renderer.js +463 -0
- package/dist/web/ui/step-timeline.js +375 -0
- package/lefthook.yml +33 -0
- package/package.json +3 -2
- package/scripts/auto-evolve-loop.ts +481 -0
- package/scripts/auto-evolve-oneshot.sh +155 -0
- package/scripts/auto-evolve-snapshot.sh +136 -0
- package/scripts/build-web.ts +35 -1
- package/scripts/detect-schema-changes.sh +48 -0
- package/scripts/diff-reviewer.ts +159 -0
- package/scripts/validate-system-prompt.ts +142 -0
- package/scripts/weekly-report.ts +364 -0
- package/src/agents/p2p-chat-tools.ts +6 -6
- package/src/agents/permission-mode.ts +127 -0
- package/src/agents/pi-sdk.ts +741 -30
- package/src/agents/pre-tool-validator.ts +213 -0
- package/src/agents/workflow-pivot-loop.ts +110 -19
- package/src/bollharness/CLAUDE.md +1 -1
- package/src/bollharness/README.md +2 -2
- package/src/bollharness/README.zh-CN.md +2 -2
- package/src/bollharness/reference/boll-reference/scripts/hooks/find-boll-root.sh +2 -2
- package/src/bollharness/scripts/context-fragments/truth-source-hierarchy.md +2 -2
- package/src/bollharness/scripts/context-fragments/version-sources.md +1 -1
- package/src/bollharness/scripts/hooks/find-project-root.sh +4 -4
- package/src/bollharness/src/scripts/checks/check_doc_freshness.ts +1 -1
- package/src/bollharness/src/scripts/checks/check_doc_links.ts +1 -1
- package/src/bollharness/src/scripts/context_router.ts +1 -1
- package/src/bollharness/src/scripts/deploy-guard.ts +1 -1
- package/src/bollharness/src/scripts/guard_router.ts +1 -1
- package/src/bollharness/src/scripts/hooks/_hook_output.ts +1 -1
- package/src/bollharness/src/scripts/hooks/risk-tracker.ts +1 -1
- package/src/bollharness/templates/scaffold/.gitignore.append +1 -1
- package/src/bollharness/templates/scaffold/CLAUDE.md +2 -2
- package/src/bollharness-integration/context-router.ts +2 -2
- package/src/bollharness-integration/guard-checker.ts +1 -1
- package/src/bootstrap/bootstrap.ts +135 -0
- package/src/bootstrap/context-collector.ts +371 -0
- package/src/bootstrap/context-hierarchy.ts +283 -0
- package/src/bootstrap/lifecycle-hooks.ts +289 -0
- package/src/bootstrap/project-context.ts +171 -0
- package/src/context-compaction/auto-compact.ts +153 -0
- package/src/context-compaction/budget-gate.ts +32 -0
- package/src/context-compaction/budget-reduce.ts +37 -0
- package/src/context-compaction/context-collapse.ts +72 -0
- package/src/context-compaction/index.ts +24 -0
- package/src/context-compaction/microcompact.ts +54 -0
- package/src/context-compaction/pipeline.ts +133 -0
- package/src/context-compaction/snip.ts +51 -0
- package/src/context-compaction/token-estimator.ts +36 -0
- package/src/context-compaction/types.ts +99 -0
- package/src/heartbeat/HealthMonitor.ts +3 -2
- package/src/index.ts +47 -2
- package/src/llm/llm-judgment-client.ts +36 -35
- package/src/llm/pi-ai.ts +135 -29
- package/src/llm/system-prompt/health.ts +159 -0
- package/src/llm/system-prompt/layers/channel/local.md +14 -0
- package/src/llm/system-prompt/layers/channel/p2p-agent.md +18 -0
- package/src/llm/system-prompt/layers/channel/p2p-visitor.md +19 -0
- package/src/llm/system-prompt/layers/core/artifacts_storage.md +89 -0
- package/src/llm/system-prompt/layers/core/evenhandedness.md +21 -0
- package/src/llm/system-prompt/layers/core/hibs_reminders.md +15 -0
- package/src/llm/system-prompt/layers/core/identity.md +37 -0
- package/src/llm/system-prompt/layers/core/knowledge.md +17 -0
- package/src/llm/system-prompt/layers/core/memory_system.md +12 -0
- package/src/llm/system-prompt/layers/core/network_filesystem.md +28 -0
- package/src/llm/system-prompt/layers/core/refusal.md +37 -0
- package/src/llm/system-prompt/layers/core/tone.md +31 -0
- package/src/llm/system-prompt/layers/core/tools.thin.md +13 -0
- package/src/llm/system-prompt/layers/core/wellbeing.md +41 -0
- package/src/llm/system-prompt/layers/role/architect.md +20 -0
- package/src/llm/system-prompt/layers/role/expert.md +19 -0
- package/src/llm/system-prompt/layers/role/implementer.md +15 -0
- package/src/llm/system-prompt/layers/role/security.md +15 -0
- package/src/llm/system-prompt/layers/tool/artifacts.md +72 -0
- package/src/llm/system-prompt/layers/tool/bash.md +25 -0
- package/src/llm/system-prompt/layers/tool/hibs_api.md +171 -0
- package/src/llm/system-prompt/layers/tool/image_search.md +70 -0
- package/src/llm/system-prompt/layers/tool/manifest.md +89 -0
- package/src/llm/system-prompt/layers/tool/mcp_apps.md +53 -0
- package/src/llm/system-prompt/layers/tool/web_search.md +83 -0
- package/src/llm/system-prompt/registry.ts +325 -0
- package/src/llm/system-prompt/strip-hibsml.ts +52 -0
- package/src/llm/tool-manifest/ask_user_input.ts +37 -0
- package/src/llm/tool-manifest/bash.ts +25 -0
- package/src/llm/tool-manifest/create_file.ts +26 -0
- package/src/llm/tool-manifest/fetch_sports_data.ts +28 -0
- package/src/llm/tool-manifest/image_search.ts +26 -0
- package/src/llm/tool-manifest/index.ts +88 -0
- package/src/llm/tool-manifest/mcp.ts +46 -0
- package/src/llm/tool-manifest/message_compose.ts +33 -0
- package/src/llm/tool-manifest/places.ts +86 -0
- package/src/llm/tool-manifest/present_files.ts +23 -0
- package/src/llm/tool-manifest/recipe.ts +42 -0
- package/src/llm/tool-manifest/recommend_apps.ts +25 -0
- package/src/llm/tool-manifest/str_replace.ts +29 -0
- package/src/llm/tool-manifest/types.ts +52 -0
- package/src/llm/tool-manifest/view.ts +26 -0
- package/src/llm/tool-manifest/weather.ts +29 -0
- package/src/llm/tool-manifest/web.ts +54 -0
- package/src/network/p2p-direct.ts +22 -0
- package/src/network/source-intent-broadcaster.ts +242 -0
- package/src/network/source-intent.ts +167 -0
- package/src/security/builtin-guards.ts +162 -0
- package/src/security/context-router-tool.ts +122 -0
- package/src/security/input-scanner.ts +287 -0
- package/src/security/react-harness.ts +177 -0
- package/src/security/tool-gate.ts +294 -0
- package/src/utils/auto-evolve-policy.ts +138 -0
- package/src/utils/clamp.ts +5 -0
- package/src/web/client.js +105 -2187
- package/src/web/client.ts +4326 -0
- package/src/web/index.html +60 -49
- package/src/web/server.ts +894 -148
- package/src/web/style.css +531 -249
- package/src/web/ui/message-renderer.ts +540 -0
- package/src/web/ui/step-timeline.ts +394 -0
- package/staging/auto-evolve/clean-001/.review-verdict +9 -0
- package/staging/auto-evolve/clean-001/clean-001.patch +14 -0
- package/staging/auto-evolve/e2e-001/.patch-id +1 -0
- package/staging/auto-evolve/e2e-001/.review-verdict +12 -0
- package/staging/auto-evolve/e2e-001/e2e-001.patch +11 -0
- package/staging/auto-evolve/test-bad/.review-verdict +12 -0
- package/staging/auto-evolve/test-bad/test-bad.patch +11 -0
- package/test-results/.last-run.json +6 -0
- package/test-results/src-test-web-loop-status-b-5734c-atus-bar-/346/230/276/347/244/272-spinner-/344/270/215/346/230/276/347/244/272/345/276/252/347/216/257/350/256/241/346/225/260/error-context.md +285 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* message-renderer.ts — 对话显示 UI (2026-06-15 从 client.js 拆出, .js → .ts)
|
|
3
|
+
*
|
|
4
|
+
* 职责 (单一):
|
|
5
|
+
* - 把 user / AI 内容渲染成 DOM 气泡 (含 marked.parse 渲染)
|
|
6
|
+
* - 处理流式 token 累积 (textNode 增量, O(1), 不重排)
|
|
7
|
+
* - finalize 流式消息为正式 AI 气泡
|
|
8
|
+
* - 折叠 think / environment_details 块
|
|
9
|
+
* - 复制 / 重新回答 / 蒸馏为判断 按钮
|
|
10
|
+
* - 挂载 step-timeline (2026-06-15) 步骤状态条到每条 AI 消息内
|
|
11
|
+
*
|
|
12
|
+
* 状态 (本模块私有):
|
|
13
|
+
* - streamingMessageEl / streamingTextNode / streamingText
|
|
14
|
+
* - lastUserCommand / lastAiContent (去重)
|
|
15
|
+
*
|
|
16
|
+
* 依赖 (import):
|
|
17
|
+
* - 浏览器 API (document, marked, fetch, navigator)
|
|
18
|
+
* - ./step-timeline (4 状态步骤条模块)
|
|
19
|
+
*
|
|
20
|
+
* 不依赖 (零业务 import, 防循环):
|
|
21
|
+
* - 不 import client.js
|
|
22
|
+
* - 不 import 任何业务模块
|
|
23
|
+
*
|
|
24
|
+
* 输入 (从 client.js 调用):
|
|
25
|
+
* - addMessage(content, type, save, container, usedIds, ctx)
|
|
26
|
+
* - handleStreamTokenEvent({ streamType, content }, ctx)
|
|
27
|
+
* - finalizeTimelineAsMessage(ctx)
|
|
28
|
+
* - handleStepEvent({ type: 'step_start'|'step_done'|'step_error', ... }, ctx)
|
|
29
|
+
* - escapeHtml(s)
|
|
30
|
+
* - getMessagesContainerForCurrent(currentChannelId, messagesContainers, messagesEl)
|
|
31
|
+
*
|
|
32
|
+
* 输出 (DOM):
|
|
33
|
+
* - .message / .bubble / .think-container / .env-container
|
|
34
|
+
* - .message-streaming (流式期间, finalize 时移除)
|
|
35
|
+
* - .message-actions (复制/重新回答/蒸馏按钮)
|
|
36
|
+
* - .used-judgments-link (P0.5 反向引用)
|
|
37
|
+
* - .step-timeline (气泡内步骤状态条, 由 ./step-timeline 渲染)
|
|
38
|
+
*/
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// 类型定义
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
import { mountStepTimeline, pushStepToTimeline, migrateStepTimeline, getStepTimeline, } from './step-timeline.js';
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// 模块私有状态
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
let streamingMessageEl = null;
|
|
47
|
+
let streamingTextNode = null;
|
|
48
|
+
let streamingText = '';
|
|
49
|
+
let lastUserCommand = '';
|
|
50
|
+
let lastAiContent = '';
|
|
51
|
+
// 滚动限频 (60ms 16fps, 减 reflow)
|
|
52
|
+
let scrollToBottomTimer = null;
|
|
53
|
+
function scheduleScrollToBottom(container) {
|
|
54
|
+
if (!container)
|
|
55
|
+
return;
|
|
56
|
+
if (scrollToBottomTimer)
|
|
57
|
+
return;
|
|
58
|
+
scrollToBottomTimer = setTimeout(() => {
|
|
59
|
+
container.scrollTop = container.scrollHeight;
|
|
60
|
+
scrollToBottomTimer = null;
|
|
61
|
+
}, 60);
|
|
62
|
+
}
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// 容器选择 (主入口或 per-channel 容器)
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
export function getMessagesContainerForCurrent(currentChannelId, messagesContainers, messagesEl) {
|
|
67
|
+
if (currentChannelId && messagesContainers.get(currentChannelId)) {
|
|
68
|
+
return messagesContainers.get(currentChannelId) || null;
|
|
69
|
+
}
|
|
70
|
+
return messagesEl;
|
|
71
|
+
}
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// HTML escape (供 think / env 折叠块用)
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
export function escapeHtml(s) {
|
|
76
|
+
return String(s ?? '').replace(/[&<>"']/g, (c) => ({
|
|
77
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
78
|
+
}[c]));
|
|
79
|
+
}
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// 主入口: 渲染一条消息气泡 (user / ai / system)
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
export function addMessage(content, type, save = true, container, usedJudgmentIds = [], ctx = { messagesEl: null, messagesContainers: new Map(), currentChannelId: null }) {
|
|
84
|
+
const messagesEl = ctx.messagesEl || (typeof document !== 'undefined' ? document.getElementById('messages') : null);
|
|
85
|
+
const messagesContainers = ctx.messagesContainers || new Map();
|
|
86
|
+
const currentChannelId = ctx.currentChannelId;
|
|
87
|
+
const msgContainer = container || getMessagesContainerForCurrent(currentChannelId, messagesContainers, messagesEl);
|
|
88
|
+
// 内存保护: 单个 channel 容器超过 200 条, 旧会话加载时淘汰最旧
|
|
89
|
+
if (!save && msgContainer && msgContainer.children.length > 200) {
|
|
90
|
+
const toRemove = msgContainer.children.length - 200;
|
|
91
|
+
for (let i = 0; i < toRemove; i++) {
|
|
92
|
+
const first = msgContainer.firstElementChild;
|
|
93
|
+
if (first)
|
|
94
|
+
msgContainer.removeChild(first);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// 去重 (save=true 时)
|
|
98
|
+
if (save) {
|
|
99
|
+
const lastContent = type === 'user' ? lastUserCommand : lastAiContent;
|
|
100
|
+
if (lastContent && content === lastContent) {
|
|
101
|
+
console.log(`[addMessage] 跳过重复的 ${type} 消息`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (type === 'user')
|
|
105
|
+
lastUserCommand = content;
|
|
106
|
+
else
|
|
107
|
+
lastAiContent = content;
|
|
108
|
+
}
|
|
109
|
+
const div = document.createElement('div');
|
|
110
|
+
div.className = `message message-${type}`;
|
|
111
|
+
// 清理: 移除 tool_call 标记
|
|
112
|
+
let cleanContent = content
|
|
113
|
+
.replace(/\[TOOL_CALL\][\s\S]*?\[\/TOOL_CALL\]/g, '')
|
|
114
|
+
.replace(/TOOL_CALL[\s\S]*?\/TOOL_CALL/g, '')
|
|
115
|
+
.replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, '')
|
|
116
|
+
.replace(/\{\s*"tool":[\s\S]*?\}/g, '')
|
|
117
|
+
.replace(/\{\s*tool\s*=>\s*["'][^"']+["']\s*(?:,\s*args\s*=>\s*\{[\s\S]*?\})?\s*\}/g, '')
|
|
118
|
+
.replace(/\[Function[^\]]*\]\s*/g, '')
|
|
119
|
+
.trim();
|
|
120
|
+
// think 折叠
|
|
121
|
+
const thinkMatch = cleanContent.match(/<think>([\s\S]*?)<\/think>/);
|
|
122
|
+
let mainContent = cleanContent;
|
|
123
|
+
let thinkContainer = null;
|
|
124
|
+
if (thinkMatch) {
|
|
125
|
+
const thinkContent = thinkMatch[1].trim();
|
|
126
|
+
mainContent = cleanContent.replace(/<think>[\s\S]*?<\/think>/, '').trim();
|
|
127
|
+
thinkContainer = buildThinkContainer(thinkContent);
|
|
128
|
+
}
|
|
129
|
+
// environment_details 折叠 + 身份头
|
|
130
|
+
const envMatch = mainContent.match(/^(.+?)\n<environment_details>([\s\S]*?)<\/environment_details>\n([\s\S]*)$/);
|
|
131
|
+
if (envMatch) {
|
|
132
|
+
const identity = envMatch[1].trim();
|
|
133
|
+
const envDetails = envMatch[2].trim();
|
|
134
|
+
const messageBody = envMatch[3].trim();
|
|
135
|
+
const header = document.createElement('div');
|
|
136
|
+
header.className = 'message-header';
|
|
137
|
+
header.textContent = identity;
|
|
138
|
+
div.appendChild(header);
|
|
139
|
+
div.appendChild(buildEnvContainer(envDetails));
|
|
140
|
+
if (thinkContainer)
|
|
141
|
+
div.appendChild(thinkContainer);
|
|
142
|
+
if (messageBody)
|
|
143
|
+
div.appendChild(buildBubble(messageBody, type));
|
|
144
|
+
}
|
|
145
|
+
else if (cleanContent) {
|
|
146
|
+
if (thinkContainer)
|
|
147
|
+
div.appendChild(thinkContainer);
|
|
148
|
+
div.appendChild(buildBubble(cleanContent, type));
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
return; // 空内容, 不上屏
|
|
152
|
+
}
|
|
153
|
+
// 纯文本用于复制按钮
|
|
154
|
+
const rawContent = cleanContent
|
|
155
|
+
.replace(/<[^>]+>/g, '')
|
|
156
|
+
.replace(/</g, '<')
|
|
157
|
+
.replace(/>/g, '>')
|
|
158
|
+
.replace(/&/g, '&')
|
|
159
|
+
.replace(/ /g, ' ');
|
|
160
|
+
// 时间
|
|
161
|
+
const time = document.createElement('div');
|
|
162
|
+
time.className = 'time';
|
|
163
|
+
time.textContent = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
|
164
|
+
// AI 消息操作按钮
|
|
165
|
+
if (type === 'ai') {
|
|
166
|
+
div.appendChild(buildMessageActions(div, rawContent, ctx));
|
|
167
|
+
}
|
|
168
|
+
// P0.5 反向引用
|
|
169
|
+
if (type === 'ai' && Array.isArray(usedJudgmentIds) && usedJudgmentIds.length > 0) {
|
|
170
|
+
const link = document.createElement('a');
|
|
171
|
+
link.className = 'used-judgments-link';
|
|
172
|
+
link.textContent = `📎 参考 ${usedJudgmentIds.length} 条原则`;
|
|
173
|
+
link.onclick = (e) => {
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
if (typeof ctx.openJudgmentsModalWithFilter === 'function') {
|
|
176
|
+
ctx.openJudgmentsModalWithFilter(usedJudgmentIds);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
div.appendChild(link);
|
|
180
|
+
}
|
|
181
|
+
// 2026-06-15: 每条 AI 消息挂一个空 step-timeline 占位 (用户决策)
|
|
182
|
+
// 后续 step_start/step_done 事件会通过 handleStepEvent 推入
|
|
183
|
+
if (type === 'ai' && msgContainer) {
|
|
184
|
+
mountStepTimeline(div, currentChannelId);
|
|
185
|
+
}
|
|
186
|
+
div.appendChild(time);
|
|
187
|
+
if (msgContainer) {
|
|
188
|
+
msgContainer.appendChild(div);
|
|
189
|
+
scheduleScrollToBottom(msgContainer);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// 私有: think 折叠块
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
function buildThinkContainer(thinkContent) {
|
|
196
|
+
const container = document.createElement('div');
|
|
197
|
+
container.className = 'think-container';
|
|
198
|
+
const toggle = document.createElement('div');
|
|
199
|
+
toggle.className = 'think-toggle';
|
|
200
|
+
toggle.innerHTML = '💭 思考过程 <span class="think-arrow">▸</span>';
|
|
201
|
+
toggle.onclick = function () {
|
|
202
|
+
const details = container.querySelector('.think-content');
|
|
203
|
+
const arrow = toggle.querySelector('.think-arrow');
|
|
204
|
+
if (!details || !arrow)
|
|
205
|
+
return;
|
|
206
|
+
if (details.style.display === 'none') {
|
|
207
|
+
details.style.display = 'block';
|
|
208
|
+
arrow.textContent = '▾';
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
details.style.display = 'none';
|
|
212
|
+
arrow.textContent = '▸';
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
const content = document.createElement('div');
|
|
216
|
+
content.className = 'think-content';
|
|
217
|
+
content.style.display = 'none';
|
|
218
|
+
content.innerHTML = `<pre>${escapeHtml(thinkContent)}</pre>`;
|
|
219
|
+
container.appendChild(toggle);
|
|
220
|
+
container.appendChild(content);
|
|
221
|
+
return container;
|
|
222
|
+
}
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// 私有: environment_details 折叠块
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
function buildEnvContainer(envDetails) {
|
|
227
|
+
const container = document.createElement('div');
|
|
228
|
+
container.className = 'env-container';
|
|
229
|
+
const toggle = document.createElement('div');
|
|
230
|
+
toggle.className = 'env-toggle';
|
|
231
|
+
toggle.innerHTML = '⚙️ 环境信息 <span class="env-arrow">▸</span>';
|
|
232
|
+
toggle.onclick = function () {
|
|
233
|
+
const details = container.querySelector('.environment-details');
|
|
234
|
+
const arrow = toggle.querySelector('.env-arrow');
|
|
235
|
+
if (!details || !arrow)
|
|
236
|
+
return;
|
|
237
|
+
if (details.style.display === 'none') {
|
|
238
|
+
details.style.display = 'block';
|
|
239
|
+
arrow.textContent = '▾';
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
details.style.display = 'none';
|
|
243
|
+
arrow.textContent = '▸';
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
const content = document.createElement('div');
|
|
247
|
+
content.className = 'environment-details';
|
|
248
|
+
content.style.display = 'none';
|
|
249
|
+
content.innerHTML = `<pre>${escapeHtml(envDetails)}</pre>`;
|
|
250
|
+
container.appendChild(toggle);
|
|
251
|
+
container.appendChild(content);
|
|
252
|
+
return container;
|
|
253
|
+
}
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// 私有: 气泡 (marked.parse 渲染)
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
function buildBubble(text, type) {
|
|
258
|
+
const bubble = document.createElement('div');
|
|
259
|
+
bubble.className = `bubble bubble-${type}`;
|
|
260
|
+
// 安全降级: CDN 加载失败时 marked 是 escape 版本, 这里不需要二次 escape
|
|
261
|
+
// window.marked 在 ts 类型上不一定有, 用 any 兜底
|
|
262
|
+
const marked = window.marked;
|
|
263
|
+
bubble.innerHTML = marked ? marked.parse(text) : escapeHtml(text);
|
|
264
|
+
return bubble;
|
|
265
|
+
}
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// 私有: 消息操作按钮 (复制 / 重新回答 / 蒸馏为判断)
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
function buildMessageActions(div, rawContent, ctx) {
|
|
270
|
+
const actions = document.createElement('div');
|
|
271
|
+
actions.className = 'message-actions';
|
|
272
|
+
// 复制
|
|
273
|
+
const copyBtn = document.createElement('button');
|
|
274
|
+
copyBtn.className = 'action-btn copy-btn';
|
|
275
|
+
copyBtn.innerHTML = copyIcon() + ' 复制';
|
|
276
|
+
copyBtn.title = '复制消息';
|
|
277
|
+
copyBtn.onclick = () => {
|
|
278
|
+
navigator.clipboard.writeText(rawContent).then(() => {
|
|
279
|
+
copyBtn.innerHTML = checkIcon() + ' 已复制';
|
|
280
|
+
setTimeout(() => { copyBtn.innerHTML = copyIcon() + ' 复制'; }, 2000);
|
|
281
|
+
});
|
|
282
|
+
};
|
|
283
|
+
actions.appendChild(copyBtn);
|
|
284
|
+
// 蒸馏为判断
|
|
285
|
+
const saveJudgmentBtn = document.createElement('button');
|
|
286
|
+
saveJudgmentBtn.className = 'action-btn save-as-judgment';
|
|
287
|
+
saveJudgmentBtn.title = 'AI 蒸馏为 30-80 字判断力 + 自动演化对齐';
|
|
288
|
+
saveJudgmentBtn.setAttribute('data-decision', rawContent.substring(0, 800));
|
|
289
|
+
if (ctx.currentChannelId)
|
|
290
|
+
saveJudgmentBtn.setAttribute('data-channel-id', ctx.currentChannelId);
|
|
291
|
+
saveJudgmentBtn.innerHTML = shieldIcon() + ' 蒸馏为判断';
|
|
292
|
+
actions.appendChild(saveJudgmentBtn);
|
|
293
|
+
// 重新回答
|
|
294
|
+
const regenBtn = document.createElement('button');
|
|
295
|
+
regenBtn.className = 'action-btn regenerate-btn';
|
|
296
|
+
regenBtn.innerHTML = refreshIcon(false) + ' 重新回答';
|
|
297
|
+
regenBtn.title = '重新生成回复';
|
|
298
|
+
regenBtn.onclick = () => {
|
|
299
|
+
regenBtn.innerHTML = refreshIcon(true) + ' 生成中...';
|
|
300
|
+
regenBtn.disabled = true;
|
|
301
|
+
const messages = div.parentElement?.querySelectorAll('.message') || [];
|
|
302
|
+
let lastUserMsg = '';
|
|
303
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
304
|
+
const msg = messages[i];
|
|
305
|
+
if (msg.classList.contains('message-user')) {
|
|
306
|
+
const bubble = msg.querySelector('.bubble');
|
|
307
|
+
if (bubble) {
|
|
308
|
+
lastUserMsg = bubble.textContent || bubble.innerText || '';
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
fetch('/regenerate', {
|
|
314
|
+
method: 'POST',
|
|
315
|
+
headers: { 'Content-Type': 'application/json' },
|
|
316
|
+
body: JSON.stringify({ channelId: ctx.currentChannelId, userMessage: lastUserMsg })
|
|
317
|
+
}).then(res => {
|
|
318
|
+
if (!res.ok)
|
|
319
|
+
throw new Error('regenerate failed');
|
|
320
|
+
}).catch(err => {
|
|
321
|
+
console.error('重新生成失败:', err);
|
|
322
|
+
regenBtn.innerHTML = refreshIcon(false) + ' 失败';
|
|
323
|
+
setTimeout(() => {
|
|
324
|
+
regenBtn.innerHTML = refreshIcon(false) + ' 重新回答';
|
|
325
|
+
regenBtn.disabled = false;
|
|
326
|
+
}, 2000);
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
actions.appendChild(regenBtn);
|
|
330
|
+
return actions;
|
|
331
|
+
}
|
|
332
|
+
function copyIcon() { return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>'; }
|
|
333
|
+
function checkIcon() { return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>'; }
|
|
334
|
+
function shieldIcon() { return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L4 6v6c0 5 3.5 9.5 8 10 4.5-.5 8-5 8-10V6l-8-4z"></path><path d="M9 12l2 2 4-4"></path></svg>'; }
|
|
335
|
+
function refreshIcon(spin = false) { const cls = spin ? ' class="spin"' : ''; return `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"${cls}><path d="M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16"></path></svg>`; }
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
// 流式 token 处理 (textNode 增量, 不重排)
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
export function handleStreamTokenEvent(data, ctx = { messagesEl: null, messagesContainers: new Map(), currentChannelId: null }) {
|
|
340
|
+
const messagesEl = ctx.messagesEl || (typeof document !== 'undefined' ? document.getElementById('messages') : null);
|
|
341
|
+
const messagesContainers = ctx.messagesContainers || new Map();
|
|
342
|
+
const currentChannelId = ctx.currentChannelId;
|
|
343
|
+
const container = getMessagesContainerForCurrent(currentChannelId, messagesContainers, messagesEl);
|
|
344
|
+
if (!container)
|
|
345
|
+
return;
|
|
346
|
+
const delta = data.content || '';
|
|
347
|
+
if (!delta)
|
|
348
|
+
return;
|
|
349
|
+
if (!streamingMessageEl || !streamingMessageEl.isConnected) {
|
|
350
|
+
// 新建流式消息
|
|
351
|
+
streamingMessageEl = document.createElement('div');
|
|
352
|
+
streamingMessageEl.className = 'message message-ai message-streaming';
|
|
353
|
+
streamingTextNode = document.createTextNode('');
|
|
354
|
+
streamingMessageEl.appendChild(streamingTextNode);
|
|
355
|
+
streamingText = '';
|
|
356
|
+
// 2026-06-15: 流式期间也挂一个空 step-timeline 占位 — step_start/done/error 事件
|
|
357
|
+
// 走 handleStepEvent, getStepTimeline 找到这个流式元素内的 timeline 推入
|
|
358
|
+
mountStepTimeline(streamingMessageEl, currentChannelId);
|
|
359
|
+
container.appendChild(streamingMessageEl);
|
|
360
|
+
// B-3: 首个 token 来时切状态到 streaming (蓝徽), 提示用户 panel 在工作
|
|
361
|
+
if (typeof ctx.setTimelineState === 'function') {
|
|
362
|
+
ctx.setTimelineState('streaming');
|
|
363
|
+
}
|
|
364
|
+
scheduleScrollToBottom(container);
|
|
365
|
+
}
|
|
366
|
+
if (streamingTextNode)
|
|
367
|
+
streamingTextNode.appendData(delta);
|
|
368
|
+
streamingText += delta;
|
|
369
|
+
scheduleScrollToBottom(container);
|
|
370
|
+
}
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
// 流式消息 finalize (done 事件): 移除流式元素, 走 addMessage 走 marked.parse
|
|
373
|
+
// ---------------------------------------------------------------------------
|
|
374
|
+
export function finalizeTimelineAsMessage(ctx = { messagesEl: null, messagesContainers: new Map(), currentChannelId: null }) {
|
|
375
|
+
const messagesEl = ctx.messagesEl || (typeof document !== 'undefined' ? document.getElementById('messages') : null);
|
|
376
|
+
const messagesContainers = ctx.messagesContainers || new Map();
|
|
377
|
+
const currentChannelId = ctx.currentChannelId;
|
|
378
|
+
const container = getMessagesContainerForCurrent(currentChannelId, messagesContainers, messagesEl);
|
|
379
|
+
if (streamingText.trim().length > 0) {
|
|
380
|
+
// 2026-06-15: finalize 时把 streaming 内的 step-timeline 整体搬到新建的正式 AI message 内
|
|
381
|
+
// addMessage 会建一个新 timeline 占位, 先记下 streaming 的引用, addMessage 后再迁移
|
|
382
|
+
// 避免节点从 0 重渲 (10+ 步的任务, 重渲闪烁会很厉害)
|
|
383
|
+
const oldStreamingEl = streamingMessageEl;
|
|
384
|
+
if (oldStreamingEl && oldStreamingEl.parentNode) {
|
|
385
|
+
oldStreamingEl.parentNode.removeChild(oldStreamingEl);
|
|
386
|
+
}
|
|
387
|
+
addMessage(streamingText, 'ai', true, container, ctx.lastUsedJudgmentIds || [], ctx);
|
|
388
|
+
if (oldStreamingEl && container) {
|
|
389
|
+
// 找刚 addMessage 创建的最后一条 ai message
|
|
390
|
+
const newAiMsg = container.querySelector('.message-ai:last-of-type');
|
|
391
|
+
if (newAiMsg && newAiMsg !== oldStreamingEl) {
|
|
392
|
+
migrateStepTimeline(oldStreamingEl, newAiMsg);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// 重置流式状态
|
|
397
|
+
streamingMessageEl = null;
|
|
398
|
+
streamingTextNode = null;
|
|
399
|
+
streamingText = '';
|
|
400
|
+
// B-3: finalize 时切 done 状态 (绿徽), hide 延迟由 client.js 的 hideTimelinePanel 统一管
|
|
401
|
+
if (typeof ctx.setTimelineState === 'function') {
|
|
402
|
+
ctx.setTimelineState('done');
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
export function handleStepEvent(data, ctx = { messagesEl: null, messagesContainers: new Map(), currentChannelId: null }) {
|
|
406
|
+
const messagesEl = ctx.messagesEl || (typeof document !== 'undefined' ? document.getElementById('messages') : null);
|
|
407
|
+
const messagesContainers = ctx.messagesContainers || new Map();
|
|
408
|
+
const currentChannelId = ctx.currentChannelId;
|
|
409
|
+
const container = getMessagesContainerForCurrent(currentChannelId, messagesContainers, messagesEl);
|
|
410
|
+
if (!container)
|
|
411
|
+
return;
|
|
412
|
+
if (!data || !data.type)
|
|
413
|
+
return;
|
|
414
|
+
// 1. 优先用正在流式的元素 (流式期间能即时看到)
|
|
415
|
+
let target = streamingMessageEl && streamingMessageEl.isConnected
|
|
416
|
+
? streamingMessageEl
|
|
417
|
+
: null;
|
|
418
|
+
// 2. 否则用最后一条 AI message
|
|
419
|
+
if (!target) {
|
|
420
|
+
const aiMsgs = container.querySelectorAll('.message-ai');
|
|
421
|
+
if (aiMsgs.length === 0)
|
|
422
|
+
return;
|
|
423
|
+
target = aiMsgs[aiMsgs.length - 1];
|
|
424
|
+
}
|
|
425
|
+
if (!target)
|
|
426
|
+
return;
|
|
427
|
+
const timeline = getStepTimeline(target);
|
|
428
|
+
if (!timeline)
|
|
429
|
+
return;
|
|
430
|
+
pushStepToTimeline(timeline, data.type, {
|
|
431
|
+
tool: data.tool || 'unknown',
|
|
432
|
+
args: data.args,
|
|
433
|
+
success: data.success,
|
|
434
|
+
output: data.output,
|
|
435
|
+
error: data.error,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
// ---------------------------------------------------------------------------
|
|
439
|
+
// 重置模块状态 (切频道时调用)
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
export function resetRendererState() {
|
|
442
|
+
streamingMessageEl = null;
|
|
443
|
+
streamingTextNode = null;
|
|
444
|
+
streamingText = '';
|
|
445
|
+
lastUserCommand = '';
|
|
446
|
+
lastAiContent = '';
|
|
447
|
+
if (scrollToBottomTimer) {
|
|
448
|
+
clearTimeout(scrollToBottomTimer);
|
|
449
|
+
scrollToBottomTimer = null;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
export const MessageRenderer = {
|
|
453
|
+
addMessage,
|
|
454
|
+
handleStreamTokenEvent,
|
|
455
|
+
finalizeTimelineAsMessage,
|
|
456
|
+
handleStepEvent,
|
|
457
|
+
escapeHtml,
|
|
458
|
+
getMessagesContainerForCurrent,
|
|
459
|
+
resetRendererState,
|
|
460
|
+
};
|
|
461
|
+
if (typeof window !== 'undefined') {
|
|
462
|
+
window.MR = MessageRenderer;
|
|
463
|
+
}
|