@bolloon/bolloon-agent 0.1.18 → 0.1.20
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/dist/utils/auto-update.js +33 -23
- package/dist/web/client.js +39 -9
- package/dist/web/index.html +6 -6
- package/dist/web/style.css +84 -9
- package/package.json +2 -2
- package/src/utils/auto-update.ts +33 -21
- package/src/web/client.js +39 -9
- package/src/web/index.html +6 -6
- package/src/web/style.css +84 -9
|
@@ -238,25 +238,38 @@ function checkNpmOutdated() {
|
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
240
|
/**
|
|
241
|
-
* 自动更新 npm 包
|
|
241
|
+
* 自动更新 npm 包 (legacy: 只传包名, 让 npm 按本地 semver 约束判断 — 不可靠)
|
|
242
|
+
* 新代码应使用 updatePackagesWithVersion 并传 name@version
|
|
242
243
|
*/
|
|
243
244
|
async function updatePackages(packages) {
|
|
244
|
-
// 记录更新前的版本,用于事后判断"是否真的升级了"
|
|
245
|
-
// 与 getInstalledVersion 的"优先读全局"保持一致 —— install 也用 -g,
|
|
246
|
-
// 否则判断和执行落在不同的目录,永远改不到那个被读取的版本号。
|
|
247
245
|
const targets = packages && packages.length > 0 ? packages : ['@bolloon/bolloon-agent'];
|
|
246
|
+
// 旧 API 没有 version, 加一个空 placeholder 走相同路径
|
|
247
|
+
return updatePackagesWithVersion(targets);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* 自动更新 npm 包, 传 `name@version` 形式的目标让 npm install 不被本地
|
|
251
|
+
* package.json 的 semver 约束卡住 (旧版只传 name 时, npm 看到本地
|
|
252
|
+
* package.json 里 "^0.1.17" 已经满足就判 up to date, 永远升不上去)
|
|
253
|
+
*/
|
|
254
|
+
async function updatePackagesWithVersion(packagesWithVersion) {
|
|
255
|
+
// 解析 `name@version` 形式, 提取 name 用于 before/after 校验
|
|
256
|
+
const parsed = packagesWithVersion.map(spec => {
|
|
257
|
+
const at = spec.lastIndexOf('@');
|
|
258
|
+
if (at <= 0)
|
|
259
|
+
return { name: spec, version: '' };
|
|
260
|
+
return { name: spec.slice(0, at), version: spec.slice(at + 1) };
|
|
261
|
+
});
|
|
262
|
+
const targets = parsed.map(p => p.name);
|
|
248
263
|
const before = new Map();
|
|
249
264
|
for (const p of targets)
|
|
250
265
|
before.set(p, getInstalledVersion(p));
|
|
251
|
-
|
|
252
|
-
const args =
|
|
253
|
-
? ['npm', 'install', '-g', ...targets]
|
|
254
|
-
: ['npm', 'install', ...targets, '--save'];
|
|
266
|
+
// 用 targetsWithVersion 直接拼命令 - 包含具体版本号, 不会被本地约束拦截
|
|
267
|
+
const args = ['npm', 'install', '-g', ...packagesWithVersion];
|
|
255
268
|
log(`\n${CYAN}📦 正在更新包...${RESET}\n`, RESET);
|
|
256
269
|
try {
|
|
257
270
|
execSync(args.join(' '), {
|
|
258
271
|
encoding: 'utf-8',
|
|
259
|
-
timeout: 300000,
|
|
272
|
+
timeout: 300000,
|
|
260
273
|
stdio: 'inherit',
|
|
261
274
|
cwd: process.cwd()
|
|
262
275
|
});
|
|
@@ -269,22 +282,16 @@ async function updatePackages(packages) {
|
|
|
269
282
|
error: e.message
|
|
270
283
|
};
|
|
271
284
|
}
|
|
272
|
-
// install 退出码 0 并不等于"真的升上去了"
|
|
273
|
-
//
|
|
285
|
+
// install 退出码 0 并不等于"真的升上去了" ("up to date" 也是 0)。
|
|
286
|
+
// 重新读取磁盘版本, 只有真的达到 latest 之一才算 updated。
|
|
274
287
|
const upgraded = [];
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
const was = before.get(p);
|
|
288
|
+
for (const p of parsed) {
|
|
289
|
+
const after = getInstalledVersion(p.name);
|
|
290
|
+
const was = before.get(p.name);
|
|
279
291
|
if (after && was && compareVersions(was, after) < 0) {
|
|
280
|
-
upgraded.push(p);
|
|
281
|
-
}
|
|
282
|
-
else if (after && was && compareVersions(was, after) === 0) {
|
|
283
|
-
// 版本没变 —— install 跑过但没改动;不当作"刚升级"
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
failed.push(p);
|
|
292
|
+
upgraded.push(p.name);
|
|
287
293
|
}
|
|
294
|
+
// 版本没变 = npm 仍判 up to date; 不当成功
|
|
288
295
|
}
|
|
289
296
|
if (upgraded.length > 0) {
|
|
290
297
|
return {
|
|
@@ -322,7 +329,10 @@ export async function checkAndUpdate() {
|
|
|
322
329
|
log(` 当前版本: ${bolloonInfo.version}\n`, RESET);
|
|
323
330
|
log(` 最新版本: ${bolloonInfo.latest}\n\n`, RESET);
|
|
324
331
|
// 自动更新
|
|
325
|
-
|
|
332
|
+
// 关键: 把目标版本号也传过去, 否则 `npm install -g @bolloon/bolloon-agent`
|
|
333
|
+
// 会按本地 package.json 的 "^0.1.17" 约束去判断, 永远装不上去
|
|
334
|
+
const targetsWithVersion = bolloonInfo.packages.map(p => `${p.name}@${p.latest}`);
|
|
335
|
+
const result = await updatePackagesWithVersion(targetsWithVersion);
|
|
326
336
|
if (result.success) {
|
|
327
337
|
log(`\n${GREEN}✅ 更新成功!请重新启动应用${RESET}\n`, RESET);
|
|
328
338
|
// 提示用户重启
|
package/dist/web/client.js
CHANGED
|
@@ -4,6 +4,8 @@ if (typeof marked === 'undefined') {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
const messagesEl = document.getElementById('messages');
|
|
7
|
+
const agentStatusEl = document.getElementById('agent-status');
|
|
8
|
+
const agentStatusTextEl = document.getElementById('agent-status-text');
|
|
7
9
|
const input = document.getElementById('input');
|
|
8
10
|
const sendBtn = document.getElementById('send');
|
|
9
11
|
const sidebar = document.getElementById('sidebar');
|
|
@@ -900,20 +902,45 @@ function addMessage(content, type, save = true, container) {
|
|
|
900
902
|
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
901
903
|
}
|
|
902
904
|
|
|
905
|
+
// Agent status bar — sits between the message list and the input box.
|
|
906
|
+
// Two visual states: "planning" (spinner) and "executing" (glowing icon).
|
|
907
|
+
// The text alternates to convey the action loop.
|
|
908
|
+
let agentStatusState = null; // 'planning' | 'executing' | null
|
|
909
|
+
let agentStatusTextIdx = 0;
|
|
910
|
+
|
|
911
|
+
const AGENT_STATUS_TEXTS = {
|
|
912
|
+
planning: ['正在计划下一步行动', '正在规划任务路径', '正在分析当前状态'],
|
|
913
|
+
executing: ['正在执行下一步行动', '正在执行任务', '正在调用工具'],
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
function setAgentStatus(state) {
|
|
917
|
+
if (!agentStatusEl || !agentStatusTextEl) return;
|
|
918
|
+
if (state === null) {
|
|
919
|
+
agentStatusEl.hidden = true;
|
|
920
|
+
agentStatusEl.removeAttribute('data-mode');
|
|
921
|
+
agentStatusState = null;
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
agentStatusEl.hidden = false;
|
|
925
|
+
agentStatusEl.setAttribute('data-mode', state);
|
|
926
|
+
agentStatusState = state;
|
|
927
|
+
// 重排一下文本, 避免长时间停留过于单调
|
|
928
|
+
agentStatusTextIdx = (agentStatusTextIdx + 1) % AGENT_STATUS_TEXTS[state].length;
|
|
929
|
+
agentStatusTextEl.textContent = AGENT_STATUS_TEXTS[state][agentStatusTextIdx];
|
|
930
|
+
}
|
|
931
|
+
|
|
903
932
|
function showTyping(container) {
|
|
904
|
-
const msgContainer = container || messagesContainers.get(currentChannelId) || messagesEl;
|
|
905
933
|
hideTyping();
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
div.innerHTML = '<div class="typing"><div class="typing-spinner"></div><span class="typing-text">思考中...</span></div>';
|
|
910
|
-
msgContainer.appendChild(div);
|
|
911
|
-
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
934
|
+
// 兼容旧路径: container 参数保留但不再使用, status bar 是全局唯一的
|
|
935
|
+
void container;
|
|
936
|
+
setAgentStatus('planning');
|
|
912
937
|
}
|
|
913
938
|
|
|
914
939
|
function hideTyping() {
|
|
915
|
-
|
|
916
|
-
|
|
940
|
+
setAgentStatus(null);
|
|
941
|
+
// 兜底: 旧版本的 #typing 元素可能还残留在 DOM 里, 顺手清掉
|
|
942
|
+
const old = document.getElementById('typing');
|
|
943
|
+
if (old) old.remove();
|
|
917
944
|
hideStreaming();
|
|
918
945
|
}
|
|
919
946
|
|
|
@@ -1346,8 +1373,10 @@ function connect(channelId) {
|
|
|
1346
1373
|
showUserCommand(data.content, container);
|
|
1347
1374
|
} else if (data.type === 'ai') {
|
|
1348
1375
|
addMessage(data.content, 'ai', true, container);
|
|
1376
|
+
hideTyping();
|
|
1349
1377
|
} else if (data.type === 'stream') {
|
|
1350
1378
|
handleStreamEvent(data, container);
|
|
1379
|
+
setAgentStatus('executing');
|
|
1351
1380
|
} else if (data.type === 'regenerating') {
|
|
1352
1381
|
const messages = container.querySelectorAll('.message-ai');
|
|
1353
1382
|
if (messages.length > 0) {
|
|
@@ -1357,6 +1386,7 @@ function connect(channelId) {
|
|
|
1357
1386
|
showTyping(container);
|
|
1358
1387
|
} else if (data.type === 'status') {
|
|
1359
1388
|
handleStatusEvent(data, container);
|
|
1389
|
+
setAgentStatus('executing');
|
|
1360
1390
|
} else if (data.type === 'done') {
|
|
1361
1391
|
hideTyping();
|
|
1362
1392
|
// AI 回复完, 把最后一条 ai 消息落盘 (兜底, 避免 server saveSession 漏写)
|
package/dist/web/index.html
CHANGED
|
@@ -261,12 +261,12 @@
|
|
|
261
261
|
</div>
|
|
262
262
|
</div>
|
|
263
263
|
|
|
264
|
-
<div class="messages" id="messages">
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
264
|
+
<div class="messages" id="messages"></div>
|
|
265
|
+
|
|
266
|
+
<div class="agent-status" id="agent-status" hidden aria-live="polite">
|
|
267
|
+
<span class="agent-status-spinner" aria-hidden="true"></span>
|
|
268
|
+
<span class="agent-status-icon" aria-hidden="true"></span>
|
|
269
|
+
<span class="agent-status-text" id="agent-status-text">正在计划下一步</span>
|
|
270
270
|
</div>
|
|
271
271
|
|
|
272
272
|
<div class="input-area">
|
package/dist/web/style.css
CHANGED
|
@@ -1462,21 +1462,96 @@ body {
|
|
|
1462
1462
|
50% { opacity: 1; transform: scale(1.2); }
|
|
1463
1463
|
}
|
|
1464
1464
|
|
|
1465
|
-
/*
|
|
1466
|
-
.
|
|
1465
|
+
/* Agent status bar (sits between .messages and .input-area) */
|
|
1466
|
+
.agent-status {
|
|
1467
|
+
display: flex;
|
|
1468
|
+
align-items: center;
|
|
1469
|
+
gap: 10px;
|
|
1470
|
+
padding: 10px 24px;
|
|
1471
|
+
background: linear-gradient(
|
|
1472
|
+
180deg,
|
|
1473
|
+
transparent 0%,
|
|
1474
|
+
var(--accent-glow) 100%
|
|
1475
|
+
);
|
|
1476
|
+
border-top: 1px solid var(--border);
|
|
1477
|
+
font-size: 13px;
|
|
1478
|
+
color: var(--text-secondary);
|
|
1467
1479
|
font-family: 'JetBrains Mono', monospace;
|
|
1480
|
+
letter-spacing: 0.3px;
|
|
1481
|
+
animation: statusFadeIn 0.3s ease-out;
|
|
1482
|
+
min-height: 36px;
|
|
1468
1483
|
}
|
|
1469
1484
|
|
|
1470
|
-
.
|
|
1471
|
-
|
|
1485
|
+
.agent-status[hidden] {
|
|
1486
|
+
display: none;
|
|
1472
1487
|
}
|
|
1473
1488
|
|
|
1474
|
-
|
|
1475
|
-
|
|
1489
|
+
@keyframes statusFadeIn {
|
|
1490
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
1491
|
+
to { opacity: 1; transform: translateY(0); }
|
|
1492
|
+
}
|
|
1476
1493
|
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1494
|
+
.agent-status-spinner {
|
|
1495
|
+
display: inline-block;
|
|
1496
|
+
width: 14px;
|
|
1497
|
+
height: 14px;
|
|
1498
|
+
border: 2px solid var(--border-light);
|
|
1499
|
+
border-top-color: var(--accent);
|
|
1500
|
+
border-right-color: var(--accent-hover);
|
|
1501
|
+
border-radius: 50%;
|
|
1502
|
+
animation: spin 0.9s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
|
1503
|
+
will-change: transform;
|
|
1504
|
+
flex-shrink: 0;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
.agent-status-icon {
|
|
1508
|
+
font-size: 14px;
|
|
1509
|
+
flex-shrink: 0;
|
|
1510
|
+
/* 默认不显示, 仅在切换为"执行"状态时显示 */
|
|
1511
|
+
display: none;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
.agent-status[data-mode="executing"] .agent-status-spinner { display: none; }
|
|
1515
|
+
.agent-status[data-mode="executing"] .agent-status-icon { display: inline-block; }
|
|
1516
|
+
.agent-status[data-mode="executing"] .agent-status-icon { animation: pulseGlow 1.4s ease-in-out infinite; }
|
|
1517
|
+
|
|
1518
|
+
.agent-status-text {
|
|
1519
|
+
flex: 1;
|
|
1520
|
+
background: linear-gradient(
|
|
1521
|
+
90deg,
|
|
1522
|
+
var(--text-secondary) 0%,
|
|
1523
|
+
var(--accent) 50%,
|
|
1524
|
+
var(--text-secondary) 100%
|
|
1525
|
+
);
|
|
1526
|
+
background-size: 200% 100%;
|
|
1527
|
+
-webkit-background-clip: text;
|
|
1528
|
+
background-clip: text;
|
|
1529
|
+
color: transparent;
|
|
1530
|
+
-webkit-text-fill-color: transparent;
|
|
1531
|
+
animation: textShimmer 2.4s linear infinite;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
@keyframes textShimmer {
|
|
1535
|
+
0% { background-position: 200% 0; }
|
|
1536
|
+
100% { background-position: -200% 0; }
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
@keyframes pulseGlow {
|
|
1540
|
+
0%, 100% { transform: scale(1); filter: drop-shadow(0 0 2px var(--accent-glow)); }
|
|
1541
|
+
50% { transform: scale(1.15); filter: drop-shadow(0 0 6px var(--accent)); }
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1545
|
+
.agent-status-spinner,
|
|
1546
|
+
.agent-status-text,
|
|
1547
|
+
.agent-status-icon,
|
|
1548
|
+
.agent-status {
|
|
1549
|
+
animation: none;
|
|
1550
|
+
}
|
|
1551
|
+
.agent-status-text {
|
|
1552
|
+
color: var(--text-secondary);
|
|
1553
|
+
-webkit-text-fill-color: var(--text-secondary);
|
|
1554
|
+
}
|
|
1480
1555
|
}
|
|
1481
1556
|
|
|
1482
1557
|
/* Input Area */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bolloon/bolloon-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
|
|
6
6
|
"main": "dist/cli.js",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"src/constraint-runtime"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@bolloon/bolloon-agent": "^0.1.
|
|
35
|
+
"@bolloon/bolloon-agent": "^0.1.19",
|
|
36
36
|
"@bolloon/constraint-runtime": "0.1.0",
|
|
37
37
|
"@chainsafe/libp2p-noise": "^17.0.0",
|
|
38
38
|
"@chainsafe/libp2p-yamux": "^8.0.1",
|
package/src/utils/auto-update.ts
CHANGED
|
@@ -280,27 +280,40 @@ function checkNpmOutdated(): OutdatedPackage[] {
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
/**
|
|
283
|
-
* 自动更新 npm 包
|
|
283
|
+
* 自动更新 npm 包 (legacy: 只传包名, 让 npm 按本地 semver 约束判断 — 不可靠)
|
|
284
|
+
* 新代码应使用 updatePackagesWithVersion 并传 name@version
|
|
284
285
|
*/
|
|
285
286
|
async function updatePackages(packages?: string[]): Promise<UpdateResult> {
|
|
286
|
-
// 记录更新前的版本,用于事后判断"是否真的升级了"
|
|
287
|
-
// 与 getInstalledVersion 的"优先读全局"保持一致 —— install 也用 -g,
|
|
288
|
-
// 否则判断和执行落在不同的目录,永远改不到那个被读取的版本号。
|
|
289
287
|
const targets = packages && packages.length > 0 ? packages : ['@bolloon/bolloon-agent'];
|
|
288
|
+
// 旧 API 没有 version, 加一个空 placeholder 走相同路径
|
|
289
|
+
return updatePackagesWithVersion(targets);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* 自动更新 npm 包, 传 `name@version` 形式的目标让 npm install 不被本地
|
|
294
|
+
* package.json 的 semver 约束卡住 (旧版只传 name 时, npm 看到本地
|
|
295
|
+
* package.json 里 "^0.1.17" 已经满足就判 up to date, 永远升不上去)
|
|
296
|
+
*/
|
|
297
|
+
async function updatePackagesWithVersion(packagesWithVersion: string[]): Promise<UpdateResult> {
|
|
298
|
+
// 解析 `name@version` 形式, 提取 name 用于 before/after 校验
|
|
299
|
+
const parsed = packagesWithVersion.map(spec => {
|
|
300
|
+
const at = spec.lastIndexOf('@');
|
|
301
|
+
if (at <= 0) return { name: spec, version: '' };
|
|
302
|
+
return { name: spec.slice(0, at), version: spec.slice(at + 1) };
|
|
303
|
+
});
|
|
304
|
+
const targets = parsed.map(p => p.name);
|
|
290
305
|
const before = new Map<string, string | null>();
|
|
291
306
|
for (const p of targets) before.set(p, getInstalledVersion(p));
|
|
292
307
|
|
|
293
|
-
|
|
294
|
-
const args =
|
|
295
|
-
? ['npm', 'install', '-g', ...targets]
|
|
296
|
-
: ['npm', 'install', ...targets, '--save'];
|
|
308
|
+
// 用 targetsWithVersion 直接拼命令 - 包含具体版本号, 不会被本地约束拦截
|
|
309
|
+
const args = ['npm', 'install', '-g', ...packagesWithVersion];
|
|
297
310
|
|
|
298
311
|
log(`\n${CYAN}📦 正在更新包...${RESET}\n`, RESET);
|
|
299
312
|
|
|
300
313
|
try {
|
|
301
314
|
execSync(args.join(' '), {
|
|
302
315
|
encoding: 'utf-8',
|
|
303
|
-
timeout: 300000,
|
|
316
|
+
timeout: 300000,
|
|
304
317
|
stdio: 'inherit',
|
|
305
318
|
cwd: process.cwd()
|
|
306
319
|
});
|
|
@@ -313,20 +326,16 @@ async function updatePackages(packages?: string[]): Promise<UpdateResult> {
|
|
|
313
326
|
};
|
|
314
327
|
}
|
|
315
328
|
|
|
316
|
-
// install 退出码 0 并不等于"真的升上去了"
|
|
317
|
-
//
|
|
329
|
+
// install 退出码 0 并不等于"真的升上去了" ("up to date" 也是 0)。
|
|
330
|
+
// 重新读取磁盘版本, 只有真的达到 latest 之一才算 updated。
|
|
318
331
|
const upgraded: string[] = [];
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
const was = before.get(p);
|
|
332
|
+
for (const p of parsed) {
|
|
333
|
+
const after = getInstalledVersion(p.name);
|
|
334
|
+
const was = before.get(p.name);
|
|
323
335
|
if (after && was && compareVersions(was, after) < 0) {
|
|
324
|
-
upgraded.push(p);
|
|
325
|
-
} else if (after && was && compareVersions(was, after) === 0) {
|
|
326
|
-
// 版本没变 —— install 跑过但没改动;不当作"刚升级"
|
|
327
|
-
} else {
|
|
328
|
-
failed.push(p);
|
|
336
|
+
upgraded.push(p.name);
|
|
329
337
|
}
|
|
338
|
+
// 版本没变 = npm 仍判 up to date; 不当成功
|
|
330
339
|
}
|
|
331
340
|
|
|
332
341
|
if (upgraded.length > 0) {
|
|
@@ -376,7 +385,10 @@ export async function checkAndUpdate(): Promise<{
|
|
|
376
385
|
log(` 最新版本: ${bolloonInfo.latest}\n\n`, RESET);
|
|
377
386
|
|
|
378
387
|
// 自动更新
|
|
379
|
-
|
|
388
|
+
// 关键: 把目标版本号也传过去, 否则 `npm install -g @bolloon/bolloon-agent`
|
|
389
|
+
// 会按本地 package.json 的 "^0.1.17" 约束去判断, 永远装不上去
|
|
390
|
+
const targetsWithVersion = bolloonInfo.packages.map(p => `${p.name}@${p.latest}`);
|
|
391
|
+
const result = await updatePackagesWithVersion(targetsWithVersion);
|
|
380
392
|
|
|
381
393
|
if (result.success) {
|
|
382
394
|
log(`\n${GREEN}✅ 更新成功!请重新启动应用${RESET}\n`, RESET);
|
package/src/web/client.js
CHANGED
|
@@ -4,6 +4,8 @@ if (typeof marked === 'undefined') {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
const messagesEl = document.getElementById('messages');
|
|
7
|
+
const agentStatusEl = document.getElementById('agent-status');
|
|
8
|
+
const agentStatusTextEl = document.getElementById('agent-status-text');
|
|
7
9
|
const input = document.getElementById('input');
|
|
8
10
|
const sendBtn = document.getElementById('send');
|
|
9
11
|
const sidebar = document.getElementById('sidebar');
|
|
@@ -900,20 +902,45 @@ function addMessage(content, type, save = true, container) {
|
|
|
900
902
|
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
901
903
|
}
|
|
902
904
|
|
|
905
|
+
// Agent status bar — sits between the message list and the input box.
|
|
906
|
+
// Two visual states: "planning" (spinner) and "executing" (glowing icon).
|
|
907
|
+
// The text alternates to convey the action loop.
|
|
908
|
+
let agentStatusState = null; // 'planning' | 'executing' | null
|
|
909
|
+
let agentStatusTextIdx = 0;
|
|
910
|
+
|
|
911
|
+
const AGENT_STATUS_TEXTS = {
|
|
912
|
+
planning: ['正在计划下一步行动', '正在规划任务路径', '正在分析当前状态'],
|
|
913
|
+
executing: ['正在执行下一步行动', '正在执行任务', '正在调用工具'],
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
function setAgentStatus(state) {
|
|
917
|
+
if (!agentStatusEl || !agentStatusTextEl) return;
|
|
918
|
+
if (state === null) {
|
|
919
|
+
agentStatusEl.hidden = true;
|
|
920
|
+
agentStatusEl.removeAttribute('data-mode');
|
|
921
|
+
agentStatusState = null;
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
agentStatusEl.hidden = false;
|
|
925
|
+
agentStatusEl.setAttribute('data-mode', state);
|
|
926
|
+
agentStatusState = state;
|
|
927
|
+
// 重排一下文本, 避免长时间停留过于单调
|
|
928
|
+
agentStatusTextIdx = (agentStatusTextIdx + 1) % AGENT_STATUS_TEXTS[state].length;
|
|
929
|
+
agentStatusTextEl.textContent = AGENT_STATUS_TEXTS[state][agentStatusTextIdx];
|
|
930
|
+
}
|
|
931
|
+
|
|
903
932
|
function showTyping(container) {
|
|
904
|
-
const msgContainer = container || messagesContainers.get(currentChannelId) || messagesEl;
|
|
905
933
|
hideTyping();
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
div.innerHTML = '<div class="typing"><div class="typing-spinner"></div><span class="typing-text">思考中...</span></div>';
|
|
910
|
-
msgContainer.appendChild(div);
|
|
911
|
-
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
934
|
+
// 兼容旧路径: container 参数保留但不再使用, status bar 是全局唯一的
|
|
935
|
+
void container;
|
|
936
|
+
setAgentStatus('planning');
|
|
912
937
|
}
|
|
913
938
|
|
|
914
939
|
function hideTyping() {
|
|
915
|
-
|
|
916
|
-
|
|
940
|
+
setAgentStatus(null);
|
|
941
|
+
// 兜底: 旧版本的 #typing 元素可能还残留在 DOM 里, 顺手清掉
|
|
942
|
+
const old = document.getElementById('typing');
|
|
943
|
+
if (old) old.remove();
|
|
917
944
|
hideStreaming();
|
|
918
945
|
}
|
|
919
946
|
|
|
@@ -1346,8 +1373,10 @@ function connect(channelId) {
|
|
|
1346
1373
|
showUserCommand(data.content, container);
|
|
1347
1374
|
} else if (data.type === 'ai') {
|
|
1348
1375
|
addMessage(data.content, 'ai', true, container);
|
|
1376
|
+
hideTyping();
|
|
1349
1377
|
} else if (data.type === 'stream') {
|
|
1350
1378
|
handleStreamEvent(data, container);
|
|
1379
|
+
setAgentStatus('executing');
|
|
1351
1380
|
} else if (data.type === 'regenerating') {
|
|
1352
1381
|
const messages = container.querySelectorAll('.message-ai');
|
|
1353
1382
|
if (messages.length > 0) {
|
|
@@ -1357,6 +1386,7 @@ function connect(channelId) {
|
|
|
1357
1386
|
showTyping(container);
|
|
1358
1387
|
} else if (data.type === 'status') {
|
|
1359
1388
|
handleStatusEvent(data, container);
|
|
1389
|
+
setAgentStatus('executing');
|
|
1360
1390
|
} else if (data.type === 'done') {
|
|
1361
1391
|
hideTyping();
|
|
1362
1392
|
// AI 回复完, 把最后一条 ai 消息落盘 (兜底, 避免 server saveSession 漏写)
|
package/src/web/index.html
CHANGED
|
@@ -261,12 +261,12 @@
|
|
|
261
261
|
</div>
|
|
262
262
|
</div>
|
|
263
263
|
|
|
264
|
-
<div class="messages" id="messages">
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
264
|
+
<div class="messages" id="messages"></div>
|
|
265
|
+
|
|
266
|
+
<div class="agent-status" id="agent-status" hidden aria-live="polite">
|
|
267
|
+
<span class="agent-status-spinner" aria-hidden="true"></span>
|
|
268
|
+
<span class="agent-status-icon" aria-hidden="true"></span>
|
|
269
|
+
<span class="agent-status-text" id="agent-status-text">正在计划下一步</span>
|
|
270
270
|
</div>
|
|
271
271
|
|
|
272
272
|
<div class="input-area">
|
package/src/web/style.css
CHANGED
|
@@ -1462,21 +1462,96 @@ body {
|
|
|
1462
1462
|
50% { opacity: 1; transform: scale(1.2); }
|
|
1463
1463
|
}
|
|
1464
1464
|
|
|
1465
|
-
/*
|
|
1466
|
-
.
|
|
1465
|
+
/* Agent status bar (sits between .messages and .input-area) */
|
|
1466
|
+
.agent-status {
|
|
1467
|
+
display: flex;
|
|
1468
|
+
align-items: center;
|
|
1469
|
+
gap: 10px;
|
|
1470
|
+
padding: 10px 24px;
|
|
1471
|
+
background: linear-gradient(
|
|
1472
|
+
180deg,
|
|
1473
|
+
transparent 0%,
|
|
1474
|
+
var(--accent-glow) 100%
|
|
1475
|
+
);
|
|
1476
|
+
border-top: 1px solid var(--border);
|
|
1477
|
+
font-size: 13px;
|
|
1478
|
+
color: var(--text-secondary);
|
|
1467
1479
|
font-family: 'JetBrains Mono', monospace;
|
|
1480
|
+
letter-spacing: 0.3px;
|
|
1481
|
+
animation: statusFadeIn 0.3s ease-out;
|
|
1482
|
+
min-height: 36px;
|
|
1468
1483
|
}
|
|
1469
1484
|
|
|
1470
|
-
.
|
|
1471
|
-
|
|
1485
|
+
.agent-status[hidden] {
|
|
1486
|
+
display: none;
|
|
1472
1487
|
}
|
|
1473
1488
|
|
|
1474
|
-
|
|
1475
|
-
|
|
1489
|
+
@keyframes statusFadeIn {
|
|
1490
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
1491
|
+
to { opacity: 1; transform: translateY(0); }
|
|
1492
|
+
}
|
|
1476
1493
|
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1494
|
+
.agent-status-spinner {
|
|
1495
|
+
display: inline-block;
|
|
1496
|
+
width: 14px;
|
|
1497
|
+
height: 14px;
|
|
1498
|
+
border: 2px solid var(--border-light);
|
|
1499
|
+
border-top-color: var(--accent);
|
|
1500
|
+
border-right-color: var(--accent-hover);
|
|
1501
|
+
border-radius: 50%;
|
|
1502
|
+
animation: spin 0.9s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
|
1503
|
+
will-change: transform;
|
|
1504
|
+
flex-shrink: 0;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
.agent-status-icon {
|
|
1508
|
+
font-size: 14px;
|
|
1509
|
+
flex-shrink: 0;
|
|
1510
|
+
/* 默认不显示, 仅在切换为"执行"状态时显示 */
|
|
1511
|
+
display: none;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
.agent-status[data-mode="executing"] .agent-status-spinner { display: none; }
|
|
1515
|
+
.agent-status[data-mode="executing"] .agent-status-icon { display: inline-block; }
|
|
1516
|
+
.agent-status[data-mode="executing"] .agent-status-icon { animation: pulseGlow 1.4s ease-in-out infinite; }
|
|
1517
|
+
|
|
1518
|
+
.agent-status-text {
|
|
1519
|
+
flex: 1;
|
|
1520
|
+
background: linear-gradient(
|
|
1521
|
+
90deg,
|
|
1522
|
+
var(--text-secondary) 0%,
|
|
1523
|
+
var(--accent) 50%,
|
|
1524
|
+
var(--text-secondary) 100%
|
|
1525
|
+
);
|
|
1526
|
+
background-size: 200% 100%;
|
|
1527
|
+
-webkit-background-clip: text;
|
|
1528
|
+
background-clip: text;
|
|
1529
|
+
color: transparent;
|
|
1530
|
+
-webkit-text-fill-color: transparent;
|
|
1531
|
+
animation: textShimmer 2.4s linear infinite;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
@keyframes textShimmer {
|
|
1535
|
+
0% { background-position: 200% 0; }
|
|
1536
|
+
100% { background-position: -200% 0; }
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
@keyframes pulseGlow {
|
|
1540
|
+
0%, 100% { transform: scale(1); filter: drop-shadow(0 0 2px var(--accent-glow)); }
|
|
1541
|
+
50% { transform: scale(1.15); filter: drop-shadow(0 0 6px var(--accent)); }
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1545
|
+
.agent-status-spinner,
|
|
1546
|
+
.agent-status-text,
|
|
1547
|
+
.agent-status-icon,
|
|
1548
|
+
.agent-status {
|
|
1549
|
+
animation: none;
|
|
1550
|
+
}
|
|
1551
|
+
.agent-status-text {
|
|
1552
|
+
color: var(--text-secondary);
|
|
1553
|
+
-webkit-text-fill-color: var(--text-secondary);
|
|
1554
|
+
}
|
|
1480
1555
|
}
|
|
1481
1556
|
|
|
1482
1557
|
/* Input Area */
|