@jesonliu/lark-claudecode-bridge 1.4.3 → 2.0.1
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/assets/web/css/main.css +774 -574
- package/assets/web/index.html +37 -17
- package/assets/web/js/auth.js +218 -0
- package/assets/web/js/bootstrap.js +36 -34
- package/assets/web/js/core.js +4 -3
- package/assets/web/js/main.js +79 -20
- package/assets/web/js/markdown.js +64 -0
- package/assets/web/js/pages/apps.js +338 -251
- package/assets/web/js/pages/chats.js +481 -0
- package/assets/web/js/pages/claude.js +243 -180
- package/assets/web/js/pages/mcp.js +59 -43
- package/assets/web/js/pages/memory.js +43 -46
- package/assets/web/js/pages/overview.js +426 -88
- package/assets/web/js/pages/permissions.js +121 -34
- package/assets/web/js/pages/plugins.js +51 -37
- package/assets/web/js/pages/skills.js +68 -92
- package/assets/web/js/pages/slash.js +31 -23
- package/assets/web/js/pages/workspaces.js +114 -73
- package/assets/web/js/theme.js +21 -18
- package/assets/web/js/ui.js +75 -32
- package/dist/access/access-control.d.ts +18 -0
- package/dist/access/access-control.js +29 -0
- package/dist/access/access-control.js.map +1 -1
- package/dist/bin/lcb.js +15 -0
- package/dist/bin/lcb.js.map +1 -1
- package/dist/feishu/name-cache.d.ts +15 -0
- package/dist/feishu/name-cache.js +82 -0
- package/dist/feishu/name-cache.js.map +1 -0
- package/dist/feishu/sibling-names.d.ts +1 -0
- package/dist/feishu/sibling-names.js +63 -0
- package/dist/feishu/sibling-names.js.map +1 -0
- package/dist/gateway/feishu-gateway.d.ts +24 -0
- package/dist/gateway/feishu-gateway.js +132 -0
- package/dist/gateway/feishu-gateway.js.map +1 -1
- package/dist/gateway/gateway-registry.d.ts +18 -0
- package/dist/gateway/gateway-registry.js +14 -0
- package/dist/gateway/gateway-registry.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +52 -9
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/web/chats-api.d.ts +93 -0
- package/dist/web/chats-api.js +603 -0
- package/dist/web/chats-api.js.map +1 -0
- package/dist/web/lifecycle.d.ts +21 -0
- package/dist/web/lifecycle.js +80 -3
- package/dist/web/lifecycle.js.map +1 -1
- package/dist/web/logs-api.d.ts +37 -0
- package/dist/web/logs-api.js +141 -0
- package/dist/web/logs-api.js.map +1 -0
- package/dist/web/server.js +196 -13
- package/dist/web/server.js.map +1 -1
- package/dist/web/webauth.d.ts +44 -0
- package/dist/web/webauth.js +221 -0
- package/dist/web/webauth.js.map +1 -0
- package/package.json +1 -1
package/assets/web/index.html
CHANGED
|
@@ -9,40 +9,60 @@
|
|
|
9
9
|
<link rel="stylesheet" href="/css/main.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
|
-
<header>
|
|
12
|
+
<header class="topbar">
|
|
13
13
|
<div class="brand">
|
|
14
|
-
<span class="logo" aria-hidden="true"><svg viewBox="0 0 24 24" width="
|
|
14
|
+
<span class="logo" aria-hidden="true"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3 4 7l4 4"/><path d="M4 7h16"/><path d="m16 21 4-4-4-4"/><path d="M20 17H4"/></svg></span>
|
|
15
15
|
<h1>飞书 Claude Code 桥接器</h1>
|
|
16
16
|
</div>
|
|
17
|
-
<span class="
|
|
18
|
-
<span class="
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
</div>
|
|
24
|
-
<span class="meta" id="hFirstRun"></span>
|
|
17
|
+
<span class="pill" id="hVer" hidden></span>
|
|
18
|
+
<span class="pill" id="hHost" hidden></span>
|
|
19
|
+
<span class="sp"></span>
|
|
20
|
+
<button class="theme-btn" id="lockBtn" title="锁定配置页" aria-label="锁定配置页" hidden></button>
|
|
21
|
+
<button class="theme-btn" id="settingsBtn" title="设置(密码 / 自动锁屏)" aria-label="设置(密码 / 自动锁屏)" hidden></button>
|
|
22
|
+
<button class="theme-btn" id="themeBtn" title="切换亮 / 暗" aria-label="切换主题"></button>
|
|
25
23
|
</header>
|
|
26
24
|
<div class="stale-bar" id="staleBar" hidden>⚠️ 配置页服务进程为旧版本(页面文件已更新):进程启停 / 重启 / 一键更新需重启桥接器(<code>lcb start</code> 或页面所在进程)后可用。</div>
|
|
27
25
|
<div class="layout">
|
|
28
|
-
<nav id="nav"></nav>
|
|
29
|
-
<main
|
|
26
|
+
<nav class="sidenav" id="nav"></nav>
|
|
27
|
+
<main>
|
|
28
|
+
<div class="page-head" id="pageHead" hidden></div>
|
|
29
|
+
<div id="view"></div>
|
|
30
|
+
<div class="mini-note" id="pageLegend" hidden></div>
|
|
31
|
+
</main>
|
|
30
32
|
</div>
|
|
31
|
-
<div class="
|
|
32
|
-
<aside class="drawer" role="dialog" aria-modal="true">
|
|
33
|
+
<div class="mask" id="drawerMask" hidden>
|
|
34
|
+
<aside class="drawer" role="dialog" aria-modal="true" id="drawerBox">
|
|
33
35
|
<div class="drawer-resize" id="drawerResize" title="拖拽调宽 · 双击恢复默认" aria-hidden="true"></div>
|
|
34
|
-
<header><h3 id="drawerTitle"></h3><button class="btn" id="drawerClose"
|
|
36
|
+
<header><h3 id="drawerTitle"></h3><button class="btn ghost" id="drawerClose" aria-label="关闭">✕</button></header>
|
|
35
37
|
<div class="drawer-body" id="drawerBody"></div>
|
|
36
38
|
<footer class="drawer-foot" id="drawerFoot"></footer>
|
|
37
39
|
</aside>
|
|
38
40
|
</div>
|
|
39
|
-
<div id="
|
|
41
|
+
<div class="mask center" id="modalMask" hidden>
|
|
42
|
+
<div class="modal" role="dialog" aria-modal="true" id="modalBox">
|
|
43
|
+
<div class="modal-head"><h3 id="modalTitle"></h3><button class="btn ghost" id="modalClose" aria-label="关闭">✕</button></div>
|
|
44
|
+
<div class="modal-body" id="modalBody"></div>
|
|
45
|
+
<div class="modal-foot" id="modalFoot"></div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="lockmask" id="lockMask" hidden>
|
|
49
|
+
<div class="lockcard" id="lockCard">
|
|
50
|
+
<span class="lock-logo"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></span>
|
|
51
|
+
<h3>配置页已锁定</h3>
|
|
52
|
+
<div class="lock-sub">请输入密码解锁配置页</div>
|
|
53
|
+
<input type="password" id="lockPwd" placeholder="••••••" autocomplete="off">
|
|
54
|
+
<div class="lock-err" id="lockErr"></div>
|
|
55
|
+
<button class="btn primary" id="lockUnlock" style="width:100%;justify-content:center">解锁</button>
|
|
56
|
+
<div class="lock-tip">每次进入配置页都需要输入密码 · 长时间无操作会自动锁屏<br>密码在页面右上角「⚙ 设置」中修改;忘记密码可在本机终端执行 <code>lcb reset-password</code> 恢复初始密码 123456</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
<div id="toast" role="status"></div>
|
|
40
60
|
<script>
|
|
41
61
|
// 模块加载失败兜底:旧进程(npm 升级后未重启)没有 /js 路由 → main.js 404 → 页面全空,
|
|
42
62
|
// 此内联提示替代 staleBar 的职责(staleBar 依赖 JS 运行,模块没加载出来时它也无法显示)
|
|
43
63
|
window.__lcbBooted = false;
|
|
44
64
|
setTimeout(() => { if (!window.__lcbBooted)
|
|
45
|
-
document.getElementById('
|
|
65
|
+
document.getElementById('view').innerHTML =
|
|
46
66
|
'<div class="card"><h3>页面资源加载失败</h3><div class="desc">配置页服务进程为旧版本,请重启桥接器(lcb start)后刷新本页。</div></div>'; }, 3000);
|
|
47
67
|
</script>
|
|
48
68
|
<script type="module" src="/js/main.js"></script>
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// ============ 配置页密码锁屏(原型设置/锁屏模块的服务端真实现) ============
|
|
2
|
+
// 行为约定:
|
|
3
|
+
// - 进页即锁:每次进入配置页(刷新/新开标签)都要输入密码,服务端会话放行 API(会话已持久化,
|
|
4
|
+
// 桥接器重启后会话延续,重启轮询/刷新不再被 401 拦截;手动锁屏/登出仍即时吊销)
|
|
5
|
+
// - 默认密码 123456;解锁响应带 isDefault,仍为默认密码时每次登录弹「修改默认密码」引导
|
|
6
|
+
// - 无操作自动锁屏:lockMinutes 分钟(服务端 webauth.json,默认 30 分钟;0=永不),双端同步过期
|
|
7
|
+
// - 恢复初始密码两条路:本弹窗(验当前密码)/ 本机终端 `lcb reset-password`(忘记密码兜底)
|
|
8
|
+
// - 密文落盘在服务端(webauth.json 只存 salt+md5),前端全程不接触哈希
|
|
9
|
+
import { $, api, toast } from './core.js';
|
|
10
|
+
import { openModal, closeModal, confirmDialog } from './ui.js';
|
|
11
|
+
|
|
12
|
+
export const AUTH = {
|
|
13
|
+
unlocked: false,
|
|
14
|
+
everUnlocked: false, // 本次页面生命周期内是否解锁过:锁屏时才需要吊销服务端会话
|
|
15
|
+
isDefault: true,
|
|
16
|
+
lockMinutes: 30,
|
|
17
|
+
lastActive: Date.now(),
|
|
18
|
+
enteredPwd: '', // 本次解锁所输密码(仅内存):引导改密时作为「当前密码」提交,免重复输入
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const minsText = (m) => (m >= 60 ? `${m / 60} 小时` : `${m} 分钟`);
|
|
22
|
+
const svg = (inner, s = 15) => `<svg viewBox="0 0 24 24" width="${s}" height="${s}" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${inner}</svg>`;
|
|
23
|
+
const IC_SETTINGS = '<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>';
|
|
24
|
+
const IC_LOCK = '<rect width="18" height="11" x="3" y="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>';
|
|
25
|
+
const IC_KEY = '<circle cx="7.5" cy="15.5" r="5.5"/><path d="m21 2-9.6 9.6"/><path d="m15.5 7.5 3 3L22 7l-3-3"/>';
|
|
26
|
+
const IC_ROTATE = '<path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5"/>';
|
|
27
|
+
|
|
28
|
+
let onUnlockCb = null;
|
|
29
|
+
|
|
30
|
+
/** 锁屏:盖遮罩 + 吊销服务端会话(内网他人不能借已解锁 cookie 直调 API) */
|
|
31
|
+
export function lockPage() {
|
|
32
|
+
const hadSession = AUTH.everUnlocked;
|
|
33
|
+
AUTH.unlocked = false;
|
|
34
|
+
AUTH.enteredPwd = '';
|
|
35
|
+
$('#lockPwd').value = '';
|
|
36
|
+
$('#lockErr').textContent = '';
|
|
37
|
+
$('#lockMask').hidden = false;
|
|
38
|
+
$('#lockBtn').hidden = true;
|
|
39
|
+
$('#settingsBtn').hidden = true;
|
|
40
|
+
if (hadSession) api('POST', '/api/auth/logout').catch(() => { /* 离线:无所谓 */ });
|
|
41
|
+
setTimeout(() => $('#lockPwd').focus(), 60);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 解锁成功后的统一入口:via='password'(输密码登录,仍默认密码时弹引导)| 'session'(会话恢复免密) */
|
|
45
|
+
async function finishUnlock(meta, via) {
|
|
46
|
+
AUTH.unlocked = true;
|
|
47
|
+
AUTH.everUnlocked = true;
|
|
48
|
+
AUTH.isDefault = !!meta.isDefault;
|
|
49
|
+
AUTH.lockMinutes = Number(meta.lockMinutes) || 0;
|
|
50
|
+
AUTH.lastActive = Date.now();
|
|
51
|
+
$('#lockMask').hidden = true;
|
|
52
|
+
$('#lockBtn').hidden = false;
|
|
53
|
+
$('#settingsBtn').hidden = false;
|
|
54
|
+
await onUnlockCb?.();
|
|
55
|
+
// 改密引导只在「输密码登录」时弹:会话恢复的刷新不重复打扰(主人反馈:免密本就是为了少打扰)
|
|
56
|
+
if (AUTH.isDefault && via === 'password') openPwdModal(true);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function tryUnlock() {
|
|
60
|
+
const pwd = $('#lockPwd').value;
|
|
61
|
+
if (!pwd) { $('#lockErr').textContent = '请输入密码'; return; }
|
|
62
|
+
const btn = $('#lockUnlock');
|
|
63
|
+
btn.disabled = true;
|
|
64
|
+
try {
|
|
65
|
+
const r = await api('POST', '/api/auth/verify', { password: pwd });
|
|
66
|
+
AUTH.enteredPwd = pwd;
|
|
67
|
+
$('#lockPwd').value = '';
|
|
68
|
+
await finishUnlock(r, 'password');
|
|
69
|
+
} catch (e) {
|
|
70
|
+
// 旧进程(页面新、API 旧):/api/auth/* 不存在 → 404「未知端点」,锁死前先讲清楚
|
|
71
|
+
$('#lockErr').textContent = /未知端点/.test(e.message)
|
|
72
|
+
? '页面已更新但配置页进程是旧版本:请重启桥接器(lcb start)后刷新本页'
|
|
73
|
+
: e.message;
|
|
74
|
+
const card = $('#lockCard');
|
|
75
|
+
card.classList.remove('shake');
|
|
76
|
+
void card.offsetWidth; // 重启动画
|
|
77
|
+
card.classList.add('shake');
|
|
78
|
+
} finally {
|
|
79
|
+
btn.disabled = false;
|
|
80
|
+
$('#lockPwd').focus();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 修改密码弹窗(guided=true:仍为默认密码时的登录引导;当前密码用本次解锁所输的 123456) */
|
|
85
|
+
export function openPwdModal(guided) {
|
|
86
|
+
openModal({
|
|
87
|
+
title: guided ? '安全提示:请修改默认密码' : '修改配置页密码',
|
|
88
|
+
width: 'min(440px,92vw)',
|
|
89
|
+
closeOnMask: !guided,
|
|
90
|
+
bodyHtml: `
|
|
91
|
+
${guided ? '<div class="banner warn" style="margin-bottom:12px"><span>当前仍在使用默认密码 <b>123456</b>。同一内网的其他人可能通过「你的内网 IP + 端口」直接打开配置页查看信息,请立即修改密码。</span></div>' : ''}
|
|
92
|
+
${guided ? '' : '<label class="fld">当前密码</label><input type="password" id="pwOld" autocomplete="off">'}
|
|
93
|
+
<label class="fld">新密码(≥6 位,不能是 123456)</label><input type="password" id="pwNew" autocomplete="off">
|
|
94
|
+
<label class="fld">确认新密码</label><input type="password" id="pwNew2" autocomplete="off">
|
|
95
|
+
<div class="lock-err" id="pwErr" style="margin-top:6px"></div>
|
|
96
|
+
<div class="mini-note">密码用于保护本机配置页(含会话页)。服务端只保存加盐哈希(webauth.json),不存明文;修改后其他已打开的配置页需重新输入密码。</div>`,
|
|
97
|
+
footHtml: `${guided ? '<button class="btn" id="pwSkip">暂不修改</button>' : ''}<span class="sp"></span><button class="btn primary" id="pwOk">确认修改</button>`,
|
|
98
|
+
});
|
|
99
|
+
const sk = $('#pwSkip');
|
|
100
|
+
if (sk) sk.onclick = () => { closeModal(); toast('已跳过。下次仍用默认密码进入时会再次提醒', true); };
|
|
101
|
+
$('#pwOk').onclick = async () => {
|
|
102
|
+
const oldv = guided ? AUTH.enteredPwd : ($('#pwOld')?.value ?? '');
|
|
103
|
+
const n1 = $('#pwNew').value;
|
|
104
|
+
const n2 = $('#pwNew2').value;
|
|
105
|
+
const err = $('#pwErr');
|
|
106
|
+
const bad = (m) => { err.textContent = m; };
|
|
107
|
+
if (!guided && !oldv) return bad('请输入当前密码');
|
|
108
|
+
if (n1.length < 6) return bad('新密码至少 6 位');
|
|
109
|
+
if (n1 === '123456') return bad('新密码不能是默认密码 123456');
|
|
110
|
+
if (n1 !== n2) return bad('两次输入的新密码不一致');
|
|
111
|
+
try {
|
|
112
|
+
await api('POST', '/api/auth/change', { old: oldv, next: n1 });
|
|
113
|
+
closeModal();
|
|
114
|
+
AUTH.isDefault = false;
|
|
115
|
+
toast('✅ 密码已修改,下次登录请使用新密码');
|
|
116
|
+
} catch (e) { bad(e.message); }
|
|
117
|
+
};
|
|
118
|
+
(guided ? $('#pwNew') : $('#pwOld'))?.focus();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 恢复初始密码 123456:验当前密码后重置(防止锁屏状态下被内网他人一键重置) */
|
|
122
|
+
function openResetModal() {
|
|
123
|
+
openModal({
|
|
124
|
+
title: '恢复初始密码',
|
|
125
|
+
width: 'min(420px,92vw)',
|
|
126
|
+
bodyHtml: `
|
|
127
|
+
<div class="banner warn" style="margin-bottom:12px"><span>恢复后配置页密码回到初始密码 <b>123456</b>,下次登录会再次引导修改;其他已打开的配置页将被登出。</span></div>
|
|
128
|
+
<label class="fld">当前密码</label><input type="password" id="pwRsOld" autocomplete="off">
|
|
129
|
+
<div class="lock-err" id="pwRsErr" style="margin-top:6px"></div>
|
|
130
|
+
<div class="mini-note">忘记当前密码?在本机终端执行 <code>lcb reset-password</code> 可免密恢复。</div>`,
|
|
131
|
+
footHtml: `<span class="sp"></span><button class="btn" id="pwRsCancel">取消</button><button class="btn primary" id="pwRsOk">恢复初始密码</button>`,
|
|
132
|
+
});
|
|
133
|
+
$('#pwRsCancel').onclick = closeModal;
|
|
134
|
+
$('#pwRsOk').onclick = async () => {
|
|
135
|
+
try {
|
|
136
|
+
await api('POST', '/api/auth/reset-default', { old: $('#pwRsOld').value });
|
|
137
|
+
closeModal();
|
|
138
|
+
AUTH.isDefault = true;
|
|
139
|
+
toast('✅ 已恢复初始密码 123456,下次登录将引导修改');
|
|
140
|
+
} catch (e) { $('#pwRsErr').textContent = e.message; }
|
|
141
|
+
};
|
|
142
|
+
$('#pwRsOld').focus();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 设置弹窗(顶栏右上角 ⚙):配置页密码 + 自动锁屏时长 */
|
|
146
|
+
export async function openSettingsModal() {
|
|
147
|
+
let meta;
|
|
148
|
+
try {
|
|
149
|
+
meta = await api('GET', '/api/auth/meta');
|
|
150
|
+
} catch (e) { toast(e.message, true); return; }
|
|
151
|
+
AUTH.isDefault = !!meta.isDefault;
|
|
152
|
+
AUTH.lockMinutes = Number(meta.lockMinutes);
|
|
153
|
+
const opts = [[30, '30 分钟'], [60, '1 小时'], [240, '4 小时'], [1440, '24 小时'], [1800, '30 小时'], [0, '永不']];
|
|
154
|
+
openModal({
|
|
155
|
+
title: '设置',
|
|
156
|
+
width: 'min(460px,92vw)',
|
|
157
|
+
bodyHtml: `
|
|
158
|
+
<div class="sub-title">配置页密码</div>
|
|
159
|
+
<div class="mut" style="font-size:12px;margin-bottom:10px">防止同一内网其他用户通过「IP + 端口」查看你的配置信息;每次进入配置页都需要输入密码${AUTH.isDefault ? '。<b style="color:var(--warn-tx)">当前仍为默认密码 123456,建议立即修改。</b>' : '。'}</div>
|
|
160
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
161
|
+
<button class="btn" id="stPwd">${svg(IC_KEY, 13)} 修改密码</button>
|
|
162
|
+
<button class="btn" id="stReset">${svg(IC_ROTATE, 13)} 恢复初始密码</button>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="mini-note" style="margin-top:8px">忘记密码时,也可在运行 LCB 的机器上执行 <code>lcb reset-password</code> 免密恢复初始密码 123456。</div>
|
|
165
|
+
<hr>
|
|
166
|
+
<div class="sub-title" style="margin-top:4px">自动锁屏</div>
|
|
167
|
+
<label class="fld">长时间无操作后锁屏(再次使用需输入密码)</label>
|
|
168
|
+
<select id="stMins">${opts.map(([v, l]) => `<option value="${v}" ${AUTH.lockMinutes === v ? 'selected' : ''}>${l}</option>`).join('')}</select>
|
|
169
|
+
<div class="mini-note" style="margin-top:8px">当前设定:${AUTH.lockMinutes ? `无操作 ${minsText(AUTH.lockMinutes)} 后自动锁屏` : '不自动锁屏'}。也可点顶栏 🔒 立即锁屏体验。</div>`,
|
|
170
|
+
footHtml: `<span class="sp"></span><button class="btn" id="stCancel">关闭</button><button class="btn primary" id="stSave">保存</button>`,
|
|
171
|
+
});
|
|
172
|
+
$('#stCancel').onclick = closeModal;
|
|
173
|
+
$('#stPwd').onclick = () => openPwdModal(false);
|
|
174
|
+
$('#stReset').onclick = async () => {
|
|
175
|
+
if (await confirmDialog({ title: '恢复初始密码', message: '确认把配置页密码恢复为初始密码 123456?\n恢复后下次登录会引导你重新设置。', danger: true, confirmText: '继续' })) openResetModal();
|
|
176
|
+
};
|
|
177
|
+
$('#stSave').onclick = async () => {
|
|
178
|
+
const v = Number($('#stMins').value);
|
|
179
|
+
try {
|
|
180
|
+
await api('PUT', '/api/auth/settings', { lockMinutes: v });
|
|
181
|
+
AUTH.lockMinutes = v;
|
|
182
|
+
AUTH.lastActive = Date.now();
|
|
183
|
+
closeModal();
|
|
184
|
+
toast(v ? `✅ 已设置:无操作 ${minsText(v)} 后自动锁屏` : '✅ 已关闭自动锁屏');
|
|
185
|
+
} catch (e) { toast(e.message, true); }
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** main.js 启动时调用一次:绑锁屏交互 + 无操作看门狗 + 会话恢复(免密) */
|
|
190
|
+
export function initAuth({ onUnlock }) {
|
|
191
|
+
onUnlockCb = onUnlock || null;
|
|
192
|
+
$('#lockUnlock').onclick = () => void tryUnlock();
|
|
193
|
+
$('#lockPwd').onkeydown = (e) => { if (e.key === 'Enter') void tryUnlock(); };
|
|
194
|
+
$('#lockPwd').oninput = () => { $('#lockErr').textContent = ''; };
|
|
195
|
+
$('#settingsBtn').innerHTML = svg(IC_SETTINGS);
|
|
196
|
+
$('#lockBtn').innerHTML = svg(IC_LOCK, 14);
|
|
197
|
+
$('#settingsBtn').onclick = () => void openSettingsModal();
|
|
198
|
+
$('#lockBtn').onclick = () => { lockPage(); toast('已锁定,输入密码可重新进入'); };
|
|
199
|
+
// 无操作看门狗:任意交互刷新 lastActive;每 30s 检查一次(与服务端会话 TTL 同步过期)
|
|
200
|
+
['mousemove', 'keydown', 'click', 'wheel', 'touchstart'].forEach((ev) =>
|
|
201
|
+
document.addEventListener(ev, () => { AUTH.lastActive = Date.now(); }, { passive: true }));
|
|
202
|
+
setInterval(() => {
|
|
203
|
+
if (!AUTH.unlocked || !AUTH.lockMinutes) return;
|
|
204
|
+
if (Date.now() - AUTH.lastActive > AUTH.lockMinutes * 60000) {
|
|
205
|
+
lockPage();
|
|
206
|
+
toast('已因长时间无操作自动锁屏,请重新输入密码', true);
|
|
207
|
+
}
|
|
208
|
+
}, 30000);
|
|
209
|
+
// 先盖锁屏(防内容闪烁),再静默恢复会话:服务端会话有效(未超时/未锁屏吊销)→ 直接进入免密;
|
|
210
|
+
// 无效(首次 / 自动锁屏后 / 手动锁屏后)→ 留在锁屏等输入。锁屏遮罩先出后撤,闪屏可忽略
|
|
211
|
+
lockPage();
|
|
212
|
+
void (async () => {
|
|
213
|
+
try {
|
|
214
|
+
const meta = await api('GET', '/api/auth/meta');
|
|
215
|
+
await finishUnlock(meta, 'session');
|
|
216
|
+
} catch { /* 无会话:留在锁屏(正常路径) */ }
|
|
217
|
+
})();
|
|
218
|
+
}
|
|
@@ -1,44 +1,46 @@
|
|
|
1
|
-
// ============ 首装向导(bootstrap;非 tab 页,由 main.js 在 firstRun
|
|
1
|
+
// ============ 首装向导(bootstrap;非 tab 页,由 main.js 在 firstRun 时渲染进 #view) ============
|
|
2
2
|
import { $, toast, api, refresh } from './core.js';
|
|
3
3
|
import { pickDirectory } from './ui.js';
|
|
4
4
|
|
|
5
5
|
export function renderBootstrap(el) {
|
|
6
6
|
el.innerHTML = `
|
|
7
|
-
<div class="
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<div
|
|
16
|
-
|
|
17
|
-
<div class="
|
|
18
|
-
<
|
|
19
|
-
|
|
7
|
+
<div class="panel">
|
|
8
|
+
<div class="panel-body">
|
|
9
|
+
<h3>👋 欢迎使用 lark-claudecode-bridge</h3>
|
|
10
|
+
<div class="desc">首次安装:完成下面三步即可开始使用。需要先在 <a href="https://open.feishu.cn/app" target="_blank">飞书开放平台</a> 创建应用并启用机器人能力(详见 README)。建议为每个应用填写名字——概览页与飞书 /status 将显示该名字,缺省显示 App ID。</div>
|
|
11
|
+
<div class="sub-title" style="margin-top:16px">① 飞书应用凭证</div>
|
|
12
|
+
<div id="bsApps"></div>
|
|
13
|
+
<button class="btn sm" id="bsAddApp">+ 再添一个应用(多机器人)</button>
|
|
14
|
+
<div class="sub-title" style="margin-top:20px">② 本机工作区(Claude 的工作目录)</div>
|
|
15
|
+
<div class="row">
|
|
16
|
+
<div><label class="fld">名字(如 demo)</label><input type="text" id="bsWsName" placeholder="demo"></div>
|
|
17
|
+
<div><label class="fld">本机绝对路径</label>
|
|
18
|
+
<div class="input-btn">
|
|
19
|
+
<input type="text" id="bsWsPath" placeholder="F:\\workspace\\demo">
|
|
20
|
+
<button class="btn sm" id="bsWsBrowse" title="浏览选择目录">浏览</button>
|
|
21
|
+
</div>
|
|
20
22
|
</div>
|
|
21
23
|
</div>
|
|
22
|
-
|
|
23
|
-
<h3 style="margin-top:20px">③ Claude 认证(可稍后在「Claude 认证」页配置)</h3>
|
|
24
|
-
<div class="radio-row">
|
|
25
|
-
<label><input type="radio" name="bsMode" value="skip" checked> 使用本机已有 ~/.claude 登录</label>
|
|
26
|
-
<label><input type="radio" name="bsMode" value="managed"> 填 API Key / Token(无需本机登录)</label>
|
|
27
|
-
</div>
|
|
28
|
-
<div id="bsManaged" style="display:none">
|
|
24
|
+
<div class="sub-title" style="margin-top:20px">③ Claude 认证 <span class="dim" style="font-weight:400">(可稍后在「Claude 认证」页配置)</span></div>
|
|
29
25
|
<div class="radio-row">
|
|
30
|
-
<label><input type="radio" name="
|
|
31
|
-
<label><input type="radio" name="
|
|
26
|
+
<label><input type="radio" name="bsMode" value="skip" checked> 使用本机已有 ~/.claude 登录</label>
|
|
27
|
+
<label><input type="radio" name="bsMode" value="managed"> 填 API Key / Token(无需本机登录)</label>
|
|
32
28
|
</div>
|
|
33
|
-
<div
|
|
34
|
-
<div
|
|
35
|
-
|
|
29
|
+
<div id="bsManaged" style="display:none">
|
|
30
|
+
<div class="radio-row">
|
|
31
|
+
<label><input type="radio" name="bsCred" value="auth_token" checked> AUTH_TOKEN(中转站常用)</label>
|
|
32
|
+
<label><input type="radio" name="bsCred" value="api_key"> API_KEY(官方)</label>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="row">
|
|
35
|
+
<div><label class="fld" id="bsCredLabel">Auth Token</label><input type="password" id="bsCred" placeholder="sk-…"></div>
|
|
36
|
+
<div><label class="fld">BASE_URL(中转站端点,官方留空)</label><input type="text" id="bsBaseUrl" placeholder="https://relay.example.com"></div>
|
|
37
|
+
</div>
|
|
38
|
+
<label class="fld">模型(如 claude-sonnet-5,可留空)</label><input type="text" id="bsModel" placeholder="claude-sonnet-5">
|
|
39
|
+
</div>
|
|
40
|
+
<div style="margin-top:20px;display:flex;align-items:center;gap:12px">
|
|
41
|
+
<button class="btn primary" id="bsSubmit" style="padding:9px 28px">完成配置</button>
|
|
42
|
+
<span class="hint">提交后运行 <code>lcb start</code> 启动桥接器</span>
|
|
36
43
|
</div>
|
|
37
|
-
<label>模型(如 claude-sonnet-5,可留空)</label><input type="text" id="bsModel" placeholder="claude-sonnet-5">
|
|
38
|
-
</div>
|
|
39
|
-
<div style="margin-top:20px">
|
|
40
|
-
<button class="btn primary" id="bsSubmit" style="padding:9px 28px">完成配置</button>
|
|
41
|
-
<span class="hint">提交后运行 <code>lcb start</code> 启动桥接器</span>
|
|
42
44
|
</div>
|
|
43
45
|
</div>`;
|
|
44
46
|
const appsBox = $('#bsApps');
|
|
@@ -47,9 +49,9 @@ export function renderBootstrap(el) {
|
|
|
47
49
|
div.className = 'row';
|
|
48
50
|
div.style.marginBottom = '8px';
|
|
49
51
|
div.innerHTML = `
|
|
50
|
-
<div><label>App ID(cli_ 开头)</label><input type="text" class="bs-appid" placeholder="cli_aabbccddeeff0011"></div>
|
|
51
|
-
<div><label>应用名字(建议填写)</label><input type="text" class="bs-name" placeholder="如:素材收集(概览/状态页显示用)"></div>
|
|
52
|
-
<div><label>App Secret</label><input type="password" class="bs-secret" placeholder=""></div>
|
|
52
|
+
<div><label class="fld">App ID(cli_ 开头)</label><input type="text" class="bs-appid" placeholder="cli_aabbccddeeff0011"></div>
|
|
53
|
+
<div><label class="fld">应用名字(建议填写)</label><input type="text" class="bs-name" placeholder="如:素材收集(概览/状态页显示用)"></div>
|
|
54
|
+
<div><label class="fld">App Secret</label><input type="password" class="bs-secret" placeholder=""></div>
|
|
53
55
|
<div class="btn-wrap"><button class="btn sm danger bs-del">删除</button></div>`;
|
|
54
56
|
div.querySelector('.bs-del').onclick = () => div.remove();
|
|
55
57
|
appsBox.appendChild(div);
|
package/assets/web/js/core.js
CHANGED
|
@@ -58,16 +58,17 @@ export async function refresh() {
|
|
|
58
58
|
try {
|
|
59
59
|
S.doc = await api('GET', '/api/config');
|
|
60
60
|
S.firstRun = false;
|
|
61
|
-
$('#hFirstRun').textContent = '';
|
|
62
61
|
} catch (e) {
|
|
63
62
|
if (String(e.message).includes('bootstrap')) {
|
|
64
63
|
S.firstRun = true;
|
|
65
64
|
S.doc = null;
|
|
66
|
-
$('#hFirstRun').textContent = '首次安装向导';
|
|
67
65
|
} else { throw e; }
|
|
68
66
|
}
|
|
69
67
|
const st = S.status || {};
|
|
70
|
-
$('#
|
|
68
|
+
const hv = $('#hVer');
|
|
69
|
+
const hh = $('#hHost');
|
|
70
|
+
if (hv) { hv.textContent = `v${st.version || '?'}`; hv.hidden = false; }
|
|
71
|
+
if (hh) { hh.textContent = S.firstRun ? `${location.host} · 首次安装向导` : location.host; hh.hidden = false; }
|
|
71
72
|
// 版本错位检测:静态页每次现读磁盘(新),API 属于运行中进程(旧)——status 无 bridge
|
|
72
73
|
// 字段说明进程早于「页面托管启停」功能,提示重启而非让用户撞「未知端点」
|
|
73
74
|
$('#staleBar').hidden = !(S.status && S.status.bridge === undefined);
|
package/assets/web/js/main.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
// ============
|
|
2
|
-
// 新增配置页:js/pages/ 下新建模块(export const page = { id, title, icon, render(el) })
|
|
1
|
+
// ============ 入口:分组导航注册表 + hash 路由 + 锁屏门 + 启动 ============
|
|
2
|
+
// 新增配置页:js/pages/ 下新建模块(export const page = { id, title, desc, icon, render(el) })
|
|
3
3
|
// 并在下方 import + PAGES 数组各加一行即可;后端 /js/* 静态服务自动覆盖新文件
|
|
4
4
|
import { S, $, esc, hooks, refresh } from './core.js';
|
|
5
5
|
import { initDrawer, isDrawerOpen, cancelDrawer, confirmDialog } from './ui.js';
|
|
6
6
|
import { initTheme } from './theme.js';
|
|
7
|
+
import { initAuth, AUTH } from './auth.js';
|
|
7
8
|
import { renderBootstrap } from './bootstrap.js';
|
|
8
9
|
import { page as pOverview } from './pages/overview.js';
|
|
9
10
|
import { page as pApps } from './pages/apps.js';
|
|
10
11
|
import { page as pWorkspaces } from './pages/workspaces.js';
|
|
12
|
+
import { page as pChats } from './pages/chats.js';
|
|
11
13
|
import { page as pClaude } from './pages/claude.js';
|
|
12
14
|
import { page as pPermissions } from './pages/permissions.js';
|
|
13
15
|
import { page as pSlash } from './pages/slash.js';
|
|
@@ -16,12 +18,22 @@ import { page as pSkills } from './pages/skills.js';
|
|
|
16
18
|
import { page as pMcp } from './pages/mcp.js';
|
|
17
19
|
import { page as pMemory } from './pages/memory.js';
|
|
18
20
|
|
|
19
|
-
const PAGES = [pOverview, pApps, pWorkspaces,
|
|
21
|
+
const PAGES = [pOverview, pChats, pApps, pWorkspaces, pSlash, pClaude, pPlugins, pSkills, pMcp, pPermissions, pMemory];
|
|
20
22
|
const pageById = new Map(PAGES.map((p) => [p.id, p]));
|
|
21
23
|
const DEFAULT_TAB = 'overview';
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
/** 侧边栏分组(原型同款):同一功能聚在一组,跨组动线从上到下 = 部署 → 接入 → 扩展 → 安全 */
|
|
26
|
+
const NAV = [
|
|
27
|
+
{ group: '总览', items: ['overview', 'chats'] },
|
|
28
|
+
{ group: '机器人接入', items: ['apps', 'workspaces', 'slash'] },
|
|
29
|
+
{ group: '模型与扩展', items: ['claude', 'plugins', 'skills', 'mcp'] },
|
|
30
|
+
{ group: '安全与记忆', items: ['permissions', 'memory'] },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// 菜单图标包装:页面自带 icon(Lucide 风格线性 SVG 内串),此处只包一层跟随选中态配色
|
|
24
34
|
const ico = (inner) => `<span class="ico"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${inner}</svg></span>`;
|
|
35
|
+
const icoSearch = '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m21 21-3.5-3.5"/></svg>';
|
|
36
|
+
const LEGEND = '<span class="tag warn">需重启</span> 标记的配置保存后需重启桥接器生效;其余改动下一条消息自动生效。';
|
|
25
37
|
|
|
26
38
|
// ---- hash 路由:#claude 等;刷新 / 分享链接 / 前进后退均可定位 ----
|
|
27
39
|
const tabFromHash = () => { const id = location.hash.slice(1); return pageById.has(id) ? id : DEFAULT_TAB; };
|
|
@@ -40,27 +52,66 @@ async function confirmLeave() {
|
|
|
40
52
|
return true;
|
|
41
53
|
}
|
|
42
54
|
|
|
43
|
-
|
|
44
|
-
const
|
|
55
|
+
function renderNav() {
|
|
56
|
+
const q = ($('#navQ')?.value || '').trim();
|
|
57
|
+
$('#nav').innerHTML = `
|
|
58
|
+
<div class="nav-search">${icoSearch}<input id="navQ" placeholder="搜索页面…" value="${esc(q)}"></div>` +
|
|
59
|
+
NAV.map((g) => {
|
|
60
|
+
const items = g.items.filter((id) => pageById.has(id));
|
|
61
|
+
if (!items.length) return '';
|
|
62
|
+
return `<div class="nav-group"><div class="nav-group-label">${g.group}</div>` +
|
|
63
|
+
items.map((id) => {
|
|
64
|
+
const p = pageById.get(id);
|
|
65
|
+
const badge = p.badge ? p.badge() : '';
|
|
66
|
+
return `<button class="nav-item ${id === activeTab ? 'active' : ''}" data-k="${id}" data-label="${esc(p.title)}">${ico(p.icon)}<span>${esc(p.title)}</span>${badge}</button>`;
|
|
67
|
+
}).join('') + '</div>';
|
|
68
|
+
}).join('');
|
|
69
|
+
$('#nav').querySelectorAll('.nav-item').forEach((b) =>
|
|
70
|
+
b.onclick = () => void tryActivate(b.dataset.k));
|
|
71
|
+
// 搜索过滤:只切换菜单项显隐、不重建 DOM;组合输入(中文 IME)期间暂停,选词结束再过滤
|
|
72
|
+
const inp = $('#navQ');
|
|
73
|
+
const applyFilter = () => {
|
|
74
|
+
const kw = (inp.value || '').trim().toLowerCase();
|
|
75
|
+
$('#nav').querySelectorAll('.nav-item').forEach((b) =>
|
|
76
|
+
b.classList.toggle('nav-hide', !!kw && !b.dataset.label.toLowerCase().includes(kw)));
|
|
77
|
+
$('#nav').querySelectorAll('.nav-group').forEach((g) => {
|
|
78
|
+
const vis = [...g.querySelectorAll('.nav-item')].some((b) => !b.classList.contains('nav-hide'));
|
|
79
|
+
g.style.display = vis ? '' : 'none';
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
let composing = false;
|
|
83
|
+
inp.addEventListener('compositionstart', () => { composing = true; });
|
|
84
|
+
inp.addEventListener('compositionend', () => { composing = false; applyFilter(); });
|
|
85
|
+
inp.oninput = () => { if (!composing) applyFilter(); };
|
|
86
|
+
applyFilter();
|
|
87
|
+
}
|
|
45
88
|
|
|
46
89
|
function render() {
|
|
47
90
|
renderNav();
|
|
48
|
-
if (S.firstRun)
|
|
91
|
+
if (S.firstRun) {
|
|
92
|
+
// 首装向导:隐藏页头与图例,无论 hash 是什么都渲染向导
|
|
93
|
+
$('#pageHead').hidden = true;
|
|
94
|
+
$('#pageLegend').hidden = true;
|
|
95
|
+
$('#view').innerHTML = '';
|
|
96
|
+
renderBootstrap($('#view'));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
49
99
|
const page = pageById.get(activeTab);
|
|
50
|
-
$('#
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
$('#
|
|
56
|
-
|
|
57
|
-
$('#
|
|
58
|
-
|
|
100
|
+
$('#pageHead').hidden = false;
|
|
101
|
+
$('#pageHead').innerHTML = `
|
|
102
|
+
<span class="ico"><svg viewBox="0 0 24 24" width="19" height="19" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${page.icon}</svg></span>
|
|
103
|
+
<div><h2>${esc(page.title)}</h2><div class="page-desc">${esc(page.desc || '')}</div></div>
|
|
104
|
+
<div class="page-actions" id="pageActions"></div>`;
|
|
105
|
+
$('#pageLegend').hidden = page.legend === false;
|
|
106
|
+
$('#pageLegend').innerHTML = page.legend === false ? '' : LEGEND;
|
|
107
|
+
$('#view').innerHTML = '';
|
|
108
|
+
void page.render($('#view')); // fire-and-forget(async 页面同旧版)
|
|
59
109
|
}
|
|
60
110
|
|
|
61
111
|
/** nav 点击与 hashchange 的唯一汇聚点:确认 → 更新 activeTab + 同步 hash → 渲染 */
|
|
62
112
|
async function tryActivate(tab) {
|
|
63
|
-
if (
|
|
113
|
+
if (!AUTH.unlocked) return; // 锁屏期间不渲染页面(解锁后 refresh() 统一触发)
|
|
114
|
+
if (tab === activeTab) return; // nav 点击后写 hash 引发的 hashchange 在此早退(无双重渲染)
|
|
64
115
|
if (!(await confirmLeave())) {
|
|
65
116
|
// 拒绝导航:把 hash 回滚到当前页;回写引发的 hashchange 用 restoring 吞掉
|
|
66
117
|
if (location.hash.slice(1) !== activeTab) { restoring = true; location.hash = activeTab; }
|
|
@@ -79,6 +130,14 @@ window.addEventListener('hashchange', () => {
|
|
|
79
130
|
// ---- 启动 ----
|
|
80
131
|
window.__lcbBooted = true; // index.html 兜底脚本标志:module 加载成功
|
|
81
132
|
hooks.rerender = render; // 破环注入:core 的 refresh/cancelDrawer 经 hooks 触发重渲染
|
|
82
|
-
initTheme(); //
|
|
83
|
-
initDrawer(); //
|
|
84
|
-
|
|
133
|
+
initTheme(); // 亮/暗切换(单按钮,localStorage 持久化)
|
|
134
|
+
initDrawer(); // 抽屉 + 通用弹窗全局监听(ESC 分层:确认框 > 弹窗 > 抽屉)
|
|
135
|
+
// 进页即锁:锁屏通过后才拉数据并渲染(onUnlock → refresh → hooks.rerender)
|
|
136
|
+
initAuth({
|
|
137
|
+
onUnlock: async () => {
|
|
138
|
+
try { await refresh(); }
|
|
139
|
+
catch (e) {
|
|
140
|
+
$('#view').innerHTML = `<div class="panel"><div class="panel-body"><h3>加载失败</h3><div class="desc">${esc(e.message)}</div></div></div>`;
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
});
|