cicy-desktop 2.1.280 → 2.1.285
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/package.json +1 -1
- package/scripts/windows/add-team.bat +43 -0
- package/scripts/windows/add-team.html +54 -0
- package/src/backends/homepage-react/assets/index-BzYBKzQW.js +377 -0
- package/src/backends/homepage-react/index.html +1 -1
- package/src/backends/local-teams.js +18 -4
- package/src/backends/sidecar-ipc.js +11 -1
- package/src/main.js +6 -2
- package/src/profiles/profile-store.js +29 -0
- package/src/profiles/trusted-origins-store.js +35 -5
- package/src/sidecar/docker.js +35 -3
- package/src/sidecar/wsl-docker.js +1 -1
- package/src/tabbrowser/panel-cells.js +84 -5
- package/src/tabbrowser/panel-preload.js +3 -0
- package/src/tabbrowser/telegram-identity.js +77 -0
- package/src/tabbrowser/telegram-matrix.html +152 -43
- package/src/utils/global-json.js +4 -0
- package/src/utils/rpc-guard.js +40 -9
- package/test/docker-preflight-and-always-allow.test.js +53 -0
- package/test/global-json-fresh-dir.test.js +17 -0
- package/test/local-teams-docker-token.test.js +18 -0
- package/test/profile-store-note.test.js +27 -0
- package/test/rpc-guard-deny-cooldown.test.js +18 -0
- package/test/telegram-identity.test.js +41 -0
- package/test/telegram-matrix.test.js +14 -8
- package/test/trusted-origins-underscore.test.js +11 -0
- package/workers/render/src/App.jsx +29 -7
- package/src/backends/homepage-react/assets/index-Bok65e-y.js +0 -377
|
@@ -35,14 +35,36 @@
|
|
|
35
35
|
.name { font-weight: 650; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
36
36
|
.chip { flex: none; padding: 1px 6px; border-radius: 999px; background: var(--head); color: #b4b4bb; font: 11px ui-monospace, monospace; }
|
|
37
37
|
.st { flex: none; color: var(--muted); font-size: 11px; white-space: nowrap; margin-left: auto; }
|
|
38
|
-
.line2 { display: flex; align-items: center; gap:
|
|
39
|
-
.line2 .
|
|
40
|
-
.line2 .
|
|
41
|
-
.line2 .
|
|
42
|
-
.line2 .
|
|
43
|
-
.line2
|
|
44
|
-
.line2
|
|
45
|
-
|
|
38
|
+
.line2 { display: flex; align-items: center; gap: 8px; margin-top: 4px; padding-left: 16px; font-size: 11.5px; color: var(--muted); min-width: 0; }
|
|
39
|
+
.line2 .tg { flex: none; max-width: 55%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #b4b4bb; }
|
|
40
|
+
.line2 .tg b { color: var(--fg); font-weight: 600; }
|
|
41
|
+
.line2 .tg.none { color: #6b6b72; }
|
|
42
|
+
.line2 .note { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #9a9aa2; cursor: text; outline: 0; border-radius: 4px; padding: 0 4px; margin: 0 -4px; }
|
|
43
|
+
.line2 .note.empty { color: #55555c; font-style: italic; }
|
|
44
|
+
.line2 .note:hover { color: #fff; }
|
|
45
|
+
/* 编辑态就地变成可输入,尺寸/位置与只读态完全一致,不发生位移 */
|
|
46
|
+
.line2 .note.editing { color: var(--fg); font-style: normal; background: #111113; box-shadow: 0 0 0 1px var(--accent); text-overflow: clip; }
|
|
47
|
+
.line2 .ip { flex: none; max-width: 45%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding: 0 6px; border-radius: 999px; background: var(--head); color: #b4b4bb; font: 11px ui-monospace, monospace; line-height: 16px; cursor: pointer; }
|
|
48
|
+
.line2 .ip.none { color: #6b6b72; font-family: inherit; }
|
|
49
|
+
.line2 .ip.busy { color: #f0b429; }
|
|
50
|
+
.line2 .ip.bad { color: #ff8a8a; }
|
|
51
|
+
.line2 .ip:hover { color: #fff; }
|
|
52
|
+
.cfg { display: none; margin: 8px 0 2px 16px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--head); }
|
|
53
|
+
.row.cfg-open .cfg { display: block; }
|
|
54
|
+
.row.cfg-open .gear { color: var(--accent); }
|
|
55
|
+
.cfg .f { display: flex; align-items: center; gap: 6px; }
|
|
56
|
+
.cfg .f + .f { margin-top: 6px; }
|
|
57
|
+
.cfg label { flex: none; width: 34px; font-size: 11px; color: var(--muted); }
|
|
58
|
+
.cfg input { flex: 1; min-width: 0; height: 26px; border: 1px solid var(--line); border-radius: 6px; padding: 0 8px; background: #111113; color: var(--fg); font: 11.5px ui-monospace, monospace; outline: 0; }
|
|
59
|
+
.cfg input:focus { border-color: var(--accent); }
|
|
60
|
+
.cfg .hint { font-size: 11px; color: var(--muted); padding-left: 40px; }
|
|
61
|
+
.cfg .hint b { color: #b4b4bb; font-weight: 500; }
|
|
62
|
+
.cfg .danger { display: flex; align-items: center; gap: 6px; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line); font-size: 11px; color: var(--muted); }
|
|
63
|
+
.cfg .danger .sp { flex: 1; }
|
|
64
|
+
.cfg .confirm { display: none; align-items: center; gap: 6px; margin-top: 8px; padding: 6px 8px; border-radius: 6px; background: rgba(255,92,92,.12); color: #ffb3b3; font-size: 11.5px; }
|
|
65
|
+
.cfg .confirm.on { display: flex; }
|
|
66
|
+
.cfg .confirm .sp { flex: 1; }
|
|
67
|
+
button.del { background: #b3261e; }
|
|
46
68
|
.row-err { padding: 4px 0 0 16px; color: #ff8a8a; font-size: 11px; display: none; }
|
|
47
69
|
.row-err.on { display: block; }
|
|
48
70
|
#empty { display: none; padding: 40px 24px; color: var(--muted); text-align: center; line-height: 1.8; }
|
|
@@ -92,7 +114,7 @@
|
|
|
92
114
|
<div>
|
|
93
115
|
<h2>无法连接 Telegram</h2>
|
|
94
116
|
<p id="err-detail"></p>
|
|
95
|
-
<div class="btns"><button id="err-retry">重试</button><button class="ghost" id="err-
|
|
117
|
+
<div class="btns"><button id="err-retry">重试</button><button class="ghost" id="err-cfg">代理设置</button></div>
|
|
96
118
|
</div>
|
|
97
119
|
</div>
|
|
98
120
|
</div>
|
|
@@ -120,6 +142,43 @@
|
|
|
120
142
|
const stateOf = (idx) => cellState.get(Number(idx)) || {};
|
|
121
143
|
const dotClass = (st) => st.loading ? 'loading' : st.failed ? 'failed' : (st.url ? 'ok' : '');
|
|
122
144
|
const stateText = (st, opened) => st.loading ? '加载中…' : st.failed ? '连接失败' : st.url ? '已加载' : (opened ? '' : '未打开');
|
|
145
|
+
const fmtAgo = (iso) => { const t = Date.parse(iso || ''); if (!t) return ''; const m = Math.round((Date.now() - t) / 60000); return m < 1 ? '刚刚' : m < 60 ? `${m} 分钟前` : m < 1440 ? `${Math.round(m / 60)} 小时前` : `${Math.round(m / 1440)} 天前`; };
|
|
146
|
+
function renderIp(p, mode) {
|
|
147
|
+
const row = rows.get(p.accountIdx); if (!row) return;
|
|
148
|
+
const el = row.querySelector('.ip');
|
|
149
|
+
el.className = 'ip';
|
|
150
|
+
if (mode === 'busy') { el.classList.add('busy'); el.textContent = '测 IP…'; el.title = ''; return; }
|
|
151
|
+
if (mode && mode.error) { el.classList.add('bad'); el.textContent = 'IP 探测失败'; el.title = mode.error + '(点击重试)'; return; }
|
|
152
|
+
const info = p.ipInfo;
|
|
153
|
+
if (info && info.ip) {
|
|
154
|
+
el.textContent = info.area ? `${info.area} · ${info.ip}` : info.ip;
|
|
155
|
+
el.title = `出口 IP ${info.ip}${info.area ? ',' + info.area : ''},${fmtAgo(info.probedAt) || '未知时间'}探测(点击重测)`;
|
|
156
|
+
} else { el.classList.add('none'); el.textContent = '测 IP'; el.title = '经该 Profile 的代理探测出口 IP / 地区'; }
|
|
157
|
+
}
|
|
158
|
+
async function probeIp(p) {
|
|
159
|
+
renderIp(p, 'busy');
|
|
160
|
+
try {
|
|
161
|
+
const r = await window.panelAPI.probeIp(p.accountIdx);
|
|
162
|
+
if (r && r.ipInfo) { p.ipInfo = r.ipInfo; renderIp(p); }
|
|
163
|
+
else renderIp(p, { error: (r && r.error) || '探测失败' });
|
|
164
|
+
} catch (e) { renderIp(p, { error: e.message || String(e) }); }
|
|
165
|
+
}
|
|
166
|
+
// 身份:优先用本次会话探到的(cellState.identity),否则用 profile 里存的(上次登录)。
|
|
167
|
+
const identityOf = (p) => stateOf(p.accountIdx).identity || p.telegram || null;
|
|
168
|
+
function renderIdentity(p) {
|
|
169
|
+
const row = rows.get(p.accountIdx); if (!row) return;
|
|
170
|
+
const el = row.querySelector('.tg');
|
|
171
|
+
const it = identityOf(p);
|
|
172
|
+
if (it && (it.username || it.displayName || it.phone)) {
|
|
173
|
+
el.classList.remove('none');
|
|
174
|
+
el.innerHTML = (it.username ? `<b>@${esc(it.username)}</b>` : '') + (it.displayName ? `${it.username ? ' · ' : ''}${esc(it.displayName)}` : '') + (!it.username && !it.displayName && it.phone ? `+${esc(it.phone)}` : '');
|
|
175
|
+
el.title = [it.username && `@${it.username}`, it.displayName, it.phone && `+${it.phone}`].filter(Boolean).join(' ');
|
|
176
|
+
} else {
|
|
177
|
+
el.classList.add('none');
|
|
178
|
+
el.textContent = openedProfiles.has(p.accountIdx) && stateOf(p.accountIdx).url ? '未登录' : '—';
|
|
179
|
+
el.title = '';
|
|
180
|
+
}
|
|
181
|
+
}
|
|
123
182
|
|
|
124
183
|
function flash(text, ms = 2500) {
|
|
125
184
|
statusEl.textContent = text; statusEl.dataset.sticky = '1';
|
|
@@ -143,8 +202,8 @@
|
|
|
143
202
|
function describeError(p, st) {
|
|
144
203
|
const e = st.error || {};
|
|
145
204
|
const desc = e.description ? `<code>${esc(e.description)}</code>` : '';
|
|
146
|
-
if (p.proxy) return `代理 <code>${esc(p.proxy)}</code> 不可用或无法访问 Telegram。${desc}<br
|
|
147
|
-
return `当前为直连,网络无法访问 Telegram。${desc}<br
|
|
205
|
+
if (p.proxy) return `代理 <code>${esc(p.proxy)}</code> 不可用或无法访问 Telegram。${desc}<br>点 ⚙ 检查或更换该 Profile 的代理,可用「测 IP」验证。`;
|
|
206
|
+
return `当前为直连,网络无法访问 Telegram。${desc}<br>点 ⚙ 为该 Profile 设置代理。`;
|
|
148
207
|
}
|
|
149
208
|
|
|
150
209
|
function renderPreviewState() {
|
|
@@ -169,43 +228,77 @@
|
|
|
169
228
|
const row = document.createElement('div');
|
|
170
229
|
row.className = 'row'; row.dataset.id = String(p.accountIdx); row.title = `persist:sandbox-${p.accountIdx}`;
|
|
171
230
|
row.innerHTML = `
|
|
172
|
-
<div class="line1"><span class="dot"></span><span class="name">${esc(p.name)}</span><span class="chip">#${p.accountIdx}</span><span class="st"></span><button class="icon reload" title="重新加载">⟳</button></div>
|
|
173
|
-
<div class="line2"><span class="
|
|
174
|
-
|
|
231
|
+
<div class="line1"><span class="dot"></span><span class="name">${esc(p.name)}</span><span class="chip">#${p.accountIdx}</span><span class="st"></span><button class="icon gear" title="代理设置">⚙</button><button class="icon reload" title="重新加载">⟳</button></div>
|
|
232
|
+
<div class="line2"><span class="tg none">—</span><span class="note" data-role="note" title="点击编辑备注,Enter 保存 / Esc 取消" spellcheck="false"></span><span class="ip none" title="经该 Profile 的代理探测出口 IP / 地区">测 IP</span></div>
|
|
233
|
+
<div class="cfg">
|
|
234
|
+
<div class="f"><label>代理</label><input data-role="proxy" placeholder="留空 = 直连,例如 http://127.0.0.1:20001" spellcheck="false"><button class="sm save">保存</button><button class="sm ghost test">测 IP</button></div>
|
|
235
|
+
<div class="hint">保存后已打开的会话会重新加载;<b>测 IP</b> 经该代理访问 IP 服务,结果显示在上方。</div>
|
|
236
|
+
<div class="danger"><span>删除此 Profile 会一并清除它的 Telegram 登录态和缓存。</span><span class="sp"></span><button class="sm ghost rm">删除</button></div>
|
|
237
|
+
<div class="confirm"><span>确认删除 <b>${esc(p.name)} #${p.accountIdx}</b>?不可恢复。</span><span class="sp"></span><button class="sm del rm-yes">确认删除</button><button class="sm ghost rm-no">取消</button></div>
|
|
238
|
+
</div>
|
|
175
239
|
<div class="row-err"></div>`;
|
|
176
|
-
const line2 = row.querySelector('.line2');
|
|
177
|
-
const val = line2.querySelector('.val');
|
|
178
|
-
const input = line2.querySelector('[data-role="proxy"]');
|
|
179
240
|
const errEl = row.querySelector('.row-err');
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
241
|
+
const noteEl = row.querySelector('.note');
|
|
242
|
+
const showNote = () => { noteEl.textContent = p.note || '添加备注'; noteEl.classList.toggle('empty', !p.note); };
|
|
243
|
+
showNote();
|
|
244
|
+
let editing = false;
|
|
245
|
+
const startEdit = (e) => {
|
|
246
|
+
e && e.stopPropagation();
|
|
247
|
+
if (editing) return;
|
|
248
|
+
editing = true;
|
|
249
|
+
noteEl.classList.add('editing'); noteEl.classList.remove('empty');
|
|
250
|
+
noteEl.textContent = p.note || '';
|
|
251
|
+
noteEl.contentEditable = 'plaintext-only';
|
|
252
|
+
noteEl.focus();
|
|
253
|
+
const range = document.createRange(); range.selectNodeContents(noteEl);
|
|
254
|
+
const sel = getSelection(); sel.removeAllRanges(); sel.addRange(range);
|
|
255
|
+
};
|
|
256
|
+
const stopEdit = () => { editing = false; noteEl.contentEditable = 'false'; noteEl.classList.remove('editing'); showNote(); };
|
|
257
|
+
const saveNote = async () => {
|
|
258
|
+
if (!editing) return;
|
|
259
|
+
const value = noteEl.textContent.replace(/\s+/g, ' ').trim().slice(0, 500);
|
|
260
|
+
stopEdit();
|
|
261
|
+
if (value === (p.note || '')) return;
|
|
186
262
|
try {
|
|
187
|
-
const updated = await window.panelAPI.
|
|
188
|
-
p.
|
|
189
|
-
// 代理只在会话加载时生效,所以已打开的必须重载。**没打开过的不要**在这里置
|
|
190
|
-
// loading:主进程没有对应的 cell,既不会创建视图也不会有 panelcells:state 回来,
|
|
191
|
-
// 行会永远卡在「加载中…」(实测 bug)。保持「未打开」,下次打开自然用新代理。
|
|
192
|
-
if (openedProfiles.has(p.accountIdx)) {
|
|
193
|
-
reloadProfile(p.accountIdx);
|
|
194
|
-
flash(updated.proxy ? `#${p.accountIdx} 代理已应用,重新加载中` : `#${p.accountIdx} 已切换直连,重新加载中`);
|
|
195
|
-
} else {
|
|
196
|
-
flash(updated.proxy ? `#${p.accountIdx} 代理已保存,打开时生效` : `#${p.accountIdx} 已保存为直连,打开时生效`);
|
|
197
|
-
}
|
|
263
|
+
const updated = await window.panelAPI.setProfileNote(p.accountIdx, value);
|
|
264
|
+
p.note = updated.note; showNote();
|
|
198
265
|
} catch (e) { errEl.textContent = e.message || String(e); errEl.classList.add('on'); }
|
|
199
|
-
finally { btn.disabled = false; }
|
|
200
266
|
};
|
|
267
|
+
noteEl.onclick = (e) => { e.stopPropagation(); startEdit(e); };
|
|
268
|
+
noteEl.onkeydown = (e) => { e.stopPropagation(); if (e.key === 'Enter') { e.preventDefault(); saveNote(); } if (e.key === 'Escape') { e.preventDefault(); stopEdit(); } };
|
|
269
|
+
noteEl.onblur = () => saveNote();
|
|
270
|
+
// 代理设置抽屉(⚙):不常用,默认收起,列表保持干净。
|
|
271
|
+
const proxyInput = row.querySelector('[data-role="proxy"]');
|
|
272
|
+
const saveBtn = row.querySelector('.cfg .save');
|
|
273
|
+
const toggleCfg = (e) => { e.stopPropagation(); const open = !row.classList.contains('cfg-open'); row.classList.toggle('cfg-open', open); if (open) { proxyInput.value = p.proxy || ''; proxyInput.focus(); } };
|
|
274
|
+
const saveProxy = async () => {
|
|
275
|
+
saveBtn.disabled = true; errEl.classList.remove('on');
|
|
276
|
+
try {
|
|
277
|
+
const updated = await window.panelAPI.setProfileProxy(p.accountIdx, proxyInput.value.trim());
|
|
278
|
+
p.proxy = updated.proxy; p.ipInfo = null; renderIp(p);
|
|
279
|
+
if (openedProfiles.has(p.accountIdx)) { reloadProfile(p.accountIdx); flash(`#${p.accountIdx} 代理已应用,重新加载中`); }
|
|
280
|
+
else flash(`#${p.accountIdx} 代理已保存,打开时生效`);
|
|
281
|
+
row.classList.remove('cfg-open');
|
|
282
|
+
} catch (e) { errEl.textContent = e.message || String(e); errEl.classList.add('on'); }
|
|
283
|
+
finally { saveBtn.disabled = false; }
|
|
284
|
+
};
|
|
285
|
+
row.querySelector('.gear').onclick = toggleCfg;
|
|
286
|
+
row.querySelector('.cfg').onclick = (e) => e.stopPropagation();
|
|
287
|
+
saveBtn.onclick = saveProxy;
|
|
288
|
+
proxyInput.onkeydown = (e) => { e.stopPropagation(); if (e.key === 'Enter') saveProxy(); if (e.key === 'Escape') row.classList.remove('cfg-open'); };
|
|
289
|
+
row.querySelector('.cfg .test').onclick = () => probeIp(p);
|
|
290
|
+
row.querySelector('.ip').onclick = (e) => { e.stopPropagation(); probeIp(p); };
|
|
291
|
+
const confirmEl = row.querySelector('.cfg .confirm');
|
|
292
|
+
row.querySelector('.rm').onclick = () => { confirmEl.classList.add('on'); row.querySelector('.rm-yes').focus(); };
|
|
293
|
+
row.querySelector('.rm-no').onclick = () => confirmEl.classList.remove('on');
|
|
294
|
+
row.querySelector('.rm-yes').onclick = async () => {
|
|
295
|
+
const btn = row.querySelector('.rm-yes'); btn.disabled = true;
|
|
296
|
+
try { await removeProfile(p); }
|
|
297
|
+
catch (e) { errEl.textContent = e.message || String(e); errEl.classList.add('on'); btn.disabled = false; }
|
|
298
|
+
};
|
|
299
|
+
row._openCfg = () => { if (!row.classList.contains('cfg-open')) toggleCfg(new Event('click')); row.scrollIntoView({ block: 'nearest' }); };
|
|
201
300
|
row.onclick = () => selectProfile(p.accountIdx);
|
|
202
|
-
val.onclick = startEdit; line2.querySelector('.pen').onclick = startEdit;
|
|
203
|
-
line2.querySelector('.cancel').onclick = stopEdit;
|
|
204
|
-
line2.querySelector('.save').onclick = (e) => { e.stopPropagation(); save(input.value); };
|
|
205
|
-
input.onclick = (e) => e.stopPropagation();
|
|
206
|
-
input.onkeydown = (e) => { e.stopPropagation(); if (e.key === 'Enter') save(input.value); if (e.key === 'Escape') stopEdit(); };
|
|
207
301
|
row.querySelector('.reload').onclick = (e) => { e.stopPropagation(); reloadProfile(p.accountIdx); };
|
|
208
|
-
row._save = save; row._startEdit = startEdit;
|
|
209
302
|
return row;
|
|
210
303
|
}
|
|
211
304
|
|
|
@@ -226,6 +319,8 @@
|
|
|
226
319
|
if (!rows.has(p.accountIdx)) { const row = buildRow(p); rows.set(p.accountIdx, row); rowsEl.appendChild(row); }
|
|
227
320
|
rows.get(p.accountIdx).classList.toggle('active', !!selected && selected.accountIdx === p.accountIdx);
|
|
228
321
|
renderRowState(p.accountIdx);
|
|
322
|
+
renderIdentity(p);
|
|
323
|
+
renderIp(p);
|
|
229
324
|
}
|
|
230
325
|
}
|
|
231
326
|
|
|
@@ -250,8 +345,10 @@
|
|
|
250
345
|
const next = { ...prev, loading: !!state.loading };
|
|
251
346
|
if (state.url !== undefined) { next.url = failed ? '' : state.url; next.title = state.title || ''; }
|
|
252
347
|
if (!state.loading) { next.failed = failed; next.error = state.error || (failed ? prev.error : null); }
|
|
348
|
+
if (state.identity) next.identity = state.identity;
|
|
253
349
|
cellState.set(idx, next);
|
|
254
350
|
renderRowState(idx);
|
|
351
|
+
{ const p = profiles.find((x) => x.accountIdx === idx); if (p) renderIdentity(p); }
|
|
255
352
|
if (selected && selected.accountIdx === idx) renderPreviewState();
|
|
256
353
|
}
|
|
257
354
|
async function pullStates({ adopt = false } = {}) {
|
|
@@ -277,6 +374,19 @@
|
|
|
277
374
|
selectProfile(target);
|
|
278
375
|
}
|
|
279
376
|
|
|
377
|
+
async function removeProfile(p) {
|
|
378
|
+
const idx = p.accountIdx;
|
|
379
|
+
await window.panelAPI.removeProfile(idx);
|
|
380
|
+
openedProfiles.delete(idx); cellState.delete(idx);
|
|
381
|
+
const row = rows.get(idx); if (row) row.remove(); rows.delete(idx);
|
|
382
|
+
profiles = profiles.filter((x) => x.accountIdx !== idx);
|
|
383
|
+
if (selected && selected.accountIdx === idx) {
|
|
384
|
+
const next = profiles[0] ? profiles[0].accountIdx : 0;
|
|
385
|
+
if (next) selectProfile(next); else { selected = null; localStorage.removeItem('telegram-matrix-profile'); selectedName.textContent = '未选择 Profile'; render(); renderPreviewState(); syncPreview(); }
|
|
386
|
+
} else render();
|
|
387
|
+
flash(`Profile #${idx} 已删除`);
|
|
388
|
+
}
|
|
389
|
+
|
|
280
390
|
addButton.onclick = async () => {
|
|
281
391
|
addButton.disabled = true; flash('正在创建…', 60000);
|
|
282
392
|
try {
|
|
@@ -289,8 +399,7 @@
|
|
|
289
399
|
};
|
|
290
400
|
document.getElementById('reload').onclick = () => { if (selected) reloadProfile(selected.accountIdx); };
|
|
291
401
|
document.getElementById('err-retry').onclick = () => { if (selected) reloadProfile(selected.accountIdx); };
|
|
292
|
-
document.getElementById('err-
|
|
293
|
-
document.getElementById('err-edit').onclick = () => { const row = selected && rows.get(selected.accountIdx); if (row) { row.scrollIntoView({ block: 'nearest' }); row._startEdit(); } };
|
|
402
|
+
document.getElementById('err-cfg').onclick = () => { const row = selected && rows.get(selected.accountIdx); if (row) row._openCfg(); };
|
|
294
403
|
|
|
295
404
|
new ResizeObserver(() => requestAnimationFrame(syncPreview)).observe(preview);
|
|
296
405
|
window.addEventListener('resize', syncPreview);
|
package/src/utils/global-json.js
CHANGED
|
@@ -9,6 +9,10 @@ function sleep(ms) {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
function withFileLock(lockPath, fn, { timeoutMs = 5000, retryDelayMs = 50 } = {}) {
|
|
12
|
+
// The lock is a directory next to the target file; on a fresh machine the
|
|
13
|
+
// parent (~/cicy-ai) does not exist yet and mkdir(lock) fails with ENOENT,
|
|
14
|
+
// which used to make every first-run write (api_token, terms, deviceInfo) fail.
|
|
15
|
+
try { fs.mkdirSync(path.dirname(lockPath), { recursive: true }); } catch (_) {}
|
|
12
16
|
const startedAt = Date.now();
|
|
13
17
|
while (true) {
|
|
14
18
|
try {
|
package/src/utils/rpc-guard.js
CHANGED
|
@@ -58,6 +58,10 @@ async function ensureRpcGrant(event, toolName, args) {
|
|
|
58
58
|
if (!wc || wc.isDestroyed()) return false;
|
|
59
59
|
const origin = originOf(wc);
|
|
60
60
|
if (_grants.get(wc.id) === origin) return true; // already allowed for this page
|
|
61
|
+
let host = ""; try { host = new URL(wc.getURL()).hostname; } catch {}
|
|
62
|
+
let store = null; try { store = require("../profiles/trusted-origins-store"); } catch {}
|
|
63
|
+
if (store && host && store.isDangerousAllowed(host)) return true; // 白名单站点 + 用户选过「始终允许」
|
|
64
|
+
const canAlways = !!(store && host && store.listAll().includes(host));
|
|
61
65
|
|
|
62
66
|
const win = BrowserWindow.fromWebContents(wc) || BrowserWindow.getFocusedWindow() || null;
|
|
63
67
|
const detail = [`来源: ${origin}`, `操作: ${toolName}`];
|
|
@@ -70,7 +74,7 @@ async function ensureRpcGrant(event, toolName, args) {
|
|
|
70
74
|
choice = await dialog.showMessageBox(win, {
|
|
71
75
|
type: "warning",
|
|
72
76
|
noLink: true,
|
|
73
|
-
buttons: ["拒绝", "允许一次", "本页面内允许"],
|
|
77
|
+
buttons: canAlways ? ["拒绝", "允许一次", "本页面内允许", "此站点始终允许(不再询问)"] : ["拒绝", "允许一次", "本页面内允许"],
|
|
74
78
|
defaultId: 0,
|
|
75
79
|
cancelId: 0,
|
|
76
80
|
title: "敏感操作请求",
|
|
@@ -80,6 +84,12 @@ async function ensureRpcGrant(event, toolName, args) {
|
|
|
80
84
|
} catch { return false; }
|
|
81
85
|
|
|
82
86
|
const response = choice && choice.response;
|
|
87
|
+
if (response === 3 && canAlways) { // persistent: allowlisted site, never ask again for dangerous tools
|
|
88
|
+
try { store.allowDangerous(host); } catch {}
|
|
89
|
+
_grants.set(wc.id, origin);
|
|
90
|
+
audit({ kind: "auth", gate: "dangerous-tool", origin, tool: toolName, decision: "always-allow", temporary: false, args: pv });
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
83
93
|
if (response === 2) { // remember for this page
|
|
84
94
|
_grants.set(wc.id, origin);
|
|
85
95
|
if (!wc.__rpcGuardWired) {
|
|
@@ -104,7 +114,12 @@ async function ensureRpcGrant(event, toolName, args) {
|
|
|
104
114
|
// trusted-origins allowlist (so it's never asked again). This replaces the old
|
|
105
115
|
// "no bridge unless pre-trusted" behaviour with explicit, on-demand consent.
|
|
106
116
|
const _sessionOrigins = new Set(); // origin -> "本次允许" for this process lifetime
|
|
107
|
-
|
|
117
|
+
// origin -> deny timestamp. A deny is sticky only for DENY_COOLDOWN_MS so a page
|
|
118
|
+
// can't spam modals, but a mistaken "拒绝" (or a modal that timed out unattended)
|
|
119
|
+
// no longer locks the origin out for the whole process lifetime — the user gets
|
|
120
|
+
// asked again after the cooldown instead of having to find the settings page.
|
|
121
|
+
const DENY_COOLDOWN_MS = 60 * 1000;
|
|
122
|
+
const _deniedOrigins = new Map();
|
|
108
123
|
const _pendingByOrigin = new Map(); // origin -> in-flight modal promise (dedup races)
|
|
109
124
|
|
|
110
125
|
// Synchronous verdict for an origin WITHOUT prompting: "allow" | "deny" | "unknown".
|
|
@@ -119,7 +134,9 @@ function originDecision(event) {
|
|
|
119
134
|
if (isTrustedUrl && isTrustedUrl(url)) return "allow"; // on the allowlist
|
|
120
135
|
const origin = originOf(wc);
|
|
121
136
|
if (_sessionOrigins.has(origin)) return "allow"; // "本次允许" earlier
|
|
122
|
-
|
|
137
|
+
const deniedAt = _deniedOrigins.get(origin);
|
|
138
|
+
if (deniedAt && Date.now() - deniedAt < DENY_COOLDOWN_MS) return "deny"; // recently refused
|
|
139
|
+
if (deniedAt) _deniedOrigins.delete(origin); // cooldown over → ask again
|
|
123
140
|
return "unknown";
|
|
124
141
|
}
|
|
125
142
|
|
|
@@ -165,15 +182,29 @@ function startOriginModal(event) {
|
|
|
165
182
|
return true;
|
|
166
183
|
}
|
|
167
184
|
if (r === 2 && host) { // 加入白名单(持久)— trusted-origins-store.add() logs the allowlist change
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
185
|
+
let res = null, err = "";
|
|
186
|
+
try { res = require("../profiles/trusted-origins-store").add(host); } catch (e) { err = e && e.message; }
|
|
187
|
+
if (res && res.ok !== false) {
|
|
171
188
|
if (refreshTrustedOrigins) refreshTrustedOrigins();
|
|
172
189
|
_sessionOrigins.add(origin);
|
|
173
190
|
return true;
|
|
174
|
-
}
|
|
191
|
+
}
|
|
192
|
+
// The user explicitly chose to trust the site; if persisting the allowlist
|
|
193
|
+
// failed, honour the intent for this run and SAY why — silently returning
|
|
194
|
+
// false here made the modal reappear on every call with no explanation.
|
|
195
|
+
_sessionOrigins.add(origin);
|
|
196
|
+
audit({ kind: "auth", gate: "origin", origin, decision: "session-allow", temporary: true, error: `allowlist-add-failed: ${(res && res.error) || err || "unknown"}` });
|
|
197
|
+
try {
|
|
198
|
+
dialog.showMessageBox(win, {
|
|
199
|
+
type: "error", noLink: true, buttons: ["知道了"],
|
|
200
|
+
title: "加入白名单失败",
|
|
201
|
+
message: `无法把 ${host} 加入受信任站点`,
|
|
202
|
+
detail: `${(res && res.error) || err || "未知错误"}\n\n本次已允许该站点;下次启动会再次询问。你也可以在 头像 → 受信任站点 里手动添加。`,
|
|
203
|
+
}).catch(() => {});
|
|
204
|
+
} catch {}
|
|
205
|
+
return true;
|
|
175
206
|
}
|
|
176
|
-
_deniedOrigins.
|
|
207
|
+
_deniedOrigins.set(origin, Date.now()); // 拒绝 / 关闭 — sticky for DENY_COOLDOWN_MS
|
|
177
208
|
audit({ kind: "auth", gate: "origin", origin, decision: "deny", temporary: true });
|
|
178
209
|
return false;
|
|
179
210
|
})();
|
|
@@ -190,4 +221,4 @@ async function ensureOriginAuthorized(event) {
|
|
|
190
221
|
return await startOriginModal(event);
|
|
191
222
|
}
|
|
192
223
|
|
|
193
|
-
module.exports = { DANGEROUS_TOOLS, isDangerousTool, ensureRpcGrant, ensureOriginAuthorized, originDecision, startOriginModal };
|
|
224
|
+
module.exports = { DENY_COOLDOWN_MS, DANGEROUS_TOOLS, isDangerousTool, ensureRpcGrant, ensureOriginAuthorized, originDecision, startOriginModal };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert/strict");
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const os = require("node:os");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
|
|
7
|
+
const read = (p) => fs.readFileSync(path.join(__dirname, "..", p), "utf8");
|
|
8
|
+
|
|
9
|
+
test("ensureWsl explains a disabled-virtualization host in the log and the card", () => {
|
|
10
|
+
const src = read("src/sidecar/docker.js");
|
|
11
|
+
assert.match(src, /async function virtualizationStatus\(\)/);
|
|
12
|
+
const ensure = src.slice(src.indexOf("async function ensureWsl"), src.indexOf("async function bootstrap"));
|
|
13
|
+
assert.match(ensure, /const virt = await virtualizationStatus\(\);/);
|
|
14
|
+
assert.match(ensure, /reason: "virtualization_disabled"/);
|
|
15
|
+
assert.match(ensure, /log\.error\(`\[bootstrap\] ✗ ensure-wsl reason=virtualization_disabled/);
|
|
16
|
+
assert.match(ensure, /BIOS/);
|
|
17
|
+
assert.match(ensure, /reason: "wsl_enable_failed", message/);
|
|
18
|
+
assert.match(read("src/sidecar/wsl-docker.js"), /const reason = w\.reason \|\| "wsl_enable_failed"; fail\(reason, w\.message\)/);
|
|
19
|
+
const ipc = read("src/backends/sidecar-ipc.js");
|
|
20
|
+
assert.match(ipc, /lastError: \(!s\.running && _lastBootstrapError\)/);
|
|
21
|
+
assert.match(ipc, /_lastBootstrapError = \{ reason: result\.reason \|\| result\.error/);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("trusted-origins: persistent dangerous-ops allow only for allowlisted hosts", () => {
|
|
25
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), "cicy-trust-"));
|
|
26
|
+
const prevHome = process.env.HOME, prevUp = process.env.USERPROFILE;
|
|
27
|
+
process.env.HOME = home; process.env.USERPROFILE = home;
|
|
28
|
+
delete require.cache[require.resolve("../src/profiles/trusted-origins-store")];
|
|
29
|
+
try {
|
|
30
|
+
const store = require("../src/profiles/trusted-origins-store");
|
|
31
|
+
assert.equal(store.allowDangerous("xs_master.cicy-ai.com").ok, false); // not allowlisted yet
|
|
32
|
+
assert.equal(store.allowDangerous("127.0.0.1").ok, true); // built-in host works too
|
|
33
|
+
assert.equal(store.isDangerousAllowed("127.0.0.1"), true);
|
|
34
|
+
assert.equal(store.add("xs_master.cicy-ai.com").ok, true);
|
|
35
|
+
assert.equal(store.allowDangerous("https://xs_master.cicy-ai.com").ok, true);
|
|
36
|
+
assert.equal(store.isDangerousAllowed("xs_master.cicy-ai.com"), true);
|
|
37
|
+
assert.equal(store.add("other.example.com").ok, true); // adding another host keeps the flag
|
|
38
|
+
assert.equal(store.isDangerousAllowed("xs_master.cicy-ai.com"), true);
|
|
39
|
+
store.remove("xs_master.cicy-ai.com"); // removal revokes it
|
|
40
|
+
assert.equal(store.isDangerousAllowed("xs_master.cicy-ai.com"), false);
|
|
41
|
+
} finally {
|
|
42
|
+
process.env.HOME = prevHome; if (prevUp === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = prevUp;
|
|
43
|
+
delete require.cache[require.resolve("../src/profiles/trusted-origins-store")];
|
|
44
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("rpc-guard offers a persistent 'always allow' only for allowlisted origins", () => {
|
|
49
|
+
const src = read("src/utils/rpc-guard.js");
|
|
50
|
+
assert.match(src, /store\.isDangerousAllowed\(host\)\) return true/);
|
|
51
|
+
assert.match(src, /canAlways \? \["拒绝", "允许一次", "本页面内允许", "此站点始终允许(不再询问)"\]/);
|
|
52
|
+
assert.match(src, /response === 3 && canAlways/);
|
|
53
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert/strict");
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const os = require("node:os");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const { updateGlobalConfig, readGlobalConfig } = require("../src/utils/global-json");
|
|
7
|
+
|
|
8
|
+
test("updateGlobalConfig works when ~/cicy-ai does not exist yet (fresh machine)", () => {
|
|
9
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "cicy-fresh-"));
|
|
10
|
+
const file = path.join(root, "cicy-ai", "global.json"); // parent dir missing on purpose
|
|
11
|
+
try {
|
|
12
|
+
const next = updateGlobalConfig(file, (c) => ({ ...c, api_token: "cicy_test" }));
|
|
13
|
+
assert.equal(next.api_token, "cicy_test");
|
|
14
|
+
assert.equal(readGlobalConfig(file).api_token, "cicy_test");
|
|
15
|
+
assert.equal(fs.existsSync(`${file}.lock`), false);
|
|
16
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
17
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert/strict");
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
|
|
6
|
+
// Static guard: openTeam must NOT use the host global.json token for the
|
|
7
|
+
// Windows Docker :8008 team — that token belongs to the container's own volume.
|
|
8
|
+
test("openTeam reads the container token for the Windows Docker :8008 team, never the host global.json", () => {
|
|
9
|
+
const src = fs.readFileSync(path.join(__dirname, "..", "src", "backends", "local-teams.js"), "utf8");
|
|
10
|
+
const open = src.slice(src.indexOf("async function openTeam"), src.indexOf("async function openTeam") + 2000);
|
|
11
|
+
// win32 + local + (is_docker | docker port) → container token
|
|
12
|
+
assert.match(open, /const isWinDockerTeam = process\.platform === "win32" && isLocalUrl/);
|
|
13
|
+
assert.match(open, /readContainerToken\(localPort\)/);
|
|
14
|
+
// when it's the win docker team, the host global.json fallback must be gated OUT
|
|
15
|
+
assert.match(open, /if \(isWinDockerTeam\)[^]*?\} else if \(isLocalUrl\) \{\s*token = readGlobal\(\)\?\.api_token/);
|
|
16
|
+
// and a missing container token refuses to open (no silent host-token page)
|
|
17
|
+
assert.match(open, /container_token_unavailable/);
|
|
18
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert/strict");
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const os = require("node:os");
|
|
6
|
+
|
|
7
|
+
test("setNote persists a trimmed note on electron profiles", () => {
|
|
8
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), "cicy-note-"));
|
|
9
|
+
const prevHome = process.env.HOME, prevUp = process.env.USERPROFILE;
|
|
10
|
+
process.env.HOME = home; process.env.USERPROFILE = home;
|
|
11
|
+
delete require.cache[require.resolve("../src/profiles/profile-store")];
|
|
12
|
+
try {
|
|
13
|
+
const store = require("../src/profiles/profile-store");
|
|
14
|
+
fs.mkdirSync(store.ELECTRON_DIR, { recursive: true });
|
|
15
|
+
const view = store.setNote("electron", 3, " 主号,别乱动 ");
|
|
16
|
+
assert.equal(view.note, "主号,别乱动");
|
|
17
|
+
assert.equal(store.getProfile("electron", 3).note, "主号,别乱动");
|
|
18
|
+
assert.equal(store.setNote("electron", 3, "").note, "");
|
|
19
|
+
assert.equal(store.removeProfile("electron", 3), true);
|
|
20
|
+
assert.equal(store.getProfile("electron", 3), null);
|
|
21
|
+
assert.equal(store.removeProfile("electron", 3), false);
|
|
22
|
+
} finally {
|
|
23
|
+
process.env.HOME = prevHome; if (prevUp === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = prevUp;
|
|
24
|
+
delete require.cache[require.resolve("../src/profiles/profile-store")];
|
|
25
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert/strict");
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
|
|
6
|
+
// Static checks: a deny must expire (cooldown) and a failed allowlist add must
|
|
7
|
+
// still honour the user's "trust" choice for the session and explain the error.
|
|
8
|
+
test("origin deny is a cooldown, not process-lifetime; failed allowlist add is explained", () => {
|
|
9
|
+
const src = fs.readFileSync(path.join(__dirname, "..", "src", "utils", "rpc-guard.js"), "utf8");
|
|
10
|
+
assert.match(src, /const DENY_COOLDOWN_MS = \d+ \* 1000;/);
|
|
11
|
+
assert.match(src, /const _deniedOrigins = new Map\(\)/);
|
|
12
|
+
assert.match(src, /Date\.now\(\) - deniedAt < DENY_COOLDOWN_MS\) return "deny"/);
|
|
13
|
+
assert.match(src, /_deniedOrigins\.set\(origin, Date\.now\(\)\)/);
|
|
14
|
+
assert.match(src, /加入白名单失败/);
|
|
15
|
+
const trustBranch = src.slice(src.indexOf("if (r === 2 && host)"), src.indexOf("_deniedOrigins.set(origin"));
|
|
16
|
+
assert.match(trustBranch, /_sessionOrigins\.add\(origin\);[^]*return true;[^]*_sessionOrigins\.add\(origin\);[^]*return true;/);
|
|
17
|
+
assert.doesNotMatch(trustBranch, /return false/);
|
|
18
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert/strict");
|
|
3
|
+
const vm = require("node:vm");
|
|
4
|
+
const {
|
|
5
|
+
isTelegramUrl,
|
|
6
|
+
TELEGRAM_IDENTITY_SCRIPT,
|
|
7
|
+
normalizeTelegramIdentity,
|
|
8
|
+
telegramLoginRecord,
|
|
9
|
+
telegramIdentityFromProfile,
|
|
10
|
+
} = require("../src/tabbrowser/telegram-identity");
|
|
11
|
+
|
|
12
|
+
test("recognizes telegram web urls only", () => {
|
|
13
|
+
assert.equal(isTelegramUrl("https://web.telegram.org/k/"), true);
|
|
14
|
+
assert.equal(isTelegramUrl("https://web.telegram.org/a/#123"), true);
|
|
15
|
+
assert.equal(isTelegramUrl("https://example.com/web.telegram.org"), false);
|
|
16
|
+
assert.equal(isTelegramUrl("chrome-error://chromewebdata/"), false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("identity script is valid JS and returns null without storage", async () => {
|
|
20
|
+
new vm.Script(TELEGRAM_IDENTITY_SCRIPT); // syntax
|
|
21
|
+
const ctx = vm.createContext({ localStorage: { getItem: () => null }, indexedDB: {} });
|
|
22
|
+
assert.equal(await vm.runInContext(TELEGRAM_IDENTITY_SCRIPT, ctx), null);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("identity script never leaks auth material", () => {
|
|
26
|
+
assert.doesNotMatch(TELEGRAM_IDENTITY_SCRIPT, /auth_key|server_salt|account\d/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("normalizes identity and maps to a profile login record", () => {
|
|
30
|
+
const it = normalizeTelegramIdentity({ id: 42, username: "@alice", firstName: " Alice ", lastName: "L", phone: "1234" });
|
|
31
|
+
assert.deepEqual(it, { id: "42", username: "alice", displayName: "Alice L", phone: "1234" });
|
|
32
|
+
assert.deepEqual(telegramLoginRecord(it), { url: "https://web.telegram.org", name: "telegram", username: "alice", mobile: "1234", note: "Alice L" });
|
|
33
|
+
assert.equal(normalizeTelegramIdentity(null), null);
|
|
34
|
+
assert.equal(normalizeTelegramIdentity({ username: "x" }), null);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("reads identity back from a stored profile", () => {
|
|
38
|
+
const p = { logins: [{ name: "gmail", username: "a@b" }, { name: "Telegram", username: "alice", mobile: "1", note: "Alice" }] };
|
|
39
|
+
assert.deepEqual(telegramIdentityFromProfile(p), { username: "alice", displayName: "Alice", phone: "1" });
|
|
40
|
+
assert.equal(telegramIdentityFromProfile({ logins: [] }), null);
|
|
41
|
+
});
|
|
@@ -58,7 +58,7 @@ test("a cached-but-offline cell is reported as failed, not loaded", () => {
|
|
|
58
58
|
// service worker 会让断网的 Telegram 也「加载成功」,只能从 session 层探。
|
|
59
59
|
assert.match(src, /net\.fetch\(target, \{[^]*session: session\.fromPartition\(partitionFor\(profileIdx\)\)/);
|
|
60
60
|
assert.match(src, /ERR_NO_NETWORK/);
|
|
61
|
-
assert.match(src, /did-stop-loading", \(\) => \{ push\(\); probeReachable\(\)
|
|
61
|
+
assert.match(src, /did-stop-loading", \(\) => \{ push\(\); probeReachable\(\);/);
|
|
62
62
|
assert.match(src, /if \(seq !== probeSeq\) return;/);
|
|
63
63
|
// 一次失败不判定,避免代理抖动误报
|
|
64
64
|
const probe = src.slice(src.indexOf("const probeReachable"), src.indexOf("wc.on(\"did-start-loading\""));
|
|
@@ -73,25 +73,31 @@ test("panel reload adopts existing session views instead of destroying them", ()
|
|
|
73
73
|
assert.match(pull, /if \(adopt\)[^]*openedProfiles\.add\(Number\(m\[1\]\)\)/);
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
test("
|
|
76
|
+
test("reloading an unopened profile creates its cell instead of a bare reload", () => {
|
|
77
77
|
const html = fs.readFileSync(path.join(__dirname, "..", "src", "tabbrowser", "telegram-matrix.html"), "utf8");
|
|
78
|
-
const save = html.slice(html.indexOf("const save = async"), html.indexOf("row.onclick ="));
|
|
79
|
-
// 只有已打开的会话才置 loading / 重载;未打开的没有 cell,置了就永远回不来。
|
|
80
|
-
assert.match(save, /openedProfiles\.has\(p\.accountIdx\)/);
|
|
81
|
-
assert.doesNotMatch(save, /cellState\.set\(p\.accountIdx, \{ loading: true \}\)/);
|
|
82
78
|
const reload = html.slice(html.indexOf("function reloadProfile"), html.indexOf("function render()"));
|
|
83
79
|
assert.match(reload, /const fresh = !openedProfiles\.has\(idx\)/);
|
|
84
80
|
assert.match(reload, /if \(!fresh\)[^]*panelAPI\.reload/);
|
|
85
81
|
});
|
|
86
82
|
|
|
87
|
-
test("matrix page exposes profile table,
|
|
83
|
+
test("matrix page exposes profile table, phone preview, note, proxy drawer and ip probe", () => {
|
|
88
84
|
const html = fs.readFileSync(path.join(__dirname, "..", "src", "tabbrowser", "telegram-matrix.html"), "utf8");
|
|
89
85
|
assert.match(html, /id="rows"/);
|
|
90
86
|
assert.match(html, /id="ov-error"/);
|
|
91
87
|
assert.match(html, /panelAPI\.states/);
|
|
92
88
|
assert.match(html, /panelAPI\.setProfileProxy/);
|
|
89
|
+
assert.match(html, /panelAPI\.probeIp/);
|
|
90
|
+
assert.match(html, /panelAPI\.removeProfile/);
|
|
91
|
+
// 删除必须二次确认:确认块默认隐藏,只有 rm-yes 才真正调用删除
|
|
92
|
+
assert.match(html, /\.cfg \.confirm \{ display: none;/);
|
|
93
|
+
assert.match(html, /querySelector\('\.rm-yes'\)\.onclick = async[^]*await removeProfile\(p\)/);
|
|
94
|
+
assert.doesNotMatch(html.slice(html.indexOf("querySelector('.rm').onclick"), html.indexOf("querySelector('.rm-no')")), /removeProfile\(/);
|
|
93
95
|
assert.match(html, /id="add-profile"/);
|
|
94
|
-
assert.match(html, /data-role="proxy"/);
|
|
96
|
+
assert.match(html, /class="cfg"[^]*data-role="proxy"/);
|
|
97
|
+
assert.match(html, /\.row\.cfg-open \.cfg \{ display: block; \}/);
|
|
98
|
+
assert.match(html, /panelAPI\.setProfileNote/);
|
|
99
|
+
assert.match(html, /data-role="note"/);
|
|
100
|
+
assert.match(html, /class="tg none"/);
|
|
95
101
|
assert.match(html, /id="phone-preview"/);
|
|
96
102
|
assert.match(html, /https:\/\/web\.telegram\.org\/k\//);
|
|
97
103
|
assert.match(html, /panelAPI\.sync/);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert/strict");
|
|
3
|
+
const { normalizeHost } = require("../src/profiles/trusted-origins-store");
|
|
4
|
+
|
|
5
|
+
test("trusted-origins accepts hostnames with underscores, bare or as URL", () => {
|
|
6
|
+
assert.equal(normalizeHost("xs_master.cicy-ai.com"), "xs_master.cicy-ai.com");
|
|
7
|
+
assert.equal(normalizeHost("https://XS_master.cicy-ai.com/path?q=1"), "xs_master.cicy-ai.com");
|
|
8
|
+
assert.equal(normalizeHost("xs_master.cicy-ai.com:8008"), "xs_master.cicy-ai.com");
|
|
9
|
+
assert.equal(normalizeHost("bad host"), "");
|
|
10
|
+
assert.equal(normalizeHost(".x.com"), "");
|
|
11
|
+
});
|