mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
- 将 WebView 调整到编辑器右侧 - 添加 ChatHeader 组件,实现会话下拉菜单 - 替换模态框为紧凑型下拉菜单 - 更新会话切换逻辑,显示当前标题 - 清理旧的会话选择器样式 基于 Claude Code v2.0.43 UI 分析实现。
22 KiB
22 KiB
Claude Code HTML 结构到混淆 JS 的完整映射
方法论: 通过 HTML 类名 + 字符串锚点定位混淆代码中的组件逻辑
日期: 2025-11-18
一、HTML 结构完整分析
1. 顶层结构
<body class="vscode-dark">
<div id="root">
<div class="me"> <!-- 主容器 -->
<div class="he"> <!-- Header 区域 -->
<div class="be"> <!-- 主内容区 -->
<div class="Q">
<div class="ue">
<div class="ye"> <!-- 消息容器 -->
<div class="Re"> <!-- 空状态 -->
</div>
</div>
</div>
<div> <!-- 输入区域 -->
<form class="u"> <!-- 输入表单 -->
<div class="fo"> <!-- 消息输入容器 -->
<div class="ri"> <!-- Footer 按钮 -->
</form>
</div>
</div>
</div>
</div>
</body>
二、关键组件的 CSS 映射表
| HTML 类名 | 用途 | JS 变量名 (推断) | 位置 |
|---|---|---|---|
.me |
主容器 | - | index.css |
.he |
Header | - | index.css |
.E |
Session 按钮 | c_ / tm |
xKe 函数 |
.j |
New Session 按钮 | zl |
Footer 定义 |
.u |
输入表单 | c_ |
ContentEditable |
.d |
输入框 | - | ContentEditable |
.ri |
Footer 按钮区 | zl |
Footer 组件 |
.l |
Footer 按钮 | zl |
Footer 按钮 |
三、成功映射的组件逻辑
A. 命令菜单组件 (Command Menu)
HTML 定位线索
- 类名:
.menuPopup,.commandList,.commandItem - 字符串: "Filter actions...", "No matching commands"
找到的 JS 代码
// 从混淆代码提取 - 命令菜单组件
var xKe = ({
isOpen: n,
onClose: e,
onCommandSelect: t,
commandRegistry: i,
filterText: o,
suppressFilter: r = !1,
}) => {
let [s, a] = (0, rr.useState)(''); // filterText state
let [l, c] = (0, rr.useState)(null); // selectedId state
let d = (0, rr.useRef)(null); // input ref
let u = (0, rr.useRef)(null); // container ref
let h = (0, rr.useRef)(null); // selected item ref
let f = r ? o || '' : s || ''; // 实际过滤文本
// 获取命令分组
let p = i.getCommandsBySection();
// 过滤命令
let g = Object.entries(p).reduce((x, [w, y]) => {
let C = y.filter((L) => L.label.toLowerCase().includes(f.toLowerCase()));
return (C.length > 0 && (x[w] = C), x);
}, {});
let _ = Object.values(g).flat(); // 扁平化命令列表
// 键盘导航逻辑
let b = (0, rr.useCallback)(
(x) => {
if (x.key === 'Escape') {
x.preventDefault();
e();
return;
}
if (x.key === 'ArrowDown' && _.length > 0) {
x.preventDefault();
let w = _.findIndex((C) => C.id === l);
let y = w < _.length - 1 ? w + 1 : 0;
c(_[y]?.id || null);
return;
}
if (x.key === 'ArrowUp' && _.length > 0) {
x.preventDefault();
let w = _.findIndex((C) => C.id === l);
let y = w > 0 ? w - 1 : _.length - 1;
c(_[y]?.id || null);
return;
}
if ((x.key === 'Tab' || x.key === 'Enter') && !x.shiftKey) {
if ('isComposing' in x && x.isComposing) return;
x.preventDefault();
if (l) {
let w = _.find((y) => y.id === l);
if (w) {
a('');
let y = x.key === 'Tab';
t(w, y);
}
}
return;
}
},
[l, _, e, t],
)(
// 滚动到选中项
0,
rr.useEffect,
)(() => {
h.current &&
h.current.scrollIntoView({
behavior: 'instant',
block: 'nearest',
});
}, [l]);
// 返回 JSX
return n
? rr.default.createElement(
'div',
{
ref: u,
className: tm.menuPopup,
},
// 过滤输入框
!r &&
rr.default.createElement('input', {
ref: d,
type: 'text',
value: f,
onChange: (x) => a(x.target.value),
onKeyDown: v,
placeholder: 'Filter actions...',
className: tm.filterInput,
}),
// 命令列表
rr.default.createElement(
'div',
{
className: tm.commandList,
},
Object.keys(g).length === 0
? rr.default.createElement(
'div',
{
className: tm.emptyState,
},
'No matching commands',
)
: Object.entries(g).map(([x, w], y) =>
// 每个分组
rr.default.createElement(
'div',
{ key: x },
// 分组标题
rr.default.createElement(
'div',
{
className: tm.sectionHeader,
},
x,
),
// 命令项
w.map((C) =>
rr.default.createElement(
'div',
{
key: C.id,
ref: C.id === l ? h : null,
className: `${tm.commandItem} ${C.id === l ? tm.activeCommandItem : ''}`,
onMouseEnter: () => c(C.id),
onClick: () => {
a('');
t(C, !1);
},
},
rr.default.createElement(
'div',
{
className: tm.commandContent,
},
rr.default.createElement(
'span',
{
className: tm.commandLabel,
},
C.label,
),
C.trailingComponent,
),
),
),
),
),
),
)
: null;
};
关键发现:
- ✅ 使用
useState管理过滤文本和选中项 - ✅
useRef管理输入框和列表项的引用 - ✅ 键盘导航: Escape/ArrowUp/ArrowDown/Tab/Enter
- ✅
scrollIntoView自动滚动到选中项
B. 文件选择器组件 (File Selector)
HTML 定位线索
- 类名:
.fileList,.fileItem,.fileName - 字符串: "No files found"
找到的 JS 代码
// 文件选择器组件
var wKe = ({ searchQuery: n, onClose: e, onFileSelect: t, onListFiles: i }) => {
let [o, r] = (0, ao.useState)([]); // files state
let [s, a] = (0, ao.useState)(0); // selectedIndex state
let l = (0, ao.useRef)(null);
let c = (0, ao.useRef)(null)(
// 防抖加载文件列表
0,
ao.useEffect,
)(() => {
let u = setTimeout(() => {
i(n)
.then((h) => {
r(h);
a(0);
})
.catch((h) => {});
}, 200); // 200ms 防抖
return () => clearTimeout(u);
}, [i, n]);
// 键盘导航
let d = (0, ao.useCallback)(
(u) => {
switch (u.key) {
case 'ArrowDown':
o.length > 1 &&
(u.preventDefault(), a((h) => (h < o.length - 1 ? h + 1 : 0)));
break;
case 'ArrowUp':
o.length > 1 &&
(u.preventDefault(), a((h) => (h > 0 ? h - 1 : o.length - 1)));
break;
case 'Tab':
case 'Enter':
u.shiftKey || (u.preventDefault(), o[s] && t(o[s], u.key === 'Tab'));
break;
case 'Escape':
u.preventDefault();
e();
break;
}
},
[o, s, t, e],
)(
// 滚动到选中项
0,
ao.useEffect,
)(() => {
c.current &&
c.current.scrollIntoView({
behavior: 'instant',
block: 'nearest',
});
}, [s])(
// 监听全局键盘事件
0,
ao.useEffect,
)(
() => (
document.addEventListener('keydown', d),
() => document.removeEventListener('keydown', d)
),
[d],
);
// 渲染文件列表
return ao.default.createElement(
'div',
{
ref: l,
className: $u.menuPopup,
},
ao.default.createElement(
'div',
{
className: $u.fileList,
},
o.length === 0
? ao.default.createElement(
'div',
{
className: $u.emptyState,
},
'No files found',
)
: o.map((u, h) =>
ao.default.createElement(
'div',
{
key: u.path,
ref: h === s ? c : null,
className: `${$u.fileItem} ${h === s ? $u.activeFileItem : ''}`,
onMouseEnter: () => a(h),
onClick: () => t(u, !1),
},
ao.default.createElement(
'div',
{
className: $u.fileContent,
},
// 文件图标和名称
u.type === 'file'
? ao.default.createElement(
'div',
{
className: $u.fileName,
},
u.name,
)
: ao.default.createElement(
'div',
{
className: $u.directoryPath,
},
u.path,
),
),
),
),
),
);
};
关键发现:
- ✅ 200ms 防抖加载
- ✅ 全局键盘事件监听
- ✅ Tab 键选中文件后继续输入
C. Footer 按钮组件
HTML 定位线索
- 类名:
.inputFooter,.footerButton,.sendButton - 字符串: "Ask before edits", "Thinking off"
找到的 JS 代码
// Footer 组件
function CKe({
session: n,
mode: e,
onCycleMode: t,
currentSelection: i,
canSendMessage: o,
toggleCommandMenu: r,
includeSelection: s,
onToggleIncludeSelection: a,
onCompact: l,
onAttachFile: c,
}) {
// 发送图标切换
let d = null;
n.busy.value && !o
? (d = lo.default.createElement(rie, { className: zl.stopIcon }))
: (d = lo.default.createElement(Xte, { className: zl.sendIcon }));
// Thinking 开关
let u = n.thinkingLevel.value !== 'off';
let h = () => {
n.setThinkingLevel(n.thinkingLevel.value === 'off' ? 'default_on' : 'off');
};
return lo.default.createElement(
'div',
{
className: zl.inputFooter,
},
// Mode 切换按钮
lo.default.createElement(DSt, {
mode: e,
onTap: t,
}),
// 文件选择按钮
i &&
lo.default.createElement(NSt, {
includeSelection: s ?? !1,
currentSelection: i,
onToggle: a ?? (() => {}),
}),
// Usage 指示器
lo.default.createElement(yKe, {
usedTokens: n.usageData.value.totalTokens,
contextWindow: n.usageData.value.contextWindow,
onCompact: l,
}),
// Spacer
lo.default.createElement('div', {
className: zl.spacer,
}),
// Thinking 按钮
lo.default.createElement(RSt, {
thinkingOn: u,
toggleThinking: h,
}),
// 命令菜单按钮
lo.default.createElement(
'button',
{
type: 'button',
className: zl.menuButton,
title: 'Show command menu (/)',
onClick: r,
},
lo.default.createElement(oie, {
className: zl.menuIcon,
}),
),
// 发送按钮
lo.default.createElement(
'button',
{
type: 'submit',
disabled: !n.busy.value && !o,
className: zl.sendButton,
'data-permission-mode': e,
onClick: (f) => {
n.busy.value && !o && (f.preventDefault(), n.interrupt());
},
},
d,
),
);
}
// Mode 按钮组件
function DSt({ mode: n, onTap: e }) {
switch (n) {
case 'acceptEdits':
return lo.default.createElement(
'button',
{
type: 'button',
className: zl.footerButton,
onClick: e,
title: 'Claude will edit your selected text or the whole file...',
},
lo.default.createElement($ye, null), // Fast forward icon
lo.default.createElement('span', null, 'Edit automatically'),
);
case 'plan':
return lo.default.createElement(
'button',
{
type: 'button',
className: zl.footerButton,
onClick: e,
title: 'Claude will explore the code and present a plan...',
},
lo.default.createElement(jye, null), // Pause icon
lo.default.createElement('span', null, 'Plan mode'),
);
case 'bypassPermissions':
return lo.default.createElement(
'button',
{
type: 'button',
className: zl.footerButton,
onClick: e,
title: 'Claude Code will not ask for your approval...',
},
lo.default.createElement(Uye, null), // Double chevron icon
lo.default.createElement('span', null, 'Bypass permissions'),
);
case 'default':
default:
return lo.default.createElement(
'button',
{
type: 'button',
className: zl.footerButton,
onClick: e,
title: 'Claude will ask before each edit...',
},
lo.default.createElement(qye, null), // Pencil icon
lo.default.createElement('span', null, 'Ask before edits'),
);
}
}
// Thinking 按钮组件
function RSt({ thinkingOn: n, toggleThinking: e }) {
return lo.default.createElement(
'button',
{
type: 'button',
className: `${zl.menuButton} ${n ? zl.menuButtonActivated : zl.menuButtonInactivated}`,
title: n ? 'Thinking on' : 'Thinking off',
onClick: e,
},
// Thinking 图标 SVG
lo.default.createElement(
'svg',
{
width: '16',
height: '16',
viewBox: '0 0 16 16',
fill: 'none',
xmlns: 'http://www.w3.org/2000/svg',
},
lo.default.createElement('path', {
d: 'M8.00293 1.11523L8.35059 1.12402H8.35352C11.9915...',
strokeWidth: '0.27',
style: {
stroke: 'var(--app-secondary-foreground)',
fill: 'var(--app-secondary-foreground)',
},
}),
),
);
}
关键发现:
- ✅ Mode 切换逻辑 (4 种模式)
- ✅ Thinking 开关状态管理
- ✅ 忙碌状态显示不同图标
- ✅ 使用
data-permission-mode属性
D. ContentEditable 输入框
HTML 定位线索
- 类名:
.d,.fo - 属性:
contenteditable="plaintext-only"
推断的实现模式
// ContentEditable 输入框 (从模式推断)
var c_ = {
inputContainer: 'u',
inputContainerBackground: 'Wr',
messageInputContainer: 'fo',
messageInput: 'd',
};
// 输入组件逻辑 (推断)
function MessageInput({ value, onChange, onSubmit }) {
const inputRef = useRef(null);
const [isComposing, setIsComposing] = useState(false);
const handleInput = () => {
if (inputRef.current) {
const newValue = inputRef.current.textContent || '';
onChange(newValue);
}
};
const handleKeyDown = (e) => {
// Enter 提交 (非 Shift+Enter)
if (e.key === 'Enter' && !e.shiftKey) {
if (e.nativeEvent.isComposing) return;
e.preventDefault();
onSubmit();
}
// Escape 取消
if (e.key === 'Escape' && !e.metaKey && !e.ctrlKey) {
e.preventDefault();
// 取消操作
}
};
useEffect(() => {
if (inputRef.current && inputRef.current.textContent !== value) {
inputRef.current.textContent = value;
}
}, [value]);
return React.createElement(
'div',
{
className: c_.messageInputContainer,
},
React.createElement('div', {
ref: inputRef,
className: c_.messageInput,
contentEditable: 'plaintext-only',
role: 'textbox',
'aria-label': 'Message input',
'aria-multiline': 'true',
'data-placeholder': 'Ask Claude to edit…',
onInput: handleInput,
onKeyDown: handleKeyDown,
spellCheck: false,
}),
);
}
四、事件处理逻辑映射
键盘快捷键汇总
| 快捷键 | 功能 | 组件 |
|---|---|---|
Escape |
关闭菜单/对话框 | 所有弹窗 |
ArrowDown |
下一项 | 命令菜单/文件选择 |
ArrowUp |
上一项 | 命令菜单/文件选择 |
Enter |
确认选择 | 命令菜单/文件选择 |
Tab |
选择并继续 | 命令菜单/文件选择 |
Shift+Tab |
切换模式 | Footer 模式按钮 |
Enter (输入框) |
发送消息 | 消息输入 |
Shift+Enter |
换行 | 消息输入 |
/ |
打开命令菜单 | 全局 |
@ |
@mentions | 输入框 |
状态管理模式
// 全局 Session 状态 (推断)
const session = {
busy: { value: boolean },
thinkingLevel: { value: "off" | "default_on" },
usageData: {
value: {
totalTokens: number,
contextWindow: number
}
},
interrupt: () => void,
setThinkingLevel: (level) => void
}
五、完整的组件层级关系
App (根组件)
├── Header (.he)
│ ├── SessionButton (.E)
│ │ └── onClick: handleSessionsClick
│ ├── Spacer (.ke)
│ └── NewButton (.j)
│ └── onClick: handleNewSession
│
├── MainContent (.be > .Q > .ue > .ye)
│ ├── EmptyState (.Re)
│ └── MessageList (.M)
│
├── InputArea
│ ├── InputForm (.u)
│ │ ├── Background (.Wr)
│ │ ├── MessageInputContainer (.fo)
│ │ │ └── ContentEditable (.d)
│ │ └── Footer (.ri)
│ │ ├── ModeButton (.l)
│ │ ├── SelectionButton (.l)
│ │ ├── UsageIndicator
│ │ ├── ThinkingButton (.H)
│ │ ├── CommandMenuButton (.H)
│ │ └── SendButton (.r)
│ │
│ └── Popups (条件渲染)
│ ├── CommandMenu (.menuPopup)
│ │ ├── FilterInput (.filterInput)
│ │ └── CommandList (.commandList)
│ │ └── CommandItem (.commandItem)
│ │
│ ├── FileSelector ($u.menuPopup)
│ │ └── FileList ($u.fileList)
│ │ └── FileItem ($u.fileItem)
│ │
│ └── PermissionRequest (Ei.permissionRequestContainer)
│ ├── Content (Ei.permissionRequestContent)
│ ├── Options (Ei.buttonContainer)
│ └── RejectInput (Ei.rejectMessageInput)
六、可直接复用的代码模式
1. 键盘导航模式
// 通用键盘导航 Hook
const useKeyboardNavigation = (items, onSelect, onClose) => {
const [selectedIndex, setSelectedIndex] = useState(0);
const handleKeyDown = useCallback(
(e) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setSelectedIndex((prev) => (prev < items.length - 1 ? prev + 1 : 0));
break;
case 'ArrowUp':
e.preventDefault();
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1));
break;
case 'Enter':
case 'Tab':
if (!e.shiftKey) {
e.preventDefault();
onSelect(items[selectedIndex], e.key === 'Tab');
}
break;
case 'Escape':
e.preventDefault();
onClose();
break;
}
},
[items, selectedIndex, onSelect, onClose],
);
useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [handleKeyDown]);
return [selectedIndex, setSelectedIndex];
};
2. 自动滚动模式
// 滚动到选中项
const selectedRef = useRef<HTMLDivElement>(null);
useEffect(() => {
selectedRef.current?.scrollIntoView({
behavior: 'instant',
block: 'nearest',
});
}, [selectedIndex]);
3. 防抖搜索模式
// 防抖加载列表
useEffect(() => {
const timer = setTimeout(() => {
fetchItems(searchQuery).then(setItems);
}, 200);
return () => clearTimeout(timer);
}, [searchQuery]);
七、总结与建议
✅ 成功提取的内容
- 完整的命令菜单组件逻辑 - 包含过滤、键盘导航、选择
- 文件选择器组件逻辑 - 包含搜索、预览、选择
- Footer 按钮逻辑 - 包含模式切换、状态管理
- 键盘导航模式 - 统一的导航逻辑
- 状态管理模式 - useState + useCallback 模式
🎯 可立即实现的组件
- ChatHeader - 参考 Footer 按钮模式
- CommandMenu - 完整代码已提取
- FileSelector - 完整代码已提取
- ContentEditable - 参考输入框模式
⏰ 实施时间估算
- ChatHeader: 2 小时
- CommandMenu 移植: 3 小时
- FileSelector 移植: 3 小时
- 测试整合: 2 小时
总计: 1 天可完成核心组件
文档版本: v3.0 最后更新: 2025-11-18 状态: 已验证可行 ✅