codexunlimit 0.0.0-stage → 0.1.0-web.8

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 (42) hide show
  1. package/README.md +75 -3
  2. package/account-pool.mjs +161 -0
  3. package/app.mjs +155 -0
  4. package/cli.mjs +95 -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 +131 -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 +82 -0
  17. package/state.mjs +23 -0
  18. package/web/app.js +152 -0
  19. package/web/assets/apple-touch-icon.png +0 -0
  20. package/web/assets/favicon-16.png +0 -0
  21. package/web/assets/favicon-32.png +0 -0
  22. package/web/assets/favicon.ico +0 -0
  23. package/web/assets/fonts.css +48 -0
  24. package/web/assets/geist-LICENSE.txt +93 -0
  25. package/web/assets/geist-latin-ext-wght-normal.woff2 +0 -0
  26. package/web/assets/geist-latin-wght-normal.woff2 +0 -0
  27. package/web/assets/geist-vietnamese-wght-normal.woff2 +0 -0
  28. package/web/assets/icon-192.png +0 -0
  29. package/web/assets/icon-512.png +0 -0
  30. package/web/assets/mark.svg +1 -0
  31. package/web/assets/newsreader-LICENSE.txt +93 -0
  32. package/web/assets/newsreader-latin-ext-wght-normal.woff2 +0 -0
  33. package/web/assets/newsreader-latin-wght-normal.woff2 +0 -0
  34. package/web/assets/newsreader-vietnamese-wght-normal.woff2 +0 -0
  35. package/web/index.html +3 -0
  36. package/web/markdown.js +15 -0
  37. package/web/messages.js +62 -0
  38. package/web/pool-ui.js +53 -0
  39. package/web/stream.js +25 -0
  40. package/web/style.css +49 -0
  41. package/windows.mjs +53 -0
  42. package/windows.ps1 +103 -0
package/web/app.js ADDED
@@ -0,0 +1,152 @@
1
+ import {readResponseStream} from './stream.js';
2
+ import {renderMarkdown} from './markdown.js';
3
+ import {mountAccounts,quotaSummary,accountOptions} from './pool-ui.js';
4
+ const $=s=>document.querySelector(s),esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
5
+ const date=v=>v?new Date(v).toLocaleString('vi-VN'):'Chưa ghi nhận';
6
+ const icons={home:'M3 10 12 3l9 7v10H3z M9 20v-7h6v7',chat:'M4 4h16v12H9l-5 4z',settings:'M5 6h14 M5 12h14 M5 18h14 M9 3v6 M15 9v6 M9 15v6',arrow:'m7 10 5-5 5 5 M12 5v14',plus:'M12 5v14 M5 12h14',check:'m5 12 4 4L19 6',link:'m9 15 6-6 M8 16l-2 2a4 4 0 0 1-6-6l4-4 M16 8l2-2a4 4 0 0 1 6 6l-4 4'};
7
+ const icon=name=>'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="'+icons[name]+'"/></svg>';
8
+ import {errors} from './messages.js';
9
+ let status,csrf='',route='home',models=[],busy=false,controller,current=null,modelLoad=0,renderFrame=null,stopped=false;
10
+ let disposeAccounts=null;
11
+ let conversations=[];try{const saved=JSON.parse(localStorage.getItem('cu.conversations')??'[]');if(Array.isArray(saved))conversations=saved.filter(c=>c&&typeof c.id==='string'&&typeof c.title==='string'&&Array.isArray(c.messages)).slice(0,30).map(c=>({...c,messages:c.messages.filter(m=>m&&['user','assistant'].includes(m.role)&&typeof m.content==='string').slice(-100)}));}catch{}
12
+ function save(){try{localStorage.setItem('cu.conversations',JSON.stringify(conversations));}catch{notice('Trình duyệt chưa lưu được lịch sử. Giữ trang này mở để tiếp tục.');}}
13
+ function notice(message){$('#notice').textContent=message;$('#notice').hidden=false;setTimeout(()=>{$('#notice').hidden=true;},6000);}
14
+ async function api(name,data){
15
+ let response;try{response=await fetch('/app/api/'+name,{method:data===undefined?'GET':'POST',credentials:'same-origin',headers:data===undefined?{}:{'content-type':'application/json','x-csrf-token':csrf},body:data===undefined?undefined:JSON.stringify(data),signal:AbortSignal.timeout(65000)});}catch{throw Error(errors.local_service_unavailable);}
16
+ const result=await response.json();if(!response.ok)throw Error(errors[result.error]??'Chưa hoàn tất thao tác. Vui lòng thử lại hoặc liên hệ người cấp mã.');return result;
17
+ }
18
+ async function refresh(){status=await api('status');csrf=status.csrf;}
19
+ const licenseLabel=()=>status.license.active?'Dịch vụ đã kích hoạt':status.license.activationRequired?'Chưa kích hoạt':'Cần kiểm tra dịch vụ';
20
+ function shell(){
21
+ $('#app').innerHTML='<div class="workspace"><aside class="rail"><a class="brand" href="#home"><span class="mark">cu</span><span>Codex Unlimit<small>Không gian làm việc</small></span></a><button class="new-chat" id="new-chat">'+icon('plus')+'Cuộc trò chuyện mới</button><nav aria-label="Điều hướng">'+[['home','home','Bắt đầu'],['chat','chat','Trò chuyện'],['accounts','link','Tài khoản & quota'],['settings','settings','Cài đặt & hỗ trợ']].map(([key,i,label])=>'<a data-route="'+key+'" href="#'+key+'">'+icon(i)+label+'</a>').join('')+'</nav><div class="history-heading">Gần đây</div><div id="history" class="history"></div><div class="rail-foot"><span class="device-dot"></span><div>Đang chạy trên máy này<small>Codex Unlimit · '+esc(status.version)+'</small></div></div></aside><div class="body"><header class="topbar"><span id="breadcrumb">Không gian của bạn</span><a class="service-pill" href="#settings"><span class="device-dot"></span>'+licenseLabel()+'</a></header><main id="main" tabindex="-1"></main><footer>Codex Unlimit <span>Gọn gàng hơn cho công việc mỗi ngày.</span></footer></div></div>';
22
+ $('#new-chat').onclick=()=>{if(busy){notice('Dừng câu trả lời hiện tại trước khi mở cuộc trò chuyện mới.');return;}current=null;location.hash='chat';render();};render();
23
+ }
24
+ function history(){
25
+ $('#history').innerHTML=conversations.length?conversations.map(c=>'<button data-conversation="'+esc(c.id)+'" class="'+(current===c.id?'selected':'')+'">'+esc(c.title)+'</button>').join(''):'<p>Những cuộc trò chuyện của bạn sẽ xuất hiện ở đây.</p>';
26
+ document.querySelectorAll('[data-conversation]').forEach(b=>b.onclick=()=>{if(busy)return;current=b.dataset.conversation;location.hash='chat';render();});
27
+ }
28
+ function render(){
29
+ if(stopped)return;
30
+ disposeAccounts?.();disposeAccounts=null;
31
+ route=['home','chat','accounts','settings'].includes(location.hash.slice(1))?location.hash.slice(1):'home';
32
+ document.querySelectorAll('[data-route]').forEach(a=>a.setAttribute('aria-current',a.dataset.route===route?'page':'false'));
33
+ $('#breadcrumb').textContent=({home:'Bắt đầu sử dụng',chat:'Trò chuyện',accounts:'Tài khoản & quota',settings:'Cài đặt & hỗ trợ'})[route];history();
34
+ if(route==='home')home();else if(route==='settings')settings();else if(route==='accounts')disposeAccounts=mountAccounts($('#main'),{api,initial:status.accounts,isBusy:()=>busy,onChange:(data,selection)=>{status.accounts=data;if(selection){status.connected=data.rows.some(row=>row.active&&row.state==='ready');models=[];modelLoad++;}}});else chat();
35
+ }
36
+ const ready=()=>status.license.active&&status.connected;
37
+ function home(){
38
+ $('#main').innerHTML='<div class="welcome"><div class="eyebrow">Chào bạn, bắt đầu thôi</div><h1>Một nơi gọn gàng.<br>Cho công việc mỗi ngày.</h1><p>Kết nối tài khoản, bắt đầu một cuộc trò chuyện hoặc tiếp tục công việc của bạn trong Codex.</p><a class="button primary" href="#'+(ready()?'chat':'settings')+'">'+(ready()?'Bắt đầu trò chuyện':'Thiết lập ngay')+' <span aria-hidden="true">↗</span></a><span class="welcome-index" aria-hidden="true">CODEX UNLIMIT</span></div><div class="section-title"><h2>Ba bước để bắt đầu</h2><span>'+(Number(status.license.active)+Number(status.connected)+Number(status.codexConfigured))+'/3 bước hoàn tất</span></div><div class="setup-list">'+[
39
+ [status.license.active,'01','Kích hoạt dịch vụ','Nhập mã do người cung cấp dịch vụ gửi cho bạn.'],
40
+ [status.connected,'02','Kết nối tài khoản ChatGPT','Đăng nhập trong Codex, sau đó kết nối tại đây.'],
41
+ [status.codexConfigured,'03','Dùng cùng Codex','Bật kết nối nếu bạn muốn tiếp tục làm việc trong Codex. Bước này là tùy chọn.']
42
+ ].map(([done,n,title,desc])=>'<a class="setup-row" href="#settings"><span class="step-number '+(done?'done':'')+'">'+(done?icon('check'):n)+'</span><div><h3>'+title+'</h3><p>'+desc+'</p></div><span class="step-state">'+(done?'Hoàn tất':'Thiết lập →')+'</span></a>').join('')+'</div><div class="workspace-note"><span class="note-symbol">i</span><p>Lịch sử trò chuyện được lưu trong trình duyệt này. Khi gửi tin nhắn, nội dung được chuyển qua dịch vụ Codex Unlimit để xử lý.</p></div>';
43
+ if(status.accounts)$("#main .welcome").insertAdjacentHTML('afterend',quotaSummary(status.accounts));
44
+ }
45
+ function settings(){
46
+ const connection=status.codex??{configured:status.codexConfigured,managed:status.codexConfigured};
47
+ $('#main').innerHTML=`<div class="page-title"><span class="eyebrow">Theo cách của bạn</span><h1>Cài đặt & hỗ trợ</h1><p>Kết nối, quản lý ứng dụng và xem phần cần xử lý tại một nơi.</p></div>
48
+ <div class="settings-grid"><div>
49
+ <section class="surface"><div class="section-title"><h2>Mã sử dụng</h2><span class="tag">${status.license.active?'Đã kích hoạt':'Cần kiểm tra'}</span></div>
50
+ <p>${status.license.active?'Sử dụng đến '+date(status.license.expiresAt)+'.':esc(errors[status.license.error]??'Nhập key bạn nhận được để kích hoạt dịch vụ trên máy này.')}</p>
51
+ <form id="activate"><label for="activation-key">Mã sử dụng Codex Unlimit</label><div class="input-action"><input id="activation-key" type="password" autocomplete="off" required maxlength="36" placeholder="TDH-…"><button class="primary" type="submit">Kích hoạt</button></div><p class="error" role="alert"></p></form><button class="text-button" id="renew">Kiểm tra thời hạn</button>
52
+ </section>
53
+ <section class="surface"><div class="section-title"><h2>Tài khoản ChatGPT</h2><span class="tag">${status.connected?'Đã kết nối':'Chưa kết nối'}</span></div>
54
+ <ol class="guide"><li>Mở Codex trên máy và đăng nhập ChatGPT.</li><li>Quay lại đây, xác nhận đồng ý rồi chọn Kết nối tài khoản.</li></ol>
55
+ <p class="subtext">Không cần sao chép mật khẩu hay mã đăng nhập. Phiên đăng nhập được dùng để xử lý yêu cầu qua Codex Unlimit; dịch vụ không lưu mã truy cập hoặc nội dung trò chuyện.</p>
56
+ <label class="privacy-choice"><input id="privacy-choice" type="checkbox" ${status.connected?'checked':''}> Tôi đồng ý dùng phiên đăng nhập để xử lý yêu cầu qua Codex Unlimit. <a href="https://codexunlimit.tudonghoa.me/privacy" target="_blank" rel="noreferrer">Quyền riêng tư</a></label>
57
+ <div class="settings-actions"><button id="connect" class="${status.connected?'':'primary'}">${status.connected?'Kết nối lại':'Kết nối tài khoản'}</button>${status.connected?'<button id="disconnect" class="text-button">Ngắt kết nối chat</button>':''}</div><p class="error" id="account-error" role="alert"></p>
58
+ </section>
59
+ <section class="surface"><div class="section-title"><h2>Làm việc trong Codex</h2><span class="tag">${status.codexConfigured?'Đã bật':connection.error?'Cần xử lý':'Chưa bật'}</span></div>
60
+ <p>${status.codexConfigured?'Codex đang được cấu hình qua ứng dụng này. Đóng và mở lại Codex sau khi thay đổi kết nối.':'Bật kết nối để dùng Codex Unlimit trong ứng dụng Codex. Cấu hình gốc được sao lưu trước khi thay đổi.'}</p>
61
+ ${connection.error?'<p class="connection-warning">'+esc(errors[connection.error]??errors.operation_failed)+'</p>':connection.managed&&!connection.configured?'<p class="connection-warning">Cấu hình đã thay đổi kể từ lần kết nối trước. Chọn Kết nối lại với Codex để sửa kết nối của ứng dụng này.</p>':''}
62
+ <div class="settings-actions"><button id="sync">${status.codexConfigured?'Khôi phục kết nối ban đầu':connection.managed?'Kết nối lại với Codex':'Bật kết nối với Codex'}</button><button id="diagnose" class="text-button">Kiểm tra kết nối</button></div>
63
+ <p class="error" id="sync-error" role="alert"></p>
64
+ <ul class="connection-checks" aria-label="Trạng thái kết nối"><li><span>Key dịch vụ</span><strong>${status.license.active?'Sẵn sàng':'Cần kiểm tra'}</strong></li><li><span>Tài khoản ChatGPT</span><strong>${status.connected?'Đã kết nối':'Chưa kết nối'}</strong></li><li><span>Cấu hình Codex</span><strong>${status.codexConfigured?'Đã bật':connection.error?'Cần xử lý':'Chưa bật'}</strong></li></ul>
65
+ </section>
66
+ <section class="surface desktop-controls"><div class="section-title"><h2>Ứng dụng trên máy</h2><span class="tag">${esc(status.version)}</span></div>
67
+ ${status.desktop?.supported?`<label class="startup-choice"><span><strong>Khởi động cùng Windows</strong><small>Chạy trong khay CU, không tự mở trình duyệt.</small></span><input id="startup-enabled" type="checkbox" ${status.desktop.startup?'checked':''}></label><p class="subtext">Biểu tượng CU nằm ở góc phải thanh tác vụ, có thể nằm trong dấu mũi tên lên. Nhấp phải để mở, khởi động lại hoặc tắt ứng dụng.</p>${!status.desktop.tray?'<button id="show-tray">Hiện biểu tượng khay CU</button>':''}`:'<p>Mở lại bằng lệnh <code>codexunlimit</code>. Đóng tab chỉ đóng trang; ứng dụng vẫn chạy nền.</p>'}
68
+ <div class="settings-actions"><a class="button" href="https://codexunlimit.tudonghoa.me/download#upgrade" target="_blank" rel="noreferrer">Cập nhật ứng dụng</a><button data-stop="exit">Tắt ứng dụng</button></div>
69
+ <details class="update-help"><summary>Chuẩn bị cập nhật hoặc gỡ</summary><p>Cài gói mới rồi mở lại: bản mới tự thay tiến trình cũ. Nếu cần dừng trước, dùng nút bên dưới; kết nối sẽ tiếp tục sau khi bạn mở bản mới.</p><button data-stop="update">Dừng để cập nhật</button><p>Để gỡ, tắt Khởi động cùng Windows, chọn Tắt ứng dụng rồi chạy <code>npm uninstall -g codexunlimit</code>.</p></details>
70
+ <p id="desktop-error" class="error" role="alert"></p>
71
+ </section>
72
+ </div><aside><section class="help-panel"><span class="eyebrow">Luôn có hướng dẫn</span><h2>Cần một chút trợ giúp?</h2><h3>Không kết nối được Codex</h3><p>Chọn Kiểm tra kết nối. Thông báo sẽ phân biệt lỗi key, tài khoản và cấu hình trên máy.</p><h3>Đang dùng bản native cũ?</h3><p>Mở bản cũ, khôi phục kết nối rồi tắt bản đó trước khi chuyển sang bản web.</p><h3>Mở lại ứng dụng</h3><p>Nhấp đúp biểu tượng CU trong khay Windows. Nếu ứng dụng đã tắt, chạy <code>codexunlimit</code>.</p><a href="https://codexunlimit.tudonghoa.me/portal" target="_blank" rel="noreferrer">Xem dịch vụ & thiết bị</a></section>
73
+ <section class="usage-note"><h2>Hoạt động trên máy</h2><strong>${status.usage.total} <small>lượt gần đây</small></strong><p>${status.usage.errors} lượt cần kiểm tra. Lưu tối đa 2.001 lượt trên máy.</p><ul>${status.recent.slice(0,5).map(item=>'<li><span>'+date(item.ts)+'</span><span>'+(item.status<400?'Hoàn tất':'Chưa hoàn tất')+'</span></li>').join('')}</ul></section>
74
+ </aside></div>`;
75
+ $('#activate').onsubmit=async event=>{event.preventDefault();await action($('#activate button'),()=>api('activate',{key:$('#activation-key').value}),$('#activate .error'));};
76
+ $('#renew').onclick=event=>action(event.target,()=>api('renew',{}),$('#activate .error'));
77
+ $('#connect').onclick=event=>action(event.target,()=>api('connect',{accepted:$('#privacy-choice').checked}),$('#account-error'));
78
+ if($('#disconnect'))$('#disconnect').onclick=event=>action(event.target,()=>api('disconnect',{}),$('#account-error'));
79
+ $('#account-error').insertAdjacentHTML('beforebegin','<p><a class="text-button" href="#accounts">Quản lý nhiều tài khoản & xem quota</a></p>');
80
+ $('#sync').onclick=event=>action(event.target,()=>api(status.codexConfigured?'unsync':'sync',{}),$('#sync-error'));
81
+ $('#diagnose').onclick=event=>action(event.target,async()=>{},$('#sync-error'),'Đã kiểm tra trạng thái kết nối.');
82
+ if($('#startup-enabled'))$('#startup-enabled').onchange=async event=>{const checkbox=event.target,enabled=checkbox.checked;await action(checkbox,()=>api('desktop/startup',{enabled}),$('#desktop-error'));if(checkbox.isConnected)checkbox.checked=!!status.desktop.startup;};
83
+ if($('#show-tray'))$('#show-tray').onclick=event=>action(event.target,()=>api('desktop/tray',{}),$('#desktop-error'));
84
+ document.querySelectorAll('[data-stop]').forEach(button=>button.onclick=()=>confirmStop(button.dataset.stop));
85
+ }
86
+ function confirmStop(mode){
87
+ const dialog=document.createElement('dialog');dialog.className='stop-dialog';dialog.setAttribute('aria-labelledby','stop-title');
88
+ dialog.innerHTML='<h2 id="stop-title">'+(mode==='update'?'Dừng để cập nhật?':'Tắt Codex Unlimit?')+'</h2><p>'+(mode==='update'?'Ứng dụng sẽ dừng và giữ thiết lập kết nối. Codex sẽ tạm thời không kết nối được cho đến khi bạn cài và mở bản mới.':'Các yêu cầu đang chạy sẽ dừng. Kết nối Codex được khôi phục về cấu hình ban đầu trước khi tắt. Key và dữ liệu vẫn được giữ.')+'</p><div class="settings-actions"><button id="cancel-stop" autofocus>Tiếp tục sử dụng</button><button id="confirm-stop" class="primary">'+(mode==='update'?'Dừng để cập nhật':'Khôi phục & tắt')+'</button></div><p class="error" role="alert"></p>';
89
+ document.body.append(dialog);dialog.addEventListener('close',()=>dialog.remove());dialog.showModal();
90
+ $('#cancel-stop').onclick=()=>dialog.close();
91
+ $('#confirm-stop').onclick=async event=>{
92
+ const button=event.currentTarget;button.disabled=true;
93
+ try{await api('stop',{mode});stopped=true;dialog.close();$('#app').innerHTML='<main class="opening"><span class="mark">cu</span><h1>Ứng dụng đã tắt</h1><p>'+(mode==='update'?'Cài gói mới, sau đó chạy lệnh bên dưới để tiếp tục.':'Kết nối Codex đã được khôi phục. Khi cần sử dụng lại, chạy lệnh bên dưới.')+'</p><code>codexunlimit</code><a href="https://codexunlimit.tudonghoa.me/download" target="_blank" rel="noreferrer">Hướng dẫn cài đặt & cập nhật</a></main>';}
94
+ catch(error){dialog.querySelector('.error').textContent=error.message;button.disabled=false;}
95
+ };
96
+ }
97
+ async function action(button,fn,error,successMessage='Đã cập nhật.'){
98
+ button.disabled=true;error.textContent='';let completed=false;
99
+ try{await fn();completed=true;await refresh();modelLoad++;models=[];shell();notice(successMessage);}
100
+ catch(failure){error.textContent=(completed?'Thao tác đã thực hiện nhưng chưa cập nhật được trạng thái. ':'')+failure.message;}
101
+ finally{button.disabled=false;}
102
+ }
103
+ function chat(){
104
+ const conversation=conversations.find(c=>c.id===current);
105
+ $('#main').innerHTML='<div class="chat-area"><div class="chat-top"><div><span class="eyebrow">Không gian trò chuyện</span><h1>'+esc(conversation?.title??'Bạn muốn làm gì hôm nay?')+'</h1></div>'+(conversation?'<button id="delete-chat" class="text-button" '+(busy?'disabled':'')+'>Xóa cuộc trò chuyện</button>':'')+'</div><div class="mobile-history"><label for="recent-chat">Cuộc trò chuyện gần đây</label><select id="recent-chat" '+(busy?'disabled':'')+'><option value="">Cuộc trò chuyện mới</option>'+conversations.map(c=>'<option value="'+esc(c.id)+'" '+(c.id===current?'selected':'')+'>'+esc(c.title)+'</option>').join('')+'</select></div><div class="chat-account-picker"><label for="chat-account">Tài khoản</label><select id="chat-account" '+(busy||!status.accounts?'disabled':'')+'>'+accountOptions(status.accounts)+'</select><a href="#accounts">Xem quota</a></div><div id="messages" class="messages" role="log" aria-label="Nội dung trò chuyện"></div><div id="chat-empty" class="chat-empty" '+(conversation?.messages.length?'hidden':'')+'><span class="chat-emblem">'+icon('chat')+'</span><h2>Từ một ý tưởng nhỏ,<br>đến công việc hoàn thành.</h2><p>Viết nội dung, tìm cách giải quyết hoặc cùng suy nghĩ.<br>Bắt đầu bằng điều bạn đang cần.</p><div class="suggestions">'+['Giúp tôi lập kế hoạch cho tuần tới','Viết một email phản hồi khách hàng','Giải thích một vấn đề theo cách dễ hiểu'].map(t=>'<button data-prompt="'+esc(t)+'">'+t+' <span>↗</span></button>').join('')+'</div></div>'+(!ready()?'<div class="setup-reminder">Hoàn tất kích hoạt và kết nối tài khoản để bắt đầu. <a href="#settings">Đi đến cài đặt →</a></div>':'')+'<form id="composer" class="composer"><label class="sr-only" for="message">Tin nhắn của bạn</label><textarea id="message" rows="3" maxlength="32000" placeholder="Viết điều bạn đang nghĩ…" '+(!ready()||busy?'disabled':'')+'></textarea><div class="composer-tools"><div><label class="sr-only" for="model">Chọn mô hình</label><select id="model" '+(busy?'disabled':'')+'><option value="">'+(ready()?'Đang tải lựa chọn…':'Kết nối tài khoản trước')+'</option></select><span class="key-hint">Enter để gửi · Shift + Enter xuống dòng</span></div><button class="send" id="send" type="submit" aria-label="Gửi tin nhắn" '+(!ready()||busy?'disabled':'')+'>'+icon('arrow')+'</button><button id="stop" type="button" '+(!busy?'hidden':'')+'>Dừng</button></div></form><p id="chat-error" class="error" role="alert"></p><p class="chat-footnote">Câu trả lời có thể chưa chính xác. Hãy kiểm tra thông tin quan trọng. Lịch sử lưu trên trình duyệt này.</p></div>';
106
+ paintMessages();
107
+ $('#chat-account').onchange=async event=>{
108
+ if(busy)return;event.target.disabled=true;
109
+ try{status.accounts=await api('accounts/select',{accountId:event.target.value});status.connected=true;models=[];modelLoad++;chat();}
110
+ catch(error){$('#chat-error').textContent=error.message;event.target.value=status.accounts?.activeId??'codex';event.target.disabled=false;}
111
+ };
112
+ $('#messages').onclick=async event=>{const button=event.target.closest('[data-copy-code]');if(!button)return;try{await navigator.clipboard.writeText(button.closest('.markdown-code').querySelector('pre code').textContent);button.textContent='Đã sao chép';}catch{notice('Chọn đoạn mã để sao chép thủ công.');}};
113
+ $('#recent-chat').onchange=e=>{if(busy)return;current=e.target.value||null;render();};
114
+ document.querySelectorAll('[data-prompt]').forEach(b=>b.onclick=()=>{if(!ready()){location.hash='settings';return;}$('#message').value=b.dataset.prompt;$('#message').focus();});
115
+ $('#composer').onsubmit=send;$('#message').onkeydown=e=>{if(e.key==='Enter'&&!e.shiftKey&&!e.isComposing){e.preventDefault();$('#composer').requestSubmit();}};
116
+ $('#stop').onclick=()=>controller?.abort();
117
+ if($('#delete-chat'))$('#delete-chat').onclick=()=>{const b=$('#delete-chat');if(b.dataset.confirm!=='yes'){b.dataset.confirm='yes';b.textContent='Bấm lần nữa để xóa';return;}conversations=conversations.filter(c=>c.id!==current);current=null;save();render();};
118
+ if(ready())loadModels();
119
+ }
120
+ async function loadModels(){
121
+ const generation=++modelLoad;
122
+ try{const loaded=models.length?models:(await api('models'+(status.accounts?'?accountId='+encodeURIComponent(status.accounts.activeId):''))).models;if(generation!==modelLoad||!$('#model')||!status.connected)return;models=loaded;$('#model').innerHTML=models.map(m=>'<option value="'+esc(m.id)+'">'+esc(m.name)+'</option>').join('');if(!models.length)throw Error('Tài khoản chưa có lựa chọn khả dụng. Thử kết nối lại trong cài đặt.');$('#send').disabled=busy;}
123
+ catch(e){if(generation===modelLoad&&$('#chat-error')){$('#chat-error').textContent=e.message;$('#send').disabled=true;}}
124
+ }
125
+ function paintMessages(){
126
+ if(!$('#messages'))return;const messages=conversations.find(c=>c.id===current)?.messages??[];
127
+ $('#messages').innerHTML=messages.map(m=>'<article class="message '+m.role+'"><span class="speaker">'+(m.role==='user'?'Bạn':'Unlimit')+'</span><div class="message-text '+(m.role==='assistant'?'markdown-body':'')+'">'+(m.role==='assistant'?renderMarkdown(m.content||(busy?'Đang soạn câu trả lời…':'Chưa nhận được câu trả lời.')):esc(m.content))+'</div>'+(m.interrupted?'<small class="interrupted">Câu trả lời chưa hoàn tất</small>':'')+'</article>').join('');
128
+ $('#chat-empty').hidden=!!messages.length;
129
+ }
130
+ async function send(e){
131
+ e.preventDefault();const value=$('#message').value.trim(),model=$('#model').value;if(busy||!value||!model||!ready())return;
132
+ let c=conversations.find(c=>c.id===current);if(!c){c={id:crypto.randomUUID(),title:value.slice(0,60),messages:[]};conversations.unshift(c);conversations=conversations.slice(0,30);current=c.id;}
133
+ if(c.messages.length>=98){$('#chat-error').textContent='Cuộc trò chuyện đã dài. Mở cuộc trò chuyện mới để tiếp tục.';return;}
134
+ const input=[...c.messages.filter(m=>m.content&&!m.interrupted).map(({role,content})=>({role,content})),{role:'user',content:value}];
135
+ if(new TextEncoder().encode(JSON.stringify({model,messages:input})).length>1048576){$('#chat-error').textContent=errors.request_too_large;return;}
136
+ c.messages.push({role:'user',content:value});
137
+ const answer={role:'assistant',content:'',interrupted:true};c.messages.push(answer);save();busy=true;controller=new AbortController();chat();history();
138
+ try{
139
+ const response=await fetch('/app/api/chat',{method:'POST',headers:{'content-type':'application/json','x-csrf-token':csrf},body:JSON.stringify({model,messages:input,accountId:status.accounts?.activeId}),signal:controller.signal});
140
+ if(!response.ok){const data=await response.json();throw Error(errors[data.error]??errors.service_unavailable);}
141
+ await readResponseStream(response.body,text=>{answer.content=text;if(route==='chat'&&renderFrame===null){
142
+ renderFrame=requestAnimationFrame(()=>{renderFrame=null;const box=$('#messages');if(!box)return;const nearBottom=box.scrollHeight-box.scrollTop-box.clientHeight<120;const target=box.querySelector('.message:last-child .message-text');if(target)target.innerHTML=renderMarkdown(answer.content);if(nearBottom)box.scrollTop=box.scrollHeight;});
143
+ }});answer.interrupted=false;
144
+ }catch(error){notice(error.name==='AbortError'?'Đã dừng câu trả lời.':error.message);}
145
+ finally{if(renderFrame!==null){cancelAnimationFrame(renderFrame);renderFrame=null;}busy=false;controller=null;save();if(route==='chat')chat();history();}
146
+ }
147
+ window.addEventListener('hashchange',()=>{if(status)render();});
148
+ try{
149
+ if(location.hash.startsWith('#ticket=')){const ticket=location.hash.slice(8);historyReplace();const result=await api('session',{ticket});csrf=result.csrf;}
150
+ await refresh();shell();
151
+ }catch(e){$('#app').innerHTML='<main class="opening"><span class="mark">cu</span><h1>Mở không gian làm việc</h1><p>'+esc(e.message)+'</p><code>codexunlimit</code><p>Chạy lệnh trên trong PowerShell (Windows) hoặc Terminal (macOS).</p><button id="retry">Thử lại</button></main>';$('#retry').onclick=()=>location.reload();}
152
+ function historyReplace(){window.history.replaceState(null,'','/app/#home');}
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,48 @@
1
+ @font-face {
2
+ font-family: 'Geist Variable';
3
+ font-style: normal;
4
+ font-display: swap;
5
+ font-weight: 100 900;
6
+ src: url(/app/assets/geist-vietnamese-wght-normal.woff2) format('woff2-variations');
7
+ unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
8
+ }
9
+ @font-face {
10
+ font-family: 'Geist Variable';
11
+ font-style: normal;
12
+ font-display: swap;
13
+ font-weight: 100 900;
14
+ src: url(/app/assets/geist-latin-ext-wght-normal.woff2) format('woff2-variations');
15
+ unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
16
+ }
17
+ @font-face {
18
+ font-family: 'Geist Variable';
19
+ font-style: normal;
20
+ font-display: swap;
21
+ font-weight: 100 900;
22
+ src: url(/app/assets/geist-latin-wght-normal.woff2) format('woff2-variations');
23
+ unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
24
+ }
25
+ @font-face {
26
+ font-family: 'Newsreader Variable';
27
+ font-style: normal;
28
+ font-display: swap;
29
+ font-weight: 200 800;
30
+ src: url(/app/assets/newsreader-vietnamese-wght-normal.woff2) format('woff2-variations');
31
+ unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
32
+ }
33
+ @font-face {
34
+ font-family: 'Newsreader Variable';
35
+ font-style: normal;
36
+ font-display: swap;
37
+ font-weight: 200 800;
38
+ src: url(/app/assets/newsreader-latin-ext-wght-normal.woff2) format('woff2-variations');
39
+ unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
40
+ }
41
+ @font-face {
42
+ font-family: 'Newsreader Variable';
43
+ font-style: normal;
44
+ font-display: swap;
45
+ font-weight: 200 800;
46
+ src: url(/app/assets/newsreader-latin-wght-normal.woff2) format('woff2-variations');
47
+ unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
48
+ }
@@ -0,0 +1,93 @@
1
+ Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font) Geist-Italic[wght].ttf: Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="17" fill="#292824"/><path d="M31 20a14 14 0 1 0 0 24" fill="none" stroke="#faf9f6" stroke-width="6" stroke-linecap="round"/><path d="M36 20v19a9 9 0 0 0 18 0V20" fill="none" stroke="#c79a7b" stroke-width="6" stroke-linecap="round"/></svg>
@@ -0,0 +1,93 @@
1
+ Copyright 2020 The Newsreader Project Authors (http://github.com/productiontype/Newsreader) Newsreader-Italic[opsz,wght].ttf: Copyright 2020 The Newsreader Project Authors (http://github.com/productiontype/Newsreader)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
package/web/index.html ADDED
@@ -0,0 +1,3 @@
1
+ <!doctype html>
2
+ <html lang="vi"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="Không gian làm việc Codex Unlimit trên máy của bạn."><title>Codex Unlimit · Không gian làm việc</title><meta name="theme-color" content="#292824"><link rel="icon" href="/app/assets/favicon.ico"><link rel="icon" href="/app/assets/mark.svg" type="image/svg+xml"><link rel="apple-touch-icon" href="/app/assets/apple-touch-icon.png"><link rel="stylesheet" href="/app/assets/fonts.css"><link rel="stylesheet" href="/app/style.css"><script type="module" src="/app/app.js"></script></head>
3
+ <body><a class="skip" href="#main">Đến nội dung</a><div id="app"><main class="opening"><span class="mark">cu</span><h1>Không gian của bạn đang mở…</h1><p>Đang kết nối Codex Unlimit trên máy này.</p></main></div><div id="notice" role="status" aria-live="polite" hidden></div></body></html>
@@ -0,0 +1,15 @@
1
+ import markdownit from '/app/vendor/markdown-it.js';
2
+
3
+ const markdown=markdownit({html:false,linkify:false,breaks:false});
4
+ const escape=markdown.utils.escapeHtml;
5
+ markdown.validateLink=value=>/^(?:https?:\/\/|mailto:|#)/i.test(value);
6
+ markdown.renderer.rules.image=(tokens,index)=>'<span class="markdown-image">[Ảnh: '+escape(tokens[index].content||'không có mô tả')+']</span>';
7
+ markdown.renderer.rules.link_open=(tokens,index,options,environment,renderer)=>{
8
+ tokens[index].attrSet('target','_blank');tokens[index].attrSet('rel','noopener noreferrer');return renderer.renderToken(tokens,index,options);
9
+ };
10
+ const fence=markdown.renderer.rules.fence;
11
+ markdown.renderer.rules.fence=(tokens,index,options,environment,renderer)=>{
12
+ const name=tokens[index].info.trim().split(/\s+/)[0],language=/^[a-z0-9_+-]{1,24}$/i.test(name)?name:'Code';
13
+ return '<div class="markdown-code"><div class="markdown-code-head"><span>'+escape(language)+'</span><button type="button" data-copy-code>Sao chép</button></div>'+fence(tokens,index,options,environment,renderer)+'</div>';
14
+ };
15
+ export function renderMarkdown(text){return markdown.render(String(text??''));}
@@ -0,0 +1,62 @@
1
+ export const errors={
2
+ pool_platform_unavailable:'Chức năng đăng nhập pool chưa hỗ trợ hệ điều hành hoặc kiến trúc máy này.',
3
+ pool_runtime_missing:'Thiếu công cụ đăng nhập đi kèm. Cài lại gói Codex Unlimit bằng npm, giữ các dependency tùy chọn.',
4
+ pool_runtime_unavailable:'Chưa mở được bộ quản lý tài khoản. Khởi động lại Codex Unlimit rồi thử lại.',
5
+ pool_provider_rejected:'Chưa thực hiện được yêu cầu với tài khoản. Đăng nhập lại tài khoản này rồi thử lại.',
6
+ pool_request_timeout:'Tài khoản chưa phản hồi kịp. Kiểm tra mạng rồi thử lại.',
7
+ pool_account_missing:'Tài khoản không còn trong pool. Tải lại danh sách rồi chọn lại.',
8
+ pool_account_expired:'Phiên tài khoản cần đăng nhập lại. Chọn Đăng nhập lại trong thẻ tài khoản.',
9
+ pool_account_exists:'Tài khoản này đã có trong pool. Chọn tài khoản đã lưu để sử dụng.',
10
+ pool_storage_invalid:'Không mở được vùng lưu tài khoản riêng. Kiểm tra thư mục dữ liệu của ứng dụng.',
11
+ pool_login_pending:'Đang có một phiên đăng nhập. Hoàn tất hoặc hủy phiên đó trước.',
12
+ pool_full:'Pool đã có 10 tài khoản. Gỡ tài khoản không còn dùng trước khi thêm mới.',
13
+ pool_login_unavailable:'Chưa mở được trang đăng nhập OpenAI. Kiểm tra mạng hoặc thử cách đăng nhập bằng mã.',
14
+ pool_login_failed:'Chưa hoàn tất đăng nhập. Thử lại và xác nhận đúng tài khoản trên trang OpenAI.',
15
+ pool_login_expired:'Phiên đăng nhập đã hết thời gian. Chọn Thêm tài khoản để bắt đầu lại.',
16
+ pool_login_interrupted:'Lần đăng nhập trước bị gián đoạn. Chọn Đăng nhập lại để tiếp tục.',
17
+ pool_quota_unavailable:'Chưa đọc được quota 5 giờ/tuần của tài khoản. Số liệu cũ vẫn được giữ; thử lại hoặc đăng nhập lại nếu phiên đã hết hạn.',
18
+ connector_update_required:'Bản đang chạy cần được cập nhật. Mở trang cài đặt, cài gói mới rồi mở lại Codex Unlimit.',
19
+ request_integrity_failed:'Yêu cầu chưa được xác thực. Cập nhật Codex Unlimit rồi thử lại.',
20
+ license_server_unavailable:'Chưa liên lạc được máy chủ kiểm tra key. Kiểm tra mạng rồi chọn Kiểm tra thời hạn.',
21
+ license_service_busy:'Máy chủ kiểm tra key đang bận. Chờ vài giây rồi thử lại; đây không phải thông báo key hết hạn.',
22
+ license_invalid_or_expired:'Key đã hết hạn hoặc bị thu hồi. Liên hệ nơi cấp key.',
23
+ license_revoked:'Key đã bị thu hồi. Liên hệ nơi cấp key.',
24
+ device_limit_reached:'Key đã đủ số máy được phép dùng. Mở trang khách hàng để kiểm tra thiết bị.',
25
+ device_revoked:'Thiết bị này đã bị thu hồi quyền sử dụng. Liên hệ nơi cấp key.',
26
+ activation_invalid:'Lần kích hoạt này không còn hợp lệ. Nhập lại key để kích hoạt.',
27
+ privacy_required:'Đánh dấu đồng ý sử dụng phiên đăng nhập trước khi kết nối.',
28
+ request_too_large:'Cuộc trò chuyện đã quá dài. Hãy mở cuộc trò chuyện mới.',
29
+ open_from_command:'Phiên mở trang đã hết hiệu lực. Chọn Mở Codex Unlimit trong khay Windows hoặc chạy lại lệnh codexunlimit.',
30
+ session_expired:'Phiên làm việc đã hết hạn. Mở lại Codex Unlimit từ khay Windows hoặc lệnh codexunlimit.',
31
+ account_required:'Chưa đọc được phiên ChatGPT trên máy này. Mở Codex, đăng nhập ChatGPT rồi chọn Kết nối lại.',
32
+ account_expired:'Phiên ChatGPT đã hết hạn. Đăng nhập lại trong Codex rồi chọn Kết nối lại.',
33
+ activation_required:'Kích hoạt key trong phần Mã sử dụng trước khi kết nối Codex.',
34
+ invalid_activation_key:'Key chưa đúng định dạng. Kiểm tra lại mã bạn nhận được.',
35
+ usage_limit:'Bạn đã đến giới hạn sử dụng hiện tại. Vui lòng thử lại sau.',
36
+ service_unavailable:'Máy chủ xử lý chưa phản hồi. Thử lại sau hoặc liên hệ hỗ trợ.',
37
+ local_service_unavailable:'Ứng dụng trên máy chưa phản hồi. Mở lại từ biểu tượng CU trong khay Windows hoặc chạy codexunlimit.',
38
+ operation_failed:'Chưa hoàn tất thao tác. Chọn Kiểm tra kết nối để xem phần cần xử lý.',
39
+ models_unavailable:'Chưa tải được danh sách mô hình. Kiểm tra thời hạn và kết nối lại tài khoản.',
40
+ invalid_message:'Tin nhắn quá dài hoặc cuộc trò chuyện đã đầy. Hãy mở cuộc trò chuyện mới.',
41
+ existing_connector_review_required:'Codex đang dùng kết nối do bản native cũ hoặc TDH Relay quản lý. Mở bản đó, chọn Khôi phục kết nối ban đầu rồi Tắt ứng dụng; sau đó kết nối lại tại đây.',
42
+ active_codex_profile_requires_manual_setup:'Codex đang chọn một profile riêng. Bỏ lựa chọn profile trong cấu hình Codex trước khi bật kết nối, hoặc nhờ hỗ trợ kiểm tra profile đó.',
43
+ multiline_root_config_requires_manual_setup:'Cấu hình Codex có nội dung nhiều dòng cần được kiểm tra trước khi sửa. Chưa thay đổi file; hãy liên hệ hỗ trợ.',
44
+ duplicate_provider_settings_review_required:'Cấu hình Codex có thiết lập kết nối bị lặp. Chưa thay đổi file; hãy nhờ hỗ trợ xử lý mục bị lặp.',
45
+ config_backup_missing_or_changed_review_required:'Bản sao cấu hình gốc không còn nguyên vẹn. Chưa ghi đè cấu hình Codex; hãy liên hệ hỗ trợ để khôi phục.',
46
+ managed_configuration_changed_review_required:'Kết nối Codex đã được một ứng dụng khác chỉnh sửa. Chưa ghi đè thay đổi đó; hãy nhờ hỗ trợ kiểm tra cấu hình.',
47
+ managed_configuration_already_present:'Có cấu hình Codex Unlimit từ lần cài khác. Mở bản đang quản lý kết nối để khôi phục trước.',
48
+ config_target_changed_review_required:'Thư mục hoặc cổng kết nối đã thay đổi. Khôi phục kết nối ở bản cũ trước khi kết nối lại.',
49
+ new_provider_settings_review_required:'Codex có danh sách mô hình riêng được thêm sau khi kết nối. Hãy nhờ hỗ trợ kiểm tra trước khi khôi phục.',
50
+ config_symlink_requires_manual_setup:'File cấu hình Codex đang là liên kết tới vị trí khác. Chưa sửa file để tránh tác động nhầm; hãy liên hệ hỗ trợ.',
51
+ config_permission_denied:'Windows chưa cho phép sửa cấu hình Codex. Đóng Codex, kiểm tra quyền ghi hoặc phần mềm bảo vệ thư mục rồi thử lại.',
52
+ config_missing:'Không tìm thấy cấu hình cần khôi phục. Chọn Kiểm tra kết nối và liên hệ hỗ trợ nếu vẫn lỗi.',
53
+ codex_home_missing:'Chưa xác định được thư mục cấu hình Codex. Mở Codex một lần rồi thử lại.',
54
+ saved_root_line_corrupt:'Bản lưu thiết lập kết nối không còn nguyên vẹn. Chưa sửa cấu hình; hãy liên hệ hỗ trợ.',
55
+ saved_root_line_moved:'Vị trí thiết lập kết nối đã thay đổi. Chưa sửa cấu hình; hãy liên hệ hỗ trợ.',
56
+ startup_unavailable:'Tự khởi động cùng hệ điều hành hiện hỗ trợ Windows.',
57
+ startup_update_failed:'Chưa cập nhật được tùy chọn khởi động cùng Windows. Kiểm tra quyền ghi thư mục Startup rồi thử lại.',
58
+ tray_unavailable:'Chưa mở được biểu tượng khay Windows. Bạn vẫn có thể mở và tắt ứng dụng tại trang này.',
59
+ invalid_input:'Thông tin gửi chưa hợp lệ. Tải lại trang rồi thử lại.',
60
+ clock_rollback_detected:'Giờ trên máy chưa khớp. Bật đồng bộ ngày giờ của hệ điều hành rồi kiểm tra thời hạn lại.',
61
+ license_signature_invalid:'Chưa xác minh được thông tin key. Chọn Kiểm tra thời hạn; nếu vẫn lỗi hãy cập nhật ứng dụng.'
62
+ };