@jesonliu/lark-claudecode-bridge 1.0.6 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/assets/web/css/main.css +46 -0
- package/assets/web/index.html +1 -0
- package/assets/web/js/pages/apps.js +118 -6
- package/assets/web/js/pages/claude.js +4 -7
- package/assets/web/js/pages/overview.js +57 -8
- package/assets/web/js/pages/permissions.js +45 -10
- package/assets/web/js/ui.js +167 -0
- package/config.example.yaml +20 -2
- package/dist/config-defaults.js +2 -1
- package/dist/config-defaults.js.map +1 -1
- package/dist/config.d.ts +4 -2
- package/dist/config.js +80 -5
- package/dist/config.js.map +1 -1
- package/dist/executor/claude-executor.d.ts +3 -0
- package/dist/executor/claude-executor.js +3 -0
- package/dist/executor/claude-executor.js.map +1 -1
- package/dist/executor/notify-server.d.ts +4 -18
- package/dist/executor/notify-server.js +6 -74
- package/dist/executor/notify-server.js.map +1 -1
- package/dist/executor/permission-gate.d.ts +10 -0
- package/dist/executor/permission-gate.js +24 -7
- package/dist/executor/permission-gate.js.map +1 -1
- package/dist/gateway/card-builder.d.ts +30 -19
- package/dist/gateway/card-builder.js +63 -46
- package/dist/gateway/card-builder.js.map +1 -1
- package/dist/gateway/diff-card.d.ts +1 -2
- package/dist/gateway/diff-card.js +2 -3
- package/dist/gateway/diff-card.js.map +1 -1
- package/dist/gateway/feishu-gateway.d.ts +2 -2
- package/dist/gateway/feishu-gateway.js +35 -5
- package/dist/gateway/feishu-gateway.js.map +1 -1
- package/dist/gateway/progress-card.d.ts +15 -5
- package/dist/gateway/progress-card.js +29 -14
- package/dist/gateway/progress-card.js.map +1 -1
- package/dist/index.js +198 -94
- package/dist/index.js.map +1 -1
- package/dist/notify-sop.d.ts +1 -1
- package/dist/notify-sop.js +21 -26
- package/dist/notify-sop.js.map +1 -1
- package/dist/session/commands.d.ts +20 -2
- package/dist/session/commands.js +58 -17
- package/dist/session/commands.js.map +1 -1
- package/dist/types.d.ts +27 -7
- package/dist/web/autostart.d.ts +49 -0
- package/dist/web/autostart.js +250 -0
- package/dist/web/autostart.js.map +1 -0
- package/dist/web/config-api.js +14 -2
- package/dist/web/config-api.js.map +1 -1
- package/dist/web/server.js +38 -1
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ lcb start
|
|
|
52
52
|
- **飞书应用** —— 多机器人管理:App ID / Secret(脱敏回显)、默认工作区、并发上限、人格补充(`append_system_prompt`)、触发词、环境变量。
|
|
53
53
|
- **工作区** —— 工作区白名单(名称 / 路径)与全局默认工作区,改动热生效。
|
|
54
54
|
- **Claude 认证** —— inherit / managed 双模式切换、认证凭证(API Key / Auth Token / Base URL)、模型、厂商档案(多套凭证一键切换)、托管环境变量。
|
|
55
|
-
- **权限** ——
|
|
55
|
+
- **权限** —— 全部工具免确认开关(`permissions.allow_all_tools`,缺省开)、免确认工具白名单(`permissions.allow_tools`)与危险命令黑名单,保存后热生效。
|
|
56
56
|
- **斜杠命令** —— 把内置命令(`/new` `/status` …)+ 自定义透传命令一键同步为飞书输入框斜杠指令(输入 `/` 弹面板,选中后可继续输入描述再发送)。
|
|
57
57
|
- **插件** —— Claude Code 插件清单(启停 / 卸载,本机 `~/.claude` 与托管目录带来源标记)、从 marketplace 安装、管理市场。
|
|
58
58
|
- **Skills** —— 四来源技能聚合清单(本机用户级 / bridge 托管 / 工作区项目级 / 插件内只读),支持新建、删除、zip 导入。
|
|
@@ -122,12 +122,14 @@ workspaces: # 工作区白名单(列表全局共享;「当前
|
|
|
122
122
|
defaults:
|
|
123
123
|
workspace: demo
|
|
124
124
|
concurrency: 3 # 通道间并发上限(未单独配置的 app 沿用)
|
|
125
|
-
# permissions: #
|
|
126
|
-
#
|
|
125
|
+
# permissions: # 工具确认与危险命令(整块可选;setup / 配置页新建时预置完整默认值,页面可增删)
|
|
126
|
+
# allow_all_tools: true # 全部工具免确认(缺省开):开启后除命中 dangerous_commands 的 Bash 外,
|
|
127
|
+
# # 所有工具(含 Write/Edit)都不再弹确认卡;关闭则回落 allow_tools 白名单语义
|
|
128
|
+
# allow_tools: # 免确认直通工具(仅 allow_all_tools 关闭时生效);配置即整体替换内置默认
|
|
127
129
|
# # 内置默认:Read/Glob/Grep/LS/TodoRead/TodoWrite/WebFetch/WebSearch/Bash
|
|
128
130
|
# dangerous_commands: # Bash 危险命令正则(不区分大小写),命中弹确认卡
|
|
129
131
|
# - 'rm\s+-rf' # 内置默认覆盖 rm -rf/sudo/git push --force/git reset --hard/mkfs/dd if=/
|
|
130
|
-
# # chmod 777/管道执行远程脚本/shutdown
|
|
132
|
+
# # chmod 777/管道执行远程脚本/shutdown 等;allow_all_tools 开启时是唯一安全网
|
|
131
133
|
# server: # Web 配置页(随 lcb start 常驻;也可 lcb ui 单独启动)
|
|
132
134
|
# enabled: true # 缺省 true;false 则不启动
|
|
133
135
|
# host: 127.0.0.1 # 仅绑回环(改非回环 = 局域网可见,注意安全)
|
package/assets/web/css/main.css
CHANGED
|
@@ -210,6 +210,9 @@ main { flex: 1; min-width: 0; padding: 24px 36px 56px; }
|
|
|
210
210
|
.card h3 { margin: 0 0 4px; font-size: 15px; font-weight: 650; }
|
|
211
211
|
.card h3 .dirty-dot { display: none; margin-left: 6px; font-size: 11.5px; color: var(--warn); font-weight: 500; }
|
|
212
212
|
.card .desc { color: var(--muted); font-size: 12.5px; margin-bottom: 12px; line-height: 1.7; }
|
|
213
|
+
/* 卡内小节(概览「版本与更新」双区块):相邻小节间分隔线,首节不加(免疫未来插入其它 div) */
|
|
214
|
+
.card-sub + .card-sub { border-top: 1px solid var(--border); margin-top: 14px; padding-top: 14px; }
|
|
215
|
+
.card-sub-title { margin: 0 0 8px; font-size: 13.5px; font-weight: 650; }
|
|
213
216
|
main h4 { font-size: 13.5px; font-weight: 600; }
|
|
214
217
|
|
|
215
218
|
/* ---------- 表单 ---------- */
|
|
@@ -314,6 +317,31 @@ tr.empty-row td { text-align: center; padding: 22px 10px; color: var(--muted); }
|
|
|
314
317
|
.radio-row label { display: flex; align-items: center; gap: 6px; margin: 0; color: var(--text); font-size: 13.5px; cursor: pointer; }
|
|
315
318
|
.radio-row input { accent-color: var(--primary); }
|
|
316
319
|
|
|
320
|
+
/* ---------- 开关(原生 checkbox + 伪元素轨道;覆盖全局 label 的 block/margin) ---------- */
|
|
321
|
+
.switch-row { display: flex; align-items: center; gap: 10px; margin: 10px 0 8px; }
|
|
322
|
+
/* 表格单元格等紧凑容器用:去掉表单区默认的上下边距,避免与同行左侧文字错位 */
|
|
323
|
+
.switch-row.tight { margin: 0; }
|
|
324
|
+
.switch {
|
|
325
|
+
position: relative; display: inline-flex; flex: 0 0 auto;
|
|
326
|
+
width: 42px; height: 24px; margin: 0; cursor: pointer;
|
|
327
|
+
}
|
|
328
|
+
.switch input { position: absolute; width: 0; height: 0; opacity: 0; }
|
|
329
|
+
.switch .track {
|
|
330
|
+
position: absolute; inset: 0; border-radius: 999px;
|
|
331
|
+
background: var(--border-strong); transition: background .16s;
|
|
332
|
+
}
|
|
333
|
+
.switch .track::after {
|
|
334
|
+
content: ''; position: absolute; top: 2px; left: 2px; width: 20px; height: 20px;
|
|
335
|
+
border-radius: 50%; background: #fff; box-shadow: var(--shadow-1); transition: transform .16s;
|
|
336
|
+
}
|
|
337
|
+
/* 开启 = 全部放行(放手模式),用 warn 色提示当前处于高风险态 */
|
|
338
|
+
.switch input:checked + .track { background: var(--warn); }
|
|
339
|
+
.switch input:checked + .track::after { transform: translateX(18px); }
|
|
340
|
+
.switch input:focus-visible + .track { box-shadow: 0 0 0 3px var(--primary-ring); }
|
|
341
|
+
.switch-label { font-size: 13.5px; color: var(--text); }
|
|
342
|
+
/* 开关开启时名单区暂不生效:整体置灰且不可交互(顺带让「恢复默认」等按钮不可点) */
|
|
343
|
+
.block-disabled { opacity: .45; pointer-events: none; }
|
|
344
|
+
|
|
317
345
|
/* ---------- 保存条 / 未保存标记 ---------- */
|
|
318
346
|
.savebar {
|
|
319
347
|
display: flex; gap: 10px; margin-top: 14px; align-items: center;
|
|
@@ -370,6 +398,7 @@ tr.empty-row td { text-align: center; padding: 22px 10px; color: var(--muted); }
|
|
|
370
398
|
.drawer-mask { position: fixed; inset: 0; background: rgba(15, 19, 24, .38); z-index: 50; display: flex; justify-content: flex-end; animation: lcb-fade .18s ease; }
|
|
371
399
|
.drawer-mask[hidden] { display: none; }
|
|
372
400
|
.drawer {
|
|
401
|
+
position: relative; /* 拖拽手柄的定位基准 */
|
|
373
402
|
width: min(980px, 94vw); height: 100%;
|
|
374
403
|
background: var(--card); border-left: 1px solid var(--border);
|
|
375
404
|
display: flex; flex-direction: column;
|
|
@@ -377,6 +406,12 @@ tr.empty-row td { text-align: center; padding: 22px 10px; color: var(--muted); }
|
|
|
377
406
|
animation: lcb-drawer .22s cubic-bezier(.22, .8, .36, 1);
|
|
378
407
|
}
|
|
379
408
|
@keyframes lcb-drawer { from { transform: translateX(48px); opacity: .4; } }
|
|
409
|
+
/* 左边缘拖拽调宽手柄:8px 命中区(半出界更易命中),视觉条仅 2px、悬停/拖拽时显主题色 */
|
|
410
|
+
.drawer-resize { position: absolute; left: -4px; top: 0; bottom: 0; width: 8px; cursor: ew-resize; z-index: 2; touch-action: none; }
|
|
411
|
+
.drawer-resize::after { content: ''; position: absolute; left: 3px; top: 0; bottom: 0; width: 2px; background: transparent; transition: background .15s; }
|
|
412
|
+
.drawer-resize:hover::after, .drawer-resize.dragging::after { background: var(--primary); }
|
|
413
|
+
/* 拖拽期间全局改光标 + 禁选中(防正文文字反白跟随) */
|
|
414
|
+
body.drawer-resizing { cursor: ew-resize; user-select: none; }
|
|
380
415
|
.drawer > header { display: flex; align-items: center; justify-content: space-between; padding: 0 20px; height: 52px; border-bottom: 1px solid var(--border); position: static; background: none; -webkit-backdrop-filter: none; backdrop-filter: none; }
|
|
381
416
|
.drawer > header h3 { margin: 0; font-size: 15px; font-weight: 650; }
|
|
382
417
|
.drawer-body { flex: 1; overflow-y: auto; padding: 18px 22px; }
|
|
@@ -415,6 +450,17 @@ tr.empty-row td { text-align: center; padding: 22px 10px; color: var(--muted); }
|
|
|
415
450
|
.combo-item b { margin-right: 6px; }
|
|
416
451
|
.combo-empty { padding: 12px; color: var(--muted); font-size: 12.5px; }
|
|
417
452
|
|
|
453
|
+
/* ---------- 可搜索多选字段(分身白名单):外框对齐输入框观感,chips + 搜索行 ---------- */
|
|
454
|
+
.msel { position: relative; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--field); padding: 2px 10px; transition: border-color .15s, box-shadow .15s; }
|
|
455
|
+
.msel:focus-within { border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-ring); }
|
|
456
|
+
.msel-chips { margin: 4px 0; }
|
|
457
|
+
.msel-chips:empty { display: none; }
|
|
458
|
+
.msel-input { border: 0; background: none; box-shadow: none; width: 100%; padding: 6px 2px; font-size: 13.5px; }
|
|
459
|
+
.msel-input:focus { border: 0; box-shadow: none; } /* 覆盖全局 input:focus(焦点态由 .msel:focus-within 呈现) */
|
|
460
|
+
.combo-item.active { background: var(--primary-weak); } /* 键盘高亮(:hover 已有) */
|
|
461
|
+
.msel-check { float: right; color: var(--primary); font-weight: 650; }
|
|
462
|
+
.chip.ghost { border-style: dashed; color: var(--muted); }
|
|
463
|
+
|
|
418
464
|
/* ---------- 操作进度弹框(update-all 真实 i/N,单操作时间缓动模拟) ---------- */
|
|
419
465
|
.prog { height: 8px; border-radius: 999px; background: var(--off-bg); border: 1px solid var(--border); overflow: hidden; margin-top: 12px; }
|
|
420
466
|
.prog-bar { height: 100%; width: 0%; background: linear-gradient(90deg, var(--primary), var(--primary-strong)); border-radius: 999px; transition: width .3s; }
|
package/assets/web/index.html
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
</div>
|
|
31
31
|
<div class="drawer-mask" id="drawerMask" hidden>
|
|
32
32
|
<aside class="drawer" role="dialog" aria-modal="true">
|
|
33
|
+
<div class="drawer-resize" id="drawerResize" title="拖拽调宽 · 双击恢复默认" aria-hidden="true"></div>
|
|
33
34
|
<header><h3 id="drawerTitle"></h3><button class="btn" id="drawerClose">✕ 关闭</button></header>
|
|
34
35
|
<div class="drawer-body" id="drawerBody"></div>
|
|
35
36
|
<footer class="drawer-foot" id="drawerFoot"></footer>
|
|
@@ -1,13 +1,69 @@
|
|
|
1
1
|
// ============ 应用(列表 + 抽屉编辑;输入实时写回 S.doc.apps,抽屉「保存」一次落盘) ============
|
|
2
|
-
import { S, $, esc, toast, snapDoc, saveDoc } from '../core.js';
|
|
3
|
-
import { openDrawer, closeDrawer, cancelDrawer, confirmDialog } from '../ui.js';
|
|
2
|
+
import { S, $, esc, toast, api, snapDoc, saveDoc } from '../core.js';
|
|
3
|
+
import { openDrawer, closeDrawer, cancelDrawer, confirmDialog, multiSelectField } from '../ui.js';
|
|
4
|
+
|
|
5
|
+
// ---- 分身白名单多选候选(/api/skills + /api/plugins;60s TTL + in-flight 去重 + stale-if-error) ----
|
|
6
|
+
/** 纯函数(可测):技能清单 → 多选候选。插件来源 value 用 `plugin:skill` 限定格式
|
|
7
|
+
* (SDK Options.skills 契约,sdk.d.ts:插件技能须限定名),裸技能用目录名;按 value 去重首胜 */
|
|
8
|
+
export function toSkillOptions(skills) {
|
|
9
|
+
const out = [];
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
for (const s of skills || []) {
|
|
12
|
+
const value = s.source === 'plugin' ? `${s.pluginName}:${s.name}` : s.name;
|
|
13
|
+
if (!value || seen.has(value)) continue;
|
|
14
|
+
seen.add(value);
|
|
15
|
+
const tag = s.source === 'plugin'
|
|
16
|
+
? `插件${s.pluginEnabled ? '' : ' · 未启用'}`
|
|
17
|
+
: s.source === 'project' ? `项目 · ${s.workspaceName || '?'}`
|
|
18
|
+
: s.source === 'managed' ? 'bridge' : '本机';
|
|
19
|
+
out.push({ value, desc: s.description, tag });
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 纯函数(可测):已装插件清单 → 多选候选。value = name(installed_plugins.json 键的
|
|
25
|
+
* @ 前段,与后端 allowedPlugins.includes(p.name) 匹配口径同源);未启用插件标记提醒 */
|
|
26
|
+
export function toPluginOptions(plugins) {
|
|
27
|
+
const out = [];
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
for (const p of plugins || []) {
|
|
30
|
+
if (!p.name || seen.has(p.name)) continue;
|
|
31
|
+
seen.add(p.name);
|
|
32
|
+
out.push({
|
|
33
|
+
value: p.name,
|
|
34
|
+
desc: p.description,
|
|
35
|
+
tag: `${p.source === 'bridge' ? 'bridge' : '本机'}${p.enabled ? '' : ' · 未启用'}${p.version ? ` · v${p.version}` : ''}`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const CAND = { at: 0, skills: null, plugins: null, skillsError: '', pluginsError: '', inflight: null };
|
|
42
|
+
const CAND_TTL = 60_000;
|
|
43
|
+
/** 拉候选(失败时保留旧缓存继续用;全新失败返回 error 字段,组件空候选但已选值不受影响) */
|
|
44
|
+
function loadDeputyCandidates() {
|
|
45
|
+
if (Date.now() - CAND.at < CAND_TTL && (CAND.skills || CAND.plugins)) return Promise.resolve({ ...CAND });
|
|
46
|
+
if (CAND.inflight) return CAND.inflight;
|
|
47
|
+
CAND.inflight = (async () => {
|
|
48
|
+
const [sk, pl] = await Promise.allSettled([api('GET', '/api/skills'), api('GET', '/api/plugins')]);
|
|
49
|
+
CAND.at = Date.now();
|
|
50
|
+
// stale-if-error:失败且有旧缓存 → 沿用旧值(error 只在无缓存可退时向上暴露)
|
|
51
|
+
if (sk.status === 'fulfilled') { CAND.skills = sk.value?.skills ?? []; CAND.skillsError = ''; }
|
|
52
|
+
else if (!CAND.skills) CAND.skillsError = sk.reason?.message || '未知错误';
|
|
53
|
+
if (pl.status === 'fulfilled') { CAND.plugins = pl.value?.plugins ?? []; CAND.pluginsError = ''; }
|
|
54
|
+
else if (!CAND.plugins) CAND.pluginsError = pl.reason?.message || '未知错误';
|
|
55
|
+
CAND.inflight = null;
|
|
56
|
+
return { ...CAND };
|
|
57
|
+
})();
|
|
58
|
+
return CAND.inflight;
|
|
59
|
+
}
|
|
4
60
|
|
|
5
61
|
function renderApps(el) {
|
|
6
62
|
const apps = S.doc.apps || (S.doc.apps = []);
|
|
7
63
|
el.innerHTML = `
|
|
8
64
|
<div class="card">
|
|
9
65
|
<h3>飞书应用(机器人)</h3>
|
|
10
|
-
<div class="desc">每个应用一条独立长连接与会话池。点击行或「编辑」在抽屉中配置;App Secret 已脱敏,留空 = 保持不变。凭证 / 名称 / 域名 / 默认工作区 / 并发 / 人格 /
|
|
66
|
+
<div class="desc">每个应用一条独立长连接与会话池。点击行或「编辑」在抽屉中配置;App Secret 已脱敏,留空 = 保持不变。凭证 / 名称 / 域名 / 默认工作区 / 并发 / 人格 / 角色改动需重启 lcb start;触发词、显式插件与环境变量改动下一条消息热生效。</div>
|
|
11
67
|
<div class="list-toolbar">
|
|
12
68
|
<button class="btn primary" id="addApp">+ 新增应用</button>
|
|
13
69
|
</div>
|
|
@@ -25,7 +81,7 @@ function renderApps(el) {
|
|
|
25
81
|
};
|
|
26
82
|
$('#appsBody').innerHTML = apps.map((app, i) => `
|
|
27
83
|
<tr data-i="${i}" style="cursor:pointer">
|
|
28
|
-
<td><b>${esc(app.name || app.app_id || '(未命名)')}</b
|
|
84
|
+
<td><b>${esc(app.name || app.app_id || '(未命名)')}</b>${app.role === 'deputy' ? ' <span class="tag off">分身</span>' : ''}</td>
|
|
29
85
|
<td><code>${esc(app.app_id || '')}</code></td>
|
|
30
86
|
<td>${esc(app.domain === 'lark' ? 'lark' : 'feishu')}</td>
|
|
31
87
|
<td>${esc(app.default_workspace || '(全局默认)')}</td>
|
|
@@ -77,8 +133,22 @@ function openAppDrawer(apps, idx, el, snap) {
|
|
|
77
133
|
<div><label>并发上限(1-100)</label>
|
|
78
134
|
<input type="number" data-f="concurrency" min="1" max="100" value="${esc(app.concurrency ?? '')}" placeholder="缺省用全局"></div>
|
|
79
135
|
</div>
|
|
80
|
-
<label
|
|
81
|
-
<
|
|
136
|
+
<label>角色(主机器人 = 全权限、首次使用需配对;分身机器人 = 限定能力、艾特即用供他人使用)</label>
|
|
137
|
+
<select data-f="role">
|
|
138
|
+
<option value="primary" ${app.role !== 'deputy' ? 'selected' : ''}>主机器人(全权限,配对码准入)</option>
|
|
139
|
+
<option value="deputy" ${app.role === 'deputy' ? 'selected' : ''}>分身机器人(限定技能 / 插件 / 工作区,艾特即用)</option>
|
|
140
|
+
</select>
|
|
141
|
+
<div id="deputyFields" style="${app.role === 'deputy' ? '' : 'display:none'}">
|
|
142
|
+
<div class="hint" style="margin:8px 0 2px">分身必填「允许的技能」与「允许的工作区」;未列入白名单的技能 / 插件不加载,任务锁定在第一个允许的工作区;管理命令在分身里一律不可用。改动需重启生效。</div>
|
|
143
|
+
<label>允许的技能(必填;输入关键词搜索选择,未列出的技能对模型不可见)</label>
|
|
144
|
+
<div data-msel="allowed_skills"></div>
|
|
145
|
+
<label>允许的工作区(必填;取值须在工作区列表内,任务锁定第一个)</label>
|
|
146
|
+
<div data-msel="allowed_workspaces"></div>
|
|
147
|
+
<label>允许的插件(可选;按名称过滤自动发现的插件,不配 = 不加载任何自动发现插件)</label>
|
|
148
|
+
<div data-msel="allowed_plugins"></div>
|
|
149
|
+
</div>
|
|
150
|
+
<label>人格补充(追加到该机器人每个会话的 system prompt,多机器人差异化定位)<span class="tag warn" style="margin-left:6px">改动需重启生效</span></label>
|
|
151
|
+
<textarea data-f="append_system_prompt" placeholder="你是我的超级助手,擅长代码开发、文案编写…(回复须带「主人」称谓等要求写在这里)">${esc(app.append_system_prompt || '')}</textarea>
|
|
82
152
|
<h4 style="margin:18px 0 2px;font-size:13.5px">触发词(命中即改写消息后再发给 Claude;下一条消息热生效)</h4>
|
|
83
153
|
<div class="hint" style="margin-bottom:6px">match 以 <code>/</code> 开头 = 消息首词精确匹配,否则 = 关键词包含;rewrite 可用 <code>{text}</code>=原文全文、<code>{args}</code>=首词后的参数;本地命令(/stop 等)不受影响;按序首个命中生效。</div>
|
|
84
154
|
<table><thead><tr><th style="width:42%">match</th><th>rewrite</th><th style="width:44px"></th></tr></thead><tbody data-list="triggers"></tbody></table>
|
|
@@ -114,6 +184,41 @@ function openAppDrawer(apps, idx, el, snap) {
|
|
|
114
184
|
else { delete app[f]; }
|
|
115
185
|
};
|
|
116
186
|
});
|
|
187
|
+
// ---- 角色(主/分身):切换 deputy 区块显示;选主时移除 role 键(yaml 干净,归一化仍为 primary) ----
|
|
188
|
+
const roleSel = body.querySelector('[data-f="role"]');
|
|
189
|
+
const deputyFields = body.querySelector('#deputyFields');
|
|
190
|
+
roleSel.addEventListener('change', () => {
|
|
191
|
+
if (roleSel.value === 'deputy') app.role = 'deputy';
|
|
192
|
+
else delete app.role;
|
|
193
|
+
deputyFields.style.display = roleSel.value === 'deputy' ? '' : 'none';
|
|
194
|
+
});
|
|
195
|
+
// ---- 分身白名单(可搜索多选):onChange 实时写回 app——空数组删键,语义与旧 textarea 一致 ----
|
|
196
|
+
const mselInst = {};
|
|
197
|
+
for (const f of ['allowed_skills', 'allowed_workspaces', 'allowed_plugins']) {
|
|
198
|
+
mselInst[f] = multiSelectField({
|
|
199
|
+
mount: body.querySelector(`[data-msel="${f}"]`),
|
|
200
|
+
value: app[f] || [],
|
|
201
|
+
placeholder: '输入关键词搜索选择…',
|
|
202
|
+
emptyText: '候选加载中…',
|
|
203
|
+
onChange: (values) => { if (values.length) app[f] = values; else delete app[f]; },
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
// 工作区候选来自配置本身(refresh 后 S.doc 是新引用,无须缓存)
|
|
207
|
+
mselInst.allowed_workspaces.setOptions(
|
|
208
|
+
wsNames.map((n) => ({ value: n })),
|
|
209
|
+
wsNames.length ? undefined : '尚未配置工作区(先到「工作区」页添加)',
|
|
210
|
+
);
|
|
211
|
+
// 技能/插件候选走 API:组件先以空候选就位,数据到达(或失败提示)后回填,已选值不受影响
|
|
212
|
+
void loadDeputyCandidates().then((c) => {
|
|
213
|
+
mselInst.allowed_skills.setOptions(
|
|
214
|
+
toSkillOptions(c.skills),
|
|
215
|
+
c.skillsError ? `候选加载失败:${c.skillsError}(已选项不受影响)` : (c.skills?.length ? undefined : '暂无已安装技能'),
|
|
216
|
+
);
|
|
217
|
+
mselInst.allowed_plugins.setOptions(
|
|
218
|
+
toPluginOptions(c.plugins),
|
|
219
|
+
c.pluginsError ? `候选加载失败:${c.pluginsError}(已选项不受影响)` : (c.plugins?.length ? undefined : '暂无已安装插件'),
|
|
220
|
+
);
|
|
221
|
+
});
|
|
117
222
|
// ---- 触发词 / 显式插件:数组行编辑,实时写回 ----
|
|
118
223
|
const LIST_DEFS = {
|
|
119
224
|
triggers: { key: 'triggers', cols: [['match', '/produce 或 关键词'], ['rewrite', '请执行内容生产流程:{args}']] },
|
|
@@ -188,6 +293,13 @@ function openAppDrawer(apps, idx, el, snap) {
|
|
|
188
293
|
};
|
|
189
294
|
foot.querySelector('#dwCancel').onclick = cancelDrawer;
|
|
190
295
|
foot.querySelector('#dwSave').onclick = async () => {
|
|
296
|
+
// 分身预校验(后端 loadConfig 校验兜底,前端提前拦截给出友好提示)
|
|
297
|
+
if (app.role === 'deputy') {
|
|
298
|
+
if (!(app.allowed_skills || []).length) return toast('分身机器人必须配置「允许的技能」(至少一个)', true);
|
|
299
|
+
if (!(app.allowed_workspaces || []).length) return toast('分身机器人必须配置「允许的工作区」(至少一个)', true);
|
|
300
|
+
const badWs = (app.allowed_workspaces || []).filter((w) => !wsNames.includes(w));
|
|
301
|
+
if (badWs.length) return toast(`「允许的工作区」含未定义的工作区:${badWs.join('、')}(请先在工作区列表中添加)`, true);
|
|
302
|
+
}
|
|
191
303
|
// 清理半填的空行:triggers/plugins 行两列全空剔除;env 以 key 非空为准(syncEnv 已过滤)。数组空则整键删除
|
|
192
304
|
for (const name of Object.keys(LIST_DEFS)) {
|
|
193
305
|
const def = LIST_DEFS[name];
|
|
@@ -172,7 +172,6 @@ function renderClaude(el) {
|
|
|
172
172
|
<td>${p.base_url ? `<code>${esc(p.base_url)}</code>` : '<span class="hint">官方</span>'}</td>
|
|
173
173
|
<td>${modelChips(p, active, i)}</td>
|
|
174
174
|
<td>
|
|
175
|
-
<button class="btn sm" data-op="use" data-i="${i}">设为当前</button>
|
|
176
175
|
<button class="btn sm" data-op="edit" data-i="${i}">编辑</button>
|
|
177
176
|
<button class="btn sm danger" data-op="del" data-i="${i}">删除</button>
|
|
178
177
|
</td>
|
|
@@ -181,15 +180,13 @@ function renderClaude(el) {
|
|
|
181
180
|
body.querySelectorAll('button[data-op]').forEach((b) => b.onclick = async () => {
|
|
182
181
|
const i = Number(b.dataset.i);
|
|
183
182
|
const p = c.profiles[i];
|
|
184
|
-
if (b.dataset.op === '
|
|
185
|
-
//
|
|
183
|
+
if (b.dataset.op === 'usemodel') {
|
|
184
|
+
// 切换 = 点击模型 chip(「设为当前」按钮已移除:默认模型必在候选集内,点其 chip 等效激活)。
|
|
185
|
+
// 走后端端点(档案凭证明文不出进程);编辑中先收尾,未保存改动自动落盘再切(如认证模式切换)
|
|
186
186
|
if (!$('#drawerMask').hidden) return toast('请先完成档案编辑(保存或取消)', true);
|
|
187
187
|
if (S.dirtyCards.size && !(await saveDoc())) return; // 保存失败已 toast 原因,保持编辑态不切换
|
|
188
188
|
try {
|
|
189
|
-
const
|
|
190
|
-
if (b.dataset.op === 'usemodel') reqBody.model = b.dataset.m;
|
|
191
|
-
else if (p.model || (p.models || [])[0]) reqBody.model = p.model || p.models[0];
|
|
192
|
-
const r = await api('POST', '/api/claude/use-profile', reqBody);
|
|
189
|
+
const r = await api('POST', '/api/claude/use-profile', { name: p.name, model: b.dataset.m });
|
|
193
190
|
toast(r.message || '已切换');
|
|
194
191
|
await refresh();
|
|
195
192
|
} catch (e) { toast(e.message, true); }
|
|
@@ -398,6 +398,45 @@ async function larkCliActionFlow(op = 'install') {
|
|
|
398
398
|
}
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
+
// ============ 开机自启开关(GET/POST /api/autostart;注册状态以 OS 查询为权威) ============
|
|
402
|
+
function paintAutostart(el, r) {
|
|
403
|
+
const tags = r.enabled
|
|
404
|
+
? '<span class="tag ok">已开启</span>'
|
|
405
|
+
: '<span class="tag off">未开启</span>';
|
|
406
|
+
const stale = r.stale ? ' <span class="tag warn">启动命令已过期,关闭再开启可更新</span>' : '';
|
|
407
|
+
const hint = r.detail ? ` <span class="hint">${esc(r.detail)}</span>` : '';
|
|
408
|
+
el.innerHTML = `<span class="switch-row tight"><label class="switch"><input type="checkbox" id="asToggle" ${r.enabled ? 'checked' : ''}><span class="track"></span></label>${tags}${stale}${hint}</span>`;
|
|
409
|
+
$('#asToggle').onchange = async (e) => {
|
|
410
|
+
const on = e.target.checked;
|
|
411
|
+
try {
|
|
412
|
+
const res = await api('POST', '/api/autostart', { enabled: on });
|
|
413
|
+
toast(on ? '✅ 已注册开机自启(下次登录时自动启动)' : '已关闭开机自启');
|
|
414
|
+
paintAutostart(el, res);
|
|
415
|
+
} catch (err) {
|
|
416
|
+
toast(err.message, true);
|
|
417
|
+
e.target.checked = !on; // 失败回滚开关视觉态(注册失败不能显示为已开)
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** 概览页「开机自启」行:首次渲染拉状态;开关点击即生效(低风险,与权限页即时保存一致) */
|
|
423
|
+
async function renderAutostart() {
|
|
424
|
+
const el = $('#asStat');
|
|
425
|
+
if (!el) return;
|
|
426
|
+
let r;
|
|
427
|
+
try {
|
|
428
|
+
r = await api('GET', '/api/autostart');
|
|
429
|
+
} catch (e) {
|
|
430
|
+
el.innerHTML = `<span class="hint">检测失败:${esc(e.message)}</span>`;
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (!r.supported) {
|
|
434
|
+
el.innerHTML = `<span class="tag off">不支持</span><span class="hint">${esc(r.detail || '当前运行方式不支持开机自启,请手动运行 lcb start')}</span>`;
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
paintAutostart(el, r);
|
|
438
|
+
}
|
|
439
|
+
|
|
401
440
|
export const page = {
|
|
402
441
|
id: 'overview',
|
|
403
442
|
title: '概览',
|
|
@@ -434,20 +473,29 @@ export const page = {
|
|
|
434
473
|
<tr><td>配置文件</td><td><code>${esc(st.configPath)}</code></td></tr>
|
|
435
474
|
<tr><td>配置页</td><td><code>${esc(location.host)}</code></td></tr>
|
|
436
475
|
<tr><td>Claude 认证</td><td>${claudeTag}(模式:${esc(st.claude?.mode || 'inherit')})</td></tr>
|
|
476
|
+
<tr><td>开机自启</td><td id="asStat"><span class="hint">检测中…</span></td></tr>
|
|
437
477
|
</table>
|
|
438
478
|
${ctl}
|
|
439
479
|
<div class="desc" style="margin-top:10px">${st.claude?.hasAuth === false ? '⚠️ 未检测到认证:到「Claude 认证」页填写,或在配置页所在机器执行 claude login。' : ''}</div>
|
|
440
480
|
</div>
|
|
441
481
|
<div class="card">
|
|
442
482
|
<h3>版本与更新</h3>
|
|
443
|
-
<
|
|
444
|
-
<
|
|
445
|
-
<
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
<
|
|
450
|
-
|
|
483
|
+
<div class="card-sub">
|
|
484
|
+
<h4 class="card-sub-title">▍LCB 桥接器</h4>
|
|
485
|
+
<table>
|
|
486
|
+
<tr><td>当前版本</td><td><span class="chip">v${esc(st.version)}</span></td></tr>
|
|
487
|
+
<tr><td>最新版本</td><td id="updLatest"><span class="hint">未检查</span></td></tr>
|
|
488
|
+
</table>
|
|
489
|
+
<div style="display:flex; gap:10px; margin-top:12px; align-items:center">
|
|
490
|
+
<button class="btn" id="btnUpdCheck">检查更新</button>
|
|
491
|
+
<span id="updArea"></span>
|
|
492
|
+
</div>
|
|
493
|
+
</div>
|
|
494
|
+
<div class="card-sub">
|
|
495
|
+
<h4 class="card-sub-title">▍飞书 CLI(lark-cli)</h4>
|
|
496
|
+
<table>
|
|
497
|
+
<tr><td>状态</td><td id="larkCliStat"><span class="hint">检测中…</span></td></tr>
|
|
498
|
+
</table>
|
|
451
499
|
</div>
|
|
452
500
|
</div>
|
|
453
501
|
<div class="card">
|
|
@@ -477,5 +525,6 @@ export const page = {
|
|
|
477
525
|
else doCheckUpdate();
|
|
478
526
|
if (LARKCLI.data && Date.now() - LARKCLI.checkedAt < 30000) renderLarkCliResult();
|
|
479
527
|
else doCheckLarkCli();
|
|
528
|
+
void renderAutostart();
|
|
480
529
|
},
|
|
481
530
|
};
|
|
@@ -23,19 +23,31 @@ function renderPermissions(el) {
|
|
|
23
23
|
function renderAllowTab(panel, p) {
|
|
24
24
|
const defaults = S.status?.permissionDefaults?.allowTools || [];
|
|
25
25
|
const isDefault = defaults.length > 0 && JSON.stringify(p.allow_tools) === JSON.stringify(defaults);
|
|
26
|
+
// 缺省即开(与后端 DEFAULT_ALLOW_ALL_TOOLS 一致):只有显式 false 才算用户主动关闭
|
|
27
|
+
const allOn = p.allow_all_tools !== false;
|
|
26
28
|
panel.innerHTML = `
|
|
27
29
|
<div class="card">
|
|
28
30
|
<h3>免确认工具</h3>
|
|
29
|
-
<div class="
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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>
|
|
34
37
|
</div>
|
|
35
|
-
${
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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>
|
|
45
|
+
</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
|
+
</div>
|
|
50
|
+
<div class="footer-note">权限改动即时保存并热生效(已有会话的下一个工具调用即用新名单)。plan 模式下写操作仍会经此判定:${allOn ? '全部免确认已开启,除危险命令外一律直通' : '白名单命中直通,未命中弹确认卡'}。</div>
|
|
39
51
|
</div>`;
|
|
40
52
|
$('#toolBody').innerHTML = p.allow_tools.map((t, i) =>
|
|
41
53
|
`<tr><td><code>${esc(t)}</code></td><td style="width:80px"><button class="btn sm danger" data-i="${i}">删除</button></td></tr>`).join('')
|
|
@@ -71,6 +83,29 @@ function renderAllowTab(panel, p) {
|
|
|
71
83
|
p.allow_tools = [...defaults];
|
|
72
84
|
if (!await saveDoc()) p.allow_tools = old;
|
|
73
85
|
};
|
|
86
|
+
// 开关即时落盘:关闭(收紧密)直接生效;开启(放宽到全放行)先过确认弹层,避免误点放行全部写操作
|
|
87
|
+
$('#allowAll').onchange = async (e) => {
|
|
88
|
+
const next = e.target.checked;
|
|
89
|
+
if (next && !(await confirmDialog({
|
|
90
|
+
title: '开启全部工具免确认',
|
|
91
|
+
message: '开启后除命中危险命令黑名单的 Bash 外,<b>所有工具(含 Write/Edit)</b>都不再弹确认卡。确定开启?',
|
|
92
|
+
danger: true,
|
|
93
|
+
confirmText: '开启',
|
|
94
|
+
}))) {
|
|
95
|
+
e.target.checked = false;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const old = p.allow_all_tools;
|
|
99
|
+
p.allow_all_tools = next;
|
|
100
|
+
// 成功路径不手动重渲染:saveDoc 内部的 refresh() 已整页重渲染并重新绑定事件(用最新的
|
|
101
|
+
// S.doc.permissions)。此处若再调 renderAllowTab(panel, p),panel 早已脱离文档,而
|
|
102
|
+
// $() 走 document 查询——那会把新 DOM 上的 handler 覆盖成闭包里旧 p(孤儿对象)的版本,
|
|
103
|
+
// 之后开关改动就落不到 S.doc 上(表现为「点了保存成功但配置没变」)。与增删条目同款模式。
|
|
104
|
+
if (!await saveDoc()) {
|
|
105
|
+
p.allow_all_tools = old;
|
|
106
|
+
e.target.checked = !next; // 失败不重渲染,DOM 仍是本次交互的元素,直接回滚勾选态
|
|
107
|
+
}
|
|
108
|
+
};
|
|
74
109
|
}
|
|
75
110
|
|
|
76
111
|
function renderDangerTab(panel, p) {
|
|
@@ -88,7 +123,7 @@ function renderDangerTab(panel, p) {
|
|
|
88
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>' : ''}
|
|
89
124
|
${isDefault ? '<div class="hint" style="margin:8px 0"><span class="tag off">与内置默认一致</span></div>' : ''}
|
|
90
125
|
<table><tbody id="dcBody"></tbody></table>
|
|
91
|
-
<div class="footer-note"
|
|
126
|
+
<div class="footer-note">权限改动即时保存并热生效(已有会话的下一个工具调用即用新名单)。「全部工具免确认」开启时本黑名单是唯一的安全网——删除正则等于放行对应命令。</div>
|
|
92
127
|
</div>`;
|
|
93
128
|
$('#dcBody').innerHTML = p.dangerous_commands.map((s, i) =>
|
|
94
129
|
`<tr><td style="width:90%"><code>${esc(s)}</code></td><td><button class="btn sm danger" data-i="${i}">删除</button></td></tr>`).join('')
|