@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.
- package/assets/web/css/main.css +774 -574
- package/assets/web/index.html +37 -17
- package/assets/web/js/auth.js +218 -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 +481 -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 +43 -46
- package/assets/web/js/pages/overview.js +426 -88
- package/assets/web/js/pages/permissions.js +121 -34
- 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/access/access-control.d.ts +18 -0
- package/dist/access/access-control.js +29 -0
- package/dist/access/access-control.js.map +1 -1
- package/dist/bin/lcb.js +15 -0
- package/dist/bin/lcb.js.map +1 -1
- package/dist/feishu/name-cache.d.ts +15 -0
- package/dist/feishu/name-cache.js +82 -0
- package/dist/feishu/name-cache.js.map +1 -0
- package/dist/feishu/sibling-names.d.ts +1 -0
- package/dist/feishu/sibling-names.js +63 -0
- package/dist/feishu/sibling-names.js.map +1 -0
- package/dist/gateway/feishu-gateway.d.ts +24 -0
- package/dist/gateway/feishu-gateway.js +132 -0
- package/dist/gateway/feishu-gateway.js.map +1 -1
- package/dist/gateway/gateway-registry.d.ts +18 -0
- package/dist/gateway/gateway-registry.js +14 -0
- package/dist/gateway/gateway-registry.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +52 -9
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/web/chats-api.d.ts +93 -0
- package/dist/web/chats-api.js +603 -0
- package/dist/web/chats-api.js.map +1 -0
- package/dist/web/lifecycle.d.ts +21 -0
- package/dist/web/lifecycle.js +80 -3
- package/dist/web/lifecycle.js.map +1 -1
- 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 +196 -13
- package/dist/web/server.js.map +1 -1
- package/dist/web/webauth.d.ts +44 -0
- package/dist/web/webauth.js +221 -0
- package/dist/web/webauth.js.map +1 -0
- package/package.json +1 -1
|
@@ -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
|
};
|
|
@@ -2,21 +2,24 @@
|
|
|
2
2
|
import { S, $, esc, toast, api } from '../core.js';
|
|
3
3
|
import { openDrawer, closeDrawer, confirmDialog } from '../ui.js';
|
|
4
4
|
|
|
5
|
+
const IC_INFO = '<circle cx="12" cy="12" r="9"/><path d="M12 8h.01"/><path d="M12 12v4"/>';
|
|
6
|
+
const svg = (inner, s = 13) => `<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
|
+
|
|
5
8
|
function renderSlash(el) {
|
|
6
9
|
el.innerHTML = `
|
|
7
|
-
<div class="
|
|
8
|
-
<h3
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<
|
|
10
|
+
<div class="panel">
|
|
11
|
+
<header class="panel-head"><h3>已注册命令</h3>
|
|
12
|
+
<div class="panel-actions">
|
|
13
|
+
<select id="scApp" style="width:auto;padding:4px 10px;font-size:12.5px"></select>
|
|
14
|
+
<button class="btn sm" id="scReload">刷新</button>
|
|
15
|
+
<button class="btn sm" id="scBuiltins">补齐内置</button>
|
|
16
|
+
</div></header>
|
|
17
|
+
<div class="panel-body">
|
|
18
|
+
<table><thead><tr><th style="width:170px">命令</th><th>描述</th><th style="width:130px">图标</th><th style="width:150px">操作</th></tr></thead>
|
|
19
|
+
<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>
|
|
20
|
+
<pre class="report" id="scReport" style="display:none;margin-top:12px"></pre>
|
|
16
21
|
</div>
|
|
17
|
-
<
|
|
18
|
-
<tbody id="scBody"><tr><td colspan="4" style="padding:36px 0"><div class="br-spin-sm"></div><div class="hint" style="text-align:center">正在从飞书拉取斜杆命令…</div></td></tr></tbody></table>
|
|
19
|
-
<pre class="report" id="scReport" style="display:none;margin-top:12px"></pre>
|
|
22
|
+
<footer class="panel-foot">${svg(IC_INFO)} 直接读写飞书开放平台(需 application:app_slash_command 权限并发布版本),改动约 5 分钟生效(PC 端 7.70+);<b>本页操作即时提交飞书,与「保存」按钮无关</b>。聊天输入 <code>/</code> 弹出指令面板。</footer>
|
|
20
23
|
</div>`;
|
|
21
24
|
const appSel = $('#scApp');
|
|
22
25
|
(S.status?.apps || []).forEach((a) => {
|
|
@@ -39,11 +42,11 @@ function renderSlash(el) {
|
|
|
39
42
|
<td><code>/${esc(x.command)}</code></td>
|
|
40
43
|
<td>${esc(x.description?.default_value || '')}</td>
|
|
41
44
|
<td class="hint">${esc(x.description?.icon?.icon_key || '—')}</td>
|
|
42
|
-
<td>
|
|
45
|
+
<td class="ops">
|
|
43
46
|
<button class="btn sm" data-op="edit">编辑</button>
|
|
44
47
|
<button class="btn sm danger" data-op="del">删除</button>
|
|
45
48
|
</td>
|
|
46
|
-
</tr>`).join('') || '<tr><td colspan="4"
|
|
49
|
+
</tr>`).join('') || '<tr class="empty"><td colspan="4">远端暂无命令;点「补齐内置」一键注册 bridge 内置指令</td></tr>';
|
|
47
50
|
$('#scBody').querySelectorAll('button[data-op]').forEach((b) => b.onclick = () => {
|
|
48
51
|
const tr = b.closest('tr');
|
|
49
52
|
if (b.dataset.op === 'del') return delRemote(tr.dataset.cid, tr.dataset.cmd);
|
|
@@ -55,7 +58,7 @@ function renderSlash(el) {
|
|
|
55
58
|
});
|
|
56
59
|
});
|
|
57
60
|
} catch (e) {
|
|
58
|
-
$('#scBody').innerHTML = `<tr><td colspan="4" class="hint">加载失败:${esc(e.message)}
|
|
61
|
+
$('#scBody').innerHTML = `<tr><td colspan="4" class="hint">加载失败:${esc(e.message)}(确认已开通斜杠命令权限后点「刷新」重试)</td></tr>`;
|
|
59
62
|
}
|
|
60
63
|
};
|
|
61
64
|
const delRemote = async (cid, cmd) => {
|
|
@@ -74,17 +77,19 @@ function renderSlash(el) {
|
|
|
74
77
|
const openSlashDrawer = ({ commandId, command, description, icon }) => {
|
|
75
78
|
const isCreate = !commandId;
|
|
76
79
|
openDrawer({
|
|
77
|
-
title: isCreate ? '
|
|
80
|
+
title: isCreate ? '新增命令' : `编辑 /${command}`,
|
|
81
|
+
narrow: true,
|
|
78
82
|
bodyHtml: `
|
|
79
|
-
<label>命令(不带
|
|
83
|
+
<label class="fld">命令(不带 /${isCreate ? '' : ';飞书侧命令名不可修改,如需改名请删除后重建'})</label>
|
|
80
84
|
<input type="text" id="dcCmd" ${isCreate ? '' : 'disabled title="飞书侧命令名不可修改(如需改名请删除后重建)"'} value="${esc(command || '')}" placeholder="produce">
|
|
81
|
-
<label>描述(指令面板展示)</label>
|
|
85
|
+
<label class="fld">描述(指令面板展示)</label>
|
|
82
86
|
<input type="text" id="dcDesc" value="${esc(description || '')}" placeholder="内容生产流程">
|
|
83
|
-
<label>图标 icon_key(可空,取值见飞书开放平台图标列表)</label>
|
|
87
|
+
<label class="fld">图标 icon_key(可空,取值见飞书开放平台图标列表)</label>
|
|
84
88
|
<input type="text" id="dcIcon" value="${esc(icon || '')}" placeholder="skill_outlined">`,
|
|
85
|
-
footHtml: `<button class="btn primary" id="dcSave">${isCreate ? '
|
|
86
|
-
onMount: () => {
|
|
87
|
-
|
|
89
|
+
footHtml: `<span class="sp"></span><button class="btn" id="dcCancel">取消</button><button class="btn primary" id="dcSave">${isCreate ? '创建' : '保存'}</button>`,
|
|
90
|
+
onMount: ({ foot }) => {
|
|
91
|
+
foot.querySelector('#dcCancel').onclick = closeDrawer;
|
|
92
|
+
foot.querySelector('#dcSave').onclick = async () => {
|
|
88
93
|
const cmd = document.getElementById('dcCmd').value.trim();
|
|
89
94
|
const desc = document.getElementById('dcDesc').value.trim();
|
|
90
95
|
const ic = document.getElementById('dcIcon').value.trim();
|
|
@@ -96,7 +101,7 @@ function renderSlash(el) {
|
|
|
96
101
|
} else {
|
|
97
102
|
await api('POST', '/api/slash-commands/action', { op: 'update', app: appSel.value, commandId, description: desc, icon: ic });
|
|
98
103
|
}
|
|
99
|
-
toast(isCreate ? `已创建 /${cmd}` : '已保存');
|
|
104
|
+
toast(`${isCreate ? `已创建 /${cmd}` : '已保存'}(即时提交飞书,约 5 分钟生效)`);
|
|
100
105
|
closeDrawer();
|
|
101
106
|
loadRemote();
|
|
102
107
|
} catch (e) { toast(e.message, true); }
|
|
@@ -106,6 +111,7 @@ function renderSlash(el) {
|
|
|
106
111
|
};
|
|
107
112
|
appSel.onchange = loadRemote;
|
|
108
113
|
$('#scReload').onclick = loadRemote;
|
|
114
|
+
$('#pageActions').innerHTML = '<button class="btn primary" id="scAdd">新增命令</button>';
|
|
109
115
|
$('#scAdd').onclick = () => openSlashDrawer({ command: '', description: '', icon: '' });
|
|
110
116
|
$('#scBuiltins').onclick = async () => {
|
|
111
117
|
if (!appSel.value) return;
|
|
@@ -129,6 +135,8 @@ function renderSlash(el) {
|
|
|
129
135
|
export const page = {
|
|
130
136
|
id: 'slash',
|
|
131
137
|
title: '斜杠命令',
|
|
138
|
+
desc: '注册到飞书开放平台的指令面板,聊天框输入 / 触发',
|
|
139
|
+
legend: false,
|
|
132
140
|
icon: '<path d="m4 17 6-6-6-6"/><path d="M12 19h8"/>',
|
|
133
141
|
render: renderSlash,
|
|
134
142
|
};
|