codexunlimit 0.0.0-stage → 0.1.0-web.10

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 (44) hide show
  1. package/README.md +94 -2
  2. package/account-pool.mjs +161 -0
  3. package/app.mjs +158 -0
  4. package/cli.mjs +108 -0
  5. package/codex-account-client.mjs +53 -0
  6. package/config.mjs +184 -0
  7. package/device.ps1 +22 -0
  8. package/doctor.mjs +20 -0
  9. package/image-http.mjs +22 -0
  10. package/io.mjs +5 -0
  11. package/license-client.mjs +150 -0
  12. package/package.json +14 -6
  13. package/product.mjs +5 -0
  14. package/protocol.mjs +28 -0
  15. package/quota.mjs +34 -0
  16. package/relay.mjs +83 -0
  17. package/state.mjs +23 -0
  18. package/updates.mjs +47 -0
  19. package/web/app.js +154 -0
  20. package/web/assets/apple-touch-icon.png +0 -0
  21. package/web/assets/favicon-16.png +0 -0
  22. package/web/assets/favicon-32.png +0 -0
  23. package/web/assets/favicon.ico +0 -0
  24. package/web/assets/fonts.css +48 -0
  25. package/web/assets/geist-LICENSE.txt +93 -0
  26. package/web/assets/geist-latin-ext-wght-normal.woff2 +0 -0
  27. package/web/assets/geist-latin-wght-normal.woff2 +0 -0
  28. package/web/assets/geist-vietnamese-wght-normal.woff2 +0 -0
  29. package/web/assets/icon-192.png +0 -0
  30. package/web/assets/icon-512.png +0 -0
  31. package/web/assets/mark.svg +1 -0
  32. package/web/assets/newsreader-LICENSE.txt +93 -0
  33. package/web/assets/newsreader-latin-ext-wght-normal.woff2 +0 -0
  34. package/web/assets/newsreader-latin-wght-normal.woff2 +0 -0
  35. package/web/assets/newsreader-vietnamese-wght-normal.woff2 +0 -0
  36. package/web/index.html +3 -0
  37. package/web/markdown.js +15 -0
  38. package/web/messages.js +63 -0
  39. package/web/pool-ui.js +53 -0
  40. package/web/stream.js +25 -0
  41. package/web/style.css +53 -0
  42. package/web/updates.js +26 -0
  43. package/windows.mjs +53 -0
  44. package/windows.ps1 +103 -0
package/web/pool-ui.js ADDED
@@ -0,0 +1,53 @@
1
+ import {errors} from './messages.js';
2
+ const escape=value=>String(value??'').replace(/[&<>"']/g,letter=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[letter]));
3
+ const date=value=>value?new Date(value).toLocaleString('vi-VN'):'Chưa có thời gian reset';
4
+ export const accountName=row=>row?.email||row?.label||'Chưa chọn tài khoản';
5
+ export function quotaCards(row){
6
+ return '<div class="local-quota-grid">'+[300,10080].map(minutes=>{
7
+ const window=row?.windows?.find(item=>item.minutes===minutes),label=minutes===300?'5 giờ':'1 tuần';
8
+ return '<div class="local-quota-window '+(window?.used>=90?'quota-warning':'')+'"><span>Quota '+label+'</span><strong>'+(window?escape(window.used.toLocaleString('vi-VN'))+'% <small>đã dùng</small>':'Chưa có số liệu')+'</strong>'+(window?'<progress max="100" value="'+window.used+'" aria-label="Quota '+label+' đã dùng"></progress><p>Reset: '+escape(date(window.reset_at))+'</p><small>'+escape(date(window.observed_at))+'</small>'+(window.stale?'<p class="quota-stale">Số liệu cũ · hãy cập nhật lại</p>':''):'<p>Chọn Cập nhật quota để kiểm tra.</p>')+'</div>';
9
+ }).join('')+'</div>';
10
+ }
11
+ export function quotaSummary(data){
12
+ const row=data?.rows?.find(item=>item.active);
13
+ return '<section class="surface local-quota-summary"><div class="section-title"><div><span class="eyebrow">Tài khoản đang dùng</span><h2>'+escape(accountName(row))+'</h2></div><a class="text-button" href="#accounts">Tài khoản & quota</a></div>'+quotaCards(row)+'<p class="subtext">Hạn mức tài khoản ChatGPT, khác thời hạn key. Số liệu chưa có không có nghĩa là đã dùng 0%.</p></section>';
14
+ }
15
+ export function accountOptions(data){return data?.rows?.length?data.rows.map(row=>'<option value="'+escape(row.id)+'" '+(row.active?'selected':'')+' '+(row.state!=='ready'?'disabled':'')+'>'+escape(accountName(row))+(row.state!=='ready'?' · cần đăng nhập':'')+'</option>').join(''):'<option value="codex">Tài khoản trong Codex</option>';}
16
+ function confirmRemoval(row,action){
17
+ const dialog=document.createElement('dialog');dialog.className='stop-dialog';dialog.setAttribute('aria-labelledby','remove-account-title');
18
+ dialog.innerHTML='<h2 id="remove-account-title">Gỡ tài khoản khỏi máy?</h2><p>'+escape(accountName(row))+' sẽ được gỡ khỏi pool. Phiên đăng nhập gốc trong Codex và lịch sử chat được giữ lại.</p><div class="settings-actions"><button data-cancel autofocus>Giữ tài khoản</button><button data-confirm class="primary">Gỡ khỏi pool</button></div><p class="error" role="alert"></p>';
19
+ document.body.append(dialog);dialog.addEventListener('close',()=>dialog.remove());dialog.showModal();dialog.querySelector('[data-cancel]').onclick=()=>dialog.close();
20
+ dialog.querySelector('[data-confirm]').onclick=async event=>{event.currentTarget.disabled=true;try{await action();dialog.close();}catch(error){dialog.querySelector('.error').textContent=error.message;event.target.disabled=false;}};
21
+ }
22
+ export function mountAccounts(target,{api,initial,onChange,isBusy=()=>false}){
23
+ let data=initial,disposed=false,timer=null,working=false;
24
+ const current=()=>!disposed&&target.isConnected;
25
+ function showError(error){if(current())target.querySelector('#pool-error').textContent=error.message;}
26
+ function update(value,selection=false){data=value;onChange(value,selection);if(current())draw();}
27
+ async function run(button,route,input,selection=false){
28
+ if(working)return;working=true;const label=button.textContent;button.disabled=true;button.textContent='Đang xử lý…';target.querySelector('#pool-error').textContent='';
29
+ try{update(await api(route,input),selection);}catch(error){showError(error);}finally{working=false;if(button.isConnected){button.disabled=false;button.textContent=label;}schedule();}
30
+ }
31
+ function schedule(){
32
+ clearTimeout(timer);if(!current()||data?.pending?.status!=='pending')return;
33
+ timer=setTimeout(async()=>{if(working){schedule();return;}try{const next=await api('accounts');if(current())update(next);}catch(error){showError(error);}schedule();},1500);
34
+ }
35
+ function draw(){
36
+ if(!current())return;
37
+ const rows=data?.rows??[],pending=data?.pending;
38
+ target.innerHTML='<div class="page-title"><span class="eyebrow">Dễ theo dõi, dễ chuyển đổi</span><h1>Tài khoản & quota</h1><p>Đăng nhập riêng từng tài khoản một lần. Chọn tài khoản để dùng cho chat và các yêu cầu mới từ Codex.</p></div><p id="pool-error" class="error" role="alert"></p>'+
39
+ (pending?.status==='pending'?'<section class="pool-login-pending" aria-live="polite"><span class="eyebrow">Đang chờ bạn đăng nhập</span><h2>Hoàn tất trên trang OpenAI</h2><p>Mở trang đăng nhập và chọn tài khoản cần thêm. Các tài khoản đã lưu vẫn được giữ nguyên.</p>'+(pending.userCode?'<div class="device-code"><code>'+escape(pending.userCode)+'</code><button data-copy-login-code>Sao chép mã</button></div>':'')+'<div class="settings-actions"><a class="button primary" href="'+escape(pending.url)+'" target="_blank" rel="noreferrer noopener">Mở trang đăng nhập</a><button data-cancel-login>Hủy đăng nhập</button></div><p class="subtext">Nếu trình duyệt đang giữ tài khoản khác, mở liên kết trong cửa sổ riêng tư. Trang này tự cập nhật khi hoàn tất.</p></section>':pending?.status==='failed'||pending?.error?'<p class="connection-warning" role="alert">'+escape(errors[pending.error]??errors.pool_login_failed)+'</p>':pending?.status==='complete'?'<p class="pool-success" role="status">Đã thêm tài khoản. Chọn Dùng tài khoản này để bắt đầu.</p>':'')+
40
+ '<div class="pool-accounts">'+rows.map(row=>'<article class="surface pool-account '+(row.active?'pool-active':'')+'" data-account-card="'+escape(row.id)+'"><div class="section-title"><div><h2>'+escape(row.label)+'</h2>'+(row.email?'<p class="pool-email">'+escape(row.email)+'</p>':'')+'</div><span class="tag">'+(row.active?'Đang dùng':row.state==='ready'?'Sẵn sàng':'Cần đăng nhập')+'</span></div>'+(row.plan?'<p class="subtext">Gói ChatGPT: '+escape(row.plan)+'</p>':'')+quotaCards(row)+(row.quotaError?'<p class="error" role="alert">'+escape(errors[row.quotaError]??errors.pool_quota_unavailable)+'</p>':'')+'<div class="settings-actions"><button data-quota-refresh="'+escape(row.id)+'" '+(row.state!=='ready'?'disabled':'')+'>Cập nhật quota</button>'+(!row.active&&row.state==='ready'?'<button class="primary" data-select-account="'+escape(row.id)+'">Dùng tài khoản này</button>':'')+(row.id!=='codex'?'<button class="text-button" data-relogin="'+escape(row.id)+'">Đăng nhập lại</button><button class="text-button" data-remove-account="'+escape(row.id)+'">Gỡ</button>':'<a class="text-button" href="#settings">Kết nối trong cài đặt</a>')+'</div></article>').join('')+'</div>'+
41
+ '<section class="surface pool-add"><div><span class="eyebrow">Thêm vào không gian của bạn</span><h2>Đăng nhập tài khoản khác</h2><p>Phiên được lưu riêng trên máy, không ghi đè tài khoản gốc trong Codex. Bạn có thể lưu tối đa '+(data?.maxAccounts??10)+' tài khoản.</p></div><form id="add-account"><label for="account-label">Tên gợi nhớ</label><input id="account-label" maxlength="60" placeholder="Ví dụ: Cá nhân, Công việc"><label for="login-method">Cách đăng nhập</label><select id="login-method"><option value="chatgpt">Mở trang đăng nhập</option><option value="chatgptDeviceCode">Dùng mã đăng nhập</option></select><label class="privacy-choice"><input id="pool-consent" type="checkbox" required> Tôi đồng ý dùng phiên của tài khoản này để xử lý yêu cầu qua Codex Unlimit.</label><button class="primary" type="submit" '+(pending?.status==='pending'?'disabled':'')+'>Thêm tài khoản</button></form></section><p class="pool-footnote">Quota được đọc từ tài khoản và phản hồi yêu cầu thực tế. Cập nhật quota không gửi tin nhắn chat. Khi đổi tài khoản, yêu cầu đang chạy giữ nguyên tài khoản ban đầu.</p>';
42
+ target.querySelector('#add-account').onsubmit=event=>{event.preventDefault();run(event.submitter,'accounts/login',{label:target.querySelector('#account-label').value.trim()||'Tài khoản '+rows.length,type:target.querySelector('#login-method').value,accepted:target.querySelector('#pool-consent').checked});};
43
+ target.querySelectorAll('[data-quota-refresh]').forEach(button=>button.onclick=()=>run(button,'quota/refresh',{accountId:button.dataset.quotaRefresh}));
44
+ target.querySelectorAll('[data-select-account]').forEach(button=>button.onclick=()=>{if(isBusy())return showError(Error('Dừng câu trả lời đang chạy trước khi đổi tài khoản.'));run(button,'accounts/select',{accountId:button.dataset.selectAccount},true);});
45
+ target.querySelectorAll('[data-relogin]').forEach(button=>button.onclick=()=>{if(data.pending?.status==='pending')return showError(Error(errors.pool_login_pending));run(button,'accounts/login',{accountId:button.dataset.relogin,type:'chatgpt',accepted:true});});
46
+ target.querySelectorAll('[data-remove-account]').forEach(button=>button.onclick=()=>{if(isBusy())return showError(Error('Dừng câu trả lời trước khi gỡ tài khoản.'));const row=rows.find(item=>item.id===button.dataset.removeAccount);confirmRemoval(row,async()=>update(await api('accounts/remove',{accountId:row.id}),row.active));});
47
+ const cancel=target.querySelector('[data-cancel-login]');if(cancel)cancel.onclick=()=>run(cancel,'accounts/cancel',{});
48
+ const copy=target.querySelector('[data-copy-login-code]');if(copy)copy.onclick=async()=>{try{await navigator.clipboard.writeText(pending.userCode);copy.textContent='Đã sao chép';}catch{showError(Error('Hãy chọn và sao chép mã đang hiển thị.'));}};
49
+ schedule();
50
+ }
51
+ draw();api('accounts').then(value=>{if(current())update(value);}).catch(showError);
52
+ return ()=>{disposed=true;clearTimeout(timer);};
53
+ }
package/web/stream.js ADDED
@@ -0,0 +1,25 @@
1
+ // SSE decoder tolerates arbitrary byte boundaries, CRLF and multi-line data.
2
+ export async function readResponseStream(body,onText,{maxText=256000,maxBytes=4*1024*1024}={}){
3
+ const reader=body.getReader(),decoder=new TextDecoder();let buffer='',complete=false,text='';
4
+ let bytes=0;
5
+ const tooLarge=()=>Error('Câu trả lời quá dài. Hãy chia yêu cầu thành các phần nhỏ hơn.');
6
+ function frame(value){
7
+ const data=value.split('\n').filter(l=>l.startsWith('data:')).map(l=>l.slice(5).trimStart()).join('\n');
8
+ if(!data||data==='[DONE]')return;
9
+ let event;try{event=JSON.parse(data);}catch{throw Error('Phản hồi bị gián đoạn. Bạn có thể gửi lại tin nhắn.');}
10
+ if(event.type==='response.output_text.delta'){if(typeof event.delta!=='string')throw Error('Phản hồi không hợp lệ.');if(text.length+event.delta.length>maxText)throw tooLarge();text+=event.delta;onText(text);}
11
+ if(event.type==='response.completed'){
12
+ complete=true;if(!text){text=(event.response?.output??[]).flatMap(x=>x.content??[]).filter(x=>x.type==='output_text').map(x=>x.text).join('');if(text.length>maxText)throw tooLarge();onText(text);}
13
+ }
14
+ if(['error','response.failed','response.incomplete'].includes(event.type))throw Error('Chưa hoàn tất câu trả lời. Vui lòng thử lại.');
15
+ }
16
+ try{
17
+ for(;;){const {value,done}=await reader.read();bytes+=value?.byteLength??0;if(bytes>maxBytes)throw tooLarge();buffer+=decoder.decode(value,{stream:!done}).replace(/\r\n/g,'\n');buffer=buffer.replace(/\r\n/g,'\n');let at;
18
+ while(!complete&&(at=buffer.indexOf('\n\n'))>=0){frame(buffer.slice(0,at));buffer=buffer.slice(at+2);}
19
+ if(buffer.length>2*1024*1024)throw Error('Phản hồi quá dài. Hãy thử một yêu cầu ngắn hơn.');
20
+ if(done||complete)break;
21
+ }
22
+ if(!complete&&buffer.trim())frame(buffer);if(!complete)throw Error('Kết nối bị gián đoạn trước khi hoàn tất.');
23
+ if(!text)throw Error('Chưa nhận được nội dung trả lời. Hãy thử lại.');return text;
24
+ }finally{await reader.cancel().catch(()=>{});reader.releaseLock();}
25
+ }
package/web/style.css ADDED
@@ -0,0 +1,53 @@
1
+ :root{font-family:"Segoe UI Variable Text","Segoe UI",-apple-system,BlinkMacSystemFont,sans-serif;color:#292824;background:#faf9f6;line-height:1.55;font-size:15px;--green:#9b513b;--muted:#75736d;--line:#e8e5df;--soft:#f3ede6}*{box-sizing:border-box}body{margin:0}a{color:var(--green);text-underline-offset:4px}button,input,textarea,select{font:inherit;color:inherit}button,.button{cursor:pointer;border:1px solid var(--line);border-radius:9px;background:#fff;padding:11px 17px;min-height:44px;font-weight:600;display:inline-flex;align-items:center;justify-content:center;gap:10px;text-decoration:none;transition:background .18s,transform .18s}button:hover,.button:hover{background:var(--soft)}button:active{transform:translateY(1px)}button:disabled{opacity:.5;cursor:default}.primary,.send{background:var(--green);color:#fff;border-color:var(--green)}.primary:hover,.send:hover{background:#7d402e}button svg,nav svg{width:21px;height:21px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}:focus-visible{outline:3px solid #c7ad93;outline-offset:4px}h1,h2,h3,p{margin:0}h1{font-size:38px;line-height:1.2;font-weight:600;letter-spacing:-1.3px;text-wrap:balance}h2{font-size:20px;letter-spacing:-.4px;font-weight:600}h3{font-size:16px;font-weight:600}p{color:var(--muted)}[hidden]{display:none!important}.skip{position:fixed;top:-100px;z-index:10;background:#fff;padding:12px}.skip:focus{top:8px}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}.workspace{display:grid;grid-template-columns:250px minmax(0,1fr);min-height:100dvh}.rail{padding:30px 20px 22px;border-right:1px solid var(--line);display:flex;flex-direction:column;height:100dvh;position:sticky;top:0;background:#f3f1ec}.brand{display:flex;align-items:center;gap:12px;text-decoration:none;color:#292824;font-size:19px;font-weight:650;letter-spacing:-.5px}.brand small{display:block;font-size:12px;font-weight:400;letter-spacing:0;color:var(--muted)}.mark{display:inline-grid;place-items:center;flex-shrink:0;background:var(--green);border-radius:11px;width:39px;height:39px;color:white;font-size:25px;font-weight:650;letter-spacing:-2px;padding-right:2px}.new-chat{margin:36px 0 24px;width:100%;background:#fff;border-color:#d6dfd6;font-size:14px}nav{display:grid;gap:5px}nav a{display:flex;align-items:center;gap:12px;padding:12px;border-radius:8px;text-decoration:none;color:var(--muted);font-weight:500;font-size:14px}nav a[aria-current=page]{color:var(--green);background:#e6e0d6}nav a:hover{background:#e6eee4}.history-heading{font-size:11px;text-transform:uppercase;letter-spacing:1.3px;color:#7a897f;padding:30px 12px 12px}.history{overflow:auto}.history p{font-size:13px;padding:0 12px;line-height:1.7}.history button{display:block;width:100%;border:0;background:transparent;text-align:left;font-weight:400;font-size:13px;padding:9px 12px;min-height:38px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.history button.selected{background:#e2ebe0}.rail-foot{margin-top:auto;display:flex;gap:10px;align-items:center;padding-top:30px;font-size:12px;color:#53665a}.rail-foot small{display:block;color:#829084;font-size:11px;margin-top:3px}.device-dot{width:6px;height:6px;background:#599173;border-radius:50%;display:inline-block;flex-shrink:0}.body{min-width:0;display:flex;flex-direction:column}.topbar{height:82px;margin:0 44px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;font-size:13px;color:var(--muted);gap:12px}.service-pill{display:flex;align-items:center;gap:8px;border:1px solid var(--line);padding:7px 12px;border-radius:20px;text-decoration:none;font-size:12px;background:#fff}main{width:100%;max-width:1240px;padding:44px;margin:0 auto;flex:1;min-width:0}footer{border-top:1px solid var(--line);margin:0 44px;padding:18px 0;color:#728075;display:flex;justify-content:space-between;gap:16px;font-size:11px}.eyebrow{text-transform:uppercase;letter-spacing:1.8px;font-size:10px;font-weight:650;color:#648570;margin-bottom:14px;display:block}.welcome{position:relative;overflow:hidden;padding:42px 44px 38px;background:#f0ebe2;border-radius:16px;margin-bottom:38px}.welcome h1{font-size:44px;line-height:1.18;max-width:670px}.welcome p{max-width:490px;margin:20px 0 26px;font-size:15px}.welcome .button{padding:12px 20px}.welcome-index{position:absolute;right:22px;bottom:24px;writing-mode:vertical-rl;font-size:9px;letter-spacing:2px;color:#789079}.section-title{display:flex;justify-content:space-between;align-items:center;gap:15px;margin-bottom:20px}.section-title>span{font-size:12px;color:var(--muted)}.setup-list{border-top:1px solid var(--line)}.setup-row{display:flex;align-items:center;gap:22px;padding:23px 4px;text-decoration:none;color:inherit;border-bottom:1px solid var(--line)}.setup-row:hover{background:#f0f5ee}.step-number{height:40px;width:40px;display:grid;place-items:center;border-radius:50%;border:1px solid #d7e0d7;font-size:13px;flex-shrink:0}.step-number.done{background:#eee6dc;color:var(--green);border-color:transparent}.step-number svg{height:20px;width:20px;fill:none;stroke:currentColor;stroke-width:1.6}.setup-row p{font-size:13px;margin-top:4px}.step-state{margin-left:auto;font-size:12px;white-space:nowrap;color:var(--green)}.workspace-note{display:flex;gap:14px;margin-top:28px;align-items:center;max-width:700px;font-size:12px}.note-symbol{border:1px solid #a2b1a5;display:grid;place-items:center;border-radius:50%;width:20px;height:20px;flex-shrink:0}.page-title{margin-bottom:34px}.page-title p{margin-top:12px}.settings-grid{display:grid;grid-template-columns:minmax(0,1.6fr) minmax(240px,1fr);gap:30px}.surface{border:1px solid var(--line);background:#fff;border-radius:12px;padding:26px;margin-bottom:22px}.surface p{font-size:14px;margin:12px 0 18px}.tag{background:var(--soft);padding:4px 8px;border-radius:4px;white-space:nowrap}label{display:block;font-size:13px;font-weight:600;margin-bottom:8px}input,textarea,select{border:1px solid #bdcbc0;border-radius:7px;padding:11px 12px;background:#fff;width:100%;min-width:0}input{min-height:46px}.input-action{display:flex;gap:8px}.input-action button{white-space:nowrap}.text-button{border:0;background:transparent;padding-left:0;color:var(--green);font-size:13px}.guide{padding-left:20px;color:#52645a;font-size:14px;line-height:2}.surface .subtext{font-size:12px}.help-panel{padding:26px;background:#f3ede6;border-radius:12px}.help-panel h3{margin-top:24px;font-size:14px}.help-panel p{font-size:13px;margin:7px 0}.help-panel a{display:block;margin-top:26px;font-size:13px}.usage-note{padding:26px 0;font-size:12px}.usage-note h2{font-size:16px}.usage-note>strong{font-size:30px;display:block;margin:14px 0 8px;font-weight:550;font-variant-numeric:tabular-nums}.usage-note small{font-size:13px;font-weight:400;color:var(--muted)}.usage-note ul{list-style:none;padding:0}.usage-note li{display:flex;justify-content:space-between;gap:8px;padding:10px 0;border-bottom:1px solid var(--line);font-size:11px}.error{color:#a24032!important;font-size:13px;margin:12px 0}.error:empty{display:none}.chat-area{max-width:860px;margin:auto;min-height:calc(100dvh - 230px);display:flex;flex-direction:column}.chat-top{display:flex;justify-content:space-between;gap:20px;align-items:center;margin-bottom:24px}.chat-top h1{font-size:23px;letter-spacing:-.5px;overflow-wrap:anywhere}.chat-top .eyebrow{margin-bottom:8px}.chat-top button{flex-shrink:0}.chat-empty{text-align:center;margin:auto;padding:38px 0 46px}.chat-emblem{display:grid;place-items:center;width:58px;height:58px;margin:0 auto 22px;border-radius:18px;background:#eee6dc;color:var(--green)}.chat-emblem svg{width:27px;height:27px;stroke:currentColor;fill:none;stroke-width:1.3}.chat-empty h2{font-size:32px;line-height:1.3;font-weight:500;letter-spacing:-1px}.chat-empty p{font-size:14px;margin-top:18px;line-height:1.8}.suggestions{display:flex;gap:10px;margin-top:32px;flex-wrap:wrap;text-align:left}.suggestions button{flex:1;min-width:160px;max-width:260px;text-align:left;justify-content:space-between;align-items:start;font-size:12px;font-weight:400;padding:15px;background:#fff;line-height:1.7}.suggestions span{color:var(--green)}.composer{border:1px solid #bdcdbd;background:#fff;border-radius:14px;box-shadow:0 4px 18px #3e583208;padding:16px;margin-top:20px}.composer textarea{resize:vertical;max-height:240px;border:0;padding:0;border-radius:0;font-size:15px;min-height:65px}.composer textarea:focus-visible{outline:0}.composer:focus-within{border-color:var(--green);box-shadow:0 0 0 2px #cbdccc}.composer-tools{display:flex;align-items:center;gap:12px;justify-content:space-between;margin-top:10px}.composer-tools>div{display:flex;gap:16px;align-items:center;min-width:0;flex-wrap:wrap}.composer select{font-size:12px;width:auto;max-width:220px;padding:7px;border-color:var(--line);background:#f5f7f3;min-height:36px}.key-hint{font-size:10px;color:#8a968c}.send{width:42px;min-height:42px;padding:8px;border-radius:9px;margin-left:auto}.chat-footnote{text-align:center;font-size:10px;margin-top:12px}.setup-reminder{padding:13px 16px;border-radius:8px;background:#f2efdf;font-size:12px}.setup-reminder a{font-weight:600}.messages{overflow:auto;max-height:56dvh;scrollbar-width:thin}.messages:empty{display:none}.message{padding:18px 0;line-height:1.8}.message.user{background:#f3ede6;padding:18px 22px;border-radius:10px;margin:12px 0}.speaker{font-size:11px;font-weight:650;color:var(--green);display:block;margin-bottom:6px}.message-text{white-space:pre-wrap;overflow-wrap:anywhere;font-size:15px}.interrupted{color:#9b6543;font-size:11px}.opening{min-height:100dvh;display:flex;flex-direction:column;gap:24px;justify-content:center;align-items:center;text-align:center;max-width:700px}.opening h1{font-size:32px}.opening code{padding:14px 30px;background:var(--soft);border-radius:8px}#notice{position:fixed;bottom:22px;left:50%;transform:translateX(-50%);max-width:calc(100% - 32px);padding:14px 22px;background:#243b30;color:white;border-radius:10px;z-index:20;font-size:13px}
2
+ @media(min-width:1600px){main{padding-top:60px}.welcome{padding:55px}.welcome h1{font-size:52px}}
3
+ @media(max-width:1100px){.workspace{grid-template-columns:220px minmax(0,1fr)}main{padding:30px}.topbar,footer{margin-left:30px;margin-right:30px}.welcome{padding:30px}.welcome h1{font-size:36px}.settings-grid{grid-template-columns:1fr}.settings-grid>aside{display:grid;grid-template-columns:1fr 1fr;gap:24px}.usage-note{padding:24px 0}.key-hint{display:none}.brand{font-size:18px}}
4
+ @media(max-width:700px){.workspace{display:block}.rail{height:auto;position:static;padding:16px;border-right:0;border-bottom:1px solid var(--line);display:grid;grid-template-columns:1fr auto;gap:14px}.brand small{font-size:10px}.new-chat{margin:0;font-size:0;width:44px;padding:10px}.new-chat svg{width:20px}.rail nav{grid-column:1/-1;display:flex;justify-content:space-between;gap:4px}nav a{padding:9px;font-size:12px;gap:7px}nav svg{width:17px}.history-heading,.history,.rail-foot{display:none}.topbar{margin:0 20px;height:63px;font-size:12px}.service-pill{font-size:10px;padding:6px 10px}main{padding:24px 20px}.welcome{padding:28px 23px;margin-bottom:28px}.welcome h1{font-size:32px}.welcome p{font-size:13px}.welcome-index{display:none}.section-title{gap:10px}.section-title h2{font-size:18px}.section-title>span{font-size:10px}.setup-row{gap:12px;padding:20px 0;align-items:start}.step-state{font-size:0}.step-state:after{content:'→';font-size:17px}.setup-row h3{font-size:14px}.setup-row p{font-size:12px}.step-number{width:32px;height:32px}.workspace-note{align-items:start}footer{margin:0 20px;font-size:10px}footer span{max-width:165px;text-align:right}.settings-grid>aside{display:block}.surface{padding:20px}.input-action{flex-wrap:wrap}.input-action button{width:100%}.page-title h1{font-size:30px}.chat-top{flex-wrap:wrap}.chat-top h1{font-size:20px}.chat-empty{padding:26px 0}.chat-empty h2{font-size:27px}.chat-empty p{font-size:12px}.suggestions{margin-top:24px;display:grid}.suggestions button{max-width:none;width:100%;padding:12px 16px}.chat-area{min-height:calc(100dvh - 300px)}.composer{padding:14px}.composer textarea{font-size:16px}.composer select{max-width:190px}.chat-footnote{font-size:9px}.messages{max-height:50dvh}.opening{padding:30px}}
5
+ @media(prefers-reduced-motion:reduce){*{transition:none!important;scroll-behavior:auto!important}}
6
+
7
+ .mobile-history{display:none}@media(max-width:700px){.mobile-history{display:block;margin-bottom:20px}.mobile-history label{font-size:11px;color:var(--muted)}.mobile-history select{max-width:100%;font-size:12px}}
8
+
9
+ :root{font-family:"Geist Variable","Segoe UI",sans-serif}.welcome h1,.chat-empty h2{font-family:"Newsreader Variable",Georgia,serif;font-weight:450}.primary,.send,.mark{background:#292824;border-color:#292824}.mark{font-size:20px;letter-spacing:-1px}.chat-empty h2{font-size:38px}.welcome h1{font-size:50px}.eyebrow{color:#8b7560}@media(max-width:700px){.welcome h1{font-size:36px}.chat-empty h2{font-size:30px}}
10
+
11
+ .privacy-choice{font-size:12px;font-weight:400;line-height:1.8;color:var(--muted);margin-bottom:20px}.privacy-choice input{width:17px;min-height:17px;vertical-align:middle;margin-right:8px}
12
+
13
+ .mark{font-size:0;background:#292824 url("/app/assets/mark.svg") center/cover no-repeat}
14
+ .settings-actions{display:flex;align-items:center;flex-wrap:wrap;gap:10px 16px;margin-top:16px}
15
+ .settings-actions .button,.settings-actions button{min-height:44px;font-size:13px}
16
+ .connection-warning{padding:14px 16px;background:#f4ede1;border-left:2px solid #b18a62;color:#6a5138;font-size:13px!important;line-height:1.8;border-radius:0 6px 6px 0}
17
+ .connection-checks{list-style:none;padding:0;margin:20px 0 0;border-top:1px solid var(--line)}
18
+ .connection-checks li{display:flex;justify-content:space-between;gap:16px;padding:12px 0;border-bottom:1px solid var(--line);font-size:12px;color:var(--muted)}
19
+ .connection-checks strong{font-weight:500;color:var(--ink);text-align:right}
20
+ .startup-choice{display:flex;justify-content:space-between;align-items:center;gap:20px;font-size:14px;font-weight:400}
21
+ .startup-choice strong{font-weight:550}.startup-choice small{display:block;margin-top:6px;color:var(--muted);font-size:12px;line-height:1.7;font-weight:400}
22
+ .startup-choice input{width:20px;height:20px;min-height:20px;flex-shrink:0;accent-color:#292824;cursor:pointer}
23
+ .update-help{margin-top:24px;border-top:1px solid var(--line);font-size:13px}.update-help summary{cursor:pointer;padding:16px 0;min-height:44px;color:#73533e}.update-help p{font-size:13px}
24
+ .stop-dialog{width:min(480px,calc(100% - 40px));max-height:calc(100dvh - 40px);overflow:auto;margin:auto;padding:28px;border:1px solid #ded7cb;border-radius:14px;background:#faf9f6;color:#292824}
25
+ .stop-dialog::backdrop{background:#29282470}.stop-dialog h2{font-size:23px}.stop-dialog p{font-size:14px;line-height:1.8;margin-top:16px}.stop-dialog .settings-actions{justify-content:flex-end;margin-top:24px}
26
+ .local-quota-summary{margin-bottom:32px}.local-quota-summary h2{overflow-wrap:anywhere;font-size:20px}.local-quota-summary .section-title{align-items:start}
27
+ .local-quota-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
28
+ .local-quota-window{min-width:0;padding:18px;background:#f5f2ec;border:1px solid #e9e2d7;border-radius:9px;font-variant-numeric:tabular-nums}
29
+ .local-quota-window>span{font-size:12px;color:#7c7163}.local-quota-window>strong{display:block;font-size:25px;font-weight:550;letter-spacing:-.7px;margin:12px 0;color:#33362d}.local-quota-window strong small{font-size:11px;letter-spacing:0;color:#817464;font-weight:400}
30
+ .local-quota-window progress{display:block;width:100%;height:6px;margin:16px 0;accent-color:#647f68}
31
+ .local-quota-window p,.local-quota-window>small{font-size:11px;line-height:1.8;color:#817464;overflow-wrap:anywhere;margin:8px 0}
32
+ .local-quota-window.quota-warning progress{accent-color:#a35b3e}.local-quota-window .quota-stale{color:#9b633e;font-weight:500}
33
+ .pool-accounts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px;margin-bottom:28px}.pool-accounts>.surface{margin:0;min-width:0}
34
+ .pool-active{border-color:#b49b7a;box-shadow:inset 0 3px #b49b7a}.pool-account .section-title{align-items:start}.pool-account .section-title>div{min-width:0}.pool-account h2{font-size:20px;overflow-wrap:anywhere}.pool-email{font-size:12px!important;overflow-wrap:anywhere;margin-top:6px!important;color:#817464}.pool-account .tag{white-space:nowrap}
35
+ .pool-account .settings-actions{gap:10px 12px}.pool-account .settings-actions button{font-size:12px;min-height:42px}.pool-account .local-quota-window strong{font-size:23px}.pool-account .local-quota-window strong small{display:block;margin-top:4px}
36
+ .pool-add{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:32px}.pool-add h2{font-size:26px}.pool-add>div>p{font-size:14px;line-height:1.8}.pool-add label{margin-top:14px}.pool-add label:first-child{margin-top:0}.pool-add form>button{min-height:46px;margin-top:8px}
37
+ .pool-login-pending{padding:28px;background:#f1ece1;border:1px solid #ded1b9;border-radius:12px;margin-bottom:24px}.pool-login-pending h2{font-size:25px;margin:10px 0}.pool-login-pending>p{font-size:14px;line-height:1.8}.device-code{display:flex;align-items:center;gap:16px;margin:20px 0;flex-wrap:wrap}.device-code code{font-size:24px;letter-spacing:2px}.device-code button{min-height:44px}.pool-success{padding:14px 18px;background:#edf1e8;border-radius:8px;color:#506744;font-size:14px}.pool-footnote{font-size:12px;line-height:1.8;color:#817464;max-width:85ch}
38
+ .chat-account-picker{display:flex;align-items:center;gap:12px;max-width:100%;margin:12px 0 20px;font-size:12px;color:#817464}.chat-account-picker label{margin:0;flex-shrink:0}.chat-account-picker select{width:auto;max-width:330px;min-width:0;font-size:12px;padding:9px 12px}.chat-account-picker a{margin-left:auto;white-space:nowrap;color:#8a6045}
39
+ .message-text.markdown-body{white-space:normal;overflow-wrap:anywhere;line-height:1.8;min-width:0}
40
+ .markdown-body>*:first-child{margin-top:0}.markdown-body>*:last-child{margin-bottom:0}.markdown-body p{margin:12px 0;color:inherit;font-size:inherit;line-height:inherit}
41
+ .markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{font-family:inherit;letter-spacing:-.3px;font-weight:600;line-height:1.4;margin:24px 0 12px}.markdown-body h1{font-size:24px}.markdown-body h2{font-size:21px}.markdown-body h3{font-size:18px}.markdown-body h4,.markdown-body h5,.markdown-body h6{font-size:16px}
42
+ .markdown-body ul,.markdown-body ol{margin:12px 0;padding-left:24px}.markdown-body li+li{margin-top:6px}.markdown-body blockquote{margin:16px 0;padding:4px 0 4px 18px;border-left:3px solid #c9b89e;color:#807461}.markdown-body hr{margin:24px 0;border:0;border-top:1px solid #ded6c9}
43
+ .markdown-body a{color:#91583c;text-underline-offset:3px}.markdown-body code{font:12px/1.8 ui-monospace,SFMono-Regular,Consolas,monospace;background:#ede7dc;padding:2px 5px;border-radius:4px}
44
+ .markdown-body pre{max-width:100%;overflow:auto;max-height:32rem;margin:16px 0;padding:18px;background:#f3eee5;border:1px solid #dfd4c1;border-radius:8px;white-space:pre;overscroll-behavior:contain}
45
+ .markdown-body pre code{padding:0;background:none;border-radius:0;white-space:pre;overflow-wrap:normal;word-break:normal;color:#464035}
46
+ .markdown-code{max-width:100%;min-width:0;border:1px solid #dfd4c1;border-radius:9px;overflow:hidden;margin:18px 0;background:#f3eee5}.markdown-code-head{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:8px 14px;border-bottom:1px solid #e2d8c8;font-size:11px;color:#817464}.markdown-code-head button{font-size:11px;min-height:34px;padding:6px 10px;border-color:#d9cebb;background:#faf7f0}.markdown-code pre{margin:0;border:0;border-radius:0}
47
+ .markdown-body table{display:block;width:max-content;max-width:100%;overflow-x:auto;border-collapse:collapse;margin:18px 0;font-size:13px}.markdown-body th,.markdown-body td{padding:10px 14px;border:1px solid #ddd4c5;text-align:left;min-width:100px}.markdown-body th{background:#eee7db;font-weight:550}.markdown-image{color:#817464;font-style:italic}
48
+ @media(max-width:1150px){.pool-accounts{grid-template-columns:minmax(0,1fr)}}
49
+ @media(max-width:700px){.rail nav{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));width:100%}.rail nav a{flex-direction:column;justify-content:center;gap:5px;text-align:center;min-height:56px;padding:8px 3px;font-size:11px;line-height:1.3}.pool-add{grid-template-columns:minmax(0,1fr);gap:20px}.pool-add input,.pool-add select{font-size:16px}.pool-login-pending{padding:20px}.local-quota-window{padding:14px 12px}.local-quota-window>strong{font-size:22px}.local-quota-window strong small{display:block;margin-top:4px}.pool-account .settings-actions button{min-height:44px}.local-quota-summary h2{font-size:17px}.local-quota-summary .section-title{flex-direction:column}.pool-account .section-title h2{font-size:18px}.chat-account-picker{flex-wrap:wrap;gap:8px 12px}.chat-account-picker select{flex:1;max-width:100%;font-size:14px}.chat-account-picker a{min-height:40px;display:flex;align-items:center}.markdown-code-head button{min-height:40px}.markdown-body table{font-size:12px}.markdown-body th,.markdown-body td{padding:8px 10px}.markdown-body h1{font-size:22px}.markdown-body h2{font-size:19px}}
50
+
51
+ /* Compact version information and update instructions at the foot of the rail. */
52
+ .rail{overflow-y:auto}.history{flex:1;min-height:48px}.rail-footer{margin-top:auto;flex-shrink:0;padding-top:20px}.rail-footer .rail-foot{padding:0 10px 12px;margin:0;font-size:11px}.update-details{border-top:1px solid var(--line);font-size:12px}.update-details summary{position:relative;padding:14px 26px 4px 10px;cursor:pointer;list-style:none;min-height:54px;border-radius:5px}.update-details summary::-webkit-details-marker{display:none}.update-details summary:hover{background:#ebe7df}.update-details summary:focus-visible{outline:2px solid #8b7560;outline-offset:2px}.update-current{display:block;color:var(--muted);font-size:11px;font-variant-numeric:tabular-nums}.update-state{display:block;font-size:11px;margin-top:5px;line-height:1.6;color:#78736a;overflow-wrap:anywhere}.update-state.update-available{color:#725035;font-weight:550}.update-available:before{content:'';display:inline-block;width:6px;height:6px;background:#a2754e;border-radius:50%;margin-right:6px;vertical-align:middle}.update-chevron{position:absolute;right:10px;top:17px;color:#8b7560}.update-details[open] .update-chevron{transform:rotate(180deg)}.update-content{padding:8px 10px 0}.update-content p,.update-content small{display:block;font-size:11px;line-height:1.7;color:var(--muted);margin:4px 0 10px}.update-command{margin:12px 0;padding:10px;background:#eae6de;border-radius:5px;white-space:pre-wrap;overflow-wrap:anywhere;color:#403b32;font:11px/1.9 Consolas,monospace;user-select:all}.update-content button{font-size:11px;min-height:34px;padding:7px 9px;max-width:100%;width:100%;margin-bottom:6px}.update-content .check-update{border:0;background:transparent;color:#78634d}.update-content small{font-size:10px;margin-top:6px}
53
+ @media(max-width:700px){.rail-footer{grid-column:1/-1;padding-top:0;width:100%}.rail-footer .rail-foot{display:none}.update-details summary{padding:10px 28px 8px 0;min-height:44px}.update-current,.update-state{display:inline-block}.update-state{margin:0 0 0 12px}.update-chevron{top:13px}.update-content{padding-left:0;max-width:420px}.update-command{font-size:12px}.update-content button{min-height:40px}.update-content p,.update-content small{font-size:12px}}
package/web/updates.js ADDED
@@ -0,0 +1,26 @@
1
+ const escape=value=>String(value??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
2
+ export function mountUpdates(element,{api,version,notice}){
3
+ let data=null,checking=false,disposed=false,lastCheck=0;
4
+ function render(){
5
+ if(disposed)return;
6
+ const expanded=element.querySelector('details')?.open??false;
7
+ const message=checking?'Đang kiểm tra phiên bản…':data?.status==='unavailable'?'Chưa kiểm tra được phiên bản':data?.available?'Có bản mới · '+data.latestVersion:'Chưa có bản cập nhật mới';
8
+ element.innerHTML='<details class="update-details"'+(expanded?' open':'')+'><summary><span class="update-current">Phiên bản '+escape(version)+'</span><span class="update-state'+(data?.available?' update-available':'')+'" role="status">'+escape(message)+'</span><span class="update-chevron" aria-hidden="true">⌃</span></summary><div class="update-content"><p>Chạy lần lượt trong PowerShell hoặc Terminal. Sau khi cập nhật, ứng dụng sẽ mở lại.</p>'+(data?.installCommand&&data?.restartCommand?'<pre class="update-command"><code>'+escape(data.installCommand)+'\n'+escape(data.restartCommand)+'</code></pre><button type="button" class="copy-update">Sao chép lệnh cập nhật</button>':'')+'<button type="button" class="check-update" '+(checking?'disabled':'')+'>Kiểm tra lại</button><small>Đóng cuộc trò chuyện đang chạy trước khi khởi động lại.</small></div></details>';
9
+ element.querySelector('.check-update').onclick=()=>check();
10
+ const copy=element.querySelector('.copy-update');
11
+ if(copy)copy.onclick=async()=>{
12
+ try{await navigator.clipboard.writeText(data.installCommand+'\n'+data.restartCommand);copy.textContent='Đã sao chép';notice('Đã sao chép 2 lệnh cập nhật. Chạy lần lượt trong PowerShell hoặc Terminal.');}
13
+ catch{notice('Chưa sao chép được. Bạn có thể chọn và sao chép 2 dòng lệnh bên trên.');}
14
+ };
15
+ }
16
+ async function check(){
17
+ if(checking||disposed)return;
18
+ checking=true;render();
19
+ try{data=await api('updates');}catch{data={...data,status:'unavailable',available:false};}
20
+ lastCheck=Date.now();checking=false;render();
21
+ }
22
+ const onVisible=()=>{if(document.visibilityState==='visible'&&Date.now()-lastCheck>=3600000)check();};
23
+ document.addEventListener('visibilitychange',onVisible);
24
+ const timer=setInterval(onVisible,3600000);check();
25
+ return ()=>{disposed=true;clearInterval(timer);document.removeEventListener('visibilitychange',onVisible);};
26
+ }
package/windows.mjs ADDED
@@ -0,0 +1,53 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import {spawn} from 'node:child_process';
5
+ import {fileURLToPath} from 'node:url';
6
+ import {VERSION} from './product.mjs';
7
+
8
+ export function createWindowsIntegration(state,{entry=fileURLToPath(new URL('./cli.mjs',import.meta.url)),startupDirectory,platform=process.platform}={}){
9
+ const supported=platform==='win32',identifier=crypto.createHash('sha256').update(state.home.toLowerCase()).digest('hex').slice(0,12);
10
+ const startupFolder=startupDirectory??path.join(process.env.APPDATA??path.join(state.home,'AppData'),'Microsoft/Windows/Start Menu/Programs/Startup');
11
+ const shortcut=path.join(startupFolder,'Codex Unlimit-'+identifier+'.lnk'),configPath=path.join(state.home,'windows-startup.json');
12
+ const script=fileURLToPath(new URL('./windows.ps1',import.meta.url));
13
+ const executable=path.join(process.env.SystemRoot??'C:/Windows','System32/WindowsPowerShell/v1.0/powershell.exe');
14
+ const config={node:process.execPath,entry,home:state.home,version:VERSION,parentPid:process.pid,shortcut,configPath,script,icon:fileURLToPath(new URL('./web/assets/favicon.ico',import.meta.url))};
15
+ let tray=null,trayReady=false,pendingTray=null;
16
+ function invoke(mode,input){
17
+ return new Promise((resolve,reject)=>{
18
+ const child=spawn(executable,['-NoProfile','-NonInteractive','-WindowStyle','Hidden','-ExecutionPolicy','Bypass','-File',script,'-Mode',mode],{windowsHide:true,stdio:['pipe','pipe','pipe']});
19
+ let output='';const timeout=setTimeout(()=>{child.kill();reject(Error('startup_update_failed'));},10000);
20
+ child.stdout.on('data',chunk=>{output=(output+chunk).slice(-4096);});child.stderr.resume();
21
+ child.once('error',()=>{clearTimeout(timeout);reject(Error('startup_update_failed'));});
22
+ child.once('exit',code=>{clearTimeout(timeout);if(code!==0)reject(Error('startup_update_failed'));else resolve(output);});
23
+ child.stdin.on('error',()=>{});child.stdin.end(JSON.stringify(input));
24
+ });
25
+ }
26
+ async function setStartup(enabled){
27
+ if(!supported)throw Error('startup_unavailable');
28
+ if(enabled){
29
+ const temporary=configPath+'.tmp-'+process.pid;
30
+ fs.writeFileSync(temporary,JSON.stringify(config),{mode:0o600});
31
+ fs.renameSync(temporary,configPath);
32
+ }
33
+ await invoke('Configure',{...config,enabled});
34
+ if(fs.existsSync(shortcut)!==enabled)throw Error('startup_update_failed');
35
+ }
36
+ function showTray(){
37
+ if(!supported)return Promise.reject(Error('tray_unavailable'));
38
+ if(trayReady&&tray?.exitCode===null)return Promise.resolve();
39
+ if(pendingTray)return pendingTray;
40
+ pendingTray=new Promise((resolve,reject)=>{
41
+ const child=spawn(executable,['-NoProfile','-NonInteractive','-STA','-WindowStyle','Hidden','-ExecutionPolicy','Bypass','-File',script,'-Mode','Tray'],{windowsHide:true,stdio:['pipe','pipe','pipe']});
42
+ tray=child;let output='',settled=false;
43
+ const timeout=setTimeout(()=>{child.kill();finish(Error('tray_unavailable'));},15000);
44
+ function finish(error){if(settled)return;settled=true;clearTimeout(timeout);error?reject(error):resolve();}
45
+ child.stdout.on('data',chunk=>{output=(output+chunk).slice(-1024);if(output.includes('CU_TRAY_READY')){trayReady=true;finish();}});
46
+ child.stderr.resume();child.once('error',()=>finish(Error('tray_unavailable')));
47
+ child.once('exit',()=>{if(tray===child){trayReady=false;tray=null;}finish(Error('tray_unavailable'));});
48
+ child.stdin.on('error',()=>{});child.stdin.end(JSON.stringify(config));child.unref();
49
+ }).finally(()=>{pendingTray=null;});
50
+ return pendingTray;
51
+ }
52
+ return {status:()=>({supported,startup:supported&&fs.existsSync(shortcut),tray:trayReady}),setStartup,showTray};
53
+ }
package/windows.ps1 ADDED
@@ -0,0 +1,103 @@
1
+ param([ValidateSet('Tray','Configure','Startup')][string]$Mode='Tray',[string]$ConfigPath)
2
+ $ErrorActionPreference='Stop'
3
+ [Console]::InputEncoding=[Text.UTF8Encoding]::new($false)
4
+ [Console]::OutputEncoding=[Text.UTF8Encoding]::new($false)
5
+ function Quote-Argument([string]$Value) {
6
+ '"'+[regex]::Replace([regex]::Replace($Value,'(\\*)"','$1$1\"'),'(\\+)$','$1$1')+'"'
7
+ }
8
+ function Start-Client([string[]]$ClientArguments) {
9
+ $start=[Diagnostics.ProcessStartInfo]::new()
10
+ $start.FileName=$script:configuration.node
11
+ $start.Arguments=(@($script:configuration.entry)+$ClientArguments+@('--home',$script:configuration.home) | ForEach-Object { Quote-Argument $_ }) -join ' '
12
+ $start.UseShellExecute=$false
13
+ $start.CreateNoWindow=$true
14
+ $start.RedirectStandardOutput=$true
15
+ $start.RedirectStandardError=$true
16
+ [Diagnostics.Process]::Start($start)
17
+ }
18
+ if($Mode -eq 'Startup') {
19
+ $script:configuration=Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json
20
+ if(!(Test-Path -LiteralPath $configuration.node -PathType Leaf) -or !(Test-Path -LiteralPath $configuration.entry -PathType Leaf)) { exit 1 }
21
+ $arguments=(@($configuration.entry,'open','--no-open','--home',$configuration.home) | ForEach-Object { Quote-Argument $_ }) -join ' '
22
+ Start-Process -FilePath $configuration.node -ArgumentList $arguments -WorkingDirectory $configuration.home -WindowStyle Hidden
23
+ exit 0
24
+ }
25
+ $script:configuration=[Console]::In.ReadToEnd() | ConvertFrom-Json
26
+ if($Mode -eq 'Configure') {
27
+ if($configuration.enabled) {
28
+ $parent=Split-Path -Parent $configuration.shortcut
29
+ [IO.Directory]::CreateDirectory($parent) | Out-Null
30
+ $shell=New-Object -ComObject WScript.Shell
31
+ try {
32
+ $link=$shell.CreateShortcut($configuration.shortcut)
33
+ $link.TargetPath=Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
34
+ $link.Arguments='-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File '+(Quote-Argument $configuration.script)+' -Mode Startup -ConfigPath '+(Quote-Argument $configuration.configPath)
35
+ $link.WorkingDirectory=$configuration.home
36
+ $link.WindowStyle=7
37
+ $link.Description='Mở Codex Unlimit trong khay Windows'
38
+ $link.IconLocation=$configuration.icon
39
+ $link.Save()
40
+ } finally { [Runtime.InteropServices.Marshal]::FinalReleaseComObject($shell) | Out-Null }
41
+ } elseif(Test-Path -LiteralPath $configuration.shortcut) { Remove-Item -LiteralPath $configuration.shortcut }
42
+ [Console]::Out.WriteLine('CU_STARTUP_UPDATED')
43
+ exit 0
44
+ }
45
+ Add-Type -AssemblyName System.Windows.Forms
46
+ Add-Type -AssemblyName System.Drawing
47
+ [Windows.Forms.Application]::EnableVisualStyles()
48
+ $script:parentProcess=[Diagnostics.Process]::GetProcessById($configuration.parentPid)
49
+ $script:notify=[Windows.Forms.NotifyIcon]::new()
50
+ $sourceIcon=[Drawing.Icon]::new($configuration.icon)
51
+ $notify.Icon=$sourceIcon.Clone()
52
+ $sourceIcon.Dispose()
53
+ $notify.Text=('Codex Unlimit · '+$configuration.version)
54
+ $menu=[Windows.Forms.ContextMenuStrip]::new()
55
+ $title=$menu.Items.Add('Codex Unlimit · '+$configuration.version)
56
+ $title.Enabled=$false
57
+ [void]$menu.Items.Add([Windows.Forms.ToolStripSeparator]::new())
58
+ $openItem=$menu.Items.Add('Mở Codex Unlimit')
59
+ $script:startupItem=$menu.Items.Add('Khởi động cùng Windows')
60
+ $startupItem.Checked=Test-Path -LiteralPath $configuration.shortcut
61
+ $restartItem=$menu.Items.Add('Khởi động lại')
62
+ [void]$menu.Items.Add([Windows.Forms.ToolStripSeparator]::new())
63
+ $exitItem=$menu.Items.Add('Tắt ứng dụng')
64
+ $script:pendingClient=$null
65
+ function Invoke-Client([string[]]$ClientArguments) {
66
+ if($script:pendingClient -and !$script:pendingClient.HasExited) { return }
67
+ if($script:pendingClient) { $script:pendingClient.Dispose() }
68
+ try { $script:pendingClient=Start-Client -ClientArguments $ClientArguments }
69
+ catch { $notify.ShowBalloonTip(5000,'Codex Unlimit','Chưa thực hiện được thao tác. Mở trang ứng dụng để kiểm tra.',[Windows.Forms.ToolTipIcon]::Warning) }
70
+ }
71
+ $openItem.Add_Click({ Invoke-Client -ClientArguments @('open') })
72
+ $notify.Add_DoubleClick({ Invoke-Client -ClientArguments @('open') })
73
+ $startupItem.Add_Click({ if($startupItem.Checked) { Invoke-Client -ClientArguments @('startup','off') } else { Invoke-Client -ClientArguments @('startup','on') } })
74
+ $restartItem.Add_Click({ Invoke-Client -ClientArguments @('restart') })
75
+ $exitItem.Add_Click({ Invoke-Client -ClientArguments @('stop') })
76
+ $menu.Add_Opening({ $startupItem.Checked=Test-Path -LiteralPath $configuration.shortcut })
77
+ $notify.ContextMenuStrip=$menu
78
+ $timer=[Windows.Forms.Timer]::new()
79
+ $timer.Interval=700
80
+ $timer.Add_Tick({
81
+ if($parentProcess.HasExited) {
82
+ if($script:pendingClient -and !$script:pendingClient.HasExited) { return }
83
+ [Windows.Forms.Application]::Exit();return
84
+ }
85
+ if($script:pendingClient -and $script:pendingClient.HasExited) {
86
+ if($script:pendingClient.ExitCode -ne 0) { $notify.ShowBalloonTip(5000,'Chưa hoàn tất thao tác','Mở Codex Unlimit để kiểm tra kết nối hoặc tắt ứng dụng từ phần Cài đặt & hỗ trợ.',[Windows.Forms.ToolTipIcon]::Warning) }
87
+ $script:pendingClient.Dispose()
88
+ $script:pendingClient=$null
89
+ $startupItem.Checked=Test-Path -LiteralPath $configuration.shortcut
90
+ }
91
+ })
92
+ try {
93
+ $notify.Visible=$true
94
+ $timer.Start()
95
+ [Console]::Out.WriteLine('CU_TRAY_READY')
96
+ [Console]::Out.Flush()
97
+ [Windows.Forms.Application]::Run()
98
+ } finally {
99
+ $timer.Stop();$timer.Dispose()
100
+ $notify.Visible=$false
101
+ $notify.Icon.Dispose();$notify.Dispose();$menu.Dispose();$parentProcess.Dispose()
102
+ if($script:pendingClient) { $script:pendingClient.Dispose() }
103
+ }