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.
- package/.dockerignore +33 -0
- package/.nvmrc +1 -0
- package/ARCHITECTURE.md +394 -0
- package/Dockerfile +50 -0
- package/LICENSE +29 -0
- package/README.md +206 -0
- package/bin/i18n.js +46 -0
- package/bin/ideaco.js +494 -0
- package/deploy.sh +15 -0
- package/docker-compose.yml +30 -0
- package/electron/main.cjs +986 -0
- package/electron/preload.cjs +14 -0
- package/electron/web-backends.cjs +854 -0
- package/jsconfig.json +8 -0
- package/next.config.mjs +34 -0
- package/package.json +134 -0
- package/postcss.config.mjs +6 -0
- package/public/demo/dashboard.png +0 -0
- package/public/demo/employee.png +0 -0
- package/public/demo/messages.png +0 -0
- package/public/demo/office.png +0 -0
- package/public/demo/requirement.png +0 -0
- package/public/logo.jpeg +0 -0
- package/public/logo.png +0 -0
- package/scripts/prepare-electron.js +67 -0
- package/scripts/release.js +76 -0
- package/src/app/api/agents/[agentId]/chat/route.js +70 -0
- package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
- package/src/app/api/agents/[agentId]/route.js +106 -0
- package/src/app/api/avatar/route.js +104 -0
- package/src/app/api/browse-dir/route.js +44 -0
- package/src/app/api/chat/route.js +265 -0
- package/src/app/api/company/factory-reset/route.js +43 -0
- package/src/app/api/company/route.js +82 -0
- package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
- package/src/app/api/departments/route.js +92 -0
- package/src/app/api/group-chat-loop/events/route.js +70 -0
- package/src/app/api/group-chat-loop/route.js +94 -0
- package/src/app/api/mailbox/route.js +100 -0
- package/src/app/api/messages/route.js +14 -0
- package/src/app/api/providers/[id]/configure/route.js +21 -0
- package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
- package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
- package/src/app/api/providers/route.js +11 -0
- package/src/app/api/requirements/route.js +242 -0
- package/src/app/api/secretary/route.js +65 -0
- package/src/app/api/system/cli-backends/route.js +91 -0
- package/src/app/api/system/cron/route.js +110 -0
- package/src/app/api/system/knowledge/route.js +104 -0
- package/src/app/api/system/plugins/route.js +40 -0
- package/src/app/api/system/skills/route.js +46 -0
- package/src/app/api/system/status/route.js +46 -0
- package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
- package/src/app/api/talent-market/[profileId]/route.js +17 -0
- package/src/app/api/talent-market/route.js +26 -0
- package/src/app/api/teams/route.js +773 -0
- package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
- package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
- package/src/app/globals.css +130 -0
- package/src/app/layout.jsx +40 -0
- package/src/app/page.jsx +97 -0
- package/src/components/AgentChatModal.jsx +164 -0
- package/src/components/AgentDetailModal.jsx +425 -0
- package/src/components/AgentSpyModal.jsx +481 -0
- package/src/components/AvatarGrid.jsx +29 -0
- package/src/components/BossProfileModal.jsx +162 -0
- package/src/components/CachedAvatar.jsx +77 -0
- package/src/components/ChatPanel.jsx +219 -0
- package/src/components/ChatShared.jsx +255 -0
- package/src/components/DepartmentDetail.jsx +842 -0
- package/src/components/DepartmentView.jsx +367 -0
- package/src/components/FileReference.jsx +260 -0
- package/src/components/FilesView.jsx +465 -0
- package/src/components/GroupChatView.jsx +799 -0
- package/src/components/Mailbox.jsx +926 -0
- package/src/components/MessagesView.jsx +112 -0
- package/src/components/OnboardingGuide.jsx +209 -0
- package/src/components/OrgTree.jsx +151 -0
- package/src/components/Overview.jsx +391 -0
- package/src/components/PixelOffice.jsx +2281 -0
- package/src/components/ProviderGrid.jsx +551 -0
- package/src/components/ProvidersBoard.jsx +16 -0
- package/src/components/RequirementDetail.jsx +1279 -0
- package/src/components/RequirementsBoard.jsx +187 -0
- package/src/components/SecretarySettings.jsx +295 -0
- package/src/components/SetupWizard.jsx +388 -0
- package/src/components/Sidebar.jsx +169 -0
- package/src/components/SystemMonitor.jsx +808 -0
- package/src/components/TalentMarket.jsx +183 -0
- package/src/components/TeamDetail.jsx +697 -0
- package/src/core/agent/base-agent.js +104 -0
- package/src/core/agent/chat-store.js +602 -0
- package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
- package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
- package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
- package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
- package/src/core/agent/cli-agent/backends/index.js +27 -0
- package/src/core/agent/cli-agent/backends/registry.js +580 -0
- package/src/core/agent/cli-agent/index.js +154 -0
- package/src/core/agent/index.js +60 -0
- package/src/core/agent/llm-agent/client.js +320 -0
- package/src/core/agent/llm-agent/index.js +97 -0
- package/src/core/agent/message-bus.js +211 -0
- package/src/core/agent/session.js +608 -0
- package/src/core/agent/tools.js +596 -0
- package/src/core/agent/web-agent/backends/base-backend.js +180 -0
- package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
- package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
- package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
- package/src/core/agent/web-agent/backends/index.js +91 -0
- package/src/core/agent/web-agent/index.js +278 -0
- package/src/core/agent/web-agent/web-client.js +407 -0
- package/src/core/employee/base-employee.js +1088 -0
- package/src/core/employee/index.js +35 -0
- package/src/core/employee/knowledge.js +327 -0
- package/src/core/employee/lifecycle.js +990 -0
- package/src/core/employee/memory/index.js +642 -0
- package/src/core/employee/memory/store.js +143 -0
- package/src/core/employee/performance.js +224 -0
- package/src/core/employee/secretary.js +625 -0
- package/src/core/employee/skills.js +398 -0
- package/src/core/index.js +38 -0
- package/src/core/organization/company.js +2600 -0
- package/src/core/organization/department.js +737 -0
- package/src/core/organization/group-chat-loop.js +264 -0
- package/src/core/organization/index.js +8 -0
- package/src/core/organization/persistence.js +111 -0
- package/src/core/organization/team.js +267 -0
- package/src/core/organization/workforce/hr.js +377 -0
- package/src/core/organization/workforce/providers.js +468 -0
- package/src/core/organization/workforce/role-archetypes.js +805 -0
- package/src/core/organization/workforce/talent-market.js +205 -0
- package/src/core/prompts.js +532 -0
- package/src/core/requirement.js +1789 -0
- package/src/core/system/audit.js +483 -0
- package/src/core/system/cron.js +449 -0
- package/src/core/system/index.js +7 -0
- package/src/core/system/plugin.js +2183 -0
- package/src/core/utils/json-parse.js +188 -0
- package/src/core/workspace.js +239 -0
- package/src/lib/api-i18n.js +211 -0
- package/src/lib/avatar.js +268 -0
- package/src/lib/client-store.js +1025 -0
- package/src/lib/config-validator.js +483 -0
- package/src/lib/format-time.js +22 -0
- package/src/lib/hooks.js +414 -0
- package/src/lib/i18n.js +134 -0
- package/src/lib/paths.js +23 -0
- package/src/lib/store.js +72 -0
- package/src/locales/de.js +393 -0
- package/src/locales/en.js +1054 -0
- package/src/locales/es.js +393 -0
- package/src/locales/fr.js +393 -0
- package/src/locales/ja.js +501 -0
- package/src/locales/ko.js +513 -0
- package/src/locales/zh.js +828 -0
- 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
|
+
}
|