ai-chat-demo.html 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>AI 知识库问答</title>
  7. <style>
  8. * { margin: 0; padding: 0; box-sizing: border-box; }
  9. body {
  10. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  11. background: #f5f5f5;
  12. height: 100vh;
  13. display: flex;
  14. flex-direction: column;
  15. }
  16. .header {
  17. background: white;
  18. padding: 16px 24px;
  19. border-bottom: 1px solid #e0e0e0;
  20. display: flex;
  21. align-items: center;
  22. gap: 16px;
  23. box-shadow: 0 2px 8px rgba(0,0,0,0.05);
  24. }
  25. .header h1 { font-size: 18px; font-weight: 600; color: #333; }
  26. .session-info {
  27. font-size: 12px;
  28. color: #666;
  29. background: #f0f0f0;
  30. padding: 4px 10px;
  31. border-radius: 12px;
  32. }
  33. .header-actions { margin-left: auto; display: flex; gap: 8px; }
  34. .btn {
  35. padding: 8px 16px;
  36. border: 1px solid #e0e0e0;
  37. background: white;
  38. border-radius: 6px;
  39. cursor: pointer;
  40. font-size: 14px;
  41. transition: background 0.2s;
  42. }
  43. .btn:hover:not(:disabled) { background: #e0e0e0; }
  44. .btn:disabled { opacity: 0.5; cursor: not-allowed; }
  45. .chat-container {
  46. flex: 1;
  47. overflow-y: auto;
  48. padding: 24px;
  49. display: flex;
  50. flex-direction: column;
  51. gap: 16px;
  52. }
  53. .message {
  54. display: flex;
  55. gap: 12px;
  56. max-width: 800px;
  57. margin: 0 auto;
  58. width: 100%;
  59. }
  60. .message.user { flex-direction: row-reverse; }
  61. .avatar {
  62. width: 36px;
  63. height: 36px;
  64. border-radius: 50%;
  65. display: flex;
  66. align-items: center;
  67. justify-content: center;
  68. font-size: 16px;
  69. flex-shrink: 0;
  70. }
  71. .message.user .avatar { background: #4a90e2; color: white; }
  72. .message.ai .avatar { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
  73. .message-content { flex: 1; min-width: 0; }
  74. .message-header {
  75. display: flex;
  76. align-items: center;
  77. gap: 8px;
  78. margin-bottom: 6px;
  79. }
  80. .message.user .message-header { flex-direction: row-reverse; }
  81. .message-name { font-weight: 600; font-size: 13px; color: #333; }
  82. .message-time { font-size: 11px; color: #999; }
  83. .message-body {
  84. padding: 12px 16px;
  85. border-radius: 12px;
  86. line-height: 1.6;
  87. }
  88. .message.user .message-body { background: #4a90e2; color: white; border-bottom-right-radius: 4px; }
  89. .message.ai .message-body { background: white; border: 1px solid #e0e0e0; border-bottom-left-radius: 4px; }
  90. .message-text { word-break: break-word; }
  91. .message-text pre { margin: 8px 0; border-radius: 6px; overflow: hidden; background: #1e1e1e; }
  92. .message-text code { font-family: 'Fira Code', 'Consolas', monospace; font-size: 13px; }
  93. .message-text pre code { padding: 12px; display: block; overflow-x: auto; color: #d4d4d4; }
  94. .message-text p { margin-bottom: 8px; }
  95. .message-text p:last-child { margin-bottom: 0; }
  96. .message-text ul, .message-text ol { margin: 8px 0; padding-left: 20px; }
  97. .message-text li { margin-bottom: 4px; }
  98. .message-text code { background: rgba(0,0,0,0.06); padding: 2px 6px; border-radius: 4px; }
  99. .message-text pre code { background: transparent; padding: 0; }
  100. .ai-image {
  101. max-width: 100%;
  102. height: auto;
  103. border-radius: 8px;
  104. margin-top: 12px;
  105. display: block;
  106. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  107. }
  108. .ai-images-container {
  109. display: grid;
  110. grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  111. gap: 12px;
  112. margin-top: 16px;
  113. padding-top: 16px;
  114. border-top: 1px solid #e0e0e0;
  115. }
  116. .ai-images-container .ai-image {
  117. margin-top: 0;
  118. width: 100%;
  119. }
  120. .thinking-container {
  121. margin-bottom: 10px;
  122. border: 1px solid #ffd666;
  123. border-radius: 8px;
  124. background: #fff9e6;
  125. overflow: hidden;
  126. }
  127. .thinking-header {
  128. padding: 8px 12px;
  129. cursor: pointer;
  130. display: flex;
  131. align-items: center;
  132. justify-content: space-between;
  133. }
  134. .thinking-title {
  135. display: flex;
  136. align-items: center;
  137. gap: 6px;
  138. font-size: 12px;
  139. font-weight: 500;
  140. color: #b8860b;
  141. }
  142. .thinking-icon {
  143. transition: transform 0.3s;
  144. display: inline-block;
  145. }
  146. .thinking-icon.expanded { transform: rotate(90deg); }
  147. .thinking-content {
  148. display: none;
  149. padding: 0 12px 12px;
  150. font-size: 12px;
  151. color: #666;
  152. white-space: pre-wrap;
  153. font-family: 'Fira Code', 'Consolas', monospace;
  154. }
  155. .thinking-content.expanded { display: block; }
  156. .welcome-section {
  157. text-align: center;
  158. padding: 40px 20px;
  159. margin: auto;
  160. color: #666;
  161. }
  162. .welcome-section h2 { font-size: 24px; margin-bottom: 8px; color: #333; }
  163. .suggestions {
  164. display: flex;
  165. flex-wrap: wrap;
  166. gap: 8px;
  167. justify-content: center;
  168. margin-top: 16px;
  169. }
  170. .suggestion-btn {
  171. padding: 8px 16px;
  172. background: white;
  173. border: 1px solid #e0e0e0;
  174. border-radius: 20px;
  175. font-size: 13px;
  176. cursor: pointer;
  177. transition: all 0.2s;
  178. }
  179. .suggestion-btn:hover { background: #4a90e2; color: white; border-color: #4a90e2; }
  180. .typing-indicator {
  181. display: flex;
  182. gap: 4px;
  183. padding: 8px 0;
  184. }
  185. .typing-indicator span {
  186. width: 8px;
  187. height: 8px;
  188. background: #999;
  189. border-radius: 50%;
  190. animation: typing 1.4s infinite;
  191. }
  192. .typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
  193. .typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
  194. @keyframes typing {
  195. 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
  196. 30% { transform: translateY(-6px); opacity: 1; }
  197. }
  198. .streaming-cursor {
  199. display: inline-block;
  200. width: 2px;
  201. height: 1em;
  202. background: #4a90e2;
  203. animation: blink 0.8s infinite;
  204. vertical-align: middle;
  205. margin-left: 2px;
  206. }
  207. @keyframes blink {
  208. 0%, 50% { opacity: 1; }
  209. 51%, 100% { opacity: 0; }
  210. }
  211. .input-container {
  212. background: white;
  213. padding: 16px 24px;
  214. border-top: 1px solid #e0e0e0;
  215. }
  216. .input-wrapper {
  217. max-width: 800px;
  218. margin: 0 auto;
  219. display: flex;
  220. gap: 12px;
  221. align-items: flex-end;
  222. }
  223. .input-box {
  224. flex: 1;
  225. border: 1px solid #e0e0e0;
  226. border-radius: 12px;
  227. padding: 12px 16px;
  228. font-size: 15px;
  229. line-height: 1.5;
  230. resize: none;
  231. min-height: 48px;
  232. max-height: 150px;
  233. font-family: inherit;
  234. outline: none;
  235. transition: border-color 0.2s;
  236. }
  237. .input-box:focus { border-color: #4a90e2; }
  238. .send-btn {
  239. width: 48px;
  240. height: 48px;
  241. border: none;
  242. background: #4a90e2;
  243. color: white;
  244. border-radius: 12px;
  245. cursor: pointer;
  246. display: flex;
  247. align-items: center;
  248. justify-content: center;
  249. transition: all 0.2s;
  250. }
  251. .send-btn:hover:not(:disabled) { background: #357abd; }
  252. .send-btn:disabled { background: #ccc; cursor: not-allowed; }
  253. .send-btn svg { width: 20px; height: 20px; }
  254. </style>
  255. </head>
  256. <body>
  257. <div class="header">
  258. <h1>🤖 AI 知识库问答</h1>
  259. <span class="session-info" id="sessionInfo">新对话</span>
  260. <div class="header-actions">
  261. <button class="btn" id="stopBtn" onclick="stopGeneration()" style="display: none;">停止生成</button>
  262. <button class="btn" onclick="clearChat()">新对话</button>
  263. </div>
  264. </div>
  265. <div class="chat-container" id="chatContainer">
  266. <div class="welcome-section" id="welcomeSection">
  267. <h2>欢迎使用 AI 知识库问答</h2>
  268. <p>基于阿里云百炼平台,智能解答您的各类问题</p>
  269. <div class="suggestions">
  270. <button class="suggestion-btn" onclick="sendMessage('这个平台支持哪些功能?')">平台支持哪些功能?</button>
  271. <button class="suggestion-btn" onclick="sendMessage('如何开始使用?')">如何开始使用?</button>
  272. <button class="suggestion-btn" onclick="sendMessage('有什么使用限制吗?')">有什么使用限制吗?</button>
  273. </div>
  274. </div>
  275. </div>
  276. <div class="input-container">
  277. <div class="input-wrapper">
  278. <textarea
  279. class="input-box"
  280. id="messageInput"
  281. placeholder="输入您的问题..."
  282. rows="1"
  283. onkeydown="handleEnter(event)"
  284. oninput="autoResize(this)"
  285. ></textarea>
  286. <button class="send-btn" id="sendBtn" onclick="sendMessage()">
  287. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  288. <path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/>
  289. </svg>
  290. </button>
  291. </div>
  292. </div>
  293. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/github.min.css">
  294. <script type="module">
  295. import { marked } from 'https://esm.sh/marked@12.0.2';
  296. import hljs from 'https://esm.sh/highlight.js@11.9.0';
  297. import { markedHighlight } from 'https://esm.sh/marked-highlight@2.1.0';
  298. marked.use(
  299. markedHighlight({
  300. langPrefix: 'hljs language-',
  301. highlight(code, lang) {
  302. const language = hljs.getLanguage(lang) ? lang : 'plaintext';
  303. return hljs.highlight(code, { language }).value;
  304. }
  305. })
  306. );
  307. marked.setOptions({
  308. breaks: true,
  309. gfm: true
  310. });
  311. const API_KEY = 'sk-323d807020674fd98accaf971f9b976d';
  312. const APP_ID = '04ce2984f5ce45838d3ba55427d62f97';
  313. const API_URL = `https://dashscope.aliyuncs.com/api/v1/apps/${APP_ID}/completion`;
  314. let sessionId = null;
  315. let isGenerating = false;
  316. let currentController = null;
  317. let messages = [];
  318. let fullThinking = '';
  319. let expandedThinking = new Set();
  320. function handleEnter(e) {
  321. if (e.key === 'Enter' && !e.shiftKey) {
  322. e.preventDefault();
  323. sendMessage();
  324. }
  325. }
  326. function autoResize(el) {
  327. el.style.height = 'auto';
  328. el.style.height = Math.min(el.scrollHeight, 150) + 'px';
  329. }
  330. function escapeHtml(text) {
  331. const div = document.createElement('div');
  332. div.textContent = text;
  333. return div.innerHTML;
  334. }
  335. function formatTime() {
  336. const now = new Date();
  337. return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
  338. }
  339. function clearChat() {
  340. sessionId = null;
  341. messages = [];
  342. fullThinking = '';
  343. expandedThinking = new Set();
  344. document.getElementById('sessionInfo').textContent = '新对话';
  345. document.getElementById('chatContainer').innerHTML = `
  346. <div class="welcome-section" id="welcomeSection">
  347. <h2>欢迎使用 AI 知识库问答</h2>
  348. <p>基于阿里云百炼平台,智能解答您的各类问题</p>
  349. <div class="suggestions">
  350. <button class="suggestion-btn" onclick="sendMessage('这个平台支持哪些功能?')">平台支持哪些功能?</button>
  351. <button class="suggestion-btn" onclick="sendMessage('如何开始使用?')">如何开始使用?</button>
  352. <button class="suggestion-btn" onclick="sendMessage('有什么使用限制吗?')">有什么使用限制吗?</button>
  353. </div>
  354. </div>
  355. `;
  356. }
  357. function removeWelcome() {
  358. const welcome = document.getElementById('welcomeSection');
  359. if (welcome) welcome.remove();
  360. }
  361. function scrollToBottom() {
  362. const container = document.getElementById('chatContainer');
  363. container.scrollTop = container.scrollHeight;
  364. }
  365. async function renderMarkdown(content) {
  366. try {
  367. return await marked.parse(content);
  368. } catch (e) {
  369. return escapeHtml(content);
  370. }
  371. }
  372. function renderImages(urls) {
  373. if (!urls || urls.length === 0) return '';
  374. let html = '<div class="ai-images-container">';
  375. for (const url of urls) {
  376. html += `<img src="${escapeHtml(url)}" class="ai-image" alt="AI生成的图片" loading="lazy">`;
  377. }
  378. html += '</div>';
  379. return html;
  380. }
  381. function filterOutImages(content) {
  382. return content.replace(/!\[[^\]]*\]\([^)]+\)/g, '').trim();
  383. }
  384. function addUserMessage(content) {
  385. removeWelcome();
  386. const container = document.getElementById('chatContainer');
  387. const html = `
  388. <div class="message user">
  389. <div class="avatar">👤</div>
  390. <div class="message-content">
  391. <div class="message-header">
  392. <span class="message-name">我</span>
  393. <span class="message-time">${formatTime()}</span>
  394. </div>
  395. <div class="message-body">
  396. <div class="message-text">${escapeHtml(content)}</div>
  397. </div>
  398. </div>
  399. </div>
  400. `;
  401. container.insertAdjacentHTML('beforeend', html);
  402. scrollToBottom();
  403. }
  404. function addAIMessageStart() {
  405. removeWelcome();
  406. const id = 'ai-' + Date.now();
  407. const container = document.getElementById('chatContainer');
  408. const html = `
  409. <div class="message ai" id="${id}">
  410. <div class="avatar">🤖</div>
  411. <div class="message-content">
  412. <div class="message-header">
  413. <span class="message-name">AI 助手</span>
  414. <span class="message-time">${formatTime()}</span>
  415. </div>
  416. <div class="message-body">
  417. <div class="typing-indicator">
  418. <span></span><span></span><span></span>
  419. </div>
  420. </div>
  421. </div>
  422. </div>
  423. `;
  424. container.insertAdjacentHTML('beforeend', html);
  425. scrollToBottom();
  426. return id;
  427. }
  428. let currentStreamingContent = {};
  429. let currentStreamingHtml = {};
  430. let pendingUpdate = {};
  431. function updateAIMessage(id, thinking, thinkingExpanded, content, contentHtml) {
  432. const msgEl = document.getElementById(id);
  433. if (!msgEl) return;
  434. if (!currentStreamingContent[id]) {
  435. currentStreamingContent[id] = '';
  436. }
  437. if (content !== currentStreamingContent[id]) {
  438. currentStreamingContent[id] = content;
  439. currentStreamingHtml[id] = { thinking, thinkingExpanded, contentHtml };
  440. }
  441. if (!pendingUpdate[id]) {
  442. pendingUpdate[id] = true;
  443. requestAnimationFrame(() => {
  444. renderStreamingUpdate(id);
  445. pendingUpdate[id] = false;
  446. });
  447. }
  448. }
  449. function renderStreamingUpdate(id) {
  450. const msgEl = document.getElementById(id);
  451. if (!msgEl) return;
  452. const data = currentStreamingHtml[id];
  453. if (!data) return;
  454. const { thinking, thinkingExpanded, contentHtml } = data;
  455. const bodyEl = msgEl.querySelector('.message-body');
  456. if (!bodyEl) return;
  457. const textEl = bodyEl.querySelector('.message-text');
  458. if (textEl) {
  459. if (textEl.innerHTML !== contentHtml) {
  460. textEl.innerHTML = contentHtml;
  461. }
  462. }
  463. if (thinking !== undefined) {
  464. const hasThinkContainer = bodyEl.querySelector('.thinking-container');
  465. if (thinking && !hasThinkContainer) {
  466. const thinkingHtml = `
  467. <div class="thinking-container" onclick="toggleThinking('${id}')">
  468. <div class="thinking-header">
  469. <div class="thinking-title">
  470. <span class="thinking-icon ${thinkingExpanded ? 'expanded' : ''}">▶</span>
  471. <span>思考过程</span>
  472. </div>
  473. </div>
  474. <div class="thinking-content ${thinkingExpanded ? 'expanded' : ''}">${escapeHtml(thinking)}</div>
  475. </div>
  476. `;
  477. bodyEl.insertAdjacentHTML('afterbegin', thinkingHtml);
  478. } else if (hasThinkContainer) {
  479. const thinkContent = hasThinkContainer.querySelector('.thinking-content');
  480. if (thinkContent) {
  481. thinkContent.textContent = thinking;
  482. }
  483. }
  484. }
  485. scrollToBottom();
  486. }
  487. function finalizeAIMessage(id, thinking, content, contentHtml, imageHtml = '') {
  488. const msgEl = document.getElementById(id);
  489. if (!msgEl) return;
  490. delete currentStreamingContent[id];
  491. delete currentStreamingHtml[id];
  492. delete pendingUpdate[id];
  493. let thinkingHtml = '';
  494. if (thinking) {
  495. const expanded = expandedThinking.has(id) ? 'expanded' : '';
  496. thinkingHtml = `
  497. <div class="thinking-container" onclick="toggleThinking('${id}')">
  498. <div class="thinking-header">
  499. <div class="thinking-title">
  500. <span class="thinking-icon ${expanded}">▶</span>
  501. <span>思考过程</span>
  502. </div>
  503. </div>
  504. <div class="thinking-content ${expanded}">${escapeHtml(thinking)}</div>
  505. </div>
  506. `;
  507. }
  508. msgEl.querySelector('.message-body').innerHTML = thinkingHtml + `
  509. <div class="message-text">${contentHtml}</div>
  510. ${imageHtml}
  511. `;
  512. scrollToBottom();
  513. }
  514. function toggleThinking(id) {
  515. if (expandedThinking.has(id)) {
  516. expandedThinking.delete(id);
  517. } else {
  518. expandedThinking.add(id);
  519. }
  520. const msgEl = document.getElementById(id);
  521. if (!msgEl) return;
  522. const content = msgEl.querySelector('.thinking-content');
  523. const icon = msgEl.querySelector('.thinking-icon');
  524. if (content) content.classList.toggle('expanded');
  525. if (icon) icon.classList.toggle('expanded');
  526. }
  527. function setInputEnabled(enabled) {
  528. document.getElementById('messageInput').disabled = !enabled;
  529. document.getElementById('sendBtn').disabled = !enabled;
  530. document.getElementById('stopBtn').style.display = enabled ? 'none' : 'inline-block';
  531. isGenerating = !enabled;
  532. }
  533. function stopGeneration() {
  534. if (currentController) {
  535. currentController.abort();
  536. currentController = null;
  537. }
  538. setInputEnabled(true);
  539. }
  540. async function sendMessage(content) {
  541. content = content || document.getElementById('messageInput').value.trim();
  542. if (!content || isGenerating) return;
  543. document.getElementById('messageInput').value = '';
  544. document.getElementById('messageInput').style.height = 'auto';
  545. messages.push({ role: 'user', content });
  546. addUserMessage(content);
  547. setInputEnabled(false);
  548. fullThinking = '';
  549. currentStreamingContent = {};
  550. currentStreamingHtml = {};
  551. const aiMsgId = addAIMessageStart();
  552. try {
  553. currentController = new AbortController();
  554. const requestBody = {
  555. input: { prompt: content },
  556. parameters: {
  557. incremental_output: true,
  558. has_thoughts: true
  559. },
  560. debug: {}
  561. };
  562. if (sessionId) {
  563. requestBody.input.session_id = sessionId;
  564. }
  565. const response = await fetch(API_URL, {
  566. method: 'POST',
  567. headers: {
  568. 'Authorization': `Bearer ${API_KEY}`,
  569. 'Content-Type': 'application/json',
  570. 'X-DashScope-SSE': 'enable'
  571. },
  572. body: JSON.stringify(requestBody),
  573. signal: currentController.signal
  574. });
  575. if (!response.ok) {
  576. const errorData = await response.json().catch(() => ({}));
  577. throw new Error(errorData.message || `请求失败: ${response.status}`);
  578. }
  579. let fullContent = '';
  580. let buffer = '';
  581. let thinkingContent = '';
  582. let hasThought = false;
  583. let chunkIndex = 0;
  584. let imageUrls = [];
  585. console.log('='.repeat(80));
  586. console.log('🚀 开始接收 AI 响应流');
  587. console.log('='.repeat(80));
  588. const reader = response.body.getReader();
  589. const decoder = new TextDecoder();
  590. while (true) {
  591. const { done, value } = await reader.read();
  592. if (done) {
  593. console.log('\n' + '='.repeat(80));
  594. console.log('✅ 响应流接收完毕');
  595. console.log('='.repeat(80));
  596. break;
  597. }
  598. chunkIndex++;
  599. console.log('\n' + '-'.repeat(80));
  600. console.log(`📦 数据块 #${chunkIndex}`);
  601. console.log('-'.repeat(80));
  602. console.log('原始二进制数据长度:', value ? value.length : 0);
  603. buffer += decoder.decode(value, { stream: true });
  604. const lines = buffer.split('\n');
  605. buffer = lines.pop() || '';
  606. console.log(`📝 分割出 ${lines.length} 行数据`);
  607. for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
  608. const line = lines[lineIndex];
  609. console.log(`\n ━━ 行 #${lineIndex + 1} ━━`);
  610. console.log(' 原始行内容:', JSON.stringify(line));
  611. if (line.startsWith('data:')) {
  612. const data = line.slice(5).trim();
  613. console.log(' ✨ 识别到 data: 前缀');
  614. console.log(' data 内容:', JSON.stringify(data));
  615. if (!data) {
  616. console.log(' ⚠️ data 为空,跳过');
  617. continue;
  618. }
  619. try {
  620. const json = JSON.parse(data);
  621. console.log('\n 🎯 完整 JSON 对象:');
  622. console.log(JSON.stringify(json, null, 2));
  623. if (json.output?.session_id) {
  624. sessionId = json.output.session_id;
  625. document.getElementById('sessionInfo').textContent = '会话ID: ' + sessionId.substring(0, 8) + '...';
  626. console.log('\n 🔑 会话 ID 更新:', sessionId);
  627. }
  628. if (json.output?.thoughts && json.output.thoughts.length > 0) {
  629. console.log('\n 💭 检测到 thoughts 字段:');
  630. console.log(' thoughts 数量:', json.output.thoughts.length);
  631. for (let i = 0; i < json.output.thoughts.length; i++) {
  632. const thought = json.output.thoughts[i];
  633. console.log(` Thought #${i + 1}:`, JSON.stringify(thought, null, 2));
  634. if (thought.action_type === 'mcp') {
  635. console.log('\n 🔧 检测到 MCP 工具调用:', thought.action_name);
  636. if (thought.observation) {
  637. try {
  638. const observation = JSON.parse(thought.observation);
  639. console.log(' 📦 observation 解析后:', JSON.stringify(observation, null, 2));
  640. if (observation.content && observation.content.length > 0) {
  641. for (const item of observation.content) {
  642. if (item.type === 'text') {
  643. try {
  644. const textData = JSON.parse(item.text);
  645. console.log(' 📦 text 解析后:', JSON.stringify(textData, null, 2));
  646. if (textData.results && textData.results.length > 0) {
  647. for (const imgUrl of textData.results) {
  648. console.log(' 🖼️ 发现图片 URL:', imgUrl);
  649. imageUrls.push(imgUrl);
  650. }
  651. }
  652. } catch (e) {
  653. console.log(' 📝 text 不是 JSON 格式,直接使用:', item.text);
  654. }
  655. }
  656. }
  657. }
  658. } catch (e) {
  659. console.log(' ⚠️ observation 解析失败:', e.message);
  660. }
  661. }
  662. }
  663. if (thought.thought) {
  664. thinkingContent += thought.thought;
  665. fullThinking = thinkingContent;
  666. hasThought = true;
  667. const html = await renderMarkdown(fullContent);
  668. updateAIMessage(aiMsgId, thinkingContent, true, fullContent, html + '<span class="streaming-cursor"></span>');
  669. }
  670. }
  671. }
  672. if (json.output?.text) {
  673. console.log('\n 📄 检测到 text 字段:');
  674. console.log(' text 内容:', JSON.stringify(json.output.text));
  675. fullContent += json.output.text;
  676. const html = await renderMarkdown(fullContent);
  677. if (hasThought) {
  678. updateAIMessage(aiMsgId, thinkingContent, true, fullContent, html + '<span class="streaming-cursor"></span>');
  679. } else {
  680. updateAIMessage(aiMsgId, '', false, fullContent, html + '<span class="streaming-cursor"></span>');
  681. }
  682. }
  683. if (json.error) {
  684. console.log('\n ❌ 检测到 error 字段:');
  685. console.log(JSON.stringify(json.error, null, 2));
  686. throw new Error(json.error.message || '响应错误');
  687. }
  688. console.log('\n 📊 完整对象结构分析:');
  689. console.log(' 顶层键:', Object.keys(json));
  690. if (json.output) {
  691. console.log(' output 键:', Object.keys(json.output));
  692. }
  693. } catch (e) {
  694. if (e.message !== 'Unexpected end of JSON input') {
  695. console.warn(' ❌ 解析错误:', e.message);
  696. console.warn(' 错误堆栈:', e.stack);
  697. } else {
  698. console.log(' ⏳ JSON 不完整,等待下一个数据块');
  699. }
  700. }
  701. } else if (line.trim() === '') {
  702. console.log(' 空行,跳过');
  703. } else {
  704. console.log(' ⚠️ 非 data: 开头的行,内容:', JSON.stringify(line));
  705. }
  706. }
  707. }
  708. expandedThinking.delete(aiMsgId);
  709. const contentWithoutImages = filterOutImages(fullContent);
  710. const finalHtml = await renderMarkdown(contentWithoutImages);
  711. const finalImageHtml = renderImages(imageUrls);
  712. finalizeAIMessage(aiMsgId, hasThought ? thinkingContent : '', contentWithoutImages, finalHtml, finalImageHtml);
  713. messages.push({ role: 'assistant', content: contentWithoutImages });
  714. } catch (error) {
  715. if (error.name === 'AbortError') {
  716. if (fullThinking || messages.length > 0) {
  717. expandedThinking.delete(aiMsgId);
  718. const lastContent = messages[messages.length - 1]?.content || '';
  719. const contentWithoutImages = filterOutImages(lastContent);
  720. const html = await renderMarkdown(contentWithoutImages);
  721. const finalImageHtml = renderImages(imageUrls);
  722. finalizeAIMessage(aiMsgId, fullThinking, contentWithoutImages, html, finalImageHtml);
  723. }
  724. } else {
  725. console.error('请求错误:', error);
  726. const msgEl = document.getElementById(aiMsgId);
  727. if (msgEl) {
  728. msgEl.querySelector('.message-body').innerHTML = `
  729. <div class="message-text" style="color: red;">❌ 错误: ${escapeHtml(error.message)}</div>
  730. `;
  731. }
  732. }
  733. }
  734. currentController = null;
  735. setInputEnabled(true);
  736. document.getElementById('messageInput').focus();
  737. }
  738. window.handleEnter = handleEnter;
  739. window.autoResize = autoResize;
  740. window.sendMessage = sendMessage;
  741. window.stopGeneration = stopGeneration;
  742. window.clearChat = clearChat;
  743. window.toggleThinking = toggleThinking;
  744. </script>
  745. </body>
  746. </html>