@jesonliu/lark-claudecode-bridge 0.13.0 → 0.14.2
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 +8 -3
- package/assets/web/index.html +375 -22
- package/dist/claude-config.d.ts +10 -5
- package/dist/claude-config.js +73 -6
- package/dist/claude-config.js.map +1 -1
- package/dist/config.js +18 -1
- package/dist/config.js.map +1 -1
- package/dist/executor/claude-executor.d.ts +7 -2
- package/dist/executor/claude-executor.js +62 -17
- package/dist/executor/claude-executor.js.map +1 -1
- package/dist/executor/output-collector.d.ts +2 -0
- package/dist/executor/output-collector.js +15 -3
- package/dist/executor/output-collector.js.map +1 -1
- package/dist/executor/permission-gate.d.ts +29 -2
- package/dist/executor/permission-gate.js +66 -3
- package/dist/executor/permission-gate.js.map +1 -1
- package/dist/executor/plugin-discovery.d.ts +19 -0
- package/dist/executor/plugin-discovery.js +40 -1
- package/dist/executor/plugin-discovery.js.map +1 -1
- package/dist/executor/plugin-manager.d.ts +16 -0
- package/dist/executor/plugin-manager.js +25 -0
- package/dist/executor/plugin-manager.js.map +1 -1
- package/dist/gateway/card-builder.d.ts +18 -0
- package/dist/gateway/card-builder.js +61 -1
- package/dist/gateway/card-builder.js.map +1 -1
- package/dist/gateway/feishu-gateway.d.ts +10 -0
- package/dist/gateway/feishu-gateway.js +4 -0
- package/dist/gateway/feishu-gateway.js.map +1 -1
- package/dist/gateway/progress-card.d.ts +8 -0
- package/dist/gateway/progress-card.js +16 -0
- package/dist/gateway/progress-card.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +130 -11
- package/dist/index.js.map +1 -1
- package/dist/session/commands.d.ts +1 -1
- package/dist/session/commands.js +45 -17
- package/dist/session/commands.js.map +1 -1
- package/dist/session/session-store.d.ts +14 -1
- package/dist/session/session-store.js +15 -0
- package/dist/session/session-store.js.map +1 -1
- package/dist/types.d.ts +11 -1
- package/dist/web/config-api.d.ts +2 -0
- package/dist/web/config-api.js +2 -0
- package/dist/web/config-api.js.map +1 -1
- package/dist/web/server.js +80 -5
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,14 +94,15 @@ lcb start
|
|
|
94
94
|
|
|
95
95
|
| 命令 | 说明 |
|
|
96
96
|
|---|---|
|
|
97
|
-
| /new |
|
|
98
|
-
| /resume | 列出/恢复历史会话(`/resume <编号>`
|
|
97
|
+
| /new | 开新会话(历史保留,`/resume` 可随时切回) |
|
|
98
|
+
| /resume | 列出/恢复历史会话(`/resume <编号>` 恢复指定会话;列表标注当前续接的会话) |
|
|
99
99
|
| /stop | 停止当前任务 |
|
|
100
100
|
| /status | 当前状态 |
|
|
101
101
|
| /ws list / /ws use \<名字\> | 工作区(切换仅 admin 可用) |
|
|
102
102
|
| /model | 查看当前模型;`/model <名字>` 通道级切换;`/model reset` 恢复默认 |
|
|
103
103
|
| /skills / /plugins / /mcp | 查看本会话实际加载的技能 / 插件 / MCP 服务 |
|
|
104
104
|
| /plugin | 插件管理:`/plugin list`(全员,含本机 ~/.claude 与托管目录两处清单);`install/uninstall/enable/disable/marketplace …`(仅 admin,默认装 ~/.claude,`--dir=managed` 装托管目录),装好下一条消息自动加载 |
|
|
105
|
+
| /reload-plugins | 重载插件:清插件发现缓存,下一条消息重新扫描加载(终端命令的 bridge 等价物) |
|
|
105
106
|
| /help | 帮助 |
|
|
106
107
|
| 其它 `/xxx` | **原文透传**给 Claude Code 派发斜杠命令(如 `/superpowers:brainstorming` 触发插件技能) |
|
|
107
108
|
|
|
@@ -167,6 +168,8 @@ concurrency: 3 # 通道间并发上限(未单独配置的 app 沿
|
|
|
167
168
|
|
|
168
169
|
配置页「Claude 认证」tab 可视化切换;managed 模式下认证/模型改动保存后即对后续任务生效(无需重启)。
|
|
169
170
|
|
|
171
|
+
**managed 模式的 MCP 与环境继承**(0.14.0 起,`lcb start` 时自动完成):本机 `~/.claude.json` 的全局 `mcpServers` 单向同步到托管目录(CLI 读 `$CLAUDE_CONFIG_DIR/.claude.json`,不同步则托管会话丢掉全部 user 级 MCP);本机 `~/.claude/settings.json` env 块中的**非认证键**(MCP 工具依赖的 `IMAGE_GEN_*`、`API_HOST` 等自定义变量)并入托管 settings.json。需要覆盖继承值或本机没有这些配置时,用**显式配置**:配置页「Claude 认证」→「环境变量」行编辑器(或 config.yaml 的 `claude.env` 键值对),优先级 `claude.env` > 本机继承 > 托管目录既有值;认证与模型 4 键(`ANTHROPIC_AUTH_TOKEN/API_KEY/BASE_URL/MODEL`)不在此生效——永远以认证表单为准。
|
|
172
|
+
|
|
170
173
|
**插件双目录(managed 模式)**:新装插件默认装到本机 `~/.claude`(与本机 claude CLI 共用一份),`/plugin install xxx --dir=managed` 或配置页安装框选「bridge 托管目录」可装到托管目录;启停/卸载自动按插件所在目录执行,两处清单在配置页「插件」tab 与 `/plugin list` 中均带来源标记。
|
|
171
174
|
|
|
172
175
|
### 开发场景工作流(`type: code-dev`)
|
|
@@ -174,11 +177,13 @@ concurrency: 3 # 通道间并发上限(未单独配置的 app 沿
|
|
|
174
177
|
给代码开发类工作区设置 `type: code-dev` 后,每个任务自动走「先计划、后执行」:
|
|
175
178
|
|
|
176
179
|
1. **计划审批**:任务以 plan mode 启动(期间只允许读操作),Claude 查阅代码后提交计划 → 飞书收到计划卡片:
|
|
177
|
-
- **✅
|
|
180
|
+
- **✅ 批准执行**:批准即授权——Claude 自动切入 acceptEdits 模式按计划开工,**后续写文件不再逐次弹确认卡**(对齐本机 CLI「批准计划 → accept edits on」语义;Bash 危险命令黑名单仍生效,命中照弹确认)
|
|
178
181
|
- **📝 按意见修改**:在卡片输入框填修改意见后点击,Claude 修订计划重新提交(同一会话内循环,直到批准或放弃)
|
|
179
182
|
- **❌ 放弃计划**:任务终止;10 分钟无操作自动放弃
|
|
180
183
|
2. **收尾汇总 diff**:任务完成后不再把改动文件逐个上传,而是发**汇总 diff 卡片**(标题含文件数与 +X/-Y 行统计,正文红绿着色,超长自动拆多张)。改动以 `git diff HEAD` + untracked 新文件为准,因此 **code-dev 工作区需要是 git 仓库**(非 git 仓库自动回退为旧的文件上传行为,并提示 `git init`)
|
|
181
184
|
|
|
185
|
+
**执行器为 Streaming Input 模式**(0.14.0 起):prompt 经持久输入流送入 CLI,stdin 全程保持打开——这是计划审批与提问卡片能稳定工作的前提(旧版单轮模式在轮次边界会触发 CLI 的 "Stream closed" 中断,属 Agent SDK 已知问题)。**提问卡片**:Claude 调用 AskUserQuestion 时飞书收到问题选项卡,点选项作答(多选题可多选)、全部作答后「提交答案」——答案直接回传模型继续任务。
|
|
186
|
+
|
|
182
187
|
**读操作免确认**:读工具与 Bash 默认直通(`ls`/`cat`/`grep` 不再弹卡),命中 `dangerous_commands` 黑名单(`rm -rf`、`sudo`、`git push --force` 等)仍弹确认卡;「本次会话不再询问」的记忆同样绕不过黑名单。想放行其它工具(如 `Edit`)往 `permissions.allow_tools` 追加即可——注意配置即**整体替换**内置默认,需把内置读工具一并写上。
|
|
183
188
|
|
|
184
189
|
> plan 卡片与 permissions 配置的改动需重启 bridge 进程生效(`workspaces[].type` 改动亦然)。
|
package/assets/web/index.html
CHANGED
|
@@ -65,6 +65,8 @@
|
|
|
65
65
|
.br-mask { position: fixed; inset: 0; background: rgba(15,19,24,.5); z-index: 90; display: flex; align-items: center; justify-content: center; }
|
|
66
66
|
.br-mask-card { background: var(--card); border-radius: 12px; padding: 24px 32px; text-align: center; box-shadow: 0 8px 30px rgba(0,0,0,.18); max-width: 440px; font-size: 14px; }
|
|
67
67
|
.br-spin { width: 26px; height: 26px; margin: 0 auto 12px; border: 3px solid #d9e2f5; border-top-color: var(--primary); border-radius: 50%; animation: brspin .9s linear infinite; }
|
|
68
|
+
/* 表格内小号 spinner(斜杠命令远端拉取等异步加载占位) */
|
|
69
|
+
.br-spin-sm { width: 18px; height: 18px; margin: 0 auto 10px; border: 2px solid #d9e2f5; border-top-color: var(--primary); border-radius: 50%; animation: brspin .9s linear infinite; }
|
|
68
70
|
@keyframes brspin { to { transform: rotate(360deg); } }
|
|
69
71
|
.app-card { border: 1px solid var(--border); border-radius: 8px; padding: 4px 16px 14px; margin-bottom: 12px; position: relative; }
|
|
70
72
|
.app-card .del { position: absolute; top: 10px; right: 12px; }
|
|
@@ -88,6 +90,31 @@
|
|
|
88
90
|
.subtabs { display: flex; gap: 8px; margin-bottom: 14px; }
|
|
89
91
|
.subtabs button { padding: 6px 14px; border-radius: 999px; border: 1px solid var(--border); background: none; cursor: pointer; font: inherit; color: var(--text); }
|
|
90
92
|
.subtabs button.active { background: #eef3ff; color: var(--primary); border-color: var(--primary); font-weight: 600; }
|
|
93
|
+
/* 居中 modal:目录选择等轻量交互(z-index 低于全页断连遮罩 br-mask=90) */
|
|
94
|
+
.modal-mask { position: fixed; inset: 0; background: rgba(15,19,24,.42); z-index: 80; display: flex; align-items: center; justify-content: center; }
|
|
95
|
+
.modal { width: min(560px, 92vw); background: var(--card); border-radius: 12px; padding: 16px 20px; box-shadow: 0 8px 30px rgba(0,0,0,.18); }
|
|
96
|
+
.dir-list { border: 1px solid var(--border); border-radius: 8px; max-height: 46vh; overflow-y: auto; margin-top: 10px; }
|
|
97
|
+
.dir-list button { display: block; width: 100%; text-align: left; padding: 7px 12px; border: 0; border-bottom: 1px solid var(--border); background: none; cursor: pointer; font: inherit; }
|
|
98
|
+
.dir-list button:last-child { border-bottom: 0; }
|
|
99
|
+
.dir-list button:hover { background: #eef3ff; color: var(--primary); }
|
|
100
|
+
.dir-list .empty { padding: 14px 12px; color: var(--muted); font-size: 12.5px; }
|
|
101
|
+
/* 输入框 + 行内按钮组合(路径框带「浏览」) */
|
|
102
|
+
.input-btn { display: flex; gap: 6px; }
|
|
103
|
+
.input-btn > input { flex: 1; min-width: 0; }
|
|
104
|
+
.input-btn > .btn { flex: 0 0 auto; align-self: center; }
|
|
105
|
+
/* 可搜索 combobox(安装插件选择器):输入过滤 + 结果面板 */
|
|
106
|
+
.combo { position: relative; }
|
|
107
|
+
.combo-panel { position: absolute; top: calc(100% + 4px); left: 0; right: 0; background: var(--card); border: 1px solid var(--border); border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,.14); max-height: 320px; overflow-y: auto; z-index: 40; }
|
|
108
|
+
.combo-panel[hidden] { display: none; }
|
|
109
|
+
.combo-item { padding: 7px 12px; border-bottom: 1px solid var(--border); cursor: pointer; }
|
|
110
|
+
.combo-item:last-child { border-bottom: 0; }
|
|
111
|
+
.combo-item:hover { background: #eef3ff; }
|
|
112
|
+
.combo-item b { margin-right: 6px; }
|
|
113
|
+
.combo-empty { padding: 12px; color: var(--muted); font-size: 12.5px; }
|
|
114
|
+
/* 操作进度弹框:进度条(update-all 真实 i/N,单操作时间缓动模拟) */
|
|
115
|
+
.prog { height: 8px; border-radius: 999px; background: var(--bg); border: 1px solid var(--border); overflow: hidden; margin-top: 12px; }
|
|
116
|
+
.prog-bar { height: 100%; width: 0%; background: var(--primary); border-radius: 999px; transition: width .3s; }
|
|
117
|
+
.prog-bar.done { background: var(--ok); }
|
|
91
118
|
</style>
|
|
92
119
|
</head>
|
|
93
120
|
<body>
|
|
@@ -171,6 +198,81 @@ async function saveDoc() {
|
|
|
171
198
|
} catch (e) { toast(`保存失败:${e.message}`, true); return false; }
|
|
172
199
|
}
|
|
173
200
|
|
|
201
|
+
// ============ 目录选择弹层(工作区路径等;数据源 GET /api/fs/dirs) ============
|
|
202
|
+
/** 后端返回的绝对路径 + 子目录名拼完整路径(Windows 反斜杠 / posix 正斜杠,按 base 自带分隔符判断) */
|
|
203
|
+
const joinPath = (base, name) => {
|
|
204
|
+
if (!base) return name;
|
|
205
|
+
const sepCh = base.includes('\\') ? '\\' : '/';
|
|
206
|
+
return base.replace(/[\\/]+$/, '') + sepCh + name;
|
|
207
|
+
};
|
|
208
|
+
/**
|
|
209
|
+
* 打开目录浏览弹层:盘符/根层 → 逐级进入 → 「选择此目录」确定。
|
|
210
|
+
* resolve(选中的绝对路径) / resolve(null)(取消)。手输路径可直接跳转,
|
|
211
|
+
* 路径不存在等错误在弹层内联提示(不关层、可改可回退)。
|
|
212
|
+
*/
|
|
213
|
+
function pickDirectory(initialPath) {
|
|
214
|
+
return new Promise((resolveP) => {
|
|
215
|
+
const mask = document.createElement('div');
|
|
216
|
+
mask.className = 'modal-mask';
|
|
217
|
+
mask.innerHTML = `
|
|
218
|
+
<div class="modal" role="dialog" aria-modal="true">
|
|
219
|
+
<h3 style="margin:0 0 10px">选择目录</h3>
|
|
220
|
+
<div class="row">
|
|
221
|
+
<input type="text" class="pd-jump" placeholder="输入或粘贴路径,回车前往">
|
|
222
|
+
<button class="btn pd-go" title="跳转到输入的路径">前往</button>
|
|
223
|
+
<button class="btn pd-up" title="返回上一级">⬆ 上级</button>
|
|
224
|
+
</div>
|
|
225
|
+
<div class="hint pd-cur" style="margin:8px 0 0;word-break:break-all"></div>
|
|
226
|
+
<div class="dir-list"><div class="empty">加载中…</div></div>
|
|
227
|
+
<div class="savebar">
|
|
228
|
+
<span class="hint pd-note"></span><span style="flex:1"></span>
|
|
229
|
+
<button class="btn pd-cancel">取消</button>
|
|
230
|
+
<button class="btn primary pd-ok">选择此目录</button>
|
|
231
|
+
</div>
|
|
232
|
+
</div>`;
|
|
233
|
+
document.body.appendChild(mask);
|
|
234
|
+
const list = mask.querySelector('.dir-list');
|
|
235
|
+
const cur = mask.querySelector('.pd-cur');
|
|
236
|
+
const upBtn = mask.querySelector('.pd-up');
|
|
237
|
+
const okBtn = mask.querySelector('.pd-ok');
|
|
238
|
+
const note = mask.querySelector('.pd-note');
|
|
239
|
+
const jumpInput = mask.querySelector('.pd-jump');
|
|
240
|
+
let current = ''; // '' = 盘符/根层(此层不可作为选择结果)
|
|
241
|
+
let currentParent = null;
|
|
242
|
+
const close = (val) => { mask.remove(); resolveP(val); };
|
|
243
|
+
const render = (r) => {
|
|
244
|
+
current = r.path || '';
|
|
245
|
+
currentParent = r.parent;
|
|
246
|
+
cur.textContent = r.drives ? '此电脑(选择一个盘符)' : (r.path || '');
|
|
247
|
+
upBtn.hidden = !r.parent;
|
|
248
|
+
okBtn.disabled = !current;
|
|
249
|
+
note.textContent = r.error || '';
|
|
250
|
+
const items = r.drives
|
|
251
|
+
? r.drives.map((d) => ({ label: d, next: d }))
|
|
252
|
+
: (r.dirs || []).map((d) => ({ label: d, next: joinPath(current, d) }));
|
|
253
|
+
list.innerHTML = items.length
|
|
254
|
+
? items.map((it) => `<button data-next="${esc(it.next)}">📁 ${esc(it.label)}</button>`).join('')
|
|
255
|
+
: `<div class="empty">${r.error ? '(上述路径无法列出子目录)' : '(没有子目录)'}</div>`;
|
|
256
|
+
list.querySelectorAll('button[data-next]').forEach((b) => b.onclick = () => go(b.dataset.next));
|
|
257
|
+
};
|
|
258
|
+
const go = async (p) => {
|
|
259
|
+
list.innerHTML = '<div class="empty">加载中…</div>';
|
|
260
|
+
try { render(await api('GET', `/api/fs/dirs?p=${encodeURIComponent(p)}`)); }
|
|
261
|
+
catch (e) { list.innerHTML = `<div class="empty">加载失败:${esc(e.message)}</div>`; }
|
|
262
|
+
};
|
|
263
|
+
const jump = () => { const v = jumpInput.value.trim(); if (v) go(v); };
|
|
264
|
+
mask.querySelector('.pd-go').onclick = jump;
|
|
265
|
+
jumpInput.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); jump(); } };
|
|
266
|
+
upBtn.onclick = () => { if (currentParent) go(currentParent); };
|
|
267
|
+
okBtn.onclick = () => { if (current) close(current); };
|
|
268
|
+
mask.querySelector('.pd-cancel').onclick = () => close(null);
|
|
269
|
+
mask.onclick = (e) => { if (e.target === mask) close(null); };
|
|
270
|
+
// 初始位置:现值路径存在则从它开始,否则停在盘符/根层(跳转框预填现值方便改错)
|
|
271
|
+
jumpInput.value = initialPath || '';
|
|
272
|
+
go(initialPath || '');
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
174
276
|
// ============ Tab 注册 ============
|
|
175
277
|
// 菜单图标:内联 SVG 线性图标(Lucide 风格,stroke:currentColor 自动跟随选中态配色)
|
|
176
278
|
const ico = (inner) => `<span class="ico"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${inner}</svg></span>`;
|
|
@@ -262,7 +364,12 @@ function renderBootstrap() {
|
|
|
262
364
|
<h3 style="margin-top:20px">② 本机工作区(Claude 的工作目录)</h3>
|
|
263
365
|
<div class="row">
|
|
264
366
|
<div><label>名字(如 demo)</label><input type="text" id="bsWsName" placeholder="demo"></div>
|
|
265
|
-
<div><label>本机绝对路径</label
|
|
367
|
+
<div><label>本机绝对路径</label>
|
|
368
|
+
<div class="input-btn">
|
|
369
|
+
<input type="text" id="bsWsPath" placeholder="F:\\workspace\\demo">
|
|
370
|
+
<button class="btn sm" id="bsWsBrowse" title="浏览选择目录">浏览</button>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
266
373
|
</div>
|
|
267
374
|
<h3 style="margin-top:20px">③ Claude 认证(可稍后在「Claude 认证」页配置)</h3>
|
|
268
375
|
<div class="radio-row">
|
|
@@ -300,6 +407,10 @@ function renderBootstrap() {
|
|
|
300
407
|
};
|
|
301
408
|
addAppRow();
|
|
302
409
|
$('#bsAddApp').onclick = addAppRow;
|
|
410
|
+
$('#bsWsBrowse').onclick = async () => {
|
|
411
|
+
const picked = await pickDirectory($('#bsWsPath').value.trim());
|
|
412
|
+
if (picked != null) $('#bsWsPath').value = picked;
|
|
413
|
+
};
|
|
303
414
|
document.querySelectorAll('input[name=bsMode]').forEach((r) => r.onchange = () => {
|
|
304
415
|
$('#bsManaged').style.display = $('[name=bsMode]:checked').value === 'managed' ? '' : 'none';
|
|
305
416
|
});
|
|
@@ -782,7 +893,10 @@ function renderWorkspaces(el) {
|
|
|
782
893
|
const tr = document.createElement('tr');
|
|
783
894
|
tr.innerHTML = `
|
|
784
895
|
<td><input type="text" data-f="name" value="${esc(ws.name || '')}"></td>
|
|
785
|
-
<td><
|
|
896
|
+
<td><div class="input-btn">
|
|
897
|
+
<input type="text" data-f="path" value="${esc(ws.path || '')}">
|
|
898
|
+
<button class="btn sm" data-browse="path" title="浏览选择目录">浏览</button>
|
|
899
|
+
</div></td>
|
|
786
900
|
<td><select data-f="type">
|
|
787
901
|
<option value="generic" ${ws.type !== 'code-dev' ? 'selected' : ''}>通用(generic)</option>
|
|
788
902
|
<option value="code-dev" ${ws.type === 'code-dev' ? 'selected' : ''}>开发工作流(code-dev)</option>
|
|
@@ -810,6 +924,14 @@ function renderWorkspaces(el) {
|
|
|
810
924
|
cardDirty('ws', true);
|
|
811
925
|
renderWorkspaces(el);
|
|
812
926
|
};
|
|
927
|
+
// 浏览选目录:回填后手动同步 ws.path(直接改 input.value 不触发 oninput)
|
|
928
|
+
tr.querySelector('[data-browse]').onclick = async () => {
|
|
929
|
+
const picked = await pickDirectory(ws.path || '');
|
|
930
|
+
if (picked == null) return;
|
|
931
|
+
ws.path = picked;
|
|
932
|
+
tr.querySelector('input[data-f=path]').value = picked;
|
|
933
|
+
cardDirty('ws', true);
|
|
934
|
+
};
|
|
813
935
|
body.appendChild(tr);
|
|
814
936
|
};
|
|
815
937
|
doc.workspaces.forEach(renderRow);
|
|
@@ -855,6 +977,7 @@ function renderClaude(el) {
|
|
|
855
977
|
<tr><td>模型</td><td>${cur.model ? `<code>${esc(cur.model)}</code>` : '<span class="hint">未设置(CLI 默认)</span>'}</td></tr>
|
|
856
978
|
<tr><td>Auth Token</td><td>${hint(cur.authToken)}</td></tr>
|
|
857
979
|
<tr><td>API Key</td><td>${hint(cur.apiKey)}</td></tr>
|
|
980
|
+
<tr><td>自定义环境变量</td><td>${cur.envCount ? `${cur.envCount} 个(写入托管 settings.json env 块,MCP 工具可用)` : '<span class="hint">无</span>'}</td></tr>
|
|
858
981
|
<tr><td>settings.json</td><td><code>${esc(S.status?.claude?.settingsPath || '—')}</code></td></tr>
|
|
859
982
|
</table>
|
|
860
983
|
${mode !== 'managed' ? '<div class="desc" style="margin-top:8px">inherit 模式上表为本机 ~/.claude 实际状态,本页不修改本机登录。要管理多厂商档案并一键切换,请切到 bridge 托管。</div>' : ''}
|
|
@@ -888,6 +1011,10 @@ function renderClaude(el) {
|
|
|
888
1011
|
<div class="hint" style="margin-top:6px">「拉取模型」用当前表单的 BASE_URL 与凭证(未填则用已保存值)请求模型列表:自动兼容带 / 不带 /v1 的地址与 Bearer / x-api-key 认证;失败可手动填写。</div>
|
|
889
1012
|
</div>
|
|
890
1013
|
</details>
|
|
1014
|
+
<h4 style="margin:14px 0 2px;font-size:13.5px">环境变量(写入托管 settings.json,MCP 工具依赖的自定义变量在此配置)</h4>
|
|
1015
|
+
<div class="hint" style="margin-bottom:6px">本机 ~/.claude/settings.json 已有的非认证键会自动继承;此处配置的键<b>优先级更高</b>(覆盖继承值)。认证相关(ANTHROPIC_AUTH_TOKEN / API_KEY / BASE_URL / MODEL)不在此生效,请用上方认证表单。</div>
|
|
1016
|
+
<table><thead><tr><th style="width:38%">KEY</th><th>VALUE</th><th style="width:44px"></th></tr></thead><tbody id="claudeEnvBody"></tbody></table>
|
|
1017
|
+
<button class="btn sm" id="claudeEnvAdd" style="margin-top:6px">+ 添加变量</button>
|
|
891
1018
|
${saveBarHtml('claude')}
|
|
892
1019
|
</div>
|
|
893
1020
|
<div class="footer-note">managed 模式把认证写入 <code>~/.lark-claudecode-bridge/claude/settings.json</code>,与本机 ~/.claude 完全隔离;认证/模型改动保存后对后续任务即生效,模式切换需重启。</div>
|
|
@@ -921,6 +1048,42 @@ function renderClaude(el) {
|
|
|
921
1048
|
};
|
|
922
1049
|
$('#baseUrl').oninput = () => { c.base_url = $('#baseUrl').value.trim() || undefined; cardDirty('claude', true); };
|
|
923
1050
|
$('#model').oninput = () => { c.model = $('#model').value.trim() || undefined; cardDirty('claude', true); };
|
|
1051
|
+
// ---- 环境变量行编辑(claude.env):对象 ↔ 行数组,KEY/VALUE 实时写回(同 apps 抽屉 env 模式) ----
|
|
1052
|
+
const cEnvRows = Object.entries(c.env || {}).map(([key, value]) => ({ key, value: String(value) }));
|
|
1053
|
+
const syncClaudeEnv = () => {
|
|
1054
|
+
const obj = {};
|
|
1055
|
+
for (const r of cEnvRows) if (r.key.trim()) obj[r.key.trim()] = r.value;
|
|
1056
|
+
if (Object.keys(obj).length) c.env = obj; else delete c.env;
|
|
1057
|
+
};
|
|
1058
|
+
const renderClaudeEnv = () => {
|
|
1059
|
+
const tb = $('#claudeEnvBody');
|
|
1060
|
+
if (!tb) return;
|
|
1061
|
+
tb.innerHTML = cEnvRows.length ? cEnvRows.map((r, i) => `
|
|
1062
|
+
<tr>
|
|
1063
|
+
<td><input type="text" data-cei="${i}" data-col="key" value="${esc(r.key)}" placeholder="SOME_KEY"></td>
|
|
1064
|
+
<td><input type="text" data-cei="${i}" data-col="value" value="${esc(r.value)}" placeholder="some-value"></td>
|
|
1065
|
+
<td><button class="btn sm danger" data-crm="${i}">删</button></td>
|
|
1066
|
+
</tr>`).join('') : '<tr><td colspan="3" class="hint">未配置(本机 ~/.claude 的非认证键自动继承)</td></tr>';
|
|
1067
|
+
tb.querySelectorAll('input[data-cei]').forEach((input) => input.oninput = () => {
|
|
1068
|
+
cEnvRows[Number(input.dataset.cei)][input.dataset.col] = input.value;
|
|
1069
|
+
syncClaudeEnv();
|
|
1070
|
+
cardDirty('claude', true);
|
|
1071
|
+
});
|
|
1072
|
+
tb.querySelectorAll('button[data-crm]').forEach((b) => b.onclick = () => {
|
|
1073
|
+
cEnvRows.splice(Number(b.dataset.crm), 1);
|
|
1074
|
+
syncClaudeEnv();
|
|
1075
|
+
cardDirty('claude', true);
|
|
1076
|
+
renderClaudeEnv();
|
|
1077
|
+
});
|
|
1078
|
+
};
|
|
1079
|
+
const envAdd = $('#claudeEnvAdd');
|
|
1080
|
+
if (envAdd) envAdd.onclick = () => { cEnvRows.push({ key: '', value: '' }); renderClaudeEnv(); tbFocusLastKey(); };
|
|
1081
|
+
const tbFocusLastKey = () => {
|
|
1082
|
+
const inputs = document.querySelectorAll('#claudeEnvBody input[data-col=key]');
|
|
1083
|
+
const last = inputs[inputs.length - 1];
|
|
1084
|
+
if (last) last.focus();
|
|
1085
|
+
};
|
|
1086
|
+
renderClaudeEnv();
|
|
924
1087
|
$('#btnModels').onclick = () => {
|
|
925
1088
|
const typed = $('#credVal').value.trim();
|
|
926
1089
|
const key = document.querySelector('input[name=cred]:checked').value;
|
|
@@ -1207,7 +1370,7 @@ function renderSlash(el) {
|
|
|
1207
1370
|
<button class="btn" id="scBuiltins">补齐内置命令</button>
|
|
1208
1371
|
</div>
|
|
1209
1372
|
<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>
|
|
1210
|
-
<tbody id="scBody"><tr><td colspan="4" class="hint"
|
|
1373
|
+
<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>
|
|
1211
1374
|
<pre class="report" id="scReport" style="display:none;margin-top:12px"></pre>
|
|
1212
1375
|
</div>`;
|
|
1213
1376
|
const appSel = $('#scApp');
|
|
@@ -1223,7 +1386,7 @@ function renderSlash(el) {
|
|
|
1223
1386
|
};
|
|
1224
1387
|
const loadRemote = async () => {
|
|
1225
1388
|
if (!appSel.value) { $('#scBody').innerHTML = '<tr><td colspan="4" class="hint">无可用应用(先到「飞书应用」页配置并保存)</td></tr>'; return; }
|
|
1226
|
-
$('#scBody').innerHTML = '<tr><td colspan="4" class="hint"
|
|
1389
|
+
$('#scBody').innerHTML = '<tr><td colspan="4" style="padding:36px 0"><div class="br-spin-sm"></div><div class="hint" style="text-align:center">正在从飞书拉取斜杆命令…</div></td></tr>';
|
|
1227
1390
|
try {
|
|
1228
1391
|
const r = await api('GET', `/api/slash-commands/remote?app=${encodeURIComponent(appSel.value)}`);
|
|
1229
1392
|
$('#scBody').innerHTML = r.remote.map((x) => `
|
|
@@ -1319,21 +1482,27 @@ async function renderPlugins(el) {
|
|
|
1319
1482
|
<div class="card">
|
|
1320
1483
|
<h3>Claude Code 插件</h3>
|
|
1321
1484
|
<div class="desc">两处目录的已启用插件都会自动加载(managed 托管目录优先):本机 <code>~/.claude</code>(来源「本机」,与本机 claude CLI 共用)与 bridge 托管目录 <code id="pluginDir">…</code>。装好后下一条消息自动加载。飞书端同样可用 <code>/plugin install xxx@marketplace</code>(仅管理员)。</div>
|
|
1322
|
-
<table><thead><tr><th>插件</th><th>市场</th><th>来源</th><th>版本</th><th style="width:90px">状态</th><th style="width:
|
|
1485
|
+
<table><thead><tr><th>插件</th><th>市场</th><th>来源</th><th>版本</th><th style="width:90px">状态</th><th style="width:250px">操作</th></tr></thead>
|
|
1323
1486
|
<tbody id="plBody"><tr><td colspan="6" class="hint">加载中…</td></tr></tbody></table>
|
|
1324
1487
|
</div>
|
|
1325
1488
|
<div class="card">
|
|
1326
1489
|
<h3>安装 / 市场</h3>
|
|
1327
|
-
<div class="
|
|
1328
|
-
|
|
1490
|
+
<div class="desc">安装来源为已添加的市场:新插件先「添加市场」,再从下拉选择安装。</div>
|
|
1491
|
+
<label>从市场安装插件</label>
|
|
1492
|
+
<div class="row" style="max-width:760px">
|
|
1493
|
+
<div class="combo">
|
|
1494
|
+
<input type="text" id="plInstall" placeholder="(加载中…)" autocomplete="off">
|
|
1495
|
+
<div class="combo-panel" id="plInstallPanel" hidden></div>
|
|
1496
|
+
</div>
|
|
1329
1497
|
<select id="plDir" style="max-width:170px">
|
|
1330
1498
|
<option value="user" selected>装到本机 ~/.claude</option>
|
|
1331
1499
|
<option value="bridge">装到 bridge 托管目录</option>
|
|
1332
1500
|
</select>
|
|
1333
1501
|
<button class="btn primary" id="plDoInstall">安装</button>
|
|
1334
1502
|
</div>
|
|
1335
|
-
<div class="hint" style="margin:6px 0">默认装本机 ~/.claude:本机 claude CLI
|
|
1336
|
-
<
|
|
1503
|
+
<div class="hint" style="margin:6px 0">默认装本机 ~/.claude:本机 claude CLI 与桥接器两处共用一份,无须安装两次。装好后下一条消息自动加载。</div>
|
|
1504
|
+
<label style="margin-top:14px">管理市场</label>
|
|
1505
|
+
<div class="row" style="max-width:760px">
|
|
1337
1506
|
<input type="text" id="plMarket" placeholder="marketplace git 地址或本机路径">
|
|
1338
1507
|
<select id="plMarketDir" style="max-width:170px">
|
|
1339
1508
|
<option value="user" selected>本机 ~/.claude</option>
|
|
@@ -1342,51 +1511,235 @@ async function renderPlugins(el) {
|
|
|
1342
1511
|
<button class="btn" id="plDoMarketAdd">添加市场</button>
|
|
1343
1512
|
<button class="btn" id="plDoMarketUpdate">全部更新</button>
|
|
1344
1513
|
</div>
|
|
1514
|
+
<div class="hint" style="margin:6px 0">「全部更新」= 刷新市场索引 + 逐个更新该目录所有已装插件。</div>
|
|
1345
1515
|
<pre class="report" id="plReport" style="display:none;margin-top:12px"></pre>
|
|
1346
1516
|
</div>`;
|
|
1347
1517
|
const report = (title, text) => {
|
|
1348
1518
|
$('#plReport').style.display = '';
|
|
1349
1519
|
$('#plReport').textContent = `${title}\n${text}`;
|
|
1350
1520
|
};
|
|
1521
|
+
let available = []; // 当前目录可装清单(扁平化,combobox 数据源)
|
|
1522
|
+
let selectedInstall = ''; // 已选中安装参数 name@marketplace(输入改动即失效)
|
|
1523
|
+
let installed = []; // 已装清单缓存(update-all 前端编排取该目录步骤用)
|
|
1524
|
+
const busyText = {
|
|
1525
|
+
install: '安装中…', uninstall: '卸载中…', update: '更新中…', 'update-all': '更新中…',
|
|
1526
|
+
'marketplace-add': '添加中…', 'marketplace-remove': '移除中…', enable: '处理中…', disable: '处理中…',
|
|
1527
|
+
};
|
|
1528
|
+
const opNames = { install: '安装', uninstall: '卸载', update: '更新', 'update-all': '全部更新', 'marketplace-add': '添加市场', 'marketplace-remove': '移除市场', enable: '启用', disable: '停用' };
|
|
1351
1529
|
const load = async () => {
|
|
1352
1530
|
try {
|
|
1353
1531
|
const r = await api('GET', '/api/plugins');
|
|
1532
|
+
installed = r.plugins || [];
|
|
1354
1533
|
$('#pluginDir').textContent = r.configDir;
|
|
1355
1534
|
const body = $('#plBody');
|
|
1356
|
-
if (!
|
|
1535
|
+
if (!installed.length) {
|
|
1357
1536
|
body.innerHTML = '<tr><td colspan="6" class="hint">尚未安装任何插件</td></tr>';
|
|
1358
1537
|
return;
|
|
1359
1538
|
}
|
|
1360
|
-
body.innerHTML =
|
|
1539
|
+
body.innerHTML = installed.map((p) => `
|
|
1361
1540
|
<tr>
|
|
1362
1541
|
<td><b>${esc(p.name)}</b></td>
|
|
1363
1542
|
<td>${esc(p.marketplace || '-')}</td>
|
|
1364
1543
|
<td>${p.source === 'bridge' ? '<span class="tag warn">bridge 托管</span>' : '<span class="tag off">本机 ~/.claude</span>'}</td>
|
|
1365
|
-
<td class="hint">${esc(p.version || '-')}</td>
|
|
1544
|
+
<td class="hint">${esc(p.version || '-')}${p.latestVersion ? ` <span class="tag warn">→ ${esc(p.latestVersion)}</span>` : ''}</td>
|
|
1366
1545
|
<td>${p.enabled ? '<span class="tag ok">已启用</span>' : '<span class="tag off">未启用</span>'}</td>
|
|
1367
1546
|
<td>
|
|
1547
|
+
<button class="btn sm" data-op="update" data-arg="${esc(p.key)}" data-dir="${p.source}">更新</button>
|
|
1368
1548
|
<button class="btn sm" data-op="${p.enabled ? 'disable' : 'enable'}" data-arg="${esc(p.key)}" data-dir="${p.source}">${p.enabled ? '停用' : '启用'}</button>
|
|
1369
1549
|
<button class="btn sm danger" data-op="uninstall" data-arg="${esc(p.key)}" data-dir="${p.source}">卸载</button>
|
|
1370
1550
|
</td>
|
|
1371
1551
|
</tr>`).join('');
|
|
1372
|
-
body.querySelectorAll('button[data-op]').forEach((b) => b.onclick = () =>
|
|
1552
|
+
body.querySelectorAll('button[data-op]').forEach((b) => b.onclick = () => {
|
|
1553
|
+
// 慢操作走进度弹框;亚秒级启停走轻量 act(弹框一闪而过反而干扰)
|
|
1554
|
+
if (b.dataset.op === 'enable' || b.dataset.op === 'disable') act(b.dataset.op, b.dataset.arg, b.dataset.dir, b);
|
|
1555
|
+
else runOpDialog(b.dataset.op, b.dataset.arg, b.dataset.dir, b);
|
|
1556
|
+
});
|
|
1373
1557
|
} catch (e) {
|
|
1558
|
+
installed = [];
|
|
1374
1559
|
$('#plBody').innerHTML = `<tr><td colspan="6" class="hint">加载失败:${esc(e.message)}</td></tr>`;
|
|
1375
1560
|
}
|
|
1376
1561
|
};
|
|
1377
|
-
|
|
1378
|
-
|
|
1562
|
+
/** 轻量操作(enable/disable 亚秒级):按钮瞬时 loading + toast,不弹框 */
|
|
1563
|
+
const act = async (op, arg, dir, btn) => {
|
|
1564
|
+
const pageBtns = el.querySelectorAll('button');
|
|
1565
|
+
const old = btn ? btn.textContent : '';
|
|
1566
|
+
pageBtns.forEach((b) => { b.disabled = true; });
|
|
1567
|
+
if (btn) btn.textContent = busyText[op] || '处理中…';
|
|
1379
1568
|
try {
|
|
1380
1569
|
const r = await api('POST', '/api/plugins/action', { op, arg, ...(dir ? { dir } : {}) });
|
|
1381
1570
|
report(r.ok ? `✅ ${op} 完成` : `❌ ${op} 失败`, r.text);
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
} catch (e) {
|
|
1571
|
+
await load();
|
|
1572
|
+
toast(r.ok ? `${opNames[op] || op}完成,下一条消息生效` : `${opNames[op] || op}失败:${r.text.slice(0, 120)}`, !r.ok);
|
|
1573
|
+
} catch (e) {
|
|
1574
|
+
report(`❌ ${op} 失败`, e.message);
|
|
1575
|
+
toast(e.message, true);
|
|
1576
|
+
} finally {
|
|
1577
|
+
pageBtns.forEach((b) => { b.disabled = false; });
|
|
1578
|
+
if (btn) btn.textContent = old;
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
/**
|
|
1582
|
+
* 慢操作进度弹框(install/update/uninstall/marketplace-add/update-all):
|
|
1583
|
+
* spinner + 进度条 + 逐步日志。update-all 走前端编排(marketplace-update + 逐插件
|
|
1584
|
+
* update,每步一请求)→ 进度真实(i/N);单操作是单请求无内部进度 → 时间缓动模拟
|
|
1585
|
+
* (前 2s 直线推进,之后渐缓封顶 90%,完成跳 100%)。进行中不可关闭;完成后列表刷新。
|
|
1586
|
+
*/
|
|
1587
|
+
const runOpDialog = async (op, arg, dir, btn) => {
|
|
1588
|
+
const dirLabel = dir === 'bridge' ? 'bridge 托管目录' : '本机 ~/.claude';
|
|
1589
|
+
const title = op === 'update-all'
|
|
1590
|
+
? `全部更新(${dirLabel})`
|
|
1591
|
+
: `${opNames[op] || op} ${arg}${dir ? `(${dirLabel})` : ''}`;
|
|
1592
|
+
// update-all 编排步骤:刷新市场索引 + 该目录逐个已装插件(失败不中断,与后端聚合语义一致)
|
|
1593
|
+
const steps = op === 'update-all'
|
|
1594
|
+
? [{ op: 'marketplace-update', arg: '', label: '刷新市场索引' },
|
|
1595
|
+
...installed.filter((p) => p.source === dir).map((p) => ({ op: 'update', arg: p.key, label: `更新 ${p.key}` }))]
|
|
1596
|
+
: [{ op, arg, label: `${opNames[op] || op} ${arg}` }];
|
|
1597
|
+
// 弹框 DOM(遮罩不可点击关闭;关闭按钮完成前不存在)
|
|
1598
|
+
const mask = document.createElement('div');
|
|
1599
|
+
mask.className = 'modal-mask';
|
|
1600
|
+
mask.innerHTML = `
|
|
1601
|
+
<div class="modal" role="dialog" aria-modal="true" style="width:min(640px,94vw)">
|
|
1602
|
+
<div style="display:flex;align-items:center;gap:10px">
|
|
1603
|
+
<div class="br-spin" style="margin:0"></div>
|
|
1604
|
+
<b id="podTitle">${esc(title)}</b>
|
|
1605
|
+
</div>
|
|
1606
|
+
<div class="prog"><div class="prog-bar" id="podBar"></div></div>
|
|
1607
|
+
<pre class="report" id="podLog" style="margin-top:12px;max-height:220px">正在执行 claude plugin …(安装/更新可能需要拉取仓库,请稍候)</pre>
|
|
1608
|
+
<div class="savebar" style="justify-content:flex-end">
|
|
1609
|
+
<button class="btn primary" id="podClose" hidden>关闭</button>
|
|
1610
|
+
</div>
|
|
1611
|
+
</div>`;
|
|
1612
|
+
document.body.appendChild(mask);
|
|
1613
|
+
const bar = mask.querySelector('#podBar');
|
|
1614
|
+
const log = mask.querySelector('#podLog');
|
|
1615
|
+
const spin = mask.querySelector('.br-spin');
|
|
1616
|
+
const titleEl = mask.querySelector('#podTitle');
|
|
1617
|
+
const closeBtn = mask.querySelector('#podClose');
|
|
1618
|
+
const setBar = (pct) => { bar.style.width = `${pct}%`; };
|
|
1619
|
+
const appendLog = (line) => { log.textContent += `\n${line}`; log.scrollTop = log.scrollHeight; };
|
|
1620
|
+
// 单请求无内部进度:缓动模拟;update-all 用真实 i/N 不走此定时器
|
|
1621
|
+
let fakePct = 0;
|
|
1622
|
+
const startedAt = Date.now();
|
|
1623
|
+
const timer = op === 'update-all' ? null : setInterval(() => {
|
|
1624
|
+
const elapsed = Date.now() - startedAt;
|
|
1625
|
+
fakePct = elapsed < 2000 ? Math.min(70, (elapsed / 2000) * 70) : Math.min(90, fakePct + 1);
|
|
1626
|
+
setBar(fakePct);
|
|
1627
|
+
}, 300);
|
|
1628
|
+
// 操作互斥 + 被点按钮进行时文案(遮罩之外的双保险)
|
|
1629
|
+
const pageBtns = el.querySelectorAll('button');
|
|
1630
|
+
const old = btn ? btn.textContent : '';
|
|
1631
|
+
pageBtns.forEach((b) => { b.disabled = true; });
|
|
1632
|
+
if (btn) btn.textContent = busyText[op] || '处理中…';
|
|
1633
|
+
report(`执行中:${op} ${arg}${dir ? `(${dirLabel})` : ''}`, '…(安装/更新可能需要拉取仓库,请稍候)');
|
|
1634
|
+
const close = async () => { mask.remove(); await Promise.all([load(), loadAvailable()]); };
|
|
1635
|
+
try {
|
|
1636
|
+
let allOk = true;
|
|
1637
|
+
const lines = [];
|
|
1638
|
+
for (let i = 0; i < steps.length; i++) {
|
|
1639
|
+
const s = steps[i];
|
|
1640
|
+
if (op === 'update-all') setBar(Math.round(((i + 1) / steps.length) * 100));
|
|
1641
|
+
const r = await api('POST', '/api/plugins/action', { op: s.op, arg: s.arg, ...(dir ? { dir } : {}) });
|
|
1642
|
+
const line = `${r.ok ? '✅' : '❌'} ${s.label}${r.ok ? '' : `:${(r.text || '').split('\n').slice(0, 3).join(' ⏎ ').slice(0, 300)}`}`;
|
|
1643
|
+
lines.push(line);
|
|
1644
|
+
appendLog(line);
|
|
1645
|
+
if (!r.ok) allOk = false;
|
|
1646
|
+
}
|
|
1647
|
+
if (timer) clearInterval(timer);
|
|
1648
|
+
setBar(100);
|
|
1649
|
+
bar.classList.add('done');
|
|
1650
|
+
spin.style.display = 'none';
|
|
1651
|
+
titleEl.textContent = `${allOk ? '✅' : '❌'} ${title} · ${allOk ? '完成' : '部分失败(详见日志与底部全文)'}`;
|
|
1652
|
+
report(allOk ? `✅ ${op} 完成` : `❌ ${op} 部分失败`, lines.join('\n'));
|
|
1653
|
+
closeBtn.hidden = false;
|
|
1654
|
+
closeBtn.onclick = () => void close();
|
|
1655
|
+
} catch (e) {
|
|
1656
|
+
if (timer) clearInterval(timer);
|
|
1657
|
+
setBar(100);
|
|
1658
|
+
spin.style.display = 'none';
|
|
1659
|
+
titleEl.textContent = `❌ ${title} · 失败`;
|
|
1660
|
+
appendLog(`❌ 请求失败:${e.message}`);
|
|
1661
|
+
report(`❌ ${op} 失败`, e.message);
|
|
1662
|
+
closeBtn.hidden = false;
|
|
1663
|
+
closeBtn.onclick = () => void close();
|
|
1664
|
+
} finally {
|
|
1665
|
+
pageBtns.forEach((b) => { b.disabled = false; });
|
|
1666
|
+
if (btn) btn.textContent = old;
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
/** 安装 combobox 数据:该目录已添加市场里的可安装插件(扁平化 + 关键词过滤面板) */
|
|
1670
|
+
const loadAvailable = async () => {
|
|
1671
|
+
const input = $('#plInstall');
|
|
1672
|
+
if (!input) return;
|
|
1673
|
+
try {
|
|
1674
|
+
const r = await api('GET', `/api/plugins/available?dir=${$('#plDir').value}`);
|
|
1675
|
+
available = [];
|
|
1676
|
+
for (const m of r.marketplaces || []) {
|
|
1677
|
+
for (const p of m.plugins || []) {
|
|
1678
|
+
available.push({ key: `${p.name}@${m.name}`, name: p.name, market: m.name, version: p.version || '', description: p.description || '' });
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
$('#plDoInstall').disabled = !available.length;
|
|
1682
|
+
input.placeholder = available.length ? '输入关键词搜索插件,从结果中选择' : '(请先在下方添加市场)';
|
|
1683
|
+
} catch (e) {
|
|
1684
|
+
available = [];
|
|
1685
|
+
$('#plDoInstall').disabled = true;
|
|
1686
|
+
input.placeholder = `(加载失败:${e.message})`;
|
|
1687
|
+
}
|
|
1688
|
+
input.value = '';
|
|
1689
|
+
selectedInstall = '';
|
|
1690
|
+
hidePanel();
|
|
1691
|
+
};
|
|
1692
|
+
const hidePanel = () => { const p = $('#plInstallPanel'); if (p) p.hidden = true; };
|
|
1693
|
+
const pickInstall = (key) => {
|
|
1694
|
+
selectedInstall = key;
|
|
1695
|
+
$('#plInstall').value = key;
|
|
1696
|
+
hidePanel();
|
|
1697
|
+
};
|
|
1698
|
+
const showMatches = () => {
|
|
1699
|
+
const panel = $('#plInstallPanel');
|
|
1700
|
+
if (!panel) return;
|
|
1701
|
+
const q = $('#plInstall').value.trim().toLowerCase();
|
|
1702
|
+
const hits = q
|
|
1703
|
+
? available.filter((it) => `${it.name} ${it.description} ${it.market}`.toLowerCase().includes(q))
|
|
1704
|
+
: available;
|
|
1705
|
+
const shown = hits.slice(0, 50);
|
|
1706
|
+
panel.hidden = false;
|
|
1707
|
+
panel.innerHTML = shown.length
|
|
1708
|
+
? shown.map((it) => `<div class="combo-item" data-key="${esc(it.key)}"><b>${esc(it.name)}</b><span class="tag off">${esc(it.market)}</span>${it.version ? ` <span class="hint">v${esc(it.version)}</span>` : ''}${it.description ? `<div class="hint">${esc(it.description.slice(0, 80))}</div>` : ''}</div>`).join('')
|
|
1709
|
+
+ (hits.length > shown.length ? `<div class="combo-empty">仅显示前 ${shown.length} 项,继续输入关键词缩小范围</div>` : '')
|
|
1710
|
+
: `<div class="combo-empty">${q ? '无匹配插件' : '(该目录还没有可安装插件)'}</div>`;
|
|
1711
|
+
panel.querySelectorAll('.combo-item').forEach((item) => {
|
|
1712
|
+
item.onmousedown = (e) => { e.preventDefault(); pickInstall(item.dataset.key); }; // mousedown+preventDefault 防输入框 blur 抢先收起面板
|
|
1713
|
+
});
|
|
1714
|
+
};
|
|
1715
|
+
$('#plInstall').oninput = () => { selectedInstall = ''; showMatches(); };
|
|
1716
|
+
$('#plInstall').onfocus = showMatches;
|
|
1717
|
+
$('#plInstall').onkeydown = (e) => {
|
|
1718
|
+
if (e.key === 'Enter') {
|
|
1719
|
+
e.preventDefault();
|
|
1720
|
+
const first = $('#plInstallPanel')?.querySelector('.combo-item');
|
|
1721
|
+
if (first) pickInstall(first.dataset.key);
|
|
1722
|
+
} else if (e.key === 'Escape') hidePanel();
|
|
1723
|
+
};
|
|
1724
|
+
// 点击 combobox 之外收起面板(只注册一次:$ 按 id 查询当前 DOM,重渲染后仍指向新面板)
|
|
1725
|
+
if (!document.body._plComboHideBound) {
|
|
1726
|
+
document.body._plComboHideBound = true;
|
|
1727
|
+
document.addEventListener('click', (e) => {
|
|
1728
|
+
if (!(e.target instanceof Element) || !e.target.closest('.combo')) hidePanel();
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
$('#plDoInstall').onclick = () => {
|
|
1732
|
+
if (!selectedInstall) return toast('请从搜索结果中选择要安装的插件', true);
|
|
1733
|
+
runOpDialog('install', selectedInstall, $('#plDir').value, $('#plDoInstall'));
|
|
1734
|
+
};
|
|
1735
|
+
$('#plDir').onchange = loadAvailable; // 安装目标目录切换 → 可装清单随之变化
|
|
1736
|
+
$('#plDoMarketAdd').onclick = () => {
|
|
1737
|
+
const v = $('#plMarket').value.trim();
|
|
1738
|
+
if (!v) return toast('请填写 marketplace git 地址或本机路径', true);
|
|
1739
|
+
runOpDialog('marketplace-add', v, $('#plMarketDir').value, $('#plDoMarketAdd'));
|
|
1385
1740
|
};
|
|
1386
|
-
$('#
|
|
1387
|
-
|
|
1388
|
-
$('#plDoMarketUpdate').onclick = () => act('marketplace-update', '', $('#plMarketDir').value);
|
|
1389
|
-
await load();
|
|
1741
|
+
$('#plDoMarketUpdate').onclick = () => runOpDialog('update-all', '', $('#plMarketDir').value, $('#plDoMarketUpdate'));
|
|
1742
|
+
await Promise.all([load(), loadAvailable()]);
|
|
1390
1743
|
}
|
|
1391
1744
|
|
|
1392
1745
|
// ============ 初始化 ============
|
package/dist/claude-config.d.ts
CHANGED
|
@@ -9,11 +9,16 @@ export declare function resolveClaudeDir(config: BridgeConfig): string;
|
|
|
9
9
|
* 现有内容整体保留(enabledPlugins / 用户或 CLI 写入的其余键),仅重写 bridge 领土键——
|
|
10
10
|
* env.ANTHROPIC_AUTH_TOKEN / ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL / ANTHROPIC_MODEL 与顶层 model。
|
|
11
11
|
* config.yaml 的 claude 段是这几个键的唯一事实源:字段清空 = 删除对应键。
|
|
12
|
-
* env
|
|
12
|
+
* env 块非领土键的合并优先级(低→高):托管目录既有值 → userEnv(本机 ~/.claude
|
|
13
|
+
* settings env 自动继承,已剔除领土键)→ configEnv(claude.env 显式配置)。
|
|
14
|
+
* 两处入口都过滤领土键:认证与模型由 config 认证字段全权决定,清空即删除的语义
|
|
15
|
+
* 不能被本机值或 env 配置绕过。
|
|
13
16
|
*/
|
|
14
|
-
export declare function buildManagedSettings(existing: unknown, claude: ClaudeConfig): Record<string, unknown>;
|
|
17
|
+
export declare function buildManagedSettings(existing: unknown, claude: ClaudeConfig, userEnv?: Record<string, string>, configEnv?: Record<string, string>): Record<string, unknown>;
|
|
15
18
|
/**
|
|
16
|
-
* managed 模式初始化:建目录 + 把 claude 段最新值合并进 settings.json
|
|
17
|
-
*
|
|
19
|
+
* managed 模式初始化:建目录 + 把 claude 段最新值合并进 settings.json,
|
|
20
|
+
* 并从本机 ~/.claude 继承 MCP servers(.claude.json mcpServers)与自定义环境变量
|
|
21
|
+
* (settings.json env 块,领土键除外)——否则托管会话读不到用户全局 MCP 及其依赖变量。
|
|
22
|
+
* lcb start 启动 bridge 前调用,保证首条消息前认证/环境就位;非 managed 模式为 no-op。
|
|
18
23
|
*/
|
|
19
|
-
export declare function initManagedClaudeDir(config: BridgeConfig): void;
|
|
24
|
+
export declare function initManagedClaudeDir(config: BridgeConfig, userClaudeDir?: string): void;
|