@jesonliu/lark-claudecode-bridge 0.15.0 → 0.18.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 +11 -5
- package/assets/web/css/main.css +111 -0
- package/assets/web/index.html +8 -1738
- package/assets/web/js/bootstrap.js +97 -0
- package/assets/web/js/core.js +73 -0
- package/assets/web/js/main.js +75 -0
- package/assets/web/js/pages/apps.js +206 -0
- package/assets/web/js/pages/claude.js +322 -0
- package/assets/web/js/pages/overview.js +214 -0
- package/assets/web/js/pages/permissions.js +109 -0
- package/assets/web/js/pages/plugins.js +285 -0
- package/assets/web/js/pages/slash.js +128 -0
- package/assets/web/js/pages/workspaces.js +100 -0
- package/assets/web/js/ui.js +123 -0
- package/config.example.yaml +3 -0
- package/dist/claude-profile.d.ts +20 -0
- package/dist/claude-profile.js +101 -0
- package/dist/claude-profile.js.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +22 -2
- package/dist/config.js.map +1 -1
- package/dist/executor/claude-executor.js +127 -13
- package/dist/executor/claude-executor.js.map +1 -1
- package/dist/executor/permission-gate.d.ts +4 -4
- package/dist/executor/permission-gate.js +13 -5
- package/dist/executor/permission-gate.js.map +1 -1
- package/dist/executor/plugin-discovery.d.ts +8 -3
- package/dist/executor/plugin-discovery.js +50 -18
- package/dist/executor/plugin-discovery.js.map +1 -1
- package/dist/executor/plugin-manager.js +3 -2
- package/dist/executor/plugin-manager.js.map +1 -1
- package/dist/gateway/card-builder.d.ts +22 -5
- package/dist/gateway/card-builder.js +106 -64
- package/dist/gateway/card-builder.js.map +1 -1
- package/dist/gateway/feishu-gateway.js +11 -1
- package/dist/gateway/feishu-gateway.js.map +1 -1
- package/dist/gateway/progress-card.d.ts +19 -1
- package/dist/gateway/progress-card.js +47 -3
- package/dist/gateway/progress-card.js.map +1 -1
- package/dist/index.js +61 -55
- package/dist/index.js.map +1 -1
- package/dist/session/commands.d.ts +3 -1
- package/dist/session/commands.js +15 -4
- package/dist/session/commands.js.map +1 -1
- package/dist/session/model-profile-command.d.ts +6 -0
- package/dist/session/model-profile-command.js +45 -0
- package/dist/session/model-profile-command.js.map +1 -0
- package/dist/session/triggers.js +9 -1
- package/dist/session/triggers.js.map +1 -1
- package/dist/types.d.ts +43 -4
- package/dist/web/server.js +68 -68
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
- **直接使用本机 Claude Code 全套配置**(inherit 模式):模型设置(`~/.claude/settings.json`)、登录态、user 级 MCP、skills、marketplace 插件自动继承,无须二次配置;已启用插件自动加载
|
|
15
15
|
- **斜杠透传**:飞书里直接发 `/superpowers:brainstorming` 等斜杠命令,原文透传给 Claude Code 展开(user skills / 插件命令均可触发)
|
|
16
16
|
- **多机器人**:一个进程同时跑 N 个飞书机器人,各自独立会话池、独立并发、独立人格(`append_system_prompt`);共享同一套 Claude 配置
|
|
17
|
-
-
|
|
17
|
+
- 写操作确认嵌在计时进度卡底部(允许 / 拒绝 / 本次会话不再询问,仅任务发起人可点;Write/Edit 直接展示红绿 diff;等待确认时正文自动收敛,决策后按钮消失、状态行显示结果)
|
|
18
18
|
- **读操作免确认**:读工具(Read/Grep/Glob 等)与 Bash 读命令默认直通,危险命令黑名单兜底;白名单可通过 `permissions.allow_tools` 自定义(配置页可增删,新建配置默认预置完整默认值)
|
|
19
19
|
- **开发场景工作流(`type: code-dev`)**:统一 plan mode——先出计划 → 飞书卡片批准/按意见修改/放弃 → 批准后自动执行;任务收尾发汇总 diff 卡片(红绿着色),不再整文件刷屏
|
|
20
20
|
- 流式进度卡片(打字机效果 + 工具调用 + 运行心跳,静默不等于卡死)
|
|
21
21
|
- **接收图片与富文本**:直接给机器人发图片(下载到 `~/.lark-claudecode-bridge/inbox/`,Claude 用 Read 工具识图);粘贴的多行/带格式内容(post 富文本)自动拍平为多行文本;不支持的类型(语音等)私聊会回复提示;入站消息按 message_id 去重(WS 重投不会导致任务跑两遍)
|
|
22
22
|
- 结果文本 + 产出文件回传(图片预览、>10 文件自动 zip;generic 工作区)
|
|
23
|
-
- 多工作区切换(/ws)、会话管理(/new /resume)、/stop 打断、模型切换(/model)、加载清单查看(/skills /plugins /mcp)、插件管理(/plugin)
|
|
23
|
+
- 多工作区切换(/ws)、会话管理(/new /resume)、/stop 打断、模型切换(/model)、厂商档案切换(/model-profile)、加载清单查看(/skills /plugins /mcp)、插件管理(/plugin)
|
|
24
|
+
- **后台子代理续跑**:主 Agent 派发的后台子代理在主回复结束后继续执行,完成后自动唤醒主循环汇总结果(进度卡可见「等待后台任务」与子代理输出)
|
|
24
25
|
- **对话内容落盘**:用户消息与 Claude 回复全文存为 JSONL(`transcripts/`,为后续知识库挖掘打底;可选保留期)
|
|
25
26
|
- 通道并发(默认 3),通道内串行
|
|
26
27
|
|
|
@@ -100,6 +101,7 @@ lcb start
|
|
|
100
101
|
| /status | 当前状态 |
|
|
101
102
|
| /ws list / /ws use \<名字\> | 工作区(切换仅 admin 可用) |
|
|
102
103
|
| /model | 查看当前模型;`/model <名字>` 通道级切换;`/model reset` 恢复默认 |
|
|
104
|
+
| /model-profile | 查看/切换厂商档案(多厂商凭证+模型整体切换,切换仅 admin;managed 模式下一条消息生效) |
|
|
103
105
|
| /skills / /plugins / /mcp | 查看本会话实际加载的技能 / 插件 / MCP 服务 |
|
|
104
106
|
| /plugin | 插件管理:`/plugin list`(全员,含本机 ~/.claude 与托管目录两处清单);`install/uninstall/enable/disable/marketplace …`(仅 admin,默认装 ~/.claude,`--dir=managed` 装托管目录),装好下一条消息自动加载 |
|
|
105
107
|
| /reload-plugins | 重载插件:清插件发现缓存,下一条消息重新扫描加载(终端命令的 bridge 等价物) |
|
|
@@ -154,6 +156,8 @@ concurrency: 3 # 通道间并发上限(未单独配置的 app 沿
|
|
|
154
156
|
# icon: skill_outlined
|
|
155
157
|
# transcripts: # 对话落盘清理策略;缺省 = 永久保留
|
|
156
158
|
# retention_days: 90
|
|
159
|
+
# session: # 会话行为微调
|
|
160
|
+
# context_remind_tokens: 150000 # 上下文超长提醒阈值(tokens);0 = 关闭;改后热生效
|
|
157
161
|
```
|
|
158
162
|
|
|
159
163
|
### 认证双模式(inherit / managed)
|
|
@@ -170,7 +174,7 @@ concurrency: 3 # 通道间并发上限(未单独配置的 app 沿
|
|
|
170
174
|
|
|
171
175
|
**managed 模式的 MCP 与环境继承**(0.14.0 起,`lcb start` 时自动完成):本机 `~/.claude.json` 的全局 `mcpServers` 单向同步到托管目录(CLI 读 `$CLAUDE_CONFIG_DIR/.claude.json`,不同步则托管会话丢掉全部 user 级 MCP);本机 `~/.claude/settings.json` env 块中的**非认证键**(MCP 工具依赖的 `IMAGE_GEN_*`、`API_HOST` 等自定义变量)并入托管 settings.json。需要覆盖继承值或本机没有这些配置时,用**显式配置**:配置页「Claude 认证」→「环境变量」行编辑器(或 config.yaml 的 `claude.env` 键值对),优先级 `claude.env` > 本机继承 > 托管目录既有值;认证与模型 4 键(`ANTHROPIC_AUTH_TOKEN/API_KEY/BASE_URL/MODEL`)不在此生效——永远以认证表单为准。
|
|
172
176
|
|
|
173
|
-
**插件双目录(managed 模式)**:新装插件默认装到本机 `~/.claude`(与本机 claude CLI 共用一份),`/plugin install xxx --dir=managed` 或配置页安装框选「bridge 托管目录」可装到托管目录;启停/卸载自动按插件所在目录执行,两处清单在配置页「插件」tab 与 `/plugin list`
|
|
177
|
+
**插件双目录(managed 模式)**:新装插件默认装到本机 `~/.claude`(与本机 claude CLI 共用一份),`/plugin install xxx --dir=managed` 或配置页安装框选「bridge 托管目录」可装到托管目录;启停/卸载自动按插件所在目录执行,两处清单在配置页「插件」tab 与 `/plugin list` 中均带来源标记。注意:**卸载按所选目录逐处执行**——同一插件在两个目录各装一份时,卸载一处不影响另一处(本机 CLI 的 `/plugins list` 看的是它自己的配置目录);配置页卸载后会校验安装清单已清除,残留(仅被禁用)会显式报错并附 CLI 输出。配置页「管理市场」支持 git 地址与本机路径(本地路径市场按 CLI 语义不复制文件,登记原路径读取)。
|
|
174
178
|
|
|
175
179
|
### 开发场景工作流(`type: code-dev`)
|
|
176
180
|
|
|
@@ -184,9 +188,11 @@ concurrency: 3 # 通道间并发上限(未单独配置的 app 沿
|
|
|
184
188
|
|
|
185
189
|
**执行器为 Streaming Input 模式**(0.14.0 起):prompt 经持久输入流送入 CLI,stdin 全程保持打开——这是计划审批与提问卡片能稳定工作的前提(旧版单轮模式在轮次边界会触发 CLI 的 "Stream closed" 中断,属 Agent SDK 已知问题)。**提问卡片**:Claude 调用 AskUserQuestion 时飞书收到问题选项卡,点选项作答(多选题可多选)、全部作答后「提交答案」——答案直接回传模型继续任务。
|
|
186
190
|
|
|
187
|
-
**读操作免确认**:读工具与 Bash 默认直通(`ls`/`cat`/`grep` 不再弹卡),命中 `dangerous_commands` 黑名单(`rm -rf`、`sudo`、`git push --force` 等)仍弹确认卡;「本次会话不再询问」的记忆同样绕不过黑名单。想放行其它工具(如 `Edit`)往 `permissions.allow_tools`
|
|
191
|
+
**读操作免确认**:读工具与 Bash 默认直通(`ls`/`cat`/`grep` 不再弹卡),命中 `dangerous_commands` 黑名单(`rm -rf`、`sudo`、`git push --force` 等)仍弹确认卡;「本次会话不再询问」的记忆同样绕不过黑名单。想放行其它工具(如 `Edit`)往 `permissions.allow_tools` 追加即可——注意配置即**整体替换**内置默认,需把内置读工具一并写上。**白名单/黑名单热生效**(0.18.0 起):配置页保存后,已有会话通道的下一个工具调用即用新名单(旧版需新通道或重启)。
|
|
188
192
|
|
|
189
|
-
|
|
193
|
+
**plan mode 下的白名单语义**:code-dev 工作区的计划阶段,Claude Code 内部对写操作强制走桥接的权限闸(官方语义:plan 模式无视 CLI 侧 allow 规则、写工具一律路由到宿主判定)——因此桥接白名单在计划阶段对写工具**依然生效**(命中直通执行,未命中弹确认卡嵌在计时卡上),直到计划批准切回可编辑模式。只读工具不经桥接直接执行。计时卡上工具行的 `✘` 表示该次工具调用**执行失败**(含首行失败原因),不代表「工具没权限」。
|
|
194
|
+
|
|
195
|
+
> plan 卡片相关与 `workspaces[].type` 的改动需重启 bridge 进程生效(permissions 配置已支持热生效,见上)。
|
|
190
196
|
|
|
191
197
|
### 配置继承(inherit 模式:本机 ~/.claude 一处配置,全机器人共享)
|
|
192
198
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/* Web 配置页样式(从 index.html 原 <style> 块整体迁出,规则未变) */
|
|
2
|
+
:root {
|
|
3
|
+
--bg: #f5f6f8; --card: #fff; --border: #e2e5ea; --text: #1f2329; --muted: #646a73;
|
|
4
|
+
--primary: #3370ff; --danger: #f54f4f; --ok: #34c724; --warn: #ff8800; --radius: 10px;
|
|
5
|
+
}
|
|
6
|
+
* { box-sizing: border-box; }
|
|
7
|
+
body { margin: 0; font: 14px/1.6 -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; background: var(--bg); color: var(--text); }
|
|
8
|
+
header { background: var(--card); border-bottom: 1px solid var(--border); padding: 14px 24px; display: flex; align-items: center; gap: 12px; position: sticky; top: 0; z-index: 10; }
|
|
9
|
+
header h1 { font-size: 17px; margin: 0; display: flex; align-items: center; gap: 9px; }
|
|
10
|
+
header .meta { color: var(--muted); font-size: 12px; }
|
|
11
|
+
header .spacer { flex: 1; }
|
|
12
|
+
.layout { display: flex; min-height: calc(100vh - 61px); }
|
|
13
|
+
nav { width: 170px; padding: 16px 8px; background: var(--card); border-right: 1px solid var(--border); }
|
|
14
|
+
nav button { display: flex; align-items: center; gap: 9px; width: 100%; padding: 9px 14px; margin: 2px 0; border: 0; background: none; border-radius: 8px; cursor: pointer; font-size: 14px; color: var(--text); }
|
|
15
|
+
nav button .ico { display: flex; align-items: center; }
|
|
16
|
+
nav button:hover { background: var(--bg); }
|
|
17
|
+
nav button.active { background: #eef3ff; color: var(--primary); font-weight: 600; }
|
|
18
|
+
main { flex: 1; padding: 20px 32px; }
|
|
19
|
+
@media (min-width: 1800px) { main { max-width: 1560px; } }
|
|
20
|
+
@media (max-width: 860px) {
|
|
21
|
+
.layout { flex-direction: column; }
|
|
22
|
+
nav { width: auto; display: flex; flex-wrap: wrap; border-right: 0; border-bottom: 1px solid var(--border); }
|
|
23
|
+
}
|
|
24
|
+
.card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 18px 20px; margin-bottom: 16px; }
|
|
25
|
+
.card h3 { margin: 0 0 4px; font-size: 15px; }
|
|
26
|
+
.card .desc { color: var(--muted); font-size: 12px; margin-bottom: 12px; }
|
|
27
|
+
label { display: block; font-size: 12px; color: var(--muted); margin: 10px 0 4px; }
|
|
28
|
+
input[type=text], input[type=password], input[type=number], select, textarea {
|
|
29
|
+
width: 100%; padding: 7px 10px; border: 1px solid var(--border); border-radius: 7px; font: inherit; background: #fff; color: var(--text);
|
|
30
|
+
}
|
|
31
|
+
input:focus, select:focus, textarea:focus { outline: 2px solid #c9d8ff; border-color: var(--primary); }
|
|
32
|
+
textarea { font-family: Consolas, monospace; font-size: 12.5px; min-height: 90px; resize: vertical; }
|
|
33
|
+
.row { display: flex; gap: 12px; } .row > * { flex: 1; min-width: 0; }
|
|
34
|
+
/* .row 内按钮按内容宽:内联 flex:0 展开为 0 1 0% 会把按钮压到单字宽(一字一行),一律走类规则 */
|
|
35
|
+
.row > .btn, .row > .btn-wrap { flex: 0 0 auto; align-self: end; }
|
|
36
|
+
.btn-wrap { display: flex; }
|
|
37
|
+
.btn, nav button, .savebar .btn, .subtabs button, .chip { white-space: nowrap; }
|
|
38
|
+
.btn { padding: 7px 16px; border-radius: 7px; border: 1px solid var(--border); background: var(--card); cursor: pointer; font: inherit; }
|
|
39
|
+
.btn:hover { border-color: var(--primary); color: var(--primary); }
|
|
40
|
+
.btn.primary { background: var(--primary); border-color: var(--primary); color: #fff; }
|
|
41
|
+
.btn.danger { color: var(--danger); } .btn.danger:hover { border-color: var(--danger); }
|
|
42
|
+
.btn.sm { padding: 3px 10px; font-size: 12px; }
|
|
43
|
+
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0; }
|
|
44
|
+
.chip { display: inline-flex; align-items: center; gap: 6px; background: var(--bg); border: 1px solid var(--border); padding: 3px 10px; border-radius: 999px; font-size: 12.5px; font-family: Consolas, monospace; }
|
|
45
|
+
.chip button { border: 0; background: none; color: var(--muted); cursor: pointer; padding: 0; font-size: 13px; }
|
|
46
|
+
.chip button:hover { color: var(--danger); }
|
|
47
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
48
|
+
th, td { text-align: left; padding: 7px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
|
|
49
|
+
th { color: var(--muted); font-weight: 500; font-size: 12px; }
|
|
50
|
+
.tag { display: inline-block; padding: 1px 8px; border-radius: 5px; font-size: 11.5px; }
|
|
51
|
+
.tag.ok { background: #e8f7e6; color: var(--ok); } .tag.off { background: var(--bg); color: var(--muted); }
|
|
52
|
+
.tag.warn { background: #fff3e0; color: var(--warn); } .tag.err { background: #fee; color: var(--danger); }
|
|
53
|
+
.radio-row { display: flex; gap: 20px; margin: 8px 0; }
|
|
54
|
+
.radio-row label { display: flex; align-items: center; gap: 6px; margin: 0; color: var(--text); font-size: 13.5px; cursor: pointer; }
|
|
55
|
+
#toast { position: fixed; top: 24px; left: 50%; transform: translateX(-50%); background: #2b2f36; color: #fff; padding: 10px 22px; border-radius: 8px; font-size: 13px; opacity: 0; transition: opacity .25s; pointer-events: none; max-width: 80vw; z-index: 100; }
|
|
56
|
+
#toast.show { opacity: .95; }
|
|
57
|
+
#toast.err { background: var(--danger); }
|
|
58
|
+
/* 桥接器重启/已离线全页遮罩 */
|
|
59
|
+
.br-mask { position: fixed; inset: 0; background: rgba(15,19,24,.5); z-index: 90; display: flex; align-items: center; justify-content: center; }
|
|
60
|
+
.br-mask-card { background: var(--card); border-radius: 12px; padding: 24px 32px; text-align: center; box-shadow: 0 8px 30px rgba(0,0,0,.18); max-width: 440px; font-size: 14px; }
|
|
61
|
+
.br-spin { width: 26px; height: 26px; margin: 0 auto 12px; border: 3px solid #d9e2f5; border-top-color: var(--primary); border-radius: 50%; animation: brspin .9s linear infinite; }
|
|
62
|
+
/* 表格内小号 spinner(斜杠命令远端拉取等异步加载占位) */
|
|
63
|
+
.br-spin-sm { width: 18px; height: 18px; margin: 0 auto 10px; border: 2px solid #d9e2f5; border-top-color: var(--primary); border-radius: 50%; animation: brspin .9s linear infinite; }
|
|
64
|
+
@keyframes brspin { to { transform: rotate(360deg); } }
|
|
65
|
+
.app-card { border: 1px solid var(--border); border-radius: 8px; padding: 4px 16px 14px; margin-bottom: 12px; position: relative; }
|
|
66
|
+
.app-card .del { position: absolute; top: 10px; right: 12px; }
|
|
67
|
+
.hint { font-size: 12px; color: var(--muted); }
|
|
68
|
+
.footer-note { color: var(--muted); font-size: 12px; margin-top: 8px; }
|
|
69
|
+
pre.report { background: #f7f8fa; border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; font-size: 12px; overflow: auto; white-space: pre-wrap; }
|
|
70
|
+
.savebar { display: flex; gap: 10px; margin-top: 12px; align-items: center; }
|
|
71
|
+
.savebar .state { font-size: 12px; color: var(--warn); }
|
|
72
|
+
.card h3 .dirty-dot { display: none; margin-left: 6px; font-size: 11.5px; color: var(--warn); font-weight: 500; }
|
|
73
|
+
.stale-bar { background: #fff3e0; color: var(--warn); padding: 8px 24px; font-size: 13px; border-bottom: 1px solid var(--border); }
|
|
74
|
+
.stale-bar[hidden] { display: none; }
|
|
75
|
+
/* 右侧抽屉:应用编辑 / 斜杆命令编辑 / 档案编辑共用 */
|
|
76
|
+
.drawer-mask { position: fixed; inset: 0; background: rgba(15,19,24,.36); z-index: 50; display: flex; justify-content: flex-end; }
|
|
77
|
+
.drawer-mask[hidden] { display: none; }
|
|
78
|
+
.drawer { width: min(980px, 94vw); height: 100%; background: var(--card); border-left: 1px solid var(--border); display: flex; flex-direction: column; box-shadow: -8px 0 24px rgba(0,0,0,.08); }
|
|
79
|
+
.drawer > header { display: flex; align-items: center; justify-content: space-between; padding: 12px 20px; border-bottom: 1px solid var(--border); }
|
|
80
|
+
.drawer > header h3 { margin: 0; font-size: 15px; }
|
|
81
|
+
.drawer-body { flex: 1; overflow-y: auto; padding: 16px 20px; }
|
|
82
|
+
.drawer-foot { padding: 12px 20px; border-top: 1px solid var(--border); display: flex; gap: 10px; align-items: center; }
|
|
83
|
+
.drawer-foot .spacer { flex: 1; }
|
|
84
|
+
.subtabs { display: flex; gap: 8px; margin-bottom: 14px; }
|
|
85
|
+
.subtabs button { padding: 6px 14px; border-radius: 999px; border: 1px solid var(--border); background: none; cursor: pointer; font: inherit; color: var(--text); }
|
|
86
|
+
.subtabs button.active { background: #eef3ff; color: var(--primary); border-color: var(--primary); font-weight: 600; }
|
|
87
|
+
/* 居中 modal:目录选择等轻量交互(z-index 低于全页断连遮罩 br-mask=90) */
|
|
88
|
+
.modal-mask { position: fixed; inset: 0; background: rgba(15,19,24,.42); z-index: 80; display: flex; align-items: center; justify-content: center; }
|
|
89
|
+
.modal { width: min(560px, 92vw); background: var(--card); border-radius: 12px; padding: 16px 20px; box-shadow: 0 8px 30px rgba(0,0,0,.18); }
|
|
90
|
+
.dir-list { border: 1px solid var(--border); border-radius: 8px; max-height: 46vh; overflow-y: auto; margin-top: 10px; }
|
|
91
|
+
.dir-list button { display: block; width: 100%; text-align: left; padding: 7px 12px; border: 0; border-bottom: 1px solid var(--border); background: none; cursor: pointer; font: inherit; }
|
|
92
|
+
.dir-list button:last-child { border-bottom: 0; }
|
|
93
|
+
.dir-list button:hover { background: #eef3ff; color: var(--primary); }
|
|
94
|
+
.dir-list .empty { padding: 14px 12px; color: var(--muted); font-size: 12.5px; }
|
|
95
|
+
/* 输入框 + 行内按钮组合(路径框带「浏览」) */
|
|
96
|
+
.input-btn { display: flex; gap: 6px; }
|
|
97
|
+
.input-btn > input { flex: 1; min-width: 0; }
|
|
98
|
+
.input-btn > .btn { flex: 0 0 auto; align-self: center; }
|
|
99
|
+
/* 可搜索 combobox(安装插件选择器):输入过滤 + 结果面板 */
|
|
100
|
+
.combo { position: relative; }
|
|
101
|
+
.combo-panel { position: absolute; top: calc(100% + 4px); left: 0; right: 0; background: var(--card); border: 1px solid var(--border); border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,.14); max-height: 320px; overflow-y: auto; z-index: 40; }
|
|
102
|
+
.combo-panel[hidden] { display: none; }
|
|
103
|
+
.combo-item { padding: 7px 12px; border-bottom: 1px solid var(--border); cursor: pointer; }
|
|
104
|
+
.combo-item:last-child { border-bottom: 0; }
|
|
105
|
+
.combo-item:hover { background: #eef3ff; }
|
|
106
|
+
.combo-item b { margin-right: 6px; }
|
|
107
|
+
.combo-empty { padding: 12px; color: var(--muted); font-size: 12.5px; }
|
|
108
|
+
/* 操作进度弹框:进度条(update-all 真实 i/N,单操作时间缓动模拟) */
|
|
109
|
+
.prog { height: 8px; border-radius: 999px; background: var(--bg); border: 1px solid var(--border); overflow: hidden; margin-top: 12px; }
|
|
110
|
+
.prog-bar { height: 100%; width: 0%; background: var(--primary); border-radius: 999px; transition: width .3s; }
|
|
111
|
+
.prog-bar.done { background: var(--ok); }
|