agent-loft 0.2.21 → 0.2.23
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/README.md +24 -13
- package/bin/cli.js +4 -2
- package/package.json +1 -1
- package/src/agents/claude/session.ts +1 -1
- package/src/agents/codex/session.ts +1 -0
- package/src/channels/feishu/bot.ts +22 -8
- package/src/channels/web/access.ts +26 -0
- package/src/channels/web/api.ts +176 -42
- package/src/channels/web/auth.ts +92 -38
- package/src/channels/web/push.ts +30 -8
- package/src/channels/web/server.ts +50 -17
- package/src/cli/doctor.ts +2 -1
- package/src/cli/passwd.ts +24 -7
- package/src/cli/user.ts +99 -0
- package/src/core/command-catalog.ts +11 -7
- package/src/core/commands.ts +1 -1
- package/src/core/hub.ts +49 -8
- package/src/core/store.ts +33 -0
- package/src/core/types.ts +5 -0
- package/src/core/uploads.ts +12 -0
- package/src/core/users.ts +261 -0
- package/web/app.js +238 -43
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# agent-loft
|
|
4
4
|
|
|
5
|
-
**多个 agent 的工作台。** 把本机的 **Claude Code**、**Codex
|
|
5
|
+
**多个 agent 的工作台。** 把本机的 **Claude Code**、**Codex**(以后还有更多)放到同一张台面上统一管理,在网页、飞书和桌面端(macOS / Windows / Linux)随时驱动它们,进展推送到飞书、iPhone 和 Mac。
|
|
6
6
|
|
|
7
7
|

|
|
8
8
|
|
|
@@ -30,7 +30,7 @@ Logo 就是这个意思:一大两小几颗星(多个 agent)站在同一张
|
|
|
30
30
|
| 🔗 | **飞书续接 Web 任务** | 飞书里发 `/web` 接上一条 Web 会话,或者在网页侧栏右键会话选「在飞书继续…」。之后回复、授权、提问都发到飞书,发 `/detach` 回来 |
|
|
31
31
|
| 🖥️ | **完整的 Web 工作台** | 流式回复、附件、语音输入、模型和思考程度切换、上下文占用、从任意回复分叉、按项目分组的侧栏、全部设置,电脑和手机(PWA)都能用 |
|
|
32
32
|
| 🔔 | **飞书 + 苹果系统推送** | 除了飞书消息,Web 会话还能推送到 iPhone、iPad 锁屏和 Mac 通知中心(标准 Web Push,不需要开发者账号),图标上显示未读数 |
|
|
33
|
-
| 🗂️ | **桌面端同时接入多台设备** | macOS
|
|
33
|
+
| 🗂️ | **桌面端同时接入多台设备** | 桌面端(macOS / Windows / Linux)同时连多台服务器,菜单栏 / 托盘一眼看完各台的额度、待处理和运行中的会话 |
|
|
34
34
|
|
|
35
35
|
## 能做什么
|
|
36
36
|
|
|
@@ -52,7 +52,8 @@ Logo 就是这个意思:一大两小几颗星(多个 agent)站在同一张
|
|
|
52
52
|
- **系统通知**:Web 会话回复完成、出错、需要授权或提问时推送到手机 / 电脑(设置 → 通知 → 「在此设备开启通知」,可以发测试通知)。飞书会话由飞书提醒,正在前台看着的会话不推。需要 HTTPS 访问,iPhone 需 iOS 16.4+ 且从主屏幕图标打开。
|
|
53
53
|
- **跨渠道同步**:飞书里的会话在 Web 上也能打开接着聊。同一个授权请求同时出现在飞书卡片和网页上,哪边先点算哪边,另一边自动变成「已处理」。
|
|
54
54
|
- **授权**:允许一次 / 本会话全部允许 / 总是允许(按钮上写明放行范围)/ 拒绝。`/autoedit on` 让 Claude Code 在这条会话里改工作目录内的文件不再询问(Bash 仍询问)。超时一律按拒绝处理。
|
|
55
|
-
- **设置都在网页上**:工作目录、会话闲置过期时间、局域网访问和端口、各 agent 的默认模型和思考程度、可选模型、Codex 沙箱、飞书机器人、配对批准、Claude
|
|
55
|
+
- **设置都在网页上**:工作目录、会话闲置过期时间、局域网访问和端口、各 agent 的默认模型和思考程度、可选模型、Codex 沙箱、飞书机器人、配对批准、Claude 的「总是允许」名单、通知设备、登录设备管理、用户管理、服务端升级。
|
|
56
|
+
- **多用户**(信任型):管理员在 设置 → 用户 里给别人建账号。每个人只看得到自己的对话、授权和通知,设置只有管理员能改,见下面「安全」。
|
|
56
57
|
|
|
57
58
|
## 安装
|
|
58
59
|
|
|
@@ -64,20 +65,26 @@ agent-loft start # 前台运行,日志里会打印首次登
|
|
|
64
65
|
agent-loft service install # 注册为开机自启服务(systemd / launchd),日志在 ~/.agent-loft/logs/
|
|
65
66
|
```
|
|
66
67
|
|
|
67
|
-
打开 `http://<机器 IP>:8800`(本机用 `http://localhost:8800
|
|
68
|
+
打开 `http://<机器 IP>:8800`(本机用 `http://localhost:8800`):第一次需要输入日志里的设置码,然后建一个管理员账号(用户名默认 `admin`)。设置码找不到的话,直接运行 `agent-loft passwd` 设密码。
|
|
68
69
|
|
|
69
|
-
到 设置 → Agent 检查 Claude Code、Codex 装好、登录了没有:Claude Code 随 agent-loft 一起安装,只需在服务端终端里登录;Codex 可以点「直接在服务器上安装」。设置 →
|
|
70
|
+
到 设置 → Agent 检查 Claude Code、Codex 装好、登录了没有:Claude Code 随 agent-loft 一起安装,只需在服务端终端里登录;Codex 可以点「直接在服务器上安装」。设置 → 系统 里能看到服务端版本,有新版本时点「升级」即可(`npm install -g agent-loft@latest`,以系统服务运行时会自动重启,打开的页面会自动刷新)。
|
|
70
71
|
|
|
71
72
|
在外面只用飞书的话不需要任何网络配置(机器人由服务端主动连向飞书)。在外面用网页、系统推送和语音输入需要 HTTPS,见下面的「安全」和[产品介绍里的「从外网访问」](docs/产品介绍.md#从外网访问)。
|
|
72
73
|
|
|
73
|
-
### macOS
|
|
74
|
+
### 桌面端(macOS / Windows / Linux)
|
|
74
75
|
|
|
75
|
-
到 [Releases](https://github.com/qijieinnet/agent-loft/releases) 下载 `desktop-v*`
|
|
76
|
+
到 [Releases](https://github.com/qijieinnet/agent-loft/releases) 下载 `desktop-v*` 里对应的安装包:
|
|
77
|
+
|
|
78
|
+
- **macOS**:对应芯片的 DMG(Apple 芯片选 arm64,Intel 选 x64),拖进「应用程序」。还没有 Apple 开发者签名,第一次打开如果提示无法验证,到 系统设置 → 隐私与安全性 里点「仍要打开」。
|
|
79
|
+
- **Windows**:`Agent-Loft-Setup-*-x64.exe`,装在用户目录里,不用管理员权限。没有代码签名,SmartScreen 拦的话点「更多信息 → 仍要运行」。
|
|
80
|
+
- **Linux**:Debian / Ubuntu 用 deb(`sudo apt install ./agent-loft-desktop_*_amd64.deb`),其他发行版用 AppImage(需要 libfuse2);ARM 选 arm64。App 内一键更新只支持 AppImage。GNOME 要装 AppIndicator 扩展才看得到托盘图标。
|
|
81
|
+
|
|
82
|
+
下面以 macOS 为例,Windows / Linux 上「菜单栏」是托盘、`⌘` 是 `Ctrl`:
|
|
76
83
|
|
|
77
84
|
- 同时连多台服务器,顶部标签栏切换、拖动排序;每台各自登录,切走了也在后台连着,回复完成、需要授权时弹系统通知,程序坞图标显示未读数。
|
|
78
|
-
- 菜单栏图标:悬停看各服务器的 Claude / Codex
|
|
85
|
+
- 菜单栏图标:悬停看各服务器的 Claude / Codex 额度、待处理和运行中的会话;双击打开主窗口。同一个账号的额度只查一次。(Windows 上点托盘图标弹出;Linux 上在托盘菜单里点「额度和待处理」。)
|
|
79
86
|
- 支持前面挡着 Cloudflare Access 的服务器:验证页直接在窗口里完成,过期会提醒;也可以配 Service Token 免验证。
|
|
80
|
-
- 首次打开可以选「这台 Mac 也作为服务端」:一键在终端里安装 agent-loft(没有 Node.js 会先用 Homebrew
|
|
87
|
+
- 首次打开可以选「这台 Mac 也作为服务端」:一键在终端里安装 agent-loft(没有 Node.js 会先用 Homebrew 装)并开机自启,装好自动连上。Linux 上一样(需要先装好 Node.js 22+,开机自启用 systemd 用户服务);Windows 上服务端还没有开机自启,可以在 WSL 里装,桌面端会自动检测到 `127.0.0.1` 上的它。可选开启「本机有会话在跑时不休眠」。
|
|
81
88
|
- 设置(`⌘,`)的服务器列表里展开某台服务器,能看它的 Agent 环境和版本,有新版本时点「升级服务端」;桌面端自己的更新在同一页的「更新」里点「更新并重启」。
|
|
82
89
|
|
|
83
90
|
桌面端只是个壳(代码在 `desktop/`),服务端照常用 npm 安装和升级。
|
|
@@ -100,7 +107,8 @@ agent-loft service install
|
|
|
100
107
|
| `agent-loft start` | 启动 |
|
|
101
108
|
| `agent-loft import [--only claude\|codex] [--disabled] [--dry-run]` | 从旧桥接器导入 |
|
|
102
109
|
| `agent-loft pair list \| approve <码> \| revoke <机器人id> <open_id>` | 配对管理(Web 设置页里也能做) |
|
|
103
|
-
| `agent-loft passwd [新密码]` | 重置 Web
|
|
110
|
+
| `agent-loft passwd [新密码] [--user 用户名]` | 重置 Web 登录密码(默认是最早的管理员),他的设备全部下线 |
|
|
111
|
+
| `agent-loft user list \| add <用户名> [--admin] \| passwd <用户名> \| role <用户名> admin\|member \| disable \| enable \| remove` | 管理 Web 登录用户(设置 → 用户 里也能做) |
|
|
104
112
|
| `agent-loft service install \| uninstall \| restart \| status` | 开机自启 |
|
|
105
113
|
| `agent-loft doctor` | 体检 |
|
|
106
114
|
|
|
@@ -114,9 +122,11 @@ agent-loft service install
|
|
|
114
122
|
|
|
115
123
|
这个服务能让 agent 在你的机器上执行命令,等于一个远程 shell。
|
|
116
124
|
|
|
117
|
-
- Web 登录密码用 scrypt
|
|
125
|
+
- Web 登录密码用 scrypt 哈希保存。首次建管理员必须用启动日志里的一次性设置码,防止「谁先打开页面谁就成了主人」。
|
|
126
|
+
- 多用户只隔开**界面上**的东西:会话列表、授权 / 提问卡、通知、`/sessions` 能列出和恢复的 agent 会话;设置、飞书机器人和飞书会话只给管理员;会落盘、对所有人生效的「总是允许」(Claude 的总是允许名单、Codex 的 execpolicy)只有管理员能点。**它不是安全边界**:所有 agent 都以同一个系统用户运行,任何用户都能让 agent 读到这台机器上的文件(别人的对话记录、配置里的密钥),用的也是同一个 Claude / Codex 订阅。只给信任的人开账号;要真正隔离,就每人用自己的系统用户各跑一份 agent-loft。
|
|
127
|
+
- 从老版本升级:原来的登录密码自动变成管理员 `admin`,已登录的设备不用重新登录。
|
|
118
128
|
- 登录态通过 HttpOnly cookie 保存,服务端只存它的哈希。同一 IP 连续输错 5 次锁 15 分钟。所有写请求和 WebSocket 都校验 Origin。页面开启了严格的 CSP。
|
|
119
|
-
- 暴露到公网时,只通过 HTTPS 反向代理(比如 Lucky)或 Cloudflare Tunnel 访问,并把不在本机的反代 IP 加进 `web.trustedProxies`(只有从这些地址来的连接才采信 `X-Forwarded-*`)。不要直接把 8800 端口映射出去。局域网访问可以在 设置 →
|
|
129
|
+
- 暴露到公网时,只通过 HTTPS 反向代理(比如 Lucky)或 Cloudflare Tunnel 访问,并把不在本机的反代 IP 加进 `web.trustedProxies`(只有从这些地址来的连接才采信 `X-Forwarded-*`)。不要直接把 8800 端口映射出去。局域网访问可以在 设置 → 系统 → 网络访问 里关掉。
|
|
120
130
|
- 审批超时一律按拒绝处理。Codex 默认沙箱为 `workspace-write`,不会因为 `~/.codex/config.toml` 里配了 `never` 或 `danger-full-access` 就绕过审批。
|
|
121
131
|
- agent 子进程拿不到本服务自己的凭据(`FEISHU_*`、`AGENT_LOFT_*` 等环境变量会被剔除)。
|
|
122
132
|
|
|
@@ -129,6 +139,7 @@ src/
|
|
|
129
139
|
interactions.ts 审批与提问:一个请求,多个渠道都能处理
|
|
130
140
|
store.ts 会话持久化(每个 agent 的会话 id 各自保存)
|
|
131
141
|
config.ts 配置(Web 可改,改完热加载)
|
|
142
|
+
users.ts Web 登录用户和归属规则(谁能看到哪条会话)
|
|
132
143
|
pairing.ts uploads.ts commands.ts …
|
|
133
144
|
agents/
|
|
134
145
|
claude/ Claude Agent SDK 驱动
|
|
@@ -137,7 +148,7 @@ src/
|
|
|
137
148
|
feishu/ 飞书:一个 FeishuBot 对应一个机器人,支持多个
|
|
138
149
|
web/ HTTP + WebSocket + 鉴权
|
|
139
150
|
web/ 前端(原生 JS,不需要构建)
|
|
140
|
-
desktop/
|
|
151
|
+
desktop/ 桌面端(Electron 外壳,macOS / Windows / Linux)
|
|
141
152
|
docs/ 产品介绍和截图
|
|
142
153
|
```
|
|
143
154
|
|
package/bin/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// 统一入口:agent-loft <start|import|pair|passwd|service|doctor>
|
|
2
|
+
// 统一入口:agent-loft <start|import|pair|passwd|user|service|doctor>
|
|
3
3
|
//
|
|
4
4
|
// 源码是 TypeScript,用 tsx 的 ESM loader 直接加载,装完即可运行,不需要先 build。
|
|
5
5
|
import { register } from "tsx/esm/api";
|
|
@@ -9,6 +9,7 @@ const COMMANDS = {
|
|
|
9
9
|
import: "../src/cli/import.ts",
|
|
10
10
|
pair: "../src/cli/pair.ts",
|
|
11
11
|
passwd: "../src/cli/passwd.ts",
|
|
12
|
+
user: "../src/cli/user.ts",
|
|
12
13
|
service: "../src/cli/service.ts",
|
|
13
14
|
doctor: "../src/cli/doctor.ts",
|
|
14
15
|
};
|
|
@@ -19,7 +20,8 @@ const USAGE = `agent-loft —— 用飞书 / Web 驱动本机的 Claude Code 和
|
|
|
19
20
|
agent-loft start 启动(默认命令)
|
|
20
21
|
agent-loft import 从旧的 feishu-claude-bridge / feishu-codex-bridge 导入配置和会话
|
|
21
22
|
agent-loft pair ... 配对管理:list | approve <配对码> | revoke <机器人id> <open_id>
|
|
22
|
-
agent-loft passwd 重置 Web
|
|
23
|
+
agent-loft passwd 重置 Web 登录密码(--user 名字 指定用户,默认是管理员)
|
|
24
|
+
agent-loft user ... Web 登录用户:list | add <名字> [--admin] | passwd | role | disable | enable | remove
|
|
23
25
|
agent-loft service ... 开机自启:install | uninstall | restart | status
|
|
24
26
|
agent-loft doctor 体检:配置 / agent / 飞书连通性
|
|
25
27
|
|
package/package.json
CHANGED
|
@@ -309,7 +309,7 @@ export class ClaudeSession implements AgentSession {
|
|
|
309
309
|
kind: "approval",
|
|
310
310
|
title: `需要授权:${toolName}`,
|
|
311
311
|
detail: describeToolCall(toolName, input),
|
|
312
|
-
...(isRememberableSignature(toolName, signature) ? { alwaysLabel: alwaysLabelOf(toolName, signature) } : {}),
|
|
312
|
+
...(isRememberableSignature(toolName, signature) ? { alwaysLabel: alwaysLabelOf(toolName, signature), alwaysGlobal: true } : {}),
|
|
313
313
|
...(sessionable ? { allowSession: true } : {}),
|
|
314
314
|
});
|
|
315
315
|
if (outcome.settlement === "allow-always" && isRememberableSignature(toolName, signature)) rememberAlways(signature);
|
|
@@ -508,6 +508,7 @@ export class CodexSession implements AgentSession {
|
|
|
508
508
|
detail: describeCommand(req),
|
|
509
509
|
// 「总是允许」到底记多宽取决于 Codex 有没有给 execpolicy 补丁,差别很大,得写在按钮上
|
|
510
510
|
alwaysLabel: amendment ? "以后同类不再问" : "本会话内不再问",
|
|
511
|
+
...(amendment ? { alwaysGlobal: true } : {}),
|
|
511
512
|
});
|
|
512
513
|
const s = outcome.settlement;
|
|
513
514
|
if (s === "allow-once") return { decision: "accept" };
|
|
@@ -13,7 +13,8 @@ import { interactions, type Interaction } from "../../core/interactions.js";
|
|
|
13
13
|
import { parseCommand } from "../../core/commands.js";
|
|
14
14
|
import { Dedup } from "../../core/dedup.js";
|
|
15
15
|
import { isPaired, requestPairing } from "../../core/pairing.js";
|
|
16
|
-
import { getConversation, listConversations, updateConversation } from "../../core/store.js";
|
|
16
|
+
import { getConversation, listConversations, sessionOwner, updateConversation } from "../../core/store.js";
|
|
17
|
+
import { adminVisible, hasMembers } from "../../core/users.js";
|
|
17
18
|
import { adoptFile, KIND_LABEL } from "../../core/uploads.js";
|
|
18
19
|
import type { SavedAttachment } from "../../core/types.js";
|
|
19
20
|
import { FeishuClient } from "./client.js";
|
|
@@ -140,14 +141,20 @@ export class FeishuBot {
|
|
|
140
141
|
const target = attachedTarget(feishuKey);
|
|
141
142
|
if (!target) return undefined;
|
|
142
143
|
const conv = getConversation(target);
|
|
143
|
-
|
|
144
|
+
// 主人后来被降成了普通成员:这条会话不再是飞书能碰的,当它没了(下条消息时自动退回来)
|
|
145
|
+
return conv && !conv.hidden && adminVisible(conv.owner) ? target : undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 挂在 Web 会话 key 上的本机器人聊天。会话已经不归管理员了就一个都不算 */
|
|
149
|
+
private watchers(key: string): string[] {
|
|
150
|
+
return adminVisible(getConversation(key)?.owner) ? watchersOf(key, this.prefix) : [];
|
|
144
151
|
}
|
|
145
152
|
|
|
146
153
|
/** 这条会话的事件该画到哪些飞书聊天里:自己的那个聊天,加上挂着它的聊天 */
|
|
147
154
|
chatsOf(key: string): string[] {
|
|
148
155
|
const out = new Set<string>();
|
|
149
156
|
if (this.mine(key)) out.add(this.chatIdOf(key));
|
|
150
|
-
for (const k of
|
|
157
|
+
for (const k of this.watchers(key)) out.add(this.chatIdOf(k));
|
|
151
158
|
return [...out];
|
|
152
159
|
}
|
|
153
160
|
|
|
@@ -155,7 +162,7 @@ export class FeishuBot {
|
|
|
155
162
|
private controls(key: string, chatId: string | undefined): boolean {
|
|
156
163
|
if (!chatId) return false;
|
|
157
164
|
if (this.mine(key)) return this.chatIdOf(key) === chatId;
|
|
158
|
-
return
|
|
165
|
+
return this.watchers(key).some((k) => this.chatIdOf(k) === chatId);
|
|
159
166
|
}
|
|
160
167
|
|
|
161
168
|
private webEntry(key: string): WebConvEntry | undefined {
|
|
@@ -206,7 +213,8 @@ export class FeishuBot {
|
|
|
206
213
|
*/
|
|
207
214
|
async attachTo(feishuKey: string, webKey: string, operatorOpenId?: string): Promise<string> {
|
|
208
215
|
const conv = getConversation(webKey);
|
|
209
|
-
|
|
216
|
+
// 飞书是管理员的地盘:普通成员的 Web 会话不能被接到飞书上
|
|
217
|
+
if (!conv || conv.channel !== "web" || conv.hidden || !adminVisible(conv.owner)) throw new Error("这条 Web 会话已不存在");
|
|
210
218
|
const chatId = this.chatIdOf(feishuKey);
|
|
211
219
|
// 本聊天自己的会话也得有:之后 /detach 回来、侧栏里显示机器人都要它
|
|
212
220
|
const allowed = this.allowedAgents();
|
|
@@ -253,8 +261,7 @@ export class FeishuBot {
|
|
|
253
261
|
private async sendWebList(feishuKey: string, chatId: string): Promise<void> {
|
|
254
262
|
const current = this.attachedWeb(feishuKey);
|
|
255
263
|
const convs = hub
|
|
256
|
-
.list()
|
|
257
|
-
.filter((c) => c.channel === "web" && !c.hidden)
|
|
264
|
+
.list((c) => c.channel === "web" && !c.hidden && adminVisible(c.owner))
|
|
258
265
|
.slice(0, 10)
|
|
259
266
|
.map((c) => this.webEntry(c.key)!)
|
|
260
267
|
.filter(Boolean);
|
|
@@ -613,7 +620,14 @@ export class FeishuBot {
|
|
|
613
620
|
const command = attachments.length > 0 ? ({ kind: "chat", text } as const) : parseCommand(text);
|
|
614
621
|
if (command.kind !== "chat") {
|
|
615
622
|
try {
|
|
616
|
-
const reply = await hub.command(key, command, {
|
|
623
|
+
const reply = await hub.command(key, command, {
|
|
624
|
+
allowedAgents: this.allowedFor(key),
|
|
625
|
+
by: "飞书命令",
|
|
626
|
+
channel: "feishu",
|
|
627
|
+
// 普通成员在 Web 上开的 agent 会话、挂着的请求,飞书里都碰不到
|
|
628
|
+
...(hasMembers() ? { sessionVisible: (id: string) => adminVisible(sessionOwner(id)) } : {}),
|
|
629
|
+
convVisible: (k) => adminVisible(getConversation(k)?.owner),
|
|
630
|
+
});
|
|
617
631
|
await this.renderReply(key, chatId, reply);
|
|
618
632
|
} catch (err) {
|
|
619
633
|
await this.client.sendText(chatId, `出错了:${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Web 上「这个用户能碰哪些东西」。规则本身在 core/users.ts 的 ownedBy。
|
|
2
|
+
import { getConversation, sessionOwner, type Conversation } from "../../core/store.js";
|
|
3
|
+
import { legacyOwnerId, listUsers, ownedBy, type User } from "../../core/users.js";
|
|
4
|
+
import type { OwnerFilter } from "./push.js";
|
|
5
|
+
|
|
6
|
+
export function canSeeConv(user: User, conv: Conversation | undefined): conv is Conversation {
|
|
7
|
+
return !!conv && ownedBy(user, conv.owner);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function canSeeKey(user: User, key: string): boolean {
|
|
11
|
+
return canSeeConv(user, getConversation(key));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 这个用户能看到、能恢复哪些 agent 会话。
|
|
16
|
+
* 只有他一个用户时什么都不会被滤掉,返回 undefined —— 列会话时就不用为了过滤多读一大批
|
|
17
|
+
*/
|
|
18
|
+
export function sessionVisibleTo(user: User): ((sessionId: string) => boolean) | undefined {
|
|
19
|
+
if (listUsers().every((u) => u.id === user.id)) return undefined;
|
|
20
|
+
return (sessionId) => ownedBy(user, sessionOwner(sessionId));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 推送订阅是不是这个用户的。升级前的订阅没记主人,算迁移出来的那个管理员的 */
|
|
24
|
+
export function pushOwnedBy(userId: string): OwnerFilter {
|
|
25
|
+
return (owner) => (owner ?? legacyOwnerId()) === userId;
|
|
26
|
+
}
|