@fxz26284407/yuanma-gateway 0.1.0 → 0.1.2
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/README.md +44 -0
- package/bin/yuanma-gateway.mjs +63 -24
- package/dist/agents.js +1 -1
- package/dist/antigravity-acp.js +132 -0
- package/dist/antigravity-conversation.js +73 -0
- package/dist/antigravity-native.js +150 -0
- package/dist/antigravity.js +660 -0
- package/dist/cli.js +14 -8
- package/dist/extension-icons.js +2 -1
- package/dist/lan.js +44 -0
- package/dist/relay-client.js +57 -0
- package/dist/relay.js +0 -55
- package/dist/server.js +28 -6
- package/dist/version.js +2 -0
- package/gateway/config.example.json +3 -1
- package/package.json +1 -1
- package/scripts/pairing-qr.mjs +23 -14
- package/scripts/restart-local-gateway.mjs +26 -13
package/README.md
CHANGED
|
@@ -2,6 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
远码的 Mac 本地网关与 HTTP/WS 公网密文中继。只包含两端网关,不包含鸿蒙工程、签名文件、个人文档库或运行数据。
|
|
4
4
|
|
|
5
|
+
## 局域网直连(本地开发版本,尚未发布到 npm)
|
|
6
|
+
|
|
7
|
+
本地代码已支持手机直接连接 Mac,无需启动 Relay。npm 已发布的 0.1.0 仍沿用原中继流程;使用直连需同时更新本地网关和鸿蒙 App,旧 App 不支持新的直连二维码。
|
|
8
|
+
|
|
9
|
+
在本仓库使用 Node.js 22 构建后运行:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm run build
|
|
13
|
+
node bin/yuanma-gateway.mjs init
|
|
14
|
+
node bin/yuanma-gateway.mjs pair
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
首次初始化的中继地址可以留空。单个常规内网 IPv4 自动选择,多个地址展示网卡与 IP 供选择;虚拟网卡需确认手机可达。默认端口为 `19089`,二维码地址为 `ws://所选内网IP:19089/v1/secure`。仅该加密入口对内网开放,本地管理接口继续使用 Unix socket。仍需准备与桌面共享的 Codex 后端及现有密钥保存模式。
|
|
18
|
+
|
|
19
|
+
普通 start/restart 沿用已保存模式;pair 默认局域网,多个内网 IP 时交互选择,直接执行:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
node bin/yuanma-gateway.mjs pair
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
非交互或需要指定网卡、端口时:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
node bin/yuanma-gateway.mjs pair --lan-host 192.168.1.10 --lan-port 19089
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
将示例 IP 替换为本机实际内网地址。IP 失效时重新选择并扫码,端口冲突时更换端口;不要将手机的 localhost 当作 Mac。网关保存连接方式,后续启动复用;设备身份与授权保持原样。
|
|
32
|
+
|
|
33
|
+
保留 Relay 扩展,切换时使用:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
node bin/yuanma-gateway.mjs pair --relay-url wss://relay.example.com
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
局域网和 Relay 首版互斥,不会自动回退公网。以下中继安装与部署说明适用于已有发布版本及需要中继扩展的用户。
|
|
40
|
+
|
|
5
41
|
## 项目系列
|
|
6
42
|
|
|
7
43
|
[远码 Yuanma · GitHub Project](https://github.com/users/fxz26284407/projects/2)
|
|
@@ -38,6 +74,14 @@
|
|
|
38
74
|
- Codex 与 DeepSeek 共用下一步建议开关、预览及采用流程,各自使用自己的模型和额度。建议只填入可编辑草稿,不自动发送。
|
|
39
75
|
- DeepSeek 建议复用 `/Applications/DeepSeek Harness.app` 内置 SDK 和既有登录,独立请求近期会话文本,不加载 Agent、会话持久化或工具。支持 `deepseek-account` 与 `deepseek-official` 路由;其他第三方提供方尚未适配。桌面 SDK 更新不兼容时,建议暂不可用,原会话仍走现有桌面 Host。
|
|
40
76
|
|
|
77
|
+
## Antigravity 接入进度
|
|
78
|
+
|
|
79
|
+
支持读取 Antigravity App 的已有工作区、会话、历史分页和全文。手机可切换第三个智能体,文件引用按 Markdown 标签展示。CLI 不可用时保留只读浏览。
|
|
80
|
+
|
|
81
|
+
控制通道使用官方 Antigravity CLI 1.5.0 的 ACP,默认可执行文件为 `~/.local/bin/agy`;其他路径可在网关配置中设置 `antigravityCli`。目前已接线已有会话发送、实时进度、手机回合取消、原生审批/字符串问题回复、模型与思考档位、执行策略及图片输入。实际用户任务的发送、审批与取消闭环尚未完成验收;新建、引导、会话菜单、文档附件、扩展、Diff 和下一步建议未开放。
|
|
82
|
+
|
|
83
|
+
进程重启后无法确认的发送会保留未知状态,不自动重发。当前只支持取消手机通过本网关发起的回合,尚不提供桌面独立回合的停止或审批接管,整体使用体验仍未与 Codex 对齐。历史与摘要单文件上限为 32 MiB,超限会明确提示。
|
|
84
|
+
|
|
41
85
|
## Mac 网关安装(npm)
|
|
42
86
|
|
|
43
87
|
需要 **macOS、Node.js 22**,以及已登录、与桌面共用的 Codex app-server。当前适配固定为 app-server **0.166.0**;其他版本需要先适配协议。DeepSeek 功能另外依赖本机已有的 DeepSeek Harness 和登录。
|
package/bin/yuanma-gateway.mjs
CHANGED
|
@@ -9,8 +9,9 @@ import {parseArgs} from 'node:util';
|
|
|
9
9
|
import {createInterface} from 'node:readline/promises';
|
|
10
10
|
import {setTimeout as delay} from 'node:timers/promises';
|
|
11
11
|
import {Rpc} from '../dist/rpc.mjs';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import {lanAddresses, lanConfig, DEFAULT_LAN_PORT} from '../dist/lan.js';
|
|
13
|
+
import {relayEndpoint} from '../dist/relay-client.js';
|
|
14
|
+
import {connectionArgs, gatewayLaunch, restartGateway, gatewayStatus, stopGateway, openGatewayLog} from '../scripts/restart-local-gateway.mjs';
|
|
14
15
|
|
|
15
16
|
process.umask(0o077);
|
|
16
17
|
const root=fileURLToPath(new URL('../',import.meta.url));
|
|
@@ -22,7 +23,10 @@ const help=`远码网关(需要 Node.js 22 和与桌面共享的 Codex 后端
|
|
|
22
23
|
yuanma-gateway init --config 原配置绝对路径 --state-dir 原状态目录绝对路径
|
|
23
24
|
yuanma-gateway start [--relay-url wss://域名]
|
|
24
25
|
yuanma-gateway restart [--relay-url wss://域名]
|
|
25
|
-
yuanma-gateway pair
|
|
26
|
+
yuanma-gateway pair(默认局域网,交互选择内网 IP)
|
|
27
|
+
yuanma-gateway pair --relay-url ws[s]://中继地址
|
|
28
|
+
局域网选项:--connection-mode lan [--lan-host 内网IP] [--lan-port 19089]
|
|
29
|
+
中继选项:--relay-url ws[s]://中继地址(start/restart 沿用原模式,pair 默认局域网)
|
|
26
30
|
yuanma-gateway status | stop | projects | devices | security
|
|
27
31
|
yuanma-gateway device-project-policy --device-id 设备ID --capability control
|
|
28
32
|
默认配置与状态位于 ~/.local/share/yuanma/gateway/,npm 升级不会覆盖。
|
|
@@ -44,6 +48,7 @@ async function createJson(path,value){
|
|
|
44
48
|
}
|
|
45
49
|
function validateConfig(config){
|
|
46
50
|
if(!Array.isArray(config.projects)||!config.projects.length||config.projects.some(p=>typeof p.id!=='string'||!p.id||!Array.isArray(p.roots)||!p.roots.length||p.roots.some(r=>typeof r!=='string'||!r.startsWith('/'))))throw Error('配置需要明确的项目 ID 和绝对根目录');
|
|
51
|
+
if(config.connectionMode!==undefined&&!['lan','relay'].includes(config.connectionMode))throw Error('配置中的连接模式无效');
|
|
47
52
|
if(config.relayUrl!=null)relayEndpoint(config.relayUrl);
|
|
48
53
|
}
|
|
49
54
|
async function installation(values){
|
|
@@ -53,16 +58,46 @@ async function installation(values){
|
|
|
53
58
|
if(typeof info.config!=='string'||!info.config.startsWith('/')||typeof info.stateDir!=='string'||!info.stateDir.startsWith('/'))throw Error('安装记录中的路径无效');
|
|
54
59
|
const config=await privateJson(info.config);validateConfig(config);
|
|
55
60
|
await lstat(info.stateDir);await privateDirectory(info.stateDir);
|
|
56
|
-
if(info.relayUrl
|
|
61
|
+
if(info.relayUrl!=null)relayEndpoint(info.relayUrl);
|
|
57
62
|
return {...info,configData:config};
|
|
58
63
|
}
|
|
59
|
-
async function
|
|
64
|
+
async function rememberConnection(info,connection){
|
|
60
65
|
const current=await privateJson(settingsPath).catch(e=>{if(e.code==='ENOENT')return null;throw e;});
|
|
61
|
-
if(!current||current.config!==info.config||current.stateDir!==info.stateDir
|
|
66
|
+
if(!current||current.config!==info.config||current.stateDir!==info.stateDir){
|
|
67
|
+
const config=await privateJson(info.config);
|
|
68
|
+
const temporary=join(dirname(info.config),'connection-'+randomUUID()+'.tmp');
|
|
69
|
+
try{await createJson(temporary,{...config,...connection,relayUrl:connection.connectionMode==='relay'?connection.relayUrl:null});await rename(temporary,info.config);}
|
|
70
|
+
finally{await unlink(temporary).catch(e=>{if(e.code!=='ENOENT')throw e;});}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
62
73
|
const temporary=join(home,'installation-'+randomUUID()+'.tmp');
|
|
63
|
-
try{await createJson(temporary,{...current,relayUrl});await rename(temporary,settingsPath);}
|
|
74
|
+
try{await createJson(temporary,{...current,...connection,relayUrl:connection.connectionMode==='relay'?connection.relayUrl:null});await rename(temporary,settingsPath);}
|
|
64
75
|
finally{await unlink(temporary).catch(e=>{if(e.code!=='ENOENT')throw e;});}
|
|
65
76
|
}
|
|
77
|
+
async function selectConnection(values,saved={},choose=false){
|
|
78
|
+
if(values['relay-url']!==undefined&&values['connection-mode']==='lan')throw Error('--relay-url 与局域网模式冲突');
|
|
79
|
+
if(values['lan-host']!==undefined&&(values['relay-url']!==undefined||values['connection-mode']==='relay'))throw Error('--lan-host 与 Relay 模式冲突');
|
|
80
|
+
const mode=values['connection-mode']??(values['relay-url']!==undefined?'relay':values['lan-host']!==undefined?'lan':saved.connectionMode??(saved.relayUrl?'relay':'lan'));
|
|
81
|
+
if(mode==='relay')return {connectionMode:'relay',relayUrl:relayEndpoint(values['relay-url']??saved.relayUrl)};
|
|
82
|
+
if(mode!=='lan')throw Error('--connection-mode 只能是 lan 或 relay');
|
|
83
|
+
let host=values['lan-host']??saved.lanHost;
|
|
84
|
+
const addresses=lanAddresses();
|
|
85
|
+
if(values['lan-host']!==undefined)lanConfig(host,values['lan-port']??saved.lanPort??DEFAULT_LAN_PORT);
|
|
86
|
+
if(!addresses.some(a=>a.address===host)||(choose&&values['lan-host']===undefined&&new Set(addresses.map(a=>a.address)).size>1)){
|
|
87
|
+
if(host&&!addresses.some(a=>a.address===host))console.log('原局域网 IP 已不可用,请重新选择。');
|
|
88
|
+
const unique=addresses.filter((a,i)=>addresses.findIndex(b=>a.address===b.address)===i);
|
|
89
|
+
if(!unique.length)throw Error('未发现局域网 IPv4,请连接 Wi-Fi 或有线网络');
|
|
90
|
+
unique.forEach((a,i)=>console.log(`${i+1}. ${a.name}: ${a.address}${a.virtual?'(虚拟网卡/VPN,请确认手机可达)':''}`));
|
|
91
|
+
if(unique.length===1&&!unique[0].virtual)host=unique[0].address;
|
|
92
|
+
else{
|
|
93
|
+
if(!process.stdin.isTTY)throw Error('请显式提供 --lan-host 选择局域网 IP');
|
|
94
|
+
const prompt=createInterface({input:process.stdin,output:process.stdout});
|
|
95
|
+
try{const previous=unique.findIndex(a=>a.address===host)+1;const answer=(await prompt.question('选择手机可达的局域网 IP 序号'+(previous?'(回车沿用 '+previous+')':'')+':')).trim()||(previous?String(previous):'');const index=Number(answer);if(!/^\d+$/.test(answer)||index<1||index>unique.length)throw Error('网卡序号无效');host=unique[index-1].address;}finally{prompt.close();}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const lan=lanConfig(host,values['lan-port']??saved.lanPort??DEFAULT_LAN_PORT);
|
|
99
|
+
return {connectionMode:'lan',lanHost:lan.host,lanPort:lan.port,relayUrl:null};
|
|
100
|
+
}
|
|
66
101
|
async function catalog(socket){
|
|
67
102
|
const rpc=new Rpc(socket,'yuanma_gateway_setup',{retainMessages:false});
|
|
68
103
|
try{await rpc.initialize();const result=await rpc.call('project/list',{limit:100});return result.data;}finally{rpc.close();}
|
|
@@ -80,16 +115,18 @@ async function init(values){
|
|
|
80
115
|
const active=await gatewayStatus(info.stateDir).catch(e=>{if(['ENOENT','ECONNREFUSED'].includes(e.code))return null;throw e;});
|
|
81
116
|
if(active)await gatewayLaunch(info.stateDir);
|
|
82
117
|
// ponytail: 原地登记既有目录,不复制正在使用的 SQLite 或身份文件。
|
|
83
|
-
await createJson(settingsPath,{config:info.config,stateDir:info.stateDir,...(active?.relayUrl||info.configData.relayUrl?{relayUrl:relayEndpoint(active?.relayUrl??info.configData.relayUrl)}:{})});
|
|
118
|
+
await createJson(settingsPath,{config:info.config,stateDir:info.stateDir,...(active?.connectionMode==='lan'?{connectionMode:'lan',lanHost:active.lanHost,lanPort:active.lanPort,relayUrl:null}:info.configData.connectionMode==='lan'?{connectionMode:'lan',lanHost:info.configData.lanHost,lanPort:info.configData.lanPort,relayUrl:null}:active?.relayUrl||info.configData.relayUrl?{relayUrl:relayEndpoint(active?.relayUrl??info.configData.relayUrl)}:{})});
|
|
84
119
|
console.log('已沿用原配置、密钥、设备授权和消息账本;请保留原目录。');return;
|
|
85
120
|
}
|
|
86
121
|
const configPath=join(home,'config.json'),stateDir=join(home,'state');
|
|
87
122
|
const saved=await privateJson(configPath).catch(e=>{if(e.code==='ENOENT')return null;throw e;});
|
|
88
|
-
if(saved){validateConfig(saved);await lstat(stateDir);await privateDirectory(stateDir);await createJson(settingsPath,{config:configPath,stateDir,...(saved.relayUrl?{relayUrl:relayEndpoint(saved.relayUrl)}:{})});console.log('已保留原配置并恢复安装记录。');return;}
|
|
123
|
+
if(saved){validateConfig(saved);await lstat(stateDir);await privateDirectory(stateDir);await createJson(settingsPath,{config:configPath,stateDir,...(saved.connectionMode==='lan'?{connectionMode:'lan',lanHost:saved.lanHost,lanPort:saved.lanPort,relayUrl:null}:saved.relayUrl?{relayUrl:relayEndpoint(saved.relayUrl)}:{})});console.log('已保留原配置并恢复安装记录。');return;}
|
|
89
124
|
let prompt;
|
|
90
|
-
const ask=async question=>{if(!process.stdin.isTTY)throw Error('非交互初始化需提供 --
|
|
125
|
+
const ask=async question=>{if(!process.stdin.isTTY)throw Error('非交互初始化需提供 --projects、--allow-development-keys 和 --accept-project-access;多网卡时还需 --lan-host');prompt??=createInterface({input:process.stdin,output:process.stdout});return prompt.question(question);};
|
|
91
126
|
try{
|
|
92
|
-
const
|
|
127
|
+
const entered=values['relay-url']??(values['connection-mode']||values['lan-host']||!process.stdin.isTTY?'':await ask('中继地址(可留空使用局域网直连):'));
|
|
128
|
+
prompt?.close();prompt=undefined;
|
|
129
|
+
const connection=await selectConnection({...values,...(entered.trim()?{'relay-url':entered.trim()}:{})});
|
|
93
130
|
const socket=resolve(values['codex-socket']??join(homedir(),'.local/share/yuanma/shared/app-server.sock'));
|
|
94
131
|
const projects=await catalog(socket);
|
|
95
132
|
if(!Array.isArray(projects)||!projects.length)throw Error('共享 Codex 后端没有可选择的项目');
|
|
@@ -105,23 +142,23 @@ async function init(values){
|
|
|
105
142
|
if(!allow)throw Error('未启用当前版本所需的密钥模式,未创建配置或网关身份');
|
|
106
143
|
const accepted=values['accept-project-access']||(await ask('配对后所有发现的项目和工作区默认可执行,初始项目列表不是权限白名单。接受此策略?[y/N]:')).trim().toLowerCase()==='y';
|
|
107
144
|
if(!accepted)throw Error('未接受现有项目访问策略,未创建配置或网关身份');
|
|
108
|
-
const config={codexSocket:socket,
|
|
145
|
+
const config={codexSocket:socket,...connection,allowDevelopmentKeys:true,projects:selected.map(p=>({id:p.id,...(p.name?{name:p.name}:{}),roots:p.roots}))};validateConfig(config);
|
|
109
146
|
await privateDirectory(stateDir);
|
|
110
147
|
await createJson(configPath,config);
|
|
111
|
-
await createJson(settingsPath,{config:configPath,stateDir
|
|
148
|
+
await createJson(settingsPath,{config:configPath,stateDir,...connection});
|
|
112
149
|
console.log('初始化完成;配置:'+configPath+'\n状态:'+stateDir+'\n接下来运行 yuanma-gateway start 和 yuanma-gateway pair。');
|
|
113
150
|
}finally{prompt?.close();}
|
|
114
151
|
}
|
|
115
|
-
async function start(info,
|
|
116
|
-
const
|
|
117
|
-
const
|
|
152
|
+
async function start(info,connection){
|
|
153
|
+
const flags=connectionArgs(connection);
|
|
154
|
+
const endpoint=connection.connectionMode==='relay'?relayEndpoint(connection.relayUrl):`ws://${connection.lanHost}:${connection.lanPort}/v1/secure`;
|
|
118
155
|
const current=await gatewayStatus(info.stateDir).catch(e=>{if(['ENOENT','ECONNREFUSED'].includes(e.code))return null;throw e;});
|
|
119
|
-
if(current){await gatewayLaunch(info.stateDir);if(
|
|
156
|
+
if(current){await gatewayLaunch(info.stateDir);if((current.endpoint??current.relayUrl)!==endpoint||current.connectionMode!==connection.connectionMode)return restartGateway(info.stateDir,connection);if(!current.backendReady)throw Error('已有网关尚未就绪,请查看状态和日志');return current;}
|
|
120
157
|
await privateDirectory(info.stateDir);
|
|
121
158
|
const logPath=join(info.stateDir,'gateway.log');
|
|
122
159
|
const log=await openGatewayLog(logPath);
|
|
123
160
|
try{
|
|
124
|
-
const args=[cli,'start','--config',info.config,'--state-dir',info.stateDir];
|
|
161
|
+
const args=[cli,'start','--config',info.config,'--state-dir',info.stateDir];args.push(...flags);
|
|
125
162
|
const child=spawn(process.execPath,args,{cwd:dirname(info.config),detached:true,stdio:['ignore',log.fd,log.fd]});child.unref();
|
|
126
163
|
let error,exited=false;child.on('error',e=>{error=e;});child.on('exit',()=>{exited=true;});
|
|
127
164
|
for(let n=0;n<150;n++){
|
|
@@ -129,7 +166,7 @@ async function start(info,relay){
|
|
|
129
166
|
if(exited)throw Error('网关启动失败,请查看 '+logPath);
|
|
130
167
|
const state=await gatewayStatus(info.stateDir).catch(e=>{if(['ENOENT','ECONNREFUSED'].includes(e.code))return null;throw e;});
|
|
131
168
|
if(state?.processId&&state.processId!==child.pid)throw Error('另一实例取得了网关状态目录,未启动第二套身份');
|
|
132
|
-
if(state?.backendReady)return state;
|
|
169
|
+
if(state?.backendReady&&state.endpoint===endpoint&&state.connectionMode===connection.connectionMode)return state;
|
|
133
170
|
await delay(100);
|
|
134
171
|
}
|
|
135
172
|
throw Error('启动尚未完成,请检查 yuanma-gateway status 和 '+logPath+';不要重复初始化');
|
|
@@ -146,7 +183,7 @@ try{
|
|
|
146
183
|
if(['help','--help','-h'].includes(action)||args.includes('--help'))console.log(help);
|
|
147
184
|
else if(['--version','version'].includes(action))console.log(JSON.parse(await readFile(join(root,'package.json'),'utf8')).version);
|
|
148
185
|
else{
|
|
149
|
-
const {values}=parseArgs({args,options:{config:{type:'string'},'state-dir':{type:'string'},'relay-url':{type:'string'},'codex-socket':{type:'string'},projects:{type:'string'},'allow-development-keys':{type:'boolean'},'accept-project-access':{type:'boolean'},backend:{type:'string'},'device-id':{type:'string'},capability:{type:'string'},'claim-id':{type:'string'},'grants-json':{type:'string'}}});
|
|
186
|
+
const {values}=parseArgs({args,options:{config:{type:'string'},'state-dir':{type:'string'},'relay-url':{type:'string'},'connection-mode':{type:'string'},'lan-host':{type:'string'},'lan-port':{type:'string'},'codex-socket':{type:'string'},projects:{type:'string'},'allow-development-keys':{type:'boolean'},'accept-project-access':{type:'boolean'},backend:{type:'string'},'device-id':{type:'string'},capability:{type:'string'},'claim-id':{type:'string'},'grants-json':{type:'string'}}});
|
|
150
187
|
if(action==='init')await init(values);
|
|
151
188
|
else if(action==='projects'){
|
|
152
189
|
if(values.backend&&values.backend!=='codex')await run(cli,['projects','--backend',values.backend]);
|
|
@@ -154,15 +191,17 @@ try{
|
|
|
154
191
|
}else if(['start','restart','stop','status','pair','devices','security','device-project-policy','pairing-pending','pairing-approve','pairing-reject','device-revoke'].includes(action)){
|
|
155
192
|
const info=await installation(values);
|
|
156
193
|
if(action==='start'||action==='restart'){
|
|
157
|
-
const
|
|
158
|
-
|
|
194
|
+
const connection=await selectConnection(values,{...info.configData,...info});
|
|
195
|
+
const state=action==='start'?await start(info,connection):await restartGateway(info.stateDir,connection);
|
|
196
|
+
await rememberConnection(info,connection);
|
|
159
197
|
console.log(JSON.stringify(state,null,2));
|
|
160
198
|
}
|
|
161
199
|
else if(action==='stop'){await stopGateway(info.stateDir);console.log('网关已停止,配置和设备数据保留。');}
|
|
162
200
|
else if(action==='status')console.log(JSON.stringify(await gatewayStatus(info.stateDir),null,2));
|
|
163
201
|
else if(action==='pair'){
|
|
164
|
-
const
|
|
165
|
-
await
|
|
202
|
+
const connection=await selectConnection({...values,'connection-mode':values['connection-mode']??(values['relay-url']!==undefined?'relay':'lan')},{...info.configData,...info},process.stdin.isTTY);
|
|
203
|
+
await start(info,connection);await rememberConnection(info,connection);
|
|
204
|
+
await run(join(root,'scripts/pairing-qr.mjs'),['--state-dir',info.stateDir,'--auto-approve','--no-ui',...connectionArgs(connection)]);
|
|
166
205
|
}else{
|
|
167
206
|
const forwarded=[action,'--state-dir',info.stateDir];
|
|
168
207
|
for(const key of ['backend','device-id','capability','claim-id','grants-json'])if(values[key]!==undefined)forwarded.push('--'+key,values[key]);
|
package/dist/agents.js
CHANGED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { dirname, basename } from 'node:path';
|
|
5
|
+
import { Fault, requireValue } from './domain.js';
|
|
6
|
+
/** Official ACP stdio only. No desktop ports, cookies, or private service credentials. */
|
|
7
|
+
export class AntigravityAcp extends EventEmitter {
|
|
8
|
+
executable;
|
|
9
|
+
dataRoot;
|
|
10
|
+
child;
|
|
11
|
+
pending = new Map();
|
|
12
|
+
starting;
|
|
13
|
+
buffer = '';
|
|
14
|
+
stopped = false;
|
|
15
|
+
ready = false;
|
|
16
|
+
constructor(executable, dataRoot) {
|
|
17
|
+
super();
|
|
18
|
+
this.executable = executable;
|
|
19
|
+
this.dataRoot = dataRoot;
|
|
20
|
+
}
|
|
21
|
+
async connect() {
|
|
22
|
+
requireValue(!this.stopped, 'BACKEND_UNAVAILABLE', 'Antigravity 连接已关闭', 503);
|
|
23
|
+
if (this.ready)
|
|
24
|
+
return;
|
|
25
|
+
if (!this.starting)
|
|
26
|
+
this.starting = this.start().finally(() => { this.starting = undefined; });
|
|
27
|
+
return this.starting;
|
|
28
|
+
}
|
|
29
|
+
async start() {
|
|
30
|
+
this.buffer = '';
|
|
31
|
+
const child = spawn(this.executable, ['acp', '--disable-telemetry', '--gemini-dir', dirname(this.dataRoot), '--app-data-dir', basename(this.dataRoot)], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
32
|
+
this.child = child;
|
|
33
|
+
child.stderr.on('data', () => { }); // Diagnostics can contain conversation text; do not forward them to logs.
|
|
34
|
+
child.once('error', () => this.disconnected(child));
|
|
35
|
+
child.once('close', () => this.disconnected(child));
|
|
36
|
+
child.stdin.on('error', () => this.disconnected(child));
|
|
37
|
+
child.stdout.setEncoding('utf8');
|
|
38
|
+
child.stdout.on('data', (chunk) => {
|
|
39
|
+
if (child !== this.child)
|
|
40
|
+
return;
|
|
41
|
+
this.buffer += chunk;
|
|
42
|
+
if (Buffer.byteLength(this.buffer) > 16 * 1024 * 1024) {
|
|
43
|
+
this.disconnected(child);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
let end;
|
|
47
|
+
while ((end = this.buffer.indexOf('\n')) >= 0) {
|
|
48
|
+
const line = this.buffer.slice(0, end);
|
|
49
|
+
this.buffer = this.buffer.slice(end + 1);
|
|
50
|
+
if (!line.trim())
|
|
51
|
+
continue;
|
|
52
|
+
let message;
|
|
53
|
+
try {
|
|
54
|
+
message = JSON.parse(line);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
this.disconnected(child);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (message.jsonrpc !== '2.0') {
|
|
61
|
+
this.disconnected(child);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (message.id !== undefined && !message.method) {
|
|
65
|
+
const entry = this.pending.get(String(message.id));
|
|
66
|
+
if (!entry)
|
|
67
|
+
continue;
|
|
68
|
+
this.pending.delete(String(message.id));
|
|
69
|
+
if (entry.timer)
|
|
70
|
+
clearTimeout(entry.timer);
|
|
71
|
+
if (message.error)
|
|
72
|
+
entry.reject(Object.assign(new Error('Antigravity 请求未完成,需核对原生结果'), { rpcError: message.error }));
|
|
73
|
+
else
|
|
74
|
+
entry.resolve(message.result);
|
|
75
|
+
}
|
|
76
|
+
else if (typeof message.method === 'string') {
|
|
77
|
+
this.emit(message.id === undefined ? 'notification' : 'request', message);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
const init = await this.call('initialize', { protocolVersion: 1, clientCapabilities: { elicitation: { form: {} } }, clientInfo: { name: 'yuanma_gateway', title: '远码', version: '0.1.0' } });
|
|
83
|
+
requireValue(init.protocolVersion === 1 && init.agentInfo?.name === 'Antigravity ACP Server' && init.agentInfo?.version === '1.5.0' && init.agentCapabilities?.loadSession, 'PROTOCOL_INCOMPATIBLE', '当前仅适配 Antigravity ACP 1.5.0,请核对 CLI 版本', 503);
|
|
84
|
+
this.ready = true;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
this.disconnected(child);
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
disconnected(child) {
|
|
92
|
+
if (child !== this.child)
|
|
93
|
+
return;
|
|
94
|
+
this.child = undefined;
|
|
95
|
+
this.ready = false;
|
|
96
|
+
child.kill('SIGTERM');
|
|
97
|
+
for (const entry of this.pending.values()) {
|
|
98
|
+
if (entry.timer)
|
|
99
|
+
clearTimeout(entry.timer);
|
|
100
|
+
entry.reject(new Error('Antigravity 连接断开,执行结果可能未知'));
|
|
101
|
+
}
|
|
102
|
+
this.pending.clear();
|
|
103
|
+
this.emit('disconnected');
|
|
104
|
+
}
|
|
105
|
+
call(method, params, timeout = 30000, id = randomUUID()) {
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
const entry = { resolve, reject };
|
|
108
|
+
if (timeout)
|
|
109
|
+
entry.timer = setTimeout(() => { this.pending.delete(id); reject(new Error('Antigravity 请求超时,结果可能未知')); }, timeout);
|
|
110
|
+
this.pending.set(id, entry);
|
|
111
|
+
try {
|
|
112
|
+
this.write({ jsonrpc: '2.0', id, method, params });
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
this.pending.delete(id);
|
|
116
|
+
if (entry.timer)
|
|
117
|
+
clearTimeout(entry.timer);
|
|
118
|
+
reject(error);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
notify(method, params) { this.write({ jsonrpc: '2.0', method, params }); }
|
|
123
|
+
reply(id, result) { this.write({ jsonrpc: '2.0', id, result }); }
|
|
124
|
+
reject(id, message = '当前客户端不支持此请求') { this.write({ jsonrpc: '2.0', id, error: { code: -32601, message } }); }
|
|
125
|
+
write(message) {
|
|
126
|
+
if (!this.child?.stdin.writable)
|
|
127
|
+
throw new Fault('BACKEND_UNAVAILABLE', '请安装并登录 Antigravity CLI 1.5.0', 503);
|
|
128
|
+
this.child.stdin.write(JSON.stringify(message) + '\n');
|
|
129
|
+
}
|
|
130
|
+
close() { this.stopped = true; if (this.child)
|
|
131
|
+
this.disconnected(this.child); this.removeAllListeners(); }
|
|
132
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { requireValue } from './domain.js';
|
|
3
|
+
export const fingerprint = (value) => createHash('sha256').update(JSON.stringify(value)).digest('hex').slice(0, 16);
|
|
4
|
+
const valueText = (value) => typeof value === 'string' ? value : value === undefined ? '' : JSON.stringify(value, null, 2);
|
|
5
|
+
export function userText(step) {
|
|
6
|
+
const text = valueText(step.content);
|
|
7
|
+
// Strip only the observed whole envelope, never arbitrary user-authored tags.
|
|
8
|
+
const match = /^<USER_REQUEST>\s*([\s\S]*?)\s*<\/USER_REQUEST>(?:\s*<(ADDITIONAL_METADATA|USER_SETTINGS_CHANGE)>[\s\S]*?<\/\2>)*\s*$/.exec(text);
|
|
9
|
+
return match ? match[1] : text;
|
|
10
|
+
}
|
|
11
|
+
export function stepParts(step) {
|
|
12
|
+
const parts = [];
|
|
13
|
+
if (step.source === 'USER_EXPLICIT' && step.type === 'USER_INPUT') {
|
|
14
|
+
const text = userText(step);
|
|
15
|
+
if (text.trim() && !/^<USER_SETTINGS_CHANGE>[\s\S]*<\/USER_SETTINGS_CHANGE>$/.test(text.trim()))
|
|
16
|
+
parts.push({ part: 'content', role: 'user', text });
|
|
17
|
+
}
|
|
18
|
+
else if (step.source === 'MODEL' && step.type === 'PLANNER_RESPONSE') {
|
|
19
|
+
if (step.thinking)
|
|
20
|
+
parts.push({ part: 'thinking', role: 'tool', text: valueText(step.thinking), label: '思考记录' });
|
|
21
|
+
if (Array.isArray(step.tool_calls) && step.tool_calls.length)
|
|
22
|
+
parts.push({ part: 'tools', role: 'tool', text: valueText(step.tool_calls), label: '工具调用记录' });
|
|
23
|
+
if (step.content)
|
|
24
|
+
parts.push({ part: 'content', role: 'assistant', text: valueText(step.content) });
|
|
25
|
+
}
|
|
26
|
+
else if (step.source === 'MODEL' && step.type === 'GENERIC' && step.content) {
|
|
27
|
+
parts.push({ part: 'content', role: 'tool', text: valueText(step.content), label: '执行记录' });
|
|
28
|
+
}
|
|
29
|
+
return parts;
|
|
30
|
+
}
|
|
31
|
+
export function recordSteps(lines) {
|
|
32
|
+
const records = [];
|
|
33
|
+
let turnId = 'agy-context';
|
|
34
|
+
const latest = new Map();
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
if (!line.trim())
|
|
37
|
+
continue;
|
|
38
|
+
let step;
|
|
39
|
+
try {
|
|
40
|
+
step = JSON.parse(line);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new Error('Antigravity 历史格式损坏,请刷新后重试');
|
|
44
|
+
}
|
|
45
|
+
requireValue(Number.isSafeInteger(step.step_index) && step.step_index >= 0 && typeof step.type === 'string' && typeof step.source === 'string', 'PROTOCOL_INCOMPATIBLE', 'Antigravity 历史步骤格式已变化', 503);
|
|
46
|
+
latest.set(step.step_index, step);
|
|
47
|
+
}
|
|
48
|
+
// Native rewrites may append another version of an existing step.
|
|
49
|
+
for (const step of [...latest.values()].sort((a, b) => a.step_index - b.step_index)) {
|
|
50
|
+
if (step.source === 'USER_EXPLICIT' && step.type === 'USER_INPUT' && !/^<USER_SETTINGS_CHANGE>/.test(userText(step).trim()))
|
|
51
|
+
turnId = 'agy-turn-' + step.step_index;
|
|
52
|
+
records.push({ step, turnId });
|
|
53
|
+
}
|
|
54
|
+
return records;
|
|
55
|
+
}
|
|
56
|
+
export function partId(step, part) { return 'agy:' + step.step_index + ':' + fingerprint(step) + ':' + part; }
|
|
57
|
+
export function conversationPage(records, nextCursor, fullReplies = new Map()) {
|
|
58
|
+
const turns = new Map();
|
|
59
|
+
for (const { step, turnId } of records)
|
|
60
|
+
for (const part of stepParts(step)) {
|
|
61
|
+
const fields = step.truncated_fields ?? [];
|
|
62
|
+
const id = partId(step, part.part), reply = part.role === 'assistant';
|
|
63
|
+
const text = fullReplies.get(id) ?? part.text;
|
|
64
|
+
const truncated = (!reply && text.length > 1000) || (fields.includes(part.part === 'tools' ? 'tool_calls' : part.part) && !fullReplies.has(id));
|
|
65
|
+
const item = { id, type: part.role === 'user' ? 'userMessage' : reply ? 'agentMessage' : 'commandExecution', role: part.role, text: reply ? text : text.slice(0, 1000), totalChars: text.length, truncated };
|
|
66
|
+
if (part.role === 'tool')
|
|
67
|
+
item.execution = { kind: part.part === 'thinking' ? 'reasoning' : 'tool', label: part.label, status: step.status === 'ERROR' ? 'failed' : 'unknown' };
|
|
68
|
+
if (!turns.has(turnId))
|
|
69
|
+
turns.set(turnId, { id: turnId, status: 'recorded', items: [] });
|
|
70
|
+
turns.get(turnId).items.push(item);
|
|
71
|
+
}
|
|
72
|
+
return { data: [...turns.values()].reverse(), nextCursor };
|
|
73
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { request } from 'node:https';
|
|
4
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
5
|
+
import { Fault, requireValue } from './domain.js';
|
|
6
|
+
const exec = promisify(execFile);
|
|
7
|
+
const app = '/Applications/Antigravity.app/Contents/Resources/app';
|
|
8
|
+
const binary = '/Applications/Antigravity.app/Contents/Resources/bin/language_server';
|
|
9
|
+
/** Same-user desktop process only. Local CSRF credentials never leave this client. */
|
|
10
|
+
export class AntigravityNative {
|
|
11
|
+
#authority;
|
|
12
|
+
#checked = 0;
|
|
13
|
+
ready = false;
|
|
14
|
+
get generation() { return this.#authority ? this.#authority.pid + ':' + this.#authority.port : ''; }
|
|
15
|
+
async connect() {
|
|
16
|
+
if (this.ready && Date.now() - this.#checked < 1000)
|
|
17
|
+
return;
|
|
18
|
+
requireValue(process.platform === 'darwin', 'BACKEND_UNAVAILABLE', 'Antigravity 桌面接入需要 macOS', 503);
|
|
19
|
+
const version = JSON.parse(await readFile(app + '/package.json', 'utf8')).version;
|
|
20
|
+
requireValue(version === '2.29.0', 'PROTOCOL_INCOMPATIBLE', '当前仅适配 Antigravity App 2.29.0,请核对桌面版本', 503);
|
|
21
|
+
const { stdout } = await exec('/bin/ps', ['-axo', 'uid,pid,ppid,command'], { maxBuffer: 4 * 1024 * 1024 });
|
|
22
|
+
const processes = stdout.split('\n').flatMap(line => {
|
|
23
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/.exec(line);
|
|
24
|
+
if (!match || Number(match[1]) !== process.getuid?.() || !match[4].startsWith(binary + ' '))
|
|
25
|
+
return [];
|
|
26
|
+
const flag = (name) => new RegExp('--' + name + '(?:=|\\s+)([^\\s]+)').exec(match[4])?.[1];
|
|
27
|
+
if (flag('app_data_dir') !== 'antigravity' || flag('subclient_type') !== 'hub' || flag('override_ide_name') !== 'antigravity')
|
|
28
|
+
return [];
|
|
29
|
+
const token = flag('csrf_token');
|
|
30
|
+
return token ? [{ pid: Number(match[2]), token }] : [];
|
|
31
|
+
});
|
|
32
|
+
requireValue(processes.length === 1, 'BACKEND_UNAVAILABLE', '请打开唯一的 Antigravity App 桌面实例', 503);
|
|
33
|
+
const desktop = processes[0];
|
|
34
|
+
const { stdout: listeners } = await exec('/usr/sbin/lsof', ['-nP', '-a', '-p', String(desktop.pid), '-iTCP', '-sTCP:LISTEN', '-Fn'], { maxBuffer: 65536 });
|
|
35
|
+
const ports = [...new Set([...listeners.matchAll(/^n(?:127\.0\.0\.1|\*|\[::1\]):(\d+)$/gm)].map(m => Number(m[1])))];
|
|
36
|
+
requireValue(ports.length > 0 && ports.length <= 4, 'BACKEND_UNAVAILABLE', '无法核实 Antigravity 桌面监听地址', 503);
|
|
37
|
+
const executable = await stat(binary);
|
|
38
|
+
requireValue(executable.isFile() && !(executable.mode & 0o002), 'UNSAFE_STORAGE', 'Antigravity 可执行文件权限异常', 403);
|
|
39
|
+
let found;
|
|
40
|
+
for (const port of ports) {
|
|
41
|
+
const authority = { ...desktop, port };
|
|
42
|
+
try {
|
|
43
|
+
const value = await this.rpc(authority, 'GetAllCascadeTrajectories', { excludeSubtrajectories: true }, 5000);
|
|
44
|
+
if (value.trajectorySummaries && typeof value.trajectorySummaries === 'object') {
|
|
45
|
+
found = authority;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch { /* Only ports owned by the verified desktop process are considered. */ }
|
|
50
|
+
}
|
|
51
|
+
requireValue(found, 'BACKEND_UNAVAILABLE', 'Antigravity 桌面会话接口未就绪', 503);
|
|
52
|
+
this.#authority = found;
|
|
53
|
+
this.#checked = Date.now();
|
|
54
|
+
this.ready = true;
|
|
55
|
+
}
|
|
56
|
+
async call(method, params, timeout = 15000) {
|
|
57
|
+
await this.connect();
|
|
58
|
+
const authority = this.#authority;
|
|
59
|
+
try {
|
|
60
|
+
return await this.rpc(authority, method, params, timeout);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
this.ready = false;
|
|
64
|
+
this.#checked = 0;
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async state() {
|
|
69
|
+
await this.connect();
|
|
70
|
+
const authority = this.#authority;
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const payload = Buffer.from('{}'), frame = Buffer.alloc(5 + payload.length);
|
|
73
|
+
frame.writeUInt32BE(payload.length, 1);
|
|
74
|
+
payload.copy(frame, 5);
|
|
75
|
+
const req = request({ hostname: '127.0.0.1', port: authority.port, path: '/exa.language_server_pb.LanguageServerService/JetboxSubscribeToState', method: 'POST', rejectUnauthorized: false, headers: { 'content-type': 'application/connect+json', 'connect-protocol-version': '1', 'x-codeium-csrf-token': authority.token } }, res => {
|
|
76
|
+
let buffer = Buffer.alloc(0);
|
|
77
|
+
res.on('data', (chunk) => {
|
|
78
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
79
|
+
if (buffer.length > 8 * 1024 * 1024) {
|
|
80
|
+
req.destroy();
|
|
81
|
+
reject(new Error('Antigravity 设置响应过大'));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (buffer.length < 5)
|
|
85
|
+
return;
|
|
86
|
+
const length = buffer.readUInt32BE(1);
|
|
87
|
+
if (buffer.length < 5 + length)
|
|
88
|
+
return;
|
|
89
|
+
try {
|
|
90
|
+
const value = JSON.parse(buffer.subarray(5, 5 + length).toString());
|
|
91
|
+
if (buffer[0] !== 0)
|
|
92
|
+
throw new Error('设置流提前结束');
|
|
93
|
+
resolve(value);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
reject(new Error('Antigravity 设置格式不兼容'));
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
req.destroy();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
res.on('end', () => reject(new Error('Antigravity 设置流已关闭')));
|
|
103
|
+
res.on('error', () => reject(new Error('Antigravity 设置连接已中断')));
|
|
104
|
+
res.on('aborted', () => reject(new Error('Antigravity 设置连接已中断')));
|
|
105
|
+
});
|
|
106
|
+
req.on('error', () => reject(new Error('无法读取 Antigravity 桌面设置')));
|
|
107
|
+
req.setTimeout(10000, () => req.destroy(new Error('设置请求超时')));
|
|
108
|
+
req.end(frame);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
rpc(authority, method, params, timeout) {
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
const payload = Buffer.from(JSON.stringify(params));
|
|
114
|
+
if (payload.length > 32 * 1024 * 1024) {
|
|
115
|
+
reject(new Fault('PAYLOAD_TOO_LARGE', 'Antigravity 输入超过接口容量限制', 413));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Match the desktop's trust policy only for the loopback port owned by its verified PID.
|
|
119
|
+
// Never disable TLS validation globally or forward this authorization to a remote host.
|
|
120
|
+
const req = request({ hostname: '127.0.0.1', port: authority.port, path: '/exa.language_server_pb.LanguageServerService/' + method, method: 'POST', rejectUnauthorized: false, headers: { 'content-type': 'application/json', 'connect-protocol-version': '1', 'content-length': payload.length, 'x-codeium-csrf-token': authority.token } }, res => {
|
|
121
|
+
let size = 0;
|
|
122
|
+
const chunks = [];
|
|
123
|
+
res.on('data', (chunk) => { size += chunk.length; if (size > 32 * 1024 * 1024) {
|
|
124
|
+
req.destroy(new Error('Antigravity 返回内容超过容量限制'));
|
|
125
|
+
return;
|
|
126
|
+
} chunks.push(chunk); });
|
|
127
|
+
res.on('error', () => reject(new Error('Antigravity 本地响应中断,结果可能未知')));
|
|
128
|
+
res.on('end', () => {
|
|
129
|
+
let value;
|
|
130
|
+
try {
|
|
131
|
+
value = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
reject(new Error('Antigravity 返回了不兼容的响应'));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (res.statusCode !== 200) {
|
|
138
|
+
reject(Object.assign(new Error('Antigravity 本地请求未完成,需核对原生状态'), { rpcError: { code: value.code, message: value.message } }));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
resolve(value);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
req.on('error', () => reject(new Error('Antigravity 桌面连接中断,结果可能未知')));
|
|
145
|
+
req.setTimeout(timeout, () => req.destroy(new Error('Antigravity 本地请求超时')));
|
|
146
|
+
req.end(payload);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
close() { this.#authority = undefined; this.#checked = 0; this.ready = false; }
|
|
150
|
+
}
|