agent-loft 0.2.17 → 0.2.19
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/package.json +1 -1
- package/src/agents/claude/driver.ts +7 -0
- package/src/agents/codex/driver.ts +5 -0
- package/src/channels/feishu/cards.ts +1 -1
- package/src/channels/web/api.ts +2 -1
- package/src/channels/web/server.ts +22 -8
- package/src/core/hub.ts +46 -1
- package/src/core/store.ts +5 -0
- package/src/core/types.ts +2 -0
- package/web/app.js +47 -40
- package/web/style.css +12 -0
package/package.json
CHANGED
|
@@ -116,6 +116,13 @@ export class ClaudeDriver implements AgentDriver {
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
async sessionTitle(id: string, cwd: string): Promise<string | undefined> {
|
|
120
|
+
if (!SESSION_ID.test(id)) return undefined;
|
|
121
|
+
// SDK 的 customTitle 已经是「手动改的名 || 自动生成的 aiTitle」,不含首条消息兜底
|
|
122
|
+
const info = await getSessionInfo(id, { dir: cwd });
|
|
123
|
+
return info?.customTitle?.replace(/\s+/g, " ").trim() || undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
119
126
|
/**
|
|
120
127
|
* 把会话记录整理成「一问一答」:用户一条,助手把这一轮所有文本拼成一条,
|
|
121
128
|
* 中间的工具调用作为过程摘要挂在助手那条上。工具结果(type=user 的 tool_result)
|
|
@@ -102,6 +102,11 @@ export class CodexDriver implements AgentDriver {
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
async sessionTitle(id: string): Promise<string | undefined> {
|
|
106
|
+
const res = await appServer.call<{ thread: Thread }>("thread/read", { threadId: id, includeTurns: false });
|
|
107
|
+
return res.thread.name?.replace(/\s+/g, " ").trim() || undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
105
110
|
async readHistory(id: string): Promise<HistoryMessage[]> {
|
|
106
111
|
const res = await appServer.call<{
|
|
107
112
|
thread: { turns?: { id?: string; items?: HistoryItem[]; startedAt?: number | null; completedAt?: number | null }[] };
|
|
@@ -61,8 +61,8 @@ export function approvalCard(interaction: Interaction, ctx: CardCtx) {
|
|
|
61
61
|
const id = interaction.id;
|
|
62
62
|
const buttons = [
|
|
63
63
|
button("允许一次", { a: "approval", r: id, d: "allow-once" }, ctx, "primary"),
|
|
64
|
-
...(spec.alwaysLabel ? [button(spec.alwaysLabel, { a: "approval", r: id, d: "allow-always" }, ctx)] : []),
|
|
65
64
|
...(spec.allowSession ? [button("本会话全部允许", { a: "approval", r: id, d: "allow-session" }, ctx)] : []),
|
|
65
|
+
...(spec.alwaysLabel ? [button(spec.alwaysLabel, { a: "approval", r: id, d: "allow-always" }, ctx)] : []),
|
|
66
66
|
button("拒绝", { a: "approval", r: id, d: "deny" }, ctx, "danger"),
|
|
67
67
|
];
|
|
68
68
|
return card(
|
package/src/channels/web/api.ts
CHANGED
|
@@ -434,7 +434,8 @@ export async function handleApi(req: IncomingMessage, res: ServerResponse, url:
|
|
|
434
434
|
if (!sub && method === "PATCH") {
|
|
435
435
|
const body = await readJsonBody<{ title?: string; hidden?: boolean }>(req);
|
|
436
436
|
updateConversation(key, {
|
|
437
|
-
|
|
437
|
+
// 手动改过名,就不再拿 agent 生成的标题覆盖
|
|
438
|
+
...(typeof body.title === "string" ? { title: body.title.trim().slice(0, 80), titleAuto: false } : {}),
|
|
438
439
|
...(typeof body.hidden === "boolean" ? { hidden: body.hidden } : {}),
|
|
439
440
|
});
|
|
440
441
|
hub.emitEvent(key, { type: "meta" });
|
|
@@ -13,6 +13,7 @@ import { parseCommand } from "../../core/commands.js";
|
|
|
13
13
|
import { hub, type EventEnvelope } from "../../core/hub.js";
|
|
14
14
|
import { interactions, type Interaction } from "../../core/interactions.js";
|
|
15
15
|
import { getConversation } from "../../core/store.js";
|
|
16
|
+
import { RUNNING_VERSION } from "../../core/upgrade.js";
|
|
16
17
|
import type { Settlement } from "../../core/types.js";
|
|
17
18
|
import { authed, handleApi, takeUploads } from "./api.js";
|
|
18
19
|
import { authEvents, ensureSetupToken, validateSession } from "./auth.js";
|
|
@@ -60,18 +61,33 @@ const MIME: Record<string, string> = {
|
|
|
60
61
|
".webmanifest": "application/manifest+json",
|
|
61
62
|
};
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
/**
|
|
65
|
+
* 静态文件一律 no-cache + Last-Modified:每次都向服务端确认一下,没变就回 304。
|
|
66
|
+
* 升级后 app.js、style.css、vendor 都会变,但地址不变,带 max-age 的话刷新页面还会拿到旧的。
|
|
67
|
+
*/
|
|
68
|
+
function serveFile(req: http.IncomingMessage, res: http.ServerResponse, file: string): void {
|
|
64
69
|
fs.stat(file, (err, stat) => {
|
|
65
70
|
if (err || !stat.isFile()) {
|
|
66
71
|
res.writeHead(404, SECURITY_HEADERS);
|
|
67
72
|
res.end("not found");
|
|
68
73
|
return;
|
|
69
74
|
}
|
|
70
|
-
|
|
75
|
+
const lastModified = new Date(Math.floor(stat.mtimeMs / 1000) * 1000);
|
|
76
|
+
const since = Date.parse(req.headers["if-modified-since"] ?? "");
|
|
77
|
+
const headers = {
|
|
71
78
|
...SECURITY_HEADERS,
|
|
79
|
+
"Cache-Control": "no-cache",
|
|
80
|
+
"Last-Modified": lastModified.toUTCString(),
|
|
81
|
+
};
|
|
82
|
+
if (!Number.isNaN(since) && lastModified.getTime() <= since) {
|
|
83
|
+
res.writeHead(304, headers);
|
|
84
|
+
res.end();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
res.writeHead(200, {
|
|
88
|
+
...headers,
|
|
72
89
|
"Content-Type": MIME[path.extname(file)] ?? "application/octet-stream",
|
|
73
90
|
"Content-Length": stat.size,
|
|
74
|
-
"Cache-Control": cache,
|
|
75
91
|
});
|
|
76
92
|
fs.createReadStream(file).pipe(res);
|
|
77
93
|
});
|
|
@@ -81,7 +97,7 @@ function serveStatic(req: http.IncomingMessage, res: http.ServerResponse, url: U
|
|
|
81
97
|
const vendor = VENDOR[url.pathname];
|
|
82
98
|
if (vendor) {
|
|
83
99
|
try {
|
|
84
|
-
serveFile(res, vendor()
|
|
100
|
+
serveFile(req, res, vendor());
|
|
85
101
|
} catch {
|
|
86
102
|
res.writeHead(404);
|
|
87
103
|
res.end();
|
|
@@ -108,9 +124,7 @@ function serveStatic(req: http.IncomingMessage, res: http.ServerResponse, url: U
|
|
|
108
124
|
res.end();
|
|
109
125
|
return;
|
|
110
126
|
}
|
|
111
|
-
|
|
112
|
-
serveFile(res, file, rel === "/index.html" || rel === "/sw.js" ? "no-cache" : "public, max-age=300");
|
|
113
|
-
void req;
|
|
127
|
+
serveFile(req, res, file);
|
|
114
128
|
}
|
|
115
129
|
|
|
116
130
|
// ---------- WebSocket ----------
|
|
@@ -397,7 +411,7 @@ export async function startWeb(): Promise<void> {
|
|
|
397
411
|
});
|
|
398
412
|
ws.on("close", () => clients.delete(client));
|
|
399
413
|
ws.on("error", () => clients.delete(client));
|
|
400
|
-
send(client, { t: "hello", seq: hub.currentSeq(), epoch: EPOCH });
|
|
414
|
+
send(client, { t: "hello", seq: hub.currentSeq(), epoch: EPOCH, version: RUNNING_VERSION });
|
|
401
415
|
});
|
|
402
416
|
} catch (err) {
|
|
403
417
|
console.error("[web] WebSocket 握手出错:", err);
|
package/src/core/hub.ts
CHANGED
|
@@ -159,6 +159,8 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
|
|
|
159
159
|
private locks = new Map<string, Promise<unknown>>();
|
|
160
160
|
/** 最近一次点停止的时间:紧接着冒出来的「被打断」错误是预期内的,不当错误显示 */
|
|
161
161
|
private stoppedAt = new Map<string, number>();
|
|
162
|
+
/** 正在等 agent 标题的对话,避免一轮轮叠加重试 */
|
|
163
|
+
private titleSyncing = new Set<string>();
|
|
162
164
|
/**
|
|
163
165
|
* 每条会话的「生命周期代次」:开新会话、分叉、恢复、切 agent、切目录时加一。
|
|
164
166
|
* 渠道收到消息后还要先做异步的事(飞书下载附件),期间会话被换掉了的话,
|
|
@@ -497,6 +499,7 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
|
|
|
497
499
|
recordFooter(holder.session?.sessionId ?? (conv ? agentBinding(conv, agent).sessionId : undefined), result.text, footer);
|
|
498
500
|
this.setInflight(key, (this.inflight.get(key) ?? 1) - 1);
|
|
499
501
|
this.refreshRuntimeOnce(key, liveId);
|
|
502
|
+
this.syncTitle(key, agent);
|
|
500
503
|
},
|
|
501
504
|
onError: (err) => {
|
|
502
505
|
if (!alive()) return;
|
|
@@ -555,6 +558,47 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
|
|
|
555
558
|
return session;
|
|
556
559
|
}
|
|
557
560
|
|
|
561
|
+
/**
|
|
562
|
+
* 自动标题换成 agent 自己起的。agent 的标题是异步生成的,一轮结束时未必已经有了,
|
|
563
|
+
* 所以隔一会儿再问几次。每轮结束都会再同步一次:agent 后来改了标题(比如在终端里 /rename)也能跟上。
|
|
564
|
+
*/
|
|
565
|
+
private syncTitle(key: string, agent: string, attempt = 0): void {
|
|
566
|
+
const delays = [1_000, 5_000, 15_000, 45_000];
|
|
567
|
+
const driver = this.drivers.get(agent);
|
|
568
|
+
if (!driver?.sessionTitle) return;
|
|
569
|
+
if (attempt === 0) {
|
|
570
|
+
if (this.titleSyncing.has(key)) return; // 上一轮的重试还在跑
|
|
571
|
+
this.titleSyncing.add(key);
|
|
572
|
+
}
|
|
573
|
+
if (attempt >= delays.length) {
|
|
574
|
+
this.titleSyncing.delete(key);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
setTimeout(() => {
|
|
578
|
+
void (async () => {
|
|
579
|
+
const conv = getConversation(key);
|
|
580
|
+
const sessionId = conv && conv.agent === agent ? agentBinding(conv, agent).sessionId : undefined;
|
|
581
|
+
if (!conv?.titleAuto || !sessionId) return this.titleSyncing.delete(key);
|
|
582
|
+
const title = await driver.sessionTitle!(sessionId, conv.cwd).catch((err) => {
|
|
583
|
+
console.error(`[${agent}] 读取会话标题失败:`, err instanceof Error ? err.message : err);
|
|
584
|
+
return undefined;
|
|
585
|
+
});
|
|
586
|
+
// 等待期间用户可能改了名、切了会话
|
|
587
|
+
const now = getConversation(key);
|
|
588
|
+
if (!now?.titleAuto || now.agent !== agent || agentBinding(now, agent).sessionId !== sessionId) {
|
|
589
|
+
return this.titleSyncing.delete(key);
|
|
590
|
+
}
|
|
591
|
+
if (!title) return this.syncTitle(key, agent, attempt + 1);
|
|
592
|
+
this.titleSyncing.delete(key);
|
|
593
|
+
const clipped = title.slice(0, 80);
|
|
594
|
+
if (clipped !== now.title) {
|
|
595
|
+
updateConversation(key, { title: clipped });
|
|
596
|
+
this.emitEvent(key, { type: "meta" });
|
|
597
|
+
}
|
|
598
|
+
})();
|
|
599
|
+
}, delays[attempt]);
|
|
600
|
+
}
|
|
601
|
+
|
|
558
602
|
/** 丢掉活着的会话(不动绑定)。挂着的审批一并取消,否则阻塞的那一轮永远走不完。 */
|
|
559
603
|
async drop(key: string, note: string): Promise<void> {
|
|
560
604
|
interactions.cancelAll(key, note);
|
|
@@ -713,7 +757,8 @@ class Hub extends EventEmitter<{ event: [EventEnvelope] }> {
|
|
|
713
757
|
const lastActive = conv.activeAt;
|
|
714
758
|
touchConversation(key);
|
|
715
759
|
if (!conv.title && conv.channel === "web" && input.text.trim()) {
|
|
716
|
-
|
|
760
|
+
// 先用首条消息占位,第一轮结束后换成 agent 自己生成的短标题(syncTitle)
|
|
761
|
+
updateConversation(key, { title: input.text.trim().replace(/\s+/g, " ").slice(0, 40), titleAuto: true });
|
|
717
762
|
this.emitEvent(key, { type: "meta" });
|
|
718
763
|
}
|
|
719
764
|
this.emitEvent(key, shown);
|
package/src/core/store.ts
CHANGED
|
@@ -28,6 +28,11 @@ export type Conversation = {
|
|
|
28
28
|
/** 渠道实例:飞书是机器人 id,Web 固定 web */
|
|
29
29
|
channelId: string;
|
|
30
30
|
title?: string;
|
|
31
|
+
/**
|
|
32
|
+
* 标题是自动起的(先用首条消息占位,之后换成 agent 生成的标题),可以继续被 agent 的标题覆盖。
|
|
33
|
+
* 用户手动改过名就清掉,之后不再动它。
|
|
34
|
+
*/
|
|
35
|
+
titleAuto?: boolean;
|
|
31
36
|
agent: string;
|
|
32
37
|
cwd: string;
|
|
33
38
|
/** cwd 是否用户显式指定。没指定过的跟随 workspaceRoot 变化 */
|
package/src/core/types.ts
CHANGED
|
@@ -201,6 +201,8 @@ export interface AgentDriver {
|
|
|
201
201
|
/** cwd 为 undefined 时列全部目录 */
|
|
202
202
|
listSessions(cwd: string | undefined, limit?: number): Promise<SessionSummary[]>;
|
|
203
203
|
sessionExists(id: string, cwd: string): Promise<boolean>;
|
|
204
|
+
/** agent 自己给会话起的标题(模型总结的或在别处改过的);还没生成就返回 undefined,不拿首条消息凑数 */
|
|
205
|
+
sessionTitle?(id: string, cwd: string): Promise<string | undefined>;
|
|
204
206
|
readHistory(id: string, cwd: string): Promise<HistoryMessage[]>;
|
|
205
207
|
/**
|
|
206
208
|
* 把会话复制出一条新的、返回新会话 id。anchor(HistoryMessage / TurnResult 里的)给了就只保留到
|
package/web/app.js
CHANGED
|
@@ -38,7 +38,6 @@ const ICON = {
|
|
|
38
38
|
stop: '<svg viewBox="0 0 24 24" fill="currentColor"><rect x="7" y="7" width="10" height="10" rx="1.5"/></svg>',
|
|
39
39
|
clip: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.4 11.6-9.2 9.2a6 6 0 0 1-8.5-8.5l9.2-9.2a4 4 0 0 1 5.7 5.7l-9.2 9.2a2 2 0 0 1-2.8-2.8l8.5-8.5"/></svg>',
|
|
40
40
|
mic: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 10a7 7 0 0 0 14 0M12 17v5"/></svg>',
|
|
41
|
-
more: '<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg>',
|
|
42
41
|
down: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><path d="m6 9 6 6 6-6"/></svg>',
|
|
43
42
|
gear: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z"/></svg>',
|
|
44
43
|
fork: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="5" r="2"/><circle cx="6" cy="19" r="2"/><circle cx="18" cy="7" r="2"/><path d="M6 7v10"/><path d="M18 9c0 5-6 4-11.2 8.6"/></svg>',
|
|
@@ -269,6 +268,12 @@ const desktop = window.agentLoftDesktop ?? null;
|
|
|
269
268
|
const desktopQuota = typeof desktop?.onQuota === "function";
|
|
270
269
|
// 桌面端里:顶栏、侧栏顶部可以按住拖动窗口(样式见 style.css 的 .in-desktop)
|
|
271
270
|
if (desktop) document.documentElement.classList.add("in-desktop");
|
|
271
|
+
/**
|
|
272
|
+
* 新版桌面端顶栏的「更多」里能打开每台服务器的设置,额度也在它的菜单栏里:侧栏底部的额度和设置就不显示了。
|
|
273
|
+
* 老版本桌面端没有这个入口,照旧显示,免得设置打不开。
|
|
274
|
+
*/
|
|
275
|
+
const desktopSettings = typeof desktop?.onOpenSettings === "function";
|
|
276
|
+
if (desktopSettings) document.documentElement.classList.add("desktop-settings");
|
|
272
277
|
|
|
273
278
|
// ---------- WebSocket ----------
|
|
274
279
|
|
|
@@ -402,6 +407,7 @@ function markGone(ix) {
|
|
|
402
407
|
function onWsMessage(msg) {
|
|
403
408
|
switch (msg.t) {
|
|
404
409
|
case "hello": {
|
|
410
|
+
if (checkServerVersion(msg.version)) return;
|
|
405
411
|
const restarted = WS.epoch && WS.epoch !== msg.epoch;
|
|
406
412
|
WS.epoch = msg.epoch;
|
|
407
413
|
WS.hellos += 1;
|
|
@@ -464,14 +470,31 @@ function onWsMessage(msg) {
|
|
|
464
470
|
}
|
|
465
471
|
}
|
|
466
472
|
|
|
467
|
-
function setConnBanner(text) {
|
|
473
|
+
function setConnBanner(text, onclick) {
|
|
468
474
|
ui.banner?.remove();
|
|
469
475
|
ui.banner = null;
|
|
470
476
|
if (!text || !ui.main) return;
|
|
471
|
-
ui.banner = h("div", { class: "conn-banner" }, text);
|
|
477
|
+
ui.banner = onclick ? h("button", { class: "conn-banner clickable", onclick }, text) : h("div", { class: "conn-banner" }, text);
|
|
472
478
|
ui.main.append(ui.banner);
|
|
473
479
|
}
|
|
474
480
|
|
|
481
|
+
/** 页面加载时连上的服务端版本。服务端升级重启后版本变了,页面上跑的还是旧前端,得刷新 */
|
|
482
|
+
let loadedServerVersion = null;
|
|
483
|
+
|
|
484
|
+
/** 服务端换了版本:刷新页面换上新前端。返回 true 表示已经在刷新了,后面不用再处理 */
|
|
485
|
+
function checkServerVersion(version) {
|
|
486
|
+
if (!version) return false;
|
|
487
|
+
if (!loadedServerVersion) loadedServerVersion = version;
|
|
488
|
+
if (version === loadedServerVersion) return false;
|
|
489
|
+
// 输入框里有没发出去的字:别直接刷掉,提示一下让用户自己点
|
|
490
|
+
if (ui.input?.value.trim()) {
|
|
491
|
+
setConnBanner(`服务端已升级到 ${version},点这里刷新页面`, () => location.reload());
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
location.reload();
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
|
|
475
498
|
// ---------- 登录 ----------
|
|
476
499
|
|
|
477
500
|
async function showAuth() {
|
|
@@ -549,6 +572,12 @@ async function boot() {
|
|
|
549
572
|
desktop?.onOpen((key) => {
|
|
550
573
|
if (key && S.convs.has(key) && key !== S.current) void openConversation(key);
|
|
551
574
|
});
|
|
575
|
+
// 桌面端「更多」里点了这台服务器的设置;已经开着就不再叠一层
|
|
576
|
+
if (desktopSettings) {
|
|
577
|
+
desktop.onOpenSettings(() => {
|
|
578
|
+
if (!document.querySelector(".overlay .settings")) void openSettings();
|
|
579
|
+
});
|
|
580
|
+
}
|
|
552
581
|
if ("serviceWorker" in navigator) {
|
|
553
582
|
navigator.serviceWorker.register("/sw.js").catch(() => {});
|
|
554
583
|
clearAppBadge();
|
|
@@ -579,6 +608,13 @@ function renderLayout() {
|
|
|
579
608
|
lastDown = { key, at: now };
|
|
580
609
|
});
|
|
581
610
|
ui.convList.addEventListener("contextmenu", (e) => {
|
|
611
|
+
// 飞书机器人那一行:右键看这个机器人的所有聊天
|
|
612
|
+
const bot = e.target.closest?.("[data-bot]")?.dataset.bot;
|
|
613
|
+
if (bot) {
|
|
614
|
+
e.preventDefault();
|
|
615
|
+
showMenu({ x: e.clientX, y: e.clientY }, [{ label: "历史会话…", onclick: () => openBotConversations(bot) }]);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
582
618
|
const key = e.target.closest?.("[data-key]")?.dataset.key;
|
|
583
619
|
if (!key || !S.convs.has(key)) return;
|
|
584
620
|
e.preventDefault();
|
|
@@ -1025,6 +1061,7 @@ function renderSidebar() {
|
|
|
1025
1061
|
"button",
|
|
1026
1062
|
{
|
|
1027
1063
|
class: `conv-item${cur?.channel === "feishu" && cur.channelId === bot.id ? " active" : ""}`,
|
|
1064
|
+
"data-bot": bot.id,
|
|
1028
1065
|
title: latest ? `飞书 · ${bot.name}\n${agentLabel(latest.agent)} · ${latest.cwd}` : `飞书 · ${bot.name}\n还没有会话`,
|
|
1029
1066
|
onclick: () => (latest ? open(latest.key) : toast("这个机器人还没有会话,先在飞书里给它发条消息")),
|
|
1030
1067
|
},
|
|
@@ -1117,13 +1154,9 @@ function renderSidebar() {
|
|
|
1117
1154
|
|
|
1118
1155
|
function renderTopbar() {
|
|
1119
1156
|
const c = S.current ? S.convs.get(S.current) : null;
|
|
1120
|
-
const more = h("button", { class: "icon-btn", title: "更多", onclick: (e) => openMoreMenu(e.currentTarget) }, icon("more"));
|
|
1121
1157
|
fill(ui.topbar,
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
h("div", { class: "top-title" }, c ? h("span", { class: "conv-name", title: convTitle(c) }, convTitle(c)) : null),
|
|
1125
|
-
c ? more : null,
|
|
1126
|
-
].filter(Boolean),
|
|
1158
|
+
h("button", { class: "icon-btn menu-btn", onclick: openSide, title: "会话列表" }, icon("menu")),
|
|
1159
|
+
h("div", { class: "top-title" }, c ? h("span", { class: "conv-name", title: convTitle(c) }, convTitle(c)) : null),
|
|
1127
1160
|
);
|
|
1128
1161
|
renderContext();
|
|
1129
1162
|
}
|
|
@@ -1380,24 +1413,6 @@ function closeMenu() {
|
|
|
1380
1413
|
removeEventListener("blur", closeMenu);
|
|
1381
1414
|
}
|
|
1382
1415
|
|
|
1383
|
-
function openMoreMenu(anchor) {
|
|
1384
|
-
const c = S.convs.get(S.current);
|
|
1385
|
-
if (!c) return;
|
|
1386
|
-
showMenu(anchor, [
|
|
1387
|
-
// 飞书机器人在侧栏只占一行(点开最近活跃的那个聊天),它的其他聊天从这里找
|
|
1388
|
-
...(c.channel === "feishu" ? [{ label: "历史会话…", onclick: () => openBotConversations(c.channelId) }] : []),
|
|
1389
|
-
{ label: "开新会话(清空上下文)", onclick: () => doAction("new") },
|
|
1390
|
-
c.canFork
|
|
1391
|
-
? { label: "分叉成新对话", onclick: () => forkConversation() }
|
|
1392
|
-
: { label: "分叉当前会话", onclick: () => doAction("fork") },
|
|
1393
|
-
{ label: "切换工作目录…", onclick: () => changeCwd() },
|
|
1394
|
-
{ label: "查看状态", onclick: () => showStatus() },
|
|
1395
|
-
"sep",
|
|
1396
|
-
{ label: "重命名", onclick: () => renameConv(S.current) },
|
|
1397
|
-
...(c.channel === "web" ? [{ label: "从列表删除", danger: true, onclick: () => deleteConv() }] : []),
|
|
1398
|
-
]);
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
1416
|
/** 侧栏会话的右键菜单:分叉 / 重命名 / 删除 */
|
|
1402
1417
|
function openConvContextMenu(key, at) {
|
|
1403
1418
|
const c = S.convs.get(key);
|
|
@@ -1948,12 +1963,12 @@ function renderInteraction(item) {
|
|
|
1948
1963
|
"div",
|
|
1949
1964
|
{ class: "ix-buttons" },
|
|
1950
1965
|
h("button", { class: "btn accent", onclick: () => ixOp(ix.id, "decide", { decision: "allow-once" }) }, "允许一次"),
|
|
1951
|
-
spec.alwaysLabel
|
|
1952
|
-
? h("button", { class: "btn", onclick: () => ixOp(ix.id, "decide", { decision: "allow-always" }) }, spec.alwaysLabel)
|
|
1953
|
-
: null,
|
|
1954
1966
|
spec.allowSession
|
|
1955
1967
|
? h("button", { class: "btn", onclick: () => ixOp(ix.id, "decide", { decision: "allow-session" }) }, "本会话全部允许")
|
|
1956
1968
|
: null,
|
|
1969
|
+
spec.alwaysLabel
|
|
1970
|
+
? h("button", { class: "btn", onclick: () => ixOp(ix.id, "decide", { decision: "allow-always" }) }, spec.alwaysLabel)
|
|
1971
|
+
: null,
|
|
1957
1972
|
h("button", { class: "btn danger", onclick: () => ixOp(ix.id, "decide", { decision: "deny" }) }, "拒绝"),
|
|
1958
1973
|
),
|
|
1959
1974
|
);
|
|
@@ -2041,7 +2056,8 @@ function renderComposer() {
|
|
|
2041
2056
|
},
|
|
2042
2057
|
});
|
|
2043
2058
|
ui.sendBtn = h("button", { class: "send-btn", title: "发送", onclick: () => (S.busy && !hasDraft() ? stopTurn() : sendMessage()) });
|
|
2044
|
-
|
|
2059
|
+
// 桌面端(Electron)里不放语音输入
|
|
2060
|
+
ui.micBtn = SpeechRec && !desktop
|
|
2045
2061
|
? h("button", { class: "icon-btn mic-btn", title: "语音输入", onclick: toggleVoice }, icon("mic"))
|
|
2046
2062
|
: null;
|
|
2047
2063
|
ui.ctxBar = h("div", { class: "ctx-bar" });
|
|
@@ -2945,15 +2961,6 @@ async function changeCwd() {
|
|
|
2945
2961
|
await load(startRel);
|
|
2946
2962
|
}
|
|
2947
2963
|
|
|
2948
|
-
async function showStatus() {
|
|
2949
|
-
try {
|
|
2950
|
-
const data = await api(`/api/conversations/${encodeURIComponent(S.current)}/status`);
|
|
2951
|
-
modal({ title: "状态", body: h("div", { class: "modal-body" }, renderMarkdown("```\n" + data.text + "\n```")) });
|
|
2952
|
-
} catch (err) {
|
|
2953
|
-
toast(err.message);
|
|
2954
|
-
}
|
|
2955
|
-
}
|
|
2956
|
-
|
|
2957
2964
|
/** 重命名:小弹窗,回车保存、Esc 取消,打开时全选原名 */
|
|
2958
2965
|
function renameConv(key = S.current) {
|
|
2959
2966
|
const c = S.convs.get(key);
|
package/web/style.css
CHANGED
|
@@ -550,6 +550,11 @@ button.conv-group:hover {
|
|
|
550
550
|
margin-left: auto;
|
|
551
551
|
}
|
|
552
552
|
|
|
553
|
+
/* 桌面端里:额度在菜单栏、设置在顶栏「更多」里,侧栏底部整条不要 */
|
|
554
|
+
.desktop-settings .side-bottom {
|
|
555
|
+
display: none;
|
|
556
|
+
}
|
|
557
|
+
|
|
553
558
|
/* ---- 侧栏底部的额度 ---- */
|
|
554
559
|
.quota {
|
|
555
560
|
position: relative;
|
|
@@ -2139,6 +2144,13 @@ button.ctx-item:hover {
|
|
|
2139
2144
|
z-index: 5;
|
|
2140
2145
|
}
|
|
2141
2146
|
|
|
2147
|
+
.conn-banner.clickable {
|
|
2148
|
+
border: 0;
|
|
2149
|
+
font: inherit;
|
|
2150
|
+
font-size: 12px;
|
|
2151
|
+
cursor: pointer;
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2142
2154
|
/* ---------- 弹层 ---------- */
|
|
2143
2155
|
|
|
2144
2156
|
.overlay {
|