@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,10 +1,12 @@
1
- // ============ Skills 管理页(#12)============
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
- // 远端 CRUD:改动即时落盘,无页内保存条
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="card">
26
- <h3>Skills</h3>
27
- <div class="desc">Claude Code 技能(每项一个目录,含 SKILL.md)。列表聚合四个来源:本机用户级、bridge 自管用户级、各工作区项目级、已装插件内的 skills(只读展示)。新增(含 zip 导入)统一进用户级生效目录。</div>
28
- <div class="list-toolbar">
29
- <button class="btn" id="skillImport">导入 zip</button>
30
- <button class="btn primary" id="skillAdd">+ 新建</button>
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
- <input type="file" id="skillZipFile" accept=".zip" style="display:none">
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
- $('#skillSearch').oninput = filterSkillRows;
47
+ bindSearch();
43
48
  bindZipImport();
44
49
  void loadSkills();
45
50
  }
46
51
 
47
- /** 搜索过滤:遍历 tr.dataset.searchText 切换 hidden */
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="5" class="desc">加载失败:${esc(e.message)}</td></tr>`;
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="5" class="desc">暂无 skill(四个来源均未发现 SKILL.md)。可点上方「+ 新建」或「导入 zip」。</td></tr>`;
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
- tr.dataset.searchText = `${s.name} ${sourceTag(s)} ${s.path} ${s.description || ''}`.toLowerCase();
106
+ // 搜索只匹配名称 + 说明两列(路径/来源不参与,避免全字段模糊误命中)
107
+ tr.dataset.searchText = `${s.name} ${s.description || ''}`.toLowerCase();
94
108
  tr.innerHTML = `
95
- <td><strong>${esc(s.name)}</strong></td>
96
- <td>${esc(s.description) || '<span class="desc">(无说明)</span>'}</td>
97
- <td><span class="chip">${tag}</span></td>
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 文件浏览(#3):目录树 + 原文/视图/双栏三种查看模式 ----------
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 class="desc">${esc(s.path)}</code></div>
173
- <div style="display:flex;gap:14px;align-items:flex-start">
174
- <div id="sbTree" style="width:250px;flex:none;border:1px solid var(--border);border-radius:8px;padding:8px;max-height:62vh;overflow:auto;font-size:13px">
175
- <div class="desc">目录加载中…</div>
176
- </div>
177
- <div style="flex:1;min-width:0">
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="desc" style="margin-bottom:6px">点击左侧文件查看内容</div>
184
- <div id="sbView" style="display:none;border:1px solid var(--border);border-radius:8px;padding:12px;max-height:56vh;overflow:auto;font-size:13px;line-height:1.6"></div>
185
- <textarea id="sbRaw" readonly style="display:none;width:100%;min-height:56vh;font-family:ui-monospace,Consolas,monospace;font-size:12px"></textarea>
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 = `<strong>${esc(r.name)}</strong> <span class="desc">· ${(r.size / 1024).toFixed(1)}KB</span>`;
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="desc" style="padding:2px 6px">加载中…</div>';
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="desc" style="padding:2px 6px;color:var(--err-tx)">${esc(e.message)}</div>`;
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="desc" style="padding:2px 6px">(空目录)</div>';
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('div');
244
- row.style.cssText = 'padding:3px 6px;border-radius:6px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis';
245
- row.onmouseenter = () => { row.style.background = 'var(--off-bg)'; };
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(--primary-weak)';
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="hint" style="margin-top:10px">创建后生成目录与 SKILL.md 模板,请到目录中补充正文与脚本。</div>`,
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="card">
8
- <h3>飞书斜杠命令(远端管理)</h3>
9
- <div class="desc">直接读写飞书开放平台已注册的斜杠命令;聊天输入 <code>/</code> 弹出指令面板,选中后可继续输入描述再发送。需给应用开通 <code>application:app_slash_command</code> 读写权限并发布版本;改动约 5 分钟后生效(PC 端 7.70+)。<b>本页操作即时提交飞书,与底部「保存」按钮无关。</b></div>
10
- <div class="list-toolbar">
11
- <div style="max-width:320px;width:100%"><label>目标应用</label><select id="scApp"></select></div>
12
- <span class="spacer"></span>
13
- <button class="btn" id="scReload">刷新列表</button>
14
- <button class="btn" id="scBuiltins">补齐内置命令</button>
15
- <button class="btn primary" id="scAdd">+ 新增命令</button>
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
- <table style="margin-top:12px"><thead><tr><th style="width:170px">命令</th><th>描述</th><th style="width:150px">图标</th><th style="width:130px">操作</th></tr></thead>
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" class="hint">远端暂无命令;点「补齐内置命令」一键注册 bridge 内置指令</td></tr>';
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)}(确认已开通斜杠命令权限后点「刷新列表」重试)</td></tr>`;
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 ? '新增斜杠命令' : `编辑 /${command}`,
80
+ title: isCreate ? '新增命令' : `编辑 /${command}`,
81
+ narrow: true,
78
82
  bodyHtml: `
79
- <label>命令(不带 /)</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 ? '创建(即时提交飞书)' : '保存(即时提交飞书)'}</button>`,
86
- onMount: () => {
87
- document.getElementById('dcSave').onclick = async () => {
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
  };