codexunlimit 0.1.0-web.10 → 0.1.0-web.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/app.mjs CHANGED
@@ -5,7 +5,7 @@ import {Readable} from 'node:stream';
5
5
  import {pipeline} from 'node:stream/promises';
6
6
  import {syncCodex,unsyncCodex,codexConnection} from './config.mjs';
7
7
  import {errors} from './web/messages.js';
8
- import {VERSION} from './product.mjs';
8
+ import {VERSION} from './product.mjs';
9
9
  import {createUpdateChecker} from './updates.mjs';
10
10
  import {boundedJson} from './io.mjs';
11
11
  import {fileURLToPath} from 'node:url';
@@ -37,7 +37,7 @@ async function body(req){
37
37
  }
38
38
  export function createLocalApp(state,{license,request=fetch,account=()=>readCodexAccount(state),desktop,pool,checkUpdates=createUpdateChecker(),onStop=()=>{}}={}){
39
39
  const sessions=new Map(),assets={'/app/':'index.html','/app/app.js':'app.js','/app/style.css':'style.css','/app/stream.js':'stream.js','/app/messages.js':'messages.js'};
40
- assets['/app/updates.js']='updates.js';
40
+ assets['/app/updates.js']='updates.js';
41
41
  assets['/app/markdown.js']='markdown.js';assets['/app/pool-ui.js']='pool-ui.js';
42
42
  assets['/app/vendor/markdown-it.js']=fileURLToPath(import.meta.resolve('markdown-it/browser'));
43
43
  for(const name of fs.readdirSync(path.join(import.meta.dirname,'web/assets')))if(/^[a-z0-9.-]+\.(woff2|css|txt|svg|png|ico)$/.test(name))assets['/app/assets/'+name]='assets/'+name;
@@ -49,7 +49,10 @@ export function createLocalApp(state,{license,request=fetch,account=()=>readCode
49
49
  const json=(status,data,extra={})=>{res.writeHead(status,{...headers,'content-type':'application/json; charset=utf-8',...extra});res.end(JSON.stringify(data));};
50
50
  try{
51
51
  const origin='http://'+req.headers.host;
52
- if(req.headers.origin&&req.headers.origin!==origin||req.headers['sec-fetch-site']==='cross-site')throw fail('local_request_denied',403);
52
+ // External links may navigate to the public shell; APIs still require same-origin access.
53
+ const entryNavigation=req.method==='GET'&&(url.pathname==='/app'||url.pathname==='/app/')&&
54
+ req.headers['sec-fetch-mode']==='navigate'&&req.headers['sec-fetch-dest']==='document';
55
+ if((req.headers.origin&&req.headers.origin!==origin)||(req.headers['sec-fetch-site']==='cross-site'&&!entryNavigation))throw fail('local_request_denied',403);
53
56
  if(req.method==='GET'&&url.pathname==='/app'){res.writeHead(302,{...headers,location:'/app/'});res.end();return true;}
54
57
  if(req.method==='GET'&&assets[url.pathname]){
55
58
  const file=assets[url.pathname],type=file.endsWith('.html')?'text/html':file.endsWith('.css')?'text/css':file.endsWith('.svg')?'image/svg+xml':file.endsWith('.png')?'image/png':file.endsWith('.ico')?'image/x-icon':file.endsWith('.woff2')?'font/woff2':file.endsWith('.txt')?'text/plain':'text/javascript';
@@ -71,7 +74,7 @@ export function createLocalApp(state,{license,request=fetch,account=()=>readCode
71
74
  const session=token&&sessions.get(digest(token));if(!session)throw fail('open_from_command',401);
72
75
  if(write&&req.headers['x-csrf-token']!==session.csrf)throw fail('session_expired',403);
73
76
  const route=url.pathname.slice('/app/api/'.length);
74
- if(!write&&route==='updates'){json(200,await checkUpdates());return true;}
77
+ if(!write&&route==='updates'){json(200,await checkUpdates());return true;}
75
78
  if(!write&&route==='status'){
76
79
  // A short-lived lease expiring does not mean the purchased service expired.
77
80
  let licenseError=null;
@@ -136,7 +139,7 @@ export function createLocalApp(state,{license,request=fetch,account=()=>readCode
136
139
  body:write?JSON.stringify(payload):undefined,redirect:'error',signal:AbortSignal.any([controller.signal,AbortSignal.timeout(600000)])});
137
140
  if(!upstream.ok){
138
141
  let code;try{code=(await boundedJson(upstream,16384)).error?.code;}catch{}
139
- const reason=upstream.status===426?'connector_update_required':['request_body_rejected','request_proof_rejected','request_body_unverified'].includes(code)?'request_integrity_failed':upstream.status===401?'account_expired':upstream.status===429?'usage_limit':upstream.status===403?'activation_required':'service_unavailable';
142
+ const reason=['local_queue_full','local_queue_timeout','license_concurrency_limit'].includes(code)?'connection_busy':code==='request_proof_expired'?'request_clock_error':upstream.status===426?'connector_update_required':['request_body_rejected','request_proof_rejected','request_body_unverified'].includes(code)?'request_integrity_failed':upstream.status===401?'account_expired':upstream.status===429?'usage_limit':upstream.status===403?'activation_required':'service_unavailable';
140
143
  throw fail(reason,upstream.status);
141
144
  }
142
145
  if(write){res.writeHead(200,{...headers,'content-type':'text/event-stream; charset=utf-8'});await pipeline(Readable.fromWeb(upstream.body),res,{signal:controller.signal});}
package/cli.mjs CHANGED
@@ -11,7 +11,7 @@ import {createConnector} from './relay.mjs';
11
11
  import {createLocalApp,createBrowserTicket,readCodexAccount} from './app.mjs';
12
12
  import {AccountPool} from './account-pool.mjs';
13
13
  import {DEFAULT_PORT,VERSION} from './product.mjs';
14
- import {unsyncCodex} from './config.mjs';
14
+ import {unsyncCodex,migrateCodexTransport} from './config.mjs';
15
15
  import {createWindowsIntegration} from './windows.mjs';
16
16
  import {errors} from './web/messages.js';
17
17
  import {diagnoseRuntime} from './doctor.mjs';
@@ -24,10 +24,11 @@ async function main(){
24
24
  if(command==='doctor'){
25
25
  const result=await diagnoseRuntime();if(args.includes('--json'))console.log(JSON.stringify(result));else{for(const check of result.checks)console.log((check.ok?'OK ':'THIẾU ')+check.name+(check.version?' '+check.version:''));console.log(result.ok?'Môi trường đã sẵn sàng. Key/tài khoản được kiểm tra trong ứng dụng.':'Cài lại gói npm, giữ dependency tùy chọn. Không cần xóa key, đăng xuất ChatGPT hoặc reset thiết bị.');}if(!result.ok)process.exitCode=1;return;
26
26
  }
27
- if(args.includes('--help')){console.log('Codex Unlimit\n codexunlimit doctor Kiểm tra Node, SQLite và Codex runtime\n codexunlimit Mở ứng dụng và khay Windows\n codexunlimit stop Khôi phục kết nối Codex và tắt ứng dụng\n codexunlimit restart Khởi động lại bằng bản đã cài mới nhất\n codexunlimit startup on Tự mở trong khay khi đăng nhập Windows\n codexunlimit startup off Tắt khởi động cùng Windows\n codexunlimit --no-open Chạy nền, không mở trình duyệt\n --no-tray Không mở khay (dành cho kiểm thử)\n --home <thư mục> Dữ liệu riêng\n --port <số cổng> Cổng cho lần cài đầu');return;}
28
- if(!['open','serve','stop','restart','startup'].includes(command))throw Error('Lệnh chưa đúng. Xem codexunlimit --help');
27
+ if(args.includes('--help')){console.log('Codex Unlimit\n codexunlimit diagnostics Xuất log kết nối đã loại thông tin riêng tư\n codexunlimit doctor Kiểm tra Node, SQLite và Codex runtime\n codexunlimit Mở ứng dụng và khay Windows\n codexunlimit stop Khôi phục kết nối Codex và tắt ứng dụng\n codexunlimit restart Khởi động lại bằng bản đã cài mới nhất\n codexunlimit startup on Tự mở trong khay khi đăng nhập Windows\n codexunlimit startup off Tắt khởi động cùng Windows\n codexunlimit --no-open Chạy nền, không mở trình duyệt\n --no-tray Không mở khay (dành cho kiểm thử)\n --home <thư mục> Dữ liệu riêng\n --port <số cổng> Cổng cho lần cài đầu');return;}
28
+ if(!['open','serve','stop','restart','startup','diagnostics'].includes(command))throw Error('Lệnh chưa đúng. Xem codexunlimit --help');
29
29
  const state=new ClientState(home);let serving=false;
30
30
  try{
31
+ if(command==='diagnostics'){console.log(JSON.stringify({version:VERSION,requests:state.diagnostics()},null,2));return;}
31
32
  const port=Number(option('--port',DEFAULT_PORT));if(!Number.isInteger(port)||port<1024||port>65535)throw Error('Cổng cần nằm trong khoảng 1024–65535.');
32
33
  state.initialize({port,codexHome:state.get('codex_home')??process.env.CODEX_HOME??path.join(os.homedir(),'.codex')});
33
34
  const desktop=createWindowsIntegration(state);
@@ -49,6 +50,7 @@ async function main(){
49
50
  let stopping=false;
50
51
  function shutdown(){if(stopping)return;stopping=true;server.close(async()=>{try{await pool.close();}finally{state.close();process.exit(0);}});server.closeIdleConnections();setTimeout(()=>server.closeAllConnections(),1500).unref();}
51
52
  server.listen(state.settings.get('relay.port'),'127.0.0.1');await new Promise((resolve,reject)=>{server.once('listening',resolve);server.once('error',reject);});
53
+ try{migrateCodexTransport(state);}catch(error){console.error(errors[error.message]??errors.operation_failed);}
52
54
  serving=true;process.once('SIGINT',shutdown);process.once('SIGTERM',shutdown);
53
55
  if(desktop.status().startup)desktop.setStartup(true).catch(()=>console.error(errors.startup_update_failed));
54
56
  if(process.platform==='win32'&&!args.includes('--no-tray'))desktop.showTray().catch(()=>console.error(errors.tray_unavailable));
@@ -87,20 +89,20 @@ async function main(){
87
89
  const url=base+'/app/#ticket='+createBrowserTicket(state);
88
90
  console.log('Codex Unlimit đang chạy trên máy của bạn.\n'+url+'\nGiữ đường dẫn này riêng tư; liên kết mở trang có hiệu lực 2 phút.');
89
91
  if(!args.includes('--no-open')){
90
- // Keep the complete local URL (including its ticket) out of shell source.
91
- const windows=process.platform==='win32';
92
- const exe=windows?path.join(process.env.SystemRoot??'C:/Windows','System32/WindowsPowerShell/v1.0/powershell.exe'):process.platform==='darwin'?'open':'xdg-open';
93
- const launchArgs=windows?['-NoProfile','-NonInteractive','-WindowStyle','Hidden','-Command',"$ErrorActionPreference='Stop'; Start-Process -FilePath $env:CU_BROWSER_URL"]:[url];
94
- try{
95
- await new Promise((resolve,reject)=>{
96
- const child=spawn(exe,launchArgs,{windowsHide:true,stdio:'ignore',env:{...process.env,CU_BROWSER_URL:url}});
97
- const timeout=setTimeout(()=>{child.kill();reject(Error('browser_open_timeout'));},15000);
98
- child.once('error',error=>{clearTimeout(timeout);reject(error);});
99
- child.once('exit',code=>{clearTimeout(timeout);code===0?resolve():reject(Error('browser_open_failed'));});
100
- });
101
- }catch{
102
- console.error('Không tự mở được trình duyệt. Sao chép đường dẫn local ở trên vào trình duyệt.');
103
- process.exitCode=1;
92
+ // Keep the complete local URL (including its ticket) out of shell source.
93
+ const windows=process.platform==='win32';
94
+ const exe=windows?path.join(process.env.SystemRoot??'C:/Windows','System32/WindowsPowerShell/v1.0/powershell.exe'):process.platform==='darwin'?'open':'xdg-open';
95
+ const launchArgs=windows?['-NoProfile','-NonInteractive','-WindowStyle','Hidden','-Command',"$ErrorActionPreference='Stop'; Start-Process -FilePath $env:CU_BROWSER_URL"]:[url];
96
+ try{
97
+ await new Promise((resolve,reject)=>{
98
+ const child=spawn(exe,launchArgs,{windowsHide:true,stdio:'ignore',env:{...process.env,CU_BROWSER_URL:url}});
99
+ const timeout=setTimeout(()=>{child.kill();reject(Error('browser_open_timeout'));},15000);
100
+ child.once('error',error=>{clearTimeout(timeout);reject(error);});
101
+ child.once('exit',code=>{clearTimeout(timeout);code===0?resolve():reject(Error('browser_open_failed'));});
102
+ });
103
+ }catch{
104
+ console.error('Không tự mở được trình duyệt. Sao chép đường dẫn local ở trên vào trình duyệt.');
105
+ process.exitCode=1;
104
106
  }
105
107
  }
106
108
  }finally{if(!serving)state.close();}
package/config.mjs CHANGED
@@ -5,6 +5,9 @@ import crypto from 'node:crypto';
5
5
  const BEGIN='# BEGIN Codexunlimit Web managed configuration';
6
6
  const END='# END Codexunlimit Web managed configuration';
7
7
  const PREVIOUS='# Codexunlimit Web saved root line: ';
8
+ const PROVIDER='codexunlimit';
9
+ const PROVIDER_BEGIN='# BEGIN Codexunlimit Web HTTP provider';
10
+ const PROVIDER_END='# END Codexunlimit Web HTTP provider';
8
11
  const ROOT_KEYS=new Set(['openai_base_url','model_provider','model_catalog_json']);
9
12
  const digest=value=>crypto.createHash('sha256').update(value).digest('hex');
10
13
 
@@ -37,6 +40,35 @@ function layout(text){
37
40
  }
38
41
  return {bom,crlf,lines,end,fields,join:lines=>bom+(crlf?lines.join('\n').replace(/\n/g,'\r\n'):lines.join('\n'))};
39
42
  }
43
+ function providerHeader(line){
44
+ return /^\[\s*(?:model_providers|"model_providers"|'model_providers')\s*\.\s*(?:codexunlimit|"codexunlimit"|'codexunlimit')\s*\]\s*(?:#.*)?$/.test(line.trim());
45
+ }
46
+ function httpProvider(baseUrl){
47
+ return [PROVIDER_BEGIN,'[model_providers.'+PROVIDER+']','name = "Codex Unlimit"','base_url = '+JSON.stringify(baseUrl),
48
+ 'wire_api = "responses"','requires_openai_auth = true','supports_websockets = false',PROVIDER_END];
49
+ }
50
+ function stripHttpProvider(text,baseUrl){
51
+ const {lines,join}=layout(text),headers=lines.flatMap((line,i)=>providerHeader(line)?[i]:[]);
52
+ const starts=lines.flatMap((line,i)=>line.trim()===PROVIDER_BEGIN?[i]:[]),ends=lines.flatMap((line,i)=>line.trim()===PROVIDER_END?[i]:[]);
53
+ if(!headers.length){if(starts.length||ends.length)throw Error('managed_configuration_changed_review_required');return {text,found:false};}
54
+ if(headers.length!==1||starts.length!==ends.length||starts.length>1)throw Error('managed_configuration_changed_review_required');
55
+ const first=headers[0];let end=first+1;while(end<lines.length&&!lines[end].trimStart().startsWith('['))end++;
56
+ const marked=starts.length===1;
57
+ if(marked&&(starts[0]!==first-1||ends[0]<=first||ends[0]>=end))throw Error('managed_configuration_changed_review_required');
58
+ const expected=new Map([['name','Codex Unlimit'],['base_url',baseUrl],['wire_api','responses'],['requires_openai_auth',true],['supports_websockets',false]]),found=new Map();
59
+ for(const line of lines.slice(first+1,marked?ends[0]:end)){
60
+ if(!line.trim()||line.trimStart().startsWith('#'))continue;
61
+ const key=rootKey(line),value=line.slice(line.indexOf('=')+1).trim(),literal=/^(?:"([^"\\]*)"|'([^']*)'|(true|false))\s*(?:#.*)?$/.exec(value);
62
+ const parsed=literal?.[3]?literal[3]==='true':literal?.[1]??literal?.[2];
63
+ if(!literal||!expected.has(key)||found.has(key)||parsed!==expected.get(key))throw Error('managed_configuration_changed_review_required');
64
+ found.set(key,parsed);
65
+ }
66
+ if(found.size!==expected.size)throw Error('managed_configuration_changed_review_required');
67
+ return {text:join(lines.filter((_,i)=>i<(marked?starts[0]:first)||i>=(marked?ends[0]+1:end))),found:true};
68
+ }
69
+ function hasProviderConflict(text){
70
+ return layout(text).lines.some(line=>providerHeader(line)||rootKey(line)==='model_providers'||/^\s*(?:model_providers|"model_providers"|'model_providers')\s*\.\s*(?:codexunlimit|"codexunlimit"|'codexunlimit')\s*[.=]/.test(line));
71
+ }
40
72
  function directRouting(text){
41
73
  const {fields}=layout(text);
42
74
  return !fields.has('profile')&&!fields.has('openai_base_url')&&!fields.has('model_catalog_json')&&
@@ -44,6 +76,7 @@ function directRouting(text){
44
76
  }
45
77
  function directConfiguration(text,previous){
46
78
  if(!directRouting(text))return null;
79
+ text=stripHttpProvider(text,previous.baseUrl).text;
47
80
  if(![BEGIN,END,PREVIOUS].some(marker=>text.includes(marker)))return text;
48
81
  const {lines,end,join}=layout(text),source=layout(verifiedOriginal(previous));
49
82
  const saved=source.lines.slice(0,source.end).filter(line=>ROOT_KEYS.has(rootKey(line)));
@@ -71,21 +104,23 @@ function verifiedOriginal(previous){
71
104
  export function managedConfig(original,baseUrl){
72
105
  if(original.includes('# BEGIN TDH Relay')||original.includes('# BEGIN Codexunlimit managed'))throw Error('existing_connector_review_required');
73
106
  if(original.includes(BEGIN)||original.includes(END)||original.includes(PREVIOUS))throw Error('managed_configuration_already_present');
107
+ if(hasProviderConflict(original))throw Error('managed_provider_conflict');
74
108
  const bom=original.startsWith('\uFEFF')?'\uFEFF':'',crlf=original.includes('\r\n');
75
109
  const {lines,end}=splitRoot(original.slice(bom.length).replace(/\r\n/g,'\n'));
76
110
  const root=lines.slice(0,end).map(line=>ROOT_KEYS.has(rootKey(line))?PREVIOUS+Buffer.from(line).toString('base64'):line);
77
- const block=[BEGIN,'openai_base_url = '+JSON.stringify(baseUrl),'model_provider = "openai"',END].join('\n');
78
- let result=[block,...root,...lines.slice(end)].join('\n');
111
+ const block=[BEGIN,'openai_base_url = '+JSON.stringify(baseUrl),'model_provider = "'+PROVIDER+'"',END].join('\n');
112
+ let result=[block,...root,...httpProvider(baseUrl),...lines.slice(end)].join('\n');
79
113
  if(crlf)result=result.replace(/\n/g,'\r\n');
80
114
  return bom+result;
81
115
  }
82
116
  export function restoreConfig(current,baseUrl,{original}={}){
83
- const {lines,end,fields,join}=layout(current);
117
+ const stripped=stripHttpProvider(current,baseUrl);
118
+ const {lines,end,fields,join}=layout(stripped.text);
84
119
  const begins=[],ends=[];
85
120
  lines.forEach((line,i)=>{if(line.trim()===BEGIN)begins.push(i);if(line.trim()===END)ends.push(i);});
86
121
  const marked=begins.length===1&&ends.length===1&&begins[0]<ends[0]&&ends[0]<end;
87
122
  const unmarked=begins.length===0&&ends.length===0&&original!==undefined;
88
- if(!marked&&!unmarked||fields.get('openai_base_url')?.value!==baseUrl||fields.get('model_provider')?.value!=='openai')
123
+ if(!marked&&!unmarked||fields.get('openai_base_url')?.value!==baseUrl||!['openai',PROVIDER].includes(fields.get('model_provider')?.value)||fields.get('model_provider')?.value===PROVIDER&&!stripped.found)
89
124
  throw Error('managed_configuration_changed_review_required');
90
125
  if(fields.has('model_catalog_json'))throw Error('new_provider_settings_review_required');
91
126
  const source=original===undefined?null:layout(original);
@@ -182,3 +217,8 @@ export function codexConnection(state){
182
217
  return {configured:false,managed:!!previous,repairable:false,error:code};
183
218
  }
184
219
  }
220
+
221
+ export function migrateCodexTransport(state){
222
+ if(!state.get('codex_sync')||!codexConnection(state).configured)return {changed:false};
223
+ return syncCodex(state);
224
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexunlimit",
3
- "version": "0.1.0-web.10",
3
+ "version": "0.1.0-web.12",
4
4
  "private": false,
5
5
  "description": "Codex Unlimit — trò chuyện và quản lý kết nối trên máy của bạn",
6
6
  "type": "module",
package/product.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export const VERSION='0.1.0-web.10';
1
+ export const VERSION='0.1.0-web.12';
2
2
  export const PRODUCT='codexunlimit-connector';
3
3
  export const DEFAULT_PORT=18457;
4
4
  export const LICENSE_ORIGIN='https://codexunlimit.tudonghoa.me';
package/relay.mjs CHANGED
@@ -5,11 +5,12 @@ import {pipeline} from 'node:stream/promises';
5
5
  import {PRODUCT,VERSION,LICENSE_ORIGIN} from './product.mjs';
6
6
  import {hash} from './protocol.mjs';
7
7
  import {isImageRoute,gatewayRequest} from './image-http.mjs';
8
+ import {boundedJson} from './io.mjs';
8
9
 
9
10
  const CONTEXT_HEADERS=['content-type','content-encoding','chatgpt-account-id','session_id','session-id','conversation_id','originator','version','openai-beta','x-codex-beta-features','x-codex-parent-thread-id','x-codex-turn-metadata','x-codex-turn-state'];
10
- export function signedHeaders(license,state,method,route,authorization,body=Buffer.alloc(0),headers=new Headers()){
11
+ export function signedHeaders(license,state,method,route,authorization,body=Buffer.alloc(0),headers=new Headers(),timestamp=Date.now()){
11
12
  const envelope=state.get('license.lease');license.check(envelope,'desktop',{allowExpired:true});
12
- const timestamp=Date.now(),nonce=crypto.randomBytes(24).toString('base64url'),key=license.device.privateKey;
13
+ const nonce=crypto.randomBytes(24).toString('base64url'),key=license.device.privateKey;
13
14
  const bodyHash=hash(body??Buffer.alloc(0));
14
15
  const contextHeaders=isImageRoute(route)?[...CONTEXT_HEADERS,'x-codex-image-turn-id']:CONTEXT_HEADERS;
15
16
  const text=Buffer.from(JSON.stringify(['cu-request-v2',envelope.payload,method,route,timestamp,nonce,hash(authorization),bodyHash,contextHeaders.map(name=>[name,headers.get(name)??''])]));
@@ -17,13 +18,43 @@ export function signedHeaders(license,state,method,route,authorization,body=Buff
17
18
  'x-cu-proof-version':'2','x-cu-body-sha256':bodyHash,
18
19
  'x-cu-proof':crypto.sign(key.asymmetricKeyType==='ec'?'sha256':null,text,key).toString('base64url')};
19
20
  }
20
- export function createConnector(state,{license,localApp,pool,onStop=()=>{},prepareStop=()=>{},origin=LICENSE_ORIGIN,request=gatewayRequest,maxBufferedBytes=128*1024*1024,bodyTimeoutMs=60000}={}){
21
+ const connectionError=(status,code)=>Object.assign(Error(code),{status,code});
22
+ const requestId=value=>typeof value==='string'&&/^[a-f0-9-]{36}$/i.test(value)?value:null;
23
+ function waitForRetry(ms,signal){
24
+ return new Promise((resolve,reject)=>{
25
+ if(signal.aborted)return reject(signal.reason);
26
+ const finish=error=>{clearTimeout(timer);signal.removeEventListener('abort',abort);error?reject(error):resolve();};
27
+ const abort=()=>finish(signal.reason??connectionError(499,'client_cancelled'));
28
+ const timer=setTimeout(()=>finish(),ms);signal.addEventListener('abort',abort,{once:true});
29
+ });
30
+ }
31
+ function connectionQueue(limit,{maxQueued,queueTimeoutMs}){
32
+ const active={inference:0,models:0},pending=[];let capacity=Math.min(2,limit);
33
+ const drain=()=>{for(let i=0;i<pending.length;){const item=pending[i];if(active[item.lane]<(item.lane==='models'?1:capacity)){pending.splice(i,1);item.grant();}else i++;}};
34
+ return {
35
+ observe(response){const value=response.headers.get('x-cu-max-concurrent');if(value&&/^[1-9][0-9]{0,2}$/.test(value)){capacity=Math.min(limit,Number(value));drain();}},
36
+ acquire(lane,signal){return new Promise((resolve,reject)=>{
37
+ if(signal.aborted)return reject(signal.reason);
38
+ if(pending.length>=maxQueued)return reject(connectionError(429,'local_queue_full'));
39
+ let timer,settled=false;
40
+ const clean=()=>{clearTimeout(timer);signal.removeEventListener('abort',abort);};
41
+ const fail=error=>{if(settled)return;settled=true;clean();const i=pending.indexOf(item);if(i>=0)pending.splice(i,1);reject(error);};
42
+ const abort=()=>fail(signal.reason??connectionError(499,'client_cancelled'));
43
+ const item={lane,grant(){if(settled)return;settled=true;clean();active[lane]++;let released=false;resolve(()=>{if(released)return;released=true;active[lane]--;drain();});}};
44
+ signal.addEventListener('abort',abort,{once:true});timer=setTimeout(()=>fail(connectionError(429,'local_queue_timeout')),queueTimeoutMs);
45
+ pending.push(item);drain();
46
+ })}
47
+ };
48
+ }
49
+ export function createConnector(state,{license,localApp,pool,onStop=()=>{},prepareStop=()=>{},origin=LICENSE_ORIGIN,request=gatewayRequest,maxBufferedBytes=128*1024*1024,bodyTimeoutMs=60000,maxQueued=32,queueTimeoutMs=15000,retryBudgetMs=15000,retryDelayMs=1000}={}){
21
50
  if(new URL(origin).protocol!=='https:')throw Error('https_required');
22
- let active=0,buffered=0;
51
+ let buffered=0,clockOffsetMs=0;
52
+ const queue=connectionQueue(state.settings.get('relay.max_concurrency')??4,{maxQueued,queueTimeoutMs});
23
53
  const server=http.createServer(async(req,res)=>{
24
- const controller=new AbortController(),started=Date.now();let counted=false,status=503,reserved=0,bodyTimer;
54
+ const controller=new AbortController(),started=Date.now(),localRequestId=crypto.randomUUID();
55
+ let status=503,reserved=0,bodyTimer,release,tracked=false,stage='routing',errorCode=null,serverRequestId=null,attempts=0,queueMs=0,uploadMs=0,proofAgeMs=null;
25
56
  req.on('aborted',()=>controller.abort());res.on('close',()=>{if(!res.writableFinished)controller.abort();});
26
- const json=(code,data)=>{res.writeHead(code,{'content-type':'application/json','cache-control':'no-store'});res.end(JSON.stringify(data));};
57
+ const json=(code,data)=>{status=code;errorCode=data?.error?.code??errorCode;res.writeHead(code,{'content-type':'application/json','cache-control':'no-store'});res.end(JSON.stringify(data));};
27
58
  try{
28
59
  if(!['127.0.0.1:'+server.address().port,'localhost:'+server.address().port].includes(req.headers.host))return json(403,{error:{code:'local_request_denied'}});
29
60
  if(localApp&&await localApp(req,res,server))return;
@@ -39,19 +70,23 @@ export function createConnector(state,{license,localApp,pool,onStop=()=>{},prepa
39
70
  const mode=req.headers['x-cu-stop-mode']??'exit';if(!['exit','update'].includes(mode))return json(400,{error:{code:'invalid_stop_mode'}});
40
71
  await prepareStop(mode);json(200,{stopped:true});setImmediate(onStop);return;
41
72
  }
73
+ tracked=/^\/v1\/(?:responses(?:\/compact)?|models(?:\?.*)?|images\/(?:generations|edits))$/.test(req.url)||isImageRoute(req.url);
74
+ if(tracked)res.setHeader('x-cu-local-request-id',localRequestId);
42
75
  if(!(req.method==='GET'&&/^\/v1\/models(?:\?client_version=[a-zA-Z0-9._%-]{1,100})?$/.test(req.url))&&!(req.method==='POST'&&(['/v1/responses','/v1/responses/compact'].includes(req.url)||isImageRoute(req.url))))return json(404,{error:{code:'route_not_found'}});
43
76
  let authorization=req.headers.authorization??'';
44
77
  const selectedAccount=pool?.active();
45
78
  if(!/^Bearer [^\s]{16,32768}$/.test(authorization)||authorization.startsWith('Bearer sk-'))return json(401,{error:{code:'chatgpt_sign_in_required'}});
46
- if(active>=state.settings.get('relay.max_concurrency'))return json(429,{error:{code:'too_many_requests'}});
79
+ stage='queue';const queuedAt=Date.now();
80
+ release=await queue.acquire(req.method==='GET'?'models':'inference',controller.signal);queueMs=Date.now()-queuedAt;
81
+ controller.signal.throwIfAborted();
47
82
  const limit=state.settings.get('relay.max_body_bytes');if(Number(req.headers['content-length']??0)>limit){req.resume();return json(413,{error:{code:'request_too_large'}});}
48
83
  const length=Number(req.headers['content-length']??limit);
49
84
  if(!Number.isSafeInteger(length)||length<0)return json(400,{error:{code:'invalid_body_length'}});
50
85
  // Account for input chunks and their contiguous copy together.
51
86
  reserved=req.method==='POST'?2*length:0;
52
- if(buffered+reserved>maxBufferedBytes){req.resume();return json(503,{error:{code:'client_memory_busy'}});}
53
- buffered+=reserved;active++;counted=true;
54
- bodyTimer=setTimeout(()=>{controller.abort();req.destroy();},bodyTimeoutMs);bodyTimer.unref();
87
+ if(buffered+reserved>maxBufferedBytes){reserved=0;req.resume();return json(503,{error:{code:'client_memory_busy'}});}
88
+ buffered+=reserved;stage='upload';const uploadStarted=Date.now();
89
+ bodyTimer=setTimeout(()=>{controller.abort(connectionError(408,'local_upload_timeout'));req.destroy();},bodyTimeoutMs);bodyTimer.unref();
55
90
  const headers=new Headers();
56
91
  for(const name of ['authorization','content-type','content-encoding','chatgpt-account-id','session_id','session-id','conversation_id','originator','user-agent','version','openai-beta','x-codex-beta-features','x-codex-parent-thread-id','x-codex-turn-metadata','x-codex-turn-state','x-codex-image-turn-id'])if(typeof req.headers[name]==='string')headers.set(name,req.headers[name]);
57
92
  const localToken=Buffer.from(String(req.headers['x-cu-control']??'')),expectedToken=Buffer.from(state.get('control_token')??'');
@@ -62,22 +97,60 @@ export function createConnector(state,{license,localApp,pool,onStop=()=>{},prepa
62
97
  }
63
98
  // Bound input in the connector too, before forwarding. No request content is persisted.
64
99
  const chunks=[];let size=0;for await(const chunk of req){size+=chunk.length;if(size>limit)throw Object.assign(Error('request_too_large'),{status:413});chunks.push(chunk);}
65
- clearTimeout(bodyTimer);const payload=req.method==='POST'?Buffer.concat(chunks):undefined;chunks.length=0;
66
- // Sign only after upload completes, so a slow upload cannot age the proof out.
67
- controller.signal.throwIfAborted();await license.forRequest('desktop');controller.signal.throwIfAborted();
68
- for(const [key,value]of Object.entries(signedHeaders(license,state,req.method,req.url,authorization,payload,headers)))headers.set(key,value);
69
- const response=await request(origin+req.url,{method:req.method,headers,body:payload,redirect:'error',signal:AbortSignal.any([controller.signal,AbortSignal.timeout(isImageRoute(req.url)?630000:1800000)])});
100
+ clearTimeout(bodyTimer);uploadMs=Date.now()-uploadStarted;const payload=req.method==='POST'?Buffer.concat(chunks):undefined;chunks.length=0;
101
+ // Queueing and upload finish before the timestamp/nonce are minted. A retry
102
+ // is allowed only after an authenticated gateway's explicit pre-upstream denial.
103
+ stage='license';controller.signal.throwIfAborted();await license.forRequest('desktop');controller.signal.throwIfAborted();
104
+ const retryDeadline=Date.now()+retryBudgetMs;let response,proofRetries=0;
105
+ for(;;){
106
+ stage='gateway';const sentAt=Date.now();
107
+ for(const [key,value]of Object.entries(signedHeaders(license,state,req.method,req.url,authorization,payload,headers,Math.round(sentAt+clockOffsetMs))))headers.set(key,value);
108
+ attempts++;
109
+ response=await request(origin+req.url,{method:req.method,headers,body:payload,redirect:'error',signal:AbortSignal.any([controller.signal,AbortSignal.timeout(isImageRoute(req.url)?630000:1800000)])});
110
+ queue.observe(response);serverRequestId=requestId(response.headers.get('x-cu-request-id'));
111
+ const receivedAt=Date.now(),serverDate=Date.parse(response.headers.get('date')??'');
112
+ if(Number.isFinite(serverDate)){
113
+ proofAgeMs=serverDate-Number(headers.get('x-cu-time'));
114
+ const offset=serverDate+500-(sentAt+receivedAt)/2;
115
+ if(serverRequestId&&receivedAt-sentAt<5000&&Math.abs(offset)<=300000)clockOffsetMs=Math.round(offset);
116
+ }
117
+ let gatewayCode=null;
118
+ if(serverRequestId&&response.status>=400&&(response.headers.get('content-type')??'').toLowerCase().startsWith('application/json')){
119
+ const data=await boundedJson(response,32768,'gateway_response_invalid').catch(()=>{throw connectionError(502,'gateway_response_invalid');});
120
+ gatewayCode=typeof data?.error?.code==='string'&&/^[a-z_]{1,80}$/.test(data.error.code)?data.error.code:null;
121
+ response=new Response(JSON.stringify(data),{status:response.status,headers:response.headers});
122
+ }
123
+ const stale=response.status===408&&gatewayCode==='request_proof_expired'&&proofRetries<1;
124
+ const busy=response.status===429&&gatewayCode==='license_concurrency_limit';
125
+ const requestedDelay=Number(response.headers.get('retry-after'))*1000;
126
+ const delay=stale?0:Math.max(retryDelayMs,Number.isFinite(requestedDelay)&&requestedDelay>0?requestedDelay:0);
127
+ if((stale||busy)&&attempts<8&&Date.now()+delay<retryDeadline){
128
+ await response.body?.cancel();if(stale)proofRetries++;
129
+ stage='retry_wait';await waitForRetry(delay,controller.signal);continue;
130
+ }
131
+ errorCode=gatewayCode;break;
132
+ }
70
133
  license.acceptLease(response.headers.get('x-cu-lease'));
71
134
  pool?.observe({token:authorization.slice(7),accountId:headers.get('chatgpt-account-id')??undefined},response);
72
135
  if(response.status>=400&&!(response.headers.get('content-type')??'').toLowerCase().startsWith('application/json')){
73
136
  response.body?.cancel().catch(()=>{});status=response.status===429?429:503;const wait=Number(response.headers.get('retry-after'));res.setHeader('retry-after',String(Number.isFinite(wait)&&wait>0?Math.min(60,Math.ceil(wait)):3));
74
137
  return json(status,{error:{code:'service_unavailable',message:'Máy chủ tạm thời chưa phản hồi hợp lệ. Không tự gửi lại tác vụ đang chạy; kiểm tra kết nối rồi thử lại.'}});
75
138
  }
76
- status=response.status;const outgoing={'cache-control':'no-store'};
139
+ stage='stream';status=response.status;const outgoing={'cache-control':'no-store'};
77
140
  for(const [key,value]of response.headers)if(['content-type','retry-after','x-cu-request-id'].includes(key)||key.startsWith('x-codex-')||isImageRoute(req.url)&&key==='x-relay-policy-mode')outgoing[key]=value;
78
- res.writeHead(status,outgoing);if(response.body)await pipeline(Readable.fromWeb(response.body),res,{signal:controller.signal});else res.end();
79
- }catch(error){status=controller.signal.aborted?499:error.status??502;if(res.headersSent)res.destroy();else{if([429,503].includes(status))res.setHeader('retry-after',String(error.retryAfter??3));json(status,{error:{code:error.status?error.code??error.message:'connection_failed'}});}}
80
- finally{clearTimeout(bodyTimer);controller.abort();if(counted){active--;buffered-=reserved;state.record({status,duration:Date.now()-started});}}
141
+ res.writeHead(status,outgoing);if(response.body)await pipeline(Readable.fromWeb(response.body),res,{signal:controller.signal});else res.end();stage='complete';
142
+ }catch(error){
143
+ const reason=controller.signal.reason?.status?controller.signal.reason:error;
144
+ status=reason?.status??(controller.signal.aborted?499:502);
145
+ errorCode=(typeof reason?.code==='string'?reason.code.toLowerCase():typeof reason?.cause?.code==='string'?reason.cause.code.toLowerCase():null)??(controller.signal.aborted?'client_cancelled':stage==='stream'?'stream_interrupted':'connection_failed');
146
+ if(res.headersSent||status===499||res.destroyed)res.destroy();
147
+ else{if([429,503].includes(status))res.setHeader('retry-after',String(reason?.retryAfter??3));json(status,{error:{code:errorCode}});}
148
+ }finally{
149
+ if(stage==='queue')queueMs=Date.now()-started;
150
+ if(stage==='upload')uploadMs=Math.max(0,Date.now()-started-queueMs);
151
+ clearTimeout(bodyTimer);controller.abort();buffered-=reserved;release?.();
152
+ if(tracked){try{state.record({status,duration:Date.now()-started,requestId:localRequestId,serverRequestId,route:req.url.split('?')[0],method:req.method,errorCode,stage,queueMs,uploadMs,attempts,proofAgeMs});}catch{}}
153
+ }
81
154
  });
82
- server.headersTimeout=15000;server.requestTimeout=60000;return server;
155
+ server.headersTimeout=15000;server.requestTimeout=bodyTimeoutMs+queueTimeoutMs;return server;
83
156
  }
package/state.mjs CHANGED
@@ -1,23 +1,33 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import crypto from 'node:crypto';
4
- import {DatabaseSync} from 'node:sqlite';
5
- import {DEFAULT_PORT,VERSION} from './product.mjs';
6
- export class ClientState{
7
- constructor(home){
8
- this.home=path.resolve(home);fs.mkdirSync(path.join(this.home,'data'),{recursive:true,mode:0o700});
9
- this.db=new DatabaseSync(path.join(this.home,'data','connector.sqlite'));
10
- this.db.exec('PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; CREATE TABLE IF NOT EXISTS settings(key TEXT PRIMARY KEY,value TEXT NOT NULL); CREATE TABLE IF NOT EXISTS desktop_usage(id INTEGER PRIMARY KEY,ts INTEGER NOT NULL,status INTEGER NOT NULL,duration_ms INTEGER NOT NULL);');
11
- this.settings={get:key=>this.get(key),set:(key,value)=>this.set(key,value)};
12
- }
13
- get(key){const row=this.db.prepare('SELECT value FROM settings WHERE key=?').get(key);return row?JSON.parse(row.value):null;}
14
- set(key,value){this.db.prepare('INSERT INTO settings VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value').run(key,JSON.stringify(value));}
15
- transaction(action){this.db.exec('BEGIN IMMEDIATE');try{const value=action();this.db.exec('COMMIT');return value;}catch(error){this.db.exec('ROLLBACK');throw error;}}
16
- initialize({port=DEFAULT_PORT,codexHome}={}){
17
- if(!this.get('initialized')){this.set('relay.port',port);this.set('control_token',crypto.randomBytes(32).toString('base64url'));this.set('initialized',true);}
18
- this.set('relay.max_body_bytes',64*1024*1024);this.set('relay.max_concurrency',4);this.set('version',VERSION);
19
- if(codexHome)this.set('codex_home',path.resolve(codexHome));
20
- }
21
- record(row){this.db.prepare('INSERT INTO desktop_usage(ts,status,duration_ms) VALUES(?,?,?)').run(Date.now(),row.status,row.duration);this.db.prepare('DELETE FROM desktop_usage WHERE id<(SELECT MAX(id)-2000 FROM desktop_usage)').run();}
22
- close(){this.db.close();}
23
- }
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import {DatabaseSync} from 'node:sqlite';
5
+ import {DEFAULT_PORT,VERSION} from './product.mjs';
6
+ export class ClientState{
7
+ constructor(home){
8
+ this.home=path.resolve(home);fs.mkdirSync(path.join(this.home,'data'),{recursive:true,mode:0o700});
9
+ this.db=new DatabaseSync(path.join(this.home,'data','connector.sqlite'));
10
+ this.db.exec('PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; CREATE TABLE IF NOT EXISTS settings(key TEXT PRIMARY KEY,value TEXT NOT NULL); CREATE TABLE IF NOT EXISTS desktop_usage(id INTEGER PRIMARY KEY,ts INTEGER NOT NULL,status INTEGER NOT NULL,duration_ms INTEGER NOT NULL);');
11
+ this.db.exec('CREATE TABLE IF NOT EXISTS connection_diagnostics(id INTEGER PRIMARY KEY,at INTEGER NOT NULL,request_id TEXT,server_request_id TEXT,route TEXT,method TEXT,status INTEGER,error_code TEXT,stage TEXT,duration_ms INTEGER,queue_ms INTEGER,upload_ms INTEGER,attempts INTEGER,proof_age_ms INTEGER);');
12
+ this.settings={get:key=>this.get(key),set:(key,value)=>this.set(key,value)};
13
+ }
14
+ get(key){const row=this.db.prepare('SELECT value FROM settings WHERE key=?').get(key);return row?JSON.parse(row.value):null;}
15
+ set(key,value){this.db.prepare('INSERT INTO settings VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value').run(key,JSON.stringify(value));}
16
+ transaction(action){this.db.exec('BEGIN IMMEDIATE');try{const value=action();this.db.exec('COMMIT');return value;}catch(error){this.db.exec('ROLLBACK');throw error;}}
17
+ initialize({port=DEFAULT_PORT,codexHome}={}){
18
+ if(!this.get('initialized')){this.set('relay.port',port);this.set('control_token',crypto.randomBytes(32).toString('base64url'));this.set('initialized',true);}
19
+ this.set('relay.max_body_bytes',64*1024*1024);this.set('relay.max_concurrency',4);this.set('version',VERSION);
20
+ if(codexHome)this.set('codex_home',path.resolve(codexHome));
21
+ }
22
+ record(row){
23
+ const now=Date.now();this.db.prepare('INSERT INTO desktop_usage(ts,status,duration_ms) VALUES(?,?,?)').run(now,row.status,row.duration);this.db.prepare('DELETE FROM desktop_usage WHERE id<(SELECT MAX(id)-2000 FROM desktop_usage)').run();
24
+ if(!/^\/(?:v1\/)?(?:responses(?:\/compact)?|models|images\/(?:generations|edits))$/.test(row.route??''))return;
25
+ const code=value=>typeof value==='string'&&/^[a-z_]{1,80}$/.test(value)?value:null;
26
+ const id=value=>typeof value==='string'&&/^[a-f0-9-]{36}$/i.test(value)?value:null;
27
+ const number=value=>Number.isSafeInteger(value)?value:null;
28
+ this.db.prepare('INSERT INTO connection_diagnostics(at,request_id,server_request_id,route,method,status,error_code,stage,duration_ms,queue_ms,upload_ms,attempts,proof_age_ms) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)').run(now,id(row.requestId),id(row.serverRequestId),row.route,['GET','POST'].includes(row.method)?row.method:null,number(row.status),code(row.errorCode),code(row.stage),number(row.duration),number(row.queueMs),number(row.uploadMs),number(row.attempts),number(row.proofAgeMs));
29
+ this.db.prepare('DELETE FROM connection_diagnostics WHERE id<=(SELECT MAX(id)-2000 FROM connection_diagnostics) OR at<?').run(now-7*86400000);
30
+ }
31
+ diagnostics(){return this.db.prepare('SELECT * FROM connection_diagnostics ORDER BY id DESC LIMIT 100').all();}
32
+ close(){this.db.close();}
33
+ }
package/web/messages.js CHANGED
@@ -1,63 +1,66 @@
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_lease_expired:'Phiên xác thực tạm thời đã hết hạn. Chọn Kiểm tra thời hạn để lấy phiên mới; nếu lặp lại, kiểm tra ngày giờ trên máy.',
23
- license_invalid_or_expired:'Key đã hết hạn hoặc bị thu hồi. Liên hệ nơi cấp key.',
24
- license_revoked:'Key đã bị thu hồi. Liên hệ nơi cấp key.',
25
- device_limit_reached:'Key đã đủ số máy được phép dùng. Mở trang khách hàng để kiểm tra thiết bị.',
26
- device_revoked:'Thiết bị này đã bị thu hồi quyền sử dụng. Liên hệ nơi cấp key.',
27
- 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.',
28
- privacy_required:'Đánh dấu đồng ý sử dụng phiên đăng nhập trước khi kết nối.',
29
- request_too_large:'Cuộc trò chuyện đã quá dài. Hãy mở cuộc trò chuyện mới.',
30
- 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.',
31
- 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.',
32
- 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.',
33
- 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.',
34
- activation_required:'Kích hoạt key trong phần Mã sử dụng trước khi kết nối Codex.',
35
- invalid_activation_key:'Key chưa đúng định dạng. Kiểm tra lại mã bạn nhận được.',
36
- usage_limit:'Bạn đã đến giới hạn sử dụng hiện tại. Vui lòng thử lại sau.',
37
- service_unavailable:'Máy chủ xử lý chưa phản hồi. Thử lại sau hoặc liên hệ hỗ trợ.',
38
- 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.',
39
- 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ý.',
40
- 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.',
41
- 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.',
42
- 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.',
43
- 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 đó.',
44
- 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ợ.',
45
- 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.',
46
- 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.',
47
- 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.',
48
- 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.',
49
- 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.',
50
- 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.',
51
- 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ợ.',
52
- 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.',
53
- 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.',
54
- 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.',
55
- 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ợ.',
56
- 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ợ.',
57
- startup_unavailable:'Tự khởi động cùng hệ điều hành hiện hỗ trợ Windows.',
58
- 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.',
59
- 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.',
60
- invalid_input:'Thông tin gửi chưa hợp lệ. Tải lại trang rồi thử lại.',
61
- 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.',
62
- 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.'
63
- };
1
+ export const errors={
2
+ connection_busy:'Các tác vụ trước vẫn đang chạy. Chờ chúng hoàn tất rồi gửi tiếp; yêu cầu này chưa được gửi tới mô hình.',
3
+ request_clock_error:'Chưa xác thực được thời điểm gửi yêu cầu. Bật đồng bộ ngày giờ trên máy rồi thử lại.',
4
+ managed_provider_conflict:'Tên kết nối Codex Unlimit đang được dùng trong cấu hình khác. Liên hệ hỗ trợ để giữ nguyên kết nối hiện có.',
5
+ 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.',
6
+ 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.',
7
+ 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.',
8
+ 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.',
9
+ 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.',
10
+ 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.',
11
+ 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.',
12
+ pool_account_exists:'Tài khoản này đã có trong pool. Chọn tài khoản đã lưu để sử dụng.',
13
+ 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.',
14
+ pool_login_pending:'Đang có một phiên đăng nhập. Hoàn tất hoặc hủy phiên đó trước.',
15
+ 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.',
16
+ 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ã.',
17
+ 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.',
18
+ 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.',
19
+ 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.',
20
+ 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.',
21
+ 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.',
22
+ 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.',
23
+ 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.',
24
+ 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.',
25
+ license_lease_expired:'Phiên xác thực tạm thời đã hết hạn. Chọn Kiểm tra thời hạn để lấy phiên mới; nếu lặp lại, kiểm tra ngày giờ trên máy.',
26
+ license_invalid_or_expired:'Key đã hết hạn hoặc bị thu hồi. Liên hệ nơi cấp key.',
27
+ license_revoked:'Key đã bị thu hồi. Liên hệ nơi cấp key.',
28
+ device_limit_reached:'Key đã đủ số máy được phép dùng. Mở trang khách hàng để kiểm tra thiết bị.',
29
+ device_revoked:'Thiết bị này đã bị thu hồi quyền sử dụng. Liên hệ nơi cấp key.',
30
+ 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.',
31
+ privacy_required:'Đánh dấu đồng ý sử dụng phiên đăng nhập trước khi kết nối.',
32
+ request_too_large:'Cuộc trò chuyện đã quá dài. Hãy mở cuộc trò chuyện mới.',
33
+ 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.',
34
+ 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.',
35
+ 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.',
36
+ 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.',
37
+ activation_required:'Kích hoạt key trong phần Mã sử dụng trước khi kết nối Codex.',
38
+ invalid_activation_key:'Key chưa đúng định dạng. Kiểm tra lại mã bạn nhận được.',
39
+ usage_limit:'Bạn đã đến giới hạn sử dụng hiện tại. Vui lòng thử lại sau.',
40
+ service_unavailable:'Máy chủ xử lý chưa phản hồi. Thử lại sau hoặc liên hệ hỗ trợ.',
41
+ 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.',
42
+ 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ý.',
43
+ 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.',
44
+ 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.',
45
+ 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.',
46
+ 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 đó.',
47
+ 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ợ.',
48
+ 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.',
49
+ 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.',
50
+ 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.',
51
+ 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.',
52
+ 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.',
53
+ 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.',
54
+ 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ợ.',
55
+ 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.',
56
+ 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.',
57
+ 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.',
58
+ 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ợ.',
59
+ 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ợ.',
60
+ startup_unavailable:'Tự khởi động cùng hệ điều hành hiện hỗ trợ Windows.',
61
+ 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.',
62
+ 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.',
63
+ invalid_input:'Thông tin gửi chưa hợp lệ. Tải lại trang rồi thử lại.',
64
+ 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.',
65
+ 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.'
66
+ };