agent-loft 0.2.39 → 0.2.41
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 +4 -3
- package/package.json +1 -1
- package/src/channels/feishu/bot.ts +1 -1
- package/src/core/command-catalog.ts +1 -1
- package/src/core/feishu-links.ts +1 -1
- package/web/css/mobile.css +19 -29
- package/web/css/remote.css +4 -2
- package/web/css/settings.css +41 -1
- package/web/lib/overlay.js +7 -3
- package/web/settings/agents.js +82 -52
- package/web/settings/commands.js +41 -111
- package/web/settings/common.js +37 -0
- package/web/settings/feishu.js +253 -67
- package/web/settings/general.js +81 -131
- package/web/settings/index.js +108 -49
- package/web/settings/memory.js +3 -78
- package/web/settings/notify.js +13 -0
- package/web/settings/security.js +146 -159
- package/web/settings/users.js +228 -120
- package/web/views/composer.js +21 -0
- package/web/views/mention.js +12 -3
- package/web/views/quota.js +2 -2
- package/web/views/remote.js +10 -0
- package/web/views/sidebar.js +5 -2
- package/web/views/slash.js +7 -2
package/web/settings/security.js
CHANGED
|
@@ -1,188 +1,175 @@
|
|
|
1
1
|
import { fill, h, toast } from "../lib/dom.js";
|
|
2
2
|
import { api } from "../lib/api.js";
|
|
3
3
|
import { relTime } from "../lib/time.js";
|
|
4
|
+
import { copyText } from "../lib/markdown.js";
|
|
4
5
|
import { isAdmin, S } from "../state.js";
|
|
5
6
|
import { dropPushSub } from "../lib/push.js";
|
|
6
|
-
import { field } from "./common.js";
|
|
7
|
-
import { feishuLinkSection } from "./memory.js";
|
|
7
|
+
import { field, saveButton } from "./common.js";
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"div",
|
|
24
|
-
{ class: "list-item" },
|
|
25
|
-
h("div", { class: "grow" }, h("div", { class: "t" }, `${request.code} · ${botName(channelId)}`), h("div", { class: "s" }, `${request.userId} · ${relTime(request.createdAt)}`)),
|
|
26
|
-
h(
|
|
27
|
-
"button",
|
|
28
|
-
{
|
|
29
|
-
class: "btn small accent",
|
|
30
|
-
onclick: async () => {
|
|
31
|
-
try {
|
|
32
|
-
await api("/api/pairing/approve", { method: "POST", body: { code: request.code } });
|
|
33
|
-
toast("已批准");
|
|
34
|
-
fill(panel, );
|
|
35
|
-
void authTab(panel);
|
|
36
|
-
} catch (err) {
|
|
37
|
-
toast(err.message);
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
"批准",
|
|
42
|
-
),
|
|
43
|
-
),
|
|
44
|
-
),
|
|
45
|
-
),
|
|
46
|
-
h(
|
|
47
|
-
"div",
|
|
48
|
-
{ class: "section" },
|
|
49
|
-
h("h4", {}, "已配对的用户"),
|
|
50
|
-
pairs.approved.length === 0
|
|
51
|
-
? h("div", { class: "muted" }, "无")
|
|
52
|
-
: pairs.approved.map(({ channelId, userId }) =>
|
|
53
|
-
h(
|
|
54
|
-
"div",
|
|
55
|
-
{ class: "list-item" },
|
|
56
|
-
h("div", { class: "grow" }, h("div", { class: "t" }, userId), h("div", { class: "s" }, botName(channelId))),
|
|
57
|
-
h(
|
|
58
|
-
"button",
|
|
59
|
-
{
|
|
60
|
-
class: "btn small danger",
|
|
61
|
-
onclick: async () => {
|
|
62
|
-
await api("/api/pairing/revoke", { method: "POST", body: { channelId, userId } });
|
|
63
|
-
fill(panel, );
|
|
64
|
-
void authTab(panel);
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
"撤销",
|
|
68
|
-
),
|
|
69
|
-
),
|
|
70
|
-
),
|
|
71
|
-
),
|
|
9
|
+
/** 设置 → 账号:我是谁、改密码、绑定飞书、我登录着的设备。别人的设备在 管理 →「用户」里 */
|
|
10
|
+
export async function accountTab(page) {
|
|
11
|
+
const logout = h("button", { class: "btn small danger" }, "退出登录");
|
|
12
|
+
logout.onclick = async () => {
|
|
13
|
+
logout.disabled = true;
|
|
14
|
+
// 退出了就别再收这个账号的通知:换个人在这台设备上登录,不能收到上一个人的
|
|
15
|
+
await dropPushSub().catch(() => {});
|
|
16
|
+
await api("/api/logout", { method: "POST" });
|
|
17
|
+
location.reload();
|
|
18
|
+
};
|
|
19
|
+
const old = h("input", { type: "password", autocomplete: "current-password" });
|
|
20
|
+
const pw = h("input", { type: "password", autocomplete: "new-password" });
|
|
21
|
+
const devicesBox = h("div", { class: "section" }, h("h4", {}, "已登录的设备"), h("span", { class: "spinner" }));
|
|
22
|
+
page.append(
|
|
72
23
|
h(
|
|
73
24
|
"div",
|
|
74
25
|
{ class: "section" },
|
|
75
|
-
h("h4", {}, "Claude「总是允许」名单"),
|
|
76
|
-
h("div", { class: "muted" }, "点过「总是允许」的会记在这里。Bash:git 表示 git 开头的命令都放行;Edit:/路径 表示该目录下的修改都放行。一行一条,删掉即撤销。"),
|
|
77
|
-
allowText,
|
|
78
26
|
h(
|
|
79
|
-
"
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
class: "btn primary",
|
|
85
|
-
onclick: async () => {
|
|
86
|
-
await api("/api/claude/allowlist", { method: "PUT", body: { list: allowText.value.split("\n") } });
|
|
87
|
-
toast("已保存");
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
"保存",
|
|
91
|
-
),
|
|
27
|
+
"h4",
|
|
28
|
+
{},
|
|
29
|
+
"当前账号",
|
|
30
|
+
h("span", { class: "grow" }),
|
|
31
|
+
logout,
|
|
92
32
|
),
|
|
33
|
+
S.user ? h("div", {}, S.user.name, " ", h("span", { class: "status-tag" }, S.user.role === "admin" ? "管理员" : "成员")) : null,
|
|
93
34
|
),
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export async function securityTab(panel) {
|
|
98
|
-
const old = h("input", { type: "password", autocomplete: "current-password" });
|
|
99
|
-
const pw = h("input", { type: "password", autocomplete: "new-password" });
|
|
100
|
-
const sessions = await api("/api/web-sessions");
|
|
101
|
-
// 管理员看到的是所有人的设备:不止一个人时标上是谁的
|
|
102
|
-
const owners = new Set(sessions.map((s) => s.userId));
|
|
103
|
-
const showOwner = isAdmin() && owners.size > 1;
|
|
104
|
-
panel.append(
|
|
105
|
-
S.user
|
|
106
|
-
? h(
|
|
107
|
-
"div",
|
|
108
|
-
{ class: "section" },
|
|
109
|
-
h("h4", {}, "当前账号"),
|
|
110
|
-
h("div", {}, S.user.name, " ", h("span", { class: "status-tag" }, S.user.role === "admin" ? "管理员" : "成员")),
|
|
111
|
-
)
|
|
112
|
-
: null,
|
|
113
35
|
h(
|
|
114
36
|
"div",
|
|
115
|
-
{ class: "section" },
|
|
37
|
+
{ class: "section", "data-form": "" },
|
|
116
38
|
h("h4", {}, "修改登录密码"),
|
|
117
39
|
field("原密码", old),
|
|
118
40
|
field("新密码(至少 8 位)", pw),
|
|
119
41
|
h(
|
|
120
42
|
"div",
|
|
121
43
|
{ class: "row" },
|
|
122
|
-
|
|
123
|
-
"
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
await api("/api/password", { method: "POST", body: { old: old.value, password: pw.value } });
|
|
129
|
-
toast("密码已修改,你的其它设备已下线");
|
|
130
|
-
old.value = pw.value = "";
|
|
131
|
-
} catch (err) {
|
|
132
|
-
toast(err.message);
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
"修改",
|
|
137
|
-
),
|
|
44
|
+
saveButton(async () => {
|
|
45
|
+
await api("/api/password", { method: "POST", body: { old: old.value, password: pw.value } });
|
|
46
|
+
toast("密码已修改,你的其它设备已下线");
|
|
47
|
+
old.value = pw.value = "";
|
|
48
|
+
void loadDevices();
|
|
49
|
+
}, "修改"),
|
|
138
50
|
),
|
|
139
51
|
),
|
|
140
52
|
feishuLinkSection(),
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
53
|
+
devicesBox,
|
|
54
|
+
);
|
|
55
|
+
const loadDevices = async () => {
|
|
56
|
+
let sessions;
|
|
57
|
+
try {
|
|
58
|
+
sessions = await api("/api/web-sessions");
|
|
59
|
+
} catch (err) {
|
|
60
|
+
fill(devicesBox, h("h4", {}, "已登录的设备"), h("div", { class: "error-text" }, err.message));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// 管理员拿到的是所有人的:这里只放自己的(以前单用户时登录的设备没有主人,也算管理员自己的)
|
|
64
|
+
const mine = isAdmin() && S.user ? sessions.filter((s) => !s.userId || s.userId === S.user.id) : sessions;
|
|
65
|
+
fill(devicesBox, h("h4", {}, `已登录的设备(${mine.length})`), deviceList(mine, { onChange: loadDevices }));
|
|
66
|
+
};
|
|
67
|
+
await loadDevices();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 登录设备列表,每台一个「下线」。showOwner:标上是谁的(管理员看所有人时) */
|
|
71
|
+
export function deviceList(sessions, { showOwner = false, onChange }) {
|
|
72
|
+
if (!sessions.length) return h("div", { class: "muted" }, "没有");
|
|
73
|
+
return sessions
|
|
74
|
+
.slice()
|
|
75
|
+
.sort((a, b) => b.lastSeen - a.lastSeen)
|
|
76
|
+
.map((s) => {
|
|
77
|
+
const off = h("button", { class: "btn small danger" }, "下线");
|
|
78
|
+
off.onclick = async () => {
|
|
79
|
+
off.disabled = true;
|
|
80
|
+
try {
|
|
81
|
+
await api("/api/web-sessions/revoke", { method: "POST", body: { id: s.id } });
|
|
82
|
+
toast("已下线");
|
|
83
|
+
onChange();
|
|
84
|
+
} catch (err) {
|
|
85
|
+
toast(err.message);
|
|
86
|
+
off.disabled = false;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
return h(
|
|
90
|
+
"div",
|
|
91
|
+
{ class: "list-item" },
|
|
146
92
|
h(
|
|
147
93
|
"div",
|
|
148
|
-
{ class: "
|
|
94
|
+
{ class: "grow" },
|
|
95
|
+
h("div", { class: "t" }, s.userAgent.slice(0, 80) || "未知设备"),
|
|
96
|
+
h("div", { class: "s" }, `${showOwner ? `${s.userName ?? "已删除的用户"} · ` : ""}${s.ip} · 最近活动 ${relTime(s.lastSeen)}`),
|
|
97
|
+
),
|
|
98
|
+
off,
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 「账号」页里的飞书绑定:Web 上生成绑定码,再在飞书私聊里发 /bind,两边都证明是本人 */
|
|
104
|
+
function feishuLinkSection() {
|
|
105
|
+
const box = h("div", { class: "section" });
|
|
106
|
+
const load = async () => {
|
|
107
|
+
let data;
|
|
108
|
+
try {
|
|
109
|
+
data = await api("/api/account/feishu");
|
|
110
|
+
} catch (err) {
|
|
111
|
+
fill(box, h("h4", {}, "飞书账号"), h("div", { class: "error-text" }, err.message));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// 没有飞书机器人、也没绑过:整节不显示
|
|
115
|
+
box.hidden = !data.bots.length && !data.links.length;
|
|
116
|
+
if (box.hidden) return fill(box);
|
|
117
|
+
const codeBox = h("div");
|
|
118
|
+
const gen = h("button", { class: "btn" }, data.links.length ? "再绑一个" : "生成绑定码");
|
|
119
|
+
gen.onclick = async () => {
|
|
120
|
+
gen.disabled = true;
|
|
121
|
+
try {
|
|
122
|
+
const { code, expiresAt } = await api("/api/account/feishu/code", { method: "POST" });
|
|
123
|
+
const cmd = `/bind ${code}`;
|
|
124
|
+
const copy = h("button", { class: "btn small" }, "复制");
|
|
125
|
+
copy.onclick = () => copyText(cmd, copy);
|
|
126
|
+
fill(
|
|
127
|
+
codeBox,
|
|
149
128
|
h(
|
|
150
129
|
"div",
|
|
151
|
-
{ class: "
|
|
152
|
-
h(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
onclick: async () => {
|
|
160
|
-
await api("/api/web-sessions/revoke", { method: "POST", body: { id: s.id } });
|
|
161
|
-
fill(panel, );
|
|
162
|
-
void securityTab(panel);
|
|
163
|
-
},
|
|
164
|
-
},
|
|
165
|
-
"下线",
|
|
130
|
+
{ class: "list-item" },
|
|
131
|
+
h(
|
|
132
|
+
"div",
|
|
133
|
+
{ class: "grow" },
|
|
134
|
+
h("div", { class: "t" }, h("code", {}, cmd)),
|
|
135
|
+
h("div", { class: "s" }, `在飞书里私聊机器人(${data.bots.map((b) => b.name).join("、") || "飞书机器人"})发这条消息,${Math.round((expiresAt - Date.now()) / 60000)} 分钟内有效。没用过机器人的话,要先让管理员批准你的配对码。`),
|
|
136
|
+
),
|
|
137
|
+
copy,
|
|
166
138
|
),
|
|
167
|
-
)
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
139
|
+
);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
toast(err.message);
|
|
142
|
+
}
|
|
143
|
+
gen.disabled = false;
|
|
144
|
+
};
|
|
145
|
+
fill(
|
|
146
|
+
box,
|
|
147
|
+
h("h4", {}, "飞书账号"),
|
|
148
|
+
h("div", { class: "muted" }, "绑定后,你和飞书机器人的私聊就是你的个人会话:用你的记忆,/sessions 只列你的会话,/web 能接上你自己的 Web 会话。群聊不受影响。"),
|
|
149
|
+
data.links.map((l) => {
|
|
150
|
+
const rm = h("button", { class: "btn small danger" }, "解除绑定");
|
|
151
|
+
rm.onclick = async () => {
|
|
152
|
+
rm.disabled = true;
|
|
153
|
+
try {
|
|
154
|
+
await api("/api/account/feishu", { method: "DELETE", body: { botId: l.botId, openId: l.openId } });
|
|
155
|
+
toast("已解除,那个私聊不再用你的记忆");
|
|
156
|
+
void load();
|
|
157
|
+
} catch (err) {
|
|
158
|
+
toast(err.message);
|
|
159
|
+
rm.disabled = false;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
return h(
|
|
163
|
+
"div",
|
|
164
|
+
{ class: "list-item" },
|
|
165
|
+
h("div", { class: "grow" }, h("div", { class: "t" }, l.botName), h("div", { class: "s" }, `${l.openIdShort} · 绑定于 ${relTime(l.boundAt)}`)),
|
|
166
|
+
rm,
|
|
167
|
+
);
|
|
168
|
+
}),
|
|
169
|
+
h("div", { class: "row" }, gen),
|
|
170
|
+
codeBox,
|
|
171
|
+
);
|
|
172
|
+
};
|
|
173
|
+
void load();
|
|
174
|
+
return box;
|
|
188
175
|
}
|