openclaw-openagent 1.0.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.
Files changed (113) hide show
  1. package/index.ts +114 -0
  2. package/openclaw.plugin.json +159 -0
  3. package/package.json +79 -0
  4. package/skills/clawlink/SKILL.md +145 -0
  5. package/skills/clawlink/SKILL.md.bak +165 -0
  6. package/src/app/channel-tools.ts +249 -0
  7. package/src/app/discovery-tools.ts +273 -0
  8. package/src/app/hooks.ts +60 -0
  9. package/src/app/index.ts +78 -0
  10. package/src/app/messaging-tools.ts +79 -0
  11. package/src/app/ops-tools.ts +155 -0
  12. package/src/app/remote-agent-tool.ts +476 -0
  13. package/src/app/types.ts +67 -0
  14. package/src/app/verbose-preflight.ts +190 -0
  15. package/src/auth/config.ts +197 -0
  16. package/src/auth/credential-manager.ts +146 -0
  17. package/src/auth/index.ts +24 -0
  18. package/src/auth/verify.ts +99 -0
  19. package/src/channel.ts +565 -0
  20. package/src/compat.ts +82 -0
  21. package/src/config/config-schema.ts +39 -0
  22. package/src/messaging/aggregator.ts +120 -0
  23. package/src/messaging/collector.ts +89 -0
  24. package/src/messaging/executor.ts +72 -0
  25. package/src/messaging/inbound.ts +150 -0
  26. package/src/messaging/index.ts +11 -0
  27. package/src/messaging/mention-protocol.ts +94 -0
  28. package/src/messaging/process-c2c-request.ts +564 -0
  29. package/src/messaging/process-message.ts +373 -0
  30. package/src/messaging/scheduler.ts +55 -0
  31. package/src/messaging/types.ts +38 -0
  32. package/src/plugin-ui/assets/agentbook-icon.svg +5 -0
  33. package/src/plugin-ui/assets/magic.svg +5 -0
  34. package/src/plugin-ui/assets/openagent-override.js +9329 -0
  35. package/src/plugin-ui/build.cjs +175 -0
  36. package/src/plugin-ui/index.ts +18 -0
  37. package/src/plugin-ui/modules/agent-book/panel/agent-book.js +458 -0
  38. package/src/plugin-ui/modules/agent-book/panel/agent-card.js +154 -0
  39. package/src/plugin-ui/modules/agent-book/panel/agent-data.js +644 -0
  40. package/src/plugin-ui/modules/agent-book/panel/inject-ui.js +456 -0
  41. package/src/plugin-ui/modules/agent-book/panel/mention-state.js +206 -0
  42. package/src/plugin-ui/modules/agent-book/panel/styles.js +670 -0
  43. package/src/plugin-ui/modules/agent-book/remote-agent-tool/components-core.js +293 -0
  44. package/src/plugin-ui/modules/agent-book/remote-agent-tool/thought-chain-card.js +208 -0
  45. package/src/plugin-ui/modules/agent-book/scanner.js +119 -0
  46. package/src/plugin-ui/modules/agent-book/travelcard/travel-cards.js +500 -0
  47. package/src/plugin-ui/modules/agent-book/travelcard/travel-engine.js +652 -0
  48. package/src/plugin-ui/modules/agent-book/travelcard/travel-styles.js +251 -0
  49. package/src/plugin-ui/modules/loader/bootstrap.js +38 -0
  50. package/src/plugin-ui/modules/loader/shared-state.js +560 -0
  51. package/src/plugin-ui/modules/loader/ws-intercept.js +199 -0
  52. package/src/plugin-ui/modules/remote-agent/chunk-parser.js +161 -0
  53. package/src/plugin-ui/modules/remote-agent/execution-card.js +269 -0
  54. package/src/plugin-ui/modules/remote-agent/markdown-renderer.js +256 -0
  55. package/src/plugin-ui/modules/remote-agent/native-style-adapter.js +146 -0
  56. package/src/plugin-ui/modules/remote-agent/output-card.js +259 -0
  57. package/src/plugin-ui/modules/remote-agent/progress-store.js +363 -0
  58. package/src/plugin-ui/modules/remote-agent/render-hooks.js +1609 -0
  59. package/src/plugin-ui/modules/remote-agent/styles.js +668 -0
  60. package/src/plugin-ui/modules/remote-agent/tool-card-model.js +56 -0
  61. package/src/plugin-ui/ui-extension-loader/backup.ts +92 -0
  62. package/src/plugin-ui/ui-extension-loader/index.ts +276 -0
  63. package/src/plugin-ui/ui-extension-loader/locator.ts +152 -0
  64. package/src/plugin-ui/ui-extension-loader/manifest.ts +57 -0
  65. package/src/plugin-ui/ui-extension-loader/registry-regex.ts +729 -0
  66. package/src/plugin-ui/ui-extension-loader/removed-extensions.ts +70 -0
  67. package/src/plugin-ui/ui-extension-loader/types.ts +68 -0
  68. package/src/proxy/auth-proxy.ts +356 -0
  69. package/src/runtime/account.ts +572 -0
  70. package/src/runtime/index.ts +7 -0
  71. package/src/runtime/plugin-runtime.ts +94 -0
  72. package/src/runtime/registry.ts +71 -0
  73. package/src/sdk/CLASS_MAP.md +143 -0
  74. package/src/sdk/index.d.ts +126 -0
  75. package/src/sdk/index.js +23990 -0
  76. package/src/sdk/modules/cloud-search-module.js +1117 -0
  77. package/src/sdk/modules/follow-module.js +1069 -0
  78. package/src/sdk/modules/group-module.js +7397 -0
  79. package/src/sdk/modules/relationship-module.js +2269 -0
  80. package/src/sdk/modules/signaling-module.js +1468 -0
  81. package/src/sdk/modules/tim-upload-plugin.js +730 -0
  82. package/src/sdk/node-env/http-request.js +90 -0
  83. package/src/sdk/node-env/index.js +57 -0
  84. package/src/sdk/node-env/storage.js +114 -0
  85. package/src/sdk/package.json +10 -0
  86. package/src/sdk/tsconfig.json +16 -0
  87. package/src/state/pending-invocation-store.ts +43 -0
  88. package/src/state/store.ts +676 -0
  89. package/src/tim/c2c.ts +451 -0
  90. package/src/tim/channels.ts +364 -0
  91. package/src/tim/client.ts +330 -0
  92. package/src/tim/index.ts +18 -0
  93. package/src/tim/messages.ts +166 -0
  94. package/src/tim/sdk-logger-init.ts +50 -0
  95. package/src/tools.ts +10 -0
  96. package/src/transport/factory.ts +95 -0
  97. package/src/transport/oasn/index.ts +17 -0
  98. package/src/transport/oasn/oasn-agent-card.ts +111 -0
  99. package/src/transport/oasn/oasn-discovery.ts +108 -0
  100. package/src/transport/oasn/oasn-files.ts +210 -0
  101. package/src/transport/oasn/oasn-http.ts +483 -0
  102. package/src/transport/oasn/oasn-invocation.ts +527 -0
  103. package/src/transport/oasn/oasn-normalize.ts +159 -0
  104. package/src/transport/oasn/oasn-register.ts +106 -0
  105. package/src/transport/oasn/oasn-transport.ts +341 -0
  106. package/src/transport/oasn/oasn-types.ts +353 -0
  107. package/src/transport/tim/index.ts +8 -0
  108. package/src/transport/tim/tim-transport.ts +515 -0
  109. package/src/transport/types.ts +541 -0
  110. package/src/types/openclaw.d.ts +97 -0
  111. package/src/types/tencentcloud-chat.d.ts +15 -0
  112. package/src/util/http.ts +113 -0
  113. package/src/util/logger.ts +131 -0
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * build.js — 将 modules/ 下的源模块按显式顺序拼接为 openagent-override.js
4
+ *
5
+ * 用法: node src/plugin-ui/build.cjs
6
+ * 输出: src/plugin-ui/assets/openagent-override.js
7
+ *
8
+ * 规则:
9
+ * - 顺序由 MANIFEST 数组决定,新增模块必须手动注册
10
+ * - 缺少任何模块立即报错退出
11
+ * - 产物不入 Git,通过 prepack hook 在 npm pack/publish 时自动生成
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ // ═══════════════════════════════════════════════════════════════
18
+ // MANIFEST — 显式构建顺序,不要用 readdir+sort
19
+ // 新增模块必须在此注册,否则不进产物
20
+ // ═══════════════════════════════════════════════════════════════
21
+ const MANIFEST = [
22
+ // ── loader ──
23
+ 'loader/shared-state.js',
24
+ 'loader/ws-intercept.js',
25
+ // ── agent-book/panel ──
26
+ 'agent-book/panel/agent-data.js',
27
+ 'agent-book/panel/agent-card.js',
28
+ 'agent-book/panel/styles.js',
29
+ 'agent-book/panel/mention-state.js',
30
+ 'agent-book/panel/agent-book.js',
31
+ 'agent-book/panel/inject-ui.js',
32
+ // ── agent-book/remote-agent-tool ──
33
+ 'agent-book/remote-agent-tool/components-core.js',
34
+ 'agent-book/remote-agent-tool/thought-chain-card.js',
35
+ // ── agent-book/travelcard ──
36
+ 'agent-book/travelcard/travel-styles.js',
37
+ 'agent-book/travelcard/travel-cards.js',
38
+ 'agent-book/travelcard/travel-engine.js',
39
+ // ── agent-book (跨模块) ──
40
+ 'agent-book/scanner.js',
41
+ // ── remote-agent(renderToolCardSidebar hook)──
42
+ // 顺序:adapter → styles → parser/model → UI cards → hooks
43
+ 'remote-agent/native-style-adapter.js',
44
+ 'remote-agent/styles.js',
45
+ 'remote-agent/chunk-parser.js',
46
+ 'remote-agent/progress-store.js',
47
+ 'remote-agent/tool-card-model.js',
48
+ 'remote-agent/execution-card.js',
49
+ 'remote-agent/markdown-renderer.js',
50
+ 'remote-agent/output-card.js',
51
+ 'remote-agent/render-hooks.js',
52
+ // ── loader (bootstrap 必须最后) ──
53
+ 'loader/bootstrap.js',
54
+ ];
55
+
56
+ // ═══════════════════════════════════════════════════════════════
57
+
58
+ const modulesDir = path.join(__dirname, 'modules');
59
+ const outFile = path.join(__dirname, 'assets', 'openagent-override.js');
60
+
61
+ const HEADER = `/**
62
+ * OpenAgent UI Override — Auto-generated, do not edit directly!
63
+ *
64
+ * Source: src/plugin-ui/modules/
65
+ * Built: ${new Date().toISOString()}
66
+ *
67
+ * To modify, edit the source modules and run:
68
+ * npm run build:override
69
+ */
70
+
71
+ (() => {
72
+ 'use strict';
73
+ `;
74
+
75
+ const FOOTER = `
76
+ })();
77
+ `;
78
+
79
+ // ── Build Logic ──
80
+
81
+ function build() {
82
+ console.log(`\n[${new Date().toLocaleTimeString()}] Building override...`);
83
+ let body = '';
84
+ let errors = 0;
85
+
86
+ for (const file of MANIFEST) {
87
+ const filePath = path.join(modulesDir, file);
88
+
89
+ if (!fs.existsSync(filePath)) {
90
+ console.error(`❌ Missing module: ${file}`);
91
+ errors++;
92
+ continue;
93
+ }
94
+
95
+ const content = fs.readFileSync(filePath, 'utf-8');
96
+ body += `\n// ════════════════════════════════════════════════════════════════\n`;
97
+ body += `// MODULE: ${file}\n`;
98
+ body += `// ════════════════════════════════════════════════════════════════\n\n`;
99
+ body += content.trimEnd() + '\n';
100
+ }
101
+
102
+ if (errors > 0) {
103
+ console.error(`\n❌ Build failed: ${errors} missing module(s)`);
104
+ if (!isWatchMode) process.exit(1);
105
+ return;
106
+ }
107
+
108
+ const output = HEADER + body + FOOTER;
109
+
110
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
111
+ fs.writeFileSync(outFile, output, 'utf-8');
112
+
113
+ const lineCount = output.split('\n').length;
114
+ console.log(`✅ Built: ${path.relative(process.cwd(), outFile)} (${lineCount} lines)`);
115
+
116
+ // ── Auto-deploy to runtime targets ──────────────────────────────────────
117
+ // 开发时自动复制到插件安装目录和 control-ui,省去手动 cp
118
+ // 任何 target 不存在都静默跳过(CI/CD 环境不影响)
119
+
120
+ const homedir = require('os').homedir();
121
+
122
+ const DEPLOY_TARGETS = [
123
+ // 插件安装目录(Gateway 启动时从这里读取并复制到 control-ui)
124
+ // 注意:安装目录仍使用旧路径 src/inject/,下次 npm publish 后才会变
125
+ path.join(homedir, '.openclaw-docker/extensions/openclaw-openagent/src/inject/assets/openagent-override.js'),
126
+ path.join(homedir, '.openclaw-docker/extensions/openclaw-openagent/src/plugin-ui/assets/openagent-override.js'),
127
+ path.join(homedir, '.openclaw/extensions/openclaw-openagent/src/inject/assets/openagent-override.js'),
128
+ path.join(homedir, '.openclaw/extensions/openclaw-openagent/src/plugin-ui/assets/openagent-override.js'),
129
+ ];
130
+
131
+ // 动态发现 control-ui 目录(通过 node 可执行文件路径推断)
132
+ try {
133
+ const nodeDir = path.dirname(process.execPath);
134
+ const controlUiCandidate = path.join(nodeDir, '..', 'lib', 'node_modules', 'openclaw', 'dist', 'control-ui', 'openagent-override.js');
135
+ DEPLOY_TARGETS.push(path.resolve(controlUiCandidate));
136
+ } catch {}
137
+
138
+ let deployed = 0;
139
+ for (const target of DEPLOY_TARGETS) {
140
+ try {
141
+ if (fs.existsSync(path.dirname(target))) {
142
+ fs.copyFileSync(outFile, target);
143
+ console.log(` → ${path.relative(homedir, target)}`);
144
+ deployed++;
145
+ }
146
+ } catch {}
147
+ }
148
+
149
+ if (deployed > 0) {
150
+ console.log(`✅ Auto-deployed to ${deployed} target(s)`);
151
+ } else {
152
+ console.log('ℹ️ No deploy targets found (standalone build)');
153
+ }
154
+ }
155
+
156
+ // ── Execution ──
157
+
158
+ const isWatchMode = process.argv.includes('--watch');
159
+
160
+ build();
161
+
162
+ if (isWatchMode) {
163
+ console.log(`\n👀 Watching for changes in ${path.relative(process.cwd(), modulesDir)}...`);
164
+
165
+ let debounceTimer;
166
+ fs.watch(modulesDir, { recursive: true }, (eventType, filename) => {
167
+ // Ignore dot files and non-js files
168
+ if (!filename || filename.startsWith('.') || !filename.endsWith('.js')) return;
169
+
170
+ clearTimeout(debounceTimer);
171
+ debounceTimer = setTimeout(() => {
172
+ build();
173
+ }, 100);
174
+ });
175
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Plugin UI — UIExtensionLoader 入口
3
+ *
4
+ * 将 UI 扩展加载委托给 ui-extension-loader 模块。
5
+ * 保持对外 API 不变(injectControlUiOverride),内部全部走新框架。
6
+ */
7
+
8
+ import { loadUIExtensions } from './ui-extension-loader/index.js';
9
+
10
+ /**
11
+ * 加载全部 UI 扩展。
12
+ *
13
+ * 由 index.ts register() 调用。内部通过 Manifest Reconciler
14
+ * 自动完成扩展点的 apply / revert / skip。
15
+ */
16
+ export async function injectControlUiOverride(): Promise<void> {
17
+ await loadUIExtensions();
18
+ }
@@ -0,0 +1,458 @@
1
+
2
+
3
+ // ── UI 构建 ──
4
+
5
+ let panel = null;
6
+ let backdrop = null;
7
+
8
+ let inputWrapperRef = null; // 保存输入框包裹层引用
9
+
10
+ // ── 键盘导航状态(§1.5)──
11
+ // 对标 TIM index.ts:13 "let selectedIndex: number = 0"
12
+ let selectedIndex = 0;
13
+ let _currentAgents = []; // 当前列表中的 agents(供键盘选中使用)
14
+ let _currentTextarea = null; // 当前关联的 textarea
15
+
16
+ function ensurePanel() {
17
+ if (panel) return panel;
18
+
19
+ // 背景遮罩(点击关闭)
20
+ backdrop = document.createElement('div');
21
+ backdrop.className = 'openagent-panel-backdrop';
22
+ backdrop.addEventListener('click', () => {
23
+ console.log('[openagent] 🔲 backdrop clicked → closePanel (dismissed)');
24
+ // backdrop 关闭 = 用户主动关闭 → 触发 dismissed(同 ESC 意图)
25
+ setDismissed();
26
+ closePanel();
27
+ });
28
+ document.body.appendChild(backdrop);
29
+
30
+ // 全局点击诊断(捕获阶段,看最先点到什么)
31
+ document.addEventListener('click', (e) => {
32
+ if (panel && panel.classList.contains('visible')) {
33
+ console.log('[openagent] 🔍 global click capture:', e.target.tagName, e.target.className, 'composedPath[0]:', e.composedPath()[0]?.tagName);
34
+ }
35
+ }, true); // 捕获阶段
36
+
37
+ // 面板 — 独立浮动卡片
38
+ panel = document.createElement('div');
39
+ panel.className = 'openagent-agent-panel';
40
+ panel.innerHTML = `
41
+ <div class="openagent-panel-head">
42
+ <span class="openagent-panel-title">
43
+ <svg class="openagent-panel-title-icon" width="28" height="28" viewBox="0 0 56 56" fill="none">
44
+ <path d="M28 25H32C34.2091 25 36 26.7909 36 29V39C36 41.2091 34.2091 43 32 43H14C11.7909 43 10 41.2091 10 39V29C10 26.7909 11.7909 25 14 25H15" stroke="#12CCF6" stroke-width="3.3" stroke-linecap="round"/>
45
+ <path d="M39 31H42C44.2091 31 46 29.2091 46 27V17C46 14.7909 44.2091 13 42 13H24C21.7909 13 20 14.7909 20 17V27C20 29.2091 21.7909 31 24 31H30.0652C30.236 31 30.3282 31.2004 30.217 31.3302L27.5 34.5" stroke="#12CCF6" stroke-width="3.3" stroke-linecap="round"/>
46
+ <path d="M27 25.5L32.5 21" stroke="#12CCF6" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
47
+ </svg>
48
+ <span class="panel-title-text">Agent Book</span>
49
+ </span>
50
+ <button class="openagent-panel-close">✕</button>
51
+ </div>
52
+ <hr class="openagent-panel-divider" />
53
+ <div class="openagent-agent-list">
54
+ <div class="openagent-loading">加载中...</div>
55
+ </div>
56
+ `;
57
+ panel.querySelector('.openagent-panel-close').addEventListener('click', () => {
58
+ // ✕ 按钮关闭 = 用户主动关闭 → 触发 dismissed(同 ESC 意图)
59
+ setDismissed();
60
+ closePanel();
61
+ });
62
+
63
+ // 挂到 body(与 backdrop 同级,z-index 自然生效)
64
+ document.body.appendChild(panel);
65
+ return panel;
66
+ }
67
+
68
+ /** 根据 inputWrapper 位置动态定位 panel */
69
+ function _positionPanel() {
70
+ if (!panel || !inputWrapperRef) return;
71
+ const rect = inputWrapperRef.getBoundingClientRect();
72
+ const viewportPadding = 16;
73
+ const width = Math.min(rect.width, window.innerWidth - viewportPadding * 2);
74
+ const left = Math.max(viewportPadding, Math.min(rect.left, window.innerWidth - width - viewportPadding));
75
+ panel.style.left = left + 'px';
76
+ panel.style.width = width + 'px';
77
+ panel.style.bottom = (window.innerHeight - rect.top + 8) + 'px';
78
+ }
79
+
80
+ /** resize 时实时跟随输入框 */
81
+ function _onResize() { _positionPanel(); }
82
+
83
+ // 搜索状态管理
84
+ let currentAbort = null; // AbortController for in-flight search
85
+ let searchDebounceId = null;
86
+
87
+ // ── @mention → systemProvenanceReceipt 状态 ──
88
+ // 当用户从 Agent Book 选中一个 agent 时,保存其 metadata。
89
+ // 下次 chat.send 时作为 systemProvenanceReceipt 注入,发送后清空。
90
+
91
+ /**
92
+ * 提取输入框文本作为搜索词(对齐 openagent-frontend NewChatPanel L82)
93
+ * 去掉所有 @ 符号,合并空白
94
+ */
95
+ function extractSearchQuery(textarea) {
96
+ if (!textarea) return '';
97
+ return textarea.value.replace(/@/g, ' ').replace(/\s+/g, ' ').trim();
98
+ }
99
+
100
+ function openPanel(textarea) {
101
+ const p = ensurePanel();
102
+ _positionPanel(); // 动态定位到 input 上方
103
+ p.classList.add('visible');
104
+ backdrop.classList.add('visible');
105
+ window.addEventListener('resize', _onResize);
106
+
107
+ const list = p.querySelector('.openagent-agent-list');
108
+ list.innerHTML = '<div class="openagent-loading">加载中...</div>';
109
+
110
+ // 提取当前输入文本作为搜索词
111
+ const query = extractSearchQuery(textarea);
112
+ console.log('[openagent] openPanel with search query:', JSON.stringify(query));
113
+
114
+ // 动态生成面板标题 tagline
115
+ _updatePanelTagline(query);
116
+
117
+ if (query) {
118
+ // 有输入文本 → 语义搜索
119
+ searchAgents(query).then(agents => {
120
+ if (!p.classList.contains('visible')) return;
121
+ renderAgentList(list, agents, textarea, query);
122
+ });
123
+ } else {
124
+ // 空输入 → 全量列表
125
+ fetchAgentSections().then(agents => {
126
+ if (!p.classList.contains('visible')) return;
127
+ renderAgentList(list, agents, textarea, query);
128
+ });
129
+ }
130
+ }
131
+
132
+ /** 更新面板标题:优先 API,降级到客户端生成 */
133
+ let _authSigCache = null; // { userSig, expiresAt }
134
+
135
+ async function _getAuthSig() {
136
+ if (_authSigCache && Date.now() < _authSigCache.expiresAt) {
137
+ return _authSigCache.userSig;
138
+ }
139
+ try {
140
+ const res = await fetch('./openagent-auth.json?t=' + Date.now());
141
+ if (!res.ok) return null;
142
+ const data = await res.json();
143
+ if (data?.userSig) {
144
+ _authSigCache = data;
145
+ return data.userSig;
146
+ }
147
+ } catch { }
148
+ return null;
149
+ }
150
+
151
+ function _updatePanelTagline(query) {
152
+ if (!panel) return;
153
+ const titleEl = panel.querySelector('.panel-title-text');
154
+ if (!titleEl) return;
155
+
156
+ // 客户端即时文案(无延迟)
157
+ const localTagline = query
158
+ ? `匹配${query.length > 12 ? query.slice(0, 12) + '…' : query}AGENT`
159
+ : '在线AGENT | 2.4M';
160
+ titleEl.textContent = localTagline;
161
+ console.log('[openagent] tagline: query=', JSON.stringify(query), 'local=', localTagline);
162
+
163
+ // 异步获取鉴权 + 调用 tagline API
164
+ _getAuthSig().then(sig => {
165
+ console.log('[openagent] tagline: authSig=', sig ? `${sig.slice(0,20)}...` : 'null');
166
+ if (!sig) return; // 无鉴权,保持客户端文案
167
+ return fetch(TAGLINE_API, {
168
+ method: 'POST',
169
+ headers: {
170
+ 'Content-Type': 'application/json',
171
+ 'Authorization': `Bearer ${sig}`,
172
+ },
173
+ body: JSON.stringify({ type: 'tagline', query: query || '' }),
174
+ });
175
+ })
176
+ .then(r => {
177
+ console.log('[openagent] tagline: response status=', r?.status);
178
+ return r?.ok ? r.json() : null;
179
+ })
180
+ .then(data => {
181
+ console.log('[openagent] tagline: API result=', JSON.stringify(data));
182
+ // 标题固定为 Agent Book,不再被 API 覆盖
183
+ })
184
+ .catch(err => { console.warn('[openagent] tagline error:', err); });
185
+ }
186
+
187
+ /**
188
+ * 实时搜索更新(debounce 300ms,对齐 MentionPickerPopup)
189
+ * 当面板打开时,textarea 每次输入都会触发
190
+ */
191
+ function updatePanelSearch(textarea) {
192
+ if (!panel || !panel.classList.contains('visible')) return;
193
+
194
+ // 取消前一个请求
195
+ if (currentAbort) currentAbort.abort();
196
+ if (searchDebounceId) clearTimeout(searchDebounceId);
197
+
198
+ const query = extractSearchQuery(textarea);
199
+ const list = panel.querySelector('.openagent-agent-list');
200
+
201
+ searchDebounceId = setTimeout(() => {
202
+ currentAbort = new AbortController();
203
+ const signal = currentAbort.signal;
204
+
205
+ list.innerHTML = '<div class="openagent-loading">搜索中...</div>';
206
+
207
+ // 同步更新面板标题
208
+ _updatePanelTagline(query);
209
+
210
+ const doSearch = query
211
+ ? searchAgents(query, signal)
212
+ : fetchAgentSections();
213
+
214
+ doSearch.then(agents => {
215
+ if (signal.aborted) return;
216
+ if (!panel.classList.contains('visible')) return;
217
+ renderAgentList(list, agents, textarea, query);
218
+ });
219
+ }, 300);
220
+ }
221
+
222
+ function closePanel() {
223
+ if (panel) panel.classList.remove('visible');
224
+ if (backdrop) backdrop.classList.remove('visible');
225
+ window.removeEventListener('resize', _onResize);
226
+ // 清理搜索状态
227
+ if (currentAbort) { currentAbort.abort(); currentAbort = null; }
228
+ if (searchDebounceId) { clearTimeout(searchDebounceId); searchDebounceId = null; }
229
+ // 同步状态机(如果 dismissed 已由 setDismissed() 设置则不覆盖)
230
+ closeMention();
231
+ }
232
+
233
+ // ── 键盘导航函数(§1.5)──
234
+ // 对标 TIM index.ts:158-173 upHandler/downHandler/enterHandler
235
+
236
+ function moveSelectionUp() {
237
+ const len = _currentAgents.length;
238
+ if (!len) return;
239
+ selectedIndex = (selectedIndex + len - 1) % len;
240
+ _highlightItem(selectedIndex);
241
+ _scrollItemIntoView(selectedIndex);
242
+ }
243
+
244
+ function moveSelectionDown() {
245
+ const len = _currentAgents.length;
246
+ if (!len) return;
247
+ selectedIndex = (selectedIndex + 1) % len;
248
+ _highlightItem(selectedIndex);
249
+ _scrollItemIntoView(selectedIndex);
250
+ }
251
+
252
+ function selectHighlightedAgent(textarea) {
253
+ const agent = _currentAgents[selectedIndex];
254
+ const ta = textarea || _currentTextarea;
255
+ if (agent && ta) {
256
+ insertMention(ta, agent.name, agent);
257
+ resetMention(); // 选中 Agent = 正常完成,清空一切(不触发 dismissed)
258
+ closePanel();
259
+ }
260
+ }
261
+
262
+ function _highlightItem(index) {
263
+ if (!panel) return;
264
+ const cards = panel.querySelectorAll('.openagent-agent-list .agent-card');
265
+ cards.forEach((c, i) => {
266
+ c.classList.toggle('openagent-card-selected', i === index);
267
+ });
268
+ }
269
+
270
+ function _scrollItemIntoView(index) {
271
+ if (!panel) return;
272
+ const cards = panel.querySelectorAll('.openagent-agent-list .agent-card');
273
+ if (cards[index]) {
274
+ cards[index].scrollIntoView({ block: 'nearest', behavior: 'smooth' });
275
+ }
276
+ }
277
+
278
+ function _appendParentSection(container, parentTitle, children, textarea, renderedAgents) {
279
+ const parentSec = _el('section', 'openagent-parent-section');
280
+ parentSec.appendChild(_text('div', 'openagent-parent-heading', parentTitle));
281
+ children.forEach(function(child) {
282
+ if (child.agents && child.agents.length > 0) {
283
+ _appendAgentSection(parentSec, child.title, child.agents, textarea, renderedAgents);
284
+ }
285
+ });
286
+ container.appendChild(parentSec);
287
+ }
288
+
289
+ function _appendAgentSection(container, title, agents, textarea, renderedAgents) {
290
+ if (!agents.length) return;
291
+ const section = _el('section', 'openagent-agent-section');
292
+ section.appendChild(_text('div', 'openagent-section-heading', title));
293
+
294
+ const grid = _el('div', 'openagent-section-grid');
295
+ agents.forEach((a) => {
296
+ const displayIndex = renderedAgents.length;
297
+ const card = AgentCard(a, {
298
+ mode: 'mention',
299
+ onAction: () => {
300
+ console.log('[openagent] onAction fired for:', a.name);
301
+ insertMention(textarea, a.name, a);
302
+ resetMention(); // 点击选中 = 正常完成,清空一切
303
+ closePanel();
304
+ }
305
+ });
306
+ card.dataset.tag = a.tag || '';
307
+ card.dataset.name = a.name || '';
308
+ card.dataset.index = displayIndex;
309
+ grid.appendChild(card);
310
+ renderedAgents.push(a);
311
+
312
+ card.addEventListener('mousedown', () => console.log('[openagent] 🖱 mousedown on card:', a.name));
313
+ const tryBtn = card.querySelector('.agent-card__action');
314
+ if (tryBtn) {
315
+ tryBtn.addEventListener('mousedown', (e) => {
316
+ console.log('[openagent] 🖱 mousedown on Try btn:', a.name, 'target:', e.target.tagName, e.target.className);
317
+ });
318
+ }
319
+ });
320
+ section.appendChild(grid);
321
+ container.appendChild(section);
322
+ }
323
+
324
+ function _appendSearchResults(container, agents, textarea, renderedAgents) {
325
+ const list = _el('div', 'openagent-search-results');
326
+ agents.forEach((a) => {
327
+ const displayIndex = renderedAgents.length;
328
+ const card = AgentCard(a, {
329
+ mode: 'search',
330
+ onAction: () => {
331
+ insertMention(textarea, a.name, a);
332
+ resetMention();
333
+ closePanel();
334
+ }
335
+ });
336
+ card.dataset.tag = a.tag || '';
337
+ card.dataset.name = a.name || '';
338
+ card.dataset.index = displayIndex;
339
+ list.appendChild(card);
340
+ renderedAgents.push(a);
341
+ });
342
+ container.appendChild(list);
343
+ }
344
+
345
+ function renderAgentList(container, agents, textarea, query) {
346
+ if (!agents.length) {
347
+ const message = CL.agentBookLastError || '暂无在线 Agent';
348
+ container.innerHTML = '';
349
+ container.appendChild(_text('div', 'openagent-loading', message));
350
+ _currentAgents = [];
351
+ _currentTextarea = textarea;
352
+ return;
353
+ }
354
+
355
+ console.log('[openagent] renderAgentList:', agents.length, 'agents, textarea:', !!textarea);
356
+
357
+ container.innerHTML = '';
358
+ _currentTextarea = textarea;
359
+
360
+ const renderedAgents = [];
361
+ const normalizedQuery = (query || '').trim();
362
+ if (normalizedQuery) {
363
+ _appendSearchResults(container, agents, textarea, renderedAgents);
364
+ } else {
365
+ agents.forEach(section => {
366
+ if (section.children && section.children.length > 0) {
367
+ _appendParentSection(container, section.title, section.children, textarea, renderedAgents);
368
+ } else {
369
+ _appendAgentSection(container, section.title, section.agents || [], textarea, renderedAgents);
370
+ }
371
+ });
372
+ }
373
+
374
+ // 保存 DOM 顺序中的 agents 列表,供键盘导航使用
375
+ _currentAgents = renderedAgents;
376
+
377
+ // 默认高亮第一项(§1.5 默认选中)
378
+ // 对标 TIM index.ts:13 "selectedIndex = 0" + index.vue:32 ":class=[index===selectedIndex&&'selected']"
379
+ selectedIndex = 0;
380
+ _highlightItem(0);
381
+ }
382
+
383
+ /**
384
+ * 选中 Agent 后插入 @name 到 textarea
385
+ * 对齐 openagent-frontend NewChatPanel L92-105:
386
+ * 找到最后一个 @,替换其后内容为 @agentName
387
+ */
388
+ function insertMention(textarea, agentName, agentData) {
389
+ // 重新查找 textarea(Lit 可能重新渲染导致引用失效)
390
+ const liveTextarea = document.querySelector(
391
+ 'textarea[placeholder*="Message"]'
392
+ ) || document.querySelector(
393
+ 'textarea[placeholder*="Enter to send"]'
394
+ ) || textarea;
395
+
396
+ console.log('[openagent] insertMention:', agentName,
397
+ 'textarea found:', !!liveTextarea,
398
+ 'same ref:', liveTextarea === textarea);
399
+
400
+ if (!liveTextarea) return;
401
+ liveTextarea.focus();
402
+
403
+ // 存储 agent 信息,发送时替换为 {openagent:...} 标记
404
+ const agentId = agentData?.agent_id || agentData?.agentId || agentData?.id || agentData?.tag ||
405
+ agentData?.user_id || agentData?.userId || agentData?.identifier || agentData?.tim_user_id || agentData?.timUserId || '';
406
+ const agentAvatar = agentData?.avatar || agentData?.avatar_url || agentData?.avatarUrl || agentData?.image || agentData?.logo || agentData?.photo || null;
407
+ console.log('[cl-diag][mention-select]', {
408
+ agentName,
409
+ resolvedAgentId: agentId || null,
410
+ rawKeys: Object.keys(agentData || {}),
411
+ rawAgent: agentData,
412
+ });
413
+ if (!agentId) {
414
+ CL.pendingMention = null;
415
+ console.warn('[openagent] mention skipped: missing agent identifier for', agentName, agentData);
416
+ return;
417
+ }
418
+ CL.pendingMention = { agent_id: agentId, name: agentName, avatar: agentAvatar };
419
+ if (CL && typeof CL.storeAgentIdentity === 'function') {
420
+ CL.storeAgentIdentity({ agent_id: agentId, name: agentName, avatar: agentAvatar });
421
+ }
422
+ console.log('[openagent] mention stored:', CL.pendingMention);
423
+
424
+ // textarea 显示 @agentName(用户友好)
425
+ const val = liveTextarea.value;
426
+ const lastAtIndex = val.lastIndexOf('@');
427
+ let newValue;
428
+ if (lastAtIndex !== -1) {
429
+ const before = val.slice(0, lastAtIndex).trimEnd();
430
+ newValue = before ? `${before} @${agentName} ` : `@${agentName} `;
431
+ } else {
432
+ newValue = `${val}@${agentName} `;
433
+ }
434
+
435
+ // 设置值
436
+ const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
437
+ HTMLTextAreaElement.prototype, 'value'
438
+ )?.set;
439
+ if (nativeInputValueSetter) {
440
+ nativeInputValueSetter.call(liveTextarea, newValue);
441
+ } else {
442
+ liveTextarea.value = newValue;
443
+ }
444
+
445
+ // 光标到末尾
446
+ liveTextarea.selectionStart = liveTextarea.selectionEnd = newValue.length;
447
+
448
+ // 触发事件让 Lit 识别变化
449
+ liveTextarea.dispatchEvent(new Event('input', { bubbles: true }));
450
+ liveTextarea.dispatchEvent(new Event('change', { bubbles: true }));
451
+
452
+ // 标记替换由 window.__clMention.onKeyDown 在 OC 注入钩子中处理
453
+ // 不再需要独立的 _installSendInterceptor
454
+ }
455
+
456
+ // _installSendInterceptor 已移除(v3)
457
+ // 标记替换逻辑迁移到 inject-ui.js → window.__clMention.onKeyDown
458
+ // 在 OC 的 handleKeyDown 钩子中执行,优先级在 slash menu 之后、Enter 发送之前