@jesonliu/lark-claudecode-bridge 1.4.3 → 2.0.0
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 +766 -574
- package/assets/web/index.html +37 -17
- package/assets/web/js/auth.js +217 -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 +446 -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 +39 -43
- package/assets/web/js/pages/overview.js +344 -76
- package/assets/web/js/pages/permissions.js +40 -32
- 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/bin/lcb.js +15 -0
- package/dist/bin/lcb.js.map +1 -1
- package/dist/gateway/feishu-gateway.d.ts +11 -0
- package/dist/gateway/feishu-gateway.js +5 -0
- package/dist/gateway/feishu-gateway.js.map +1 -1
- package/dist/gateway/gateway-registry.d.ts +16 -0
- package/dist/gateway/gateway-registry.js +10 -0
- package/dist/gateway/gateway-registry.js.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/web/chats-api.d.ts +77 -0
- package/dist/web/chats-api.js +475 -0
- package/dist/web/chats-api.js.map +1 -0
- 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 +125 -6
- package/dist/web/server.js.map +1 -1
- package/dist/web/webauth.d.ts +44 -0
- package/dist/web/webauth.js +177 -0
- package/dist/web/webauth.js.map +1 -0
- package/package.json +1 -1
|
@@ -2,21 +2,24 @@
|
|
|
2
2
|
import { S, $, esc, toast, api } from '../core.js';
|
|
3
3
|
import { openDrawer, closeDrawer, confirmDialog } from '../ui.js';
|
|
4
4
|
|
|
5
|
+
const IC_INFO = '<circle cx="12" cy="12" r="9"/><path d="M12 8h.01"/><path d="M12 12v4"/>';
|
|
6
|
+
const svg = (inner, s = 13) => `<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>`;
|
|
7
|
+
|
|
5
8
|
function renderSlash(el) {
|
|
6
9
|
el.innerHTML = `
|
|
7
|
-
<div class="
|
|
8
|
-
<h3
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<
|
|
10
|
+
<div class="panel">
|
|
11
|
+
<header class="panel-head"><h3>已注册命令</h3>
|
|
12
|
+
<div class="panel-actions">
|
|
13
|
+
<select id="scApp" style="width:auto;padding:4px 10px;font-size:12.5px"></select>
|
|
14
|
+
<button class="btn sm" id="scReload">刷新</button>
|
|
15
|
+
<button class="btn sm" id="scBuiltins">补齐内置</button>
|
|
16
|
+
</div></header>
|
|
17
|
+
<div class="panel-body">
|
|
18
|
+
<table><thead><tr><th style="width:170px">命令</th><th>描述</th><th style="width:130px">图标</th><th style="width:150px">操作</th></tr></thead>
|
|
19
|
+
<tbody id="scBody"><tr><td colspan="4" style="padding:36px 0"><div class="br-spin-sm"></div><div class="hint" style="text-align:center">正在从飞书拉取斜杆命令…</div></td></tr></tbody></table>
|
|
20
|
+
<pre class="report" id="scReport" style="display:none;margin-top:12px"></pre>
|
|
16
21
|
</div>
|
|
17
|
-
<
|
|
18
|
-
<tbody id="scBody"><tr><td colspan="4" style="padding:36px 0"><div class="br-spin-sm"></div><div class="hint" style="text-align:center">正在从飞书拉取斜杆命令…</div></td></tr></tbody></table>
|
|
19
|
-
<pre class="report" id="scReport" style="display:none;margin-top:12px"></pre>
|
|
22
|
+
<footer class="panel-foot">${svg(IC_INFO)} 直接读写飞书开放平台(需 application:app_slash_command 权限并发布版本),改动约 5 分钟生效(PC 端 7.70+);<b>本页操作即时提交飞书,与「保存」按钮无关</b>。聊天输入 <code>/</code> 弹出指令面板。</footer>
|
|
20
23
|
</div>`;
|
|
21
24
|
const appSel = $('#scApp');
|
|
22
25
|
(S.status?.apps || []).forEach((a) => {
|
|
@@ -39,11 +42,11 @@ function renderSlash(el) {
|
|
|
39
42
|
<td><code>/${esc(x.command)}</code></td>
|
|
40
43
|
<td>${esc(x.description?.default_value || '')}</td>
|
|
41
44
|
<td class="hint">${esc(x.description?.icon?.icon_key || '—')}</td>
|
|
42
|
-
<td>
|
|
45
|
+
<td class="ops">
|
|
43
46
|
<button class="btn sm" data-op="edit">编辑</button>
|
|
44
47
|
<button class="btn sm danger" data-op="del">删除</button>
|
|
45
48
|
</td>
|
|
46
|
-
</tr>`).join('') || '<tr><td colspan="4"
|
|
49
|
+
</tr>`).join('') || '<tr class="empty"><td colspan="4">远端暂无命令;点「补齐内置」一键注册 bridge 内置指令</td></tr>';
|
|
47
50
|
$('#scBody').querySelectorAll('button[data-op]').forEach((b) => b.onclick = () => {
|
|
48
51
|
const tr = b.closest('tr');
|
|
49
52
|
if (b.dataset.op === 'del') return delRemote(tr.dataset.cid, tr.dataset.cmd);
|
|
@@ -55,7 +58,7 @@ function renderSlash(el) {
|
|
|
55
58
|
});
|
|
56
59
|
});
|
|
57
60
|
} catch (e) {
|
|
58
|
-
$('#scBody').innerHTML = `<tr><td colspan="4" class="hint">加载失败:${esc(e.message)}
|
|
61
|
+
$('#scBody').innerHTML = `<tr><td colspan="4" class="hint">加载失败:${esc(e.message)}(确认已开通斜杠命令权限后点「刷新」重试)</td></tr>`;
|
|
59
62
|
}
|
|
60
63
|
};
|
|
61
64
|
const delRemote = async (cid, cmd) => {
|
|
@@ -74,17 +77,19 @@ function renderSlash(el) {
|
|
|
74
77
|
const openSlashDrawer = ({ commandId, command, description, icon }) => {
|
|
75
78
|
const isCreate = !commandId;
|
|
76
79
|
openDrawer({
|
|
77
|
-
title: isCreate ? '
|
|
80
|
+
title: isCreate ? '新增命令' : `编辑 /${command}`,
|
|
81
|
+
narrow: true,
|
|
78
82
|
bodyHtml: `
|
|
79
|
-
<label>命令(不带
|
|
83
|
+
<label class="fld">命令(不带 /${isCreate ? '' : ';飞书侧命令名不可修改,如需改名请删除后重建'})</label>
|
|
80
84
|
<input type="text" id="dcCmd" ${isCreate ? '' : 'disabled title="飞书侧命令名不可修改(如需改名请删除后重建)"'} value="${esc(command || '')}" placeholder="produce">
|
|
81
|
-
<label>描述(指令面板展示)</label>
|
|
85
|
+
<label class="fld">描述(指令面板展示)</label>
|
|
82
86
|
<input type="text" id="dcDesc" value="${esc(description || '')}" placeholder="内容生产流程">
|
|
83
|
-
<label>图标 icon_key(可空,取值见飞书开放平台图标列表)</label>
|
|
87
|
+
<label class="fld">图标 icon_key(可空,取值见飞书开放平台图标列表)</label>
|
|
84
88
|
<input type="text" id="dcIcon" value="${esc(icon || '')}" placeholder="skill_outlined">`,
|
|
85
|
-
footHtml: `<button class="btn primary" id="dcSave">${isCreate ? '
|
|
86
|
-
onMount: () => {
|
|
87
|
-
|
|
89
|
+
footHtml: `<span class="sp"></span><button class="btn" id="dcCancel">取消</button><button class="btn primary" id="dcSave">${isCreate ? '创建' : '保存'}</button>`,
|
|
90
|
+
onMount: ({ foot }) => {
|
|
91
|
+
foot.querySelector('#dcCancel').onclick = closeDrawer;
|
|
92
|
+
foot.querySelector('#dcSave').onclick = async () => {
|
|
88
93
|
const cmd = document.getElementById('dcCmd').value.trim();
|
|
89
94
|
const desc = document.getElementById('dcDesc').value.trim();
|
|
90
95
|
const ic = document.getElementById('dcIcon').value.trim();
|
|
@@ -96,7 +101,7 @@ function renderSlash(el) {
|
|
|
96
101
|
} else {
|
|
97
102
|
await api('POST', '/api/slash-commands/action', { op: 'update', app: appSel.value, commandId, description: desc, icon: ic });
|
|
98
103
|
}
|
|
99
|
-
toast(isCreate ? `已创建 /${cmd}` : '已保存');
|
|
104
|
+
toast(`${isCreate ? `已创建 /${cmd}` : '已保存'}(即时提交飞书,约 5 分钟生效)`);
|
|
100
105
|
closeDrawer();
|
|
101
106
|
loadRemote();
|
|
102
107
|
} catch (e) { toast(e.message, true); }
|
|
@@ -106,6 +111,7 @@ function renderSlash(el) {
|
|
|
106
111
|
};
|
|
107
112
|
appSel.onchange = loadRemote;
|
|
108
113
|
$('#scReload').onclick = loadRemote;
|
|
114
|
+
$('#pageActions').innerHTML = '<button class="btn primary" id="scAdd">新增命令</button>';
|
|
109
115
|
$('#scAdd').onclick = () => openSlashDrawer({ command: '', description: '', icon: '' });
|
|
110
116
|
$('#scBuiltins').onclick = async () => {
|
|
111
117
|
if (!appSel.value) return;
|
|
@@ -129,6 +135,8 @@ function renderSlash(el) {
|
|
|
129
135
|
export const page = {
|
|
130
136
|
id: 'slash',
|
|
131
137
|
title: '斜杠命令',
|
|
138
|
+
desc: '注册到飞书开放平台的指令面板,聊天框输入 / 触发',
|
|
139
|
+
legend: false,
|
|
132
140
|
icon: '<path d="m4 17 6-6-6-6"/><path d="M12 19h8"/>',
|
|
133
141
|
render: renderSlash,
|
|
134
142
|
};
|
|
@@ -1,101 +1,141 @@
|
|
|
1
|
-
// ============
|
|
1
|
+
// ============ 工作区(卡片列表 + 两步选目录弹窗 + 全局默认;卡级保存,热生效) ============
|
|
2
|
+
// 列表行内「设为默认」改 defaults.workspace;添加/修改走弹窗:第一步选文件夹,第二步填名称。
|
|
2
3
|
import { S, $, esc, toast, cardDirty, applyDirty } from '../core.js';
|
|
3
|
-
import { saveBarHtml, dirtyDotHtml, bindSaveBar, pickDirectory, confirmDialog } from '../ui.js';
|
|
4
|
+
import { saveBarHtml, dirtyDotHtml, bindSaveBar, pickDirectory, confirmDialog, openModal, closeModal } from '../ui.js';
|
|
5
|
+
|
|
6
|
+
const svg = (inner, s = 16) => `<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>`;
|
|
7
|
+
const IC = {
|
|
8
|
+
folder: '<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>',
|
|
9
|
+
folderOpen: '<path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"/>',
|
|
10
|
+
info: '<circle cx="12" cy="12" r="9"/><path d="M12 8h.01"/><path d="M12 12v4"/>',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** 两步式工作区弹窗:选目录(pickDirectory)→ 已选路径卡 + 名称输入 → 确认写回 doc */
|
|
14
|
+
function wsFolderModal(doc, idx, el) {
|
|
15
|
+
const isNew = idx < 0;
|
|
16
|
+
const old = isNew ? { name: '', path: '' } : doc.workspaces[idx];
|
|
17
|
+
const w = { name: old.name || '', path: old.path || '' };
|
|
18
|
+
const paint = () => {
|
|
19
|
+
openModal({
|
|
20
|
+
title: isNew ? '添加工作区' : '修改工作区',
|
|
21
|
+
width: 'min(460px,92vw)',
|
|
22
|
+
bodyHtml: `
|
|
23
|
+
<div class="mut" style="font-size:12.5px;margin:-4px 0 12px">第一步选择文件夹,接着填写名称。</div>
|
|
24
|
+
${w.path
|
|
25
|
+
? `<div class="picked-path"><span class="ws-ico">${svg(IC.folderOpen)}</span>
|
|
26
|
+
<div style="min-width:0"><b>${esc(w.path.replace(/[\\/]+$/, '').split(/[\\/]/).pop())}</b><div class="hint mono" style="word-break:break-all">${esc(w.path)}</div></div></div>
|
|
27
|
+
<button class="btn primary" id="wsPick" style="margin-top:8px">${svg(IC.folder, 14)} 重新选择文件夹</button>`
|
|
28
|
+
: `<button class="btn primary" id="wsPick" style="width:100%;justify-content:center">${svg(IC.folder, 14)} 选择文件夹</button>`}
|
|
29
|
+
<label class="fld" style="margin-top:16px">工作区名称</label>
|
|
30
|
+
<input type="text" id="wsName" value="${esc(w.name)}" placeholder="如 电商项目">
|
|
31
|
+
<div class="mini-note" style="margin-top:6px">名称用于在工作区列表中识别,可随时修改。</div>
|
|
32
|
+
${w.path ? '' : '<div class="mini-note" style="margin-top:10px">如需更换目录,请点击上方按钮重新选择。</div>'}`,
|
|
33
|
+
footHtml: `<span class="sp"></span><button class="btn" id="wsMCancel">取消</button><button class="btn primary" id="wsMOk">${isNew ? '确认添加' : '保存'}</button>`,
|
|
34
|
+
});
|
|
35
|
+
$('#wsPick').onclick = async () => {
|
|
36
|
+
const picked = await pickDirectory(w.path || '');
|
|
37
|
+
if (picked == null) return; // 取消选择:留在弹窗,原路径不动
|
|
38
|
+
w.path = picked;
|
|
39
|
+
if (!w.name) w.name = picked.replace(/[\\/]+$/, '').split(/[\\/]/).pop() || '';
|
|
40
|
+
paint();
|
|
41
|
+
$('#wsName').value = w.name;
|
|
42
|
+
};
|
|
43
|
+
$('#wsMCancel').onclick = closeModal;
|
|
44
|
+
$('#wsMOk').onclick = () => {
|
|
45
|
+
if (!w.path) return toast('请先选择文件夹', true);
|
|
46
|
+
w.name = $('#wsName').value.trim();
|
|
47
|
+
if (!w.name) return toast('请填写工作区名称', true);
|
|
48
|
+
if (isNew) doc.workspaces.push({ ...w });
|
|
49
|
+
else doc.workspaces[idx] = { ...w };
|
|
50
|
+
// 改名联动:全局默认指向旧名时同步,否则保存会因 defaults.workspace 不在工作区列表报错
|
|
51
|
+
if (!isNew && doc.defaults?.workspace && doc.defaults.workspace === old.name) {
|
|
52
|
+
doc.defaults = { ...(doc.defaults || {}), workspace: w.name };
|
|
53
|
+
cardDirty('def', true);
|
|
54
|
+
}
|
|
55
|
+
closeModal();
|
|
56
|
+
cardDirty('ws', true);
|
|
57
|
+
renderWorkspaces(el);
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
paint();
|
|
61
|
+
}
|
|
4
62
|
|
|
5
63
|
function renderWorkspaces(el) {
|
|
6
64
|
const doc = S.doc;
|
|
7
65
|
doc.workspaces = doc.workspaces || [];
|
|
66
|
+
const defWs = doc.defaults?.workspace || '';
|
|
8
67
|
el.innerHTML = `
|
|
9
|
-
<div class="
|
|
10
|
-
<h3
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<table><thead><tr><th style="width:180px">名字</th><th>路径</th><th style="width:50px"></th></tr></thead>
|
|
16
|
-
<tbody id="wsBody"></tbody></table>
|
|
68
|
+
<div class="panel">
|
|
69
|
+
<header class="panel-head"><h3>工作区目录${dirtyDotHtml('ws')}</h3>
|
|
70
|
+
<span class="panel-sub">选择本机文件夹并设置一个便于识别的名称</span>
|
|
71
|
+
<div class="panel-actions"><button class="btn primary sm" id="wsAdd">+ 添加工作区</button></div>
|
|
72
|
+
</header>
|
|
73
|
+
<div class="panel-body"><div class="ws-list" id="wsList"></div></div>
|
|
17
74
|
${saveBarHtml('ws')}
|
|
18
|
-
<
|
|
75
|
+
<footer class="panel-foot">${svg(IC.info, 13)} git 仓库工作区的任务收尾会自动发送汇总 diff 卡片;运行中的桥接器下一条消息自动生效,无需重启。</footer>
|
|
19
76
|
</div>
|
|
20
|
-
<div class="
|
|
21
|
-
<h3>全局默认${dirtyDotHtml('def')}</h3>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<div
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
<
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
77
|
+
<div class="panel">
|
|
78
|
+
<header class="panel-head"><h3>全局默认${dirtyDotHtml('def')}</h3>
|
|
79
|
+
<span class="panel-sub">用于新会话的默认行为;默认工作区在上方列表行内「设为默认」</span></header>
|
|
80
|
+
<div class="panel-body">
|
|
81
|
+
<div class="grid3">
|
|
82
|
+
<div><label class="fld">全局并发(concurrency)</label><input type="number" id="globalCc" min="1" max="100"></div>
|
|
83
|
+
<div><label class="fld">上下文提醒阈值(session.context_remind_tokens;0=关闭)</label><input type="number" id="defCtxRemind" min="0" max="10000000"></div>
|
|
84
|
+
<div><label class="fld">飞书卡片宽度(card.width)</label>
|
|
85
|
+
<select id="cardWidth">
|
|
86
|
+
<option value="default">默认(飞书默认)</option>
|
|
87
|
+
<option value="fill">撑满聊天窗口</option>
|
|
88
|
+
</select>
|
|
89
|
+
</div>
|
|
31
90
|
</div>
|
|
32
91
|
</div>
|
|
33
92
|
${saveBarHtml('def')}
|
|
34
93
|
</div>`;
|
|
35
94
|
bindSaveBar(el, 'ws');
|
|
36
95
|
bindSaveBar(el, 'def');
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
</div></td>
|
|
56
|
-
<td><button class="btn sm danger">删除</button></td>`;
|
|
57
|
-
tr.querySelectorAll('[data-f]').forEach((input) => {
|
|
58
|
-
input.oninput = input.onchange = () => {
|
|
59
|
-
const f = input.dataset.f;
|
|
60
|
-
if (f === 'name') {
|
|
61
|
-
// 改名联动:全局默认指向旧名时同步,否则保存会因 defaults.workspace 不在工作区列表报错
|
|
62
|
-
if (doc.defaults?.workspace && doc.defaults.workspace === ws.name) {
|
|
63
|
-
doc.defaults = { ...(doc.defaults || {}), workspace: input.value };
|
|
64
|
-
}
|
|
65
|
-
ws.name = input.value;
|
|
66
|
-
syncDefWsOptions();
|
|
67
|
-
}
|
|
68
|
-
else ws[f] = input.value;
|
|
69
|
-
cardDirty('ws', true);
|
|
70
|
-
};
|
|
96
|
+
const drawRows = () => {
|
|
97
|
+
$('#wsList').innerHTML = doc.workspaces.map((w, i) => `
|
|
98
|
+
<div class="ws-item">
|
|
99
|
+
<span class="ws-ico">${svg(IC.folder)}</span>
|
|
100
|
+
<div class="ws-main">
|
|
101
|
+
<div class="ws-name">${esc(w.name) || '<span class="mut">(未命名)</span>'} ${defWs === w.name ? '<span class="tag ok">默认</span>' : ''}</div>
|
|
102
|
+
<div class="ws-path">${esc(w.path) || '(未选择目录)'}</div>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="ws-ops">
|
|
105
|
+
${defWs === w.name ? '' : `<button class="btn sm" data-wdef="${i}">设为默认</button>`}
|
|
106
|
+
<button class="btn sm" data-wedit="${i}">修改</button>
|
|
107
|
+
<button class="btn sm danger" data-wdel="${i}">删除</button>
|
|
108
|
+
</div>
|
|
109
|
+
</div>`).join('') || '<div class="empty">暂无工作区,点右上「+ 添加工作区」开始</div>';
|
|
110
|
+
$('#wsList').querySelectorAll('[data-wdef]').forEach((b) => b.onclick = () => {
|
|
111
|
+
doc.defaults = { ...(doc.defaults || {}), workspace: doc.workspaces[Number(b.dataset.wdef)].name };
|
|
112
|
+
cardDirty('def', true);
|
|
113
|
+
renderWorkspaces(el);
|
|
71
114
|
});
|
|
72
|
-
|
|
73
|
-
|
|
115
|
+
$('#wsList').querySelectorAll('[data-wedit]').forEach((b) => b.onclick = () => wsFolderModal(doc, Number(b.dataset.wedit), el));
|
|
116
|
+
$('#wsList').querySelectorAll('[data-wdel]').forEach((b) => b.onclick = async () => {
|
|
74
117
|
if (doc.workspaces.length <= 1) return toast('至少保留一个工作区', true);
|
|
118
|
+
const idx = Number(b.dataset.wdel);
|
|
119
|
+
const w = doc.workspaces[idx];
|
|
75
120
|
if (!(await confirmDialog({
|
|
76
121
|
title: '删除工作区',
|
|
77
|
-
message: `确定删除工作区「${esc(
|
|
122
|
+
message: `确定删除工作区「${esc(w.name || '未命名')}」?随本卡「保存」生效,保存前可点「还原」撤销。`,
|
|
78
123
|
danger: true,
|
|
79
124
|
confirmText: '删除',
|
|
80
125
|
}))) return;
|
|
81
126
|
doc.workspaces.splice(idx, 1);
|
|
127
|
+
// 删除的是默认工作区:默认顺移到剩余第一个,避免保存时 defaults.workspace 悬空报错
|
|
128
|
+
if (doc.defaults?.workspace === w.name) {
|
|
129
|
+
doc.defaults = { ...(doc.defaults || {}), workspace: doc.workspaces[0]?.name || '' };
|
|
130
|
+
cardDirty('def', true);
|
|
131
|
+
toast('已同步切换全局默认工作区,请一并保存', true);
|
|
132
|
+
}
|
|
82
133
|
cardDirty('ws', true);
|
|
83
134
|
renderWorkspaces(el);
|
|
84
|
-
};
|
|
85
|
-
// 浏览选目录:回填后手动同步 ws.path(直接改 input.value 不触发 oninput)
|
|
86
|
-
tr.querySelector('[data-browse]').onclick = async () => {
|
|
87
|
-
const picked = await pickDirectory(ws.path || '');
|
|
88
|
-
if (picked == null) return;
|
|
89
|
-
ws.path = picked;
|
|
90
|
-
tr.querySelector('input[data-f=path]').value = picked;
|
|
91
|
-
cardDirty('ws', true);
|
|
92
|
-
};
|
|
93
|
-
body.appendChild(tr);
|
|
135
|
+
});
|
|
94
136
|
};
|
|
95
|
-
|
|
96
|
-
$('#wsAdd').onclick = () =>
|
|
97
|
-
syncDefWsOptions();
|
|
98
|
-
$('#defWs').onchange = () => { doc.defaults = { ...(doc.defaults || {}), workspace: $('#defWs').value }; cardDirty('def', true); };
|
|
137
|
+
drawRows();
|
|
138
|
+
$('#wsAdd').onclick = () => wsFolderModal(doc, -1, el);
|
|
99
139
|
$('#globalCc').value = doc.concurrency ?? 3;
|
|
100
140
|
$('#globalCc').oninput = () => { doc.concurrency = Number($('#globalCc').value) || 3; cardDirty('def', true); };
|
|
101
141
|
// 上下文提醒阈值:空值 = 移除 session 段(PUT 跳过 = 磁盘原值未变,等价回滚);合法数字 = 写 session 段
|
|
@@ -117,6 +157,7 @@ function renderWorkspaces(el) {
|
|
|
117
157
|
export const page = {
|
|
118
158
|
id: 'workspaces',
|
|
119
159
|
title: '工作区',
|
|
160
|
+
desc: 'Claude 的工作目录白名单与全局默认',
|
|
120
161
|
icon: '<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>',
|
|
121
162
|
render: renderWorkspaces,
|
|
122
163
|
};
|
package/assets/web/js/theme.js
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
|
-
// ============
|
|
2
|
-
|
|
1
|
+
// ============ 主题切换:亮 / 暗单按钮(原型同款),localStorage 持久化 ============
|
|
2
|
+
import { $ } from './core.js';
|
|
3
|
+
|
|
3
4
|
const KEY = 'lcb-theme';
|
|
4
|
-
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
5
|
-
let current = 'auto';
|
|
6
5
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
6
|
+
const IC_SUN = '<circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M4.9 4.9l1.4 1.4m11.3 11.3 1.4 1.4M2 12h2m16 0h2M4.9 19.1l1.4-1.4m11.3-11.3 1.4-1.4"/>';
|
|
7
|
+
const IC_MOON = '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>';
|
|
8
|
+
const svg = (inner) => `<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>`;
|
|
9
|
+
|
|
10
|
+
function apply(theme) {
|
|
11
|
+
document.documentElement.dataset.theme = theme;
|
|
12
|
+
const btn = $('#themeBtn');
|
|
13
|
+
if (btn) btn.innerHTML = svg(theme === 'dark' ? IC_SUN : IC_MOON); // 暗色时显示「切回亮色」的太阳
|
|
14
|
+
}
|
|
16
15
|
|
|
17
|
-
/** main.js 启动时调用一次:恢复上次选择 + 绑定切换
|
|
16
|
+
/** main.js 启动时调用一次:恢复上次选择 + 绑定切换 */
|
|
18
17
|
export function initTheme() {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
let saved = null;
|
|
19
|
+
try { saved = localStorage.getItem(KEY); } catch { /* 隐私模式等:读失败按默认 */ }
|
|
20
|
+
apply(saved === 'dark' ? 'dark' : 'light');
|
|
21
|
+
$('#themeBtn').onclick = () => {
|
|
22
|
+
const next = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
|
|
23
|
+
apply(next);
|
|
24
|
+
try { localStorage.setItem(KEY, next); } catch { /* 写失败可容忍 */ }
|
|
25
|
+
};
|
|
23
26
|
}
|
package/assets/web/js/ui.js
CHANGED
|
@@ -90,7 +90,7 @@ const joinPath = (base, name) => {
|
|
|
90
90
|
return base.replace(/[\\/]+$/, '') + sepCh + name;
|
|
91
91
|
};
|
|
92
92
|
/**
|
|
93
|
-
*
|
|
93
|
+
* 打开目录浏览弹层(文件管理器风格):盘符/根层 → 逐级进入 → 「选择此目录」确定。
|
|
94
94
|
* resolve(选中的绝对路径) / resolve(null)(取消)。手输路径可直接跳转,
|
|
95
95
|
* 路径不存在等错误在弹层内联提示(不关层、可改可回退)。
|
|
96
96
|
*/
|
|
@@ -99,64 +99,97 @@ export function pickDirectory(initialPath) {
|
|
|
99
99
|
const mask = document.createElement('div');
|
|
100
100
|
mask.className = 'modal-mask';
|
|
101
101
|
mask.innerHTML = `
|
|
102
|
-
<div class="modal" role="dialog" aria-modal="true">
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
<
|
|
106
|
-
<button class="btn pd-go" title="跳转到输入的路径">前往</button>
|
|
107
|
-
<button class="btn pd-up" title="返回上一级">上级目录</button>
|
|
102
|
+
<div class="modal dp" role="dialog" aria-modal="true" style = "padding:30px;">
|
|
103
|
+
<div class="dp-head">
|
|
104
|
+
<b>选择目录</b>
|
|
105
|
+
<span class="dp-path mono" title=""></span>
|
|
108
106
|
</div>
|
|
109
|
-
<div class="
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
<div class="dp-toolbar">
|
|
108
|
+
<input type="text" class="dp-jump" placeholder="输入或粘贴路径,回车前往">
|
|
109
|
+
<button class="btn sm dp-go" title="跳转到输入的路径">前往</button>
|
|
110
|
+
<button class="btn sm dp-up" title="返回上一级">上级</button>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="dp-grid"><div class="dp-empty">加载中…</div></div>
|
|
113
|
+
<div class="dp-foot">
|
|
114
|
+
<span class="hint dp-note"></span>
|
|
115
|
+
<span style="flex:1"></span>
|
|
116
|
+
<button class="btn dp-cancel">取消</button>
|
|
117
|
+
<button class="btn primary dp-ok">选择此目录</button>
|
|
115
118
|
</div>
|
|
116
119
|
</div>`;
|
|
117
120
|
document.body.appendChild(mask);
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
const upBtn = mask.querySelector('.
|
|
121
|
-
const okBtn = mask.querySelector('.
|
|
122
|
-
const note = mask.querySelector('.
|
|
123
|
-
const jumpInput = mask.querySelector('.
|
|
121
|
+
const grid = mask.querySelector('.dp-grid');
|
|
122
|
+
const pathEl = mask.querySelector('.dp-path');
|
|
123
|
+
const upBtn = mask.querySelector('.dp-up');
|
|
124
|
+
const okBtn = mask.querySelector('.dp-ok');
|
|
125
|
+
const note = mask.querySelector('.dp-note');
|
|
126
|
+
const jumpInput = mask.querySelector('.dp-jump');
|
|
127
|
+
const FOLDER_SVG = '<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"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>';
|
|
124
128
|
let current = ''; // '' = 盘符/根层(此层不可作为选择结果)
|
|
125
129
|
let currentParent = null;
|
|
126
130
|
const close = (val) => { mask.remove(); resolveP(val); };
|
|
127
131
|
const render = (r) => {
|
|
128
132
|
current = r.path || '';
|
|
129
133
|
currentParent = r.parent;
|
|
130
|
-
|
|
134
|
+
const curText = r.drives ? '此电脑(选择一个盘符)' : (r.path || '');
|
|
135
|
+
pathEl.textContent = curText;
|
|
136
|
+
pathEl.title = curText;
|
|
131
137
|
upBtn.hidden = !r.parent;
|
|
132
138
|
okBtn.disabled = !current;
|
|
133
139
|
note.textContent = r.error || '';
|
|
134
140
|
const items = r.drives
|
|
135
141
|
? r.drives.map((d) => ({ label: d, next: d }))
|
|
136
142
|
: (r.dirs || []).map((d) => ({ label: d, next: joinPath(current, d) }));
|
|
137
|
-
|
|
138
|
-
? items.map((it) => `<button data-next="${esc(it.next)}">${esc(it.label)}</button>`).join('')
|
|
139
|
-
: `<div class="empty">${r.error ? '(上述路径无法列出子目录)' : '(没有子目录)'}</div>`;
|
|
140
|
-
|
|
143
|
+
grid.innerHTML = items.length
|
|
144
|
+
? items.map((it) => `<button type="button" class="dp-item" data-next="${esc(it.next)}" title="${esc(it.next)}">${FOLDER_SVG}<span>${esc(it.label)}</span></button>`).join('')
|
|
145
|
+
: `<div class="dp-empty">${r.error ? '(上述路径无法列出子目录)' : '(没有子目录)'}</div>`;
|
|
146
|
+
grid.querySelectorAll('.dp-item').forEach((b) => b.onclick = () => go(b.dataset.next));
|
|
141
147
|
};
|
|
142
148
|
const go = async (p) => {
|
|
143
|
-
|
|
149
|
+
grid.innerHTML = '<div class="dp-empty">加载中…</div>';
|
|
144
150
|
try { render(await api('GET', `/api/fs/dirs?p=${encodeURIComponent(p)}`)); }
|
|
145
|
-
catch (e) {
|
|
151
|
+
catch (e) { grid.innerHTML = `<div class="dp-empty">加载失败:${esc(e.message)}</div>`; }
|
|
146
152
|
};
|
|
147
153
|
const jump = () => { const v = jumpInput.value.trim(); if (v) go(v); };
|
|
148
|
-
mask.querySelector('.
|
|
154
|
+
mask.querySelector('.dp-go').onclick = jump;
|
|
149
155
|
jumpInput.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); jump(); } };
|
|
150
156
|
upBtn.onclick = () => { if (currentParent) go(currentParent); };
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
157
|
+
// 关闭统一走 closeWithUnbind:弹层销毁时解绑捕获段按键监听
|
|
158
|
+
const unbind = bindDialogKeys((k) => { if (k === 'Escape') close(null); });
|
|
159
|
+
const closeWithUnbind = (val) => { unbind(); close(val); };
|
|
160
|
+
okBtn.onclick = () => { if (current) closeWithUnbind(current); };
|
|
161
|
+
mask.querySelector('.dp-cancel').onclick = () => closeWithUnbind(null);
|
|
162
|
+
mask.onclick = (e) => { if (e.target === mask) closeWithUnbind(null); };
|
|
154
163
|
// 初始位置:现值路径存在则从它开始,否则停在盘符/根层(跳转框预填现值方便改错)
|
|
155
164
|
jumpInput.value = initialPath || '';
|
|
156
165
|
go(initialPath || '');
|
|
166
|
+
jumpInput.focus();
|
|
157
167
|
});
|
|
158
168
|
}
|
|
159
169
|
|
|
170
|
+
// ============ 通用居中弹窗(挂壳层 #modalMask;CLI 授权 / 设置 / 改密 / 工作区选目录用) ============
|
|
171
|
+
let modalOnClose = null;
|
|
172
|
+
/** 打开通用居中弹窗。width 例:'min(460px,92vw)';closeOnMask=false 时点遮罩不关(强制引导类) */
|
|
173
|
+
export function openModal({ title, bodyHtml, footHtml, width = '', onClose = null, closeOnMask = true }) {
|
|
174
|
+
$('#modalTitle').textContent = title;
|
|
175
|
+
$('#modalBody').innerHTML = bodyHtml;
|
|
176
|
+
$('#modalFoot').innerHTML = footHtml || '';
|
|
177
|
+
$('#modalBox').style.width = width || '';
|
|
178
|
+
modalOnClose = onClose || null;
|
|
179
|
+
$('#modalMask').dataset.closeOnMask = closeOnMask ? '1' : '';
|
|
180
|
+
$('#modalMask').hidden = false;
|
|
181
|
+
return $('#modalBody');
|
|
182
|
+
}
|
|
183
|
+
export function closeModal() {
|
|
184
|
+
if ($('#modalMask').hidden) return;
|
|
185
|
+
$('#modalMask').hidden = true;
|
|
186
|
+
$('#modalBox').style.width = '';
|
|
187
|
+
const f = modalOnClose;
|
|
188
|
+
modalOnClose = null;
|
|
189
|
+
f?.();
|
|
190
|
+
}
|
|
191
|
+
export const isModalOpen = () => !$('#modalMask').hidden;
|
|
192
|
+
|
|
160
193
|
// ============ 通用右侧抽屉(应用编辑 / 斜杆命令编辑 / 档案编辑共用) ============
|
|
161
194
|
let drawerOnMount = null;
|
|
162
195
|
let drawerSnap = null; // 抽屉打开前的 doc 快照(取消回滚用;远端 CRUD 抽屉不传 snap 则不回滚)
|
|
@@ -233,8 +266,18 @@ function initDrawerResize() {
|
|
|
233
266
|
export function initDrawer() {
|
|
234
267
|
$('#drawerClose').onclick = dismissDrawer;
|
|
235
268
|
$('#drawerMask').addEventListener('mousedown', (e) => { if (e.target === e.currentTarget) dismissDrawer(); });
|
|
236
|
-
// ESC
|
|
237
|
-
|
|
269
|
+
// 通用弹窗关闭路径:X / 点遮罩(closeOnMask 才关)/ ESC;层级 = 确认框 > 弹窗 > 抽屉
|
|
270
|
+
$('#modalClose').onclick = closeModal;
|
|
271
|
+
$('#modalMask').addEventListener('mousedown', (e) => {
|
|
272
|
+
if (e.target === e.currentTarget && $('#modalMask').dataset.closeOnMask) closeModal();
|
|
273
|
+
});
|
|
274
|
+
// ESC 分层:风格化弹窗(确认/输入/进度)自己在捕获段拦截;其次通用弹窗;最后抽屉
|
|
275
|
+
document.addEventListener('keydown', (e) => {
|
|
276
|
+
if (e.key !== 'Escape') return;
|
|
277
|
+
if (openDialogs) return;
|
|
278
|
+
if (isModalOpen()) { closeModal(); return; }
|
|
279
|
+
dismissDrawer();
|
|
280
|
+
});
|
|
238
281
|
initDrawerResize();
|
|
239
282
|
}
|
|
240
283
|
|
package/dist/bin/lcb.js
CHANGED
|
@@ -11,6 +11,7 @@ import { startBridge } from '../index.js';
|
|
|
11
11
|
import { loadConfig, CONFIG_PATH, CONFIG_DIR } from '../config.js';
|
|
12
12
|
import { initManagedClaudeDir } from '../claude-config.js';
|
|
13
13
|
import { startWebServer } from '../web/server.js';
|
|
14
|
+
import { resetPasswordToDefault } from '../web/webauth.js';
|
|
14
15
|
import { AccessControl } from '../access/access-control.js';
|
|
15
16
|
import { VERSION } from '../version.js';
|
|
16
17
|
import { join } from 'node:path';
|
|
@@ -84,6 +85,19 @@ async function main() {
|
|
|
84
85
|
console.log('按 Ctrl+C 退出配置页');
|
|
85
86
|
break;
|
|
86
87
|
}
|
|
88
|
+
case 'reset-password': {
|
|
89
|
+
// 忘记配置页密码的兜底:恢复初始密码 123456(下次登录会再次引导修改)。
|
|
90
|
+
// 只有能登上这台机器的人才能执行——内网里隔着浏览器的人做不到,因此是安全的恢复通道
|
|
91
|
+
const r = resetPasswordToDefault(CONFIG_PATH);
|
|
92
|
+
if (!r.ok) {
|
|
93
|
+
console.error(`❌ ${r.error}`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
console.log('✅ 配置页密码已恢复为初始密码 123456');
|
|
97
|
+
console.log('注:webauth.json 每次校验现读现验,配置页无需重启,刷新页面输入 123456 即可进入;');
|
|
98
|
+
console.log(' 登录后会再次引导你设置新密码。');
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
87
101
|
case 'pair': {
|
|
88
102
|
const access = AccessControl.load(join(CONFIG_DIR, 'access.json'));
|
|
89
103
|
const code = args[0];
|
|
@@ -220,6 +234,7 @@ async function main() {
|
|
|
220
234
|
lcb setup 引导式配置(命令行问答,支持多个机器人应用)
|
|
221
235
|
lcb start 启动桥接器(前台;含 Web 配置页,首次安装自动进入引导)
|
|
222
236
|
lcb ui 仅启动 Web 配置页(可与运行中的桥接器共存)
|
|
237
|
+
lcb reset-password 配置页密码忘记时,恢复初始密码 123456
|
|
223
238
|
lcb pair <code> 批准配对码
|
|
224
239
|
lcb app list 列出机器人应用
|
|
225
240
|
lcb app add 添加机器人应用(重启后生效)
|