@jesonliu/lark-claudecode-bridge 1.4.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/web/css/main.css +766 -574
- package/assets/web/index.html +37 -17
- package/assets/web/js/auth.js +217 -0
- package/assets/web/js/bootstrap.js +36 -34
- package/assets/web/js/core.js +4 -3
- package/assets/web/js/main.js +79 -20
- package/assets/web/js/markdown.js +64 -0
- package/assets/web/js/pages/apps.js +338 -251
- package/assets/web/js/pages/chats.js +446 -0
- package/assets/web/js/pages/claude.js +243 -180
- package/assets/web/js/pages/mcp.js +59 -43
- package/assets/web/js/pages/memory.js +39 -43
- package/assets/web/js/pages/overview.js +344 -76
- package/assets/web/js/pages/permissions.js +40 -32
- package/assets/web/js/pages/plugins.js +51 -37
- package/assets/web/js/pages/skills.js +68 -92
- package/assets/web/js/pages/slash.js +31 -23
- package/assets/web/js/pages/workspaces.js +114 -73
- package/assets/web/js/theme.js +21 -18
- package/assets/web/js/ui.js +75 -32
- package/dist/bin/lcb.js +15 -0
- package/dist/bin/lcb.js.map +1 -1
- package/dist/gateway/feishu-gateway.d.ts +11 -0
- package/dist/gateway/feishu-gateway.js +5 -0
- package/dist/gateway/feishu-gateway.js.map +1 -1
- package/dist/gateway/gateway-registry.d.ts +16 -0
- package/dist/gateway/gateway-registry.js +10 -0
- package/dist/gateway/gateway-registry.js.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/web/chats-api.d.ts +77 -0
- package/dist/web/chats-api.js +475 -0
- package/dist/web/chats-api.js.map +1 -0
- package/dist/web/logs-api.d.ts +37 -0
- package/dist/web/logs-api.js +141 -0
- package/dist/web/logs-api.js.map +1 -0
- package/dist/web/server.js +125 -6
- package/dist/web/server.js.map +1 -1
- package/dist/web/webauth.d.ts +44 -0
- package/dist/web/webauth.js +177 -0
- package/dist/web/webauth.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// ============ 权限(页内一级 tab:免确认工具 / 危险命令黑名单) ============
|
|
2
|
+
// 改动即时保存并热生效;「全部工具免确认」缺省开启(显式 false 才算关闭),开启时黑名单是唯一安全网。
|
|
2
3
|
import { S, $, esc, toast, saveDoc } 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,7 +14,7 @@ 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' : ''}"
|
|
17
|
+
<button data-t="danger" class="${permTab === 'danger' ? 'active' : ''}">危险命令黑名单 <span class="tag off" style="margin-left:4px">${p.dangerous_commands.length}</span></button>
|
|
14
18
|
</div>
|
|
15
19
|
<div id="permPanel"></div>`;
|
|
16
20
|
el.querySelectorAll('.tabs button').forEach((b) =>
|
|
@@ -26,28 +30,30 @@ function renderAllowTab(panel, p) {
|
|
|
26
30
|
// 缺省即开(与后端 DEFAULT_ALLOW_ALL_TOOLS 一致):只有显式 false 才算用户主动关闭
|
|
27
31
|
const allOn = p.allow_all_tools !== false;
|
|
28
32
|
panel.innerHTML = `
|
|
29
|
-
<div class="
|
|
30
|
-
<h3>免确认工具</h3>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<div class="
|
|
41
|
-
<div
|
|
42
|
-
<
|
|
43
|
-
<
|
|
44
|
-
<
|
|
33
|
+
<div class="panel">
|
|
34
|
+
<header class="panel-head"><h3>免确认工具</h3>
|
|
35
|
+
<div class="panel-actions">
|
|
36
|
+
${isDefault ? '<span class="tag off">与内置默认一致</span>' : ''}
|
|
37
|
+
<span class="tip" data-tip="开启后除命中黑名单的 Bash 外,所有工具(含写入)都不再弹确认卡">${svg(IC_INFO)}</span>
|
|
38
|
+
</div></header>
|
|
39
|
+
<div class="panel-body">
|
|
40
|
+
<div class="switch-row" style="margin-top:0">
|
|
41
|
+
<label class="switch"><input type="checkbox" id="allowAll" ${allOn ? 'checked' : ''}><span class="track"></span></label>
|
|
42
|
+
<span class="switch-label">全部工具免确认(除危险命令黑名单外)</span>
|
|
43
|
+
</div>
|
|
44
|
+
${allOn ? '<div class="banner warn" style="margin:8px 0"><span>已开启全部放行——下方名单暂不生效,黑名单是唯一安全网。</span></div>' : ''}
|
|
45
|
+
<div style="${allOn ? 'opacity:.45;pointer-events:none' : ''}">
|
|
46
|
+
<div class="desc" style="margin:8px 0 4px">名单内工具直接放行不弹确认卡(配置即整体替换内置默认)。Bash 仍受危险命令黑名单约束。</div>
|
|
47
|
+
<table><tbody id="toolBody"></tbody></table>
|
|
48
|
+
${p.allow_tools.length === 0 ? '<div class="banner warn" style="margin:8px 0"><span>⚠️ 名单为空:所有工具(含 Write/Edit)都会弹确认卡</span></div>' : ''}
|
|
49
|
+
<div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap">
|
|
50
|
+
<input type="text" id="newTool" placeholder="工具名,如 Write" style="max-width:300px">
|
|
51
|
+
<button class="btn" id="addTool">添加</button>
|
|
52
|
+
<button class="btn" id="resetTools" ${defaults.length ? '' : 'disabled title="status 不可用,无法取默认值"'}>恢复默认</button>
|
|
53
|
+
</div>
|
|
45
54
|
</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
55
|
</div>
|
|
50
|
-
<
|
|
56
|
+
<footer class="panel-foot">权限改动即时保存并热生效(已有会话的下一个工具调用即用新名单)。plan 模式下写操作仍会经此判定:${allOn ? '全部免确认已开启,除危险命令外一律直通' : '白名单命中直通,未命中弹确认卡'}。</footer>
|
|
51
57
|
</div>`;
|
|
52
58
|
$('#toolBody').innerHTML = p.allow_tools.map((t, i) =>
|
|
53
59
|
`<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 +118,19 @@ function renderDangerTab(panel, p) {
|
|
|
112
118
|
const defaults = S.status?.permissionDefaults?.dangerousCommands || [];
|
|
113
119
|
const isDefault = defaults.length > 0 && JSON.stringify(p.dangerous_commands) === JSON.stringify(defaults);
|
|
114
120
|
panel.innerHTML = `
|
|
115
|
-
<div class="
|
|
116
|
-
<h3>危险命令黑名单</h3>
|
|
117
|
-
|
|
118
|
-
<div class="
|
|
119
|
-
|
|
120
|
-
<
|
|
121
|
-
<
|
|
121
|
+
<div class="panel">
|
|
122
|
+
<header class="panel-head"><h3>危险命令黑名单</h3>
|
|
123
|
+
<div class="panel-actions"><span class="tip" data-tip="正则(不区分大小写),命中的 Bash 命令弹确认卡">${svg(IC_INFO)}</span></div></header>
|
|
124
|
+
<div class="panel-body">
|
|
125
|
+
${p.dangerous_commands.length === 0 ? '<div class="banner err" style="margin-bottom:10px"><span>黑名单为空——所有 Bash 命令(含 rm -rf、sudo)将免确认直通,请确认有意为之。</span></div>' : ''}
|
|
126
|
+
<table><tbody id="dcBody"></tbody></table>
|
|
127
|
+
<div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap">
|
|
128
|
+
<input type="text" id="newDc" placeholder="正则源串,如 \\bgit\\s+push\\b.*--force" style="max-width:420px">
|
|
129
|
+
<button class="btn" id="addDc">添加</button>
|
|
130
|
+
<button class="btn" id="resetDc" ${defaults.length ? '' : 'disabled title="status 不可用,无法取默认值"'}>恢复默认</button>
|
|
131
|
+
</div>
|
|
122
132
|
</div>
|
|
123
|
-
|
|
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>
|
|
133
|
+
<footer class="panel-foot">正则源串不区分大小写,命中即弹确认卡;删除正则 = 放宽对应命令为直通,请谨慎。改动即时保存并热生效。「全部工具免确认」开启时本黑名单是唯一的安全网。</footer>
|
|
127
134
|
</div>`;
|
|
128
135
|
$('#dcBody').innerHTML = p.dangerous_commands.map((s, i) =>
|
|
129
136
|
`<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 +172,7 @@ function renderDangerTab(panel, p) {
|
|
|
165
172
|
export const page = {
|
|
166
173
|
id: 'permissions',
|
|
167
174
|
title: '权限',
|
|
175
|
+
desc: '工具免确认名单与危险命令黑名单,改动即时生效',
|
|
168
176
|
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
177
|
render: renderPermissions,
|
|
170
178
|
};
|
|
@@ -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="
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
<div class="
|
|
14
|
-
|
|
15
|
-
|
|
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="
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<input type="text" id="
|
|
24
|
-
<
|
|
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="
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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"
|
|
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="
|
|
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
|
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
// ============ Skills
|
|
1
|
+
// ============ Skills 管理 ============
|
|
2
2
|
// 后端:GET /api/skills · POST /api/skills/action(create/delete)· POST /api/skills/import(zip)
|
|
3
3
|
// 四来源:用户级·本机(~/.claude/skills)/ 用户级·bridge(managed 生效目录)/ 项目级(工作区 .claude/skills)/
|
|
4
4
|
// 已装插件内(<plugin-installPath>/skills;plugin 只读,未启用时标「(未启用)」)。
|
|
5
|
-
//
|
|
5
|
+
// 搜索只匹配「名称 + 说明」两列;过滤只切换行显隐不重建 DOM(兼容中文输入法组合输入)。
|
|
6
|
+
// 远端 CRUD:改动即时落盘,无页内保存条。
|
|
6
7
|
import { $, esc, toast, api } from '../core.js';
|
|
7
8
|
import { openDrawer, closeDrawer, confirmDialog, promptDialog } from '../ui.js';
|
|
9
|
+
import { renderMarkdown } from '../markdown.js';
|
|
8
10
|
|
|
9
11
|
/** 来源标签(function:plugin 来源需要拼插件名与启用状态) */
|
|
10
12
|
const SOURCE_TAG = {
|
|
@@ -17,34 +19,46 @@ const sourceTag = (s) => {
|
|
|
17
19
|
if (s.source === 'plugin') return `插件 · ${esc(s.pluginName || '?')}${s.pluginEnabled ? '' : '(未启用)'}`;
|
|
18
20
|
return SOURCE_TAG[s.source] || s.source;
|
|
19
21
|
};
|
|
22
|
+
const sourceTagClass = (s) => (s.source === 'bridge' ? 'info' : s.source === 'plugin' ? (s.pluginEnabled ? 'off' : 'warn') : 'ghost');
|
|
20
23
|
/** 来源是否可删(project / plugin 只读;其他用户级可删) */
|
|
21
24
|
const isDeletable = (s) => s.source !== 'project' && s.source !== 'plugin';
|
|
22
25
|
|
|
23
26
|
function renderSkills(el) {
|
|
24
27
|
el.innerHTML = `
|
|
25
|
-
<div class="
|
|
26
|
-
<h3
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
<div class="panel">
|
|
29
|
+
<header class="panel-head"><h3>技能列表</h3>
|
|
30
|
+
<div class="panel-actions">
|
|
31
|
+
<input type="search" id="skillSearch" placeholder="过滤:名称 / 说明…" style="width:210px;padding:4px 10px;font-size:12.5px">
|
|
32
|
+
<button class="btn sm" id="skillImport">导入 zip</button>
|
|
33
|
+
<button class="btn primary sm" id="skillAdd">+ 新建</button>
|
|
34
|
+
</div></header>
|
|
35
|
+
<div class="panel-body">
|
|
36
|
+
<input type="file" id="skillZipFile" accept=".zip" style="display:none">
|
|
37
|
+
<table><thead><tr>
|
|
38
|
+
<th style="width:220px">名字</th><th>说明</th><th style="width:190px">来源</th><th style="width:130px">操作</th>
|
|
39
|
+
</tr></thead>
|
|
40
|
+
<tbody id="skillBody"><tr><td colspan="4" class="hint">加载中…</td></tr></tbody>
|
|
41
|
+
</table>
|
|
42
|
+
<div class="mini-note" id="skillEffective" style="margin-top:10px"></div>
|
|
31
43
|
</div>
|
|
32
|
-
<
|
|
33
|
-
<input type="search" class="list-search" id="skillSearch" placeholder="按名字 / 来源 / 路径过滤…">
|
|
34
|
-
<table><thead><tr>
|
|
35
|
-
<th style="width:170px">名字</th><th>说明</th><th style="width:130px">来源</th><th style="width:240px">路径</th><th style="width:130px"></th>
|
|
36
|
-
</tr></thead>
|
|
37
|
-
<tbody id="skillBody"><tr><td colspan="5" class="desc">加载中…</td></tr></tbody>
|
|
38
|
-
</table>
|
|
39
|
-
<div class="desc" id="skillEffective" style="margin-top:10px"></div>
|
|
44
|
+
<footer class="panel-foot">新增 / 导入统一落盘用户级生效目录;项目级与插件内来源只读。改动即时生效。</footer>
|
|
40
45
|
</div>`;
|
|
41
46
|
$('#skillAdd').onclick = addSkill;
|
|
42
|
-
|
|
47
|
+
bindSearch();
|
|
43
48
|
bindZipImport();
|
|
44
49
|
void loadSkills();
|
|
45
50
|
}
|
|
46
51
|
|
|
47
|
-
/**
|
|
52
|
+
/** 搜索过滤:只匹配名称+说明;只切换 tr 显隐不重建 DOM;组合输入(中文 IME)期间暂停 */
|
|
53
|
+
function bindSearch() {
|
|
54
|
+
const inp = $('#skillSearch');
|
|
55
|
+
let composing = false;
|
|
56
|
+
inp.addEventListener('compositionstart', () => { composing = true; });
|
|
57
|
+
inp.addEventListener('compositionend', () => { composing = false; filterSkillRows(); });
|
|
58
|
+
inp.oninput = () => { if (!composing) filterSkillRows(); };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 搜索过滤:遍历 tr.dataset.searchText 切换 hidden(关键词只命中名称与说明) */
|
|
48
62
|
function filterSkillRows() {
|
|
49
63
|
const q = ($('#skillSearch').value || '').trim().toLowerCase();
|
|
50
64
|
const body = $('#skillBody');
|
|
@@ -70,7 +84,7 @@ async function loadSkills() {
|
|
|
70
84
|
try {
|
|
71
85
|
data = await api('GET', '/api/skills');
|
|
72
86
|
} catch (e) {
|
|
73
|
-
body.innerHTML = `<tr><td colspan="
|
|
87
|
+
body.innerHTML = `<tr><td colspan="4" class="hint">加载失败:${esc(e.message)}</td></tr>`;
|
|
74
88
|
return;
|
|
75
89
|
}
|
|
76
90
|
const eff = $('#skillEffective');
|
|
@@ -83,22 +97,21 @@ async function loadSkills() {
|
|
|
83
97
|
}
|
|
84
98
|
const skills = data.skills || [];
|
|
85
99
|
if (skills.length === 0) {
|
|
86
|
-
body.innerHTML = `<tr class="empty-row"><td colspan="
|
|
100
|
+
body.innerHTML = `<tr class="empty-row"><td colspan="4" class="empty">暂无 skill(四个来源均未发现 SKILL.md)<br><span class="mini-note">可点右上「+ 新建」或「导入 zip」。</span></td></tr>`;
|
|
87
101
|
return;
|
|
88
102
|
}
|
|
89
103
|
body.innerHTML = '';
|
|
90
104
|
for (const s of skills) {
|
|
91
|
-
const tag = sourceTag(s);
|
|
92
105
|
const tr = document.createElement('tr');
|
|
93
|
-
|
|
106
|
+
// 搜索只匹配名称 + 说明两列(路径/来源不参与,避免全字段模糊误命中)
|
|
107
|
+
tr.dataset.searchText = `${s.name} ${s.description || ''}`.toLowerCase();
|
|
94
108
|
tr.innerHTML = `
|
|
95
|
-
<td><
|
|
96
|
-
<td>${esc(s.description) || '<span class="
|
|
97
|
-
<td><span class="
|
|
98
|
-
<td><code class="desc">${esc(s.path)}</code></td>
|
|
109
|
+
<td><b>${esc(s.name)}</b>${s.path ? `<div class="mini-note" style="margin-top:2px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="完整目录:${esc(s.path)}"><code>${esc(s.path)}</code></div>` : ''}</td>
|
|
110
|
+
<td>${esc(s.description) || '<span class="mut">(无说明)</span>'}</td>
|
|
111
|
+
<td><span class="tag ${sourceTagClass(s)}">${sourceTag(s)}</span></td>
|
|
99
112
|
<td class="ops">
|
|
100
113
|
<button class="btn sm" data-browse="${esc(s.name)}">查看</button>
|
|
101
|
-
${isDeletable(s) ? `<button class="btn sm danger" data-del="${esc(s.name)}">删除</button>` : ''}
|
|
114
|
+
${isDeletable(s) ? `<button class="btn sm danger" data-del="${esc(s.name)}">删除</button>` : '<span class="dim" style="font-size:11.5px">只读</span>'}
|
|
102
115
|
</td>`;
|
|
103
116
|
tr.querySelector('[data-browse]').onclick = () => browseSkill(s);
|
|
104
117
|
const delBtn = tr.querySelector('[data-del]');
|
|
@@ -121,71 +134,31 @@ async function delSkill(s) {
|
|
|
121
134
|
.catch((e) => toast(e.message, true));
|
|
122
135
|
}
|
|
123
136
|
|
|
124
|
-
// ---------- skill
|
|
125
|
-
|
|
126
|
-
/** 极简 markdown → HTML(先整体转义再还原语法,绝无 XSS 面):标题/代码块/行内代码/粗斜体/列表/引用/链接/分割线 */
|
|
127
|
-
function renderMarkdown(src) {
|
|
128
|
-
const codeBlocks = [];
|
|
129
|
-
let text = String(src).replace(/```(\w*)\n?([\s\S]*?)(?:```|$)/g, (_m, lang, code) => {
|
|
130
|
-
codeBlocks.push(`<pre style="background:var(--code-bg);border:1px solid var(--border);border-radius:8px;padding:10px;overflow:auto;font-size:12px"><code>${esc(code.replace(/\n$/, ''))}</code></pre>`);
|
|
131
|
-
return `${codeBlocks.length - 1}`;
|
|
132
|
-
});
|
|
133
|
-
const lines = text.split('\n');
|
|
134
|
-
const out = [];
|
|
135
|
-
let listOpen = false;
|
|
136
|
-
const closeList = () => { if (listOpen) { out.push('</ul>'); listOpen = false; } };
|
|
137
|
-
const inline = (s) => esc(s)
|
|
138
|
-
.replace(/`([^`]+)`/g, '<code style="background:var(--code-bg);padding:1px 5px;border-radius:4px;font-size:12px">$1</code>')
|
|
139
|
-
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
140
|
-
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
|
|
141
|
-
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener" style="color:var(--primary)">$1</a>');
|
|
142
|
-
for (const line of lines) {
|
|
143
|
-
const block = /^ (\d+) $/.exec(line);
|
|
144
|
-
if (block) { closeList(); out.push(codeBlocks[Number(block[1])]); continue; }
|
|
145
|
-
const h = /^(#{1,4})\s+(.*)$/.exec(line);
|
|
146
|
-
if (h) { closeList(); const lv = h[1].length; out.push(`<h${lv + 1} style="margin:12px 0 6px">${inline(h[2])}</h${lv + 1}>`); continue; }
|
|
147
|
-
if (/^\s*[-*]\s+/.test(line)) {
|
|
148
|
-
if (!listOpen) { out.push('<ul style="margin:4px 0;padding-left:22px">'); listOpen = true; }
|
|
149
|
-
out.push(`<li>${inline(line.replace(/^\s*[-*]\s+/, ''))}</li>`);
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
if (/^\s*\d+\.\s+/.test(line)) {
|
|
153
|
-
if (!listOpen) { out.push('<ul style="margin:4px 0;padding-left:22px">'); listOpen = true; }
|
|
154
|
-
out.push(`<li>${inline(line.replace(/^\s*\d+\.\s+/, ''))}</li>`);
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
closeList();
|
|
158
|
-
if (/^---+\s*$/.test(line)) { out.push('<hr style="border:none;border-top:1px solid var(--border);margin:10px 0">'); continue; }
|
|
159
|
-
if (/^>\s?/.test(line)) { out.push(`<blockquote style="margin:6px 0;padding:4px 12px;border-left:3px solid var(--border);color:var(--muted,#667)">${inline(line.replace(/^>\s?/, ''))}</blockquote>`); continue; }
|
|
160
|
-
if (line.trim() === '') { out.push(''); continue; }
|
|
161
|
-
out.push(`<p style="margin:4px 0">${inline(line)}</p>`);
|
|
162
|
-
}
|
|
163
|
-
closeList();
|
|
164
|
-
return out.join('\n');
|
|
165
|
-
}
|
|
137
|
+
// ---------- skill 文件浏览:目录树 + 原文/视图/双栏三种查看模式 ----------
|
|
138
|
+
// markdown 渲染走公共模块 ../markdown.js(与会话页 AI 气泡同源,含表格支持)
|
|
166
139
|
|
|
167
140
|
/** 浏览抽屉:左侧文件树(目录懒加载展开),右侧查看区(markdown 文件支持 原文/视图/两者 三模式) */
|
|
168
141
|
function browseSkill(s) {
|
|
169
142
|
openDrawer({
|
|
170
143
|
title: `${s.name} · ${sourceTag(s)}`,
|
|
171
144
|
bodyHtml: `
|
|
172
|
-
<div class="hint" style="margin:0 0 10px"><code
|
|
173
|
-
<div
|
|
174
|
-
<
|
|
175
|
-
<
|
|
176
|
-
</
|
|
177
|
-
<div
|
|
145
|
+
<div class="hint" style="margin:0 0 10px"><code>${esc(s.path)}</code></div>
|
|
146
|
+
<div class="dw-layout">
|
|
147
|
+
<nav class="dw-nav" id="sbTree" style="width:250px;border:1px solid var(--border);border-radius:10px;padding:8px;max-height:62vh;overflow:auto;font-size:13px">
|
|
148
|
+
<span class="hint">目录加载中…</span>
|
|
149
|
+
</nav>
|
|
150
|
+
<div class="dw-pane">
|
|
178
151
|
<div id="sbModes" style="display:none;gap:8px;margin-bottom:8px" class="subtabs">
|
|
179
152
|
<button data-mode="both" class="active">两者都看</button>
|
|
180
153
|
<button data-mode="raw">只看原文</button>
|
|
181
154
|
<button data-mode="view">只看视图</button>
|
|
182
155
|
</div>
|
|
183
|
-
<div id="sbFileName" class="
|
|
184
|
-
<div id="sbView" style="display:none;border:1px solid var(--border);border-radius:
|
|
185
|
-
<textarea id="sbRaw" readonly style="display:none;width:100%;min-height:56vh;font-family:
|
|
156
|
+
<div id="sbFileName" class="mini-note" style="margin-bottom:6px">点击左侧文件查看内容</div>
|
|
157
|
+
<div id="sbView" style="display:none;border:1px solid var(--border);border-radius:10px;padding:12px;max-height:56vh;overflow:auto;font-size:13px;line-height:1.6;background:var(--sunk)"></div>
|
|
158
|
+
<textarea id="sbRaw" readonly style="display:none;width:100%;min-height:56vh;font-family:var(--mono);font-size:12px"></textarea>
|
|
186
159
|
</div>
|
|
187
160
|
</div>`,
|
|
188
|
-
footHtml: `<button class="btn" data-act="close">关闭</button>`,
|
|
161
|
+
footHtml: `<span class="sp"></span><button class="btn" data-act="close">关闭</button>`,
|
|
189
162
|
onMount({ body, foot }) {
|
|
190
163
|
foot.querySelector('[data-act="close"]').onclick = () => closeDrawer();
|
|
191
164
|
const tree = body.querySelector('#sbTree');
|
|
@@ -207,7 +180,7 @@ function browseSkill(s) {
|
|
|
207
180
|
fileName.textContent = `加载中:${fp}`;
|
|
208
181
|
try {
|
|
209
182
|
const r = await api('GET', `/api/skills/raw?path=${encodeURIComponent(fp)}`);
|
|
210
|
-
fileName.innerHTML = `<
|
|
183
|
+
fileName.innerHTML = `<b>${esc(r.name)}</b> <span class="dim">· ${(r.size / 1024).toFixed(1)}KB</span>`;
|
|
211
184
|
rawPane.value = r.content;
|
|
212
185
|
if (isMd(r.name)) {
|
|
213
186
|
viewPane.innerHTML = renderMarkdown(r.content);
|
|
@@ -225,25 +198,25 @@ function browseSkill(s) {
|
|
|
225
198
|
}
|
|
226
199
|
};
|
|
227
200
|
const renderDir = async (dir, container) => {
|
|
228
|
-
container.innerHTML = '<div class="
|
|
201
|
+
container.innerHTML = '<div class="hint" style="padding:2px 6px">加载中…</div>';
|
|
229
202
|
let files;
|
|
230
203
|
try {
|
|
231
204
|
const r = await api('GET', `/api/skills/files?path=${encodeURIComponent(dir)}`);
|
|
232
205
|
files = r.files || [];
|
|
233
206
|
} catch (e) {
|
|
234
|
-
container.innerHTML = `<div class="
|
|
207
|
+
container.innerHTML = `<div class="hint" style="padding:2px 6px;color:var(--err-tx)">${esc(e.message)}</div>`;
|
|
235
208
|
return;
|
|
236
209
|
}
|
|
237
210
|
container.innerHTML = '';
|
|
238
211
|
if (files.length === 0) {
|
|
239
|
-
container.innerHTML = '<div class="
|
|
212
|
+
container.innerHTML = '<div class="hint" style="padding:2px 6px">(空目录)</div>';
|
|
240
213
|
return;
|
|
241
214
|
}
|
|
242
215
|
for (const f of files) {
|
|
243
|
-
const row = document.createElement('
|
|
244
|
-
row.style.cssText = 'padding:
|
|
245
|
-
row.onmouseenter = () => { row.style.background = 'var(--
|
|
246
|
-
row.onmouseleave = () => { row.style.background = ''; };
|
|
216
|
+
const row = document.createElement('button');
|
|
217
|
+
row.style.cssText = 'display:block;width:100%;text-align:left;border:0;background:none;font:inherit;cursor:pointer;padding:4px 8px;border-radius:7px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis';
|
|
218
|
+
row.onmouseenter = () => { row.style.background = 'var(--pri-w)'; };
|
|
219
|
+
row.onmouseleave = () => { if (!row.dataset.active) row.style.background = ''; };
|
|
247
220
|
const childPath = `${dir.replace(/[\\/]+$/, '')}/${f.name}`;
|
|
248
221
|
if (f.isDir) {
|
|
249
222
|
row.textContent = `▸ 📁 ${f.name}`;
|
|
@@ -267,7 +240,7 @@ function browseSkill(s) {
|
|
|
267
240
|
row.onclick = () => {
|
|
268
241
|
tree.querySelectorAll('[data-active]').forEach((x) => { delete x.dataset.active; x.style.background = ''; x.style.fontWeight = ''; });
|
|
269
242
|
row.dataset.active = '1';
|
|
270
|
-
row.style.background = 'var(--
|
|
243
|
+
row.style.background = 'var(--pri-w)';
|
|
271
244
|
row.style.fontWeight = '600';
|
|
272
245
|
void openFile(childPath);
|
|
273
246
|
};
|
|
@@ -283,13 +256,14 @@ function browseSkill(s) {
|
|
|
283
256
|
function addSkill() {
|
|
284
257
|
openDrawer({
|
|
285
258
|
title: '新建 skill(用户级)',
|
|
259
|
+
narrow: true,
|
|
286
260
|
bodyHtml: `
|
|
287
|
-
<label>名字(字母/数字/下划线/连字符,1-64 位)</label>
|
|
261
|
+
<label class="fld">名字(字母/数字/下划线/连字符,1-64 位)</label>
|
|
288
262
|
<input type="text" id="nskName" placeholder="my-skill">
|
|
289
|
-
<label>说明(frontmatter description)</label>
|
|
263
|
+
<label class="fld">说明(frontmatter description)</label>
|
|
290
264
|
<input type="text" id="nskDesc" placeholder="这个 skill 做什么、什么时候用">
|
|
291
|
-
<div class="
|
|
292
|
-
footHtml: `<button class="btn" data-act="cancel">取消</button><button class="btn primary" data-act="save">创建</button>`,
|
|
265
|
+
<div class="mini-note" style="margin-top:10px">创建后生成目录与 SKILL.md 模板,请到目录中补充正文与脚本。</div>`,
|
|
266
|
+
footHtml: `<span class="sp"></span><button class="btn" data-act="cancel">取消</button><button class="btn primary" data-act="save">创建</button>`,
|
|
293
267
|
onMount({ foot }) {
|
|
294
268
|
foot.querySelector('[data-act="cancel"]').onclick = () => closeDrawer();
|
|
295
269
|
foot.querySelector('[data-act="save"]').onclick = async () => {
|
|
@@ -337,6 +311,8 @@ function bindZipImport() {
|
|
|
337
311
|
export const page = {
|
|
338
312
|
id: 'skills',
|
|
339
313
|
title: 'Skills',
|
|
314
|
+
desc: '四个来源的技能聚合:本机 / bridge / 项目 / 插件(只读)',
|
|
315
|
+
legend: false,
|
|
340
316
|
icon: '<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>',
|
|
341
317
|
render: renderSkills,
|
|
342
318
|
};
|