magic-agent 2.3.9 → 2.4.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/client-ui-mobile/index.html +65 -4
- package/cmd/magic-client/main.go +5 -0
- package/internal/client/instance.go +59 -0
- package/internal/client/service.go +16 -1
- package/internal/client/service_test.go +5 -3
- package/npm/apk/magic-agent-mobile-release.apk +0 -0
- package/npm/apk/version.json +3 -3
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-arm64/magic-client +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-client +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-client +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-client +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/npm/dist/win32-x64/magic-client.exe +0 -0
- package/package.json +1 -1
|
@@ -1600,7 +1600,13 @@ button.setrow{font-family:inherit}
|
|
|
1600
1600
|
list.forEach(function (c) { if (c && c.id === id) hit = c; });
|
|
1601
1601
|
/* 用户主动删掉种出来的默认环境:记一笔,下次启动别再自动加回来。 */
|
|
1602
1602
|
if (hit && hit.seed) { try { localStorage.setItem('magic.remote.seedoff.v1', '1'); } catch (e) {} }
|
|
1603
|
-
|
|
1603
|
+
/* 同一台桌面可能有多条入口(公司本地 / 公司远程)共享一个 eid:
|
|
1604
|
+
只有删掉最后一条时才清队列,否则删一个别名会把另一个还在用的队列带走。 */
|
|
1605
|
+
var sharedEid = false;
|
|
1606
|
+
if (hit && hit.eid) {
|
|
1607
|
+
sharedEid = list.some(function (c) { return c && c.id !== id && c.eid && c.eid === hit.eid; });
|
|
1608
|
+
}
|
|
1609
|
+
if (hit && !keepQueue && !sharedEid) dropEnvQueue(hit);
|
|
1604
1610
|
saveConns(list.filter(function (c) { return c.id !== id; }));
|
|
1605
1611
|
}
|
|
1606
1612
|
/* 清掉某条环境名下的排队行(eid 桶与老 host 桶都清)。 */
|
|
@@ -1671,6 +1677,44 @@ button.setrow{font-family:inherit}
|
|
|
1671
1677
|
for (var j = 0; j < l2.length; j++) if (l2[j].id === rec.id) l2[j].seed = true;
|
|
1672
1678
|
saveConns(l2);
|
|
1673
1679
|
}
|
|
1680
|
+
/* 当前连接所属环境的 eid(临时记录会先补号)。
|
|
1681
|
+
用途:同一台桌面的「公司本地 / 公司远程」两条入口要共享一个环境身份 ——
|
|
1682
|
+
它们是同一个 magic-client、同一份队列与设置,不能因为地址不同就算两台。 */
|
|
1683
|
+
function activeEnvEid() {
|
|
1684
|
+
var c = activeConn();
|
|
1685
|
+
if (!c) return '';
|
|
1686
|
+
c = ensureEnvId(c) || c;
|
|
1687
|
+
return c.eid || '';
|
|
1688
|
+
}
|
|
1689
|
+
/* 把一条环境记录并入 toEid:队列行与配置槽一起迁过去(幂等)。 */
|
|
1690
|
+
function mergeEnvId(rec, toEid) {
|
|
1691
|
+
if (!rec || !toEid || rec.eid === toEid) return false;
|
|
1692
|
+
var from = rec.eid ? eidKey(rec.eid) : envHashOf(rec);
|
|
1693
|
+
rec.eid = toEid;
|
|
1694
|
+
migrateEnvData(from, eidKey(toEid));
|
|
1695
|
+
return true;
|
|
1696
|
+
}
|
|
1697
|
+
/* 服务端报了实例 id(/desk/health.instanceId):把当前环境钉到 `srv:<id>`。
|
|
1698
|
+
同一台桌面的本地 / 局域网 / 公网入口会拿到同一个 id —— 于是不同 URL 也是同一个环境:
|
|
1699
|
+
队列、任务模式、引擎/空间选择都共用一份。老服务端没有该字段 → 什么也不做。 */
|
|
1700
|
+
function adoptServerEid(sid) {
|
|
1701
|
+
sid = String(sid || '').trim();
|
|
1702
|
+
if (!sid) return false;
|
|
1703
|
+
var want = 'srv:' + sid;
|
|
1704
|
+
var cur = activeConn();
|
|
1705
|
+
if (!cur || !cur.id) return false;
|
|
1706
|
+
var list = loadConns(), changed = false;
|
|
1707
|
+
for (var i = 0; i < list.length; i++) {
|
|
1708
|
+
if (!list[i] || list[i].id !== cur.id) continue;
|
|
1709
|
+
if (list[i].eid === want) break;
|
|
1710
|
+
if (list[i].eid) mergeEnvId(list[i], want);
|
|
1711
|
+
else list[i].eid = want;
|
|
1712
|
+
changed = true;
|
|
1713
|
+
break;
|
|
1714
|
+
}
|
|
1715
|
+
if (changed) saveConns(list);
|
|
1716
|
+
return changed;
|
|
1717
|
+
}
|
|
1674
1718
|
function seedCurrentConn() {
|
|
1675
1719
|
var url = connUrlNow();
|
|
1676
1720
|
if (!url || !TOKEN) return;
|
|
@@ -1704,6 +1748,10 @@ button.setrow{font-family:inherit}
|
|
|
1704
1748
|
var url = String(urls[0] || '').replace(/\/+$/, '');
|
|
1705
1749
|
if (!url) return;
|
|
1706
1750
|
if (url === String(connUrlNow()).replace(/\/+$/, '')) return;
|
|
1751
|
+
/* 这些远程入口是**当前这台桌面**自己上报的(/desk/health),
|
|
1752
|
+
与当前连接是同一个 magic-client —— 新记录必须沿用当前环境的 eid,
|
|
1753
|
+
否则「公司本地 / 公司远程」会被当成两台,队列与设置各存一份。 */
|
|
1754
|
+
var homeEid = activeEnvEid();
|
|
1707
1755
|
var list = loadConns();
|
|
1708
1756
|
/* 隧道换域名(cloudflared 每次重启都换):把上一枚**默认命名**的种子远程记录换到新地址。
|
|
1709
1757
|
用户改过名字的不动 —— 那条是他自己配的环境。 */
|
|
@@ -1722,18 +1770,23 @@ button.setrow{font-family:inherit}
|
|
|
1722
1770
|
var c = list[i];
|
|
1723
1771
|
if (!c) continue;
|
|
1724
1772
|
if (c.url === url && String(c.token || '') === String(TOKEN)) {
|
|
1725
|
-
|
|
1773
|
+
var changed = false;
|
|
1774
|
+
if (c.seed && c.name === SEED_LOCAL) { c.name = SEED_NAME; changed = true; }
|
|
1775
|
+
/* 老数据 / 先远程后本地:把这条并到当前桌面的 eid 上。 */
|
|
1776
|
+
if (homeEid && mergeEnvId(c, homeEid)) changed = true;
|
|
1777
|
+
if (changed) saveConns(list);
|
|
1726
1778
|
return;
|
|
1727
1779
|
}
|
|
1728
1780
|
if (c.seed && c.url === url) {
|
|
1729
1781
|
c.token = TOKEN;
|
|
1730
1782
|
c.id = connIdOf({ url: url, token: TOKEN });
|
|
1731
1783
|
c.at = Date.now();
|
|
1784
|
+
if (homeEid) mergeEnvId(c, homeEid);
|
|
1732
1785
|
saveConns(list);
|
|
1733
1786
|
return;
|
|
1734
1787
|
}
|
|
1735
1788
|
}
|
|
1736
|
-
seedMark(upsertConn({ url: url, token: TOKEN, name: SEED_NAME, eid: replacedEid }));
|
|
1789
|
+
seedMark(upsertConn({ url: url, token: TOKEN, name: SEED_NAME, eid: homeEid || replacedEid }));
|
|
1737
1790
|
}
|
|
1738
1791
|
|
|
1739
1792
|
/* ── 设备现场(按环境隔离)──
|
|
@@ -3219,6 +3272,9 @@ button.setrow{font-family:inherit}
|
|
|
3219
3272
|
CORE_FEATURES = ((a[2] && a[2].features) || []);
|
|
3220
3273
|
/* 连接的身份(app):连着哪台设备就记到哪条连接上,环境菜单里当类型标签用。 */
|
|
3221
3274
|
if (a[2] && a[2].app) noteActiveMeta({ app: a[2].app });
|
|
3275
|
+
/* 服务实例身份:同一台桌面的多个入口(本地 / 局域网 / 公网)归成一个环境。
|
|
3276
|
+
必须排在 seedRemoteConns 之前 —— 后者新建「公司远程」时要继承当前 eid。 */
|
|
3277
|
+
if (a[2] && a[2].instanceId) adoptServerEid(a[2].instanceId);
|
|
3222
3278
|
/* 桌面绑了公网隧道时上报 publicUrl(cloudflared,不在同一 Wi-Fi 也能连),
|
|
3223
3279
|
没有则退回局域网入口:自动补一条「公司远程」环境,用户不用手抄地址。 */
|
|
3224
3280
|
if (a[2]) {
|
|
@@ -3249,7 +3305,12 @@ button.setrow{font-family:inherit}
|
|
|
3249
3305
|
}).catch(function (e) {
|
|
3250
3306
|
if (seq !== CONN_SEQ) return;
|
|
3251
3307
|
if (quiet) return; // 后台轮询失败:保留现有列表(见上面的说明)
|
|
3252
|
-
|
|
3308
|
+
/* 失败原因要带上**实际打的地址**:Photo/WebView 里 "Failed to fetch" 只说「没连上」,
|
|
3309
|
+
而真正的原因通常是「这台手机到那个 IP 不通」(换了网络 / 环境里存的是别的网段)。
|
|
3310
|
+
不写地址的话,用户和应用都无从判断该改哪一边。 */
|
|
3311
|
+
var why = (e && e.message ? e.message : String(e));
|
|
3312
|
+
var addr = BASE || location.origin || '(同源)';
|
|
3313
|
+
historyList.innerHTML = '<div class="history__hint">读取会话失败:' + why + '<br>地址:' + addr + '</div>';
|
|
3253
3314
|
});
|
|
3254
3315
|
}
|
|
3255
3316
|
/* ── 会话列表:**置顶 / 任务 / 空间** 三段(对齐截图)──
|
package/cmd/magic-client/main.go
CHANGED
|
@@ -62,6 +62,7 @@ func main() {
|
|
|
62
62
|
proxyAddr = fs.String("proxy-addr", "", "SOCKS5 出口监听地址(供移动端 VPN 隧道核心使用),如 0.0.0.0:1080")
|
|
63
63
|
browseUA = fs.String("browse-ua", "", "内置浏览器中转的 User-Agent:mobile(默认)/ desktop / 任意自定义串")
|
|
64
64
|
readyFile = fs.String("ready-file", "", "监听成功后把 {pid,port,url,lanAddrs} 原子写到这个路径(`magic-client start` 用它拿端口,别手工传)")
|
|
65
|
+
anyOrigin = fs.Bool("allow-any-origin", false, "对任意 Origin 回 CORS 头(默认只放行本机页面)。--lan 时自动开启;令牌仍是唯一门锁")
|
|
65
66
|
)
|
|
66
67
|
_ = fs.Parse(os.Args[2:])
|
|
67
68
|
|
|
@@ -90,6 +91,10 @@ func main() {
|
|
|
90
91
|
Token: tok,
|
|
91
92
|
Lan: *lan,
|
|
92
93
|
BrowseUA: *browseUA,
|
|
94
|
+
/* 绑到局域网 = 「允许来自任意 IP 的页面访问」:再按 loopback 卡 CORS Origin
|
|
95
|
+
等于把手机 / 别的设备的页面全判成跨域(用户侧就是「只能本地访问」)。
|
|
96
|
+
显式 --allow-any-origin 也可以单独打开(绑回环但要给容器页面用)。 */
|
|
97
|
+
AllowAnyOrigin: *anyOrigin || *lan,
|
|
93
98
|
})
|
|
94
99
|
if err != nil {
|
|
95
100
|
// 契约不匹配是**要立刻停**的错误:带着不匹配的能力跑起来只会让人以为是界面 bug。
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
package client
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"crypto/rand"
|
|
5
|
+
"encoding/hex"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"strings"
|
|
9
|
+
"sync"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
服务实例的稳定身份(2026-10-23)。
|
|
14
|
+
|
|
15
|
+
为什么需要:同一台桌面可能有多条入口(公司本地 127.0.0.1、局域网 IP、cloudflared
|
|
16
|
+
公网域名),它们在客户端看来是不同 URL。移动端要用「这是不是同一台桌面」来决定
|
|
17
|
+
队列与设置要不要共用一份 —— 只看地址会把同一台机器拆成好几个环境。
|
|
18
|
+
|
|
19
|
+
id 落在 profile 的 DataDir 下(`instance-id`),第一次生成后不再变:
|
|
20
|
+
|
|
21
|
+
· 改端口 / 换隧道域名 / 手机换 Wi-Fi 都不影响;
|
|
22
|
+
· 只删 `instance-id` 才会换身份(等同于「这是一台新桌面」)。
|
|
23
|
+
|
|
24
|
+
/desk/health 把它报给客户端(`instanceId`,只增字段)。老客户端忽略;
|
|
25
|
+
新客户端据此把同一台桌面的多个入口并成一个环境身份。
|
|
26
|
+
*/
|
|
27
|
+
type instanceIDCache struct {
|
|
28
|
+
once sync.Once
|
|
29
|
+
id string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func (s *Service) instanceID() string {
|
|
33
|
+
s.inst.once.Do(func() {
|
|
34
|
+
dir := s.opts.Profile.DataDir()
|
|
35
|
+
if strings.TrimSpace(dir) == "" {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
path := filepath.Join(dir, "instance-id")
|
|
39
|
+
if b, err := os.ReadFile(path); err == nil {
|
|
40
|
+
if v := strings.TrimSpace(string(b)); len(v) >= 8 {
|
|
41
|
+
s.inst.id = v
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
var buf [8]byte
|
|
46
|
+
if _, err := rand.Read(buf[:]); err != nil {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
id := hex.EncodeToString(buf[:])
|
|
50
|
+
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
if err := os.WriteFile(path, []byte(id+"\n"), 0o600); err != nil {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
s.inst.id = id
|
|
57
|
+
})
|
|
58
|
+
return s.inst.id
|
|
59
|
+
}
|
|
@@ -80,6 +80,14 @@ type Options struct {
|
|
|
80
80
|
// 为什么可配:UA 会决定站点给哪一版页面。中国大陆出口 + 移动 UA 时,Google 会跳到
|
|
81
81
|
// http://www.google.cn/m(该路径本身就是 404);换成桌面 UA 则走香港跳转、可正常打开。
|
|
82
82
|
BrowseUA string
|
|
83
|
+
// AllowAnyOrigin 对**任意 Origin** 回 CORS 头(2026-10-23)。
|
|
84
|
+
//
|
|
85
|
+
// 默认只放行「本机页面」(见 withCORS):APK / 桌面壳这些容器源的 Origin 各不相同,
|
|
86
|
+
// 自托管部署的入口也不止本机 —— 只放行 loopback 时,从别的设备 / 别的源打开的页面
|
|
87
|
+
// 会被浏览器判成跨域失败(用户侧就是「只能本地访问」)。
|
|
88
|
+
// 打开它 = 任意网页都能**读**响应;真正的门锁仍是令牌(没有 X-Magic-Token 一律 401)。
|
|
89
|
+
// `--lan` 时默认开启(都绑 0.0.0.0 了,再按 loopback 卡 Origin 没有意义)。
|
|
90
|
+
AllowAnyOrigin bool
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
// Service 本地服务。
|
|
@@ -98,6 +106,9 @@ type Service struct {
|
|
|
98
106
|
// jobs 服务端作业队列(同一 workspace 串行 + 幂等 + 持久化),见 jobs.go。
|
|
99
107
|
jobs *jobQueue
|
|
100
108
|
|
|
109
|
+
// inst 服务实例的稳定 id(见 instance.go):同一台桌面的多个入口靠它归一。
|
|
110
|
+
inst instanceIDCache
|
|
111
|
+
|
|
101
112
|
// 内置浏览器中转用的 HTTP 客户端(惰性建、带 cookie jar,按上游 host 存会话)。
|
|
102
113
|
// 见 browse.go —— 它让 iframe 的流量走**桌面端网络**,而不是手机直连。
|
|
103
114
|
browseOnce sync.Once
|
|
@@ -309,7 +320,7 @@ func (s *Service) withCORS(next http.Handler) http.Handler {
|
|
|
309
320
|
if origin != "" {
|
|
310
321
|
// Origin 会随请求方变,不加 Vary 会把一个源的响应喂给另一个源。
|
|
311
322
|
w.Header().Add("Vary", "Origin")
|
|
312
|
-
if localOrigin(origin) {
|
|
323
|
+
if s.opts.AllowAnyOrigin || localOrigin(origin) {
|
|
313
324
|
w.Header().Set("Access-Control-Allow-Origin", origin)
|
|
314
325
|
/* ⚠️ 写方法必须点名(2026-10-23 用户:「删除无法删除」)。
|
|
315
326
|
只写 GET/POST 时,容器里(源=https://localhost)的删除 / 重命名 / 置顶同步
|
|
@@ -415,6 +426,10 @@ func (s *Service) handleHealth(w http.ResponseWriter, r *http.Request) {
|
|
|
415
426
|
// 服务端作业队列(2026-10-23):有它才谈得上「多客户端串行 + 幂等」。
|
|
416
427
|
// 老插件没有该字段 → 客户端退回本地排队(见 client-ui-mobile 的 fallback)。
|
|
417
428
|
"jobs": true,
|
|
429
|
+
// 服务实例的稳定 id(2026-10-23,只增字段):同一台桌面的本地 / 局域网 / 公网
|
|
430
|
+
// 入口可能有多条 URL,客户端据此把它们归一成同一个环境(队列与设置共用)。
|
|
431
|
+
// 老插件没有该字段 → 客户端退回按连接各自算环境。
|
|
432
|
+
"instanceId": s.instanceID(),
|
|
418
433
|
// 远程移动端界面在不在(见 handleMobileUI)。
|
|
419
434
|
// 客户端的闸门页要据此决定「跳远程 /m/ 还是用包里自带的 ui.html」。
|
|
420
435
|
// 为什么不放在 features 里:features 是**core 的能力位**(s.contract.Features),
|
|
@@ -71,11 +71,13 @@ const hostedContract = `{"contractVersion":2,"features":["feature.session.events
|
|
|
71
71
|
|
|
72
72
|
func newTestService(t *testing.T, core string) *Service {
|
|
73
73
|
t.Helper()
|
|
74
|
-
/*
|
|
75
|
-
|
|
74
|
+
/* 测试互不干扰:作业目录 / 实例 id 都放临时目录;并发上限放大(假 core 不退出,
|
|
75
|
+
不放大就会把同一进程里后续用例堵在队列里)。串行语义由 TestJobs* 用独立服务端覆盖。 */
|
|
76
76
|
t.Setenv("MAGIC_CLIENT_JOBS_DIR", t.TempDir())
|
|
77
77
|
t.Setenv("MAGIC_CLIENT_MAX_RUNNING", "64")
|
|
78
|
-
|
|
78
|
+
prof := DefaultProfile()
|
|
79
|
+
prof.Storage.Dir = t.TempDir()
|
|
80
|
+
svc, err := New(Options{MagicAgent: core, Profile: prof, Token: "tok"})
|
|
79
81
|
if err != nil {
|
|
80
82
|
t.Fatalf("New: %v", err)
|
|
81
83
|
}
|
|
Binary file
|
package/npm/apk/version.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|