@fxz26284407/yuanma-gateway 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -16
- package/bin/yuanma-gateway.mjs +40 -38
- package/dist/antigravity-native.js +21 -36
- package/dist/antigravity.js +56 -40
- package/dist/browser-extensions.js +6 -19
- package/dist/changes.js +2 -2
- package/dist/cli.js +24 -23
- package/dist/codex.js +82 -32
- package/dist/deepseek-native.js +6 -0
- package/dist/deepseek-suggestion-runner.mjs +42 -19
- package/dist/deepseek-suggestion.js +2 -19
- package/dist/deepseek.js +119 -48
- package/dist/extension-icons.js +3 -3
- package/dist/identity.js +4 -3
- package/dist/ledger.js +10 -2
- package/dist/media.js +3 -2
- package/dist/mobile-history.js +1 -1
- package/dist/noise.js +1 -1
- package/dist/platform/adapter.js +64 -0
- package/dist/platform/deepseek-sdk.js +28 -0
- package/dist/{desktop-sync.js → platform/macos-desktop-sync.js} +1 -1
- package/dist/platform/macos.js +207 -0
- package/dist/platform/windows-native.mjs +45 -0
- package/dist/platform/windows.js +289 -0
- package/dist/platform.mjs +8 -0
- package/dist/project-access.js +1 -1
- package/dist/rpc.mjs +5 -2
- package/dist/secure-ws.js +2 -2
- package/dist/server.js +74 -50
- package/dist/thread-titles.js +3 -2
- package/dist/uploads.js +15 -38
- package/package.json +7 -4
- package/scripts/approval-ui.html +1 -1
- package/scripts/approval-ui.mjs +3 -2
- package/scripts/pairing-qr.mjs +20 -15
- package/scripts/restart-local-gateway.mjs +13 -17
- package/scripts/terminal-progress.mjs +12 -0
- package/scripts/windows-codex-bridge.cs +142 -0
- package/scripts/windows-codex.ps1 +79 -0
package/README.md
CHANGED
|
@@ -1,31 +1,39 @@
|
|
|
1
1
|
# 远码网关(yuanma-gateway)
|
|
2
2
|
|
|
3
|
-
远码的
|
|
3
|
+
远码的 macOS / Windows 本地网关与 HTTP/WS 公网密文中继。只包含两端网关,不包含鸿蒙工程、签名文件、个人文档库或运行数据。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 平台支持
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
从 0.1.8 起,macOS 与 Windows 11 x64 使用同一个 npm 包和版本,通过系统适配器连接 Codex、DeepSeek Harness 与 Antigravity。要求 Node.js 22,安装和升级命令相同。Windows 项目目录兼容反斜杠与正斜杠。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Windows 本地管理接口使用仅监听回环地址的鉴权入口,文件权限按当前用户 SID 设置;macOS 继续使用 Unix socket。二维码已缩小,配对准备阶段显示等待提示。
|
|
10
|
+
|
|
11
|
+
## 局域网直连
|
|
12
|
+
|
|
13
|
+
已发布的网关支持手机与电脑在同一局域网直连,无需部署 Relay。当前 App 最低要求网关 0.1.7,请同时更新手机与网关。
|
|
14
|
+
|
|
15
|
+
在电脑安装 Node.js 22,并准备好与桌面共用的 Codex app-server 入口后执行:
|
|
10
16
|
|
|
11
17
|
```sh
|
|
12
|
-
npm
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
npm install -g @fxz26284407/yuanma-gateway@latest --registry=https://registry.npmmirror.com
|
|
19
|
+
yuanma-gateway init
|
|
20
|
+
yuanma-gateway pair
|
|
15
21
|
```
|
|
16
22
|
|
|
17
|
-
|
|
23
|
+
安装命令仅本次使用 npmmirror 镜像,不修改全局 npm 配置。镜像同步新版本可能有延迟;安装不到所需版本时,将命令中的镜像地址改为 https://registry.npmjs.org。
|
|
24
|
+
|
|
25
|
+
首次初始化的中继地址可以留空。单个常规内网 IPv4 自动选择,多个地址展示网卡与 IP 供选择;虚拟网卡需确认手机可达。默认端口为 `19089`,二维码地址为 `ws://所选内网IP:19089/v1/secure`。仅该加密入口对内网开放,本地管理接口使用上述平台各自的受保护入口。仍需准备与桌面共享的 Codex 后端及现有密钥保存模式。
|
|
18
26
|
|
|
19
27
|
普通 start/restart 沿用已保存模式;pair 默认局域网,多个内网 IP 时交互选择,直接执行:
|
|
20
28
|
|
|
21
29
|
```sh
|
|
22
|
-
|
|
30
|
+
yuanma-gateway pair
|
|
23
31
|
```
|
|
24
32
|
|
|
25
33
|
非交互或需要指定网卡、端口时:
|
|
26
34
|
|
|
27
35
|
```sh
|
|
28
|
-
|
|
36
|
+
yuanma-gateway pair --lan-host 192.168.1.10 --lan-port 19089
|
|
29
37
|
```
|
|
30
38
|
|
|
31
39
|
将示例 IP 替换为本机实际内网地址。IP 失效时重新选择并扫码,端口冲突时更换端口;不要将手机的 localhost 当作 Mac。网关保存连接方式,后续启动复用;设备身份与授权保持原样。
|
|
@@ -33,10 +41,10 @@ node bin/yuanma-gateway.mjs pair --lan-host 192.168.1.10 --lan-port 19089
|
|
|
33
41
|
保留 Relay 扩展,切换时使用:
|
|
34
42
|
|
|
35
43
|
```sh
|
|
36
|
-
|
|
44
|
+
yuanma-gateway pair --relay-url wss://relay.example.com
|
|
37
45
|
```
|
|
38
46
|
|
|
39
|
-
局域网和 Relay
|
|
47
|
+
局域网和 Relay 两种连接方式互斥,不会自动回退公网。跨网络使用时可按下文部署中继。
|
|
40
48
|
|
|
41
49
|
## 项目系列
|
|
42
50
|
|
|
@@ -91,7 +99,7 @@ node bin/yuanma-gateway.mjs pair --relay-url wss://relay.example.com
|
|
|
91
99
|
```sh
|
|
92
100
|
nvm install 22
|
|
93
101
|
nvm use 22
|
|
94
|
-
npm install -g @fxz26284407/yuanma-gateway@
|
|
102
|
+
npm install -g @fxz26284407/yuanma-gateway@latest --registry=https://registry.npmmirror.com
|
|
95
103
|
yuanma-gateway --version
|
|
96
104
|
```
|
|
97
105
|
|
|
@@ -110,10 +118,10 @@ yuanma-gateway projects
|
|
|
110
118
|
### 初始化
|
|
111
119
|
|
|
112
120
|
```sh
|
|
113
|
-
yuanma-gateway init
|
|
121
|
+
yuanma-gateway init
|
|
114
122
|
```
|
|
115
123
|
|
|
116
|
-
|
|
124
|
+
同一局域网使用时,中继地址留空;跨网络使用时,填写自行部署的实际 `wss://` 中继地址,也可使用 `yuanma-gateway init --relay-url wss://relay.example.com`。初始化读取真实 Codex 项目列表,由你选择初始项目。**现有产品会继续同步发现其他项目;配对后所有发现的项目和工作区默认可执行,初始列表不是权限白名单。** 初始化会明确说明并要求接受这项既有策略;会话中的执行审批仍独立管理。
|
|
117
125
|
|
|
118
126
|
当前版本的远程连接还需显式启用本机私有文件保存传输密钥的模式;交互界面会说明并征求选择。
|
|
119
127
|
|
|
@@ -193,7 +201,7 @@ yuanma-gateway restart
|
|
|
193
201
|
### 升级与停止
|
|
194
202
|
|
|
195
203
|
```sh
|
|
196
|
-
npm install -g @fxz26284407/yuanma-gateway@latest --registry=https://registry.
|
|
204
|
+
npm install -g @fxz26284407/yuanma-gateway@latest --registry=https://registry.npmmirror.com
|
|
197
205
|
yuanma-gateway restart
|
|
198
206
|
```
|
|
199
207
|
|
package/bin/yuanma-gateway.mjs
CHANGED
|
@@ -1,53 +1,45 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import {lstat, readFile, open, rename, unlink} from 'node:fs/promises';
|
|
3
3
|
import {randomUUID} from 'node:crypto';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {dirname, join, resolve} from 'node:path';
|
|
4
|
+
import {dirname, join, resolve,isAbsolute} from 'node:path';
|
|
5
|
+
import {platform} from '../dist/platform.mjs';
|
|
7
6
|
import {fileURLToPath} from 'node:url';
|
|
8
7
|
import {parseArgs} from 'node:util';
|
|
9
8
|
import {createInterface} from 'node:readline/promises';
|
|
10
9
|
import {setTimeout as delay} from 'node:timers/promises';
|
|
11
|
-
import {
|
|
10
|
+
import {CodexBackend} from '../dist/codex.js';
|
|
11
|
+
import {withProgress} from '../scripts/terminal-progress.mjs';
|
|
12
12
|
import {lanAddresses, lanConfig, DEFAULT_LAN_PORT} from '../dist/lan.js';
|
|
13
13
|
import {relayEndpoint} from '../dist/relay-client.js';
|
|
14
14
|
import {connectionArgs, gatewayLaunch, restartGateway, gatewayStatus, stopGateway, openGatewayLog} from '../scripts/restart-local-gateway.mjs';
|
|
15
15
|
|
|
16
16
|
process.umask(0o077);
|
|
17
17
|
const root=fileURLToPath(new URL('../',import.meta.url));
|
|
18
|
-
const home=
|
|
18
|
+
const home=platform.gatewayHome;
|
|
19
19
|
const settingsPath=join(home,'installation.json');
|
|
20
20
|
const cli=join(root,'dist/cli.js');
|
|
21
|
-
const help=`远码网关(需要 Node.js 22
|
|
22
|
-
yuanma-gateway init [--relay-url wss://域名] [--projects 项目ID,项目ID] [--allow-development-keys] [--accept-project-access]
|
|
21
|
+
const help=`远码网关(需要 Node.js 22 和已登录的桌面智能体)
|
|
22
|
+
yuanma-gateway init [--backend codex|deepseek|antigravity] [--codex-endpoint 本机共享入口] [--relay-url wss://域名] [--projects 项目ID,项目ID] [--allow-development-keys] [--accept-project-access]
|
|
23
23
|
yuanma-gateway init --config 原配置绝对路径 --state-dir 原状态目录绝对路径
|
|
24
24
|
yuanma-gateway start [--relay-url wss://域名]
|
|
25
25
|
yuanma-gateway restart [--relay-url wss://域名]
|
|
26
|
-
yuanma-gateway pair
|
|
26
|
+
yuanma-gateway pair(默认小二维码,旧版手机可加 --legacy-qr)
|
|
27
27
|
yuanma-gateway pair --relay-url ws[s]://中继地址
|
|
28
28
|
局域网选项:--connection-mode lan [--lan-host 内网IP] [--lan-port 19089]
|
|
29
29
|
中继选项:--relay-url ws[s]://中继地址(start/restart 沿用原模式,pair 默认局域网)
|
|
30
30
|
yuanma-gateway status | stop | projects | devices | security
|
|
31
31
|
yuanma-gateway device-project-policy --device-id 设备ID --capability control
|
|
32
|
-
默认配置与状态位于
|
|
32
|
+
默认配置与状态位于 ${home},npm 升级不会覆盖。
|
|
33
33
|
当前版本配对后所有发现的项目和工作区默认可执行;会话执行审批独立管理。`;
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const s=await lstat(path);
|
|
38
|
-
if(!s.isDirectory()||s.isSymbolicLink()||s.uid!==process.getuid()||(s.mode&0o077))throw Error('目录必须由当前用户拥有且权限为 700:'+path);
|
|
39
|
-
}
|
|
40
|
-
async function privateJson(path){
|
|
41
|
-
const s=await lstat(path);
|
|
42
|
-
if(!s.isFile()||s.isSymbolicLink()||s.nlink!==1||s.uid!==process.getuid()||(s.mode&0o077))throw Error('配置必须是当前用户的私有普通文件(权限 600):'+path);
|
|
43
|
-
return JSON.parse(await readFile(path,'utf8'));
|
|
44
|
-
}
|
|
35
|
+
const privateDirectory=path=>platform.files.privateDirectory(path);
|
|
36
|
+
const privateJson=path=>platform.files.readPrivateJson(path);
|
|
45
37
|
async function createJson(path,value){
|
|
46
38
|
const f=await open(path,'wx',0o600);
|
|
47
|
-
try{await f.writeFile(JSON.stringify(value,null,2)+'\n');await f.sync();}finally{await f.close();}
|
|
39
|
+
try{platform.files.protectCreatedFile(path);await f.writeFile(JSON.stringify(value,null,2)+'\n');await f.sync();}finally{await f.close();}
|
|
48
40
|
}
|
|
49
41
|
function validateConfig(config){
|
|
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
|
|
42
|
+
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'||!isAbsolute(r))))throw Error('配置需要明确的项目 ID 和绝对根目录');
|
|
51
43
|
if(config.connectionMode!==undefined&&!['lan','relay'].includes(config.connectionMode))throw Error('配置中的连接模式无效');
|
|
52
44
|
if(config.relayUrl!=null)relayEndpoint(config.relayUrl);
|
|
53
45
|
}
|
|
@@ -55,7 +47,7 @@ async function installation(values){
|
|
|
55
47
|
const selected=values.config||values['state-dir'];
|
|
56
48
|
if(selected&&(!values.config||!values['state-dir']))throw Error('--config 和 --state-dir 必须同时指定,避免误用另一套身份');
|
|
57
49
|
const info=selected?{config:resolve(values.config),stateDir:resolve(values['state-dir'])}:await privateJson(settingsPath).catch(e=>{if(e.code==='ENOENT')throw Error('尚未初始化,请先运行 yuanma-gateway init');throw e;});
|
|
58
|
-
if(typeof info.config!=='string'||!info.config
|
|
50
|
+
if(typeof info.config!=='string'||!isAbsolute(info.config)||typeof info.stateDir!=='string'||!isAbsolute(info.stateDir))throw Error('安装记录中的路径无效');
|
|
59
51
|
const config=await privateJson(info.config);validateConfig(config);
|
|
60
52
|
await lstat(info.stateDir);await privateDirectory(info.stateDir);
|
|
61
53
|
if(info.relayUrl!=null)relayEndpoint(info.relayUrl);
|
|
@@ -99,8 +91,8 @@ async function selectConnection(values,saved={},choose=false){
|
|
|
99
91
|
return {connectionMode:'lan',lanHost:lan.host,lanPort:lan.port,relayUrl:null};
|
|
100
92
|
}
|
|
101
93
|
async function catalog(socket){
|
|
102
|
-
const
|
|
103
|
-
try{
|
|
94
|
+
const backend=new CodexBackend(socket,[]);
|
|
95
|
+
try{return await backend.projectCatalog();}finally{await backend.close();}
|
|
104
96
|
}
|
|
105
97
|
async function init(values){
|
|
106
98
|
await privateDirectory(home);
|
|
@@ -127,9 +119,17 @@ async function init(values){
|
|
|
127
119
|
const entered=values['relay-url']??(values['connection-mode']||values['lan-host']||!process.stdin.isTTY?'':await ask('中继地址(可留空使用局域网直连):'));
|
|
128
120
|
prompt?.close();prompt=undefined;
|
|
129
121
|
const connection=await selectConnection({...values,...(entered.trim()?{'relay-url':entered.trim()}:{})});
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
|
|
122
|
+
const supplied=values['codex-endpoint']??values['codex-socket']??platform.codex.defaultEndpoint;
|
|
123
|
+
const socket=!supplied||/^wss?:\/\//.test(supplied)?supplied:resolve(supplied);
|
|
124
|
+
const backend=values.backend??'codex';
|
|
125
|
+
let projects;
|
|
126
|
+
if(backend==='codex')projects=await catalog(socket);
|
|
127
|
+
else if(['deepseek','antigravity'].includes(backend)){
|
|
128
|
+
const {DeepSeekBackend}=await import('../dist/deepseek.js'),{AntigravityBackend}=await import('../dist/antigravity.js');
|
|
129
|
+
const native=backend==='deepseek'?new DeepSeekBackend([]):new AntigravityBackend([]);
|
|
130
|
+
try{projects=await native.projectCatalog();}finally{native.close();}
|
|
131
|
+
}else throw Error('未知智能体:'+backend);
|
|
132
|
+
if(!Array.isArray(projects)||!projects.length)throw Error('所选桌面智能体没有可选择的项目');
|
|
133
133
|
let selected;
|
|
134
134
|
if(values.projects){const ids=values.projects.split(',').map(x=>x.trim()).filter(Boolean);selected=projects.filter(p=>ids.includes(p.id));if(!ids.length||new Set(ids).size!==ids.length||selected.length!==ids.length)throw Error('--projects 包含未知或重复的项目 ID');}
|
|
135
135
|
else{
|
|
@@ -138,11 +138,11 @@ async function init(values){
|
|
|
138
138
|
if(!indices.length||new Set(indices).size!==indices.length||indices.some(i=>!Number.isInteger(i)||i<1||i>projects.length))throw Error('项目序号无效');
|
|
139
139
|
selected=indices.map(i=>projects[i-1]);
|
|
140
140
|
}
|
|
141
|
-
const allow=values['allow-development-keys']||(await ask('
|
|
141
|
+
const allow=values['allow-development-keys']||(await ask('当前网关使用仅当前用户可访问的本机文件保存传输密钥,启用此模式?[y/N]:')).trim().toLowerCase()==='y';
|
|
142
142
|
if(!allow)throw Error('未启用当前版本所需的密钥模式,未创建配置或网关身份');
|
|
143
143
|
const accepted=values['accept-project-access']||(await ask('配对后所有发现的项目和工作区默认可执行,初始项目列表不是权限白名单。接受此策略?[y/N]:')).trim().toLowerCase()==='y';
|
|
144
144
|
if(!accepted)throw Error('未接受现有项目访问策略,未创建配置或网关身份');
|
|
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);
|
|
145
|
+
const config={...(socket?{codexSocket:socket}:{}),backend,...connection,allowDevelopmentKeys:true,projects:selected.map(p=>({id:p.id,...(p.name?{name:p.name}:{}),roots:p.roots}))};validateConfig(config);
|
|
146
146
|
await privateDirectory(stateDir);
|
|
147
147
|
await createJson(configPath,config);
|
|
148
148
|
await createJson(settingsPath,{config:configPath,stateDir,...connection});
|
|
@@ -158,10 +158,11 @@ async function start(info,connection){
|
|
|
158
158
|
const logPath=join(info.stateDir,'gateway.log');
|
|
159
159
|
const log=await openGatewayLog(logPath);
|
|
160
160
|
try{
|
|
161
|
-
const args=[cli,'start','--config',info.config,'--state-dir',info.stateDir];args.push(...flags);
|
|
162
|
-
const child=spawn(process.execPath,args,{cwd:dirname(info.config),detached:true,stdio:['ignore',log.fd,log.fd]});child.unref();
|
|
161
|
+
const args=[cli,'start','--config',info.config,'--state-dir',info.stateDir];if(info.configData.backend)args.push('--backend',info.configData.backend);args.push(...flags);
|
|
162
|
+
const child=platform.spawn(process.execPath,args,{cwd:dirname(info.config),detached:true,stdio:['ignore',log.fd,log.fd]});child.unref();
|
|
163
163
|
let error,exited=false;child.on('error',e=>{error=e;});child.on('exit',()=>{exited=true;});
|
|
164
|
-
|
|
164
|
+
const deadline=Date.now()+platform.startupTimeoutMs;
|
|
165
|
+
while(Date.now()<deadline){
|
|
165
166
|
if(error)throw error;
|
|
166
167
|
if(exited)throw Error('网关启动失败,请查看 '+logPath);
|
|
167
168
|
const state=await gatewayStatus(info.stateDir).catch(e=>{if(['ENOENT','ECONNREFUSED'].includes(e.code))return null;throw e;});
|
|
@@ -173,21 +174,22 @@ async function start(info,connection){
|
|
|
173
174
|
}finally{await log.close();}
|
|
174
175
|
}
|
|
175
176
|
async function run(path,args){
|
|
176
|
-
const child=spawn(process.execPath,[path,...args],{stdio:'inherit'});
|
|
177
|
+
const child=platform.spawn(process.execPath,[path,...args],{stdio:'inherit'});
|
|
177
178
|
await new Promise((ok,fail)=>{child.on('error',fail);child.on('exit',(code,signal)=>{if(code===0)ok();else fail(Error('命令退出:'+(signal??code)));});});
|
|
178
179
|
}
|
|
179
180
|
|
|
180
181
|
try{
|
|
182
|
+
platform.supportedHost();
|
|
181
183
|
if(!process.versions.node.startsWith('22.'))throw Error('请使用 Node.js 22(nvm install 22 && nvm use 22)');
|
|
182
184
|
const [action='help',...args]=process.argv.slice(2);
|
|
183
185
|
if(['help','--help','-h'].includes(action)||args.includes('--help'))console.log(help);
|
|
184
186
|
else if(['--version','version'].includes(action))console.log(JSON.parse(await readFile(join(root,'package.json'),'utf8')).version);
|
|
185
187
|
else{
|
|
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'}}});
|
|
188
|
+
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'},'codex-endpoint':{type:'string'},projects:{type:'string'},'allow-development-keys':{type:'boolean'},'accept-project-access':{type:'boolean'},'legacy-qr':{type:'boolean'},backend:{type:'string'},'device-id':{type:'string'},capability:{type:'string'},'claim-id':{type:'string'},'grants-json':{type:'string'}}});
|
|
187
189
|
if(action==='init')await init(values);
|
|
188
190
|
else if(action==='projects'){
|
|
189
191
|
if(values.backend&&values.backend!=='codex')await run(cli,['projects','--backend',values.backend]);
|
|
190
|
-
else{const socket=values['codex-
|
|
192
|
+
else{const socket=values['codex-endpoint']??values['codex-socket']??platform.codex.defaultEndpoint;console.log(JSON.stringify(await catalog(socket),null,2));}
|
|
191
193
|
}else if(['start','restart','stop','status','pair','devices','security','device-project-policy','pairing-pending','pairing-approve','pairing-reject','device-revoke'].includes(action)){
|
|
192
194
|
const info=await installation(values);
|
|
193
195
|
if(action==='start'||action==='restart'){
|
|
@@ -200,8 +202,8 @@ try{
|
|
|
200
202
|
else if(action==='status')console.log(JSON.stringify(await gatewayStatus(info.stateDir),null,2));
|
|
201
203
|
else if(action==='pair'){
|
|
202
204
|
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)]);
|
|
205
|
+
await withProgress('正在确认网关并准备连接…',async()=>{await start(info,connection);await rememberConnection(info,connection);});
|
|
206
|
+
await run(join(root,'scripts/pairing-qr.mjs'),['--state-dir',info.stateDir,'--auto-approve','--no-ui',...(values['legacy-qr']?['--legacy-qr']:[]),...connectionArgs(connection)]);
|
|
205
207
|
}else{
|
|
206
208
|
const forwarded=[action,'--state-dir',info.stateDir];
|
|
207
209
|
for(const key of ['backend','device-id','capability','claim-id','grants-json'])if(values[key]!==undefined)forwarded.push('--'+key,values[key]);
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
2
|
-
import { promisify } from 'node:util';
|
|
3
1
|
import { request } from 'node:https';
|
|
4
|
-
import { readFile, stat } from 'node:fs/promises';
|
|
5
2
|
import { randomUUID } from 'node:crypto';
|
|
6
3
|
import { Fault, requireValue } from './domain.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
import { platform } from './platform.mjs';
|
|
5
|
+
/** Protobuf JSON omits an empty map. Accept the native empty response, but reject unrelated/malformed payloads. */
|
|
6
|
+
export function cascadeSummaries(value) {
|
|
7
|
+
requireValue(value && typeof value === 'object' && !Array.isArray(value) && (value.trajectorySummaries !== undefined || Object.keys(value).length === 0), 'PROTOCOL_INCOMPATIBLE', '桌面会话目录格式已变化', 503);
|
|
8
|
+
const summaries = value.trajectorySummaries === undefined ? {} : value.trajectorySummaries;
|
|
9
|
+
requireValue(summaries && typeof summaries === 'object' && !Array.isArray(summaries), 'PROTOCOL_INCOMPATIBLE', '桌面会话目录格式已变化', 503);
|
|
10
|
+
return summaries;
|
|
11
|
+
}
|
|
10
12
|
/** Same-user desktop process only. Local CSRF credentials never leave this client. */
|
|
11
13
|
export class AntigravityNative {
|
|
12
14
|
#authority;
|
|
@@ -39,40 +41,18 @@ export class AntigravityNative {
|
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
async discover(epoch) {
|
|
42
|
-
|
|
43
|
-
const version = JSON.parse(await readFile(app + '/package.json', 'utf8')).version;
|
|
44
|
-
requireValue(version === '2.29.0', 'PROTOCOL_INCOMPATIBLE', '当前仅适配 Antigravity App 2.29.0,请核对桌面版本', 503);
|
|
45
|
-
const { stdout } = await exec('/bin/ps', ['-axo', 'uid,pid,ppid,command'], { maxBuffer: 4 * 1024 * 1024 });
|
|
46
|
-
const processes = stdout.split('\n').flatMap(line => {
|
|
47
|
-
const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/.exec(line);
|
|
48
|
-
if (!match || Number(match[1]) !== process.getuid?.() || !match[4].startsWith(binary + ' '))
|
|
49
|
-
return [];
|
|
50
|
-
const flag = (name) => new RegExp('--' + name + '(?:=|\\s+)([^\\s]+)').exec(match[4])?.[1];
|
|
51
|
-
if (flag('app_data_dir') !== 'antigravity' || flag('subclient_type') !== 'hub' || flag('override_ide_name') !== 'antigravity')
|
|
52
|
-
return [];
|
|
53
|
-
const token = flag('csrf_token');
|
|
54
|
-
return token ? [{ pid: Number(match[2]), token }] : [];
|
|
55
|
-
});
|
|
56
|
-
requireValue(processes.length === 1, 'BACKEND_UNAVAILABLE', '请打开唯一的 Antigravity App 桌面实例', 503);
|
|
57
|
-
const desktop = processes[0];
|
|
58
|
-
const { stdout: listeners } = await exec('/usr/sbin/lsof', ['-nP', '-a', '-p', String(desktop.pid), '-iTCP', '-sTCP:LISTEN', '-Fn'], { maxBuffer: 65536 });
|
|
59
|
-
const ports = [...new Set([...listeners.matchAll(/^n(?:127\.0\.0\.1|\*|\[::1\]):(\d+)$/gm)].map(m => Number(m[1])))];
|
|
60
|
-
requireValue(ports.length > 0, 'BACKEND_UNAVAILABLE', '无法核实 Antigravity 桌面监听地址', 503);
|
|
61
|
-
const executable = await stat(binary);
|
|
62
|
-
requireValue(executable.isFile() && !(executable.mode & 0o002), 'UNSAFE_STORAGE', 'Antigravity 可执行文件权限异常', 403);
|
|
44
|
+
const desktop = await platform.antigravity.discover();
|
|
63
45
|
let found;
|
|
64
|
-
for (const port of ports) {
|
|
65
|
-
const authority = {
|
|
46
|
+
for (const port of desktop.ports) {
|
|
47
|
+
const authority = { pid: desktop.pid, port, token: desktop.token };
|
|
66
48
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
break;
|
|
71
|
-
}
|
|
49
|
+
cascadeSummaries(await this.rpc(authority, 'GetAllCascadeTrajectories', { excludeSubtrajectories: true }, 5000));
|
|
50
|
+
found = authority;
|
|
51
|
+
break;
|
|
72
52
|
}
|
|
73
53
|
catch { /* Only ports owned by the verified desktop process are considered. */ }
|
|
74
54
|
}
|
|
75
|
-
requireValue(found, 'BACKEND_UNAVAILABLE',
|
|
55
|
+
requireValue(found, 'BACKEND_UNAVAILABLE', platform.antigravity.unavailableMessage, 503);
|
|
76
56
|
requireValue(epoch === this.#epoch, 'BACKEND_UNAVAILABLE', 'Antigravity 连接已关闭', 503);
|
|
77
57
|
this.#authority = found;
|
|
78
58
|
this.#checked = Date.now();
|
|
@@ -82,7 +62,12 @@ export class AntigravityNative {
|
|
|
82
62
|
await this.connect();
|
|
83
63
|
const authority = this.#authority;
|
|
84
64
|
try {
|
|
85
|
-
|
|
65
|
+
const result = await this.rpc(authority, method, params, timeout, signal);
|
|
66
|
+
if (method === 'GetCascadeTrajectorySteps') {
|
|
67
|
+
requireValue(result && typeof result === 'object' && !Array.isArray(result) && (result.steps === undefined || Array.isArray(result.steps)), 'PROTOCOL_INCOMPATIBLE', '原生步骤列表格式已变化', 503);
|
|
68
|
+
return { ...result, steps: result.steps ?? [] };
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
86
71
|
}
|
|
87
72
|
catch (error) {
|
|
88
73
|
if (!signal?.aborted && this.#authority === authority) {
|
package/dist/antigravity.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
|
-
import {
|
|
3
|
-
import { open, realpath, stat, readFile, writeFile, rename, lstat, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { realpath, stat, readFile, writeFile, rename, lstat, mkdir } from 'node:fs/promises';
|
|
4
3
|
import { homedir } from 'node:os';
|
|
5
4
|
import { join, relative, isAbsolute, sep, basename, dirname, resolve } from 'node:path';
|
|
6
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
6
|
import { randomUUID } from 'node:crypto';
|
|
8
|
-
import {
|
|
7
|
+
import { platform } from './platform.mjs';
|
|
9
8
|
import { imageChunk } from './media.js';
|
|
10
|
-
import { AntigravityNative } from './antigravity-native.js';
|
|
9
|
+
import { AntigravityNative, cascadeSummaries } from './antigravity-native.js';
|
|
11
10
|
import { messageInput, resolveExtensions } from './extensions.js';
|
|
12
11
|
import { iconSource, iconFields, extensionIconChunk } from './extension-icons.js';
|
|
13
12
|
import { permissionKey } from './permissions.js';
|
|
@@ -42,7 +41,7 @@ export class AntigravityBackend {
|
|
|
42
41
|
kind = 'antigravity';
|
|
43
42
|
// Writes are available only while attached to the verified desktop process.
|
|
44
43
|
get readOnly() { return !this.desktop.ready; }
|
|
45
|
-
get capabilities() { return { commands: this.readOnly ? [] : agentCommands, modeControl: false, modelSettings: !this.readOnly, permissions: !this.readOnly, suggestions: !this.readOnly, steer: !this.readOnly, images: !this.readOnly, files: !this.readOnly, extensions: !this.readOnly, changes: !this.readOnly, projectActions: [{ action: 'rename', label: '重命名' }], threadActions: [{ action: 'rename', label: '重命名' }, { action: 'pin', label: '置顶', activeLabel: '取消置顶' }, { action: 'archive', label: '归档' }] }; }
|
|
44
|
+
get capabilities() { return { turnOutcome: this.desktop.ready, commands: this.readOnly ? [] : agentCommands, modeControl: false, modelSettings: !this.readOnly, permissions: !this.readOnly, suggestions: !this.readOnly, steer: !this.readOnly, images: !this.readOnly, files: !this.readOnly, extensions: !this.readOnly, changes: !this.readOnly && platform.antigravity.diffAvailable, projectActions: [{ action: 'rename', label: '重命名' }], threadActions: [{ action: 'rename', label: '重命名' }, { action: 'pin', label: '置顶', activeLabel: '取消置顶' }, { action: 'archive', label: '归档' }] }; }
|
|
46
45
|
permissionSelections = new Map();
|
|
47
46
|
ready = false;
|
|
48
47
|
logs = new Map();
|
|
@@ -63,10 +62,10 @@ export class AntigravityBackend {
|
|
|
63
62
|
async bytes(path, limit) {
|
|
64
63
|
const root = await realpath(this.dataRoot), resolved = await realpath(path);
|
|
65
64
|
requireValue(within(root, resolved), 'UNSAFE_STORAGE', 'Antigravity 文件超出数据目录', 403);
|
|
66
|
-
const file = await
|
|
65
|
+
const file = await platform.files.openNativeRead(resolved);
|
|
67
66
|
try {
|
|
68
67
|
const before = await file.stat();
|
|
69
|
-
requireValue(before.isFile() && before.
|
|
68
|
+
requireValue(before.isFile() && before.size <= limit, 'UNSAFE_STORAGE', 'Antigravity 文件不可读取或超过当前容量限制', 413);
|
|
70
69
|
const bytes = Buffer.alloc(before.size);
|
|
71
70
|
let offset = 0;
|
|
72
71
|
while (offset < bytes.length) {
|
|
@@ -91,9 +90,9 @@ export class AntigravityBackend {
|
|
|
91
90
|
catch {
|
|
92
91
|
return this.diskSessions();
|
|
93
92
|
}
|
|
94
|
-
|
|
93
|
+
const summaries = cascadeSummaries(response);
|
|
95
94
|
const sessions = [];
|
|
96
|
-
for (const [id, row] of Object.entries(
|
|
95
|
+
for (const [id, row] of Object.entries(summaries)) {
|
|
97
96
|
if (!UUID.test(id) || row.trajectoryMetadata?.parentConversationId || row.annotations?.archived)
|
|
98
97
|
continue;
|
|
99
98
|
const uris = row.trajectoryMetadata?.workspaceUris ?? row.workspaces?.map((w) => w.workspaceFolderAbsoluteUri);
|
|
@@ -108,7 +107,10 @@ export class AntigravityBackend {
|
|
|
108
107
|
} }));
|
|
109
108
|
if (roots.some(root => root === null))
|
|
110
109
|
continue;
|
|
111
|
-
|
|
110
|
+
// Protobuf JSON omits the scalar zero; disk-only summaries keep an unknown count.
|
|
111
|
+
const stepCount = row.stepCount === undefined ? 0 : row.stepCount;
|
|
112
|
+
requireValue(Number.isSafeInteger(stepCount) && stepCount >= 0, 'PROTOCOL_INCOMPATIBLE', '原生会话步骤数量无效', 503);
|
|
113
|
+
sessions.push({ conversation_id: id, title: row.annotations?.title ?? row.summary ?? '未命名会话', workspace_uris: JSON.stringify(uris), status: row.status ?? 'unknown', not_fully_idle: 0, last_modified_time: row.lastModifiedTime ?? '', app_data_dir: 'antigravity', parent_conversation_id: '', roots: [...new Set(roots)].sort(), statusKnown: ['CASCADE_RUN_STATUS_IDLE', 'CASCADE_RUN_STATUS_RUNNING', 'CASCADE_RUN_STATUS_CANCELING', 'CASCADE_RUN_STATUS_BUSY'].includes(row.status), stepCount, lastUserInputStepIndex: row.lastUserInputStepIndex, pinned: row.annotations?.pinned === true });
|
|
112
114
|
}
|
|
113
115
|
this.ready = true;
|
|
114
116
|
return sessions.sort((a, b) => Date.parse(b.last_modified_time) - Date.parse(a.last_modified_time));
|
|
@@ -124,7 +126,8 @@ export class AntigravityBackend {
|
|
|
124
126
|
const root = await realpath(this.dataRoot);
|
|
125
127
|
for (const suffix of ['', '-wal', '-shm']) {
|
|
126
128
|
const resolved = await realpath(path + suffix), file = await stat(resolved);
|
|
127
|
-
|
|
129
|
+
const owned = platform.files.owned(resolved, file);
|
|
130
|
+
requireValue(within(root, resolved) && file.isFile() && owned && file.size <= 32 * 1024 * 1024, 'UNSAFE_STORAGE', 'Antigravity 摘要文件不可读取或超过容量限制', 413);
|
|
128
131
|
}
|
|
129
132
|
db = new Database(await realpath(path), { readonly: true, fileMustExist: true });
|
|
130
133
|
}
|
|
@@ -222,11 +225,12 @@ export class AntigravityBackend {
|
|
|
222
225
|
async createProject(name, idempotencyKey, parentPath = '~/workspace') {
|
|
223
226
|
name = name.trim();
|
|
224
227
|
requireValue(name.length > 0 && name.length <= 80 && name !== '.' && name !== '..' && !/[\\/\x00-\x1f\x7f]/.test(name), 'INVALID_REQUEST', '项目名称无效', 400);
|
|
228
|
+
platform.checkProjectName(name);
|
|
225
229
|
const key = digest({ kind: this.kind, idempotencyKey }), id = key.slice(0, 8) + '-' + key.slice(8, 12) + '-4' + key.slice(13, 16) + '-a' + key.slice(17, 20) + '-' + key.slice(20, 32);
|
|
226
230
|
const existing = (await this.nativeProjects()).find(p => p.native.id === id);
|
|
227
231
|
if (existing)
|
|
228
232
|
return existing.scope;
|
|
229
|
-
const parent =
|
|
233
|
+
const parent = platform.projectParent(parentPath);
|
|
230
234
|
await mkdir(parent, { recursive: true });
|
|
231
235
|
const root = join(await realpath(parent), name);
|
|
232
236
|
await mkdir(root, { recursive: true });
|
|
@@ -322,7 +326,15 @@ export class AntigravityBackend {
|
|
|
322
326
|
return { id, name: catalog.trajectorySummaries[id].summary || '新会话', status: 'idle' };
|
|
323
327
|
}
|
|
324
328
|
requireValue(!creation.recoverOnly, 'CREATION_UNKNOWN', '原创建请求尚未确认,请稍后在原项目重试', 409);
|
|
325
|
-
|
|
329
|
+
let result;
|
|
330
|
+
try {
|
|
331
|
+
result = await this.desktop.call('StartCascade', { cascadeId: id, source: 'CORTEX_TRAJECTORY_SOURCE_CASCADE_CLIENT', workspaceUris: [], projectEnvConfig: { projectId: project.native.id, defaultProjectEnvironment: {} }, backend: 'BACKEND_TYPE_LOCAL' });
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
if (error.rpcError?.code === 'invalid_argument')
|
|
335
|
+
throw new Fault('CREATION_NOT_STARTED', 'Antigravity 未接受新建请求,请在原项目重试', 400);
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
326
338
|
if (result.cascadeId !== id)
|
|
327
339
|
throw new Error('Antigravity 会话创建结果未知,请核对原项目');
|
|
328
340
|
await this.session(id, projectId);
|
|
@@ -495,7 +507,26 @@ export class AntigravityBackend {
|
|
|
495
507
|
const f = this.desktop.ready ? await this.follow(row).catch(() => undefined) : undefined;
|
|
496
508
|
const records = f ? nativeRecords(f.steps, f.fallbackTurn) : [], latestTurnId = records.at(-1)?.turnId ?? (row.lastUserInputStepIndex !== undefined ? 'agy-turn-' + row.lastUserInputStepIndex : log.records.at(-1)?.turnId);
|
|
497
509
|
const status = f?.update.status ?? row.status, nativeBusy = status !== 'CASCADE_RUN_STATUS_IDLE' || !!row.not_fully_idle;
|
|
498
|
-
|
|
510
|
+
const waiting = !!f && [...f.steps.values()].some(step => step.requestedInteraction && !['completed', 'failed', 'cancelled'].includes(stepStatus(step.status)));
|
|
511
|
+
let outcome = 'unknown';
|
|
512
|
+
if (f && ['CASCADE_RUN_STATUS_RUNNING', 'CASCADE_RUN_STATUS_BUSY', 'CASCADE_RUN_STATUS_CANCELING'].includes(status))
|
|
513
|
+
outcome = 'running';
|
|
514
|
+
const tailIndex = (row.stepCount ?? 0) - 1;
|
|
515
|
+
const tail = f?.steps.get(tailIndex);
|
|
516
|
+
const reply = tail?.plannerResponse;
|
|
517
|
+
if (row.status === 'CASCADE_RUN_STATUS_IDLE' && f?.update.status === row.status && !nativeBusy && !waiting &&
|
|
518
|
+
row.lastUserInputStepIndex !== undefined && latestTurnId === 'agy-turn-' + row.lastUserInputStepIndex &&
|
|
519
|
+
records.at(-1)?.step.step_index === tailIndex && tail?.type === 'CORTEX_STEP_TYPE_PLANNER_RESPONSE' &&
|
|
520
|
+
tail.metadata?.source === 'CORTEX_STEP_SOURCE_MODEL' && reply) {
|
|
521
|
+
if (tail.status === 'CORTEX_STEP_STATUS_CANCELED' || reply.stopReason === 'STOP_REASON_CLIENT_CANCELED')
|
|
522
|
+
outcome = 'cancelled';
|
|
523
|
+
else if (tail.status === 'CORTEX_STEP_STATUS_ERROR' || reply.stopReason === 'STOP_REASON_ERROR')
|
|
524
|
+
outcome = 'failed';
|
|
525
|
+
else if (tail.status === 'CORTEX_STEP_STATUS_DONE' && reply.stopReason === 'STOP_REASON_STOP_PATTERN' &&
|
|
526
|
+
!reply.toolCalls?.length && typeof reply.response === 'string' && reply.response.trim())
|
|
527
|
+
outcome = 'completed';
|
|
528
|
+
}
|
|
529
|
+
return { turnOutcome: latestTurnId ? { turnId: latestTurnId, state: outcome } : undefined, runtime: await this.runtime(threadId), name: row.title, active: nativeBusy, statusKnown: row.statusKnown === true, turnId: row.statusKnown && nativeBusy ? latestTurnId ?? null : null, waiting, latestTurnId, permissions };
|
|
499
530
|
}
|
|
500
531
|
async runtime(threadId) {
|
|
501
532
|
if (!this.desktop.ready)
|
|
@@ -691,7 +722,8 @@ export class AntigravityBackend {
|
|
|
691
722
|
const file = await lstat(this.preferencesPath).catch(missing);
|
|
692
723
|
if (!file)
|
|
693
724
|
return;
|
|
694
|
-
|
|
725
|
+
const owned = platform.files.preferencesOwned(this.preferencesPath, file);
|
|
726
|
+
requireValue(file.isFile() && !file.isSymbolicLink() && owned && file.size <= 512 * 1024, 'UNSAFE_STORAGE', '模型设置文件不可读取', 403);
|
|
695
727
|
const value = JSON.parse(await readFile(this.preferencesPath, 'utf8'));
|
|
696
728
|
requireValue(value.version === 1 && value.models && typeof value.models === 'object', 'PROTOCOL_INCOMPATIBLE', '模型设置文件格式无效', 503);
|
|
697
729
|
for (const [id, row] of Object.entries(value.models))
|
|
@@ -765,6 +797,7 @@ export class AntigravityBackend {
|
|
|
765
797
|
requireValue(Buffer.byteLength(text) <= 512 * 1024, 'STORAGE_FULL', '会话模型设置已满', 413);
|
|
766
798
|
const temp = path + '.' + randomUUID();
|
|
767
799
|
await writeFile(temp, text, { mode: 0o600, flag: 'wx' });
|
|
800
|
+
platform.files.protectCreatedFile(temp);
|
|
768
801
|
await rename(temp, path);
|
|
769
802
|
});
|
|
770
803
|
await this.preferencesWrite;
|
|
@@ -772,23 +805,22 @@ export class AntigravityBackend {
|
|
|
772
805
|
return { model, effort };
|
|
773
806
|
}
|
|
774
807
|
async getPermissions(projectId, threadId) {
|
|
775
|
-
await this.session(threadId, projectId);
|
|
808
|
+
const row = await this.session(threadId, projectId);
|
|
776
809
|
const state = await this.desktop.state();
|
|
777
810
|
const preset = state.userConfig?.userSettings?.permissionPreset ?? state.state?.userSettings?.permissionPreset;
|
|
778
811
|
const mode = preset === 'AGENT_PERMISSION_PRESET_REQUEST_REVIEW' ? 'request-review' : preset === 'AGENT_PERMISSION_PRESET_TURBO' ? 'turbo' : 'desktop';
|
|
779
812
|
const options = [
|
|
780
|
-
{ value: 'desktop', label: '沿用桌面策略', description: '
|
|
813
|
+
{ value: 'desktop', label: '沿用桌面策略', description: '沿用桌面当前的执行权限,不修改桌面设置', requiresConfirmation: false },
|
|
781
814
|
{ value: 'request-review', label: '要求审查', description: '将 Antigravity 桌面全局策略切换为要求审查,影响后续任务', requiresConfirmation: true },
|
|
782
815
|
{ value: 'turbo', label: 'Turbo', description: '将 Antigravity 桌面全局策略切换为 Turbo,影响后续任务', requiresConfirmation: true }
|
|
783
816
|
];
|
|
784
|
-
|
|
785
|
-
return { mode, scope: 'desktop-global', mobileControl: !this.readOnly && (selection === 'desktop' || selection === mode), options };
|
|
817
|
+
return { mode, scope: 'desktop-global', mobileControl: row.statusKnown && !this.readOnly && typeof preset === 'string' && preset.length > 0, options };
|
|
786
818
|
}
|
|
787
819
|
async setPermissions(projectId, threadId, mode, confirmed = false) {
|
|
788
820
|
const row = await this.session(threadId, projectId);
|
|
789
821
|
requireValue(row.statusKnown && !this.readOnly, 'BACKEND_UNAVAILABLE', '请先打开 Antigravity 桌面应用', 503);
|
|
790
822
|
requireValue(['desktop', 'request-review', 'turbo'].includes(mode), 'INVALID_REQUEST', '执行策略无效', 400);
|
|
791
|
-
requireValue(confirmed, 'CONFIRM_FULL_ACCESS', '请确认使用所选执行策略', 428);
|
|
823
|
+
requireValue(mode === 'desktop' || confirmed, 'CONFIRM_FULL_ACCESS', '请确认使用所选执行策略', 428);
|
|
792
824
|
if (mode !== 'desktop') {
|
|
793
825
|
const preset = mode === 'turbo' ? 'AGENT_PERMISSION_PRESET_TURBO' : 'AGENT_PERMISSION_PRESET_REQUEST_REVIEW';
|
|
794
826
|
await this.desktop.call('JetboxWriteState', { userConfig: { userSettings: { permissionPreset: preset } } });
|
|
@@ -799,7 +831,9 @@ export class AntigravityBackend {
|
|
|
799
831
|
return this.getPermissions(projectId, threadId);
|
|
800
832
|
}
|
|
801
833
|
async interactionSnapshot(projectId, threadId, fresh = false) {
|
|
802
|
-
await this.session(threadId, projectId);
|
|
834
|
+
const row = await this.session(threadId, projectId);
|
|
835
|
+
if (row.stepCount === 0)
|
|
836
|
+
return { at: Date.now(), generation: this.desktop.generation, trajectoryId: '', steps: [] };
|
|
803
837
|
const cached = this.interactions.get(threadId);
|
|
804
838
|
if (!fresh && cached?.generation === this.desktop.generation && Date.now() - cached.at < 1500)
|
|
805
839
|
return cached;
|
|
@@ -920,25 +954,7 @@ export class AntigravityBackend {
|
|
|
920
954
|
}
|
|
921
955
|
const original = data.originalContents ?? '', modified = data.modifiedContents ?? '';
|
|
922
956
|
requireValue(typeof original === 'string' && typeof modified === 'string', 'PROTOCOL_INCOMPATIBLE', '原生差异内容格式已变化', 503);
|
|
923
|
-
const diff = await
|
|
924
|
-
// Native diff reads pipes: no worktree reads, temporary files or reconstructed Git state.
|
|
925
|
-
const child = spawn('/usr/bin/diff', ['-u', '-L', path, '-L', path, original.length ? '/dev/fd/3' : '/dev/null', modified.length ? '/dev/fd/4' : '/dev/null'], { stdio: ['ignore', 'pipe', 'ignore', 'pipe', 'pipe'] });
|
|
926
|
-
let text = '';
|
|
927
|
-
const timer = setTimeout(() => { child.kill(); no(new Error('差异生成超时')); }, 10000);
|
|
928
|
-
timer.unref();
|
|
929
|
-
child.stdout.setEncoding('utf8');
|
|
930
|
-
child.stdout.on('data', (chunk) => { text += chunk; if (Buffer.byteLength(text) > 4 * 1024 * 1024) {
|
|
931
|
-
child.kill();
|
|
932
|
-
no(new Error('单个差异超过容量限制'));
|
|
933
|
-
} });
|
|
934
|
-
child.once('error', e => { clearTimeout(timer); no(e); });
|
|
935
|
-
child.once('close', code => { clearTimeout(timer); code === 0 || code === 1 ? ok(text) : no(new Error('无法生成原生差异')); });
|
|
936
|
-
for (const [fd, content] of [[3, original], [4, modified]]) {
|
|
937
|
-
const pipe = child.stdio[fd];
|
|
938
|
-
pipe.on('error', () => { });
|
|
939
|
-
pipe.end(content);
|
|
940
|
-
}
|
|
941
|
-
}).catch(() => { partial = true; return ''; });
|
|
957
|
+
const diff = await platform.antigravity.diff(path, original, modified).catch(() => { partial = true; return ''; });
|
|
942
958
|
if (diff)
|
|
943
959
|
items.push({ type: 'fileChange', status: 'completed', changes: [{ path, kind: { type: 'update' }, diff }] });
|
|
944
960
|
else if (data.additions || data.deletions)
|