ideaco 1.1.5

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 (159) hide show
  1. package/.dockerignore +33 -0
  2. package/.nvmrc +1 -0
  3. package/ARCHITECTURE.md +394 -0
  4. package/Dockerfile +50 -0
  5. package/LICENSE +29 -0
  6. package/README.md +206 -0
  7. package/bin/i18n.js +46 -0
  8. package/bin/ideaco.js +494 -0
  9. package/deploy.sh +15 -0
  10. package/docker-compose.yml +30 -0
  11. package/electron/main.cjs +986 -0
  12. package/electron/preload.cjs +14 -0
  13. package/electron/web-backends.cjs +854 -0
  14. package/jsconfig.json +8 -0
  15. package/next.config.mjs +34 -0
  16. package/package.json +134 -0
  17. package/postcss.config.mjs +6 -0
  18. package/public/demo/dashboard.png +0 -0
  19. package/public/demo/employee.png +0 -0
  20. package/public/demo/messages.png +0 -0
  21. package/public/demo/office.png +0 -0
  22. package/public/demo/requirement.png +0 -0
  23. package/public/logo.jpeg +0 -0
  24. package/public/logo.png +0 -0
  25. package/scripts/prepare-electron.js +67 -0
  26. package/scripts/release.js +76 -0
  27. package/src/app/api/agents/[agentId]/chat/route.js +70 -0
  28. package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
  29. package/src/app/api/agents/[agentId]/route.js +106 -0
  30. package/src/app/api/avatar/route.js +104 -0
  31. package/src/app/api/browse-dir/route.js +44 -0
  32. package/src/app/api/chat/route.js +265 -0
  33. package/src/app/api/company/factory-reset/route.js +43 -0
  34. package/src/app/api/company/route.js +82 -0
  35. package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
  36. package/src/app/api/departments/route.js +92 -0
  37. package/src/app/api/group-chat-loop/events/route.js +70 -0
  38. package/src/app/api/group-chat-loop/route.js +94 -0
  39. package/src/app/api/mailbox/route.js +100 -0
  40. package/src/app/api/messages/route.js +14 -0
  41. package/src/app/api/providers/[id]/configure/route.js +21 -0
  42. package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
  43. package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
  44. package/src/app/api/providers/route.js +11 -0
  45. package/src/app/api/requirements/route.js +242 -0
  46. package/src/app/api/secretary/route.js +65 -0
  47. package/src/app/api/system/cli-backends/route.js +91 -0
  48. package/src/app/api/system/cron/route.js +110 -0
  49. package/src/app/api/system/knowledge/route.js +104 -0
  50. package/src/app/api/system/plugins/route.js +40 -0
  51. package/src/app/api/system/skills/route.js +46 -0
  52. package/src/app/api/system/status/route.js +46 -0
  53. package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
  54. package/src/app/api/talent-market/[profileId]/route.js +17 -0
  55. package/src/app/api/talent-market/route.js +26 -0
  56. package/src/app/api/teams/route.js +773 -0
  57. package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
  58. package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
  59. package/src/app/globals.css +130 -0
  60. package/src/app/layout.jsx +40 -0
  61. package/src/app/page.jsx +97 -0
  62. package/src/components/AgentChatModal.jsx +164 -0
  63. package/src/components/AgentDetailModal.jsx +425 -0
  64. package/src/components/AgentSpyModal.jsx +481 -0
  65. package/src/components/AvatarGrid.jsx +29 -0
  66. package/src/components/BossProfileModal.jsx +162 -0
  67. package/src/components/CachedAvatar.jsx +77 -0
  68. package/src/components/ChatPanel.jsx +219 -0
  69. package/src/components/ChatShared.jsx +255 -0
  70. package/src/components/DepartmentDetail.jsx +842 -0
  71. package/src/components/DepartmentView.jsx +367 -0
  72. package/src/components/FileReference.jsx +260 -0
  73. package/src/components/FilesView.jsx +465 -0
  74. package/src/components/GroupChatView.jsx +799 -0
  75. package/src/components/Mailbox.jsx +926 -0
  76. package/src/components/MessagesView.jsx +112 -0
  77. package/src/components/OnboardingGuide.jsx +209 -0
  78. package/src/components/OrgTree.jsx +151 -0
  79. package/src/components/Overview.jsx +391 -0
  80. package/src/components/PixelOffice.jsx +2281 -0
  81. package/src/components/ProviderGrid.jsx +551 -0
  82. package/src/components/ProvidersBoard.jsx +16 -0
  83. package/src/components/RequirementDetail.jsx +1279 -0
  84. package/src/components/RequirementsBoard.jsx +187 -0
  85. package/src/components/SecretarySettings.jsx +295 -0
  86. package/src/components/SetupWizard.jsx +388 -0
  87. package/src/components/Sidebar.jsx +169 -0
  88. package/src/components/SystemMonitor.jsx +808 -0
  89. package/src/components/TalentMarket.jsx +183 -0
  90. package/src/components/TeamDetail.jsx +697 -0
  91. package/src/core/agent/base-agent.js +104 -0
  92. package/src/core/agent/chat-store.js +602 -0
  93. package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
  94. package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
  95. package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
  96. package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
  97. package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
  98. package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
  99. package/src/core/agent/cli-agent/backends/index.js +27 -0
  100. package/src/core/agent/cli-agent/backends/registry.js +580 -0
  101. package/src/core/agent/cli-agent/index.js +154 -0
  102. package/src/core/agent/index.js +60 -0
  103. package/src/core/agent/llm-agent/client.js +320 -0
  104. package/src/core/agent/llm-agent/index.js +97 -0
  105. package/src/core/agent/message-bus.js +211 -0
  106. package/src/core/agent/session.js +608 -0
  107. package/src/core/agent/tools.js +596 -0
  108. package/src/core/agent/web-agent/backends/base-backend.js +180 -0
  109. package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
  110. package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
  111. package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
  112. package/src/core/agent/web-agent/backends/index.js +91 -0
  113. package/src/core/agent/web-agent/index.js +278 -0
  114. package/src/core/agent/web-agent/web-client.js +407 -0
  115. package/src/core/employee/base-employee.js +1088 -0
  116. package/src/core/employee/index.js +35 -0
  117. package/src/core/employee/knowledge.js +327 -0
  118. package/src/core/employee/lifecycle.js +990 -0
  119. package/src/core/employee/memory/index.js +642 -0
  120. package/src/core/employee/memory/store.js +143 -0
  121. package/src/core/employee/performance.js +224 -0
  122. package/src/core/employee/secretary.js +625 -0
  123. package/src/core/employee/skills.js +398 -0
  124. package/src/core/index.js +38 -0
  125. package/src/core/organization/company.js +2600 -0
  126. package/src/core/organization/department.js +737 -0
  127. package/src/core/organization/group-chat-loop.js +264 -0
  128. package/src/core/organization/index.js +8 -0
  129. package/src/core/organization/persistence.js +111 -0
  130. package/src/core/organization/team.js +267 -0
  131. package/src/core/organization/workforce/hr.js +377 -0
  132. package/src/core/organization/workforce/providers.js +468 -0
  133. package/src/core/organization/workforce/role-archetypes.js +805 -0
  134. package/src/core/organization/workforce/talent-market.js +205 -0
  135. package/src/core/prompts.js +532 -0
  136. package/src/core/requirement.js +1789 -0
  137. package/src/core/system/audit.js +483 -0
  138. package/src/core/system/cron.js +449 -0
  139. package/src/core/system/index.js +7 -0
  140. package/src/core/system/plugin.js +2183 -0
  141. package/src/core/utils/json-parse.js +188 -0
  142. package/src/core/workspace.js +239 -0
  143. package/src/lib/api-i18n.js +211 -0
  144. package/src/lib/avatar.js +268 -0
  145. package/src/lib/client-store.js +1025 -0
  146. package/src/lib/config-validator.js +483 -0
  147. package/src/lib/format-time.js +22 -0
  148. package/src/lib/hooks.js +414 -0
  149. package/src/lib/i18n.js +134 -0
  150. package/src/lib/paths.js +23 -0
  151. package/src/lib/store.js +72 -0
  152. package/src/locales/de.js +393 -0
  153. package/src/locales/en.js +1054 -0
  154. package/src/locales/es.js +393 -0
  155. package/src/locales/fr.js +393 -0
  156. package/src/locales/ja.js +501 -0
  157. package/src/locales/ko.js +513 -0
  158. package/src/locales/zh.js +828 -0
  159. package/tailwind.config.mjs +11 -0
@@ -0,0 +1,303 @@
1
+ /**
2
+ * ChatGPT DOM 脚本 — 在浏览器页面中执行的 DOM 操作脚本
3
+ *
4
+ * 这些函数生成纯 JS 字符串,通过 webContents.executeJavaScript() 注入到 ChatGPT 页面中执行。
5
+ * 所有脚本必须是自包含的(不能引用外部变量),因为它们在渲染进程的沙盒中运行。
6
+ *
7
+ * 从 electron/main.cjs 中抽出,便于维护和扩展。
8
+ */
9
+ import { getSelectors } from './config.js';
10
+
11
+ /**
12
+ * 构建"发送消息"的 DOM 脚本
13
+ * 在 ChatGPT 页面中找到输入框,输入消息,然后点击发送按钮。
14
+ *
15
+ * @param {string} message - 要发送的消息
16
+ * @returns {string} 可执行的 JS 脚本字符串,返回 { ok: true } 或 { error: string }
17
+ */
18
+ export function buildSendMessageScript(message) {
19
+ const escaped = JSON.stringify(message);
20
+ const inputSels = JSON.stringify(getSelectors('input'));
21
+ const sendSels = JSON.stringify(getSelectors('send'));
22
+ return `
23
+ (async () => {
24
+ try {
25
+ // --- Step 1: 查找输入框 ---
26
+ const inputSelectors = ${inputSels};
27
+ let inputEl = null;
28
+ for (const sel of inputSelectors) {
29
+ const el = document.querySelector(sel);
30
+ if (el && el.offsetParent !== null) { inputEl = el; break; }
31
+ }
32
+ if (!inputEl) {
33
+ return { error: "Cannot find input element" };
34
+ }
35
+
36
+ // --- Step 2: 清除已有内容并输入消息 ---
37
+ inputEl.focus();
38
+ const msg = ${escaped};
39
+
40
+ if (inputEl.tagName === "TEXTAREA" || inputEl.tagName === "INPUT") {
41
+ const nativeSetter = Object.getOwnPropertyDescriptor(
42
+ window.HTMLTextAreaElement.prototype, 'value'
43
+ )?.set || Object.getOwnPropertyDescriptor(
44
+ window.HTMLInputElement.prototype, 'value'
45
+ )?.set;
46
+ if (nativeSetter) {
47
+ nativeSetter.call(inputEl, msg);
48
+ } else {
49
+ inputEl.value = msg;
50
+ }
51
+ inputEl.dispatchEvent(new Event("input", { bubbles: true }));
52
+ } else {
53
+ inputEl.innerHTML = "<p>" + msg.replace(/\\n/g, "</p><p>") + "</p>";
54
+ inputEl.dispatchEvent(new Event("input", { bubbles: true }));
55
+ }
56
+
57
+ await new Promise(r => setTimeout(r, 300));
58
+
59
+ // --- Step 3: 查找并点击发送按钮 ---
60
+ const sendSelectors = ${sendSels};
61
+ let sendBtn = null;
62
+ for (const sel of sendSelectors) {
63
+ const btn = document.querySelector(sel);
64
+ if (btn && !btn.disabled && btn.offsetParent !== null) { sendBtn = btn; break; }
65
+ }
66
+ if (!sendBtn) {
67
+ return { error: "Cannot find send button" };
68
+ }
69
+
70
+ sendBtn.click();
71
+ return { ok: true };
72
+ } catch (err) {
73
+ return { error: err.message || String(err) };
74
+ }
75
+ })()
76
+ `;
77
+ }
78
+
79
+ /**
80
+ * 构建"读取回复"的 DOM 脚本
81
+ * 轮询 ChatGPT 页面,读取最新的 assistant 回复内容。
82
+ *
83
+ * @returns {string} 可执行的 JS 脚本字符串,返回 { text, isStreaming, matchedSelector, matchedStopSelector, elementCount }
84
+ */
85
+ export function buildReadResponseScript() {
86
+ const responseSels = JSON.stringify(getSelectors('response'));
87
+ const stopSels = JSON.stringify(getSelectors('stop'));
88
+ return `
89
+ (() => {
90
+ const clean = (t) => t.replace(/[\\u200B-\\u200D\\uFEFF]/g, "").trim();
91
+
92
+ const selectors = ${responseSels};
93
+ let allEls = [];
94
+ let matchedSelector = null;
95
+ for (const sel of selectors) {
96
+ const els = document.querySelectorAll(sel);
97
+ if (els.length > 0) { allEls = Array.from(els); matchedSelector = sel; break; }
98
+ }
99
+
100
+ // 总是使用最后一个匹配的元素(最新的回复)
101
+ const last = allEls.length > 0 ? allEls[allEls.length - 1] : null;
102
+ let text = "";
103
+ if (last) {
104
+ const assistantInner = last.querySelector('[data-message-author-role="assistant"]');
105
+ const target = assistantInner || last;
106
+ const mdContainer = target.querySelector('[class*="markdown"]') || target.querySelector('.prose') || target;
107
+ text = clean(mdContainer.innerText || mdContainer.textContent || "");
108
+ }
109
+
110
+ // 动态回退 1:查找任何 assistant-like 内容
111
+ if (!text) {
112
+ const assistantEls = document.querySelectorAll('[data-message-author-role="assistant"]');
113
+ if (assistantEls.length > 0) {
114
+ const lastAssistant = assistantEls[assistantEls.length - 1];
115
+ const md = lastAssistant.querySelector('[class*="markdown"]') || lastAssistant.querySelector('.prose') || lastAssistant;
116
+ text = clean(md.innerText || md.textContent || "");
117
+ matchedSelector = '[data-message-author-role="assistant"] (dynamic fallback)';
118
+ allEls = Array.from(assistantEls);
119
+ }
120
+ }
121
+
122
+ // 动态回退 2:查找 article 元素
123
+ if (!text) {
124
+ const articles = document.querySelectorAll('article');
125
+ if (articles.length > 0) {
126
+ const lastArticle = articles[articles.length - 1];
127
+ const md = lastArticle.querySelector('[class*="markdown"]') || lastArticle.querySelector('.prose') || lastArticle;
128
+ const candidate = clean(md.innerText || md.textContent || "");
129
+ if (candidate.length > 5) {
130
+ text = candidate;
131
+ matchedSelector = 'article (dynamic fallback)';
132
+ allEls = Array.from(articles);
133
+ }
134
+ }
135
+ }
136
+
137
+ // 动态回退 3:暴力扫描 main 区域
138
+ if (!text) {
139
+ const main = document.querySelector('main') || document.querySelector('[role="main"]');
140
+ if (main) {
141
+ const walker = document.createTreeWalker(main, NodeFilter.SHOW_ELEMENT, {
142
+ acceptNode: (node) => {
143
+ if (node.closest('nav, header, footer, textarea, [contenteditable], input, button')) return NodeFilter.FILTER_REJECT;
144
+ const t = clean(node.innerText || '');
145
+ if (t.length > 30) return NodeFilter.FILTER_ACCEPT;
146
+ return NodeFilter.FILTER_SKIP;
147
+ }
148
+ });
149
+ let lastNode = null;
150
+ while (walker.nextNode()) { lastNode = walker.currentNode; }
151
+ if (lastNode) {
152
+ const candidate = clean(lastNode.innerText || '');
153
+ if (candidate.length > 30) {
154
+ text = candidate;
155
+ matchedSelector = 'TreeWalker brute-force (main)';
156
+ allEls = [lastNode];
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ // 停止按钮检测(判断是否还在流式输出)
163
+ const stopSelectors = ${stopSels};
164
+ let isStreaming = false;
165
+ let matchedStopSelector = null;
166
+ for (const sel of stopSelectors) {
167
+ const stopEl = document.querySelector(sel);
168
+ if (stopEl && stopEl.offsetParent !== null) { isStreaming = true; matchedStopSelector = sel; break; }
169
+ }
170
+
171
+ return { text, isStreaming, matchedSelector, matchedStopSelector, elementCount: allEls.length };
172
+ })()
173
+ `;
174
+ }
175
+
176
+ /**
177
+ * 构建"新建对话"的 DOM 脚本
178
+ * 点击 ChatGPT 的"新建对话"按钮,或直接导航到首页。
179
+ *
180
+ * @returns {string} 可执行的 JS 脚本字符串,返回 { ok: true }
181
+ */
182
+ export function buildNewChatScript() {
183
+ const newChatSels = JSON.stringify(getSelectors('newChat'));
184
+ return `
185
+ (() => {
186
+ const selectors = ${newChatSels};
187
+ for (const sel of selectors) {
188
+ const el = document.querySelector(sel);
189
+ if (el) { el.click(); return { ok: true }; }
190
+ }
191
+ window.location.href = 'https://chatgpt.com/';
192
+ return { ok: true, navigated: true };
193
+ })()
194
+ `;
195
+ }
196
+
197
+ /**
198
+ * 构建 DOM 诊断脚本(调试用)
199
+ * 当选择器匹配失败时,收集页面 DOM 结构信息帮助排查。
200
+ *
201
+ * @returns {string} 可执行的 JS 脚本字符串
202
+ */
203
+ export function buildDiagnosticScript() {
204
+ return `
205
+ (() => {
206
+ const url = location.href;
207
+ const title = document.title;
208
+ const bodyLen = document.body ? document.body.innerHTML.length : 0;
209
+ const hasDialog = !!document.querySelector('dialog, [role="dialog"], [role="alertdialog"]');
210
+ const hasCaptcha = !!document.querySelector('[class*="captcha"], [id*="captcha"], iframe[src*="captcha"]');
211
+ const hasLogin = !!document.querySelector('[data-testid="login-button"], [class*="login"], a[href*="/auth/login"]');
212
+ const probe = [];
213
+ const candidates = ['article', '[data-message-author-role]', '[class*="markdown"]', '.prose',
214
+ '[data-testid]', 'main', '[role="main"]', '[class*="conversation"]', '[class*="message"]',
215
+ '[class*="response"]', '[class*="chat"]', '[class*="turn"]', '[class*="agent"]'];
216
+ for (const sel of candidates) {
217
+ const count = document.querySelectorAll(sel).length;
218
+ if (count > 0) probe.push(sel + ':' + count);
219
+ }
220
+ const mainEl = document.querySelector('main') || document.querySelector('[role="main"]');
221
+ let mainChildren = '';
222
+ if (mainEl) {
223
+ mainChildren = Array.from(mainEl.children).slice(0, 5).map(c =>
224
+ c.tagName.toLowerCase() + (c.className ? '.' + String(c.className).split(' ').slice(0,3).join('.') : '') + (c.id ? '#' + c.id : '')
225
+ ).join(', ');
226
+ }
227
+ return { url, title, bodyLen, hasDialog, hasCaptcha, hasLogin, probe, mainChildren };
228
+ })()
229
+ `;
230
+ }
231
+
232
+ /**
233
+ * 构建最后手段的文本提取脚本
234
+ * 当所有选择器都失败时,尝试从页面 main 区域提取最长的文本块。
235
+ *
236
+ * @returns {string} 可执行的 JS 脚本字符串
237
+ */
238
+ export function buildLastResortExtractionScript() {
239
+ return `
240
+ (() => {
241
+ const clean = (t) => t.replace(/[\\u200B-\\u200D\\uFEFF]/g, "").trim();
242
+ const main = document.querySelector('main') || document.querySelector('[role="main"]');
243
+ if (!main) return '';
244
+ const allDivs = main.querySelectorAll('div, p, span, section');
245
+ let bestText = '';
246
+ let bestLen = 0;
247
+ for (const el of allDivs) {
248
+ if (el.closest('nav, header, footer, textarea, [contenteditable]')) continue;
249
+ const t = clean(el.innerText || '');
250
+ if (t.length > bestLen && t.length > 20) {
251
+ bestText = t;
252
+ bestLen = t.length;
253
+ }
254
+ }
255
+ return bestText;
256
+ })()
257
+ `;
258
+ }
259
+
260
+ /**
261
+ * 构建响应选择器泛化脚本(校准时使用)
262
+ * 用户点击一个回复气泡后,尝试找到能匹配所有类似气泡的通用选择器。
263
+ *
264
+ * @param {string} specificSelector - 用户点击的元素对应的具体选择器
265
+ * @returns {string} 可执行的 JS 脚本字符串,返回泛化后的选择器
266
+ */
267
+ export function buildResponseSelectorGeneralizationScript(specificSelector) {
268
+ return `
269
+ (function() {
270
+ const clicked = document.querySelector(${JSON.stringify(specificSelector)});
271
+ if (!clicked) return ${JSON.stringify(specificSelector)};
272
+
273
+ const directMatches = document.querySelectorAll(${JSON.stringify(specificSelector)});
274
+ if (directMatches.length > 1) return ${JSON.stringify(specificSelector)};
275
+
276
+ let el = clicked;
277
+ const maxDepth = 6;
278
+ for (let depth = 0; depth < maxDepth && el && el !== document.body; depth++) {
279
+ const role = el.getAttribute('data-message-author-role');
280
+ if (role === 'assistant') {
281
+ const sel = el.tagName.toLowerCase() + '[data-message-author-role="assistant"]';
282
+ if (document.querySelectorAll(sel).length >= 1) return sel;
283
+ }
284
+ const testId = el.getAttribute('data-testid');
285
+ if (testId && testId.includes('conversation') || testId && testId.includes('message')) {
286
+ const sel = el.tagName.toLowerCase() + '[data-testid="' + testId + '"]';
287
+ if (document.querySelectorAll(sel).length > 1) return sel;
288
+ }
289
+ if (el.classList.length > 0) {
290
+ for (const cls of el.classList) {
291
+ if (cls.startsWith('__') || cls.length < 3) continue;
292
+ const sel = el.tagName.toLowerCase() + '.' + cls;
293
+ const matches = document.querySelectorAll(sel);
294
+ if (matches.length > 1 && matches.length < 50) return sel;
295
+ }
296
+ }
297
+ el = el.parentElement;
298
+ }
299
+
300
+ return ${JSON.stringify(specificSelector)};
301
+ })()
302
+ `;
303
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Web Backends 模块入口
3
+ *
4
+ * 管理所有 web 后端(ChatGPT、Claude、DeepSeek 等)。
5
+ * 每个后端有自己的文件夹,包含配置(config.js)、DOM 脚本(dom-scripts.js)和客户端(client.js)。
6
+ *
7
+ * 目录结构:
8
+ * backends/
9
+ * ├── index.js - 本文件,模块入口
10
+ * ├── base-backend.js - Web 后端基类
11
+ * └── chatgpt/
12
+ * ├── config.js - ChatGPT 站点配置和选择器
13
+ * ├── dom-scripts.js - ChatGPT DOM 交互脚本
14
+ * └── client.js - ChatGPTBackend 类
15
+ *
16
+ * 扩展新后端(如 Claude、DeepSeek):
17
+ * 1. 新建 backends/claude/ 文件夹
18
+ * 2. 创建 config.js(站点 URL、选择器)、dom-scripts.js(DOM 脚本)、client.js(后端类)
19
+ * 3. 在此文件中注册新后端
20
+ */
21
+
22
+ import { BaseWebBackend } from './base-backend.js';
23
+ import { chatgptBackend } from './chatgpt/client.js';
24
+
25
+ /**
26
+ * Web 后端注册表
27
+ * 通过 backendId 查找对应的后端实例。
28
+ */
29
+ class WebBackendRegistry {
30
+ constructor() {
31
+ /** @type {Map<string, BaseWebBackend>} */
32
+ this._backends = new Map();
33
+ }
34
+
35
+ /**
36
+ * 注册一个后端
37
+ * @param {BaseWebBackend} backend
38
+ */
39
+ register(backend) {
40
+ this._backends.set(backend.id, backend);
41
+ }
42
+
43
+ /**
44
+ * 根据 backendId 获取后端
45
+ * @param {string} backendId - 如 'chatgpt', 'claude', 'deepseek'
46
+ * @returns {BaseWebBackend|null}
47
+ */
48
+ get(backendId) {
49
+ return this._backends.get(backendId) || null;
50
+ }
51
+
52
+ /**
53
+ * 根据 providerId 获取后端(兼容旧的 provider 命名,如 'web-chatgpt-xxx')
54
+ * @param {string} providerId
55
+ * @returns {BaseWebBackend|null}
56
+ */
57
+ getByProviderId(providerId) {
58
+ if (!providerId) return null;
59
+ // 尝试精确匹配
60
+ for (const [id, backend] of this._backends) {
61
+ if (providerId === id || providerId.includes(id)) {
62
+ return backend;
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+
68
+ /**
69
+ * 获取所有已注册的后端
70
+ * @returns {BaseWebBackend[]}
71
+ */
72
+ getAll() {
73
+ return Array.from(this._backends.values());
74
+ }
75
+
76
+ /**
77
+ * 获取所有后端 ID
78
+ * @returns {string[]}
79
+ */
80
+ getAllIds() {
81
+ return Array.from(this._backends.keys());
82
+ }
83
+ }
84
+
85
+ // 创建全局注册表单例并注册内置后端
86
+ export const webBackendRegistry = new WebBackendRegistry();
87
+ webBackendRegistry.register(chatgptBackend);
88
+
89
+ // 导出
90
+ export { BaseWebBackend } from './base-backend.js';
91
+ export { ChatGPTBackend, chatgptBackend } from './chatgpt/client.js';
@@ -0,0 +1,278 @@
1
+ import { BaseAgent } from '../base-agent.js';
2
+ import { webClientRegistry } from './web-client.js';
3
+
4
+ /**
5
+ * WebAgent — Communication engine powered by browser DOM automation.
6
+ *
7
+ * Opens a hidden Electron BrowserWindow, lets the user log in once, then controls
8
+ * the web chat interface (e.g. ChatGPT web, Claude web) via DOM scripting.
9
+ * No API key required — uses the user's existing subscription.
10
+ *
11
+ * Limitations vs LLMAgent:
12
+ * - No native tool calling (web UIs don't expose function calling)
13
+ * - Simulates tool calls by embedding tool definitions in the system prompt
14
+ * - Session may expire and need re-login
15
+ */
16
+ export class WebAgent extends BaseAgent {
17
+ /**
18
+ * @param {object} config
19
+ * @param {object} config.provider - Web provider config
20
+ */
21
+ constructor(config) {
22
+ super();
23
+ this.provider = config.provider;
24
+ // Employee ID for per-employee session isolation (set by Employee layer)
25
+ this._employeeId = null;
26
+ }
27
+
28
+ /**
29
+ * Set the employee ID for session isolation.
30
+ * Called by the Employee layer to bind this agent to a specific employee.
31
+ * @param {string} employeeId
32
+ */
33
+ setEmployeeId(employeeId) {
34
+ this._employeeId = employeeId;
35
+ }
36
+
37
+ get agentType() {
38
+ return 'web';
39
+ }
40
+
41
+ isAvailable() {
42
+ // DOM mode: Electron manages cookies via Chromium session, no explicit cookie field needed
43
+ return !!(this.provider && this.provider.enabled);
44
+ }
45
+
46
+ canChat() {
47
+ return this.isAvailable();
48
+ }
49
+
50
+ async chat(messages, options = {}) {
51
+ if (!this.isAvailable()) {
52
+ throw new Error(`WebAgent provider "${this.provider?.name}" is not available`);
53
+ }
54
+ return await webClientRegistry.chat(this.provider.id, messages, {
55
+ ...options,
56
+ model: this.provider.webModel || this.provider.model,
57
+ sessionId: this._employeeId || options.sessionId || null,
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Reset the ChatGPT conversation for this employee's session.
63
+ * Next chat will start a new conversation.
64
+ */
65
+ resetConversation() {
66
+ if (this.provider?.id) {
67
+ webClientRegistry.resetConversation(this.provider.id, this._employeeId || undefined);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Check if this employee's session needs a new conversation (too many messages).
73
+ * @param {number} [maxMessages]
74
+ * @returns {boolean}
75
+ */
76
+ needsNewSession(maxMessages) {
77
+ if (!this.provider?.id || !this._employeeId) return false;
78
+ return webClientRegistry.needsNewSession(this.provider.id, this._employeeId, maxMessages);
79
+ }
80
+
81
+ /**
82
+ * WebAgent's chatWithTools implementation.
83
+ * Since web APIs don't support native function calling, we simulate it
84
+ * by embedding tool definitions in the prompt and parsing structured output.
85
+ */
86
+ async chatWithTools(messages, toolExecutor, options = {}) {
87
+ if (!this.isAvailable()) {
88
+ throw new Error(`WebAgent provider "${this.provider?.name}" is not available`);
89
+ }
90
+
91
+ const maxIterations = options.maxIterations || 5;
92
+ const onToolCall = options.onToolCall || null;
93
+ const conversationMessages = [...messages];
94
+ const toolResults = [];
95
+
96
+ // Inject tool definitions into system message
97
+ const toolDefs = toolExecutor.definitions || [];
98
+ if (toolDefs.length > 0) {
99
+ const toolsPrompt = this._buildToolsPrompt(toolDefs);
100
+ // Prepend to first system message or add new one
101
+ const sysIdx = conversationMessages.findIndex(m => m.role === 'system');
102
+ if (sysIdx >= 0) {
103
+ conversationMessages[sysIdx] = {
104
+ ...conversationMessages[sysIdx],
105
+ content: conversationMessages[sysIdx].content + '\n\n' + toolsPrompt,
106
+ };
107
+ } else {
108
+ conversationMessages.unshift({ role: 'system', content: toolsPrompt });
109
+ }
110
+ }
111
+
112
+ for (let i = 0; i < maxIterations; i++) {
113
+ // Only force new conversation on the first iteration; subsequent iterations reuse
114
+ const iterOptions = i === 0 ? options : { ...options, newConversation: false };
115
+ const response = await this.chat(conversationMessages, iterOptions);
116
+
117
+ // Try to parse tool calls from the response
118
+ const parsedCalls = this._parseToolCalls(response.content);
119
+
120
+ if (!parsedCalls || parsedCalls.length === 0) {
121
+ // No tool calls found — this is the final response
122
+ return {
123
+ content: response.content,
124
+ toolResults,
125
+ messages: conversationMessages,
126
+ usage: response.usage,
127
+ };
128
+ }
129
+
130
+ // Process extracted tool calls
131
+ conversationMessages.push({ role: 'assistant', content: response.content });
132
+
133
+ const callResultTexts = [];
134
+ for (const call of parsedCalls) {
135
+ if (onToolCall) {
136
+ try { onToolCall({ tool: call.name, args: call.args, status: 'start' }); } catch {}
137
+ }
138
+
139
+ let result;
140
+ try {
141
+ result = await toolExecutor.execute(call.name, call.args);
142
+ toolResults.push({ tool: call.name, args: call.args, result, success: true });
143
+ if (onToolCall) {
144
+ try { onToolCall({ tool: call.name, args: call.args, status: 'done', success: true }); } catch {}
145
+ }
146
+ } catch (error) {
147
+ result = `Tool execution error: ${error.message}`;
148
+ toolResults.push({ tool: call.name, args: call.args, error: error.message, success: false });
149
+ if (onToolCall) {
150
+ try { onToolCall({ tool: call.name, args: call.args, status: 'error', error: error.message }); } catch {}
151
+ }
152
+ }
153
+
154
+ callResultTexts.push(
155
+ `[Tool Result: ${call.name}]\n${typeof result === 'string' ? result : JSON.stringify(result, null, 2)}`
156
+ );
157
+ }
158
+
159
+ // Feed tool results back as user message
160
+ conversationMessages.push({
161
+ role: 'user',
162
+ content: callResultTexts.join('\n\n'),
163
+ });
164
+ }
165
+
166
+ // Exceeded max iterations — one final call without tool prompt (reuse conversation)
167
+ const finalResponse = await this.chat(conversationMessages, { ...options, newConversation: false });
168
+ return {
169
+ content: finalResponse.content,
170
+ toolResults,
171
+ messages: conversationMessages,
172
+ usage: finalResponse.usage,
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Build a prompt section that describes available tools.
178
+ */
179
+ _buildToolsPrompt(toolDefs) {
180
+ const toolDescriptions = toolDefs.map(t => {
181
+ const params = t.function?.parameters?.properties || {};
182
+ const required = t.function?.parameters?.required || [];
183
+ const paramList = Object.entries(params).map(([name, schema]) => {
184
+ const req = required.includes(name) ? ' (required)' : ' (optional)';
185
+ return ` - ${name}${req}: ${schema.description || schema.type || ''}`;
186
+ }).join('\n');
187
+ return `### ${t.function.name}\n${t.function.description || ''}\nParameters:\n${paramList}`;
188
+ }).join('\n\n');
189
+
190
+ return `## Available Tools
191
+
192
+ You have access to the following tools. To use a tool, include a tool call block in your response using this exact format:
193
+
194
+ \`\`\`tool_call
195
+ {"name": "tool_name", "args": {"param1": "value1"}}
196
+ \`\`\`
197
+
198
+ You can make multiple tool calls in a single response. After receiving tool results, continue your work.
199
+
200
+ ${toolDescriptions}`;
201
+ }
202
+
203
+ /**
204
+ * Parse tool calls from LLM text response.
205
+ * Looks for ```tool_call blocks.
206
+ */
207
+ _parseToolCalls(content) {
208
+ if (!content) return null;
209
+ const calls = [];
210
+ const regex = /```tool_call\s*\n([\s\S]*?)```/g;
211
+ let match;
212
+ while ((match = regex.exec(content)) !== null) {
213
+ try {
214
+ const parsed = JSON.parse(match[1].trim());
215
+ if (parsed.name) {
216
+ calls.push({ name: parsed.name, args: parsed.args || {} });
217
+ }
218
+ } catch {
219
+ // Skip malformed tool calls
220
+ }
221
+ }
222
+ return calls.length > 0 ? calls : null;
223
+ }
224
+
225
+ getDisplayInfo() {
226
+ return {
227
+ name: this.provider?.name || 'Unknown Web Provider',
228
+ provider: this.provider?.provider || 'Web',
229
+ model: this.provider?.webModel || this.provider?.model || 'unknown',
230
+ type: 'web',
231
+ category: this.provider?.category || 'general',
232
+ };
233
+ }
234
+
235
+ getProviderDisplayInfo() {
236
+ return {
237
+ id: this.provider?.id,
238
+ name: this.provider?.name,
239
+ provider: this.provider?.provider,
240
+ };
241
+ }
242
+
243
+ switchProvider(newProvider) {
244
+ this.provider = newProvider;
245
+ }
246
+
247
+ getCostPerToken() {
248
+ return 0; // Web-based, no direct token cost
249
+ }
250
+
251
+ serializeAgent() {
252
+ return {
253
+ agentType: 'web',
254
+ provider: this.provider ? {
255
+ id: this.provider.id,
256
+ name: this.provider.name,
257
+ provider: this.provider.provider,
258
+ model: this.provider.model,
259
+ webModel: this.provider.webModel,
260
+ category: this.provider.category,
261
+ costPerToken: 0,
262
+ enabled: this.provider.enabled,
263
+ isWeb: true,
264
+ } : null,
265
+ };
266
+ }
267
+
268
+ /**
269
+ * Restore WebAgent from serialized data.
270
+ */
271
+ static deserialize(data, providerRegistry) {
272
+ let provider = data.provider;
273
+ if (providerRegistry && data.provider?.id) {
274
+ provider = providerRegistry.getById(data.provider.id) || data.provider;
275
+ }
276
+ return new WebAgent({ provider });
277
+ }
278
+ }