cicy-desktop 2.1.78 → 2.1.79

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 (54) hide show
  1. package/bin/cicy-desktop +7 -7
  2. package/package.json +6 -6
  3. package/src/backends/homepage-preload.js +22 -0
  4. package/src/backends/homepage-react/assets/index-CKpaMBKz.css +1 -0
  5. package/src/backends/homepage-react/assets/index-CSsNZgC5.js +365 -0
  6. package/src/backends/homepage-react/index.html +2 -2
  7. package/src/backends/homepage-window.js +52 -7
  8. package/src/backends/ipc.js +57 -0
  9. package/src/backends/local-teams.js +73 -26
  10. package/src/backends/sidecar-ipc.js +11 -0
  11. package/src/backends/webview-preload.js +5 -3
  12. package/src/backends/window-manager.js +13 -3
  13. package/src/chrome/chrome-launcher.js +5 -4
  14. package/src/chrome/debugger-port-resolver.js +1 -1
  15. package/src/cloud/cloud-client.js +237 -41
  16. package/src/cluster/types.js +0 -5
  17. package/src/extension/inject.js +1 -1
  18. package/src/main.js +282 -88
  19. package/src/master/chrome-config.js +2 -2
  20. package/src/preload-rpc.js +1 -1
  21. package/src/profiles/profile-store.js +321 -0
  22. package/src/profiles/trusted-origins-store.js +95 -0
  23. package/src/server/worker-observability-routes.js +0 -2
  24. package/src/sidecar/cicy-code.js +84 -23
  25. package/src/sidecar/localbin.js +20 -3
  26. package/src/sidecar/native.js +3 -3
  27. package/src/sidecar/version.js +45 -0
  28. package/src/tabbrowser/newtab-protocol.js +54 -0
  29. package/src/tabbrowser/tab-browser.html +151 -0
  30. package/src/tabbrowser/tab-shell-preload.js +28 -0
  31. package/src/tabbrowser/tab-shell.html +227 -0
  32. package/src/tools/account-tools.js +191 -25
  33. package/src/tools/chrome-tools.js +173 -37
  34. package/src/tools/device-tools.js +25 -0
  35. package/src/tools/index.js +2 -0
  36. package/src/tools/tab-browser-tools.js +453 -0
  37. package/src/tools/window-tools.js +64 -7
  38. package/src/utils/brand-host-electron.js +25 -0
  39. package/src/utils/context-menu-options.js +80 -0
  40. package/src/utils/cookie-logins.js +58 -0
  41. package/src/utils/ip-probe.js +50 -0
  42. package/src/utils/rpc-audit.js +53 -0
  43. package/src/utils/rpc-guard.js +189 -0
  44. package/src/utils/window-monitor.js +5 -15
  45. package/src/utils/window-registry.js +210 -0
  46. package/src/utils/window-thumbnails.js +126 -0
  47. package/src/utils/window-utils.js +146 -109
  48. package/workers/render/package-lock.json +6 -6
  49. package/workers/render/src/App.css +36 -2
  50. package/workers/render/src/App.jsx +587 -103
  51. package/src/backends/artifact-ipc.js +0 -142
  52. package/src/backends/homepage-react/assets/index-DE9m6JTn.css +0 -1
  53. package/src/backends/homepage-react/assets/index-DLYMzgf5.js +0 -365
  54. package/src/cluster/artifact-registry.js +0 -61
@@ -3,6 +3,10 @@ const path = require("path");
3
3
  const os = require("os");
4
4
  const { z } = require("zod");
5
5
 
6
+ const profileStore = require("../profiles/profile-store");
7
+ const { summarizeCookieLogins } = require("../utils/cookie-logins");
8
+ const { probeIpViaSession } = require("../utils/ip-probe");
9
+
6
10
  const ACCOUNT_DIR = path.join(os.homedir(), "data", "electron");
7
11
 
8
12
  function ensureAccountDir() {
@@ -175,40 +179,25 @@ module.exports = (registerTool) => {
175
179
  { tag: "Account" }
176
180
  );
177
181
 
178
- // 设置账户代理
182
+ // 设置账户代理(持久化到 account-N.json + 立即应用到 session)
179
183
  registerTool(
180
184
  "set_account_proxy",
181
- "为指定账户设置代理,该账户下所有窗口都会使用此代理",
185
+ "为指定账户设置代理:持久化写入 account-N.json,并立即应用到该 sandbox session(之后新开窗口也会自动套用)。留空则清除。",
182
186
  z.object({
183
187
  accountIdx: z.number().describe("账户索引"),
184
- proxy: z.string().optional().describe("代理地址,如 http://127.0.0.1:8888,留空则清除代理"),
188
+ proxy: z.string().optional().describe("代理地址,如 socks5://127.0.0.1:20001,留空则清除代理"),
185
189
  }),
186
190
  async ({ accountIdx, proxy }) => {
187
191
  try {
192
+ // 1) persist the desired proxy (canonical {url, enabled}) to the store
193
+ const view = profileStore.setProxy("electron", accountIdx, proxy || "");
194
+ // 2) apply to the live session now
188
195
  const { session } = require("electron");
189
196
  const ses = session.fromPartition(`persist:sandbox-${accountIdx}`);
190
-
191
- if (proxy) {
192
- await ses.setProxy({ proxyRules: proxy });
193
- return {
194
- content: [
195
- {
196
- type: "text",
197
- text: JSON.stringify({ success: true, accountIdx, proxy }, null, 2),
198
- },
199
- ],
200
- };
201
- } else {
202
- await ses.setProxy({ proxyRules: "" });
203
- return {
204
- content: [
205
- {
206
- type: "text",
207
- text: JSON.stringify({ success: true, accountIdx, proxy: "cleared" }, null, 2),
208
- },
209
- ],
210
- };
211
- }
197
+ await ses.setProxy({ proxyRules: profileStore.proxyRules(view.proxy) });
198
+ return {
199
+ content: [{ type: "text", text: JSON.stringify({ success: true, accountIdx, proxy: view.proxy }, null, 2) }],
200
+ };
212
201
  } catch (error) {
213
202
  return {
214
203
  content: [{ type: "text", text: `Error: ${error.message}` }],
@@ -218,4 +207,181 @@ module.exports = (registerTool) => {
218
207
  },
219
208
  { tag: "Account" }
220
209
  );
210
+
211
+ // ── Unified profile surface (mirrors agent-chrome verbs) ──────────────────
212
+ const toResult = (obj, isError = false) => ({
213
+ content: [{ type: "text", text: JSON.stringify(obj, null, 2) }],
214
+ ...(isError ? { isError: true } : {}),
215
+ });
216
+
217
+ registerTool(
218
+ "electron_list_profiles",
219
+ "列出所有 Electron profile(从 account-N.json 配置读取,含 name/proxy/logins)",
220
+ z.object({}),
221
+ async () => {
222
+ try {
223
+ return toResult(profileStore.listProfiles("electron"));
224
+ } catch (e) {
225
+ return toResult({ error: e.message }, true);
226
+ }
227
+ },
228
+ { tag: "Account" }
229
+ );
230
+
231
+ registerTool(
232
+ "electron_add_profile",
233
+ "新增一个 Electron profile:在 ~/data/electron/ 写入下一个 account-N.json(N=现有最大+1,至少为 1),返回统一视图",
234
+ z.object({ name: z.string().optional().describe("可选:profile 名称(metadata.name)") }),
235
+ async ({ name } = {}) => {
236
+ try {
237
+ ensureAccountDir();
238
+ const re = /^account-(\d+)\.json$/;
239
+ const nums = fs.readdirSync(ACCOUNT_DIR).map((f) => (re.exec(f) ? Number(re.exec(f)[1]) : null)).filter((n) => typeof n === "number");
240
+ const next = nums.length ? Math.max(...nums) + 1 : 1;
241
+ const file = path.join(ACCOUNT_DIR, `account-${next}.json`);
242
+ if (fs.existsSync(file)) return toResult({ error: `account-${next}.json already exists` }, true);
243
+ const now = new Date().toISOString();
244
+ const data = {
245
+ accountIdx: next, createdAt: now, updatedAt: now, windows: [],
246
+ metadata: { description: `Account ${next}`, ...(name ? { name } : {}), tags: [] },
247
+ proxy: { url: "", enabled: false }, logins: [],
248
+ };
249
+ fs.writeFileSync(file, JSON.stringify(data, null, 2));
250
+ return toResult({ success: true, accountIdx: next, profile: profileStore.getProfile("electron", next) });
251
+ } catch (e) {
252
+ return toResult({ error: e.message }, true);
253
+ }
254
+ },
255
+ { tag: "Account" }
256
+ );
257
+
258
+ registerTool(
259
+ "electron_get_profile",
260
+ "获取单个 Electron profile 的统一视图(name/proxy/logins/partition)",
261
+ z.object({ accountIdx: z.number().describe("账户索引") }),
262
+ async ({ accountIdx }) => {
263
+ try {
264
+ const p = profileStore.getProfile("electron", accountIdx);
265
+ return p ? toResult(p) : toResult({ error: `electron-${accountIdx} not found` }, true);
266
+ } catch (e) {
267
+ return toResult({ error: e.message }, true);
268
+ }
269
+ },
270
+ { tag: "Account" }
271
+ );
272
+
273
+ registerTool(
274
+ "electron_probe_ip",
275
+ "探测某 Electron profile 当前出口 IP + 地区(经该 sandbox session 的代理),写入 config 并盖探测时间;可重复探测",
276
+ z.object({ accountIdx: z.number().describe("账户索引") }),
277
+ async ({ accountIdx }) => {
278
+ try {
279
+ const { session } = require("electron");
280
+ const ses = session.fromPartition(`persist:sandbox-${accountIdx}`);
281
+ // Ensure the profile's configured proxy is applied to the session before
282
+ // probing — otherwise (e.g. no window opened yet) it goes DIRECT and we'd
283
+ // report the machine's own IP instead of the profile's egress.
284
+ const prof = profileStore.getProfile("electron", accountIdx);
285
+ const rules = profileStore.proxyRules(prof && prof.proxy);
286
+ await ses.setProxy({ proxyRules: rules || "direct://" });
287
+ const info = await probeIpViaSession(ses);
288
+ const view = profileStore.setIpInfo("electron", accountIdx, info);
289
+ return toResult({ accountIdx, backend: "electron", proxy: rules || null, ipInfo: view.ipInfo });
290
+ } catch (e) {
291
+ return toResult({ error: e.message }, true);
292
+ }
293
+ },
294
+ { tag: "Account" }
295
+ );
296
+
297
+ registerTool(
298
+ "electron_detect_logins",
299
+ "探测指定 Electron profile(persist:sandbox-N)当前 session 登录了哪些站点:读取该 partition 全部 cookie 按域名归并,标记带会话 cookie 的域名(cookie 在≠一定登录,仅强信号)",
300
+ z.object({ accountIdx: z.number().describe("账户索引") }),
301
+ async ({ accountIdx }) => {
302
+ try {
303
+ const { session } = require("electron");
304
+ const ses = session.fromPartition(`persist:sandbox-${accountIdx}`);
305
+ const cookies = await ses.cookies.get({});
306
+ return toResult({ accountIdx, backend: "electron", ...summarizeCookieLogins(cookies) });
307
+ } catch (e) {
308
+ return toResult({ error: e.message }, true);
309
+ }
310
+ },
311
+ { tag: "Account" }
312
+ );
313
+
314
+ registerTool(
315
+ "electron_profile_login_add",
316
+ "记录某 Electron profile 登录了哪个平台账号(每个平台一条,重复平台覆盖)",
317
+ z.object({
318
+ accountIdx: z.number().describe("账户索引"),
319
+ platform: z.string().describe("平台,如 github / google / x"),
320
+ account: z.string().describe("账号标识,如 alice@x.com"),
321
+ }),
322
+ async ({ accountIdx, platform, account }) => {
323
+ try {
324
+ return toResult(profileStore.addLogin("electron", accountIdx, platform, account));
325
+ } catch (e) {
326
+ return toResult({ error: e.message }, true);
327
+ }
328
+ },
329
+ { tag: "Account" }
330
+ );
331
+
332
+ registerTool(
333
+ "electron_profile_login_set",
334
+ "新增/更新某 Electron profile 的一条登录记录(富字段:地址/名称/用户名/邮箱/手机/2FA/备用邮箱/备注;按名称 name 归并,只覆盖传入的非空字段)",
335
+ z.object({
336
+ accountIdx: z.number().describe("账户索引"),
337
+ name: z.string().describe("名称=平台/站点名,如 抖音 / Google(归并键)"),
338
+ url: z.string().optional().describe("地址,如 https://www.douyin.com"),
339
+ username: z.string().optional().describe("用户名"),
340
+ email: z.string().optional().describe("邮箱"),
341
+ mobile: z.string().optional().describe("手机号"),
342
+ twofa: z.string().optional().describe("2FA(TOTP 秘钥或说明)"),
343
+ secondEmail: z.string().optional().describe("备用邮箱"),
344
+ note: z.string().optional().describe("备注"),
345
+ loginAt: z.string().optional().describe("登录时间 ISO(不传则首次记录时自动 now)"),
346
+ }),
347
+ async ({ accountIdx, ...login }) => {
348
+ try {
349
+ return toResult(profileStore.setLogin("electron", accountIdx, login));
350
+ } catch (e) {
351
+ return toResult({ error: e.message }, true);
352
+ }
353
+ },
354
+ { tag: "Account" }
355
+ );
356
+
357
+ registerTool(
358
+ "electron_profile_login_rm",
359
+ "移除某 Electron profile 的某平台登录记录",
360
+ z.object({
361
+ accountIdx: z.number().describe("账户索引"),
362
+ platform: z.string().describe("平台"),
363
+ }),
364
+ async ({ accountIdx, platform }) => {
365
+ try {
366
+ return toResult(profileStore.removeLogin("electron", accountIdx, platform));
367
+ } catch (e) {
368
+ return toResult({ error: e.message }, true);
369
+ }
370
+ },
371
+ { tag: "Account" }
372
+ );
373
+
374
+ registerTool(
375
+ "electron_profile_logins",
376
+ "列出某 Electron profile 已记录的平台登录",
377
+ z.object({ accountIdx: z.number().describe("账户索引") }),
378
+ async ({ accountIdx }) => {
379
+ try {
380
+ return toResult(profileStore.listLogins("electron", accountIdx));
381
+ } catch (e) {
382
+ return toResult({ error: e.message }, true);
383
+ }
384
+ },
385
+ { tag: "Account" }
386
+ );
221
387
  };
@@ -17,6 +17,9 @@ const { isPortOpen } = require("../utils/process-utils");
17
17
  const { getVersion, getTargets, createTarget, activateTarget, callCdp } = require("../chrome/chrome-cdp-client");
18
18
  const { resolveChromeDebuggerPort } = require("../chrome/debugger-port-resolver");
19
19
  const { config } = require("../config");
20
+ const profileStore = require("../profiles/profile-store");
21
+ const { summarizeCookieLogins } = require("../utils/cookie-logins");
22
+ const { probeIpViaSession } = require("../utils/ip-probe");
20
23
 
21
24
  const PRIVATE_CHROME_JSON = path.join(os.homedir(), "cicy-ai", "db", "chrome.json");
22
25
  const PRIVATE_CHROME_TMP_DIR = path.join(os.homedir(), "chrome", "_tmp");
@@ -39,14 +42,24 @@ function expandHome(input) {
39
42
  return input;
40
43
  }
41
44
 
45
+ // tildify — collapse the home-dir prefix back to "~" for DISPLAY only.
46
+ // The machine has its own user (e.g. /Users/ton); surfacing that absolute path
47
+ // is non-portable for other people/agents, so anything shown/returned uses "~".
48
+ // NEVER feed a tildified path to Chrome's --user-data-dir or fs (use expandHome).
49
+ function tildify(input) {
50
+ if (typeof input !== "string" || input.length === 0) return input;
51
+ const home = os.homedir();
52
+ if (input === home) return "~";
53
+ if (input.startsWith(home + path.sep)) return "~/" + input.slice(home.length + 1);
54
+ return input;
55
+ }
56
+
57
+ // Delegate to the shared normalizer (profile-store) so chrome + electron agree
58
+ // on proxy encoding. Accepts string | {enable,url} | {url,enabled}; returns the
59
+ // URL string when enabled, else null (the contract chrome-launcher expects).
42
60
  function normalizePrivateProxy(proxyValue) {
43
- if (typeof proxyValue === "string") {
44
- return proxyValue || null;
45
- }
46
- if (proxyValue && typeof proxyValue === "object" && proxyValue.enable && proxyValue.url) {
47
- return proxyValue.url;
48
- }
49
- return null;
61
+ const p = profileStore.normalizeProxy(proxyValue);
62
+ return p.enabled && p.url ? p.url : null;
50
63
  }
51
64
 
52
65
  function readPrivateChromeConfig() {
@@ -71,7 +84,7 @@ function listPrivateChromeEntries({ includeHidden = false } = {}) {
71
84
  const entries = Object.entries(data)
72
85
  .filter(([k]) => (includeHidden ? true : !k.startsWith("__")))
73
86
  .map(([profileKey, entry]) => {
74
- const m = /^account_(\d+)$/.exec(profileKey);
87
+ const m = /^profile_(\d+)$/.exec(profileKey);
75
88
  const accountIdx = m ? Number(m[1]) : null;
76
89
  return { profileKey, accountIdx, entry };
77
90
  })
@@ -87,7 +100,7 @@ function listPrivateChromeEntries({ includeHidden = false } = {}) {
87
100
 
88
101
  function getPrivateChromeEntryByAccountIdx(accountIdx) {
89
102
  const data = readPrivateChromeConfig();
90
- const profileKey = `account_${accountIdx}`;
103
+ const profileKey = `profile_${accountIdx}`;
91
104
  const entry = data[profileKey] || null;
92
105
  if (!entry) return null;
93
106
  return { profileKey, accountIdx, entry };
@@ -144,7 +157,7 @@ function normalizeEffectiveChromeProfile(accountIdx, effectiveChromeProfile) {
144
157
  const platform = parsed.platform && typeof parsed.platform === "object" ? parsed.platform : {};
145
158
 
146
159
  return {
147
- profileKey: `account_${accountIdx}`,
160
+ profileKey: `profile_${accountIdx}`,
148
161
  accountIdx,
149
162
  gmail,
150
163
  orgPath,
@@ -279,7 +292,7 @@ async function launchOrActivateProfile({
279
292
  effectiveChromeProfile,
280
293
  }) {
281
294
  const registry = getChromeRuntimeRegistry();
282
- const profileKey = `account_${accountIdx}`;
295
+ const profileKey = `profile_${accountIdx}`;
283
296
 
284
297
  let normalized;
285
298
  let profileSource;
@@ -290,7 +303,7 @@ async function launchOrActivateProfile({
290
303
  const cfg = getPrivateChromeEntryByAccountIdx(accountIdx);
291
304
  if (!cfg) {
292
305
  throw new Error(
293
- `Missing chrome profile for account_${accountIdx}. Use master dispatch or pass effectiveChromeProfile.`
306
+ `Missing chrome profile for profile_${accountIdx}. Use master dispatch or pass effectiveChromeProfile.`
294
307
  );
295
308
  }
296
309
  normalized = normalizePrivateChromeEntry(cfg.profileKey, cfg.accountIdx, cfg.entry);
@@ -308,6 +321,8 @@ async function launchOrActivateProfile({
308
321
  const effectiveUserDataDirRoot =
309
322
  normalized.expanded.rpaDir ||
310
323
  getDefaultUserDataDirRoot(accountIdx, config.chromeUserDataRoot || DEFAULT_USER_DATA_BASE_ROOT);
324
+ // ~-collapsed form for anything returned/stored for display (portable across users)
325
+ const displayUserDataDir = tildify(effectiveUserDataDirRoot);
311
326
 
312
327
  // Script parity: if /json/version reachable => activate first page target and return reused
313
328
  const liveStatus = await probeChromeDebugger(effectivePort);
@@ -323,7 +338,7 @@ async function launchOrActivateProfile({
323
338
  status: "running",
324
339
  debuggerPort: effectivePort,
325
340
  proxy: effectiveProxy || null,
326
- userDataDirRoot: effectiveUserDataDirRoot,
341
+ userDataDirRoot: displayUserDataDir,
327
342
  profileDirectory: getProfileDirectory(accountIdx),
328
343
  url: url || null,
329
344
  webSocketDebuggerUrl: liveStatus.webSocketDebuggerUrl || null,
@@ -339,7 +354,7 @@ async function launchOrActivateProfile({
339
354
  gmail: normalized.gmail,
340
355
  port: effectivePort,
341
356
  proxy: effectiveProxy || null,
342
- userDataDirRoot: effectiveUserDataDirRoot,
357
+ userDataDirRoot: displayUserDataDir,
343
358
  runtime: nextRuntime,
344
359
  liveStatus,
345
360
  targetsPreview: buildTargetsPreview(targets),
@@ -363,7 +378,7 @@ async function launchOrActivateProfile({
363
378
  debuggerPort: effectivePort,
364
379
  proxy: effectiveProxy || null,
365
380
  chromeBinary: config.chromeBinary || null,
366
- userDataDirRoot: effectiveUserDataDirRoot,
381
+ userDataDirRoot: displayUserDataDir,
367
382
  profileDirectory: getProfileDirectory(accountIdx),
368
383
  url: url || null,
369
384
  error: null,
@@ -385,7 +400,7 @@ async function launchOrActivateProfile({
385
400
  debuggerPort: launched.debuggerPort,
386
401
  proxy: launched.proxy,
387
402
  chromeBinary: launched.chromeBinary,
388
- userDataDirRoot: launched.userDataDirRoot,
403
+ userDataDirRoot: tildify(launched.userDataDirRoot),
389
404
  profileDirectory: launched.profileDirectory,
390
405
  url: launched.url,
391
406
  webSocketDebuggerUrl: launched.webSocketDebuggerUrl,
@@ -406,7 +421,7 @@ async function launchOrActivateProfile({
406
421
  gmail: normalized.gmail,
407
422
  port: effectivePort,
408
423
  proxy: effectiveProxy || null,
409
- userDataDirRoot: effectiveUserDataDirRoot,
424
+ userDataDirRoot: displayUserDataDir,
410
425
  runtime: nextRuntime,
411
426
  activatedTargetId,
412
427
  targetsPreview: buildTargetsPreview(targets),
@@ -440,12 +455,16 @@ function registerChromeTools(registerTool) {
440
455
  profileKey,
441
456
  accountIdx,
442
457
  gmail: normalized.gmail,
458
+ note: normalized.note,
443
459
  orgPath: normalized.orgPath,
444
460
  rpaDir: normalized.rpaDir,
445
461
  port,
446
462
  proxy: normalized.proxyUrl,
447
463
  proxyRaw: normalized.proxy,
448
464
  platform: normalized.platform,
465
+ // parity with electron_list_profiles so the panel renders the same row
466
+ ipInfo: profileStore.normalizeIpInfo(entry.ipInfo),
467
+ logins: (Array.isArray(entry.logins) ? entry.logins : []).map(profileStore.normalizeLogin),
449
468
  runtime,
450
469
  liveStatus,
451
470
  });
@@ -528,14 +547,14 @@ function registerChromeTools(registerTool) {
528
547
  "chrome_get_profile",
529
548
  "获取指定 accountIdx 的 profile:privateConfig + runtime + liveStatus(脚本心智)",
530
549
  z.object({
531
- accountIdx: z.number().describe("账户索引,映射到 ~/cicy-ai/db/chrome.json 的 account_<idx>"),
550
+ accountIdx: z.number().describe("账户索引,映射到 ~/cicy-ai/db/chrome.json 的 profile_<idx>"),
532
551
  }),
533
552
  async ({ accountIdx }) => {
534
553
  const registry = getChromeRuntimeRegistry();
535
554
  const cfg = getPrivateChromeEntryByAccountIdx(accountIdx);
536
555
  if (!cfg) {
537
556
  return toToolResult(
538
- { error: `Missing chrome.json entry: account_${accountIdx}` },
557
+ { error: `Missing chrome.json entry: profile_${accountIdx}` },
539
558
  { isError: true }
540
559
  );
541
560
  }
@@ -587,7 +606,7 @@ function registerChromeTools(registerTool) {
587
606
 
588
607
  registerTool(
589
608
  "chrome_add_profile",
590
- "新增账号(等价于 chrome-rpa.sh add):从 ~/chrome/__tmp 创建 ~/chrome/account_N 并写回 ~/cicy-ai/db/chrome.json",
609
+ "新增账号(等价于 chrome-rpa.sh add):从 ~/chrome/__tmp 创建 ~/chrome/profile_N 并写回 ~/cicy-ai/db/chrome.json",
591
610
  z.object({
592
611
  gmail: z.string().optional().describe("可选:新账号 gmail"),
593
612
  orgPath: z.string().optional().describe("可选:orgPath(默认 Profile 9)"),
@@ -596,13 +615,13 @@ function registerChromeTools(registerTool) {
596
615
  async ({ gmail, orgPath, launchAfterCreate } = {}) => {
597
616
  const data = readPrivateChromeConfig();
598
617
  const nums = Object.keys(data)
599
- .map((k) => (/^account_(\d+)$/.exec(k) ? Number(/^account_(\d+)$/.exec(k)[1]) : null))
618
+ .map((k) => (/^profile_(\d+)$/.exec(k) ? Number(/^profile_(\d+)$/.exec(k)[1]) : null))
600
619
  .filter((n) => typeof n === "number");
601
620
  const nextNum = nums.length ? Math.max(...nums) + 1 : 1;
602
621
 
603
- const profileKey = `account_${nextNum}`;
622
+ const profileKey = `profile_${nextNum}`;
604
623
  const port = 11000 + nextNum;
605
- const rpaDirTilde = `~/chrome/${profileKey}`;
624
+ const rpaDirTilde = `~/chrome/profile_${nextNum}`;
606
625
  const rpaDir = expandHome(rpaDirTilde);
607
626
 
608
627
  if (fs.existsSync(rpaDir)) {
@@ -652,23 +671,92 @@ function registerChromeTools(registerTool) {
652
671
 
653
672
  registerTool(
654
673
  "chrome_set_profile_proxy",
655
- "设置 ~/cicy-ai/db/chrome.json 中指定 accountIdx 的 proxy(脚本对齐;下次启动生效)",
674
+ "设置 ~/cicy-ai/db/chrome.json 中指定 accountIdx 的 proxy(持久化为 {url,enabled};下次启动生效)",
656
675
  z.object({
657
676
  accountIdx: z.number().describe("账户索引"),
658
677
  proxy: z.string().optional().describe("代理 URL;留空则清空"),
659
678
  }),
660
679
  async ({ accountIdx, proxy } = {}) => {
661
- const data = readPrivateChromeConfig();
662
- const key = `account_${accountIdx}`;
663
- if (!data[key]) {
664
- return toToolResult({ error: `Missing chrome.json entry: ${key}` }, { isError: true });
680
+ try {
681
+ const view = profileStore.setProxy("chrome", accountIdx, proxy || "");
682
+ return toToolResult({ success: true, profileKey: `profile_${accountIdx}`, profile: view });
683
+ } catch (e) {
684
+ return toToolResult({ error: e.message }, { isError: true });
685
+ }
686
+ },
687
+ { tag: "Chrome" }
688
+ );
689
+
690
+ registerTool(
691
+ "chrome_profile_login_add",
692
+ "记录某 Chrome profile 登录了哪个平台账号(每个平台一条,重复平台覆盖)",
693
+ z.object({
694
+ accountIdx: z.number().describe("账户索引"),
695
+ platform: z.string().describe("平台,如 github / google / x"),
696
+ account: z.string().describe("账号标识,如 alice@x.com"),
697
+ }),
698
+ async ({ accountIdx, platform, account } = {}) => {
699
+ try {
700
+ return toToolResult(profileStore.addLogin("chrome", accountIdx, platform, account));
701
+ } catch (e) {
702
+ return toToolResult({ error: e.message }, { isError: true });
703
+ }
704
+ },
705
+ { tag: "Chrome" }
706
+ );
707
+
708
+ registerTool(
709
+ "chrome_profile_login_set",
710
+ "新增/更新某 Chrome profile 的一条登录记录(富字段:地址/名称/用户名/邮箱/手机/2FA/备用邮箱/备注;按名称 name 归并,只覆盖传入的非空字段)",
711
+ z.object({
712
+ accountIdx: z.number().describe("账户索引"),
713
+ name: z.string().describe("名称=平台/站点名,如 抖音 / Google(归并键)"),
714
+ url: z.string().optional().describe("地址,如 https://www.douyin.com"),
715
+ username: z.string().optional().describe("用户名"),
716
+ email: z.string().optional().describe("邮箱"),
717
+ mobile: z.string().optional().describe("手机号"),
718
+ twofa: z.string().optional().describe("2FA(TOTP 秘钥或说明)"),
719
+ secondEmail: z.string().optional().describe("备用邮箱"),
720
+ note: z.string().optional().describe("备注"),
721
+ loginAt: z.string().optional().describe("登录时间 ISO(不传则首次记录时自动 now)"),
722
+ }),
723
+ async ({ accountIdx, ...login } = {}) => {
724
+ try {
725
+ return toToolResult(profileStore.setLogin("chrome", accountIdx, login));
726
+ } catch (e) {
727
+ return toToolResult({ error: e.message }, { isError: true });
728
+ }
729
+ },
730
+ { tag: "Chrome" }
731
+ );
732
+
733
+ registerTool(
734
+ "chrome_profile_login_rm",
735
+ "移除某 Chrome profile 的某平台登录记录",
736
+ z.object({
737
+ accountIdx: z.number().describe("账户索引"),
738
+ platform: z.string().describe("平台"),
739
+ }),
740
+ async ({ accountIdx, platform } = {}) => {
741
+ try {
742
+ return toToolResult(profileStore.removeLogin("chrome", accountIdx, platform));
743
+ } catch (e) {
744
+ return toToolResult({ error: e.message }, { isError: true });
745
+ }
746
+ },
747
+ { tag: "Chrome" }
748
+ );
749
+
750
+ registerTool(
751
+ "chrome_profile_logins",
752
+ "列出某 Chrome profile 已记录的平台登录",
753
+ z.object({ accountIdx: z.number().describe("账户索引") }),
754
+ async ({ accountIdx } = {}) => {
755
+ try {
756
+ return toToolResult(profileStore.listLogins("chrome", accountIdx));
757
+ } catch (e) {
758
+ return toToolResult({ error: e.message }, { isError: true });
665
759
  }
666
- data[key] = {
667
- ...data[key],
668
- proxy: typeof proxy === "string" ? proxy : "",
669
- };
670
- writePrivateChromeConfig(data);
671
- return toToolResult({ success: true, profileKey: key, privateConfig: data[key] });
672
760
  },
673
761
  { tag: "Chrome" }
674
762
  );
@@ -696,7 +784,7 @@ function registerChromeTools(registerTool) {
696
784
  }),
697
785
  async ({ accountIdx, note, accounts } = {}) => {
698
786
  const data = readPrivateChromeConfig();
699
- const key = `account_${accountIdx}`;
787
+ const key = `profile_${accountIdx}`;
700
788
  if (!data[key]) {
701
789
  return toToolResult({ error: `Missing chrome.json entry: ${key}` }, { isError: true });
702
790
  }
@@ -775,7 +863,7 @@ function registerChromeTools(registerTool) {
775
863
  const cfg = getPrivateChromeEntryByAccountIdx(accountIdx);
776
864
  const { debuggerPort: port } = resolveChromeDebuggerPort(accountIdx, {
777
865
  registry,
778
- chromeConfig: cfg ? { [`account_${accountIdx}`]: cfg.entry } : null,
866
+ chromeConfig: cfg ? { [`profile_${accountIdx}`]: cfg.entry } : null,
779
867
  });
780
868
 
781
869
  if (!port) {
@@ -813,7 +901,7 @@ function registerChromeTools(registerTool) {
813
901
  const cfg = getPrivateChromeEntryByAccountIdx(accountIdx);
814
902
  const { debuggerPort: port } = resolveChromeDebuggerPort(accountIdx, {
815
903
  registry,
816
- chromeConfig: cfg ? { [`account_${accountIdx}`]: cfg.entry } : null,
904
+ chromeConfig: cfg ? { [`profile_${accountIdx}`]: cfg.entry } : null,
817
905
  });
818
906
 
819
907
  if (!port) {
@@ -841,6 +929,54 @@ function registerChromeTools(registerTool) {
841
929
  },
842
930
  { tag: "Chrome" }
843
931
  );
932
+
933
+ registerTool(
934
+ "chrome_probe_ip",
935
+ "探测某 Chrome profile 当前出口 IP + 地区(经其配置代理,用临时 Electron session,无需启动 Chrome),写入 config 并盖探测时间;可重复探测",
936
+ z.object({ accountIdx: z.number().describe("账户索引") }),
937
+ async ({ accountIdx }) => {
938
+ try {
939
+ const { session } = require("electron");
940
+ const view = profileStore.getProfile("chrome", accountIdx);
941
+ const rules = profileStore.proxyRules(view && view.proxy);
942
+ const ses = session.fromPartition(`chrome-ipprobe-${accountIdx}`, { cache: false });
943
+ await ses.setProxy({ proxyRules: rules || "direct://" });
944
+ const info = await probeIpViaSession(ses);
945
+ const saved = profileStore.setIpInfo("chrome", accountIdx, info);
946
+ return toToolResult({ accountIdx, backend: "chrome", proxy: rules || null, ipInfo: saved.ipInfo });
947
+ } catch (e) {
948
+ return toToolResult({ error: e.message }, { isError: true });
949
+ }
950
+ },
951
+ { tag: "Chrome" }
952
+ );
953
+
954
+ registerTool(
955
+ "chrome_detect_logins",
956
+ "探测指定 Chrome profile 当前 session 登录了哪些站点:读取全部 cookie 按域名归并,标记带会话 cookie 的域名(cookie 在≠一定登录,仅强信号;profile 需在运行)",
957
+ z.object({ accountIdx: z.number().describe("账户索引") }),
958
+ async ({ accountIdx }) => {
959
+ const registry = getChromeRuntimeRegistry();
960
+ const cfg = getPrivateChromeEntryByAccountIdx(accountIdx);
961
+ const { debuggerPort: port } = resolveChromeDebuggerPort(accountIdx, {
962
+ registry,
963
+ chromeConfig: cfg ? { [`profile_${accountIdx}`]: cfg.entry } : null,
964
+ });
965
+ if (!port) {
966
+ return toToolResult(
967
+ { error: `Missing debuggerPort for accountIdx=${accountIdx}(profile 未启动?先 launch)` },
968
+ { isError: true }
969
+ );
970
+ }
971
+ try {
972
+ const { cookies } = await callCdp({ debuggerPort: port, method: "Storage.getCookies", params: {} });
973
+ return toToolResult({ accountIdx, backend: "chrome", ...summarizeCookieLogins(cookies) });
974
+ } catch (error) {
975
+ return toToolResult({ error: error.message }, { isError: true });
976
+ }
977
+ },
978
+ { tag: "Chrome" }
979
+ );
844
980
  }
845
981
 
846
982
  module.exports = registerChromeTools;
@@ -0,0 +1,25 @@
1
+ const { z } = require("zod");
2
+
3
+ // get_device_info — return this machine's identity + detected network/locale
4
+ // info, read from global.json (deviceInfo block + deviceId). Instant, no
5
+ // network: the egress-IP/region/system-language detection runs at startup
6
+ // (src/main.js → cloud-client.detectAndPersistDeviceInfo) and persists here.
7
+ // The cicy-code app SPA calls this over electronRPC and forwards it on the
8
+ // chat-WS register so the cicy-code backend (and agent-webpage `clients`) can
9
+ // see each desktop client's ip / region / system language / deviceId.
10
+ module.exports = (registerTool) => {
11
+ registerTool(
12
+ "get_device_info",
13
+ "返回本机 deviceId + 出口公网 IP + IP 所在地区 + 系统语言(读 global.json 的 deviceInfo,秒回,不发网络请求)",
14
+ z.object({}),
15
+ async () => {
16
+ try {
17
+ const info = require("../cloud/cloud-client").getDeviceInfo();
18
+ return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }] };
19
+ } catch (error) {
20
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
21
+ }
22
+ },
23
+ { tag: "Device" }
24
+ );
25
+ };
@@ -13,9 +13,11 @@ module.exports = [
13
13
  require("./system-tools"),
14
14
  require("./automation-tools"),
15
15
  require("./account-tools"),
16
+ require("./device-tools"),
16
17
  require("./download-tools"),
17
18
  require("./ipc-bridge"),
18
19
  require("./hook-gemini"),
19
20
  require("./chrome-tools"),
21
+ require("./tab-browser-tools"),
20
22
  require("./list-tools"),
21
23
  ];