@jesonliu/lark-claudecode-bridge 1.4.2 → 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 +430 -101
- 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 +135 -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 +54 -41
- package/dist/index.js.map +1 -1
- package/dist/memory-installer.d.ts +7 -0
- package/dist/memory-installer.js +22 -5
- package/dist/memory-installer.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
|
|
|
@@ -305,6 +348,66 @@ export function progressDialog({ title = '执行中', note = '请稍候…', rep
|
|
|
305
348
|
};
|
|
306
349
|
}
|
|
307
350
|
|
|
351
|
+
// ============ 分步进度弹窗(重启流程等真实状态驱动的串行步骤展示) ============
|
|
352
|
+
/**
|
|
353
|
+
* 分步进度弹窗:步骤状态由调用方按**真实探测结果**驱动(区别于 progressDialog 的时间缓动
|
|
354
|
+
* 假进度),适合「LCB 重启 → 记忆增强启动」这类前后步骤状态源不同的流程。
|
|
355
|
+
* 视觉沿用记忆页安装流水线 stepper 的 badge 语言(✓/✗/spinner/序号圆),纵向排布。
|
|
356
|
+
*
|
|
357
|
+
* 可随时关闭是刻意设计:重启是幂等操作、记忆自举在桥接器进程内后台进行,关弹窗只是
|
|
358
|
+
* 停止**观看**,不影响流程本身——所以这里不做 progressDialog 那种「完成前禁关」。
|
|
359
|
+
*
|
|
360
|
+
* @param {string} opts.title 弹窗标题
|
|
361
|
+
* @param {Array<{title: string, detail?: string, status?: 'pending'|'running'|'done'|'failed'|'warn'|'skipped'}>} opts.steps 初始步骤
|
|
362
|
+
* @returns {{ setSteps(steps): void, setHint(html): void, onClose(fn): void, unmount(): void }}
|
|
363
|
+
* setSteps 整体替换步骤并重绘;setHint 设置底部提示区(放「手动刷新」等动作);
|
|
364
|
+
* onClose 注册关闭回调(调用方停轮询/刷新状态);unmount 直接销毁。
|
|
365
|
+
*/
|
|
366
|
+
export function stepsDialog({ title = '执行进度', steps = [] } = {}) {
|
|
367
|
+
const { mask, unmount } = mountDialog(`
|
|
368
|
+
<div class="modal dlg" role="dialog" aria-modal="true" style="width:min(520px,94vw)">
|
|
369
|
+
<div style="display:flex;align-items:center;gap:10px">
|
|
370
|
+
<b class="dlg-title" style="margin:0;flex:1">${esc(title)}</b>
|
|
371
|
+
<button class="btn sm" data-sd="close">后台运行</button>
|
|
372
|
+
</div>
|
|
373
|
+
<div data-sd="steps" style="display:flex;flex-direction:column;gap:6px;margin-top:14px"></div>
|
|
374
|
+
<div class="hint" data-sd="hint" style="margin-top:10px"></div>
|
|
375
|
+
</div>`);
|
|
376
|
+
const stepsEl = mask.querySelector('[data-sd="steps"]');
|
|
377
|
+
const hintEl = mask.querySelector('[data-sd="hint"]');
|
|
378
|
+
let onCloseFn = null;
|
|
379
|
+
// badge 视觉与 memory.js installStepsHtml 同源:done=绿✓ failed=红✗ running=spinner
|
|
380
|
+
// pending=序号圈;warn(黄!)为本组件扩展——「未就绪但不构成失败」(记忆增强不阻断语义)
|
|
381
|
+
const BADGE = {
|
|
382
|
+
done: '<span style="width:20px;height:20px;border-radius:50%;background:var(--ok-tx);color:#fff;display:inline-flex;align-items:center;justify-content:center;font-size:11px;flex:0 0 auto">✓</span>',
|
|
383
|
+
failed: '<span style="width:20px;height:20px;border-radius:50%;background:var(--err-tx);color:#fff;display:inline-flex;align-items:center;justify-content:center;font-size:11px;flex:0 0 auto">✗</span>',
|
|
384
|
+
warn: '<span style="width:20px;height:20px;border-radius:50%;background:var(--warn-tx);color:#fff;display:inline-flex;align-items:center;justify-content:center;font-size:11px;flex:0 0 auto">!</span>',
|
|
385
|
+
running: '<span class="br-spin-sm" style="margin:0;flex:0 0 auto"></span>',
|
|
386
|
+
skipped: '<span style="width:20px;height:20px;border-radius:50%;background:var(--muted);color:#fff;display:inline-flex;align-items:center;justify-content:center;font-size:11px;flex:0 0 auto">–</span>',
|
|
387
|
+
};
|
|
388
|
+
const paint = (list) => {
|
|
389
|
+
stepsEl.innerHTML = list.map((s) => {
|
|
390
|
+
const status = s.status || 'pending';
|
|
391
|
+
const badge = BADGE[status]
|
|
392
|
+
|| `<span style="width:20px;height:20px;border-radius:50%;border:1.5px solid var(--muted);color:var(--muted);display:inline-flex;align-items:center;justify-content:center;font-size:11px;flex:0 0 auto">${list.indexOf(s) + 1}</span>`;
|
|
393
|
+
return `<div style="background:var(--off-bg);border-radius:10px;padding:10px 12px">
|
|
394
|
+
<div style="display:flex;align-items:center;gap:8px">${badge}<b style="font-size:12.5px">${esc(s.title)}</b></div>
|
|
395
|
+
<div class="hint" style="margin-top:5px;font-size:11px;line-height:1.55;word-break:break-all">${s.detail ? esc(s.detail) : ' '}</div>
|
|
396
|
+
</div>`;
|
|
397
|
+
}).join('');
|
|
398
|
+
};
|
|
399
|
+
paint(steps);
|
|
400
|
+
const close = () => { unbind(); unmount(); onCloseFn?.(); };
|
|
401
|
+
const unbind = bindDialogKeys((k) => { if (k === 'Escape') close(); });
|
|
402
|
+
mask.querySelector('[data-sd="close"]').onclick = close;
|
|
403
|
+
return {
|
|
404
|
+
setSteps(list) { steps = list; paint(list); },
|
|
405
|
+
setHint(html) { hintEl.innerHTML = html || ''; },
|
|
406
|
+
onClose(fn) { onCloseFn = fn; },
|
|
407
|
+
unmount() { unbind(); unmount(); },
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
308
411
|
// ============ 可搜索多选字段(分身白名单等;仅限候选内选择,遗留值保留可删) ============
|
|
309
412
|
/** 纯函数(可测):候选过滤——大小写不敏感子串匹配 value+label+desc;>50 项截断(面板尾行提示) */
|
|
310
413
|
export function filterMselOptions(options, query) {
|