openclaw-openagent 1.0.13 → 1.0.14
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/dist/src/messaging/executor.d.ts +2 -3
- package/dist/src/messaging/executor.js +4 -31
- package/dist/src/plugin-ui/assets/openagent-override.js +4 -5
- package/dist/src/runtime/update-checker.d.ts +1 -1
- package/dist/src/runtime/update-checker.js +9 -46
- package/package.json +1 -3
- package/src/app/channel-tools.ts +0 -249
- package/src/app/discovery-tools.ts +0 -273
- package/src/app/download-file-tool.ts +0 -133
- package/src/app/hooks.ts +0 -144
- package/src/app/index.ts +0 -84
- package/src/app/messaging-tools.ts +0 -79
- package/src/app/ops-tools.ts +0 -155
- package/src/app/remote-agent-tool.ts +0 -762
- package/src/app/types.ts +0 -68
- package/src/app/upload-file-tool.ts +0 -411
- package/src/app/verbose-preflight.ts +0 -190
- package/src/auth/config.ts +0 -266
- package/src/auth/credential-manager.ts +0 -146
- package/src/auth/index.ts +0 -24
- package/src/auth/verify.ts +0 -99
- package/src/channel.ts +0 -566
- package/src/compat.ts +0 -94
- package/src/config/config-schema.ts +0 -45
- package/src/messaging/aggregator.ts +0 -120
- package/src/messaging/collector.ts +0 -89
- package/src/messaging/executor.ts +0 -72
- package/src/messaging/inbound.ts +0 -150
- package/src/messaging/index.ts +0 -11
- package/src/messaging/mention-protocol.ts +0 -94
- package/src/messaging/process-c2c-request.ts +0 -564
- package/src/messaging/process-message.ts +0 -373
- package/src/messaging/scheduler.ts +0 -55
- package/src/messaging/types.ts +0 -38
- package/src/plugin-ui/adapters/oc-2026-04.js +0 -103
- package/src/plugin-ui/adapters/oc-2026-05.js +0 -125
- package/src/plugin-ui/adapters/oc-2026-06.js +0 -125
- package/src/plugin-ui/adapters/oc-unknown.js +0 -48
- package/src/plugin-ui/assets/agentbook-icon.svg +0 -5
- package/src/plugin-ui/assets/bg.png +0 -0
- package/src/plugin-ui/assets/icon.png +0 -0
- package/src/plugin-ui/assets/magic.svg +0 -5
- package/src/plugin-ui/assets/openagent-override.js +0 -11263
- package/src/plugin-ui/build.cjs +0 -463
- package/src/plugin-ui/index.ts +0 -18
- package/src/plugin-ui/modules/agent-book/panel/agent-book.js +0 -570
- package/src/plugin-ui/modules/agent-book/panel/agent-card.js +0 -170
- package/src/plugin-ui/modules/agent-book/panel/agent-data.js +0 -643
- package/src/plugin-ui/modules/agent-book/panel/inject-ui.js +0 -665
- package/src/plugin-ui/modules/agent-book/panel/mention-state.js +0 -206
- package/src/plugin-ui/modules/agent-book/panel/styles.js +0 -832
- package/src/plugin-ui/modules/agent-book/panel/theme-adapter.js +0 -86
- package/src/plugin-ui/modules/agent-book/remote-agent-tool/components-core.js +0 -295
- package/src/plugin-ui/modules/agent-book/remote-agent-tool/thought-chain-card.js +0 -211
- package/src/plugin-ui/modules/agent-book/scanner.js +0 -131
- package/src/plugin-ui/modules/agent-book/travelcard/travel-cards.js +0 -500
- package/src/plugin-ui/modules/agent-book/travelcard/travel-engine.js +0 -652
- package/src/plugin-ui/modules/agent-book/travelcard/travel-styles.js +0 -262
- package/src/plugin-ui/modules/loader/bootstrap.js +0 -133
- package/src/plugin-ui/modules/loader/shared-state.js +0 -838
- package/src/plugin-ui/modules/loader/ws-intercept.js +0 -199
- package/src/plugin-ui/modules/remote-agent/chunk-parser.js +0 -161
- package/src/plugin-ui/modules/remote-agent/execution-card.js +0 -530
- package/src/plugin-ui/modules/remote-agent/markdown-renderer.js +0 -256
- package/src/plugin-ui/modules/remote-agent/media-links.js +0 -151
- package/src/plugin-ui/modules/remote-agent/native-style-adapter.js +0 -134
- package/src/plugin-ui/modules/remote-agent/output-card.js +0 -342
- package/src/plugin-ui/modules/remote-agent/progress-store.js +0 -363
- package/src/plugin-ui/modules/remote-agent/render-hooks.js +0 -1700
- package/src/plugin-ui/modules/remote-agent/styles.js +0 -903
- package/src/plugin-ui/modules/remote-agent/tool-card-model.js +0 -130
- package/src/plugin-ui/postinstall-deploy.cjs +0 -52
- package/src/plugin-ui/ui-extension-loader/backup.ts +0 -92
- package/src/plugin-ui/ui-extension-loader/index.ts +0 -276
- package/src/plugin-ui/ui-extension-loader/locator.ts +0 -152
- package/src/plugin-ui/ui-extension-loader/manifest.ts +0 -57
- package/src/plugin-ui/ui-extension-loader/registry-regex.ts +0 -944
- package/src/plugin-ui/ui-extension-loader/removed-extensions.ts +0 -70
- package/src/plugin-ui/ui-extension-loader/types.ts +0 -72
- package/src/proxy/auth-proxy.ts +0 -414
- package/src/runtime/account.ts +0 -549
- package/src/runtime/index.ts +0 -7
- package/src/runtime/plugin-runtime.ts +0 -94
- package/src/runtime/registry.ts +0 -71
- package/src/runtime/update-checker.ts +0 -286
- package/src/sdk/CLASS_MAP.md +0 -143
- package/src/sdk/index.d.ts +0 -126
- package/src/sdk/index.js +0 -23990
- package/src/sdk/modules/cloud-search-module.js +0 -1117
- package/src/sdk/modules/follow-module.js +0 -1069
- package/src/sdk/modules/group-module.js +0 -7397
- package/src/sdk/modules/relationship-module.js +0 -2269
- package/src/sdk/modules/signaling-module.js +0 -1468
- package/src/sdk/modules/tim-upload-plugin.js +0 -730
- package/src/sdk/node-env/http-request.js +0 -90
- package/src/sdk/node-env/index.js +0 -57
- package/src/sdk/node-env/storage.js +0 -114
- package/src/sdk/package.json +0 -10
- package/src/sdk/tsconfig.json +0 -16
- package/src/state/pending-invocation-store.ts +0 -43
- package/src/state/store.ts +0 -756
- package/src/tim/c2c.ts +0 -451
- package/src/tim/channels.ts +0 -364
- package/src/tim/client.ts +0 -330
- package/src/tim/index.ts +0 -18
- package/src/tim/messages.ts +0 -166
- package/src/tim/sdk-logger-init.ts +0 -50
- package/src/tools.ts +0 -10
- package/src/transport/factory.ts +0 -95
- package/src/transport/oasn/index.ts +0 -17
- package/src/transport/oasn/oasn-agent-card.ts +0 -111
- package/src/transport/oasn/oasn-discovery.ts +0 -108
- package/src/transport/oasn/oasn-files.ts +0 -210
- package/src/transport/oasn/oasn-http.ts +0 -529
- package/src/transport/oasn/oasn-invocation.ts +0 -604
- package/src/transport/oasn/oasn-normalize.ts +0 -133
- package/src/transport/oasn/oasn-register.ts +0 -33
- package/src/transport/oasn/oasn-transport.ts +0 -318
- package/src/transport/oasn/oasn-types.ts +0 -404
- package/src/transport/tim/index.ts +0 -8
- package/src/transport/tim/tim-transport.ts +0 -515
- package/src/transport/types.ts +0 -538
- package/src/types/openclaw-plugin-sdk-media-store.d.ts +0 -9
- package/src/types/openclaw.d.ts +0 -97
- package/src/types/tencentcloud-chat.d.ts +0 -15
- package/src/util/http.ts +0 -113
- package/src/util/logger.ts +0 -131
- package/src/util/url-resolver.ts +0 -77
|
@@ -1,665 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
// ── 注入逻辑(v3 — OC handleKeyDown/handleInput 钩子驱动)──
|
|
3
|
-
//
|
|
4
|
-
// 事件处理策略:
|
|
5
|
-
// keydown / input → 由 OC 注入钩子(mention-hooks UIExtension)调用 window.__clMention
|
|
6
|
-
// selectionchange → document 级事件委托(不绑 textarea,不怕 Lit 重建)
|
|
7
|
-
// beforeinput → document 级事件委托(同上)
|
|
8
|
-
// compositionstart/end → document 级事件委托(同上)
|
|
9
|
-
//
|
|
10
|
-
// tryInject() 只做 DOM 注入(@ 按钮 + CSS class),不绑定任何事件。
|
|
11
|
-
|
|
12
|
-
// ── textarea 选择器(多版本兼容)──
|
|
13
|
-
// v3 "Message..." / v4 "发消息..." / v5.x 结构化选择器
|
|
14
|
-
const _CL_TEXTAREA_SELECTOR = [
|
|
15
|
-
'textarea[placeholder*="发消息"]',
|
|
16
|
-
'textarea[placeholder*="Message"]',
|
|
17
|
-
'textarea[placeholder*="Enter to send"]',
|
|
18
|
-
'.agent-chat__composer-combobox textarea',
|
|
19
|
-
'.chat-composer textarea',
|
|
20
|
-
'.chat-input textarea',
|
|
21
|
-
'[class*="composer"] textarea:not([placeholder*="Search"])',
|
|
22
|
-
'[class*="chat-input"] textarea',
|
|
23
|
-
'[data-component="chat-input"] textarea',
|
|
24
|
-
].join(', ');
|
|
25
|
-
|
|
26
|
-
function _getTextarea() {
|
|
27
|
-
return document.querySelector(_CL_TEXTAREA_SELECTOR);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function _setTextareaValue(textarea, value) {
|
|
31
|
-
const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
|
|
32
|
-
if (setter) setter.call(textarea, value);
|
|
33
|
-
else textarea.value = value;
|
|
34
|
-
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function _escapeRemoteAgentMarkerValue(value) {
|
|
38
|
-
return String(value || '')
|
|
39
|
-
.replace(/\s+/g, ' ')
|
|
40
|
-
.trim()
|
|
41
|
-
.replace(/"/g, "'");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function _buildRemoteAgentMarker(mention, userText) {
|
|
45
|
-
const hasChinese = /[\u4e00-\u9fff]/.test(userText || '');
|
|
46
|
-
const instruction = hasChinese
|
|
47
|
-
? '请调用 openagent_call_remote_agent 工具将此请求转发给远端 Agent 处理'
|
|
48
|
-
: 'Please invoke the openagent_call_remote_agent tool to forward this request to the remote agent';
|
|
49
|
-
const attrs = [
|
|
50
|
-
`agent_id="${_escapeRemoteAgentMarkerValue(mention.agent_id)}"`,
|
|
51
|
-
`agent_name="${_escapeRemoteAgentMarkerValue(mention.name)}"`,
|
|
52
|
-
];
|
|
53
|
-
if (mention.agent_bio) {
|
|
54
|
-
attrs.push(`agent_bio="${_escapeRemoteAgentMarkerValue(mention.agent_bio)}"`);
|
|
55
|
-
}
|
|
56
|
-
attrs.push(`instruction="${_escapeRemoteAgentMarkerValue(instruction)}"`);
|
|
57
|
-
return `{openagent_call_remote_agent:${attrs.join(',')}}`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function _composeRemoteAgentMessage(textarea, mention) {
|
|
61
|
-
const currentVal = textarea.value || '';
|
|
62
|
-
const marker = _buildRemoteAgentMarker(mention, currentVal);
|
|
63
|
-
const escapedName = mention.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
64
|
-
const atPattern = new RegExp('@' + escapedName + '\\s*');
|
|
65
|
-
return {
|
|
66
|
-
currentVal,
|
|
67
|
-
marker,
|
|
68
|
-
message: currentVal.replace(atPattern, marker + ' ')
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function _isLikelyNativeSendButton(target, textarea) {
|
|
73
|
-
const button = target && target.closest ? target.closest('button, [role="button"]') : null;
|
|
74
|
-
if (!button || !textarea) return false;
|
|
75
|
-
if (button.classList && button.classList.contains('openagent-at-btn')) return false;
|
|
76
|
-
|
|
77
|
-
const inputContainer = textarea.closest('.agent-chat__composer-combobox')
|
|
78
|
-
|| textarea.closest('.chat-input')
|
|
79
|
-
|| textarea.closest('[class*="chat-composer"]')
|
|
80
|
-
|| textarea.closest('[class*="chat"]')
|
|
81
|
-
|| textarea.parentElement;
|
|
82
|
-
if (!inputContainer || !inputContainer.contains(button)) return false;
|
|
83
|
-
|
|
84
|
-
const label = [
|
|
85
|
-
button.getAttribute('aria-label'),
|
|
86
|
-
button.getAttribute('title'),
|
|
87
|
-
button.textContent
|
|
88
|
-
].filter(Boolean).join(' ').toLowerCase();
|
|
89
|
-
if (/send|发送/.test(label)) return true;
|
|
90
|
-
|
|
91
|
-
const buttons = Array.from(inputContainer.querySelectorAll('button, [role="button"]'))
|
|
92
|
-
.filter(function(btn) {
|
|
93
|
-
if (btn.classList && btn.classList.contains('openagent-at-btn')) return false;
|
|
94
|
-
if (btn.disabled || btn.getAttribute('aria-disabled') === 'true') return false;
|
|
95
|
-
return btn.offsetParent !== null;
|
|
96
|
-
});
|
|
97
|
-
return buttons.length > 0 && buttons[buttons.length - 1] === button;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function _sendPendingMention(textarea) {
|
|
101
|
-
if (!textarea || !CL.pendingMention) return false;
|
|
102
|
-
if (CL.remoteSendInFlight) return true;
|
|
103
|
-
|
|
104
|
-
const mention = CL.pendingMention;
|
|
105
|
-
const payload = _composeRemoteAgentMessage(textarea, mention);
|
|
106
|
-
CL.remoteSendInFlight = true;
|
|
107
|
-
|
|
108
|
-
console.log('[cl-diag][send-marker]', {
|
|
109
|
-
agentId: mention.agent_id,
|
|
110
|
-
agentName: mention.name,
|
|
111
|
-
userTextPreview: payload.currentVal.slice(0, 200),
|
|
112
|
-
markerPreview: payload.marker.slice(0, 240),
|
|
113
|
-
sendValuePreview: payload.message.slice(0, 260),
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// 不把内部 marker 写入 textarea。可见输入框按原生发送行为立即清空;
|
|
117
|
-
// 真正发给 OpenClaw 的带 marker 文本只作为 handleSendChat 参数传入。
|
|
118
|
-
_setTextareaValue(textarea, '');
|
|
119
|
-
|
|
120
|
-
(async () => {
|
|
121
|
-
try {
|
|
122
|
-
CL.lastMentionAgentId = mention.agent_id;
|
|
123
|
-
CL.pendingMention = null;
|
|
124
|
-
await CL.sendRemoteAgentMessage(payload.message);
|
|
125
|
-
console.log('[openagent] remote call sent');
|
|
126
|
-
} catch (err) {
|
|
127
|
-
CL.pendingMention = mention;
|
|
128
|
-
_setTextareaValue(textarea, payload.currentVal);
|
|
129
|
-
console.error('[openagent] remote call send failed', err);
|
|
130
|
-
window.alert?.('OpenAgent remote call 未发送:请检查 gateway 连接后重试。');
|
|
131
|
-
} finally {
|
|
132
|
-
CL.remoteSendInFlight = false;
|
|
133
|
-
}
|
|
134
|
-
})();
|
|
135
|
-
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function _installSendClickInterceptor() {
|
|
140
|
-
if (window.__openagentSendClickInterceptorInstalled) return;
|
|
141
|
-
window.__openagentSendClickInterceptorInstalled = true;
|
|
142
|
-
document.addEventListener('click', function(e) {
|
|
143
|
-
if (!CL.pendingMention || CL.remoteSendInFlight) return;
|
|
144
|
-
const textarea = _getTextarea();
|
|
145
|
-
if (!_isLikelyNativeSendButton(e.target, textarea)) return;
|
|
146
|
-
|
|
147
|
-
e.preventDefault();
|
|
148
|
-
e.stopPropagation();
|
|
149
|
-
if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
|
|
150
|
-
_sendPendingMention(textarea);
|
|
151
|
-
}, true);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function _installSendKeydownInterceptor() {
|
|
155
|
-
if (window.__openagentSendKeydownInterceptorInstalled) return;
|
|
156
|
-
window.__openagentSendKeydownInterceptorInstalled = true;
|
|
157
|
-
document.addEventListener('keydown', function(e) {
|
|
158
|
-
if (!CL.pendingMention || CL.remoteSendInFlight) return;
|
|
159
|
-
if (e.key !== 'Enter' || e.shiftKey || e.isComposing) return;
|
|
160
|
-
|
|
161
|
-
const textarea = _getTextarea();
|
|
162
|
-
if (!textarea || e.target !== textarea) return;
|
|
163
|
-
|
|
164
|
-
e.preventDefault();
|
|
165
|
-
e.stopPropagation();
|
|
166
|
-
if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
|
|
167
|
-
_sendPendingMention(textarea);
|
|
168
|
-
}, true);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* 多策略定位 textarea 所在输入区域的工具栏行。
|
|
173
|
-
* v4.x: 工具栏在 textarea.parentElement.children 中
|
|
174
|
-
* v5.x: 工具栏可能嵌套在祖父容器或包含 toolbar class
|
|
175
|
-
* 返回 null 表示未找到则回退到 textarea 父级插入
|
|
176
|
-
*/
|
|
177
|
-
function _findToolbarRow(textarea) {
|
|
178
|
-
var parent = textarea.parentElement;
|
|
179
|
-
|
|
180
|
-
// 策略 1(v4.x): textarea 兄弟元素含 SVG 图标
|
|
181
|
-
if (parent) {
|
|
182
|
-
for (var i = 0; i < parent.children.length; i++) {
|
|
183
|
-
if (parent.children[i] !== textarea && parent.children[i].querySelector('svg')) return parent.children[i];
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// 策略 2(v4.x): 祖父级兄弟元素含 SVG 图标
|
|
188
|
-
if (parent) {
|
|
189
|
-
var grandparent = parent.parentElement;
|
|
190
|
-
if (grandparent) {
|
|
191
|
-
for (var j = 0; j < grandparent.children.length; j++) {
|
|
192
|
-
if (grandparent.children[j] !== parent && grandparent.children[j].querySelector('svg')) return grandparent.children[j];
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// 策略 3(v5.x): 搜索包含 toolbar class 的元素
|
|
198
|
-
var chatContainer = textarea.closest('[class*="chat"]');
|
|
199
|
-
if (chatContainer) {
|
|
200
|
-
var toolbarEl = chatContainer.querySelector('[class*="toolbar"]');
|
|
201
|
-
if (toolbarEl) return toolbarEl;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// 策略 4(v5.x fallback): 搜索 voice/mic 按钮定位其容器
|
|
205
|
-
var voiceBtn = document.querySelector(
|
|
206
|
-
'button[aria-label*="voice"], button[aria-label*="语音"], ' +
|
|
207
|
-
'button[aria-label*="mic"], button[aria-label*="麦克风"]'
|
|
208
|
-
);
|
|
209
|
-
if (voiceBtn) return voiceBtn.parentElement;
|
|
210
|
-
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function tryInject() {
|
|
215
|
-
const textarea = _getTextarea();
|
|
216
|
-
|
|
217
|
-
if (!textarea) return false;
|
|
218
|
-
_installSendClickInterceptor();
|
|
219
|
-
_installSendKeydownInterceptor();
|
|
220
|
-
if (document.querySelector('.openagent-at-btn')) return true; // 已注入
|
|
221
|
-
|
|
222
|
-
// ── 定位插入点:麦克风图标所在的 toolbar 行(多策略)──
|
|
223
|
-
var toolbarRow = _findToolbarRow(textarea);
|
|
224
|
-
|
|
225
|
-
// ── 创建 Agent Book 按钮 ──
|
|
226
|
-
const atBtn = document.createElement('button');
|
|
227
|
-
atBtn.className = 'openagent-at-btn';
|
|
228
|
-
atBtn.innerHTML = '<span class="at-sym">@</span> Agent Book';
|
|
229
|
-
atBtn.addEventListener('click', () => {
|
|
230
|
-
// 在 textarea 中插入 @ 字符,让状态机自然检测 → openPanel
|
|
231
|
-
const ta = _getTextarea();
|
|
232
|
-
if (!ta) return;
|
|
233
|
-
ta.focus();
|
|
234
|
-
const val = ta.value;
|
|
235
|
-
const pos = ta.selectionStart;
|
|
236
|
-
// 如果光标前已经是 @,不重复插入
|
|
237
|
-
if (pos > 0 && val[pos - 1] === '@') {
|
|
238
|
-
// 按钮主动触发 → 清除 dismissed 状态,确保弹窗能打开
|
|
239
|
-
if (typeof resetMention === 'function') resetMention();
|
|
240
|
-
_handleStateChange(ta);
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
// 插入 @ 字符(前面加空格分隔,除非行首/空白后)
|
|
244
|
-
const needSpace = pos > 0 && !/[\s\n]/.test(val[pos - 1]);
|
|
245
|
-
const insert = needSpace ? ' @' : '@';
|
|
246
|
-
const newVal = val.slice(0, pos) + insert + val.slice(pos);
|
|
247
|
-
const setter = Object.getOwnPropertyDescriptor(
|
|
248
|
-
HTMLTextAreaElement.prototype, 'value'
|
|
249
|
-
)?.set;
|
|
250
|
-
if (setter) setter.call(ta, newVal);
|
|
251
|
-
else ta.value = newVal;
|
|
252
|
-
ta.selectionStart = ta.selectionEnd = pos + insert.length;
|
|
253
|
-
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
254
|
-
// input 事件 → OC handleInput → __clMention.onInput → 状态机打开面板
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
if (toolbarRow) {
|
|
258
|
-
const toolbarLeft = toolbarRow.querySelector('[class*="toolbar-left"]') || toolbarRow;
|
|
259
|
-
const iconButtons = toolbarLeft.querySelectorAll('button, [role="button"]');
|
|
260
|
-
// 优先按 aria-label 匹配 voice/mic 按钮,回退到位置假设
|
|
261
|
-
let micBtn = null;
|
|
262
|
-
for (var i = 0; i < iconButtons.length; i++) {
|
|
263
|
-
var label = (iconButtons[i].getAttribute('aria-label') || '').toLowerCase();
|
|
264
|
-
if (/voice|mic|语音|麦克风/.test(label)) { micBtn = iconButtons[i]; break; }
|
|
265
|
-
}
|
|
266
|
-
if (!micBtn && iconButtons.length >= 2) {
|
|
267
|
-
micBtn = iconButtons[1]; // fallback: v4.x 中 mic 一般是第 2 个按钮
|
|
268
|
-
} else if (!micBtn && iconButtons.length === 1) {
|
|
269
|
-
micBtn = iconButtons[0];
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (micBtn) {
|
|
273
|
-
micBtn.after(atBtn);
|
|
274
|
-
} else {
|
|
275
|
-
toolbarLeft.appendChild(atBtn);
|
|
276
|
-
}
|
|
277
|
-
} else {
|
|
278
|
-
// 无工具栏时回退到 textarea 的父级插入
|
|
279
|
-
var fallbackParent = textarea.parentElement;
|
|
280
|
-
if (fallbackParent) fallbackParent.appendChild(atBtn);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// ── 给输入区域加 position: relative 包裹层 ──
|
|
284
|
-
var inputParent = textarea.parentElement;
|
|
285
|
-
const inputContainer = textarea.closest('.agent-chat__composer-combobox')
|
|
286
|
-
|| textarea.closest('.chat-input')
|
|
287
|
-
|| textarea.closest('[class*="chat-composer"]')
|
|
288
|
-
|| textarea.closest('[class*="chat"]')
|
|
289
|
-
|| (inputParent ? inputParent.parentElement : null)
|
|
290
|
-
|| inputParent;
|
|
291
|
-
if (inputContainer) {
|
|
292
|
-
inputContainer.classList.add('openagent-input-wrapper');
|
|
293
|
-
inputWrapperRef = inputContainer;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
console.log('[openagent] ✅ Agent Book UI injected (v3 hook-driven)');
|
|
297
|
-
|
|
298
|
-
// ── 提示栏(Figma 141:3293):页面加载即显示 ──
|
|
299
|
-
_injectHintBar(inputContainer || textarea.parentElement, toolbarRow);
|
|
300
|
-
|
|
301
|
-
return true;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/** 注入提示栏到输入区域上方 */
|
|
305
|
-
function _injectHintBar(anchorEl, toolbarEl) {
|
|
306
|
-
if (!anchorEl || document.querySelector('.openagent-hint-bar')) return;
|
|
307
|
-
|
|
308
|
-
const hintBar = document.createElement('div');
|
|
309
|
-
hintBar.className = 'openagent-hint-bar';
|
|
310
|
-
hintBar.innerHTML = '' +
|
|
311
|
-
'<div class="openagent-hint-bar__content">' +
|
|
312
|
-
'<div class="openagent-hint-bar__icon">' +
|
|
313
|
-
'<svg xmlns="http://www.w3.org/2000/svg" width="27" height="27" viewBox="0 0 27 27" fill="none">' +
|
|
314
|
-
'<rect width="27" height="27" rx="6" fill="url(#paint0_linear_141_3277)" fill-opacity="0.1"/>' +
|
|
315
|
-
'<path d="M13.5 6.75098C16.8137 6.75098 19.5 9.43727 19.5 12.751C19.5 14.3288 18.8894 15.7629 17.8936 16.834L20.7803 19.7207C21.0732 20.0136 21.0732 20.4884 20.7803 20.7812C20.4874 21.0741 20.0126 21.0741 19.7197 20.7812L16.7383 17.7998C15.8038 18.4004 14.6933 18.751 13.5 18.751C10.1863 18.751 7.5 16.0647 7.5 12.751C7.5 9.43727 10.1863 6.75098 13.5 6.75098ZM13.5 8.25098C11.0147 8.25098 9 10.2657 9 12.751C9 15.2363 11.0147 17.251 13.5 17.251C15.9853 17.251 18 15.2363 18 12.751C18 10.2657 15.9853 8.25098 13.5 8.25098Z" fill="url(#paint1_linear_141_3277)"/>' +
|
|
316
|
-
'<defs>' +
|
|
317
|
-
'<linearGradient id="paint0_linear_141_3277" x1="0" y1="13.5" x2="27" y2="13.5" gradientUnits="userSpaceOnUse">' +
|
|
318
|
-
'<stop stop-color="#8274FF"/>' +
|
|
319
|
-
'<stop offset="1" stop-color="#3890FF"/>' +
|
|
320
|
-
'</linearGradient>' +
|
|
321
|
-
'<linearGradient id="paint1_linear_141_3277" x1="7.5" y1="13.8759" x2="20.9999" y2="13.8759" gradientUnits="userSpaceOnUse">' +
|
|
322
|
-
'<stop stop-color="#8274FF"/>' +
|
|
323
|
-
'<stop offset="1" stop-color="#3890FF"/>' +
|
|
324
|
-
'</linearGradient>' +
|
|
325
|
-
'</defs>' +
|
|
326
|
-
'</svg>' +
|
|
327
|
-
'</div>' +
|
|
328
|
-
'<span class="openagent-hint-bar__text">输入你的需求,调配远端Agent为你解决</span>' +
|
|
329
|
-
'</div>' +
|
|
330
|
-
'<button class="openagent-hint-bar__close">' +
|
|
331
|
-
'<svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 21 21" fill="none">' +
|
|
332
|
-
'<path d="M5.625 5.625L15.375 15.375" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>' +
|
|
333
|
-
'<path d="M15.375 5.625L5.625 15.375" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>' +
|
|
334
|
-
'</svg>' +
|
|
335
|
-
'</button>';
|
|
336
|
-
|
|
337
|
-
hintBar.querySelector('.openagent-hint-bar__close').addEventListener('click', () => {
|
|
338
|
-
hintBar.style.display = 'none';
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
// ── V2 版本兼容:≥ 2026.6.9 使用 absolute 定位,bottom 动态跟随输入区高度 ──
|
|
342
|
-
var isV2 = typeof _clIsHostVersionGte === 'function' && _clIsHostVersionGte('2026.6.9');
|
|
343
|
-
if (isV2) {
|
|
344
|
-
hintBar.style.position = 'absolute';
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// 插入到 wrapper 外面、上方
|
|
348
|
-
anchorEl.parentElement.insertBefore(hintBar, anchorEl);
|
|
349
|
-
|
|
350
|
-
function _findHintBarToolbar() {
|
|
351
|
-
if (toolbarEl) return toolbarEl;
|
|
352
|
-
var parentEl = anchorEl.parentElement;
|
|
353
|
-
if (parentEl) {
|
|
354
|
-
for (var i = 0; i < parentEl.children.length; i++) {
|
|
355
|
-
var child = parentEl.children[i];
|
|
356
|
-
if (child !== anchorEl && child.className && String(child.className).indexOf('agent-chat__toolbar') !== -1) {
|
|
357
|
-
return child;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
return anchorEl.closest && anchorEl.closest('.agent-chat__input')
|
|
362
|
-
? anchorEl.closest('.agent-chat__input').querySelector('.agent-chat__toolbar')
|
|
363
|
-
: null;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function _readElementHeight(el) {
|
|
367
|
-
if (!el) return 0;
|
|
368
|
-
var rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
|
|
369
|
-
if (rect && rect.height > 0) return rect.height;
|
|
370
|
-
return typeof el.offsetHeight === 'number' ? el.offsetHeight : 0;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// 跟随 composer wrapper 的实际尺寸。侧栏拖拽/布局重排不一定触发 window resize。
|
|
374
|
-
function _syncHintBarLayout() {
|
|
375
|
-
var rect = anchorEl.getBoundingClientRect();
|
|
376
|
-
if (rect.width > 0) {
|
|
377
|
-
hintBar.style.width = rect.width + 'px';
|
|
378
|
-
var rootStyle = document.documentElement && document.documentElement.style;
|
|
379
|
-
if (rootStyle) {
|
|
380
|
-
rootStyle.setProperty('--openagent-input-wrapper-right-offset', Math.max(0, window.innerWidth - rect.right) + 'px');
|
|
381
|
-
rootStyle.setProperty('--openagent-input-wrapper-right-edge', Math.max(0, rect.right) + 'px');
|
|
382
|
-
rootStyle.setProperty('--openagent-input-wrapper-width', rect.width + 'px');
|
|
383
|
-
}
|
|
384
|
-
_syncSidebarRightEdge(rect);
|
|
385
|
-
}
|
|
386
|
-
if (isV2) {
|
|
387
|
-
var toolbar = _findHintBarToolbar();
|
|
388
|
-
var bottom = _readElementHeight(anchorEl) + _readElementHeight(toolbar) + 6;
|
|
389
|
-
hintBar.style.bottom = bottom + 'px';
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function _syncSidebarRightEdge(inputRect) {
|
|
394
|
-
var sidebars = document.querySelectorAll('.chat-sidebar');
|
|
395
|
-
for (var i = 0; i < sidebars.length; i++) {
|
|
396
|
-
var sidebar = sidebars[i];
|
|
397
|
-
var sidebarRect = sidebar.getBoundingClientRect();
|
|
398
|
-
if (!sidebarRect || sidebarRect.width <= 0) continue;
|
|
399
|
-
var left = Math.max(0, sidebarRect.left);
|
|
400
|
-
var targetRight = Math.min(window.innerWidth, Math.max(0, inputRect.right));
|
|
401
|
-
var targetWidth = Math.floor(targetRight - left);
|
|
402
|
-
if (targetWidth <= 0) continue;
|
|
403
|
-
sidebar.style.setProperty('--openagent-sidebar-target-width', targetWidth + 'px');
|
|
404
|
-
sidebar.style.setProperty('width', targetWidth + 'px', 'important');
|
|
405
|
-
sidebar.style.setProperty('max-width', targetWidth + 'px', 'important');
|
|
406
|
-
sidebar.style.setProperty('flex-basis', targetWidth + 'px', 'important');
|
|
407
|
-
sidebar.style.setProperty('margin-right', '0px', 'important');
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
function _scheduleHintBarLayoutSync() {
|
|
412
|
-
window.requestAnimationFrame
|
|
413
|
-
? window.requestAnimationFrame(_syncHintBarLayout)
|
|
414
|
-
: setTimeout(_syncHintBarLayout, 0);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
_syncHintBarLayout();
|
|
418
|
-
_scheduleHintBarLayoutSync();
|
|
419
|
-
window.addEventListener('resize', _scheduleHintBarLayoutSync);
|
|
420
|
-
if (typeof ResizeObserver === 'function') {
|
|
421
|
-
var hintBarResizeObserver = new ResizeObserver(_scheduleHintBarLayoutSync);
|
|
422
|
-
hintBarResizeObserver.observe(anchorEl);
|
|
423
|
-
var toolbarForResize = _findHintBarToolbar();
|
|
424
|
-
if (toolbarForResize) hintBarResizeObserver.observe(toolbarForResize);
|
|
425
|
-
}
|
|
426
|
-
if (typeof MutationObserver === 'function' && document.body) {
|
|
427
|
-
var sidebarLayoutObserver = new MutationObserver(_scheduleHintBarLayoutSync);
|
|
428
|
-
sidebarLayoutObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// ════════════════════════════════════════════════════════════════
|
|
433
|
-
// ── window.__clMention — OC 注入钩子的桥接对象 ──
|
|
434
|
-
// ════════════════════════════════════════════════════════════════
|
|
435
|
-
//
|
|
436
|
-
// OC 的 handleKeyDown 中注入了:
|
|
437
|
-
// if (window.__clMention?.onKeyDown(e)) return;
|
|
438
|
-
//
|
|
439
|
-
// OC 的 handleInput 中注入了:
|
|
440
|
-
// window.__clMention?.onInput?.(target);
|
|
441
|
-
//
|
|
442
|
-
// 所有业务逻辑在此处理,注入端只做调用转发。
|
|
443
|
-
|
|
444
|
-
window.__clMention = {
|
|
445
|
-
|
|
446
|
-
/**
|
|
447
|
-
* OC handleKeyDown 钩子。
|
|
448
|
-
* 返回 true → OC 跳过后续处理(包括 Enter 发送)。
|
|
449
|
-
* 返回 false → OC 继续正常处理。
|
|
450
|
-
*/
|
|
451
|
-
onKeyDown(e) {
|
|
452
|
-
const textarea = e.target;
|
|
453
|
-
|
|
454
|
-
// ── 1. 面板打开时:键盘导航 ──
|
|
455
|
-
if (isActive()) {
|
|
456
|
-
switch (e.key) {
|
|
457
|
-
case 'ArrowDown':
|
|
458
|
-
e.preventDefault();
|
|
459
|
-
moveSelectionDown();
|
|
460
|
-
return true;
|
|
461
|
-
|
|
462
|
-
case 'ArrowUp':
|
|
463
|
-
e.preventDefault();
|
|
464
|
-
moveSelectionUp();
|
|
465
|
-
return true;
|
|
466
|
-
|
|
467
|
-
case 'Enter':
|
|
468
|
-
if (e.shiftKey) return false; // Shift+Enter = 换行,交给 OC
|
|
469
|
-
e.preventDefault();
|
|
470
|
-
selectHighlightedAgent(textarea);
|
|
471
|
-
return true; // 阻止 OC 发送
|
|
472
|
-
|
|
473
|
-
case 'Escape':
|
|
474
|
-
e.preventDefault();
|
|
475
|
-
setDismissed();
|
|
476
|
-
closePanel();
|
|
477
|
-
return true;
|
|
478
|
-
}
|
|
479
|
-
// 其他键(字符输入):不拦截,让 OC 继续 → handleInput → onInput
|
|
480
|
-
return false;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// ── 2. Backspace 原子删除 @mention ──
|
|
484
|
-
// 对标 TIM mention.ts:321-356
|
|
485
|
-
// 整体删除 @AgentName(含 @ 符号),不保留 @,不重新弹出面板
|
|
486
|
-
if (e.key === 'Backspace' && CL.pendingMention) {
|
|
487
|
-
const val = textarea.value;
|
|
488
|
-
const pos = textarea.selectionStart;
|
|
489
|
-
const name = CL.pendingMention.name;
|
|
490
|
-
const mention = '@' + name;
|
|
491
|
-
|
|
492
|
-
const searchStart = Math.max(0, pos - mention.length - 1);
|
|
493
|
-
const nearText = val.substring(searchStart, pos);
|
|
494
|
-
const idx = nearText.lastIndexOf(mention);
|
|
495
|
-
|
|
496
|
-
if (idx !== -1) {
|
|
497
|
-
const mentionStart = searchStart + idx;
|
|
498
|
-
let mentionEnd = mentionStart + mention.length;
|
|
499
|
-
if (val[mentionEnd] === ' ') mentionEnd++; // 包含尾部空格
|
|
500
|
-
|
|
501
|
-
if (pos > mentionStart && pos <= mentionEnd) {
|
|
502
|
-
e.preventDefault();
|
|
503
|
-
// 整体删除 @AgentName(含 @ 符号)
|
|
504
|
-
const newVal = val.slice(0, mentionStart) + val.slice(mentionEnd);
|
|
505
|
-
|
|
506
|
-
const setter = Object.getOwnPropertyDescriptor(
|
|
507
|
-
HTMLTextAreaElement.prototype, 'value'
|
|
508
|
-
)?.set;
|
|
509
|
-
if (setter) setter.call(textarea, newVal);
|
|
510
|
-
else textarea.value = newVal;
|
|
511
|
-
|
|
512
|
-
// 光标放在删除位置
|
|
513
|
-
textarea.selectionStart = textarea.selectionEnd = mentionStart;
|
|
514
|
-
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
515
|
-
CL.pendingMention = null;
|
|
516
|
-
// @ 已被删除,状态机不会检测到 @ → 面板不会弹出
|
|
517
|
-
return true;
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// ── 3. Enter 发送时:标记替换(面板关闭 + 有 pendingMention)──
|
|
523
|
-
// 这里接管原生发送,只是为了把可见 @mention 替换成 OpenAgent 内部 marker。
|
|
524
|
-
if (e.key === 'Enter' && !e.shiftKey && CL.pendingMention) {
|
|
525
|
-
e.preventDefault();
|
|
526
|
-
_sendPendingMention(textarea);
|
|
527
|
-
|
|
528
|
-
// 返回 true:OC 注入钩子会停止原生 Enter 发送,避免重复发送。
|
|
529
|
-
return true;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
return false;
|
|
533
|
-
},
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* OC handleInput 钩子。
|
|
537
|
-
* 在 OC 的 props.onDraftChange(target.value) 之后调用。
|
|
538
|
-
*/
|
|
539
|
-
onInput(textarea) {
|
|
540
|
-
_handleStateChange(textarea);
|
|
541
|
-
},
|
|
542
|
-
|
|
543
|
-
// ── 暴露给外部查询的辅助方法 ──
|
|
544
|
-
active() { return isActive(); },
|
|
545
|
-
};
|
|
546
|
-
|
|
547
|
-
// ════════════════════════════════════════════════════════════════
|
|
548
|
-
// ── 事件委托(document 级,不怕 Lit 重建)──
|
|
549
|
-
// ════════════════════════════════════════════════════════════════
|
|
550
|
-
|
|
551
|
-
// ── 输入法组合状态跟踪 ──
|
|
552
|
-
// 对标 suggestion.ts:508 "editor.view.composing"
|
|
553
|
-
// 输入法组合中的 @ 不应触发面板(§1.1 ⑥)
|
|
554
|
-
document.addEventListener('compositionstart', (e) => {
|
|
555
|
-
if (e.target?.matches?.(_CL_TEXTAREA_SELECTOR)) {
|
|
556
|
-
setComposing(true);
|
|
557
|
-
}
|
|
558
|
-
});
|
|
559
|
-
document.addEventListener('compositionend', (e) => {
|
|
560
|
-
if (e.target?.matches?.(_CL_TEXTAREA_SELECTOR)) {
|
|
561
|
-
setComposing(false);
|
|
562
|
-
// 输入法确认后可能产生 @,OC 的 handleInput 会触发 onInput
|
|
563
|
-
// 但 compositionend 在 input 之前触发,需要主动评估一次
|
|
564
|
-
_handleStateChange(e.target);
|
|
565
|
-
}
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
// ── 光标变化检测 ──
|
|
569
|
-
// 对标 suggestion.ts:473 "选区变化也产生 transaction"
|
|
570
|
-
// 实现 §1.3 ⑤ "点击输入框内其他位置 → 面板关闭"
|
|
571
|
-
let _guardSelection = false;
|
|
572
|
-
let _lastCursorPos = -1; // 追踪上次光标位置,用于判断进入 mention 的方向
|
|
573
|
-
document.addEventListener('selectionchange', () => {
|
|
574
|
-
const textarea = _getTextarea();
|
|
575
|
-
if (!textarea || document.activeElement !== textarea) return;
|
|
576
|
-
if (_guardSelection) return;
|
|
577
|
-
|
|
578
|
-
// mention 光标保护(§1.6 原子标签)
|
|
579
|
-
// 对标 TIM:左键从右侧进入 → 跳到 @ 之前;右键从左侧进入 → 跳到末尾
|
|
580
|
-
const mentionRange = _findMentionRange(textarea.value);
|
|
581
|
-
if (mentionRange) {
|
|
582
|
-
const pos = textarea.selectionStart;
|
|
583
|
-
if (pos > mentionRange.start && pos < mentionRange.end &&
|
|
584
|
-
textarea.selectionStart === textarea.selectionEnd) {
|
|
585
|
-
_guardSelection = true;
|
|
586
|
-
if (_lastCursorPos >= 0 && _lastCursorPos >= mentionRange.end) {
|
|
587
|
-
// 箭头左键从右侧进入 → 跳到 @ 之前
|
|
588
|
-
textarea.selectionStart = textarea.selectionEnd = mentionRange.start;
|
|
589
|
-
} else if (_lastCursorPos >= 0 && _lastCursorPos <= mentionRange.start) {
|
|
590
|
-
// 箭头右键从左侧进入 → 跳到末尾
|
|
591
|
-
textarea.selectionStart = textarea.selectionEnd = mentionRange.end;
|
|
592
|
-
} else {
|
|
593
|
-
// 鼠标点击跳入 或 _lastCursorPos 未知(-1) → 跳到最近边界
|
|
594
|
-
const distToStart = pos - mentionRange.start;
|
|
595
|
-
const distToEnd = mentionRange.end - pos;
|
|
596
|
-
textarea.selectionStart = textarea.selectionEnd =
|
|
597
|
-
distToStart <= distToEnd ? mentionRange.start : mentionRange.end;
|
|
598
|
-
}
|
|
599
|
-
_lastCursorPos = textarea.selectionStart;
|
|
600
|
-
_guardSelection = false;
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
_lastCursorPos = textarea.selectionStart;
|
|
606
|
-
// 状态机评估(光标移走 → 可能关闭面板)
|
|
607
|
-
_handleStateChange(textarea);
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
// ── mention 原子保护:阻止在 @mention 内部输入字符(§1.6)──
|
|
611
|
-
// 事件委托:OC 不监听 beforeinput,不会冲突
|
|
612
|
-
document.addEventListener('beforeinput', (e) => {
|
|
613
|
-
if (!e.target?.matches?.(_CL_TEXTAREA_SELECTOR)) return;
|
|
614
|
-
const textarea = e.target;
|
|
615
|
-
const mentionRange = _findMentionRange(textarea.value);
|
|
616
|
-
if (!mentionRange) return;
|
|
617
|
-
const selStart = textarea.selectionStart;
|
|
618
|
-
const selEnd = textarea.selectionEnd;
|
|
619
|
-
// 编辑范围与 mention 重叠 → 阻止
|
|
620
|
-
if (selStart > mentionRange.start && selStart < mentionRange.end) {
|
|
621
|
-
e.preventDefault();
|
|
622
|
-
} else if (selEnd > mentionRange.start && selEnd < mentionRange.end) {
|
|
623
|
-
e.preventDefault();
|
|
624
|
-
}
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
// ── 辅助函数 ──
|
|
628
|
-
|
|
629
|
-
/**
|
|
630
|
-
* 统一的状态变化处理:调用状态机 → 根据结果操作面板。
|
|
631
|
-
*/
|
|
632
|
-
function _handleStateChange(textarea) {
|
|
633
|
-
const val = textarea.value;
|
|
634
|
-
const result = evaluateState(val, textarea.selectionStart, textarea.selectionEnd);
|
|
635
|
-
|
|
636
|
-
switch (result.action) {
|
|
637
|
-
case 'open':
|
|
638
|
-
openPanel(textarea);
|
|
639
|
-
break;
|
|
640
|
-
case 'update':
|
|
641
|
-
updatePanelSearch(textarea);
|
|
642
|
-
break;
|
|
643
|
-
case 'close':
|
|
644
|
-
closePanel();
|
|
645
|
-
break;
|
|
646
|
-
// 'none': 不做任何操作
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
/**
|
|
651
|
-
* 找到 textarea.value 中当前 mention 的范围。
|
|
652
|
-
* 用于原子保护(beforeinput / selectionchange)。
|
|
653
|
-
*
|
|
654
|
-
* @returns {{ start: number, end: number }|null}
|
|
655
|
-
*/
|
|
656
|
-
function _findMentionRange(value) {
|
|
657
|
-
if (!CL.pendingMention) return null;
|
|
658
|
-
const name = CL.pendingMention.name;
|
|
659
|
-
const mention = '@' + name;
|
|
660
|
-
const idx = value.lastIndexOf(mention);
|
|
661
|
-
if (idx === -1) return null;
|
|
662
|
-
let end = idx + mention.length;
|
|
663
|
-
if (value[end] === ' ') end++; // 包含尾部空格
|
|
664
|
-
return { start: idx, end };
|
|
665
|
-
}
|