@jesonliu/lark-claudecode-bridge 1.4.3 → 2.0.1

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.
Files changed (56) hide show
  1. package/assets/web/css/main.css +774 -574
  2. package/assets/web/index.html +37 -17
  3. package/assets/web/js/auth.js +218 -0
  4. package/assets/web/js/bootstrap.js +36 -34
  5. package/assets/web/js/core.js +4 -3
  6. package/assets/web/js/main.js +79 -20
  7. package/assets/web/js/markdown.js +64 -0
  8. package/assets/web/js/pages/apps.js +338 -251
  9. package/assets/web/js/pages/chats.js +481 -0
  10. package/assets/web/js/pages/claude.js +243 -180
  11. package/assets/web/js/pages/mcp.js +59 -43
  12. package/assets/web/js/pages/memory.js +43 -46
  13. package/assets/web/js/pages/overview.js +426 -88
  14. package/assets/web/js/pages/permissions.js +121 -34
  15. package/assets/web/js/pages/plugins.js +51 -37
  16. package/assets/web/js/pages/skills.js +68 -92
  17. package/assets/web/js/pages/slash.js +31 -23
  18. package/assets/web/js/pages/workspaces.js +114 -73
  19. package/assets/web/js/theme.js +21 -18
  20. package/assets/web/js/ui.js +75 -32
  21. package/dist/access/access-control.d.ts +18 -0
  22. package/dist/access/access-control.js +29 -0
  23. package/dist/access/access-control.js.map +1 -1
  24. package/dist/bin/lcb.js +15 -0
  25. package/dist/bin/lcb.js.map +1 -1
  26. package/dist/feishu/name-cache.d.ts +15 -0
  27. package/dist/feishu/name-cache.js +82 -0
  28. package/dist/feishu/name-cache.js.map +1 -0
  29. package/dist/feishu/sibling-names.d.ts +1 -0
  30. package/dist/feishu/sibling-names.js +63 -0
  31. package/dist/feishu/sibling-names.js.map +1 -0
  32. package/dist/gateway/feishu-gateway.d.ts +24 -0
  33. package/dist/gateway/feishu-gateway.js +132 -0
  34. package/dist/gateway/feishu-gateway.js.map +1 -1
  35. package/dist/gateway/gateway-registry.d.ts +18 -0
  36. package/dist/gateway/gateway-registry.js +14 -0
  37. package/dist/gateway/gateway-registry.js.map +1 -0
  38. package/dist/index.d.ts +4 -0
  39. package/dist/index.js +52 -9
  40. package/dist/index.js.map +1 -1
  41. package/dist/types.d.ts +2 -0
  42. package/dist/web/chats-api.d.ts +93 -0
  43. package/dist/web/chats-api.js +603 -0
  44. package/dist/web/chats-api.js.map +1 -0
  45. package/dist/web/lifecycle.d.ts +21 -0
  46. package/dist/web/lifecycle.js +80 -3
  47. package/dist/web/lifecycle.js.map +1 -1
  48. package/dist/web/logs-api.d.ts +37 -0
  49. package/dist/web/logs-api.js +141 -0
  50. package/dist/web/logs-api.js.map +1 -0
  51. package/dist/web/server.js +196 -13
  52. package/dist/web/server.js.map +1 -1
  53. package/dist/web/webauth.d.ts +44 -0
  54. package/dist/web/webauth.js +221 -0
  55. package/dist/web/webauth.js.map +1 -0
  56. package/package.json +1 -1
@@ -1,7 +1,11 @@
1
- // ============ 权限(页内一级 tab:免确认工具 / 危险命令黑名单) ============
2
- import { S, $, esc, toast, saveDoc } from '../core.js';
1
+ // ============ 权限(页内一级 tab:免确认工具 / 危险命令黑名单 / 接入申请) ============
2
+ // 改动即时保存并热生效;「全部工具免确认」缺省开启(显式 false 才算关闭),开启时黑名单是唯一安全网。
3
+ import { S, $, esc, toast, saveDoc, api } from '../core.js';
3
4
  import { confirmDialog } from '../ui.js';
4
5
 
6
+ const svg = (inner, s = 14) => `<svg viewBox="0 0 24 24" width="${s}" height="${s}" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${inner}</svg>`;
7
+ const IC_INFO = '<circle cx="12" cy="12" r="9"/><path d="M12 8h.01"/><path d="M12 12v4"/>';
8
+
5
9
  let permTab = 'allow'; // tab 状态:切换 / 重渲染保持
6
10
  function renderPermissions(el) {
7
11
  const p = S.doc.permissions || (S.doc.permissions = {});
@@ -10,14 +14,93 @@ function renderPermissions(el) {
10
14
  el.innerHTML = `
11
15
  <div class="tabs">
12
16
  <button data-t="allow" class="${permTab === 'allow' ? 'active' : ''}">免确认工具</button>
13
- <button data-t="danger" class="${permTab === 'danger' ? 'active' : ''}">危险命令黑名单</button>
17
+ <button data-t="danger" class="${permTab === 'danger' ? 'active' : ''}">危险命令黑名单 <span class="tag off" style="margin-left:4px">${p.dangerous_commands.length}</span></button>
18
+ <button data-t="pair" class="${permTab === 'pair' ? 'active' : ''}">接入申请 <span class="tag off" style="margin-left:4px" id="pairCount">…</span></button>
14
19
  </div>
15
20
  <div id="permPanel"></div>`;
16
21
  el.querySelectorAll('.tabs button').forEach((b) =>
17
22
  b.onclick = () => { permTab = b.dataset.t; renderPermissions(el); });
18
23
  const panel = $('#permPanel');
19
24
  if (permTab === 'danger') renderDangerTab(panel, p);
25
+ else if (permTab === 'pair') renderPairTab(panel);
20
26
  else renderAllowTab(panel, p);
27
+ if (permTab !== 'pair') void refreshPairCount(); // 徽标数量任何 tab 下都保持新鲜
28
+ }
29
+
30
+ /** 接入申请徽标数量(tab 头);面板不存在时静默退出(页面已切换) */
31
+ async function refreshPairCount() {
32
+ let n = 0;
33
+ try { n = (await api('GET', '/api/access/pending')).pending.length; } catch { /* 失联:保持原样 */ }
34
+ const el = $('#pairCount');
35
+ if (el) { el.textContent = String(n); el.className = `tag ${n > 0 ? 'warn' : 'off'}`; }
36
+ return n;
37
+ }
38
+
39
+ // ============ 接入申请 tab:配对授权(码不再发到来话群,私发管理员 + 此处审批) ============
40
+ let pairPollTimer = null;
41
+ function renderPairTab(panel) {
42
+ if (pairPollTimer) { clearInterval(pairPollTimer); pairPollTimer = null; }
43
+ panel.innerHTML = `
44
+ <div class="panel">
45
+ <header class="panel-head"><h3>接入申请</h3>
46
+ <div class="panel-actions">
47
+ <span class="tip" data-tip="未知用户私聊/群里艾特机器人时,配对码私聊发给管理员并出现在这里;批准后立即生效,也可忽略(15 分钟自动过期)">${svg(IC_INFO)}</span>
48
+ <button class="btn sm" id="pairRefresh">↻ 刷新</button>
49
+ </div></header>
50
+ <div class="panel-body">
51
+ <div id="pairList"><span class="hint">加载中…</span></div>
52
+ </div>
53
+ <footer class="panel-foot">批准 = 该用户获得使用权限(member);不批准可忽略,申请 15 分钟后自动过期。终端 <code>lcb pair &lt;码&gt;</code> 双通道并存。配对码私聊发给当前配置的管理员(该机器人最早的使用者),不再发到来话群。</footer>
54
+ </div>`;
55
+ $('#pairRefresh').onclick = () => void loadPairList();
56
+ void loadPairList();
57
+ pairPollTimer = setInterval(() => { if (panel.isConnected) void loadPairList(true); else { clearInterval(pairPollTimer); pairPollTimer = null; } }, 30_000);
58
+ }
59
+
60
+ async function loadPairList(silent = false) {
61
+ const box = $('#pairList');
62
+ if (!box) return;
63
+ try {
64
+ const { pending } = await api('GET', '/api/access/pending');
65
+ await refreshPairCount();
66
+ if (!box.isConnected) return;
67
+ if (!pending.length) {
68
+ box.innerHTML = '<div class="banner ok" style="margin:6px 0"><span>暂无接入申请。未知用户联系机器人时会自动出现在这里(配对码同时私聊发给管理员)。</span></div>';
69
+ return;
70
+ }
71
+ box.innerHTML = `<table><thead><tr><th>申请人</th><th>机器人</th><th>配对码</th><th>剩余有效期</th><th style="width:170px">操作</th></tr></thead>
72
+ <tbody>${pending.map((it) => `
73
+ <tr>
74
+ <td><b>${esc(it.displayName || it.name)}</b>${it.displayName && it.displayName !== it.name ? ` <span class="hint" style="font-size:11px">${esc(it.name)}</span>` : ''}</td>
75
+ <td>${esc(it.appName || it.appId || '—')}</td>
76
+ <td><code>${esc(it.code)}</code></td>
77
+ <td>${it.expiresInSec > 60 ? `${Math.floor(it.expiresInSec / 60)} 分钟` : '<span class="tag warn">即将过期</span>'}</td>
78
+ <td style="display:flex;gap:6px">
79
+ <button class="btn sm primary" data-act="approve" data-code="${esc(it.code)}" data-name="${esc(it.displayName || it.name)}">授权</button>
80
+ <button class="btn sm danger" data-act="reject" data-code="${esc(it.code)}" data-name="${esc(it.displayName || it.name)}">拒绝</button>
81
+ </td>
82
+ </tr>`).join('')}</tbody></table>`;
83
+ box.querySelectorAll('button[data-act]').forEach((b) => {
84
+ b.onclick = async () => {
85
+ const approve = b.dataset.act === 'approve';
86
+ if (!(await confirmDialog({
87
+ title: approve ? '批准接入申请' : '拒绝接入申请',
88
+ message: approve
89
+ ? `确定批准 <b>${esc(b.dataset.name)}</b> 使用机器人?批准后对方即可开始对话。`
90
+ : `确定拒绝 <b>${esc(b.dataset.name)}</b> 的申请?对方再次发消息可重新发起申请。`,
91
+ confirmText: approve ? '批准' : '拒绝',
92
+ danger: !approve,
93
+ }))) return;
94
+ try {
95
+ await api('POST', `/api/access/pending/${approve ? 'approve' : 'reject'}`, { code: b.dataset.code });
96
+ toast(approve ? `✅ 已批准 ${b.dataset.name}` : '已拒绝该申请');
97
+ } catch (e) { toast(e.message, true); }
98
+ void loadPairList();
99
+ };
100
+ });
101
+ } catch (e) {
102
+ if (!silent) box.innerHTML = `<div class="banner err" style="margin:6px 0"><span>加载失败:${esc(e.message)}</span></div>`;
103
+ }
21
104
  }
22
105
 
23
106
  function renderAllowTab(panel, p) {
@@ -26,28 +109,30 @@ function renderAllowTab(panel, p) {
26
109
  // 缺省即开(与后端 DEFAULT_ALLOW_ALL_TOOLS 一致):只有显式 false 才算用户主动关闭
27
110
  const allOn = p.allow_all_tools !== false;
28
111
  panel.innerHTML = `
29
- <div class="card">
30
- <h3>免确认工具</h3>
31
- <div class="switch-row">
32
- <label class="switch">
33
- <input type="checkbox" id="allowAll" ${allOn ? 'checked' : ''}>
34
- <span class="track"></span>
35
- </label>
36
- <span class="switch-label">全部工具免确认(除危险命令黑名单外)</span>
37
- </div>
38
- ${allOn ? '<div style="background:var(--warn-bg);color:var(--warn-tx);border-radius:7px;padding:7px 12px;margin:8px 0">⚠️ 已开启全部放行:除命中危险命令黑名单的 Bash 外,所有工具(含 Write/Edit)都不再弹确认卡,下方名单暂不生效</div>' : ''}
39
- <div class="${allOn ? 'block-disabled' : ''}">
40
- <div class="desc">名单内工具直接放行不弹确认卡(配置即整体替换内置默认)。Bash 仍受危险命令黑名单约束。</div>
41
- <div class="list-toolbar">
42
- <input type="text" id="newTool" placeholder="工具名,如 Write" style="max-width:320px">
43
- <button class="btn" id="addTool">添加</button>
44
- <button class="btn" id="resetTools" ${defaults.length ? '' : 'disabled title="status 不可用,无法取默认值"'}>恢复默认</button>
112
+ <div class="panel">
113
+ <header class="panel-head"><h3>免确认工具</h3>
114
+ <div class="panel-actions">
115
+ ${isDefault ? '<span class="tag off">与内置默认一致</span>' : ''}
116
+ <span class="tip" data-tip="开启后除命中黑名单的 Bash 外,所有工具(含写入)都不再弹确认卡">${svg(IC_INFO)}</span>
117
+ </div></header>
118
+ <div class="panel-body">
119
+ <div class="switch-row" style="margin-top:0">
120
+ <label class="switch"><input type="checkbox" id="allowAll" ${allOn ? 'checked' : ''}><span class="track"></span></label>
121
+ <span class="switch-label">全部工具免确认(除危险命令黑名单外)</span>
122
+ </div>
123
+ ${allOn ? '<div class="banner warn" style="margin:8px 0"><span>已开启全部放行——下方名单暂不生效,黑名单是唯一安全网。</span></div>' : ''}
124
+ <div style="${allOn ? 'opacity:.45;pointer-events:none' : ''}">
125
+ <div class="desc" style="margin:8px 0 4px">名单内工具直接放行不弹确认卡(配置即整体替换内置默认)。Bash 仍受危险命令黑名单约束。</div>
126
+ <table><tbody id="toolBody"></tbody></table>
127
+ ${p.allow_tools.length === 0 ? '<div class="banner warn" style="margin:8px 0"><span>⚠️ 名单为空:所有工具(含 Write/Edit)都会弹确认卡</span></div>' : ''}
128
+ <div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap">
129
+ <input type="text" id="newTool" placeholder="工具名,如 Write" style="max-width:300px">
130
+ <button class="btn" id="addTool">添加</button>
131
+ <button class="btn" id="resetTools" ${defaults.length ? '' : 'disabled title="status 不可用,无法取默认值"'}>恢复默认</button>
132
+ </div>
45
133
  </div>
46
- ${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>' : ''}
47
- ${isDefault ? '<div class="hint" style="margin:8px 0"><span class="tag off">与内置默认一致</span></div>' : ''}
48
- <table><tbody id="toolBody"></tbody></table>
49
134
  </div>
50
- <div class="footer-note">权限改动即时保存并热生效(已有会话的下一个工具调用即用新名单)。plan 模式下写操作仍会经此判定:${allOn ? '全部免确认已开启,除危险命令外一律直通' : '白名单命中直通,未命中弹确认卡'}。</div>
135
+ <footer class="panel-foot">权限改动即时保存并热生效(已有会话的下一个工具调用即用新名单)。plan 模式下写操作仍会经此判定:${allOn ? '全部免确认已开启,除危险命令外一律直通' : '白名单命中直通,未命中弹确认卡'}。</footer>
51
136
  </div>`;
52
137
  $('#toolBody').innerHTML = p.allow_tools.map((t, i) =>
53
138
  `<tr><td><code>${esc(t)}</code></td><td style="width:80px"><button class="btn sm danger" data-i="${i}">删除</button></td></tr>`).join('')
@@ -112,18 +197,19 @@ function renderDangerTab(panel, p) {
112
197
  const defaults = S.status?.permissionDefaults?.dangerousCommands || [];
113
198
  const isDefault = defaults.length > 0 && JSON.stringify(p.dangerous_commands) === JSON.stringify(defaults);
114
199
  panel.innerHTML = `
115
- <div class="card">
116
- <h3>危险命令黑名单</h3>
117
- <div class="desc">正则源串(不区分大小写),命中即弹确认卡。删除正则 = 放宽对应命令为直通,请谨慎。</div>
118
- <div class="list-toolbar">
119
- <input type="text" id="newDc" placeholder="正则源串,如 \\bgit\\s+push\\b.*--force" style="max-width:480px">
120
- <button class="btn" id="addDc">添加</button>
121
- <button class="btn" id="resetDc" ${defaults.length ? '' : 'disabled title="status 不可用,无法取默认值"'}>恢复默认</button>
200
+ <div class="panel">
201
+ <header class="panel-head"><h3>危险命令黑名单</h3>
202
+ <div class="panel-actions"><span class="tip" data-tip="正则(不区分大小写),命中的 Bash 命令弹确认卡">${svg(IC_INFO)}</span></div></header>
203
+ <div class="panel-body">
204
+ ${p.dangerous_commands.length === 0 ? '<div class="banner err" style="margin-bottom:10px"><span>黑名单为空——所有 Bash 命令(含 rm -rf、sudo)将免确认直通,请确认有意为之。</span></div>' : ''}
205
+ <table><tbody id="dcBody"></tbody></table>
206
+ <div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap">
207
+ <input type="text" id="newDc" placeholder="正则源串,如 \\bgit\\s+push\\b.*--force" style="max-width:420px">
208
+ <button class="btn" id="addDc">添加</button>
209
+ <button class="btn" id="resetDc" ${defaults.length ? '' : 'disabled title="status 不可用,无法取默认值"'}>恢复默认</button>
210
+ </div>
122
211
  </div>
123
- ${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>' : ''}
124
- ${isDefault ? '<div class="hint" style="margin:8px 0"><span class="tag off">与内置默认一致</span></div>' : ''}
125
- <table><tbody id="dcBody"></tbody></table>
126
- <div class="footer-note">权限改动即时保存并热生效(已有会话的下一个工具调用即用新名单)。「全部工具免确认」开启时本黑名单是唯一的安全网——删除正则等于放行对应命令。</div>
212
+ <footer class="panel-foot">正则源串不区分大小写,命中即弹确认卡;删除正则 = 放宽对应命令为直通,请谨慎。改动即时保存并热生效。「全部工具免确认」开启时本黑名单是唯一的安全网。</footer>
127
213
  </div>`;
128
214
  $('#dcBody').innerHTML = p.dangerous_commands.map((s, i) =>
129
215
  `<tr><td style="width:90%"><code>${esc(s)}</code></td><td><button class="btn sm danger" data-i="${i}">删除</button></td></tr>`).join('')
@@ -165,6 +251,7 @@ function renderDangerTab(panel, p) {
165
251
  export const page = {
166
252
  id: 'permissions',
167
253
  title: '权限',
254
+ desc: '工具免确认名单、危险命令黑名单与接入申请审批,改动即时生效',
168
255
  icon: '<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1 1 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/>',
169
256
  render: renderPermissions,
170
257
  };
@@ -1,48 +1,58 @@
1
- // ============ 插件 ============
1
+ // ============ 插件(已装列表 + 管理市场 / 从市场安装 双卡;市场卡在左:先添加市场再安装) ============
2
2
  import { $, esc, toast, api } from '../core.js';
3
3
  import { confirmDialog } from '../ui.js';
4
4
 
5
5
  // 点击 combobox 之外收起面板的监听只注册一次(模块级标志,等价原 document.body 标记法)
6
6
  let comboHideBound = false;
7
+ let badgeCount = 0; // 有新版本的插件数(导航徽标;进过本页后可见)
7
8
 
8
9
  async function renderPlugins(el) {
9
10
  el.innerHTML = `
10
- <div class="card">
11
- <h3>Claude Code 插件 <button class="btn sm" id="plCheckUpdate" style="margin-left:8px">检查更新</button></h3>
12
- <div class="desc">两处目录的已启用插件都会自动加载(managed 托管目录优先):本机 <code>~/.claude</code>(来源「本机」,与本机 claude CLI 共用)与 bridge 托管目录 <code id="pluginDir">…</code>。装好后下一条消息自动加载。飞书端同样可用 <code>/plugin install xxx@marketplace</code>(仅管理员)。打开页面在后台自动检查远程市场新版本(不阻塞列表,5 分钟内不重复;只检测不升级,升级点单行「更新」)。</div>
13
- <div class="hint" id="plSyncNote" hidden style="margin:6px 0"></div>
14
- <table><thead><tr><th>插件</th><th>市场</th><th>来源</th><th>版本</th><th style="width:90px">状态</th><th style="width:250px">操作</th></tr></thead>
15
- <tbody id="plBody"><tr><td colspan="6" class="hint">加载中…</td></tr></tbody></table>
11
+ <div class="panel">
12
+ <header class="panel-head"><h3>已安装插件</h3>
13
+ <div class="panel-actions"><button class="btn sm" id="plCheckUpdate">检查更新</button></div></header>
14
+ <div class="panel-body">
15
+ <div class="hint" id="plSyncNote" hidden style="margin:0 0 8px"></div>
16
+ <table><thead><tr><th>插件</th><th style="width:120px">市场</th><th style="width:130px">来源</th><th style="width:170px">版本</th><th style="width:90px">状态</th><th style="width:230px">操作</th></tr></thead>
17
+ <tbody id="plBody"><tr><td colspan="6" class="hint">加载中…</td></tr></tbody></table>
18
+ </div>
19
+ <footer class="panel-foot">两处目录的已启用插件都会自动加载(managed 托管目录优先):本机 <code>~/.claude</code> 与 bridge 托管目录 <code id="pluginDir">…</code>。启停 / 更新后<b>下一条消息生效</b>,无须重启;飞书端亦可用 <code>/plugin install xxx@marketplace</code>(仅管理员)。</footer>
16
20
  </div>
17
- <div class="card">
18
- <h3>安装 / 市场</h3>
19
- <div class="desc">安装来源为已添加的市场:新插件先「添加市场」,再从下拉选择安装。</div>
20
- <label>从市场安装插件</label>
21
- <div class="row" style="max-width:760px">
22
- <div class="combo">
23
- <input type="text" id="plInstall" placeholder="(加载中…)" autocomplete="off">
24
- <div class="combo-panel" id="plInstallPanel" hidden></div>
21
+ <div class="grid2" style="gap:16px;align-items:start">
22
+ <div class="panel" style="margin:0">
23
+ <header class="panel-head"><h3>管理市场</h3></header>
24
+ <div class="panel-body">
25
+ <div class="mini-note" style="margin-bottom:10px">安装来源为已添加的市场:新插件先在这里「添加市场」,再到右侧安装。</div>
26
+ <label class="fld">添加市场(git 地址或本机路径)</label>
27
+ <div class="input-btn"><input type="text" id="plMarket" placeholder="https://github.com/…/marketplace"><button class="btn" id="plDoMarketAdd">添加</button></div>
28
+ <label class="fld" style="margin-top:12px">目标目录</label>
29
+ <div class="input-btn"><select id="plMarketDir">
30
+ <option value="user" selected>本机 ~/.claude</option>
31
+ <option value="bridge">bridge 托管目录</option>
32
+ </select><button class="btn" id="plDoMarketUpdate">全部更新</button></div>
33
+ <div class="mini-note" style="margin-top:8px">「全部更新」= 刷新市场索引 + 逐个更新该目录所有已装插件。</div>
25
34
  </div>
26
- <select id="plDir" style="max-width:170px">
27
- <option value="user" selected>装到本机 ~/.claude</option>
28
- <option value="bridge">装到 bridge 托管目录</option>
29
- </select>
30
- <button class="btn primary" id="plDoInstall">安装</button>
31
35
  </div>
32
- <div class="hint" style="margin:6px 0">默认装本机 ~/.claude:本机 claude CLI 与桥接器两处共用一份,无须安装两次。装好后下一条消息自动加载。</div>
33
- <label style="margin-top:14px">管理市场</label>
34
- <div class="row" style="max-width:760px">
35
- <input type="text" id="plMarket" placeholder="marketplace git 地址或本机路径">
36
- <select id="plMarketDir" style="max-width:170px">
37
- <option value="user" selected>本机 ~/.claude</option>
38
- <option value="bridge">bridge 托管目录</option>
39
- </select>
40
- <button class="btn" id="plDoMarketAdd">添加市场</button>
41
- <button class="btn" id="plDoMarketUpdate">全部更新</button>
36
+ <div class="panel" style="margin:0">
37
+ <header class="panel-head"><h3>从市场安装</h3></header>
38
+ <div class="panel-body">
39
+ <label class="fld">搜索插件 <span class="tip" data-tip="候选来自已添加市场的全部插件;也可在飞书端 /plugin install xxx@marketplace(仅管理员)">ⓘ</span></label>
40
+ <div class="input-btn" style="align-items:flex-start">
41
+ <div class="combo" style="flex:1;min-width:0">
42
+ <input type="text" id="plInstall" placeholder="(加载中…)" autocomplete="off">
43
+ <div class="combo-panel" id="plInstallPanel" hidden></div>
44
+ </div>
45
+ </div>
46
+ <label class="fld" style="margin-top:12px">安装到</label>
47
+ <select id="plDir">
48
+ <option value="user" selected>本机 ~/.claude(推荐:本机 CLI 与桥接器共用一份)</option>
49
+ <option value="bridge">bridge 托管目录</option>
50
+ </select>
51
+ <div style="margin-top:12px"><button class="btn primary" id="plDoInstall">安装</button></div>
52
+ </div>
42
53
  </div>
43
- <div class="hint" style="margin:6px 0">「全部更新」= 刷新市场索引 + 逐个更新该目录所有已装插件。</div>
44
- <pre class="report" id="plReport" style="display:none;margin-top:12px"></pre>
45
- </div>`;
54
+ </div>
55
+ <pre class="report" id="plReport" style="display:none;margin-top:0"></pre>`;
46
56
  const report = (title, text) => {
47
57
  $('#plReport').style.display = '';
48
58
  $('#plReport').textContent = `${title}\n${text}`;
@@ -65,11 +75,12 @@ async function renderPlugins(el) {
65
75
  /** 渲染已装插件表(本地秒出与后台刷新完成两条路径共用;入参 /api/plugins 响应) */
66
76
  const renderList = (r) => {
67
77
  installed = r.plugins || [];
78
+ badgeCount = installed.filter((p) => p.latestVersion).length;
68
79
  $('#pluginDir').textContent = r.configDir;
69
80
  const body = $('#plBody');
70
81
  if (!body) return; // 页面已切走(后台刷新回调晚于切页)
71
82
  if (!installed.length) {
72
- body.innerHTML = '<tr><td colspan="6" class="hint">尚未安装任何插件</td></tr>';
83
+ body.innerHTML = '<tr class="empty"><td colspan="6">尚未安装任何插件</td></tr>';
73
84
  return;
74
85
  }
75
86
  body.innerHTML = installed.map((p) => `
@@ -77,9 +88,9 @@ async function renderPlugins(el) {
77
88
  <td><b>${esc(p.name)}</b></td>
78
89
  <td>${esc(p.marketplace || '-')}</td>
79
90
  <td>${p.source === 'bridge' ? '<span class="tag warn">bridge 托管</span>' : '<span class="tag off">本机 ~/.claude</span>'}</td>
80
- <td class="hint">${esc(p.version || '-')}${p.latestVersion ? ` <span class="tag warn">→ ${esc(p.latestVersion)}</span>` : ''}</td>
91
+ <td><span class="mono">${esc(p.version || '-')}</span>${p.latestVersion ? ` <span class="tag warn">→ ${esc(p.latestVersion)}</span>` : ''}</td>
81
92
  <td>${p.enabled ? '<span class="tag ok">已启用</span>' : '<span class="tag off">未启用</span>'}</td>
82
- <td>
93
+ <td class="ops">
83
94
  <button class="btn sm" data-op="update" data-arg="${esc(p.key)}" data-dir="${p.source}">更新</button>
84
95
  <button class="btn sm" data-op="${p.enabled ? 'disable' : 'enable'}" data-arg="${esc(p.key)}" data-dir="${p.source}">${p.enabled ? '停用' : '启用'}</button>
85
96
  <button class="btn sm danger" data-op="uninstall" data-arg="${esc(p.key)}" data-dir="${p.source}">卸载</button>
@@ -263,7 +274,7 @@ async function renderPlugins(el) {
263
274
  }
264
275
  }
265
276
  $('#plDoInstall').disabled = !available.length;
266
- input.placeholder = available.length ? '输入关键词搜索插件,从结果中选择' : '(请先在下方添加市场)';
277
+ input.placeholder = available.length ? '输入关键词搜索插件,从结果中选择' : '(请先在左侧添加市场)';
267
278
  } catch (e) {
268
279
  available = [];
269
280
  $('#plDoInstall').disabled = true;
@@ -360,6 +371,9 @@ async function renderPlugins(el) {
360
371
  export const page = {
361
372
  id: 'plugins',
362
373
  title: '插件',
374
+ desc: '两处目录的 Claude Code 插件统一管理,改动下一条消息自动生效',
375
+ legend: false,
376
+ badge: () => (badgeCount ? `<span class="nav-badge">${badgeCount}</span>` : ''),
363
377
  icon: '<path d="M19.439 7.85c-.049.322.059.648.289.878l1.568 1.568c.47.47.706 1.087.706 1.704s-.235 1.233-.706 1.704l-1.611 1.611a.98.98 0 0 1-.837.276c-.47-.07-.802-.48-.968-.925a2.501 2.501 0 1 0-3.214 3.214c.446.166.855.497.925.968a.979.979 0 0 1-.276.837l-1.61 1.61a2.404 2.404 0 0 1-1.705.707 2.402 2.402 0 0 1-1.704-.706l-1.568-1.568a1.026 1.026 0 0 0-.877-.29c-.493.074-.84.504-1.02.968a2.5 2.5 0 1 1-3.237-3.237c.464-.18.894-.527.967-1.02a1.026 1.026 0 0 0-.289-.877l-1.568-1.568A2.402 2.402 0 0 1 1.998 12c0-.617.236-1.234.706-1.704L4.23 8.77c.24-.24.581-.353.917-.303.515.077.877.528 1.073 1.01a2.5 2.5 0 1 0 3.259-3.259c-.482-.196-.933-.558-1.01-1.073-.05-.336.062-.676.303-.917l1.525-1.525A2.402 2.402 0 0 1 12 1.998c.617 0 1.234.236 1.704.706l1.568 1.568c.23.23.556.338.877.29.493-.074.84-.504 1.02-.968a2.5 2.5 0 1 1 3.237 3.237c-.464.18-.894.527-.967 1.02Z"/>',
364
378
  render: renderPlugins,
365
379
  };