@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
package/assets/web/index.html
CHANGED
|
@@ -3,14 +3,24 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<
|
|
6
|
+
<meta name="color-scheme" content="light dark">
|
|
7
|
+
<title>飞书 Claude Code 桥接器</title>
|
|
8
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect width='24' height='24' rx='6' fill='%233370ff'/%3E%3Cg stroke='%23fff' stroke-width='1.9' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8 7 5.5 9.5 8 12'/%3E%3Cpath d='M5.5 9.5H15'/%3E%3Cpath d='M16 12 18.5 14.5 16 17'/%3E%3Cpath d='M18.5 14.5H9'/%3E%3C/g%3E%3C/svg%3E">
|
|
7
9
|
<link rel="stylesheet" href="/css/main.css">
|
|
8
10
|
</head>
|
|
9
11
|
<body>
|
|
10
12
|
<header>
|
|
11
|
-
<
|
|
13
|
+
<div class="brand">
|
|
14
|
+
<span class="logo" aria-hidden="true"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3 4 7l4 4"/><path d="M4 7h16"/><path d="m16 21 4-4-4-4"/><path d="M20 17H4"/></svg></span>
|
|
15
|
+
<h1>飞书 Claude Code 桥接器</h1>
|
|
16
|
+
</div>
|
|
12
17
|
<span class="meta" id="hMeta">加载中…</span>
|
|
13
18
|
<span class="spacer"></span>
|
|
19
|
+
<div class="theme-switch" id="themeSwitch" role="group" aria-label="界面风格">
|
|
20
|
+
<button data-opt="auto" title="跟随系统"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="3" rx="2"/><path d="M8 21h8"/><path d="M12 17v4"/></svg></button>
|
|
21
|
+
<button data-opt="light" title="亮色风格"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><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"/></svg></button>
|
|
22
|
+
<button data-opt="dark" title="暗色风格"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg></button>
|
|
23
|
+
</div>
|
|
14
24
|
<span class="meta" id="hFirstRun"></span>
|
|
15
25
|
</header>
|
|
16
26
|
<div class="stale-bar" id="staleBar" hidden>⚠️ 配置页服务进程为旧版本(页面文件已更新):进程启停 / 重启 / 一键更新需重启桥接器(<code>lcb start</code> 或页面所在进程)后可用。</div>
|
package/assets/web/js/main.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// 新增配置页:js/pages/ 下新建模块(export const page = { id, title, icon, render(el) })
|
|
3
3
|
// 并在下方 import + PAGES 数组各加一行即可;后端 /js/* 静态服务自动覆盖新文件
|
|
4
4
|
import { S, $, esc, hooks, refresh } from './core.js';
|
|
5
|
-
import { initDrawer, isDrawerOpen, cancelDrawer } from './ui.js';
|
|
5
|
+
import { initDrawer, isDrawerOpen, cancelDrawer, confirmDialog } from './ui.js';
|
|
6
|
+
import { initTheme } from './theme.js';
|
|
6
7
|
import { renderBootstrap } from './bootstrap.js';
|
|
7
8
|
import { page as pOverview } from './pages/overview.js';
|
|
8
9
|
import { page as pApps } from './pages/apps.js';
|
|
@@ -27,35 +28,39 @@ let activeTab = tabFromHash(); // 首载即定位(首次 render 前)
|
|
|
27
28
|
let restoring = false; // 拒绝导航后回写 hash 触发的 hashchange 自抑制标志
|
|
28
29
|
|
|
29
30
|
/** 切页保护:抽屉编辑中 / 页内表单卡有未保存改动 → 确认后才切换(切走即丢弃) */
|
|
30
|
-
function confirmLeave() {
|
|
31
|
+
async function confirmLeave() {
|
|
31
32
|
if (isDrawerOpen()) {
|
|
32
|
-
if (!
|
|
33
|
+
if (!(await confirmDialog({ title: '放弃未保存的修改?', message: '编辑抽屉尚未保存,切换页面将丢弃这些修改。', danger: true, confirmText: '放弃修改' }))) return false;
|
|
33
34
|
cancelDrawer(); // 回滚抽屉编辑(内部 rerender 旧页),再由调用方 render 新页
|
|
34
35
|
} else if (S.dirtyCards.size) {
|
|
35
|
-
if (!
|
|
36
|
+
if (!(await confirmDialog({ title: '放弃未保存的修改?', message: `有 ${S.dirtyCards.size} 处未保存的修改,切换页面将丢弃。`, danger: true, confirmText: '放弃修改' }))) return false;
|
|
36
37
|
S.dirtyCards.clear();
|
|
37
38
|
}
|
|
38
39
|
return true;
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/** 页头(图标徽章 + 标题):注入到 #main 顶部,页面本体渲染进 #pageBody */
|
|
43
|
+
const pageHeadHtml = (p) => `<div class="page-head"><span class="ico"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${p.icon}</svg></span><h2>${p.title}</h2></div><div id="pageBody"></div>`;
|
|
44
|
+
|
|
41
45
|
function render() {
|
|
42
46
|
renderNav();
|
|
43
47
|
if (S.firstRun) return renderBootstrap($('#main')); // 首装向导:无论 hash 是什么都渲染向导
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
const page = pageById.get(activeTab);
|
|
49
|
+
$('#main').innerHTML = pageHeadHtml(page);
|
|
50
|
+
void page.render($('#pageBody')); // fire-and-forget(plugins 页为 async,同旧版)
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
function renderNav() {
|
|
49
54
|
$('#nav').innerHTML = PAGES.map((p) =>
|
|
50
55
|
`<button data-k="${p.id}" class="${p.id === activeTab ? 'active' : ''}">${ico(p.icon)}<span>${p.title}</span></button>`).join('');
|
|
51
56
|
$('#nav').querySelectorAll('button').forEach((b) =>
|
|
52
|
-
b.onclick = () => tryActivate(b.dataset.k));
|
|
57
|
+
b.onclick = () => void tryActivate(b.dataset.k));
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
/** nav 点击与 hashchange 的唯一汇聚点:确认 → 更新 activeTab + 同步 hash → 渲染 */
|
|
56
|
-
function tryActivate(tab) {
|
|
61
|
+
async function tryActivate(tab) {
|
|
57
62
|
if (tab === activeTab) return; // nav 点击后写 hash 引发的 hashchange 在此早退(无双重渲染)
|
|
58
|
-
if (!confirmLeave()) {
|
|
63
|
+
if (!(await confirmLeave())) {
|
|
59
64
|
// 拒绝导航:把 hash 回滚到当前页;回写引发的 hashchange 用 restoring 吞掉
|
|
60
65
|
if (location.hash.slice(1) !== activeTab) { restoring = true; location.hash = activeTab; }
|
|
61
66
|
return;
|
|
@@ -67,11 +72,12 @@ function tryActivate(tab) {
|
|
|
67
72
|
|
|
68
73
|
window.addEventListener('hashchange', () => {
|
|
69
74
|
if (restoring) { restoring = false; return; }
|
|
70
|
-
tryActivate(tabFromHash()); // 覆盖手改地址栏 / 分享链接 / 前进后退
|
|
75
|
+
void tryActivate(tabFromHash()); // 覆盖手改地址栏 / 分享链接 / 前进后退
|
|
71
76
|
});
|
|
72
77
|
|
|
73
78
|
// ---- 启动 ----
|
|
74
79
|
window.__lcbBooted = true; // index.html 兜底脚本标志:module 加载成功
|
|
75
80
|
hooks.rerender = render; // 破环注入:core 的 refresh/cancelDrawer 经 hooks 触发重渲染
|
|
81
|
+
initTheme(); // 主题切换(跟随系统 / 亮色 / 暗色)
|
|
76
82
|
initDrawer(); // 抽屉全局监听(X / ESC / 点遮罩)一次性注册
|
|
77
83
|
refresh().catch((e) => { $('#main').innerHTML = `<div class="card"><h3>加载失败</h3><div class="desc">${esc(e.message)}</div></div>`; });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// ============ 应用(列表 + 抽屉编辑;输入实时写回 S.doc.apps,抽屉「保存」一次落盘) ============
|
|
2
2
|
import { S, $, esc, toast, snapDoc, saveDoc } from '../core.js';
|
|
3
|
-
import { openDrawer, closeDrawer, cancelDrawer } from '../ui.js';
|
|
3
|
+
import { openDrawer, closeDrawer, cancelDrawer, confirmDialog } from '../ui.js';
|
|
4
4
|
|
|
5
5
|
function renderApps(el) {
|
|
6
6
|
const apps = S.doc.apps || (S.doc.apps = []);
|
|
@@ -8,7 +8,7 @@ function renderApps(el) {
|
|
|
8
8
|
<div class="card">
|
|
9
9
|
<h3>飞书应用(机器人)</h3>
|
|
10
10
|
<div class="desc">每个应用一条独立长连接与会话池。点击行或「编辑」在抽屉中配置;App Secret 已脱敏,留空 = 保持不变。凭证 / 名称 / 域名 / 默认工作区 / 并发 / 人格 / 环境变量改动需重启 lcb start;触发词与显式插件热生效。</div>
|
|
11
|
-
<div
|
|
11
|
+
<div class="list-toolbar">
|
|
12
12
|
<button class="btn primary" id="addApp">+ 新增应用</button>
|
|
13
13
|
</div>
|
|
14
14
|
<table>
|
|
@@ -126,7 +126,7 @@ function openAppDrawer(apps, idx, el, snap) {
|
|
|
126
126
|
tb.innerHTML = list.map((item, i) => `
|
|
127
127
|
<tr>
|
|
128
128
|
${def.cols.map(([col, ph]) => `<td><input type="text" data-l="${i}" data-c="${col}" value="${esc(item?.[col] ?? '')}" placeholder="${esc(ph)}"></td>`).join('')}
|
|
129
|
-
<td><button class="btn sm danger" data-rm="${i}"
|
|
129
|
+
<td><button class="btn sm danger" data-rm="${i}">删除</button></td>
|
|
130
130
|
</tr>`).join('');
|
|
131
131
|
tb.querySelectorAll('input[data-l]').forEach((input) => input.oninput = () => {
|
|
132
132
|
app[def.key][Number(input.dataset.l)][input.dataset.c] = input.value;
|
|
@@ -151,7 +151,7 @@ function openAppDrawer(apps, idx, el, snap) {
|
|
|
151
151
|
<tr>
|
|
152
152
|
<td><input type="text" data-ei="${i}" data-c="key" value="${esc(r.key)}" placeholder="SOME_KEY"></td>
|
|
153
153
|
<td><input type="text" data-ei="${i}" data-c="value" value="${esc(r.value)}" placeholder="some-value"></td>
|
|
154
|
-
<td><button class="btn sm danger" data-rm="${i}"
|
|
154
|
+
<td><button class="btn sm danger" data-rm="${i}">删除</button></td>
|
|
155
155
|
</tr>`).join('');
|
|
156
156
|
tb.querySelectorAll('input[data-ei]').forEach((input) => input.oninput = () => {
|
|
157
157
|
envRows[Number(input.dataset.ei)][input.dataset.c] = input.value;
|
|
@@ -176,7 +176,12 @@ function openAppDrawer(apps, idx, el, snap) {
|
|
|
176
176
|
// ---- 删除 / 保存 / 取消 ----
|
|
177
177
|
foot.querySelector('#dwDel').onclick = async () => {
|
|
178
178
|
if (apps.length <= 1) return toast('至少保留一个应用', true);
|
|
179
|
-
if (!
|
|
179
|
+
if (!(await confirmDialog({
|
|
180
|
+
title: '删除应用',
|
|
181
|
+
message: `确认删除应用「${esc(app.name || app.app_id || '未命名')}」?删除将立即保存生效。`,
|
|
182
|
+
danger: true,
|
|
183
|
+
confirmText: '删除',
|
|
184
|
+
}))) return;
|
|
180
185
|
apps.splice(idx, 1);
|
|
181
186
|
if (await saveDoc()) closeDrawer();
|
|
182
187
|
else cancelDrawer(); // 落盘失败:回滚删除,保留编辑现场让用户重试
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// ============ Claude 认证 ============
|
|
2
2
|
import { S, $, esc, toast, api, snapDoc, saveDoc, cardDirty, applyDirty, refresh } from '../core.js';
|
|
3
|
-
import { openDrawer, closeDrawer, cancelDrawer, saveBarHtml, dirtyDotHtml, bindSaveBar } from '../ui.js';
|
|
3
|
+
import { openDrawer, closeDrawer, cancelDrawer, saveBarHtml, dirtyDotHtml, bindSaveBar, confirmDialog } from '../ui.js';
|
|
4
4
|
|
|
5
5
|
/** 模型拉取公共逻辑:getBody() 组请求体(凭证仅携带本次输入的明文,空则不带给后端回落磁盘值) */
|
|
6
6
|
async function pullModels(getBody, btn, datalistId) {
|
|
@@ -80,7 +80,7 @@ function renderClaude(el) {
|
|
|
80
80
|
<div class="card" id="pfCard" style="${mode === 'managed' ? '' : 'display:none'}">
|
|
81
81
|
<h3>厂商档案</h3>
|
|
82
82
|
<div class="desc">保存多套厂商 / 中转站凭证,每个档案可维护多个候选模型(如 fable / opus / sonnet / haiku)。「设为当前」整套切换;点候选模型即时切换当前模型。档案在抽屉「保存」后即可切换。</div>
|
|
83
|
-
<div
|
|
83
|
+
<div class="list-toolbar"><button class="btn primary" id="pfAdd">+ 新增档案</button></div>
|
|
84
84
|
<table><thead><tr><th style="width:120px">名称</th><th style="width:100px">凭证类型</th><th>BASE_URL</th><th>模型(点击切换)</th><th style="width:190px">操作</th></tr></thead>
|
|
85
85
|
<tbody id="pfBody"></tbody></table>
|
|
86
86
|
</div>`;
|
|
@@ -120,7 +120,7 @@ function renderClaude(el) {
|
|
|
120
120
|
<tr>
|
|
121
121
|
<td><input type="text" data-cei="${i}" data-col="key" value="${esc(r.key)}" placeholder="SOME_KEY"></td>
|
|
122
122
|
<td><input type="text" data-cei="${i}" data-col="value" value="${esc(r.value)}" placeholder="some-value"></td>
|
|
123
|
-
<td><button class="btn sm danger" data-crm="${i}"
|
|
123
|
+
<td><button class="btn sm danger" data-crm="${i}">删除</button></td>
|
|
124
124
|
</tr>`).join('') : '<tr><td colspan="3" class="hint">未配置(本机 ~/.claude 的非认证键自动继承)</td></tr>';
|
|
125
125
|
tb.querySelectorAll('input[data-cei]').forEach((input) => input.oninput = () => {
|
|
126
126
|
cEnvRows[Number(input.dataset.cei)][input.dataset.col] = input.value;
|
|
@@ -196,7 +196,12 @@ function renderClaude(el) {
|
|
|
196
196
|
return;
|
|
197
197
|
}
|
|
198
198
|
if (b.dataset.op === 'del') {
|
|
199
|
-
if (!
|
|
199
|
+
if (!(await confirmDialog({
|
|
200
|
+
title: '删除档案',
|
|
201
|
+
message: `确认删除档案「${esc(p.name || '未命名')}」?删除将立即保存生效。`,
|
|
202
|
+
danger: true,
|
|
203
|
+
confirmText: '删除',
|
|
204
|
+
}))) return;
|
|
200
205
|
c.profiles.splice(i, 1);
|
|
201
206
|
await saveDoc();
|
|
202
207
|
return;
|
|
@@ -301,7 +306,12 @@ function openProfileDrawer(c, i, el, snap) {
|
|
|
301
306
|
}), document.getElementById('dwPfPull'), 'dwPfModelList');
|
|
302
307
|
};
|
|
303
308
|
document.getElementById('dwPfDel').onclick = async () => {
|
|
304
|
-
if (!
|
|
309
|
+
if (!(await confirmDialog({
|
|
310
|
+
title: '删除档案',
|
|
311
|
+
message: `确认删除档案「${esc(p.name || '未命名')}」?删除将立即保存生效。`,
|
|
312
|
+
danger: true,
|
|
313
|
+
confirmText: '删除',
|
|
314
|
+
}))) return;
|
|
305
315
|
c.profiles.splice(i, 1);
|
|
306
316
|
if (await saveDoc()) closeDrawer();
|
|
307
317
|
else cancelDrawer();
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// 四来源:bridge 自管(mcp/servers.json,页面管理 + 注入生效)/ 用户级·本机(~/.claude.json,展示+可删)/
|
|
4
4
|
// 项目级(工作区 .mcp.json,只读)/ 已装插件内(<plugin-installPath>/.mcp.json,只读;未启用时标「(未启用)」)。
|
|
5
5
|
import { $, esc, toast, api } from '../core.js';
|
|
6
|
-
import { openDrawer, closeDrawer } from '../ui.js';
|
|
6
|
+
import { openDrawer, closeDrawer, confirmDialog } from '../ui.js';
|
|
7
7
|
|
|
8
8
|
const SOURCE_TAG = {
|
|
9
9
|
bridge: 'bridge · 可管理',
|
|
@@ -23,11 +23,11 @@ function renderMcp(el) {
|
|
|
23
23
|
<div class="card">
|
|
24
24
|
<h3>MCP Servers</h3>
|
|
25
25
|
<div class="desc">stdio 走 command + args(本地进程),http/sse 走 url(远程服务)。页面添加的配置存 bridge 自管目录(见下方路径),运行中的任务即时生效;本机 ~/.claude.json 与工作区 .mcp.json 为只读来源(前者可删);已装插件内 .mcp.json 或 plugin.json 内 mcpServers 字段只读展示。</div>
|
|
26
|
-
<div
|
|
27
|
-
<button class="btn
|
|
28
|
-
<button class="btn
|
|
26
|
+
<div class="list-toolbar">
|
|
27
|
+
<button class="btn" id="mcpRefresh">刷新</button>
|
|
28
|
+
<button class="btn primary" id="mcpAdd">+ 添加 MCP</button>
|
|
29
29
|
</div>
|
|
30
|
-
<input type="search" id="mcpSearch" placeholder="
|
|
30
|
+
<input type="search" class="list-search" id="mcpSearch" placeholder="按名字 / 来源 / 类型 / URL 过滤…">
|
|
31
31
|
<table><thead><tr>
|
|
32
32
|
<th style="width:150px">名字</th><th>类型 / 连接</th><th style="width:130px">来源</th><th style="width:130px">状态</th><th style="width:130px"></th>
|
|
33
33
|
</tr></thead>
|
|
@@ -94,7 +94,7 @@ async function loadMcp() {
|
|
|
94
94
|
<td class="mcp-status" data-status-for="${esc(s.name)}"><span class="desc">未检测</span></td>
|
|
95
95
|
<td>
|
|
96
96
|
<button class="btn sm" data-view="${esc(s.name)}">查看</button>
|
|
97
|
-
${isDeletable(s) ? `<button class="btn sm danger" data-del="${esc(s.name)}"
|
|
97
|
+
${isDeletable(s) ? `<button class="btn sm danger" data-del="${esc(s.name)}">删除</button>` : '<span class="desc">只读</span>'}
|
|
98
98
|
</td>`;
|
|
99
99
|
tr.querySelector('[data-view]').onclick = () => viewMcp(s);
|
|
100
100
|
const delBtn = tr.querySelector('[data-del]');
|
|
@@ -115,25 +115,31 @@ async function checkMcp(s, cell) {
|
|
|
115
115
|
try {
|
|
116
116
|
const r = await api('POST', '/api/mcp/check', { name: s.name, source: s.source, workspaceName: s.workspaceName });
|
|
117
117
|
const map = {
|
|
118
|
-
ok: `<span style="color
|
|
119
|
-
failed: `<span style="color
|
|
120
|
-
unreachable: `<span style="color
|
|
118
|
+
ok: `<span style="color:var(--ok-tx)">✔ 正常</span>`,
|
|
119
|
+
failed: `<span style="color:var(--err-tx)">✘ 异常</span>`,
|
|
120
|
+
unreachable: `<span style="color:var(--warn-tx)">⚠ 不可达</span>`,
|
|
121
121
|
};
|
|
122
122
|
cell.innerHTML = `${map[r.status] || esc(r.status)}<div class="desc" style="font-size:11px;white-space:normal">${esc(r.detail || '')}</div>`;
|
|
123
123
|
} catch (e) {
|
|
124
|
-
cell.innerHTML = `<span style="color
|
|
124
|
+
cell.innerHTML = `<span style="color:var(--err-tx)">检测失败</span>`;
|
|
125
125
|
toast(e.message, true);
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
function delMcp(s) {
|
|
130
|
-
if (!
|
|
129
|
+
async function delMcp(s) {
|
|
130
|
+
if (!(await confirmDialog({
|
|
131
|
+
title: '删除 MCP server',
|
|
132
|
+
message: `确定删除 MCP server <b>${esc(s.name)}</b>(${sourceTag(s)})?删除后引用它的任务将无法启动该 server。`,
|
|
133
|
+
danger: true,
|
|
134
|
+
confirmText: '删除',
|
|
135
|
+
}))) return;
|
|
131
136
|
api('POST', '/api/mcp/action', { op: 'remove', name: s.name, source: s.source })
|
|
132
137
|
.then(() => { toast('已删除'); void loadMcp(); })
|
|
133
138
|
.catch((e) => toast(e.message, true));
|
|
134
139
|
}
|
|
135
140
|
|
|
136
|
-
/** 抽屉查看:完整配置 JSON + env 表(${VAR}
|
|
141
|
+
/** 抽屉查看:完整配置 JSON + env 表(${VAR}/${VAR:-default} 引用已按多来源合并展开;未设置的变量标红)。
|
|
142
|
+
* bridge 来源支持「编辑」:JSON 原文可改,保存走 op:update(任务级热生效,无须重启) */
|
|
137
143
|
function viewMcp(s) {
|
|
138
144
|
const envKeys = Object.keys(s.config.env || {});
|
|
139
145
|
const envRows = envKeys.length
|
|
@@ -142,22 +148,52 @@ function viewMcp(s) {
|
|
|
142
148
|
const raw = String((s.config.env || {})[k] ?? '');
|
|
143
149
|
const cur = (s.resolvedEnv || {})[k] ?? raw;
|
|
144
150
|
return `<tr><td><code>${esc(k)}</code></td><td><code class="desc">${esc(raw)}</code></td>
|
|
145
|
-
<td>${missing ? `<span style="color
|
|
151
|
+
<td>${missing ? `<span style="color:var(--err-tx)">未设置(${esc(cur)})</span>` : `<code>${esc(cur)}</code>`}</td></tr>`;
|
|
146
152
|
}).join('')
|
|
147
153
|
: `<tr><td colspan="3" class="desc">(无 env 配置)</td></tr>`;
|
|
148
154
|
const onlyRead = !isDeletable(s);
|
|
155
|
+
const editable = s.source === 'bridge'; // 后端 op:update 只写 bridge servers.json;machine-user 只能删
|
|
149
156
|
openDrawer({
|
|
150
157
|
title: `MCP · ${s.name}`,
|
|
151
158
|
bodyHtml: `
|
|
152
159
|
<div class="hint" style="margin:0 0 8px">来源:${sourceTag(s)} · 配置文件:<code class="desc">${esc(s.path)}</code></div>
|
|
153
|
-
${onlyRead ? `<div class="hint" style="margin:0 0 8px;color
|
|
154
|
-
|
|
155
|
-
<
|
|
156
|
-
|
|
160
|
+
${onlyRead ? `<div class="hint" style="margin:0 0 8px;color:var(--warn-tx)">只读来源——此 MCP 由 ${s.source === 'plugin' ? '插件' : '项目'}自带,编辑请前往 ${s.source === 'plugin' ? '插件目录' : '对应工作区目录'}手动修改。</div>` : ''}
|
|
161
|
+
${!editable && !onlyRead ? `<div class="hint" style="margin:0 0 8px;color:var(--warn-tx)">用户级·本机来源不支持页面编辑(可删除);如需修改请在 ~/.claude.json 手动改,或删除后到本页重新添加。</div>` : ''}
|
|
162
|
+
<div data-pane="view">
|
|
163
|
+
<label>配置(JSON 原文)</label>
|
|
164
|
+
<textarea readonly rows="10" style="min-height:140px">${esc(JSON.stringify(s.config, null, 2))}</textarea>
|
|
165
|
+
</div>
|
|
166
|
+
<div data-pane="edit" style="display:none">
|
|
167
|
+
<label>配置(JSON,可编辑——env 也在其中改)</label>
|
|
168
|
+
<textarea id="emJson" rows="12" style="min-height:180px">${esc(JSON.stringify(s.config, null, 2))}</textarea>
|
|
169
|
+
<div class="hint" style="margin-top:6px">保存后立即对新任务生效(bridge 自管 servers.json 每任务现读)。</div>
|
|
170
|
+
</div>
|
|
171
|
+
<label>环境变量(引用值 → 当前值;当前值已合并 飞书应用 env › Claude 认证 env › settings.json env › 系统环境,支持 \${VAR:-默认值})</label>
|
|
157
172
|
<table><thead><tr><th style="width:130px">变量</th><th>配置值</th><th>当前值</th></tr></thead><tbody>${envRows}</tbody></table>`,
|
|
158
|
-
footHtml:
|
|
159
|
-
onMount({ foot }) {
|
|
173
|
+
footHtml: `${editable ? '<button class="btn" data-act="edit">编辑</button><button class="btn primary" data-act="save" style="display:none">保存</button>' : ''}<button class="btn" data-act="close">关闭</button>`,
|
|
174
|
+
onMount({ body, foot }) {
|
|
160
175
|
foot.querySelector('[data-act="close"]').onclick = () => closeDrawer();
|
|
176
|
+
const editBtn = foot.querySelector('[data-act="edit"]');
|
|
177
|
+
const saveBtn = foot.querySelector('[data-act="save"]');
|
|
178
|
+
if (editBtn) {
|
|
179
|
+
editBtn.onclick = () => {
|
|
180
|
+
body.querySelector('[data-pane="view"]').style.display = 'none';
|
|
181
|
+
body.querySelector('[data-pane="edit"]').style.display = '';
|
|
182
|
+
editBtn.style.display = 'none';
|
|
183
|
+
saveBtn.style.display = '';
|
|
184
|
+
};
|
|
185
|
+
saveBtn.onclick = async () => {
|
|
186
|
+
const raw = body.querySelector('#emJson').value.trim();
|
|
187
|
+
let config;
|
|
188
|
+
try { config = JSON.parse(raw); } catch (e) { return toast(`JSON 解析失败:${e.message}`, true); }
|
|
189
|
+
try {
|
|
190
|
+
await api('POST', '/api/mcp/action', { op: 'update', name: s.name, config });
|
|
191
|
+
toast('已保存(任务级热生效,无须重启)');
|
|
192
|
+
closeDrawer();
|
|
193
|
+
void loadMcp();
|
|
194
|
+
} catch (e) { toast(e.message, true); }
|
|
195
|
+
};
|
|
196
|
+
}
|
|
161
197
|
},
|
|
162
198
|
});
|
|
163
199
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// ============ 概览 ============
|
|
2
2
|
// 桥接器启停 / 版本更新(宿主进程管理:embedded 页面随进程生死,独立页面经 PID 跨进程操作)
|
|
3
3
|
import { S, $, esc, toast, api, refresh } from '../core.js';
|
|
4
|
+
import { confirmDialog } from '../ui.js';
|
|
4
5
|
|
|
5
6
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6
7
|
const UPD = { checkedAt: 0, data: null };
|
|
@@ -54,9 +55,9 @@ async function startFlow() {
|
|
|
54
55
|
async function stopFlow() {
|
|
55
56
|
const embedded = !!S.status?.embedded;
|
|
56
57
|
const msg = embedded
|
|
57
|
-
? '停止后本配置页将随桥接器进程一同关闭。\n之后可运行 lcb ui
|
|
58
|
-
: '
|
|
59
|
-
if (!
|
|
58
|
+
? '停止后本配置页将随桥接器进程一同关闭。\n之后可运行 lcb ui 重开配置页(并在其中重新启动桥接器)。'
|
|
59
|
+
: '停止后机器人将不再响应消息。';
|
|
60
|
+
if (!(await confirmDialog({ title: '停止桥接器', message: msg, danger: true, confirmText: '停止' }))) return;
|
|
60
61
|
try {
|
|
61
62
|
const r = await bridgeActionSafe('stop');
|
|
62
63
|
if (embedded) {
|
|
@@ -119,7 +120,7 @@ async function doCheckUpdate() {
|
|
|
119
120
|
async function runUpdateFlow() {
|
|
120
121
|
const running = S.status?.embedded || S.status?.bridge?.running;
|
|
121
122
|
const extra = running ? ',完成后自动重启桥接器(页面将短暂失联后自动恢复)' : '(桥接器未在运行,下次启动生效)';
|
|
122
|
-
if (!
|
|
123
|
+
if (!(await confirmDialog({ title: '一键更新', message: `将通过 npm 安装最新版本${extra},可能需要 1-2 分钟。`, confirmText: '开始更新' }))) return;
|
|
123
124
|
const btn = $('#btnUpdRun');
|
|
124
125
|
if (btn) { btn.disabled = true; btn.textContent = '更新中…'; }
|
|
125
126
|
try {
|
|
@@ -1,48 +1,56 @@
|
|
|
1
|
-
// ============
|
|
1
|
+
// ============ 权限(页内一级 tab:免确认工具 / 危险命令黑名单) ============
|
|
2
2
|
import { S, $, esc, toast, saveDoc } from '../core.js';
|
|
3
|
+
import { confirmDialog } from '../ui.js';
|
|
3
4
|
|
|
4
|
-
let permTab = 'allow'; //
|
|
5
|
+
let permTab = 'allow'; // tab 状态:切换 / 重渲染保持
|
|
5
6
|
function renderPermissions(el) {
|
|
6
7
|
const p = S.doc.permissions || (S.doc.permissions = {});
|
|
7
8
|
p.allow_tools = p.allow_tools || [];
|
|
8
9
|
p.dangerous_commands = p.dangerous_commands || [];
|
|
9
10
|
el.innerHTML = `
|
|
10
|
-
<div class="
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
</div>`;
|
|
17
|
-
el.querySelectorAll('.subtabs button').forEach((b) =>
|
|
11
|
+
<div class="tabs">
|
|
12
|
+
<button data-t="allow" class="${permTab === 'allow' ? 'active' : ''}">免确认工具</button>
|
|
13
|
+
<button data-t="danger" class="${permTab === 'danger' ? 'active' : ''}">危险命令黑名单</button>
|
|
14
|
+
</div>
|
|
15
|
+
<div id="permPanel"></div>`;
|
|
16
|
+
el.querySelectorAll('.tabs button').forEach((b) =>
|
|
18
17
|
b.onclick = () => { permTab = b.dataset.t; renderPermissions(el); });
|
|
19
18
|
const panel = $('#permPanel');
|
|
20
|
-
if (permTab === 'danger') renderDangerTab(panel, p
|
|
21
|
-
else renderAllowTab(panel, p
|
|
19
|
+
if (permTab === 'danger') renderDangerTab(panel, p);
|
|
20
|
+
else renderAllowTab(panel, p);
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
function renderAllowTab(panel, p
|
|
23
|
+
function renderAllowTab(panel, p) {
|
|
25
24
|
const defaults = S.status?.permissionDefaults?.allowTools || [];
|
|
26
25
|
const isDefault = defaults.length > 0 && JSON.stringify(p.allow_tools) === JSON.stringify(defaults);
|
|
27
26
|
panel.innerHTML = `
|
|
27
|
+
<div class="card">
|
|
28
|
+
<h3>免确认工具</h3>
|
|
28
29
|
<div class="desc">名单内工具直接放行不弹确认卡(配置即整体替换内置默认)。Bash 仍受危险命令黑名单约束。</div>
|
|
29
|
-
<div class="
|
|
30
|
-
<input type="text" id="newTool" placeholder="工具名,如 Write">
|
|
31
|
-
<
|
|
32
|
-
<
|
|
30
|
+
<div class="list-toolbar">
|
|
31
|
+
<input type="text" id="newTool" placeholder="工具名,如 Write" style="max-width:320px">
|
|
32
|
+
<button class="btn" id="addTool">添加</button>
|
|
33
|
+
<button class="btn" id="resetTools" ${defaults.length ? '' : 'disabled title="status 不可用,无法取默认值"'}>恢复默认</button>
|
|
33
34
|
</div>
|
|
34
|
-
${p.allow_tools.length === 0 ? '<div style="background
|
|
35
|
+
${p.allow_tools.length === 0 ? '<div style="background:var(--warn-bg);color:var(--warn-tx);border-radius:7px;padding:7px 12px;margin:8px 0">⚠️ 名单为空:所有工具(含 Write/Edit)都会弹确认卡</div>' : ''}
|
|
35
36
|
${isDefault ? '<div class="hint" style="margin:8px 0"><span class="tag off">与内置默认一致</span></div>' : ''}
|
|
36
37
|
<table><tbody id="toolBody"></tbody></table>
|
|
37
|
-
<div class="footer-note">权限改动即时保存并热生效(已有会话的下一个工具调用即用新名单)。plan 模式下写操作仍会经此判定:白名单命中直通,未命中弹确认卡。</div
|
|
38
|
+
<div class="footer-note">权限改动即时保存并热生效(已有会话的下一个工具调用即用新名单)。plan 模式下写操作仍会经此判定:白名单命中直通,未命中弹确认卡。</div>
|
|
39
|
+
</div>`;
|
|
38
40
|
$('#toolBody').innerHTML = p.allow_tools.map((t, i) =>
|
|
39
41
|
`<tr><td><code>${esc(t)}</code></td><td style="width:80px"><button class="btn sm danger" data-i="${i}">删除</button></td></tr>`).join('')
|
|
40
42
|
|| '<tr><td colspan="2" class="hint">(空)</td></tr>';
|
|
41
43
|
// 即时保存:低风险高频操作,免去「改完再点保存」两步;失败回滚内存保持一致
|
|
42
44
|
$('#toolBody').querySelectorAll('button').forEach((b) =>
|
|
43
45
|
b.onclick = async () => {
|
|
46
|
+
const i = Number(b.dataset.i);
|
|
47
|
+
if (!(await confirmDialog({
|
|
48
|
+
title: '移出免确认名单',
|
|
49
|
+
message: `确定将工具 <code>${esc(p.allow_tools[i])}</code> 移出免确认名单?移出后该工具的调用将恢复弹确认卡。`,
|
|
50
|
+
confirmText: '移出',
|
|
51
|
+
}))) return;
|
|
44
52
|
const old = [...p.allow_tools];
|
|
45
|
-
p.allow_tools.splice(
|
|
53
|
+
p.allow_tools.splice(i, 1);
|
|
46
54
|
if (!await saveDoc()) p.allow_tools = old;
|
|
47
55
|
});
|
|
48
56
|
$('#addTool').onclick = async () => {
|
|
@@ -54,34 +62,48 @@ function renderAllowTab(panel, p, el) {
|
|
|
54
62
|
};
|
|
55
63
|
$('#resetTools').onclick = async () => {
|
|
56
64
|
if (!defaults.length) return;
|
|
57
|
-
if (!
|
|
65
|
+
if (!(await confirmDialog({
|
|
66
|
+
title: '恢复默认名单',
|
|
67
|
+
message: '恢复为内置默认名单?当前自定义项将被覆盖(立即保存生效)。',
|
|
68
|
+
confirmText: '恢复默认',
|
|
69
|
+
}))) return;
|
|
58
70
|
const old = [...p.allow_tools];
|
|
59
71
|
p.allow_tools = [...defaults];
|
|
60
72
|
if (!await saveDoc()) p.allow_tools = old;
|
|
61
73
|
};
|
|
62
74
|
}
|
|
63
75
|
|
|
64
|
-
function renderDangerTab(panel, p
|
|
76
|
+
function renderDangerTab(panel, p) {
|
|
65
77
|
const defaults = S.status?.permissionDefaults?.dangerousCommands || [];
|
|
66
78
|
const isDefault = defaults.length > 0 && JSON.stringify(p.dangerous_commands) === JSON.stringify(defaults);
|
|
67
79
|
panel.innerHTML = `
|
|
80
|
+
<div class="card">
|
|
81
|
+
<h3>危险命令黑名单</h3>
|
|
68
82
|
<div class="desc">正则源串(不区分大小写),命中即弹确认卡。删除正则 = 放宽对应命令为直通,请谨慎。</div>
|
|
69
|
-
<div class="
|
|
70
|
-
<input type="text" id="newDc" placeholder="正则源串,如 \\bgit\\s+push\\b.*--force">
|
|
71
|
-
<
|
|
72
|
-
<
|
|
83
|
+
<div class="list-toolbar">
|
|
84
|
+
<input type="text" id="newDc" placeholder="正则源串,如 \\bgit\\s+push\\b.*--force" style="max-width:480px">
|
|
85
|
+
<button class="btn" id="addDc">添加</button>
|
|
86
|
+
<button class="btn" id="resetDc" ${defaults.length ? '' : 'disabled title="status 不可用,无法取默认值"'}>恢复默认</button>
|
|
73
87
|
</div>
|
|
74
|
-
${p.dangerous_commands.length === 0 ? '<div style="background
|
|
88
|
+
${p.dangerous_commands.length === 0 ? '<div style="background:var(--err-bg);color:var(--err-tx);border-radius:7px;padding:7px 12px;margin:8px 0">⚠️ 黑名单为空:所有 Bash 命令(含 rm -rf、sudo)将免确认直通,请确认有意为之</div>' : ''}
|
|
75
89
|
${isDefault ? '<div class="hint" style="margin:8px 0"><span class="tag off">与内置默认一致</span></div>' : ''}
|
|
76
90
|
<table><tbody id="dcBody"></tbody></table>
|
|
77
|
-
<div class="footer-note">权限改动即时保存并热生效(已有会话的下一个工具调用即用新名单)。plan 模式下写操作仍会经此判定:白名单命中直通,未命中弹确认卡。</div
|
|
91
|
+
<div class="footer-note">权限改动即时保存并热生效(已有会话的下一个工具调用即用新名单)。plan 模式下写操作仍会经此判定:白名单命中直通,未命中弹确认卡。</div>
|
|
92
|
+
</div>`;
|
|
78
93
|
$('#dcBody').innerHTML = p.dangerous_commands.map((s, i) =>
|
|
79
94
|
`<tr><td style="width:90%"><code>${esc(s)}</code></td><td><button class="btn sm danger" data-i="${i}">删除</button></td></tr>`).join('')
|
|
80
95
|
|| '<tr><td colspan="2" class="hint">(空)</td></tr>';
|
|
81
96
|
$('#dcBody').querySelectorAll('button').forEach((b) =>
|
|
82
97
|
b.onclick = async () => {
|
|
98
|
+
const i = Number(b.dataset.i);
|
|
99
|
+
if (!(await confirmDialog({
|
|
100
|
+
title: '删除黑名单正则',
|
|
101
|
+
message: `确定删除正则 <code>${esc(p.dangerous_commands[i])}</code>?删除后对应命令将免确认直通,请确认有意为之。`,
|
|
102
|
+
danger: true,
|
|
103
|
+
confirmText: '删除',
|
|
104
|
+
}))) return;
|
|
83
105
|
const old = [...p.dangerous_commands];
|
|
84
|
-
p.dangerous_commands.splice(
|
|
106
|
+
p.dangerous_commands.splice(i, 1);
|
|
85
107
|
if (!await saveDoc()) p.dangerous_commands = old;
|
|
86
108
|
});
|
|
87
109
|
$('#addDc').onclick = async () => {
|
|
@@ -94,7 +116,11 @@ function renderDangerTab(panel, p, el) {
|
|
|
94
116
|
};
|
|
95
117
|
$('#resetDc').onclick = async () => {
|
|
96
118
|
if (!defaults.length) return;
|
|
97
|
-
if (!
|
|
119
|
+
if (!(await confirmDialog({
|
|
120
|
+
title: '恢复默认黑名单',
|
|
121
|
+
message: '恢复为内置默认黑名单?当前自定义项将被覆盖(立即保存生效)。',
|
|
122
|
+
confirmText: '恢复默认',
|
|
123
|
+
}))) return;
|
|
98
124
|
const old = [...p.dangerous_commands];
|
|
99
125
|
p.dangerous_commands = [...defaults];
|
|
100
126
|
if (!await saveDoc()) p.dangerous_commands = old;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// ============ 插件 ============
|
|
2
2
|
import { $, esc, toast, api } from '../core.js';
|
|
3
|
+
import { confirmDialog } from '../ui.js';
|
|
3
4
|
|
|
4
5
|
// 点击 combobox 之外收起面板的监听只注册一次(模块级标志,等价原 document.body 标记法)
|
|
5
6
|
let comboHideBound = false;
|
|
@@ -76,12 +77,25 @@ async function renderPlugins(el) {
|
|
|
76
77
|
<button class="btn sm danger" data-op="uninstall" data-arg="${esc(p.key)}" data-dir="${p.source}">卸载</button>
|
|
77
78
|
</td>
|
|
78
79
|
</tr>`).join('');
|
|
79
|
-
body.querySelectorAll('button[data-op]').forEach((b) => b.onclick = () => {
|
|
80
|
+
body.querySelectorAll('button[data-op]').forEach((b) => b.onclick = async () => {
|
|
80
81
|
const { op, arg, dir } = b.dataset;
|
|
81
|
-
//
|
|
82
|
-
if (op === 'disable' && !
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
// 影响性操作先二次确认(启用/安装/市场添加为新增类,无需确认)
|
|
83
|
+
if (op === 'disable' && !(await confirmDialog({
|
|
84
|
+
title: '停用插件',
|
|
85
|
+
message: `确定停用插件 <code>${esc(arg)}</code> 吗?停用后下一条消息不再加载该插件,可随时再启用。`,
|
|
86
|
+
confirmText: '停用',
|
|
87
|
+
}))) return;
|
|
88
|
+
if (op === 'update' && !(await confirmDialog({
|
|
89
|
+
title: '更新插件',
|
|
90
|
+
message: `确定更新插件 <code>${esc(arg)}</code> 吗?更新后下一条消息按新版本加载。`,
|
|
91
|
+
confirmText: '更新',
|
|
92
|
+
}))) return;
|
|
93
|
+
if (op === 'uninstall' && !(await confirmDialog({
|
|
94
|
+
title: '卸载插件',
|
|
95
|
+
message: `确定卸载插件 <code>${esc(arg)}</code> 吗?卸载后需重新安装才能恢复,进行中的会话不受影响。`,
|
|
96
|
+
danger: true,
|
|
97
|
+
confirmText: '卸载',
|
|
98
|
+
}))) return;
|
|
85
99
|
// 慢操作走进度弹框;亚秒级启停走轻量 act(弹框一闪而过反而干扰)
|
|
86
100
|
if (op === 'enable' || op === 'disable') act(op, arg, dir, b);
|
|
87
101
|
else runOpDialog(op, arg, dir, b);
|
|
@@ -273,7 +287,16 @@ async function renderPlugins(el) {
|
|
|
273
287
|
$('#plDir').value = $('#plMarketDir').value;
|
|
274
288
|
runOpDialog('marketplace-add', v, $('#plMarketDir').value, $('#plDoMarketAdd'));
|
|
275
289
|
};
|
|
276
|
-
$('#plDoMarketUpdate').onclick = () =>
|
|
290
|
+
$('#plDoMarketUpdate').onclick = async () => {
|
|
291
|
+
// 影响面大(该目录全部已装插件逐个更新):先二次确认
|
|
292
|
+
const dirLabel = $('#plMarketDir').value === 'bridge' ? 'bridge 托管目录' : '本机 ~/.claude';
|
|
293
|
+
if (!(await confirmDialog({
|
|
294
|
+
title: '全部更新',
|
|
295
|
+
message: `将刷新市场索引,并逐个更新「${dirLabel}」下的全部已装插件。确定继续?`,
|
|
296
|
+
confirmText: '全部更新',
|
|
297
|
+
}))) return;
|
|
298
|
+
runOpDialog('update-all', '', $('#plMarketDir').value, $('#plDoMarketUpdate'));
|
|
299
|
+
};
|
|
277
300
|
await Promise.all([load(), loadAvailable()]);
|
|
278
301
|
}
|
|
279
302
|
|