cicy-desktop 2.1.342 → 2.1.358

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 (48) hide show
  1. package/package.json +1 -1
  2. package/scripts/build-homepage.cjs +16 -1
  3. package/src/app-updater.js +30 -0
  4. package/src/backends/homepage-preload.js +3 -1
  5. package/src/backends/homepage-react/assets/index-Buyk0RlW.js +655 -0
  6. package/src/backends/homepage-react/assets/index-CVk0uc2g.css +1 -0
  7. package/src/backends/homepage-react/index.html +5 -3
  8. package/src/backends/homepage-react/panel/facebook-matrix.html +305 -87
  9. package/src/backends/homepage-react/panel/ipcheck.html +112 -0
  10. package/src/backends/homepage-react/panel/matrix.html +595 -0
  11. package/src/backends/homepage-react/panel/telegram-matrix.html +96 -22
  12. package/src/backends/homepage-react/panel/tiktok-matrix.html +599 -0
  13. package/src/backends/hub-client.js +108 -2
  14. package/src/backends/sidecar-ipc.js +33 -6
  15. package/src/main.js +67 -11
  16. package/src/sidecar/host-mihomo.js +47 -2
  17. package/src/tabbrowser/facebook-identity.js +12 -1
  18. package/src/tabbrowser/facebook-matrix.html +305 -87
  19. package/src/tabbrowser/fb-login.js +115 -0
  20. package/src/tabbrowser/newtab-protocol.js +6 -1
  21. package/src/tabbrowser/panel-cells.js +14 -1
  22. package/src/tabbrowser/telegram-matrix.html +96 -22
  23. package/src/tabbrowser/telegram-web-preferences.js +5 -1
  24. package/src/tabbrowser/tiktok-matrix.html +305 -87
  25. package/src/tools/tab-browser-tools.js +21 -100
  26. package/src/tray.js +22 -0
  27. package/test/bootstrap-failure-reporting.test.js +1 -1
  28. package/test/facebook-identity.test.js +3 -1
  29. package/test/facebook-matrix.test.js +2 -1
  30. package/test/telegram-matrix.test.js +2 -2
  31. package/test/tiktok-matrix.test.js +2 -1
  32. package/tools/arm-fblogin.js +32 -0
  33. package/tools/wsd-ws +114 -0
  34. package/workers/render/index.html +3 -1
  35. package/workers/render/package.json +2 -3
  36. package/workers/render/public/panel/facebook-matrix.html +305 -87
  37. package/workers/render/public/panel/ipcheck.html +112 -0
  38. package/workers/render/public/panel/matrix.html +595 -0
  39. package/workers/render/public/panel/telegram-matrix.html +96 -22
  40. package/workers/render/public/panel/tiktok-matrix.html +599 -0
  41. package/workers/render/src/App.css +4 -0
  42. package/workers/render/src/App.jsx +917 -52
  43. package/workers/render/stamp-panel-version.mjs +16 -0
  44. package/workers/render/worker.js +1 -1
  45. package/src/backends/homepage-react/assets/index-C6Zrdgc0.css +0 -1
  46. package/src/backends/homepage-react/assets/index-OG6JB73F.js +0 -411
  47. package/src/tabbrowser/cicy-code-tab-restore.js +0 -10
  48. package/test/cicy-code-tab-restore.test.js +0 -21
@@ -3,6 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <meta name="pv" content="7aad42d12a05">
6
7
  <title>Telegram 矩阵</title>
7
8
  <style>
8
9
  :root { color-scheme: dark; --bg:#0A0A0A; --cell:#141416; --head:#1a1a1c; --line:rgba(255,255,255,.08); --fg:#e6e6e9; --muted:#8b8b92; --accent:#3b82f6; --hover:rgba(255,255,255,.06); font-family: Inter, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; }
@@ -45,6 +46,7 @@
45
46
  .line2 .phone { flex: none; color: #b4b4bb; font: 11px ui-monospace, monospace; white-space: nowrap; }
46
47
  .line2 .phone.none { display: none; }
47
48
  button.code { background: #1f6feb; padding: 3px 8px; font-size: 11px; }
49
+ button.code.on { background: #c0142e; }
48
50
  .ghead .phone { color: #b4b4bb; font: 11px ui-monospace, monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
49
51
  /* 代理设置弹窗 */
50
52
  .modal { position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,.55); }
@@ -84,32 +86,35 @@
84
86
  /* 批量登录:未登录的会话一次全部铺开(最多 10 格),每格一个独立 BrowserView */
85
87
  #grid-title { display: none; width: 100%; margin-bottom: 6px; align-items: center; justify-content: space-between; gap: 8px; color: #b4b4bb; font-size: 12px; }
86
88
  #grid-title b { color: var(--fg); font-size: 13px; }
87
- #grid { display: none; width: 100%; flex: 1 1 auto; min-height: 0; gap: 8px; grid-auto-rows: minmax(0, 1fr); }
89
+ #grid { display: none; width: 100%; flex: 1 1 auto; min-height: 0; gap: 8px; flex-direction: row; flex-wrap: wrap; align-content: flex-start; overflow-x: hidden; overflow-y: auto; }
88
90
  /* 批量模式 = 整页只有 webview:侧栏整个隐藏,顶部一条细工具栏,其余全是格子 */
89
91
  body.batch { grid-template-columns: 1fr; }
90
92
  body.batch aside { display: none; }
91
93
  main.batch { padding: 8px 10px 10px; }
92
94
  main.batch #preview-title, main.batch #phone { display: none; }
93
95
  main.batch #grid-title { display: flex; }
94
- main.batch #grid { display: grid; }
95
- .gcell { display: flex; flex-direction: column; min-width: 0; min-height: 0; border: 1px solid var(--line); border-radius: 14px; background: var(--cell); overflow: hidden; }
96
+ main.batch #grid { display: flex; }
97
+ .gcell { display: flex; flex-direction: column; flex: none; width: 360px; height: 50vh; min-height: 0; border: 1px solid var(--line); border-radius: 14px; background: var(--cell); overflow: hidden; }
96
98
  .gcell.done { border-color: rgba(63,185,80,.6); }
97
99
  .gcell.failed { border-color: rgba(255,92,92,.6); }
98
100
  .ghead { flex: none; height: 30px; padding: 0 8px; display: flex; align-items: center; gap: 6px; font-size: 12px; border-bottom: 1px solid var(--line); background: var(--head); min-width: 0; }
99
101
  .ghead .name { font-size: 12px; }
100
102
  .ghead .st { margin-left: auto; }
101
103
  .gcell.done .ghead .st { color: #3fb950; font-weight: 600; }
102
- .gbody { flex: 1 1 auto; min-height: 0; background: #fff; }
104
+ .gbody { flex: 1 1 auto; min-height: 0; width: 100%; background: #fff; }
103
105
  #grid-empty { grid-column: 1 / -1; display: grid; place-items: center; color: var(--muted); }
104
106
  .spinner { width: 28px; height: 28px; border: 3px solid rgba(255,255,255,.12); border-top-color: var(--accent); border-radius: 50%; animation: spin .9s linear infinite; margin: 0 auto 12px; }
105
107
  @keyframes spin { to { transform: rotate(360deg); } }
108
+ /* 机器 id 徽章:机器多了一眼认出这是哪台。列表/批量两种模式都显示。 */
109
+ .mid{display:inline-block;margin-right:8px;padding:2px 9px;border-radius:7px;font:700 13px ui-monospace,monospace;
110
+ background:#1f6feb;color:#fff;letter-spacing:.5px;vertical-align:middle;box-shadow:0 1px 4px rgba(0,0,0,.4)}
106
111
  </style>
107
112
  </head>
108
113
  <body>
109
114
  <aside>
110
115
  <header>
111
- <h1>Telegram 矩阵 <small id="count"></small></h1>
112
- <div class="actions"><button class="ghost" id="batch-login" title="把所有未登录的 Profile 一次全部打开(最多 10 个)">批量登录</button><button id="add-profile" title="新建一个独立会话(persist:sandbox-N)">+ 添加 Profile</button></div>
116
+ <h1><span id="mid-list" class="mid" hidden></span>Telegram 矩阵 <small id="count"></small></h1>
117
+ <div class="actions"><button class="ghost" id="batch-login" title="平铺所有 Profile 的会话到网格(最多 10 个)">批量网格</button><button id="add-profile" title="新建一个独立会话(persist:sandbox-N)">+ 添加 Profile</button></div>
113
118
  </header>
114
119
  <div class="list">
115
120
  <div id="rows"></div>
@@ -119,13 +124,13 @@
119
124
  </aside>
120
125
  <main>
121
126
  <div id="grid-title">
122
- <div class="left"><b>批量登录</b><span id="grid-count"></span></div>
123
- <div class="actions"><button class="ghost" id="grid-reload" title="重新加载所有格子">全部重载</button><button class="ghost" id="grid-exit">退出批量</button></div>
127
+ <div class="left"><span id="mid-batch" class="mid" hidden></span><b>批量登录</b><span id="grid-count"></span></div>
128
+ <div class="actions"><button class="ghost" id="grid-reload" title="重新加载所有格子">全部重载</button><button id="grid-add-profile" title="新建一个独立会话(persist:sandbox-N)">+ 添加 Profile</button></div>
124
129
  </div>
125
130
  <div id="grid"></div>
126
131
  <div id="preview-title">
127
132
  <div class="left"><span class="dot" id="sel-dot"></span><span id="selected-name">未选择 Profile</span><span id="status"></span></div>
128
- <div class="actions"><button class="code" id="open-code" title="在该 Profile 的 tab 里打开接码 URL">接码</button><button class="ghost" id="probe-ip" title="经该 Profile 的代理探测出口 IP / 地区">测 IP</button><button class="ghost" id="reload" title="重新加载 Telegram Web">重新加载</button></div>
133
+ <div class="actions"><button class="code" id="open-code" title="在格子上叠加/关闭接码 webview(同代理出口)">接码</button><button class="ghost" id="probe-ip" title="经该 Profile 的代理探测出口 IP / 地区">测 IP</button><button class="ghost" id="reload" title="重新加载 Telegram Web">重新加载</button></div>
129
134
  </div>
130
135
  <div id="phone">
131
136
  <div id="phone-preview"></div>
@@ -169,6 +174,7 @@
169
174
  let profiles = [];
170
175
  let selected = null;
171
176
  const openedProfiles = new Set(); // 打开过的会话保留在后台(登录态不丢),只有选中的贴到预览区
177
+ const codeOpen = new Set(); // 已叠开接码 overlay 的 accountIdx(同 partition 的接码 webview 覆盖在格子上)
172
178
  const cellState = new Map(); // accountIdx -> { loading, failed, error, url, title }
173
179
  const rows = new Map(); // accountIdx -> row element
174
180
  const BATCH_MAX = 10; // 一次最多铺开多少个登录格
@@ -180,6 +186,14 @@
180
186
  const gcells = new Map(); // accountIdx -> { el, body }
181
187
 
182
188
  const esc = (value) => String(value || '').replace(/[&<>"']/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
189
+ // 机器 id:app 打开面板时把本机 id 拼进 URL(?team=xs-3007)。两处徽章都填上。
190
+ (function(){ try {
191
+ const t = new URLSearchParams(location.search).get('team');
192
+ if (t) for (const id of ['mid-list','mid-batch']) {
193
+ const el = document.getElementById(id); if (el) { el.textContent = t; el.hidden = false; el.title = '本机: ' + t; }
194
+ }
195
+ if (t) document.title = t + ' · Telegram 矩阵';
196
+ } catch (e) {} })();
183
197
  const cellId = (idx) => `telegram-preview-${idx}`;
184
198
  const stateOf = (idx) => cellState.get(Number(idx)) || {};
185
199
  const dotClass = (st) => st.loading ? 'loading' : st.failed ? 'failed' : (st.url ? 'ok' : '');
@@ -215,10 +229,22 @@
215
229
  el.classList.toggle('none', !ph); el.textContent = ph; el.title = codeUrlOf(p) ? `接码:${codeUrlOf(p)}` : '';
216
230
  const g = gcells.get(p.accountIdx); if (g) { g.el.querySelector('.phone').textContent = ph; g.el.querySelector('.code').disabled = !codeUrlOf(p); }
217
231
  }
218
- async function openCode(p) {
219
- if (!codeUrlOf(p)) { flash(`#${p.accountIdx} 还没有接码 URL,点 ⚙ 填写`, 4000); return; }
220
- try { await window.panelAPI.openCodeUrl(p.accountIdx, codeUrlOf(p)); flash(`#${p.accountIdx} 接码页已在 Profile 窗口打开`); }
221
- catch (e) { flash(e.message || String(e), 6000); }
232
+ // 接码 = 在当前格子的 Telegram webview 上方叠一个同尺寸、同 partition(persist:sandbox-idx,
233
+ // 走该 profile 的代理出口)、更高 z 层的接码 webview。再点一次关闭。与 tg-autologin 自动化里
234
+ // 的 overlay 完全一致的呈现,只是重载/关闭放在格子 header(BrowserView 之上无法放页面按钮)。
235
+ function openCode(p) {
236
+ if (!codeUrlOf(p)) { flash(`#${p.accountIdx} 还没有接码 URL,点 ⚙ 填写`, 4000); return; }
237
+ if (!openedProfiles.has(p.accountIdx)) { openedProfiles.add(p.accountIdx); cellState.set(p.accountIdx, { loading: true }); }
238
+ if (codeOpen.has(p.accountIdx)) { codeOpen.delete(p.accountIdx); flash(`#${p.accountIdx} 已关闭接码`); }
239
+ else { codeOpen.add(p.accountIdx); flash(`#${p.accountIdx} 接码已叠加在格子上`); }
240
+ updateCodeButtons(p.accountIdx);
241
+ syncPreview();
242
+ }
243
+ function updateCodeButtons(idx) {
244
+ const on = codeOpen.has(idx);
245
+ const g = gcells.get(idx);
246
+ if (g) { const b = g.el.querySelector('.code'); if (b) { b.classList.toggle('on', on); b.textContent = on ? '关闭接码' : '接码'; } }
247
+ if (selected && selected.accountIdx === idx) { const b = document.getElementById('open-code'); if (b) { b.classList.toggle('on', on); b.textContent = on ? '关闭接码' : '接码'; } }
222
248
  }
223
249
  function renderIdentity(p) {
224
250
  renderPhone(p);
@@ -246,15 +272,21 @@
246
272
  if (!window.panelAPI) return;
247
273
  const rect = preview.getBoundingClientRect();
248
274
  const box = (r) => ({ x: r.x, y: r.y, w: r.width, h: r.height });
249
- const cells = [...openedProfiles].map((profileId) => {
275
+ const cells = []; const overlays = [];
276
+ for (const profileId of openedProfiles) {
250
277
  let visible = false, r = null;
251
278
  if (batchMode) {
252
279
  const g = gcells.get(profileId);
253
280
  if (g && batch.has(profileId)) { visible = true; r = box(g.body.getBoundingClientRect()); }
254
281
  } else if (selected && profileId === selected.accountIdx) { visible = true; r = box(rect); }
255
- return { id: cellId(profileId), url: TELEGRAM_URL, profile: profileId, visible, rect: r };
256
- });
257
- try { window.panelAPI.sync(cells); } catch (e) {}
282
+ cells.push({ id: cellId(profileId), url: TELEGRAM_URL, profile: profileId, visible, rect: r });
283
+ // 接码叠加层:同 rect、同 profile(=同 partition/代理),排在 tg 格子之后 → attach 更晚 → 叠最上层
284
+ if (visible && codeOpen.has(profileId)) {
285
+ const p = profiles.find((x) => x.accountIdx === profileId); const cu = p && codeUrlOf(p);
286
+ if (cu) overlays.push({ id: 'code-' + profileId, url: cu, profile: profileId, visible: true, rect: r });
287
+ }
288
+ }
289
+ try { window.panelAPI.sync(cells.concat(overlays)); } catch (e) {}
258
290
  }
259
291
 
260
292
  function describeError(p, st) {
@@ -272,6 +304,7 @@
272
304
  if (selected && st.failed) errDetail.innerHTML = describeError(selected, st);
273
305
  selDot.className = `dot ${selected ? dotClass(st) : ''}`;
274
306
  if (selected && !statusEl.dataset.sticky) statusEl.textContent = stateText(st, true);
307
+ if (selected) updateCodeButtons(selected.accountIdx);
275
308
  }
276
309
 
277
310
  function renderRowState(idx) {
@@ -289,7 +322,7 @@
289
322
  function buildGridCell(p) {
290
323
  const el = document.createElement('div');
291
324
  el.className = 'gcell'; el.dataset.id = String(p.accountIdx);
292
- el.innerHTML = `<div class="ghead"><span class="dot"></span><span class="name">${esc(p.name)}</span><span class="chip">#${p.accountIdx}</span><span class="phone">${esc(phoneOf(p))}</span><span class="st"></span><button class="code" title="在该 Profile 的 tab 里打开接码 URL">接码</button><button class="icon reload" title="重新加载">⟳</button></div><div class="gbody"></div>`;
325
+ el.innerHTML = `<div class="ghead"><span class="dot"></span><span class="name">${esc(p.name)}</span><span class="chip">#${p.accountIdx}</span><span class="phone">${esc(phoneOf(p))}</span><span class="st"></span><button class="code" title="在格子上叠加/关闭接码 webview(同代理出口)">接码</button><button class="icon reload" title="重新加载">⟳</button></div><div class="gbody"></div>`;
293
326
  el.querySelector('.reload').onclick = () => reloadProfile(p.accountIdx);
294
327
  el.querySelector('.code').onclick = () => openCode(p);
295
328
  el.querySelector('.code').disabled = !codeUrlOf(p);
@@ -304,11 +337,13 @@
304
337
  g.el.classList.toggle('failed', !!st.failed && !st.loading);
305
338
  g.el.querySelector('.dot').className = `dot ${dotClass(st)}`;
306
339
  g.el.querySelector('.st').textContent = done ? '✓ 已登录' : stateText(st, true);
340
+ updateCodeButtons(idx);
307
341
  if (batchMode) { const n = [...batch].filter((i) => { const q = profiles.find((x) => x.accountIdx === i); return q && hasIdentity(q); }).length; gridCount.textContent = `${batch.size} 个会话 · ${n} 已登录`; }
308
342
  }
309
343
  function enterBatch() {
310
- const todo = profiles.filter((p) => !hasIdentity(p)).slice(0, BATCH_MAX);
311
- if (!todo.length) { flash('没有未登录的 Profile'); return; }
344
+ // 默认网格 = 平铺全部 profile 的会话(不再只挑未登录的);仍受 BATCH_MAX 上限保护。
345
+ const todo = profiles.slice(0, BATCH_MAX);
346
+ if (!todo.length) { flash('没有 Profile'); return; }
312
347
  batch.clear(); gridEl.innerHTML = ''; gcells.clear();
313
348
  for (const p of todo) {
314
349
  batch.add(p.accountIdx);
@@ -319,17 +354,18 @@
319
354
  gridEl.style.gridTemplateColumns = `repeat(${gridCols(todo.length)}, minmax(0, 1fr))`;
320
355
  batchMode = true; mainEl.classList.add('batch'); document.body.classList.add('batch'); document.getElementById('batch-login').textContent = '退出批量';
321
356
  render();
357
+ syncPreview(); // 后台 tab 里 rAF 不会触发,先直接同步一次把格子建出来
322
358
  requestAnimationFrame(syncPreview);
323
359
  setTimeout(pullStates, 300);
324
360
  }
325
361
  function exitBatch() {
326
362
  if (!batchMode) return;
327
- batchMode = false; mainEl.classList.remove('batch'); document.body.classList.remove('batch'); document.getElementById('batch-login').textContent = '批量登录';
363
+ batchMode = false; mainEl.classList.remove('batch'); document.body.classList.remove('batch'); document.getElementById('batch-login').textContent = '批量网格';
328
364
  batch.clear(); gcells.clear(); gridEl.innerHTML = '';
329
365
  render(); renderPreviewState(); syncPreview(); // 会话留在后台,登录态不丢
330
366
  }
331
367
  document.getElementById('batch-login').onclick = () => (batchMode ? exitBatch() : enterBatch());
332
- document.getElementById('grid-exit').onclick = exitBatch;
368
+ document.getElementById('grid-add-profile').onclick = () => document.getElementById('add-profile').click();
333
369
  document.getElementById('grid-reload').onclick = () => { for (const idx of batch) reloadProfile(idx); };
334
370
  new ResizeObserver(() => requestAnimationFrame(syncPreview)).observe(gridEl);
335
371
 
@@ -347,6 +383,8 @@
347
383
  }
348
384
 
349
385
  function reloadProfile(idx) {
386
+ // 接码 overlay 开着时,重载指向接码 webview(用户看到的最上层就是它)
387
+ if (codeOpen.has(idx)) { try { window.panelAPI.reload('code-' + idx); } catch (e) {} return; }
350
388
  const fresh = !openedProfiles.has(idx);
351
389
  openedProfiles.add(idx);
352
390
  cellState.set(idx, { loading: true }); renderRowState(idx);
@@ -511,11 +549,47 @@
511
549
 
512
550
  new ResizeObserver(() => requestAnimationFrame(syncPreview)).observe(preview);
513
551
  window.addEventListener('resize', syncPreview);
552
+ document.addEventListener('visibilitychange', () => { if (!document.hidden) { syncPreview(); setTimeout(syncPreview, 500); setTimeout(pullStates, 800); } });
514
553
  try { window.panelAPI.onCellState(applyCellState); } catch (e) {}
515
554
  pullStates({ adopt: true })
516
555
  .catch(() => {})
517
556
  .then(() => loadProfiles())
557
+ // 打开面板即默认进批量网格:把所有 profile 的 webview 平铺出来。
558
+ // 崩溃循环保护:若上次开机自动批量后没在几秒内清除标记(说明渲染进程崩溃重载了),
559
+ // 这次跳过自动批量、停在列表,避免弱机反复「崩溃→自动批量→再崩」的刷新循环。
560
+ .then(() => {
561
+ try {
562
+ const K = 'tg-matrix-autobatch-pending';
563
+ let prev = null; try { prev = localStorage.getItem(K); } catch (e) {}
564
+ if (prev && (Date.now() - Number(prev)) < 20000) {
565
+ try { localStorage.removeItem(K); } catch (e) {}
566
+ flash('上次批量疑似导致崩溃,已跳过自动批量。点「批量网格」可手动打开。', 6000);
567
+ return;
568
+ }
569
+ try { localStorage.setItem(K, String(Date.now())); } catch (e) {}
570
+ enterBatch();
571
+ setTimeout(() => { try { localStorage.removeItem(K); } catch (e) {} }, 8000);
572
+ } catch (e) {}
573
+ })
518
574
  .catch((e) => { flash(e.message || String(e), 8000); render(); });
575
+
576
+ // 自动更新:构建时给本页打了内容版本号(<meta name="pv">)。每 60s 拉一次自身
577
+ // (同源 cicyui://panel,无跨域;cicyui handler 每次都从远端取最新),版本变了就 reload。
578
+ // 这样服务端一部署,所有开着的面板自己就切到新版,不用手动逐台重载。DEV 态不轮询。
579
+ (function autoUpdate() {
580
+ const meta = document.querySelector('meta[name="pv"]');
581
+ const PV = meta ? meta.content : '';
582
+ if (!PV || PV === 'DEV') return;
583
+ setInterval(async () => {
584
+ try {
585
+ const r = await fetch(location.href, { cache: 'no-store' });
586
+ if (!r.ok) return;
587
+ const t = await r.text();
588
+ const mm = t.match(/<meta name="pv" content="([^"]*)">/);
589
+ if (mm && mm[1] && mm[1] !== PV) location.reload();
590
+ } catch (e) {}
591
+ }, 60000);
592
+ })();
519
593
  </script>
520
594
  </body>
521
595
  </html>