agent-loft 0.2.21 → 0.2.22

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # agent-loft
4
4
 
5
- **多个 agent 的工作台。** 把本机的 **Claude Code**、**Codex**(以后还有更多)放到同一张台面上统一管理,在网页、飞书和 Mac 桌面端随时驱动它们,进展推送到飞书、iPhone 和 Mac。
5
+ **多个 agent 的工作台。** 把本机的 **Claude Code**、**Codex**(以后还有更多)放到同一张台面上统一管理,在网页、飞书和桌面端(macOS / Windows / Linux)随时驱动它们,进展推送到飞书、iPhone 和 Mac。
6
6
 
7
7
  ![Web 工作台](docs/images/web-chat.png)
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
 
@@ -66,18 +66,24 @@ agent-loft service install # 注册为开机自启服务(systemd / laun
66
66
 
67
67
  打开 `http://<机器 IP>:8800`(本机用 `http://localhost:8800`):第一次需要输入日志里的设置码,然后设一个登录密码。设置码找不到的话,直接运行 `agent-loft passwd` 设密码。
68
68
 
69
- 到 设置 → Agent 检查 Claude Code、Codex 装好、登录了没有:Claude Code 随 agent-loft 一起安装,只需在服务端终端里登录;Codex 可以点「直接在服务器上安装」。设置 → 通用 里能看到服务端版本,有新版本时点「升级」即可(`npm install -g agent-loft@latest`,以系统服务运行时会自动重启,打开的页面会自动刷新)。
69
+ 到 设置 → Agent 检查 Claude Code、Codex 装好、登录了没有:Claude Code 随 agent-loft 一起安装,只需在服务端终端里登录;Codex 可以点「直接在服务器上安装」。设置 → 系统 里能看到服务端版本,有新版本时点「升级」即可(`npm install -g agent-loft@latest`,以系统服务运行时会自动重启,打开的页面会自动刷新)。
70
70
 
71
71
  在外面只用飞书的话不需要任何网络配置(机器人由服务端主动连向飞书)。在外面用网页、系统推送和语音输入需要 HTTPS,见下面的「安全」和[产品介绍里的「从外网访问」](docs/产品介绍.md#从外网访问)。
72
72
 
73
- ### macOS 桌面端
73
+ ### 桌面端(macOS / Windows / Linux)
74
74
 
75
- 到 [Releases](https://github.com/qijieinnet/agent-loft/releases) 下载 `desktop-v*` 里对应芯片的 DMG(Apple 芯片选 arm64,Intel 选 x64),拖进「应用程序」。还没有 Apple 开发者签名,第一次打开如果提示无法验证,到 系统设置 → 隐私与安全性 里点「仍要打开」。
75
+ 到 [Releases](https://github.com/qijieinnet/agent-loft/releases) 下载 `desktop-v*` 里对应的安装包:
76
+
77
+ - **macOS**:对应芯片的 DMG(Apple 芯片选 arm64,Intel 选 x64),拖进「应用程序」。还没有 Apple 开发者签名,第一次打开如果提示无法验证,到 系统设置 → 隐私与安全性 里点「仍要打开」。
78
+ - **Windows**:`Agent-Loft-Setup-*-x64.exe`,装在用户目录里,不用管理员权限。没有代码签名,SmartScreen 拦的话点「更多信息 → 仍要运行」。
79
+ - **Linux**:Debian / Ubuntu 用 deb(`sudo apt install ./agent-loft-desktop_*_amd64.deb`),其他发行版用 AppImage(需要 libfuse2);ARM 选 arm64。App 内一键更新只支持 AppImage。GNOME 要装 AppIndicator 扩展才看得到托盘图标。
80
+
81
+ 下面以 macOS 为例,Windows / Linux 上「菜单栏」是托盘、`⌘` 是 `Ctrl`:
76
82
 
77
83
  - 同时连多台服务器,顶部标签栏切换、拖动排序;每台各自登录,切走了也在后台连着,回复完成、需要授权时弹系统通知,程序坞图标显示未读数。
78
- - 菜单栏图标:悬停看各服务器的 Claude / Codex 额度、待处理和运行中的会话;双击打开主窗口。同一个账号的额度只查一次。
84
+ - 菜单栏图标:悬停看各服务器的 Claude / Codex 额度、待处理和运行中的会话;双击打开主窗口。同一个账号的额度只查一次。(Windows 上点托盘图标弹出;Linux 上在托盘菜单里点「额度和待处理」。)
79
85
  - 支持前面挡着 Cloudflare Access 的服务器:验证页直接在窗口里完成,过期会提醒;也可以配 Service Token 免验证。
80
- - 首次打开可以选「这台 Mac 也作为服务端」:一键在终端里安装 agent-loft(没有 Node.js 会先用 Homebrew 装)并开机自启,装好自动连上。可选开启「本机有会话在跑时不休眠」。
86
+ - 首次打开可以选「这台 Mac 也作为服务端」:一键在终端里安装 agent-loft(没有 Node.js 会先用 Homebrew 装)并开机自启,装好自动连上。Linux 上一样(需要先装好 Node.js 22+,开机自启用 systemd 用户服务);Windows 上服务端还没有开机自启,可以在 WSL 里装,桌面端会自动检测到 `127.0.0.1` 上的它。可选开启「本机有会话在跑时不休眠」。
81
87
  - 设置(`⌘,`)的服务器列表里展开某台服务器,能看它的 Agent 环境和版本,有新版本时点「升级服务端」;桌面端自己的更新在同一页的「更新」里点「更新并重启」。
82
88
 
83
89
  桌面端只是个壳(代码在 `desktop/`),服务端照常用 npm 安装和升级。
@@ -116,7 +122,7 @@ agent-loft service install
116
122
 
117
123
  - Web 登录密码用 scrypt 哈希保存。首次设置必须用启动日志里的一次性设置码,防止「谁先打开页面谁就成了主人」。
118
124
  - 登录态通过 HttpOnly cookie 保存,服务端只存它的哈希。同一 IP 连续输错 5 次锁 15 分钟。所有写请求和 WebSocket 都校验 Origin。页面开启了严格的 CSP。
119
- - 暴露到公网时,只通过 HTTPS 反向代理(比如 Lucky)或 Cloudflare Tunnel 访问,并把不在本机的反代 IP 加进 `web.trustedProxies`(只有从这些地址来的连接才采信 `X-Forwarded-*`)。不要直接把 8800 端口映射出去。局域网访问可以在 设置 → 通用 → 网络访问 里关掉。
125
+ - 暴露到公网时,只通过 HTTPS 反向代理(比如 Lucky)或 Cloudflare Tunnel 访问,并把不在本机的反代 IP 加进 `web.trustedProxies`(只有从这些地址来的连接才采信 `X-Forwarded-*`)。不要直接把 8800 端口映射出去。局域网访问可以在 设置 → 系统 → 网络访问 里关掉。
120
126
  - 审批超时一律按拒绝处理。Codex 默认沙箱为 `workspace-write`,不会因为 `~/.codex/config.toml` 里配了 `never` 或 `danger-full-access` 就绕过审批。
121
127
  - agent 子进程拿不到本服务自己的凭据(`FEISHU_*`、`AGENT_LOFT_*` 等环境变量会被剔除)。
122
128
 
@@ -137,7 +143,7 @@ src/
137
143
  feishu/ 飞书:一个 FeishuBot 对应一个机器人,支持多个
138
144
  web/ HTTP + WebSocket + 鉴权
139
145
  web/ 前端(原生 JS,不需要构建)
140
- desktop/ macOS 桌面端(Electron 外壳)
146
+ desktop/ 桌面端(Electron 外壳,macOS / Windows / Linux)
141
147
  docs/ 产品介绍和截图
142
148
  ```
143
149
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-loft",
3
- "version": "0.2.21",
3
+ "version": "0.2.22",
4
4
  "type": "module",
5
5
  "description": "多个 agent 的工作台:把本机 Claude Code / Codex 放到一处统一管理,用飞书和 Web 页面随时驱动——会话、审批、模型切换都共用",
6
6
  "keywords": [
@@ -16,25 +16,26 @@ export type BuiltinSpec = {
16
16
  feishuOnly?: boolean;
17
17
  };
18
18
 
19
+ // 按常用程度排:这也是设置 → 命令、「/」列表和 /help 里的默认顺序
19
20
  export const BUILTIN_COMMANDS: BuiltinSpec[] = [
20
21
  { name: "new", args: "[模型]", description: "开一条新会话" },
21
- { name: "status", description: "查看当前会话、模型、目录、剩余额度" },
22
22
  { name: "stop", description: "中断当前执行" },
23
23
  { name: "model", args: "[名称]", description: "切换模型(不带参数弹选择)" },
24
24
  { name: "effort", args: "[程度]", description: "切换思考程度" },
25
+ { name: "status", description: "查看当前会话、模型、目录、剩余额度" },
25
26
  { name: "sessions", args: "[all]", description: "列出历史会话,点按钮恢复;加 all 列所有目录" },
26
27
  { name: "resume", args: "<会话ID>", description: "恢复指定会话" },
28
+ { name: "cd", args: "<相对目录>", description: "切换工作目录(限 workspace 内)" },
29
+ { name: "pwd", description: "显示当前工作目录" },
27
30
  { name: "fork", description: "从当前会话分叉,原会话保留" },
31
+ { name: "autoedit", args: "[on|off]", description: "本对话自动允许改工作目录内的文件(Bash 仍询问)" },
28
32
  { name: "agent", args: "[名称]", description: "查看 / 切换 agent", multiAgentOnly: true },
29
33
  { name: "claude", description: "切换到 Claude Code", multiAgentOnly: true },
30
34
  { name: "codex", description: "切换到 Codex", multiAgentOnly: true },
31
- { name: "cd", args: "<相对目录>", description: "切换工作目录(限 workspace 内)" },
32
- { name: "pwd", description: "显示当前工作目录" },
33
- { name: "autoedit", args: "[on|off]", description: "本对话自动允许改工作目录内的文件(Bash 仍询问)" },
34
35
  { name: "web", description: "接上一条 Web 会话,在飞书里查看和回复", feishuOnly: true },
35
36
  { name: "detach", description: "不再接着 Web 会话,回到这个聊天自己的会话", feishuOnly: true },
36
- { name: "approve", args: "<请求ID> allow-once|allow-always|allow-session|deny", description: "审批兜底(卡片失效时用)" },
37
37
  { name: "help", description: "显示帮助" },
38
+ { name: "approve", args: "<请求ID> allow-once|allow-always|allow-session|deny", description: "审批兜底(卡片失效时用)" },
38
39
  ];
39
40
 
40
41
  /** agent 自己报上来的命令 */
@@ -132,7 +133,10 @@ function needsArgs(args: string | undefined): boolean {
132
133
  * 合并内置命令和 agent 原生命令,再按用户配置排序。
133
134
  * - 只对接得上的 agent 才列 /claude /codex 这类快捷切换;
134
135
  * - 和内置命令重名的原生命令丢掉:反正会被 agent-loft 先拦下来,列出来只会误导;
135
- * - 配置里排过序的在前,按配置的顺序;没排过的(比如新装的 skill)按默认顺序接在后面。
136
+ * - 配置里排过序的在前,按配置的顺序;没排过的(比如新装的 skill)按默认顺序接在后面;
137
+ * - 默认顺序:agent-loft 内置命令(按常用程度)→ 自定义命令 → agent 原生命令;
138
+ * - agent 原生命令默认不显示:只有在设置里保存过(出现在 order 里)且没被隐藏的才显示。
139
+ * 设置页每次保存都会把整张表的顺序存下来,所以存过的照用户的选择,以后新装的 skill 默认不勾。
136
140
  */
137
141
  export function buildCatalog(params: {
138
142
  allowedAgents: string[];
@@ -202,7 +206,7 @@ export function buildCatalog(params: {
202
206
  if (rb !== undefined) return 1;
203
207
  return a.i - b.i;
204
208
  })
205
- .map(({ c }) => ({ ...c, hidden: hidden.has(c.name) }));
209
+ .map(({ c }) => ({ ...c, hidden: hidden.has(c.name) || (c.source === "agent" && !rank.has(c.name)) }));
206
210
  }
207
211
 
208
212
  /** 「/name args」,给人看的写法 */
@@ -91,7 +91,7 @@ export function helpText(agents: string[], channel?: string): string {
91
91
  "",
92
92
  "Agent 给选项时会发一张提问卡,点按钮作答;都不合适就直接发消息,那条消息会被当成你的回答。",
93
93
  "",
94
- "**命令**(需整条消息只有命令;只发一个 `/` 可以列出全部命令,包括 agent 自己的)",
94
+ "**命令**(需整条消息只有命令;只发一个 `/` 可以列出命令;agent 自己的命令默认不在列表里,可以在设置 → 命令里勾选,没勾的照样可以直接发)",
95
95
  "",
96
96
  ...BUILTIN_COMMANDS.filter((c) => !c.multiAgentOnly || multi)
97
97
  .filter((c) => !c.feishuOnly || channel === "feishu")
package/web/app.js CHANGED
@@ -3224,7 +3224,7 @@ async function openSettings() {
3224
3224
  const nav = h("div", { class: "settings-nav" });
3225
3225
  const panel = h("div", { class: "settings-body" });
3226
3226
  // 设置弹窗高度固定,切标签页时不跟着内容一伸一缩
3227
- const close = modal({ title: "设置", wide: true, tall: true, body: h("div", { class: "settings" }, nav, panel) });
3227
+ modal({ title: "设置", wide: true, tall: true, body: h("div", { class: "settings" }, nav, panel) });
3228
3228
  let cfg;
3229
3229
  try {
3230
3230
  cfg = await api("/api/config");
@@ -3240,7 +3240,7 @@ async function openSettings() {
3240
3240
  ["授权", () => authTab(panel)],
3241
3241
  ["通知", () => notifyTab(panel)],
3242
3242
  ["安全", () => securityTab(panel)],
3243
- ["系统", () => systemTab(panel, close)],
3243
+ ["系统", () => systemTab(cfg, panel)],
3244
3244
  ];
3245
3245
  const select = (i) => {
3246
3246
  nav.querySelectorAll("button").forEach((b, j) => b.classList.toggle("on", i === j));
@@ -3461,7 +3461,6 @@ function serverVersionSection() {
3461
3461
  }
3462
3462
 
3463
3463
  function generalTab(cfg, panel) {
3464
- panel.append(serverVersionSection());
3465
3464
  const root = h("input", { value: cfg.workspaceRoot });
3466
3465
  const approval = h("input", { type: "number", min: 1, value: Math.round(cfg.approvalTimeoutMs / 60000) });
3467
3466
  const ttl = h("input", { type: "number", min: 0, value: Math.round(cfg.sessionTtlMs / 3600000) });
@@ -3501,7 +3500,6 @@ function generalTab(cfg, panel) {
3501
3500
  ),
3502
3501
  ),
3503
3502
  );
3504
- panel.append(networkSection(cfg));
3505
3503
  }
3506
3504
 
3507
3505
  /** 只有本机能连的监听地址(和服务端 listen.ts 的判断一致) */
@@ -3707,7 +3705,8 @@ function customDialog({ agent, entry, taken }, onSave) {
3707
3705
 
3708
3706
  /**
3709
3707
  * 设置 → 命令:每个 agent 一张表,拖动或用箭头排序,取消勾选就不在「/」列表里出现。
3710
- * 保存时把整张表的顺序存下来;以后新装的 skill 不在表里,会自动接在最后。
3708
+ * 默认 agent-loft 内置命令按常用程度排在最前,agent 自己的命令排在后面且默认不勾(服务端 buildCatalog 决定)。
3709
+ * 保存时把整张表的顺序存下来;以后新装的 skill 不在表里,会自动接在最后、默认不勾。
3711
3710
  * 用户可以自己添加命令(一段预设的提示词),自己加的可以编辑、删除,增删改后立即保存。
3712
3711
  */
3713
3712
  function commandsTab(cfg, panel) {
@@ -3919,7 +3918,7 @@ function commandsTab(cfg, panel) {
3919
3918
  h(
3920
3919
  "div",
3921
3920
  { class: "muted" },
3922
- "在输入框里打「/」时弹出的命令,按这里的顺序排列;保存后会自动同步到飞书机器人的指令面板。取消勾选的不会出现在列表里,但照样可以手动输入。带 agent 名标记的是 agent 自己的命令(skills 等),会原样转给 agent。",
3921
+ "在输入框里打「/」时弹出的命令,按这里的顺序排列;保存后会自动同步到飞书机器人的指令面板。取消勾选的不会出现在列表里,但照样可以手动输入。带 agent 名标记的是 agent 自己的命令(skills 等),默认不勾选,需要的话勾上并保存;它们会原样转给 agent。",
3923
3922
  ),
3924
3923
  tabs,
3925
3924
  h(
@@ -4530,40 +4529,46 @@ async function securityTab(panel) {
4530
4529
  );
4531
4530
  }
4532
4531
 
4533
- async function systemTab(panel) {
4534
- const sys = await api("/api/system");
4535
- panel.append(
4532
+ async function systemTab(cfg, panel) {
4533
+ // 系统信息要等接口返回,先占好位置,版本和网络访问两节不用等它
4534
+ const sysBox = h("div", { class: "section" }, h("h4", {}, "系统", h("span", { class: "grow" }), h("span", { class: "spinner" })));
4535
+ panel.append(sysBox, serverVersionSection(), networkSection(cfg));
4536
+ let sys;
4537
+ try {
4538
+ sys = await api("/api/system");
4539
+ } catch (err) {
4540
+ fill(sysBox, h("h4", {}, "系统"), h("div", { class: "error-text" }, err.message));
4541
+ return;
4542
+ }
4543
+ fill(
4544
+ sysBox,
4545
+ h("h4", {}, "系统"),
4546
+ h("div", {}, `版本 ${sys.version} · Node ${sys.node} · 已运行 ${Math.round(sys.uptimeMs / 60000)} 分钟`),
4547
+ h("div", { class: "muted" }, sys.underSystemd ? "以系统服务运行" : "前台运行(重启需要在终端里操作)"),
4536
4548
  h(
4537
4549
  "div",
4538
- { class: "section" },
4539
- h("h4", {}, "系统"),
4540
- h("div", {}, `版本 ${sys.version} · Node ${sys.node} · 已运行 ${Math.round(sys.uptimeMs / 60000)} 分钟`),
4541
- h("div", { class: "muted" }, sys.underSystemd ? "以系统服务运行" : "前台运行(重启需要在终端里操作)"),
4550
+ { class: "row" },
4542
4551
  h(
4543
- "div",
4544
- { class: "row" },
4545
- h(
4546
- "button",
4547
- {
4548
- class: "btn",
4549
- disabled: !sys.underSystemd,
4550
- onclick: async () => {
4551
- if (!(await confirmDialog({ title: "重启 agent-loft?", message: "进行中的对话会被中断。", confirmText: "重启", danger: true }))) return;
4552
- try {
4553
- await api("/api/system/restart", { method: "POST" });
4554
- toast("正在重启…");
4555
- setTimeout(() => location.reload(), 5000);
4556
- } catch (err) {
4557
- toast(err.message);
4558
- }
4559
- },
4552
+ "button",
4553
+ {
4554
+ class: "btn",
4555
+ disabled: !sys.underSystemd,
4556
+ onclick: async () => {
4557
+ if (!(await confirmDialog({ title: "重启 agent-loft?", message: "进行中的对话会被中断。", confirmText: "重启", danger: true }))) return;
4558
+ try {
4559
+ await api("/api/system/restart", { method: "POST" });
4560
+ toast("正在重启…");
4561
+ setTimeout(() => location.reload(), 5000);
4562
+ } catch (err) {
4563
+ toast(err.message);
4564
+ }
4560
4565
  },
4561
- "重启服务",
4562
- ),
4563
- "Notification" in window && Notification.permission !== "granted"
4564
- ? h("button", { class: "btn", onclick: () => Notification.requestPermission() }, "开启浏览器通知")
4565
- : null,
4566
+ },
4567
+ "重启服务",
4566
4568
  ),
4569
+ "Notification" in window && Notification.permission !== "granted"
4570
+ ? h("button", { class: "btn", onclick: () => Notification.requestPermission() }, "开启浏览器通知")
4571
+ : null,
4567
4572
  ),
4568
4573
  );
4569
4574
  }