@jesonliu/lark-claudecode-bridge 0.19.0 → 0.20.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/README.md +10 -10
- package/assets/web/css/main.css +509 -113
- package/assets/web/index.html +12 -2
- package/assets/web/js/main.js +16 -10
- package/assets/web/js/pages/apps.js +10 -5
- package/assets/web/js/pages/claude.js +15 -5
- package/assets/web/js/pages/mcp.js +56 -20
- package/assets/web/js/pages/overview.js +5 -4
- package/assets/web/js/pages/permissions.js +56 -30
- package/assets/web/js/pages/plugins.js +29 -6
- package/assets/web/js/pages/skills.js +184 -11
- package/assets/web/js/pages/slash.js +11 -5
- package/assets/web/js/pages/workspaces.js +15 -14
- package/assets/web/js/theme.js +23 -0
- package/assets/web/js/ui.js +75 -4
- package/config.example.yaml +2 -3
- package/dist/access/access-control.d.ts +17 -1
- package/dist/access/access-control.js +31 -2
- package/dist/access/access-control.js.map +1 -1
- package/dist/claude-config.d.ts +14 -2
- package/dist/claude-config.js +75 -4
- package/dist/claude-config.js.map +1 -1
- package/dist/cli/setup-wizard.d.ts +0 -1
- package/dist/cli/setup-wizard.js +2 -4
- package/dist/cli/setup-wizard.js.map +1 -1
- package/dist/config.js +6 -7
- package/dist/config.js.map +1 -1
- package/dist/executor/claude-executor.d.ts +1 -1
- package/dist/executor/claude-executor.js +4 -2
- package/dist/executor/claude-executor.js.map +1 -1
- package/dist/executor/notify-server.d.ts +9 -2
- package/dist/executor/notify-server.js +34 -9
- package/dist/executor/notify-server.js.map +1 -1
- package/dist/gateway/card-builder.d.ts +38 -0
- package/dist/gateway/card-builder.js +139 -18
- package/dist/gateway/card-builder.js.map +1 -1
- package/dist/gateway/diff-card.js +1 -1
- package/dist/gateway/diff-card.js.map +1 -1
- package/dist/gateway/feishu-gateway.d.ts +20 -3
- package/dist/gateway/feishu-gateway.js +58 -4
- package/dist/gateway/feishu-gateway.js.map +1 -1
- package/dist/gateway/progress-card.d.ts +51 -0
- package/dist/gateway/progress-card.js +166 -7
- package/dist/gateway/progress-card.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +195 -42
- package/dist/index.js.map +1 -1
- package/dist/notify-sop.d.ts +1 -1
- package/dist/notify-sop.js +5 -2
- package/dist/notify-sop.js.map +1 -1
- package/dist/session/commands.d.ts +1 -1
- package/dist/session/commands.js +25 -2
- package/dist/session/commands.js.map +1 -1
- package/dist/session/session-store.d.ts +3 -0
- package/dist/session/session-store.js.map +1 -1
- package/dist/types.d.ts +11 -5
- package/dist/util/workspace-diff.js +2 -2
- package/dist/util/workspace-diff.js.map +1 -1
- package/dist/web/server.js +96 -3
- package/dist/web/server.js.map +1 -1
- package/dist/web/skills-mcp-api.d.ts +11 -4
- package/dist/web/skills-mcp-api.js +47 -11
- package/dist/web/skills-mcp-api.js.map +1 -1
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// 已装插件内(<plugin-installPath>/skills;plugin 只读,未启用时标「(未启用)」)。
|
|
5
5
|
// 远端 CRUD:改动即时落盘,无页内保存条
|
|
6
6
|
import { $, esc, toast, api } from '../core.js';
|
|
7
|
-
import { openDrawer, closeDrawer } from '../ui.js';
|
|
7
|
+
import { openDrawer, closeDrawer, confirmDialog, promptDialog } from '../ui.js';
|
|
8
8
|
|
|
9
9
|
/** 来源标签(function:plugin 来源需要拼插件名与启用状态) */
|
|
10
10
|
const SOURCE_TAG = {
|
|
@@ -25,14 +25,14 @@ function renderSkills(el) {
|
|
|
25
25
|
<div class="card">
|
|
26
26
|
<h3>Skills</h3>
|
|
27
27
|
<div class="desc">Claude Code 技能(每项一个目录,含 SKILL.md)。列表聚合四个来源:本机用户级、bridge 自管用户级、各工作区项目级、已装插件内的 skills(只读展示)。新增(含 zip 导入)统一进用户级生效目录。</div>
|
|
28
|
-
<div
|
|
29
|
-
<button class="btn
|
|
30
|
-
<button class="btn
|
|
28
|
+
<div class="list-toolbar">
|
|
29
|
+
<button class="btn" id="skillImport">导入 zip</button>
|
|
30
|
+
<button class="btn primary" id="skillAdd">+ 新建</button>
|
|
31
31
|
</div>
|
|
32
32
|
<input type="file" id="skillZipFile" accept=".zip" style="display:none">
|
|
33
|
-
<input type="search" id="skillSearch" placeholder="
|
|
33
|
+
<input type="search" class="list-search" id="skillSearch" placeholder="按名字 / 来源 / 路径过滤…">
|
|
34
34
|
<table><thead><tr>
|
|
35
|
-
<th style="width:170px">名字</th><th>说明</th><th style="width:130px">来源</th><th style="width:240px">路径</th><th style="width:
|
|
35
|
+
<th style="width:170px">名字</th><th>说明</th><th style="width:130px">来源</th><th style="width:240px">路径</th><th style="width:110px"></th>
|
|
36
36
|
</tr></thead>
|
|
37
37
|
<tbody id="skillBody"><tr><td colspan="5" class="desc">加载中…</td></tr></tbody>
|
|
38
38
|
</table>
|
|
@@ -83,7 +83,7 @@ async function loadSkills() {
|
|
|
83
83
|
}
|
|
84
84
|
const skills = data.skills || [];
|
|
85
85
|
if (skills.length === 0) {
|
|
86
|
-
body.innerHTML = `<tr class="empty-row"><td colspan="5" class="desc">暂无 skill(四个来源均未发现 SKILL.md)。可点上方「+
|
|
86
|
+
body.innerHTML = `<tr class="empty-row"><td colspan="5" class="desc">暂无 skill(四个来源均未发现 SKILL.md)。可点上方「+ 新建」或「导入 zip」。</td></tr>`;
|
|
87
87
|
return;
|
|
88
88
|
}
|
|
89
89
|
body.innerHTML = '';
|
|
@@ -96,7 +96,11 @@ async function loadSkills() {
|
|
|
96
96
|
<td>${esc(s.description) || '<span class="desc">(无说明)</span>'}</td>
|
|
97
97
|
<td><span class="chip">${tag}</span></td>
|
|
98
98
|
<td><code class="desc">${esc(s.path)}</code></td>
|
|
99
|
-
<td
|
|
99
|
+
<td>
|
|
100
|
+
<button class="btn sm" data-browse="${esc(s.name)}">查看</button>
|
|
101
|
+
${isDeletable(s) ? `<button class="btn sm danger" data-del="${esc(s.name)}">删除</button>` : ''}
|
|
102
|
+
</td>`;
|
|
103
|
+
tr.querySelector('[data-browse]').onclick = () => browseSkill(s);
|
|
100
104
|
const delBtn = tr.querySelector('[data-del]');
|
|
101
105
|
if (delBtn) delBtn.onclick = () => delSkill(s);
|
|
102
106
|
body.appendChild(tr);
|
|
@@ -105,13 +109,177 @@ async function loadSkills() {
|
|
|
105
109
|
filterSkillRows();
|
|
106
110
|
}
|
|
107
111
|
|
|
108
|
-
function delSkill(s) {
|
|
109
|
-
if (!
|
|
112
|
+
async function delSkill(s) {
|
|
113
|
+
if (!(await confirmDialog({
|
|
114
|
+
title: '删除 skill',
|
|
115
|
+
message: `确定删除 skill <b>${esc(s.name)}</b>(${sourceTag(s)})?该目录会被整体移除(不可恢复)。`,
|
|
116
|
+
danger: true,
|
|
117
|
+
confirmText: '删除',
|
|
118
|
+
}))) return;
|
|
110
119
|
api('POST', '/api/skills/action', { op: 'delete', name: s.name, source: s.source, workspaceName: s.workspaceName })
|
|
111
120
|
.then(() => { toast('已删除'); void loadSkills(); })
|
|
112
121
|
.catch((e) => toast(e.message, true));
|
|
113
122
|
}
|
|
114
123
|
|
|
124
|
+
// ---------- skill 文件浏览(#3):目录树 + 原文/视图/双栏三种查看模式 ----------
|
|
125
|
+
|
|
126
|
+
/** 极简 markdown → HTML(先整体转义再还原语法,绝无 XSS 面):标题/代码块/行内代码/粗斜体/列表/引用/链接/分割线 */
|
|
127
|
+
function renderMarkdown(src) {
|
|
128
|
+
const codeBlocks = [];
|
|
129
|
+
let text = String(src).replace(/```(\w*)\n?([\s\S]*?)(?:```|$)/g, (_m, lang, code) => {
|
|
130
|
+
codeBlocks.push(`<pre style="background:var(--code-bg);border:1px solid var(--border);border-radius:8px;padding:10px;overflow:auto;font-size:12px"><code>${esc(code.replace(/\n$/, ''))}</code></pre>`);
|
|
131
|
+
return `${codeBlocks.length - 1}`;
|
|
132
|
+
});
|
|
133
|
+
const lines = text.split('\n');
|
|
134
|
+
const out = [];
|
|
135
|
+
let listOpen = false;
|
|
136
|
+
const closeList = () => { if (listOpen) { out.push('</ul>'); listOpen = false; } };
|
|
137
|
+
const inline = (s) => esc(s)
|
|
138
|
+
.replace(/`([^`]+)`/g, '<code style="background:var(--code-bg);padding:1px 5px;border-radius:4px;font-size:12px">$1</code>')
|
|
139
|
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
140
|
+
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
|
|
141
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener" style="color:var(--primary)">$1</a>');
|
|
142
|
+
for (const line of lines) {
|
|
143
|
+
const block = /^ (\d+) $/.exec(line);
|
|
144
|
+
if (block) { closeList(); out.push(codeBlocks[Number(block[1])]); continue; }
|
|
145
|
+
const h = /^(#{1,4})\s+(.*)$/.exec(line);
|
|
146
|
+
if (h) { closeList(); const lv = h[1].length; out.push(`<h${lv + 1} style="margin:12px 0 6px">${inline(h[2])}</h${lv + 1}>`); continue; }
|
|
147
|
+
if (/^\s*[-*]\s+/.test(line)) {
|
|
148
|
+
if (!listOpen) { out.push('<ul style="margin:4px 0;padding-left:22px">'); listOpen = true; }
|
|
149
|
+
out.push(`<li>${inline(line.replace(/^\s*[-*]\s+/, ''))}</li>`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (/^\s*\d+\.\s+/.test(line)) {
|
|
153
|
+
if (!listOpen) { out.push('<ul style="margin:4px 0;padding-left:22px">'); listOpen = true; }
|
|
154
|
+
out.push(`<li>${inline(line.replace(/^\s*\d+\.\s+/, ''))}</li>`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
closeList();
|
|
158
|
+
if (/^---+\s*$/.test(line)) { out.push('<hr style="border:none;border-top:1px solid var(--border);margin:10px 0">'); continue; }
|
|
159
|
+
if (/^>\s?/.test(line)) { out.push(`<blockquote style="margin:6px 0;padding:4px 12px;border-left:3px solid var(--border);color:var(--muted,#667)">${inline(line.replace(/^>\s?/, ''))}</blockquote>`); continue; }
|
|
160
|
+
if (line.trim() === '') { out.push(''); continue; }
|
|
161
|
+
out.push(`<p style="margin:4px 0">${inline(line)}</p>`);
|
|
162
|
+
}
|
|
163
|
+
closeList();
|
|
164
|
+
return out.join('\n');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 浏览抽屉:左侧文件树(目录懒加载展开),右侧查看区(markdown 文件支持 原文/视图/两者 三模式) */
|
|
168
|
+
function browseSkill(s) {
|
|
169
|
+
openDrawer({
|
|
170
|
+
title: `${s.name} · ${sourceTag(s)}`,
|
|
171
|
+
bodyHtml: `
|
|
172
|
+
<div class="hint" style="margin:0 0 10px"><code class="desc">${esc(s.path)}</code></div>
|
|
173
|
+
<div style="display:flex;gap:14px;align-items:flex-start">
|
|
174
|
+
<div id="sbTree" style="width:250px;flex:none;border:1px solid var(--border);border-radius:8px;padding:8px;max-height:62vh;overflow:auto;font-size:13px">
|
|
175
|
+
<div class="desc">目录加载中…</div>
|
|
176
|
+
</div>
|
|
177
|
+
<div style="flex:1;min-width:0">
|
|
178
|
+
<div id="sbModes" style="display:none;gap:8px;margin-bottom:8px" class="subtabs">
|
|
179
|
+
<button data-mode="both" class="active">两者都看</button>
|
|
180
|
+
<button data-mode="raw">只看原文</button>
|
|
181
|
+
<button data-mode="view">只看视图</button>
|
|
182
|
+
</div>
|
|
183
|
+
<div id="sbFileName" class="desc" style="margin-bottom:6px">点击左侧文件查看内容</div>
|
|
184
|
+
<div id="sbView" style="display:none;border:1px solid var(--border);border-radius:8px;padding:12px;max-height:56vh;overflow:auto;font-size:13px;line-height:1.6"></div>
|
|
185
|
+
<textarea id="sbRaw" readonly style="display:none;width:100%;min-height:56vh;font-family:ui-monospace,Consolas,monospace;font-size:12px"></textarea>
|
|
186
|
+
</div>
|
|
187
|
+
</div>`,
|
|
188
|
+
footHtml: `<button class="btn" data-act="close">关闭</button>`,
|
|
189
|
+
onMount({ body, foot }) {
|
|
190
|
+
foot.querySelector('[data-act="close"]').onclick = () => closeDrawer();
|
|
191
|
+
const tree = body.querySelector('#sbTree');
|
|
192
|
+
const modes = body.querySelector('#sbModes');
|
|
193
|
+
const fileName = body.querySelector('#sbFileName');
|
|
194
|
+
const viewPane = body.querySelector('#sbView');
|
|
195
|
+
const rawPane = body.querySelector('#sbRaw');
|
|
196
|
+
let mode = 'both';
|
|
197
|
+
const isMd = (p) => /\.(md|markdown)$/i.test(p);
|
|
198
|
+
const applyMode = () => {
|
|
199
|
+
modes.querySelectorAll('[data-mode]').forEach((b) => b.classList.toggle('active', b.dataset.mode === mode));
|
|
200
|
+
viewPane.style.display = mode === 'raw' ? 'none' : '';
|
|
201
|
+
rawPane.style.display = mode === 'view' ? 'none' : '';
|
|
202
|
+
};
|
|
203
|
+
modes.querySelectorAll('[data-mode]').forEach((b) => {
|
|
204
|
+
b.onclick = () => { mode = b.dataset.mode; applyMode(); };
|
|
205
|
+
});
|
|
206
|
+
const openFile = async (fp) => {
|
|
207
|
+
fileName.textContent = `加载中:${fp}`;
|
|
208
|
+
try {
|
|
209
|
+
const r = await api('GET', `/api/skills/raw?path=${encodeURIComponent(fp)}`);
|
|
210
|
+
fileName.innerHTML = `<strong>${esc(r.name)}</strong> <span class="desc">· ${(r.size / 1024).toFixed(1)}KB</span>`;
|
|
211
|
+
rawPane.value = r.content;
|
|
212
|
+
if (isMd(r.name)) {
|
|
213
|
+
viewPane.innerHTML = renderMarkdown(r.content);
|
|
214
|
+
modes.style.display = 'flex';
|
|
215
|
+
applyMode(); // md:按当前模式(默认两者都看)
|
|
216
|
+
} else {
|
|
217
|
+
viewPane.innerHTML = `<pre style="margin:0;white-space:pre-wrap;font-size:12px">${esc(r.content)}</pre>`;
|
|
218
|
+
modes.style.display = 'none';
|
|
219
|
+
viewPane.style.display = '';
|
|
220
|
+
rawPane.style.display = 'none'; // 非 md:格式化视图即原文等宽展示
|
|
221
|
+
}
|
|
222
|
+
} catch (e) {
|
|
223
|
+
fileName.textContent = '';
|
|
224
|
+
toast(e.message, true);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
const renderDir = async (dir, container) => {
|
|
228
|
+
container.innerHTML = '<div class="desc" style="padding:2px 6px">加载中…</div>';
|
|
229
|
+
let files;
|
|
230
|
+
try {
|
|
231
|
+
const r = await api('GET', `/api/skills/files?path=${encodeURIComponent(dir)}`);
|
|
232
|
+
files = r.files || [];
|
|
233
|
+
} catch (e) {
|
|
234
|
+
container.innerHTML = `<div class="desc" style="padding:2px 6px;color:var(--err-tx)">${esc(e.message)}</div>`;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
container.innerHTML = '';
|
|
238
|
+
if (files.length === 0) {
|
|
239
|
+
container.innerHTML = '<div class="desc" style="padding:2px 6px">(空目录)</div>';
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
for (const f of files) {
|
|
243
|
+
const row = document.createElement('div');
|
|
244
|
+
row.style.cssText = 'padding:3px 6px;border-radius:6px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis';
|
|
245
|
+
row.onmouseenter = () => { row.style.background = 'var(--off-bg)'; };
|
|
246
|
+
row.onmouseleave = () => { row.style.background = ''; };
|
|
247
|
+
const childPath = `${dir.replace(/[\\/]+$/, '')}/${f.name}`;
|
|
248
|
+
if (f.isDir) {
|
|
249
|
+
row.textContent = `▸ 📁 ${f.name}`;
|
|
250
|
+
let expanded = false;
|
|
251
|
+
let childBox = null;
|
|
252
|
+
row.onclick = async () => {
|
|
253
|
+
expanded = !expanded;
|
|
254
|
+
row.textContent = `${expanded ? '▾' : '▸'} 📁 ${f.name}`;
|
|
255
|
+
if (expanded && !childBox) {
|
|
256
|
+
childBox = document.createElement('div');
|
|
257
|
+
childBox.style.marginLeft = '14px';
|
|
258
|
+
row.after(childBox);
|
|
259
|
+
await renderDir(childPath, childBox);
|
|
260
|
+
} else if (childBox) {
|
|
261
|
+
childBox.style.display = expanded ? '' : 'none';
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
} else {
|
|
265
|
+
row.textContent = `📄 ${f.name}`;
|
|
266
|
+
row.title = `${f.name}(${(f.size / 1024).toFixed(1)}KB)`;
|
|
267
|
+
row.onclick = () => {
|
|
268
|
+
tree.querySelectorAll('[data-active]').forEach((x) => { delete x.dataset.active; x.style.background = ''; x.style.fontWeight = ''; });
|
|
269
|
+
row.dataset.active = '1';
|
|
270
|
+
row.style.background = 'var(--primary-weak)';
|
|
271
|
+
row.style.fontWeight = '600';
|
|
272
|
+
void openFile(childPath);
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
container.appendChild(row);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
void renderDir(s.path, tree);
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
115
283
|
function addSkill() {
|
|
116
284
|
openDrawer({
|
|
117
285
|
title: '新建 skill(用户级)',
|
|
@@ -152,7 +320,12 @@ function bindZipImport() {
|
|
|
152
320
|
const zipBase64 = btoa(String.fromCharCode(...new Uint8Array(buf)));
|
|
153
321
|
// 根打包(无子目录层级)时后端拿不到名字,先问一次;目录打包可留空由后端取名
|
|
154
322
|
const guess = file.name.replace(/\.zip$/i, '');
|
|
155
|
-
const name =
|
|
323
|
+
const name = (await promptDialog({
|
|
324
|
+
title: '导入 skill',
|
|
325
|
+
message: `zip 内含目录结构时可留空(将取目录名);根打包时必填。建议:${esc(guess)}`,
|
|
326
|
+
placeholder: 'skill 名(可留空)',
|
|
327
|
+
confirmText: '导入',
|
|
328
|
+
})) ?? '';
|
|
156
329
|
try {
|
|
157
330
|
const r = await api('POST', '/api/skills/import', { zipBase64, name: name.trim() || undefined });
|
|
158
331
|
toast(`已导入 "${r.name}"(${r.files} 个文件)`);
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
// ============ 斜杠命令(远端直操作飞书 API;本页与 config.yaml / 保存按钮无关) ============
|
|
2
2
|
import { S, $, esc, toast, api } from '../core.js';
|
|
3
|
-
import { openDrawer, closeDrawer } from '../ui.js';
|
|
3
|
+
import { openDrawer, closeDrawer, confirmDialog } from '../ui.js';
|
|
4
4
|
|
|
5
5
|
function renderSlash(el) {
|
|
6
6
|
el.innerHTML = `
|
|
7
7
|
<div class="card">
|
|
8
8
|
<h3>飞书斜杠命令(远端管理)</h3>
|
|
9
9
|
<div class="desc">直接读写飞书开放平台已注册的斜杠命令;聊天输入 <code>/</code> 弹出指令面板,选中后可继续输入描述再发送。需给应用开通 <code>application:app_slash_command</code> 读写权限并发布版本;改动约 5 分钟后生效(PC 端 7.70+)。<b>本页操作即时提交飞书,与底部「保存」按钮无关。</b></div>
|
|
10
|
-
<div class="
|
|
11
|
-
<div style="max-width:320px"><label>目标应用</label><select id="scApp"></select></div>
|
|
10
|
+
<div class="list-toolbar">
|
|
11
|
+
<div style="max-width:320px;width:100%"><label>目标应用</label><select id="scApp"></select></div>
|
|
12
|
+
<span class="spacer"></span>
|
|
12
13
|
<button class="btn" id="scReload">刷新列表</button>
|
|
13
|
-
<button class="btn primary" id="scAdd">+ 新增命令</button>
|
|
14
14
|
<button class="btn" id="scBuiltins">补齐内置命令</button>
|
|
15
|
+
<button class="btn primary" id="scAdd">+ 新增命令</button>
|
|
15
16
|
</div>
|
|
16
17
|
<table style="margin-top:12px"><thead><tr><th style="width:170px">命令</th><th>描述</th><th style="width:150px">图标</th><th style="width:130px">操作</th></tr></thead>
|
|
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>
|
|
@@ -58,7 +59,12 @@ function renderSlash(el) {
|
|
|
58
59
|
}
|
|
59
60
|
};
|
|
60
61
|
const delRemote = async (cid, cmd) => {
|
|
61
|
-
if (!
|
|
62
|
+
if (!(await confirmDialog({
|
|
63
|
+
title: '删除斜杠命令',
|
|
64
|
+
message: `确认删除远端命令 <code>/${esc(cmd)}</code>?约 5 分钟后从飞书面板消失,不可恢复。`,
|
|
65
|
+
danger: true,
|
|
66
|
+
confirmText: '删除',
|
|
67
|
+
}))) return;
|
|
62
68
|
try {
|
|
63
69
|
await api('POST', '/api/slash-commands/action', { op: 'delete', app: appSel.value, commandId: cid });
|
|
64
70
|
toast(`已删除 /${cmd}`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// ============ 工作区(行内编辑 + 卡级保存;改动落盘后热生效) ============
|
|
2
2
|
import { S, $, esc, toast, cardDirty, applyDirty } from '../core.js';
|
|
3
|
-
import { saveBarHtml, dirtyDotHtml, bindSaveBar, pickDirectory } from '../ui.js';
|
|
3
|
+
import { saveBarHtml, dirtyDotHtml, bindSaveBar, pickDirectory, confirmDialog } from '../ui.js';
|
|
4
4
|
|
|
5
5
|
function renderWorkspaces(el) {
|
|
6
6
|
const doc = S.doc;
|
|
@@ -8,11 +8,11 @@ function renderWorkspaces(el) {
|
|
|
8
8
|
el.innerHTML = `
|
|
9
9
|
<div class="card">
|
|
10
10
|
<h3>工作区${dirtyDotHtml('ws')}</h3>
|
|
11
|
-
<div class="desc">Claude
|
|
12
|
-
<div
|
|
13
|
-
<button class="btn
|
|
11
|
+
<div class="desc">Claude 的工作目录白名单。需要「先出方案再执行」时,在飞书会话里发 /plan 按通道切换计划模式;git 仓库工作区任务收尾会自动发汇总 diff 卡片。</div>
|
|
12
|
+
<div class="list-toolbar">
|
|
13
|
+
<button class="btn primary" id="wsAdd">+ 添加工作区</button>
|
|
14
14
|
</div>
|
|
15
|
-
<table><thead><tr><th style="width:180px">名字</th><th>路径</th><th style="width:
|
|
15
|
+
<table><thead><tr><th style="width:180px">名字</th><th>路径</th><th style="width:50px"></th></tr></thead>
|
|
16
16
|
<tbody id="wsBody"></tbody></table>
|
|
17
17
|
${saveBarHtml('ws')}
|
|
18
18
|
<div class="footer-note">运行中的桥接器下一条消息自动生效,无需重启。</div>
|
|
@@ -53,16 +53,11 @@ function renderWorkspaces(el) {
|
|
|
53
53
|
<input type="text" data-f="path" value="${esc(ws.path || '')}">
|
|
54
54
|
<button class="btn sm" data-browse="path" title="浏览选择目录">浏览</button>
|
|
55
55
|
</div></td>
|
|
56
|
-
<td><
|
|
57
|
-
<option value="generic" ${ws.type !== 'code-dev' ? 'selected' : ''}>通用(generic)</option>
|
|
58
|
-
<option value="code-dev" ${ws.type === 'code-dev' ? 'selected' : ''}>开发工作流(code-dev)</option>
|
|
59
|
-
</select></td>
|
|
60
|
-
<td><button class="btn sm danger">删</button></td>`;
|
|
56
|
+
<td><button class="btn sm danger">删除</button></td>`;
|
|
61
57
|
tr.querySelectorAll('[data-f]').forEach((input) => {
|
|
62
58
|
input.oninput = input.onchange = () => {
|
|
63
59
|
const f = input.dataset.f;
|
|
64
|
-
if (f === '
|
|
65
|
-
else if (f === 'name') {
|
|
60
|
+
if (f === 'name') {
|
|
66
61
|
// 改名联动:全局默认指向旧名时同步,否则保存会因 defaults.workspace 不在工作区列表报错
|
|
67
62
|
if (doc.defaults?.workspace && doc.defaults.workspace === ws.name) {
|
|
68
63
|
doc.defaults = { ...(doc.defaults || {}), workspace: input.value };
|
|
@@ -74,9 +69,15 @@ function renderWorkspaces(el) {
|
|
|
74
69
|
cardDirty('ws', true);
|
|
75
70
|
};
|
|
76
71
|
});
|
|
77
|
-
//
|
|
78
|
-
tr.querySelector('.danger').onclick = () => {
|
|
72
|
+
// 「删除」按钮精确选择(class="btn sm danger");原 tr.querySelector('.btn') 命中「浏览」并被浏览逻辑覆盖,故点「删除」无反应
|
|
73
|
+
tr.querySelector('.danger').onclick = async () => {
|
|
79
74
|
if (doc.workspaces.length <= 1) return toast('至少保留一个工作区', true);
|
|
75
|
+
if (!(await confirmDialog({
|
|
76
|
+
title: '删除工作区',
|
|
77
|
+
message: `确定删除工作区「${esc(ws.name || '未命名')}」?随本卡「保存」生效,保存前可点「还原」撤销。`,
|
|
78
|
+
danger: true,
|
|
79
|
+
confirmText: '删除',
|
|
80
|
+
}))) return;
|
|
80
81
|
doc.workspaces.splice(idx, 1);
|
|
81
82
|
cardDirty('ws', true);
|
|
82
83
|
renderWorkspaces(el);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// ============ 主题切换:跟随系统 / 亮色 / 暗色(顶栏分段控件,localStorage 持久化) ============
|
|
2
|
+
// data-theme 写在 <html> 上:auto = 不设属性(CSS 媒体查询接管);light/dark = 手动覆盖
|
|
3
|
+
const KEY = 'lcb-theme';
|
|
4
|
+
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
5
|
+
let current = 'auto';
|
|
6
|
+
|
|
7
|
+
const apply = (mode) => {
|
|
8
|
+
current = mode;
|
|
9
|
+
try { localStorage.setItem(KEY, mode); } catch { /* 隐私模式等写入失败可容忍 */ }
|
|
10
|
+
const root = document.documentElement;
|
|
11
|
+
if (mode === 'auto') delete root.dataset.theme;
|
|
12
|
+
else root.dataset.theme = mode;
|
|
13
|
+
document.querySelectorAll('#themeSwitch button').forEach((b) =>
|
|
14
|
+
b.classList.toggle('active', b.dataset.opt === mode));
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** main.js 启动时调用一次:恢复上次选择 + 绑定切换 + auto 模式跟随系统变化 */
|
|
18
|
+
export function initTheme() {
|
|
19
|
+
apply(localStorage.getItem(KEY) || 'auto');
|
|
20
|
+
mq.addEventListener('change', () => { if (current === 'auto') apply('auto'); }); // 仅为刷新 active 态展示
|
|
21
|
+
document.querySelectorAll('#themeSwitch button').forEach((b) =>
|
|
22
|
+
b.onclick = () => apply(b.dataset.opt));
|
|
23
|
+
}
|
package/assets/web/js/ui.js
CHANGED
|
@@ -1,6 +1,76 @@
|
|
|
1
|
-
// ============ 公共 UI 组件:保存条 / 目录选择弹层 / 右侧抽屉 ============
|
|
1
|
+
// ============ 公共 UI 组件:保存条 / 目录选择弹层 / 右侧抽屉 / 风格化确认弹窗 ============
|
|
2
2
|
import { S, $, esc, toast, api, saveDoc, refresh, hooks } from './core.js';
|
|
3
3
|
|
|
4
|
+
// ============ 风格化弹窗(替代浏览器原生 confirm / prompt) ============
|
|
5
|
+
let openDialogs = 0; // 打开中的弹窗计数:ESC 只关最上层弹窗,不误触抽屉
|
|
6
|
+
/** 挂载弹窗遮罩(.modal-mask + .dlg);返回卸载函数 */
|
|
7
|
+
function mountDialog(innerHTML) {
|
|
8
|
+
const mask = document.createElement('div');
|
|
9
|
+
mask.className = 'modal-mask';
|
|
10
|
+
mask.innerHTML = innerHTML;
|
|
11
|
+
document.body.appendChild(mask);
|
|
12
|
+
openDialogs++;
|
|
13
|
+
return { mask, unmount() { openDialogs--; mask.remove(); } };
|
|
14
|
+
}
|
|
15
|
+
/** 捕获阶段拦截 ESC / Enter(先于抽屉的冒泡监听),弹窗关闭后自动解绑 */
|
|
16
|
+
function bindDialogKeys(onKey) {
|
|
17
|
+
const handler = (e) => {
|
|
18
|
+
if (e.key !== 'Escape' && e.key !== 'Enter') return;
|
|
19
|
+
e.stopPropagation();
|
|
20
|
+
onKey(e.key);
|
|
21
|
+
};
|
|
22
|
+
document.addEventListener('keydown', handler, true);
|
|
23
|
+
return () => document.removeEventListener('keydown', handler, true);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 二次确认弹窗(替代 window.confirm)→ Promise<boolean>。
|
|
27
|
+
* message 支持 HTML(调用方负责 esc 用户数据);danger=true 时确认键为红色实心。
|
|
28
|
+
*/
|
|
29
|
+
export function confirmDialog({ title = '确认操作', message = '', confirmText = '确定', cancelText = '取消', danger = false }) {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const { mask, unmount } = mountDialog(`
|
|
32
|
+
<div class="modal dlg" role="alertdialog" aria-modal="true">
|
|
33
|
+
<h3 class="dlg-title${danger ? ' danger' : ''}">${danger ? '<span class="dlg-ico" aria-hidden="true">!</span>' : ''}${title}</h3>
|
|
34
|
+
<div class="dlg-msg">${message}</div>
|
|
35
|
+
<div class="dlg-foot">
|
|
36
|
+
<button class="btn" data-dlg="cancel">${esc(cancelText)}</button>
|
|
37
|
+
<button class="btn ${danger ? 'danger-solid' : 'primary'}" data-dlg="ok">${esc(confirmText)}</button>
|
|
38
|
+
</div>
|
|
39
|
+
</div>`);
|
|
40
|
+
const done = (v) => { unbind(); unmount(); resolve(v); };
|
|
41
|
+
const unbind = bindDialogKeys((key) => done(key === 'Enter'));
|
|
42
|
+
mask.querySelector('[data-dlg="ok"]').onclick = () => done(true);
|
|
43
|
+
mask.querySelector('[data-dlg="cancel"]').onclick = () => done(false);
|
|
44
|
+
mask.onclick = (e) => { if (e.target === mask) done(false); };
|
|
45
|
+
mask.querySelector('[data-dlg="ok"]').focus();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 输入弹窗(替代 window.prompt)→ Promise<string|null>(null = 取消)。
|
|
50
|
+
*/
|
|
51
|
+
export function promptDialog({ title = '请输入', message = '', placeholder = '', value = '', confirmText = '确定', cancelText = '取消' }) {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
const { mask, unmount } = mountDialog(`
|
|
54
|
+
<div class="modal dlg" role="dialog" aria-modal="true">
|
|
55
|
+
<h3 class="dlg-title">${title}</h3>
|
|
56
|
+
<div class="dlg-msg">${message}</div>
|
|
57
|
+
<input type="text" class="dlg-input" placeholder="${esc(placeholder)}" value="${esc(value)}">
|
|
58
|
+
<div class="dlg-foot">
|
|
59
|
+
<button class="btn" data-dlg="cancel">${esc(cancelText)}</button>
|
|
60
|
+
<button class="btn primary" data-dlg="ok">${esc(confirmText)}</button>
|
|
61
|
+
</div>
|
|
62
|
+
</div>`);
|
|
63
|
+
const input = mask.querySelector('.dlg-input');
|
|
64
|
+
const done = (v) => { unbind(); unmount(); resolve(v); };
|
|
65
|
+
const unbind = bindDialogKeys((key) => done(key === 'Enter' ? input.value : null));
|
|
66
|
+
mask.querySelector('[data-dlg="ok"]').onclick = () => done(input.value);
|
|
67
|
+
mask.querySelector('[data-dlg="cancel"]').onclick = () => done(null);
|
|
68
|
+
mask.onclick = (e) => { if (e.target === mask) done(null); };
|
|
69
|
+
input.focus();
|
|
70
|
+
input.select();
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
4
74
|
/** 卡级保存行(页内表单卡通用):保存 = saveDoc 落盘;还原 = 重拉磁盘配置 */
|
|
5
75
|
export const saveBarHtml = (id) => `<div class="savebar" id="bar-${id}" style="display:none"><span class="state">有未保存的修改</span><span style="flex:1"></span><button class="btn" data-discard="${id}">还原</button><button class="btn primary" data-save="${id}">保存</button></div>`;
|
|
6
76
|
export const dirtyDotHtml = (id) => `<span class="dirty-dot" id="dirty-${id}">· 未保存</span>`;
|
|
@@ -34,7 +104,7 @@ export function pickDirectory(initialPath) {
|
|
|
34
104
|
<div class="row">
|
|
35
105
|
<input type="text" class="pd-jump" placeholder="输入或粘贴路径,回车前往">
|
|
36
106
|
<button class="btn pd-go" title="跳转到输入的路径">前往</button>
|
|
37
|
-
<button class="btn pd-up" title="返回上一级"
|
|
107
|
+
<button class="btn pd-up" title="返回上一级">上级目录</button>
|
|
38
108
|
</div>
|
|
39
109
|
<div class="hint pd-cur" style="margin:8px 0 0;word-break:break-all"></div>
|
|
40
110
|
<div class="dir-list"><div class="empty">加载中…</div></div>
|
|
@@ -65,7 +135,7 @@ export function pickDirectory(initialPath) {
|
|
|
65
135
|
? r.drives.map((d) => ({ label: d, next: d }))
|
|
66
136
|
: (r.dirs || []).map((d) => ({ label: d, next: joinPath(current, d) }));
|
|
67
137
|
list.innerHTML = items.length
|
|
68
|
-
? items.map((it) => `<button data-next="${esc(it.next)}"
|
|
138
|
+
? items.map((it) => `<button data-next="${esc(it.next)}">${esc(it.label)}</button>`).join('')
|
|
69
139
|
: `<div class="empty">${r.error ? '(上述路径无法列出子目录)' : '(没有子目录)'}</div>`;
|
|
70
140
|
list.querySelectorAll('button[data-next]').forEach((b) => b.onclick = () => go(b.dataset.next));
|
|
71
141
|
};
|
|
@@ -119,5 +189,6 @@ const dismissDrawer = () => (drawerSnap ? cancelDrawer() : closeDrawer());
|
|
|
119
189
|
export function initDrawer() {
|
|
120
190
|
$('#drawerClose').onclick = dismissDrawer;
|
|
121
191
|
$('#drawerMask').addEventListener('mousedown', (e) => { if (e.target === e.currentTarget) dismissDrawer(); });
|
|
122
|
-
|
|
192
|
+
// ESC 有风格化弹窗打开时归弹窗(其自身在捕获阶段拦截并 stopPropagation,此处计数兜底)
|
|
193
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !openDialogs) dismissDrawer(); });
|
|
123
194
|
}
|
package/config.example.yaml
CHANGED
|
@@ -37,11 +37,10 @@ apps:
|
|
|
37
37
|
workspaces: # 工作区白名单:名字 + 路径(所有机器人共享列表,当前使用哪个 per-app 隔离)
|
|
38
38
|
- name: demo
|
|
39
39
|
path: F:\workspace\demo
|
|
40
|
-
#
|
|
41
|
-
|
|
40
|
+
# 需要「先出方案再执行」时,在飞书会话里发 /plan 按通道切换计划模式;
|
|
41
|
+
# git 仓库工作区任务收尾自动发汇总 diff 卡片(不再整文件上传)
|
|
42
42
|
# - name: my-project
|
|
43
43
|
# path: F:\workspace\my-project
|
|
44
|
-
# type: code-dev
|
|
45
44
|
defaults:
|
|
46
45
|
workspace: demo
|
|
47
46
|
concurrency: 3 # 全局并发上限(未单独配置的 app 沿用此值)
|
|
@@ -3,12 +3,14 @@ export interface AccessStoreData {
|
|
|
3
3
|
name: string;
|
|
4
4
|
role: 'admin' | 'member';
|
|
5
5
|
pairedAt: string;
|
|
6
|
+
appId?: string;
|
|
6
7
|
}>;
|
|
7
8
|
pending: Record<string, {
|
|
8
9
|
userId: string;
|
|
9
10
|
name: string;
|
|
10
11
|
code: string;
|
|
11
12
|
expiresAt: number;
|
|
13
|
+
appId?: string;
|
|
12
14
|
}>;
|
|
13
15
|
}
|
|
14
16
|
export declare class AccessControl {
|
|
@@ -26,12 +28,26 @@ export declare class AccessControl {
|
|
|
26
28
|
private save;
|
|
27
29
|
isAllowed(userId: string): boolean;
|
|
28
30
|
isAdmin(userId: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* 是否已有用户。传 appId 时按应用统计——多应用部署下 open_id 按应用隔离,
|
|
33
|
+
* 「首个使用者免配对自动 admin」也应按应用判定:新应用的第一个使用者同样直接成为
|
|
34
|
+
* admin,而不是因为别的应用已有用户就被迫走配对码。无 appId 的历史记录不计入
|
|
35
|
+
* 任何应用(它们会在下次发消息时被 ensureAppId 补登)。
|
|
36
|
+
*/
|
|
37
|
+
hasUsers(appId?: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* 直接添加用户(首个使用者免配对自动成为 admin 用)。
|
|
40
|
+
* 已存在时按传入 role/name 覆盖——与 approvePairing 的写语义一致(后写胜出)。
|
|
41
|
+
*/
|
|
42
|
+
addUser(userId: string, name: string, role: 'admin' | 'member', appId?: string): void;
|
|
43
|
+
/** 历史用户记录缺 appId 时按当前来路补登(老版本 access.json 的平滑迁移) */
|
|
44
|
+
ensureAppId(userId: string, appId: string): void;
|
|
29
45
|
listPending(): Array<{
|
|
30
46
|
userId: string;
|
|
31
47
|
name: string;
|
|
32
48
|
code: string;
|
|
33
49
|
}>;
|
|
34
|
-
beginPairing(userId: string, name: string): string;
|
|
50
|
+
beginPairing(userId: string, name: string, appId?: string): string;
|
|
35
51
|
approvePairing(code: string): {
|
|
36
52
|
ok: boolean;
|
|
37
53
|
userId?: string;
|
|
@@ -43,14 +43,42 @@ export class AccessControl {
|
|
|
43
43
|
isAdmin(userId) {
|
|
44
44
|
return this.data.users[userId]?.role === 'admin';
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* 是否已有用户。传 appId 时按应用统计——多应用部署下 open_id 按应用隔离,
|
|
48
|
+
* 「首个使用者免配对自动 admin」也应按应用判定:新应用的第一个使用者同样直接成为
|
|
49
|
+
* admin,而不是因为别的应用已有用户就被迫走配对码。无 appId 的历史记录不计入
|
|
50
|
+
* 任何应用(它们会在下次发消息时被 ensureAppId 补登)。
|
|
51
|
+
*/
|
|
52
|
+
hasUsers(appId) {
|
|
53
|
+
if (appId === undefined)
|
|
54
|
+
return Object.keys(this.data.users).length > 0;
|
|
55
|
+
return Object.values(this.data.users).some((u) => u.appId === appId);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 直接添加用户(首个使用者免配对自动成为 admin 用)。
|
|
59
|
+
* 已存在时按传入 role/name 覆盖——与 approvePairing 的写语义一致(后写胜出)。
|
|
60
|
+
*/
|
|
61
|
+
addUser(userId, name, role, appId) {
|
|
62
|
+
this.data.users[userId] = { name, role, pairedAt: new Date().toISOString(), ...(appId ? { appId } : {}) };
|
|
63
|
+
delete this.data.pending[userId]; // 残留的配对申请一并清掉
|
|
64
|
+
this.save();
|
|
65
|
+
}
|
|
66
|
+
/** 历史用户记录缺 appId 时按当前来路补登(老版本 access.json 的平滑迁移) */
|
|
67
|
+
ensureAppId(userId, appId) {
|
|
68
|
+
const u = this.data.users[userId];
|
|
69
|
+
if (u && !u.appId) {
|
|
70
|
+
u.appId = appId;
|
|
71
|
+
this.save();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
46
74
|
listPending() {
|
|
47
75
|
this.evict();
|
|
48
76
|
return Object.values(this.data.pending).map(({ userId, name, code }) => ({ userId, name, code }));
|
|
49
77
|
}
|
|
50
|
-
beginPairing(userId, name) {
|
|
78
|
+
beginPairing(userId, name, appId) {
|
|
51
79
|
this.evict();
|
|
52
80
|
const code = String(randomInt(0, 1_000_000)).padStart(6, '0');
|
|
53
|
-
this.data.pending[userId] = { userId, name, code, expiresAt: Date.now() + TTL_MS };
|
|
81
|
+
this.data.pending[userId] = { userId, name, code, expiresAt: Date.now() + TTL_MS, ...(appId ? { appId } : {}) };
|
|
54
82
|
this.save();
|
|
55
83
|
return code;
|
|
56
84
|
}
|
|
@@ -64,6 +92,7 @@ export class AccessControl {
|
|
|
64
92
|
name: entry.name,
|
|
65
93
|
role: isFirstAdmin ? 'admin' : 'member',
|
|
66
94
|
pairedAt: new Date().toISOString(),
|
|
95
|
+
...(entry.appId ? { appId: entry.appId } : {}),
|
|
67
96
|
};
|
|
68
97
|
delete this.data.pending[entry.userId];
|
|
69
98
|
this.save();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access-control.js","sourceRoot":"","sources":["../../src/access/access-control.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,MAAM,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAO9B,MAAM,OAAO,aAAa;IAEJ,SAAS;IADrB,IAAI,CAAkB;IAC9B,YAAoB,SAAiB,EAAE,IAAsB;yBAAzC,SAAS;QAC3B,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IACjD,CAAC;IACD,MAAM,CAAC,IAAI,CAAC,SAAiB;QAC3B,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAoB,CAAC;YAC3E,OAAO,IAAI,aAAa,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,IAAI,EAAE,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC,CAAC;QAC9F,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,aAAa,CAAC,SAAS,CAAC,CAAC;QACtC,CAAC;IACH,CAAC;IACD;;;;;OAKG;IACH,MAAM;QACJ,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAoB,CAAC;YAChF,IAAI,CAAC,IAAI,GAAG,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,IAAI,EAAE,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;QACrE,CAAC;QAAC,MAAM,CAAC;YACP,iBAAiB;QACnB,CAAC;IACH,CAAC;IACO,IAAI;QACV,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACxD,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAC5E,CAAC;IACD,SAAS,CAAC,MAAc;QACtB,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,CAAC,MAAc;QACpB,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,IAAI,KAAK,OAAO,CAAC;IACnD,CAAC;IACD,WAAW;QACT,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACpG,CAAC;IACD,YAAY,CAAC,MAAc,EAAE,IAAY;
|
|
1
|
+
{"version":3,"file":"access-control.js","sourceRoot":"","sources":["../../src/access/access-control.ts"],"names":[],"mappings":"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"}
|
package/dist/claude-config.d.ts
CHANGED
|
@@ -15,10 +15,22 @@ export declare function resolveClaudeDir(config: BridgeConfig): string;
|
|
|
15
15
|
* 不能被本机值或 env 配置绕过。
|
|
16
16
|
*/
|
|
17
17
|
export declare function buildManagedSettings(existing: unknown, claude: ClaudeConfig, userEnv?: Record<string, string>, configEnv?: Record<string, string>): Record<string, unknown>;
|
|
18
|
+
/**
|
|
19
|
+
* managed 模式下把本机 ~/.claude/skills 桥接到托管目录的 skills/,让托管会话看到
|
|
20
|
+
* user 范围的 skills(否则 ~/.claude/skills/* 永远不可见——因为 Claude Code 只从
|
|
21
|
+
* $CLAUDE_CONFIG_DIR/skills 发现 user skills,managed 模式 CLAUDE_CONFIG_DIR 不再
|
|
22
|
+
* 指向 ~/.claude)。每个 skill 在托管目录下用目录链接(Windows junction / POSIX
|
|
23
|
+
* symlink)挂到真实位置——skill 更新在原处落地,链接侧无感。
|
|
24
|
+
* 返回 bridged 名单供日志;同名已存在(非 symlink)时报错并跳过(用户已托管自定义
|
|
25
|
+
* 副本,勿覆盖)。
|
|
26
|
+
*/
|
|
27
|
+
export declare function bridgeUserSkills(managedDir: string, userClaudeDir: string): string[];
|
|
18
28
|
/**
|
|
19
29
|
* managed 模式初始化:建目录 + 把 claude 段最新值合并进 settings.json,
|
|
20
|
-
* 并从本机 ~/.claude 继承 MCP servers(.claude.json mcpServers
|
|
21
|
-
* (settings.json env
|
|
30
|
+
* 并从本机 ~/.claude 继承 MCP servers(.claude.json mcpServers)、自定义环境变量
|
|
31
|
+
* (settings.json env 块,领土键除外)与 user skills(junction 桥接)——否则托管
|
|
32
|
+
* 会话读不到用户全局 MCP 及其依赖变量、看不到 ~/.claude/skills 下的 skill
|
|
33
|
+
* (典型表现:飞书斜杠命令 /<skill-name> 透传给 CLI 后报 Unknown command)。
|
|
22
34
|
* lcb start 启动 bridge 前调用,保证首条消息前认证/环境就位;非 managed 模式为 no-op。
|
|
23
35
|
*/
|
|
24
36
|
export declare function initManagedClaudeDir(config: BridgeConfig, userClaudeDir?: string): void;
|