Repository navigation
Expand file tree
/
Copy pathsettings.js
More file actions
209 lines (198 loc) · 7.87 KB
/
Copy pathsettings.js
File metadata and controls
209 lines (198 loc) · 7.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
/**
* GitHub 意识设置页脚本(CSP 禁内联,外挂加载)。
* 数据面:
* GET /secrets → [{ key, saved, tail?, hostSource?, hostAvailable? }]
* (永远拿不回 gh/PAT 值)
* PUT /secrets/github_pat → { value } 一次性入库(204)
* DELETE /secrets/github_pat → 清除
* GET /wake → 叫醒电子脑(幂等)
* 测试连接经 BroadcastChannel('cindy-github') 递活给电子脑:
* 发 { type:'test-connection', reqId },按 reqId 每 400ms 重发直到收到
* { type:'test-connection-result', reqId, ok, login, message },15s 超时。
*/
(function () {
'use strict';
var KEY = 'github_pat';
var bc = new BroadcastChannel('cindy-github');
var ghAvailable = false;
var hostManagedSetup = false;
function $(id) { return document.getElementById(id); }
var statusTimer = null;
function showStatus(text, sticky) {
$('status').textContent = text;
if (statusTimer) clearTimeout(statusTimer);
if (!sticky) statusTimer = setTimeout(function () { $('status').textContent = ''; }, 4000);
}
/** 宿主状态卡只展示可用布尔;不缓存展示账号,避免本机切号后的旧身份误导。 */
function renderHostAccount(available) {
var box = $('host-account');
box.textContent = '';
var row = document.createElement('div');
row.className = 'account';
var who = document.createElement('span');
who.className = 'who';
who.textContent = available ? '已连接 GitHub,可直接使用' : '尚未连接 GitHub';
row.appendChild(who);
var tag = document.createElement('span');
tag.className = 'tag';
tag.textContent = available ? '优先使用' : '可使用备用 Token';
row.appendChild(tag);
box.appendChild(row);
}
/** 渲染备用 PAT 保存状态(saved + tail 指纹)。 */
function renderFallbackAccount(saved, tail) {
var box = $('fallback-account');
box.textContent = '';
if (!saved) return;
var row = document.createElement('div');
row.className = 'account';
var who = document.createElement('span');
who.className = 'who';
who.textContent = '已保存备用 Token';
row.appendChild(who);
var tag = document.createElement('span');
tag.className = 'tag';
tag.textContent = tail ? '尾号 ' + tail : '';
row.appendChild(tag);
box.appendChild(row);
}
async function load() {
try {
var saved = false;
var tail = '';
var hostAvailable = false;
var managedSetup = false;
var list = await (await fetch('/secrets')).json();
for (var i = 0; i < list.length; i++) {
if (list[i] && list[i].key === KEY) {
saved = Boolean(list[i].saved);
tail = list[i].tail || '';
hostAvailable = list[i].hostSource === 'gh-cli' && Boolean(list[i].hostAvailable);
managedSetup = Boolean(list[i].hostManagedSetup);
}
}
ghAvailable = hostAvailable;
hostManagedSetup = managedSetup;
$('legacy-account').hidden = managedSetup;
$('test').hidden = managedSetup;
if (!hostAvailable && saved) $('other-methods').open = true;
renderHostAccount(hostAvailable);
renderFallbackAccount(saved, tail);
// 单凭证语义要在输入行上说破:已保存时空输入框容易被误读成"还能再绑
// 一个",实际再存 = 覆盖唯一的一条。占位与按钮文案切成「更换」。
$('token').placeholder = saved ? '粘贴新的备用 Token 以更换(覆盖当前)' : '粘贴备用 Token';
$('save').textContent = saved ? '更换' : '保存';
$('test').disabled = !hostAvailable && !saved;
$('clear').disabled = !saved;
} catch (e) {
showStatus('状态加载失败,请稍后重试');
}
}
/**
* 眼睛按钮只服务「正在输入的新值」的核对——已存的值永远读不回,空框时
* 点它必然没反应,干脆藏掉免得被误会成"看已存 token"的坏按钮;隐藏同时
* 复位密文态,下次粘贴默认遮蔽。(交互与 xd-mivo 设置页一致。)
*/
function syncEye() {
var input = $('token');
var eye = $('eye');
var empty = input.value.length === 0;
eye.classList.toggle('hidden', empty);
if (empty) {
input.type = 'password';
eye.classList.remove('revealed');
}
}
async function save() {
var input = $('token');
var value = input.value.trim();
if (!value) { showStatus('请先粘贴 token'); return; }
$('save').disabled = true;
try {
var r = await fetch('/secrets/' + KEY, { method: 'PUT', body: JSON.stringify({ value: value }) });
if (r.status !== 204) { showStatus('保存失败(' + r.status + '),请重试', true); return; }
input.value = '';
syncEye();
await load();
// 保存只确认凭证已写入;认证来源可能在异步窗口内切换,不能把
// 「当前连接」的成功误报成刚保存的备用 Token 已验证。
if (ghAvailable) showStatus('备用 Token 已保存;当前仍优先使用本机 GitHub 登录');
else if (hostManagedSetup) showStatus('备用 Token 已保存,连接状态见上方。');
else showStatus('备用 Token 已保存;点击“测试连接”可检查当前 GitHub 连接');
} catch (e) {
showStatus('保存失败,请重试', true);
} finally {
$('save').disabled = false;
}
}
var testSeq = 0;
async function test() {
var reqId = 'test-' + Date.now() + '-' + (++testSeq);
var btn = $('test');
btn.disabled = true;
showStatus('正在检查当前 GitHub 连接…', true);
try {
await fetch('/wake');
} catch (e) { /* 叫不醒也让重发兜底 */ }
var settled = false;
var timer = null;
var deadline = setTimeout(function () {
if (settled) return;
settled = true;
if (timer) clearInterval(timer);
showStatus('测试超时——电子脑未响应,请稍后重试', true);
void load();
}, 15000);
bc.addEventListener('message', function onMsg(ev) {
var m = ev && ev.data;
if (!m || m.type !== 'test-connection-result' || m.reqId !== reqId) return;
if (settled) return;
settled = true;
bc.removeEventListener('message', onMsg);
clearTimeout(deadline);
if (timer) clearInterval(timer);
if (m.ok) {
// 成功由系统提示(notify)宣告,页内不重复挂灰字,只清掉「正在连接…」。
showStatus('');
} else {
// 失败保留页内:toast 几秒就消失,失败原因要留在页上照着修;
// 且 notify 有 5s 限速,快速重试失败时页内是唯一反馈。
showStatus(m.message || '连接失败', true);
}
void load();
});
var send = function () { bc.postMessage({ type: 'test-connection', reqId: reqId }); };
send();
timer = setInterval(function () {
if (settled) { clearInterval(timer); return; }
send();
}, 400);
}
async function clearToken() {
$('clear').disabled = true;
try {
var response = await fetch('/secrets/' + KEY, { method: 'DELETE' });
if (response.status !== 204) throw new Error('clear-failed');
showStatus('已清除');
} catch (e) {
showStatus('清除失败,请重试');
} finally {
void load();
}
}
$('eye').addEventListener('click', function () {
var input = $('token');
var reveal = input.type === 'password';
input.type = reveal ? 'text' : 'password';
$('eye').classList.toggle('revealed', reveal);
});
$('token').addEventListener('input', syncEye);
$('save').addEventListener('click', function () { void save(); });
$('test').addEventListener('click', function () { void test(); });
$('clear').addEventListener('click', function () { void clearToken(); });
$('token').addEventListener('keydown', function (ev) {
if (ev.key === 'Enter') { ev.preventDefault(); void save(); }
});
syncEye();
void load();
})();