magic-agent 2.3.2 → 2.3.9
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-CONTRACT.md +30 -4
- package/README.md +1 -1
- package/client-ui/shared/conv/07-stream-delegation.js +15 -0
- package/client-ui/shared/conversation.js +39 -0
- package/client-ui/shared/transport-http.js +10 -0
- package/client-ui-mobile/index.html +368 -44
- package/internal/agent/engine.go +2 -2
- package/internal/agent/llmengine.go +0 -8
- package/internal/agent/llmengine_test.go +0 -17
- package/internal/agent/llmmodels.go +0 -35
- package/internal/client/cors_test.go +32 -0
- package/internal/client/jobs.go +1160 -0
- package/internal/client/jobs_test.go +369 -0
- package/internal/client/service.go +235 -140
- package/internal/client/service_test.go +43 -0
- package/internal/session/session.go +15 -1
- package/internal/session/workspace_lock.go +165 -0
- package/internal/session/workspace_lock_test.go +90 -0
- package/internal/suggest/suggest.go +8 -25
- package/internal/suggest/suggest_test.go +0 -6
- 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
|
@@ -1575,24 +1575,44 @@ button.setrow{font-family:inherit}
|
|
|
1575
1575
|
var id = connIdOf(t);
|
|
1576
1576
|
var old = findConn(id) || {};
|
|
1577
1577
|
var list = loadConns().filter(function (c) { return c.id !== id; });
|
|
1578
|
+
/* eid:环境的稳定身份(见 ensureEnvId)。改地址/改令牌时由调用方带过来(t.eid),
|
|
1579
|
+
否则沿用老记录的;都没有才发新号。 */
|
|
1580
|
+
var eid = t.eid || old.eid || newEid();
|
|
1578
1581
|
var next = {
|
|
1579
1582
|
id: id, url: t.url || '', token: t.token || '',
|
|
1580
1583
|
name: (t.name != null && t.name !== '') ? t.name : (old.name || ''),
|
|
1581
1584
|
host: connHost(t.url), app: old.app || '', at: Date.now(),
|
|
1585
|
+
eid: eid,
|
|
1582
1586
|
seed: !!old.seed // 种出来的默认环境:改名/改地址后仍然认出它
|
|
1583
1587
|
};
|
|
1584
1588
|
list.unshift(next);
|
|
1585
1589
|
saveConns(list);
|
|
1590
|
+
/* 第一次拿到 eid:把这个环境原先按 host 哈希分区的队列行与配置槽迁过来。 */
|
|
1591
|
+
if (!old.eid && !t.eid) migrateEnvData(envHashOf(next), eidKey(eid));
|
|
1586
1592
|
return next;
|
|
1587
1593
|
}
|
|
1588
|
-
|
|
1594
|
+
/* 删环境时把它名下的排队任务一起清掉:地址可能被别的服务器复用,
|
|
1595
|
+
留着就会在下次连上时被当成那台的任务发出去。
|
|
1596
|
+
keepQueue=true 用于「改地址/改令牌」——那不是删环境,eid 不变,队列要跟着走。 */
|
|
1597
|
+
function removeConn(id, keepQueue) {
|
|
1589
1598
|
var list = loadConns();
|
|
1590
1599
|
var hit = null;
|
|
1591
1600
|
list.forEach(function (c) { if (c && c.id === id) hit = c; });
|
|
1592
1601
|
/* 用户主动删掉种出来的默认环境:记一笔,下次启动别再自动加回来。 */
|
|
1593
1602
|
if (hit && hit.seed) { try { localStorage.setItem('magic.remote.seedoff.v1', '1'); } catch (e) {} }
|
|
1603
|
+
if (hit && !keepQueue) dropEnvQueue(hit);
|
|
1594
1604
|
saveConns(list.filter(function (c) { return c.id !== id; }));
|
|
1595
1605
|
}
|
|
1606
|
+
/* 清掉某条环境名下的排队行(eid 桶与老 host 桶都清)。 */
|
|
1607
|
+
function dropEnvQueue(conn) {
|
|
1608
|
+
if (!conn) return;
|
|
1609
|
+
var keys = [eidKey(conn.eid), envHashOf(conn)].filter(function (k) { return !!k; });
|
|
1610
|
+
if (!keys.length) return;
|
|
1611
|
+
try {
|
|
1612
|
+
var rows = readQueueAll().filter(function (p) { return !p || keys.indexOf(String(p.env || '')) < 0; });
|
|
1613
|
+
localStorage.setItem(QUEUE_KEY, JSON.stringify(rows));
|
|
1614
|
+
} catch (e) {}
|
|
1615
|
+
}
|
|
1596
1616
|
function clearConns() {
|
|
1597
1617
|
saveConns([]);
|
|
1598
1618
|
try {
|
|
@@ -1688,9 +1708,12 @@ button.setrow{font-family:inherit}
|
|
|
1688
1708
|
/* 隧道换域名(cloudflared 每次重启都换):把上一枚**默认命名**的种子远程记录换到新地址。
|
|
1689
1709
|
用户改过名字的不动 —— 那条是他自己配的环境。 */
|
|
1690
1710
|
var kept = [];
|
|
1711
|
+
var replacedEid = '';
|
|
1691
1712
|
for (var k = 0; k < list.length; k++) {
|
|
1692
1713
|
var o = list[k];
|
|
1693
|
-
|
|
1714
|
+
/* 隧道换域名 = 同一台桌面换了入口:把它的 eid 带过去,
|
|
1715
|
+
否则新记录会发新号,这台环境排队中的任务与配置槽全部变孤儿。 */
|
|
1716
|
+
if (o && o.seed && o.name === SEED_NAME && o.url !== url) { replacedEid = o.eid || replacedEid; continue; }
|
|
1694
1717
|
kept.push(o);
|
|
1695
1718
|
}
|
|
1696
1719
|
if (kept.length !== list.length) saveConns(kept);
|
|
@@ -1710,16 +1733,17 @@ button.setrow{font-family:inherit}
|
|
|
1710
1733
|
return;
|
|
1711
1734
|
}
|
|
1712
1735
|
}
|
|
1713
|
-
seedMark(upsertConn({ url: url, token: TOKEN, name: SEED_NAME }));
|
|
1736
|
+
seedMark(upsertConn({ url: url, token: TOKEN, name: SEED_NAME, eid: replacedEid }));
|
|
1714
1737
|
}
|
|
1715
1738
|
|
|
1716
1739
|
/* ── 设备现场(按环境隔离)──
|
|
1717
1740
|
2026-10-22 用户:「切换环境后 所有东西都要跟着变 包括会话 需求 我的这些 是选择环境上的东西」。
|
|
1718
1741
|
会话与需求本来就来自桌面端(切完重拉);这里解决的是**留在本机的那部分状态**:
|
|
1719
1742
|
主题 / 字体 / 任务模式 / 默认引擎与模型 / 团队配置 / 当前空间 / 置顶 / 通知 / 浏览器收藏。
|
|
1720
|
-
|
|
1743
|
+
做法:切环境时把这一组键快照到「按环境身份分桶」的槽里,再把目标环境的槽写回来,然后整页重载
|
|
1721
1744
|
—— 内存态全部重新初始化,不留上一台机器的影子。
|
|
1722
|
-
⚠️
|
|
1745
|
+
⚠️ 分桶键用稳定 eid(见 ensureEnvId),不是 url|token(令牌会轮换)、也不是 host:port
|
|
1746
|
+
(换 IP/端口/隧道域名就变成另一台,现场全丢)。老数据按 host 哈希做一次迁移。 */
|
|
1723
1747
|
var ENV_SCOPE_KEYS = [
|
|
1724
1748
|
'magic.ws', 'magic.taskmode', 'magic.hosting.sessions.v1', 'magic.engine', 'magic.model',
|
|
1725
1749
|
'magic.suggest.engine', 'magic.suggest.model',
|
|
@@ -1738,11 +1762,64 @@ button.setrow{font-family:inherit}
|
|
|
1738
1762
|
for (var i = 0; i < str.length; i++) h = ((h << 5) + h + str.charCodeAt(i)) >>> 0;
|
|
1739
1763
|
return h.toString(36);
|
|
1740
1764
|
}
|
|
1741
|
-
|
|
1765
|
+
/* ── 稳定环境身份(eid)──
|
|
1766
|
+
地址会变(IP/端口/隧道域名),令牌会轮换(App 每次启动换注入令牌),两者都不能当环境身份:
|
|
1767
|
+
地址变了 → 旧队列/旧配置变孤儿;地址被别的服务器复用 → 旧队列发错环境。
|
|
1768
|
+
eid 在环境第一次落盘时生成,之后跟着这条记录走:改地址、改令牌都不换号。 */
|
|
1769
|
+
function newEid() {
|
|
1770
|
+
return 'e' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
1771
|
+
}
|
|
1772
|
+
function eidKey(id) { return id ? ('eid:' + id) : ''; }
|
|
1773
|
+
/* 老版本按 host:port 分区的键(只用于一次性迁移和「还没拿到 eid」的临时记录)。 */
|
|
1774
|
+
function envHashOf(conn) {
|
|
1742
1775
|
var host = connHost((conn && conn.url) || connUrlNow());
|
|
1743
1776
|
return host ? envScopeHash(host) : '';
|
|
1744
1777
|
}
|
|
1778
|
+
function envScopeIdOf(conn) {
|
|
1779
|
+
if (conn && conn.eid) return eidKey(conn.eid);
|
|
1780
|
+
return envHashOf(conn);
|
|
1781
|
+
}
|
|
1745
1782
|
function envScopeId() { return envScopeIdOf(activeConn()); }
|
|
1783
|
+
/* 给一条**已落盘**的环境补 eid(老记录没有),并把按 host 分区的旧队列/旧配置槽迁过来。 */
|
|
1784
|
+
function ensureEnvId(conn) {
|
|
1785
|
+
if (!conn) return conn;
|
|
1786
|
+
var oldHash = envHashOf(conn);
|
|
1787
|
+
if (conn.eid) {
|
|
1788
|
+
migrateEnvData(oldHash, eidKey(conn.eid)); // 上次可能只写了一半:重入也要幂等补齐
|
|
1789
|
+
return conn;
|
|
1790
|
+
}
|
|
1791
|
+
var list = loadConns(), hit = null;
|
|
1792
|
+
/* 扫码/粘贴进来的临时目标没有 id:按 url+令牌现算一个,才能跟上已落盘的那条。 */
|
|
1793
|
+
var wantId = conn.id || connIdOf({
|
|
1794
|
+
url: conn.url || connUrlNow(),
|
|
1795
|
+
token: conn.token != null ? conn.token : (TOKEN || '')
|
|
1796
|
+
});
|
|
1797
|
+
for (var i = 0; i < list.length; i++) if (list[i] && list[i].id === wantId) { hit = list[i]; break; }
|
|
1798
|
+
if (!hit) return conn; // 临时记录(还没落盘):等 upsertConn 落盘时再发号
|
|
1799
|
+
hit.eid = newEid();
|
|
1800
|
+
saveConns(list);
|
|
1801
|
+
migrateEnvData(oldHash, eidKey(hit.eid));
|
|
1802
|
+
return hit;
|
|
1803
|
+
}
|
|
1804
|
+
/* 把旧键名下的排队行与配置槽搬到新键名下(幂等)。 */
|
|
1805
|
+
function migrateEnvData(from, to) {
|
|
1806
|
+
if (!from || !to || from === to) return;
|
|
1807
|
+
migrateQueueEnv(from, to);
|
|
1808
|
+
try {
|
|
1809
|
+
var v = localStorage.getItem(ENV_SCOPE_PREFIX + from);
|
|
1810
|
+
if (v != null) {
|
|
1811
|
+
if (localStorage.getItem(ENV_SCOPE_PREFIX + to) == null) localStorage.setItem(ENV_SCOPE_PREFIX + to, v);
|
|
1812
|
+
localStorage.removeItem(ENV_SCOPE_PREFIX + from);
|
|
1813
|
+
}
|
|
1814
|
+
} catch (e) {}
|
|
1815
|
+
}
|
|
1816
|
+
function migrateQueueEnv(from, to) {
|
|
1817
|
+
try {
|
|
1818
|
+
var rows = readQueueAll(), changed = false;
|
|
1819
|
+
rows.forEach(function (p) { if (p && String(p.env || '') === from) { p.env = to; changed = true; } });
|
|
1820
|
+
if (changed) localStorage.setItem(QUEUE_KEY, JSON.stringify(rows));
|
|
1821
|
+
} catch (e) {}
|
|
1822
|
+
}
|
|
1746
1823
|
/* 把当前这组键存进 id 的槽(切走之前调用)。 */
|
|
1747
1824
|
function saveEnvScope(id) {
|
|
1748
1825
|
if (!id) return;
|
|
@@ -1893,7 +1970,8 @@ button.setrow{font-family:inherit}
|
|
|
1893
1970
|
if (btn) btn.classList.remove('is-open');
|
|
1894
1971
|
}
|
|
1895
1972
|
/* 换环境 = 换一台桌面:对话、轮询、运行态都属于旧连接,必须一起收掉。
|
|
1896
|
-
|
|
1973
|
+
排队任务按**环境**分别落库(见 QUEUE_KEY):旧环境那批留在原地,切回来再接着跑 ——
|
|
1974
|
+
队列绝不能被带到另一台环境上跑,那边的会话结束事件这边根本收不到。 */
|
|
1897
1975
|
/* 换环境 = 换一台桌面:会话 / 需求 / 我的设置全都属于那台机器。
|
|
1898
1976
|
做法分两步:① 把当前这台的本机状态快照进它的槽,再把目标环境的槽写回来;
|
|
1899
1977
|
② 记住「停在哪个环境」并整页重载 —— 重载后内存态从零初始化,按目标环境的
|
|
@@ -1901,6 +1979,7 @@ button.setrow{font-family:inherit}
|
|
|
1901
1979
|
⚠️ 不在这里手写「清哪些变量」:那种清单永远会漏(漏一个就带着上一台的数据)。 */
|
|
1902
1980
|
function switchEnv(c) {
|
|
1903
1981
|
if (!c) return;
|
|
1982
|
+
c = ensureEnvId(c) || c; // 先把目标环境的 eid 补上,下面取槽才是稳定键
|
|
1904
1983
|
if (c.url === connUrlNow() && String(c.token || '') === String(TOKEN || '')) {
|
|
1905
1984
|
toast('已在「' + (connLabel(c) || '当前环境') + '」');
|
|
1906
1985
|
return;
|
|
@@ -2057,9 +2136,12 @@ button.setrow{font-family:inherit}
|
|
|
2057
2136
|
}
|
|
2058
2137
|
function persist(t) {
|
|
2059
2138
|
var wasActive = !!(conn && conn.url === connUrlNow() && String(conn.token || '') === String(TOKEN || ''));
|
|
2139
|
+
/* 改地址/改令牌 = 同一台环境换了入口:eid 带过去,队列与配置槽跟着走。 */
|
|
2140
|
+
if (conn && conn.eid) t.eid = conn.eid;
|
|
2060
2141
|
var next = upsertConn(t);
|
|
2061
|
-
/* 改了地址/令牌 = 换了一条记录:旧的那条要删掉,别留一条连不上的僵尸。
|
|
2062
|
-
|
|
2142
|
+
/* 改了地址/令牌 = 换了一条记录:旧的那条要删掉,别留一条连不上的僵尸。
|
|
2143
|
+
队列已经跟着 eid 走了,这里不能再按旧记录清一遍。 */
|
|
2144
|
+
if (conn && conn.id !== next.id) removeConn(conn.id, true);
|
|
2063
2145
|
/* 名称允许清空,也允许改:upsertConn 会沿用旧名,这里按输入框的最终值覆盖。 */
|
|
2064
2146
|
var list = loadConns();
|
|
2065
2147
|
for (var i = 0; i < list.length; i++) {
|
|
@@ -2886,12 +2968,12 @@ button.setrow{font-family:inherit}
|
|
|
2886
2968
|
TEAM.fresh = false;
|
|
2887
2969
|
HOSTING.fresh = false;
|
|
2888
2970
|
try {
|
|
2889
|
-
var transport = window.MagicTransportHttp({ base: BASE, token: TOKEN, client: 'mobile' });
|
|
2971
|
+
var transport = wrapTransportForJobs(window.MagicTransportHttp({ base: BASE, token: TOKEN, client: 'mobile' }));
|
|
2890
2972
|
conv = window.MagicConversation.create({
|
|
2891
2973
|
el: convEl, transport: transport, titleEl: titleEl, workspace: WS,
|
|
2892
2974
|
engine: RUN_PREF.engine, model: RUN_PREF.model,
|
|
2893
2975
|
/* 建议功能默认关闭:只有「我的 → 建议引擎」里显式选了引擎才传非空值;
|
|
2894
|
-
模型为空 =
|
|
2976
|
+
模型为空 = 引擎默认(建议生成对 llm 用 no-think 变体作默认值)。 */
|
|
2895
2977
|
suggestEngine: SUGGEST.engine, suggestModel: SUGGEST.model,
|
|
2896
2978
|
showPermission: false, showAttach: true, beforeAsk: gateAsk,
|
|
2897
2979
|
onInterrupt: cancelWaitingAsk,
|
|
@@ -3012,6 +3094,15 @@ button.setrow{font-family:inherit}
|
|
|
3012
3094
|
}
|
|
3013
3095
|
|
|
3014
3096
|
function connect(target) {
|
|
3097
|
+
if (!target) return;
|
|
3098
|
+
/* 已经连着另一台:换环境必须走 switchEnv(整页重载)。
|
|
3099
|
+
就地改 BASE/TOKEN 会把内存里的队列、串行门、会话组件留在旧环境,
|
|
3100
|
+
而 saveQueue 又会把旧 PENDING 行按新环境重标后发出去 —— 这正是跨环境串队的入口。 */
|
|
3101
|
+
if ((BASE || TOKEN) && (String(target.url || '') !== String(connUrlNow()) ||
|
|
3102
|
+
String(target.token || '') !== String(TOKEN || ''))) {
|
|
3103
|
+
switchEnv(target);
|
|
3104
|
+
return;
|
|
3105
|
+
}
|
|
3015
3106
|
BASE = target.url || '';
|
|
3016
3107
|
TOKEN = target.token || '';
|
|
3017
3108
|
CONN_SEQ++; // 上一台桌面还在飞的请求:回来时直接丢弃(见 loadSessions)
|
|
@@ -3036,6 +3127,7 @@ button.setrow{font-family:inherit}
|
|
|
3036
3127
|
ACTIVE_ID = '';
|
|
3037
3128
|
try { localStorage.removeItem(ACTIVE_ENV_KEY); } catch (e) {} // 主动断开 = 下次回启动页
|
|
3038
3129
|
SERVER_VERSION = ''; // 版本号属于「那一条连接」:断开就清掉,避免下次连到别的机器还显示旧值
|
|
3130
|
+
SERVER_JOBS = false; // 作业队列能力也属于那一条连接
|
|
3039
3131
|
/* 排队任务**不跟着断连消失**(它们已经落库):只清内存里的等待态,下次连上由
|
|
3040
3132
|
restoreQueue 从磁盘接回来。串行门的 FIFO 也一并重置 —— 里面的 tail 是挂在一条
|
|
3041
3133
|
已废弃页面上的 promise,留着会让新连接的第一条永远排在它后面。 */
|
|
@@ -3114,6 +3206,7 @@ button.setrow{font-family:inherit}
|
|
|
3114
3206
|
TITLES = a[1] || {};
|
|
3115
3207
|
DW = (a[2] && a[2].defaultWorkspace) || '';
|
|
3116
3208
|
DW_READY = true;
|
|
3209
|
+
SERVER_JOBS = !!(a[2] && a[2].jobs);
|
|
3117
3210
|
/* 环境配置以桌面端为准:连上后拉一次(见 pullPrefs)。只拉一次,
|
|
3118
3211
|
后面本地的改动由 schedulePushPrefs 推回去。 */
|
|
3119
3212
|
if (!PREFS_PULLED) pullPrefs();
|
|
@@ -3177,6 +3270,10 @@ button.setrow{font-family:inherit}
|
|
|
3177
3270
|
var TITLES = {}; // 服务端标题覆盖表(GET /desk/titles),重命名用
|
|
3178
3271
|
var DW = ''; // 桌面默认工作空间(GET /desk/health 的 defaultWorkspace = core 的 cwd)
|
|
3179
3272
|
var DW_READY = false; // DW 是否已从 health 落定(空间键归一依赖它,见 restoreQueue)
|
|
3273
|
+
/* 服务端作业队列是否可用(GET /desk/health 的 jobs 字段,2026-10-23)。
|
|
3274
|
+
有它:排队与互斥在服务端(多客户端同一 workspace 串行 + request_id 幂等);
|
|
3275
|
+
没有:退回本地串行门(老服务端,只有本设备自己排)。 */
|
|
3276
|
+
var SERVER_JOBS = false;
|
|
3180
3277
|
var SPACES_ROOT = ''; // 工作空间根(health.workspacesRoot)
|
|
3181
3278
|
var ROOT_DIRS = []; // 根目录下 magic-* 目录(GET /desk/dirs)
|
|
3182
3279
|
var CUSTOM_SPACES = [];
|
|
@@ -3361,18 +3458,19 @@ button.setrow{font-family:inherit}
|
|
|
3361
3458
|
rowMenuEl.style.top = Math.max(12, Math.min(r.top, window.innerHeight - mh - 12)) + 'px';
|
|
3362
3459
|
rowMenuEl.style.left = Math.max(12, Math.min(r.left + 10, window.innerWidth - mw - 12)) + 'px';
|
|
3363
3460
|
}
|
|
3364
|
-
/* 重命名:写服务端标题表(PUT /desk/session/{id}/title),成功后就地更新并重绘。
|
|
3461
|
+
/* 重命名:写服务端标题表(PUT /desk/session/{id}/title),成功后就地更新并重绘。
|
|
3462
|
+
输入走抽屉里的真输入框(openTextInputSheet)—— window.prompt 在悬浮球 / WebView 里
|
|
3463
|
+
点了没反应,与「自定义路径」是同一处坑(2026-10-23)。 */
|
|
3365
3464
|
function renameSession(s) {
|
|
3366
3465
|
var id = sidOf(s);
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
.catch(function (e) { window.alert('重命名失败:' + (e && e.message ? e.message : e)); });
|
|
3466
|
+
openTextInputSheet({ title: '重命名会话', value: titleOf(s), emptyHint: '名称不能为空' }, function (name) {
|
|
3467
|
+
var hdr = { 'Content-Type': 'application/json' };
|
|
3468
|
+
if (TOKEN) hdr['X-Magic-Token'] = TOKEN;
|
|
3469
|
+
fetch(BASE + '/desk/session/' + encodeURIComponent(id) + '/title', { method: 'PUT', headers: hdr, body: JSON.stringify({ title: name }) })
|
|
3470
|
+
.then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
|
|
3471
|
+
.then(function () { TITLES[id] = name; renderSessions(LAST_SESSIONS); })
|
|
3472
|
+
.catch(function (e) { toast('重命名失败:' + (e && e.message ? e.message : e)); });
|
|
3473
|
+
});
|
|
3376
3474
|
}
|
|
3377
3475
|
/* 删除一棵会话树:删主会话时,挂在它下面的**子任务会话**也要从本地列表一起消失。
|
|
3378
3476
|
服务端(session.Delete)已经级联删了登记记录 / 日志 / 标题 / 状态;这里只是不让
|
|
@@ -4028,6 +4126,38 @@ button.setrow{font-family:inherit}
|
|
|
4028
4126
|
else { b.style.cursor = 'default'; }
|
|
4029
4127
|
body.appendChild(b);
|
|
4030
4128
|
}
|
|
4129
|
+
/* 单行文本输入抽屉(重命名 / 自定义路径共用)。
|
|
4130
|
+
⚠️ **不用 window.prompt**:悬浮球(Electron 小窗)与 Android WebView 都没实现它,
|
|
4131
|
+
调用只会静默返回 null —— 表现就是「点击无效」且抽屉原样不动(2026-10-23 报障)。
|
|
4132
|
+
这里换成抽屉里的真输入框,样式沿用启动页那套 .field / .btn;空值给红字提示,
|
|
4133
|
+
不再静默 return(否则和上面的 bug 长得一模一样)。 */
|
|
4134
|
+
function openTextInputSheet(opt, onOk) {
|
|
4135
|
+
openHostSheet(opt.title || '', function (body) {
|
|
4136
|
+
var f = hEl('div', 'field');
|
|
4137
|
+
var inp = document.createElement('input');
|
|
4138
|
+
inp.type = 'text';
|
|
4139
|
+
inp.value = opt.value || '';
|
|
4140
|
+
if (opt.placeholder) inp.placeholder = opt.placeholder;
|
|
4141
|
+
inp.setAttribute('autocapitalize', 'off');
|
|
4142
|
+
inp.setAttribute('autocorrect', 'off');
|
|
4143
|
+
inp.setAttribute('spellcheck', 'false');
|
|
4144
|
+
f.appendChild(inp);
|
|
4145
|
+
body.appendChild(f);
|
|
4146
|
+
var err = hEl('div', 'field__err'); err.hidden = true;
|
|
4147
|
+
body.appendChild(err);
|
|
4148
|
+
var ok = hEl('button', 'btn btn--filled', '确定'); ok.type = 'button';
|
|
4149
|
+
body.appendChild(ok);
|
|
4150
|
+
function commit() {
|
|
4151
|
+
var v = String(inp.value || '').trim();
|
|
4152
|
+
if (!v) { err.textContent = opt.emptyHint || '不能为空'; err.hidden = false; inp.focus(); return; }
|
|
4153
|
+
closeHostSheet();
|
|
4154
|
+
if (onOk) onOk(v);
|
|
4155
|
+
}
|
|
4156
|
+
ok.onclick = commit;
|
|
4157
|
+
inp.onkeydown = function (e) { if (e.key === 'Enter') { e.preventDefault(); commit(); } };
|
|
4158
|
+
setTimeout(function () { try { inp.focus(); inp.select(); } catch (e) {} }, 30);
|
|
4159
|
+
});
|
|
4160
|
+
}
|
|
4031
4161
|
function applyWs() { if (conv && conv.setWorkspace) conv.setWorkspace(WS); }
|
|
4032
4162
|
/* 工作空间值的显示:WS 为空 = 默认(不传 -w,core 用调用方 cwd)。
|
|
4033
4163
|
⚠️ **绝不显示目录名**(2026-10-21 用户:「默认就是要不属于任何空间目录」)。
|
|
@@ -4138,7 +4268,7 @@ button.setrow{font-family:inherit}
|
|
|
4138
4268
|
组件被销毁重建后,它的 owner='conv' 条目还留在 PENDING 里,由
|
|
4139
4269
|
adoptPendingFor 接回新实例 —— 状态只能落在条目上,不能只存在组件内存。 */
|
|
4140
4270
|
owner: info.owner || 'gate',
|
|
4141
|
-
hide: !!hide, dev: devKey(), at: Date.now() };
|
|
4271
|
+
hide: !!hide, env: queueEnvKey(), dev: devKey(), at: Date.now() };
|
|
4142
4272
|
PENDING.push(p); saveQueue();
|
|
4143
4273
|
if (!hide) renderSessions(LAST_SESSIONS);
|
|
4144
4274
|
return p;
|
|
@@ -4184,14 +4314,88 @@ button.setrow{font-family:inherit}
|
|
|
4184
4314
|
delete QPEND[id];
|
|
4185
4315
|
cancelPending(pid); // 标取消 + 从 PENDING 与磁盘上一并删掉
|
|
4186
4316
|
}
|
|
4187
|
-
/*
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4317
|
+
/* 服务端已受理(open 到达):本地排队行交给服务端,删掉并清干净映射。 */
|
|
4318
|
+
function clearPendingRow(id) {
|
|
4319
|
+
if (!id) return;
|
|
4320
|
+
Object.keys(QPEND).forEach(function (k) { if (QPEND[k] === id) delete QPEND[k]; });
|
|
4321
|
+
ASK_WAITING = ASK_WAITING.filter(function (x) { return !x || x.id !== id; });
|
|
4322
|
+
dropPending(id);
|
|
4323
|
+
}
|
|
4324
|
+
/* ── 把服务端作业队列接进对话组件(2026-10-23)──
|
|
4325
|
+
· 每个 ask 都带 request_id:与本地排队行同一个 id,断线/重启/换标签页重发也只跑一次;
|
|
4326
|
+
· 排队时服务端发 `event: queued`(老服务端没有,忽略)→ 把本地行翻成可见的「等待中」;
|
|
4327
|
+
· `open` 到达 = 服务端已受理并开跑 → 删本地行。此后刷新页面也不会再重发。
|
|
4328
|
+
老服务端(没有 jobs 能力)也照样带这些字段 —— 多出来的键会被忽略,行为不变。 */
|
|
4329
|
+
function wrapTransportForJobs(t) {
|
|
4330
|
+
if (!t || t.__jobsWrapped) return t;
|
|
4331
|
+
t.__jobsWrapped = true;
|
|
4332
|
+
var baseAsk = t.ask;
|
|
4333
|
+
t.ask = function (payload, h) {
|
|
4334
|
+
var p = payload || {};
|
|
4335
|
+
var pend = pendingOf(p.prompt);
|
|
4336
|
+
var rid = (pend && pend.id) || ('q' + Date.now() + '-' + Math.floor(Math.random() * 100000));
|
|
4337
|
+
p.request_id = rid;
|
|
4338
|
+
/* 串行模式由用户选;并行模式显式传 false,服务端仍会套全局并发上限。 */
|
|
4339
|
+
if (p.serial === undefined) p.serial = (TASK_MODE === 'serial');
|
|
4340
|
+
var hs = h || {};
|
|
4341
|
+
var onOpen = hs.onOpen, onEvent = hs.onEvent, onEnd = hs.onEnd, onErr = hs.onError;
|
|
4342
|
+
if (pend && !pend.gone) {
|
|
4343
|
+
pend.dispatching = true;
|
|
4344
|
+
pend.requestId = rid;
|
|
4345
|
+
if (ASK_WAITING.indexOf(pend) < 0) ASK_WAITING.push(pend);
|
|
4346
|
+
}
|
|
4347
|
+
hs.onEvent = function (ev) {
|
|
4348
|
+
if (ev && ev.state === 'queued' && pend && !pend.gone) {
|
|
4349
|
+
if (ev.job) pend.job = ev.job;
|
|
4350
|
+
pend.pos = ev.position || 0;
|
|
4351
|
+
if (pend.hide) { pend.hide = false; renderSessions(LAST_SESSIONS); }
|
|
4352
|
+
if (typeof pend.queuedCb === 'function') { try { pend.queuedCb(true); } catch (e) {} }
|
|
4353
|
+
}
|
|
4354
|
+
if (onEvent) { try { onEvent(ev); } catch (e) {} }
|
|
4355
|
+
};
|
|
4356
|
+
hs.onOpen = function (id) {
|
|
4357
|
+
if (pend && !pend.gone) {
|
|
4358
|
+
if (typeof pend.queuedCb === 'function') { try { pend.queuedCb(false); } catch (e) {} }
|
|
4359
|
+
clearPendingRow(pend.id);
|
|
4360
|
+
}
|
|
4361
|
+
if (onOpen) { try { onOpen(id); } catch (e) {} }
|
|
4362
|
+
};
|
|
4363
|
+
hs.onEnd = function (code, ev) {
|
|
4364
|
+
if (pend && !pend.gone) {
|
|
4365
|
+
if (typeof pend.queuedCb === 'function') { try { pend.queuedCb(false); } catch (e) {} }
|
|
4366
|
+
ASK_WAITING = ASK_WAITING.filter(function (x) { return x !== pend; });
|
|
4367
|
+
}
|
|
4368
|
+
if (onEnd) { try { onEnd(code, ev); } catch (e) {} }
|
|
4369
|
+
};
|
|
4370
|
+
hs.onError = function (e) {
|
|
4371
|
+
/* 发不出去:本地行留着(下次连上 / 重开 App 由 restoreQueue 重发,request_id 不变)。 */
|
|
4372
|
+
if (pend && !pend.gone) { pend.dispatching = false; }
|
|
4373
|
+
if (onErr) { try { onErr(e); } catch (e2) {} }
|
|
4374
|
+
};
|
|
4375
|
+
return baseAsk.call(t, p, hs);
|
|
4376
|
+
};
|
|
4377
|
+
return t;
|
|
4378
|
+
}
|
|
4379
|
+
/* ── 队列落库(按**环境**分区)──
|
|
4380
|
+
分区键用稳定 eid(见 ensureEnvId),与 ENV_SCOPE 同一口径;不是令牌也不是地址:
|
|
4381
|
+
· 令牌会变 —— App 每次启动会换注入令牌,按令牌分桶等于每次重启换一个新桶;
|
|
4382
|
+
· 令牌还会**重复** —— 多台桌面用同一枚显式 `--token` 部署时,按令牌分桶就把
|
|
4383
|
+
A 的队列当成 B 的:切过去直接发到 B,而「A 的会话结束」事件还在 A,这边等不到;
|
|
4384
|
+
· 地址也会变/会被复用 —— 按 host 分桶要么变孤儿,要么把旧任务发给后来占用
|
|
4385
|
+
同一地址的另一台服务器。
|
|
4386
|
+
loadQueue 只认当前环境那批,别的**留在原地不动**(切回去时接着跑)——
|
|
4192
4387
|
所以这里是读改写,不是整体覆盖。 */
|
|
4193
4388
|
var QUEUE_KEY = 'magic.serial.queue.v1';
|
|
4389
|
+
function queueEnvKey() { return envScopeIdOf(activeConn()) || String(BASE || ''); }
|
|
4390
|
+
/* 兼容旧行(没有 env、只有令牌派生的 dev):只有令牌正好是当前这台时才认领 ——
|
|
4391
|
+
认领后下一次写盘会补上 env。其余环境的行原样留着,等切回去再接。 */
|
|
4194
4392
|
function devKey() { return String(TOKEN || '').slice(0, 12) || String(BASE || ''); }
|
|
4393
|
+
function rowEnv(p) {
|
|
4394
|
+
if (!p) return '';
|
|
4395
|
+
if (p.env) return String(p.env);
|
|
4396
|
+
if (p.dev && p.dev === devKey()) return queueEnvKey();
|
|
4397
|
+
return '';
|
|
4398
|
+
}
|
|
4195
4399
|
function readQueueAll() {
|
|
4196
4400
|
try {
|
|
4197
4401
|
var a = JSON.parse(localStorage.getItem(QUEUE_KEY) || '[]');
|
|
@@ -4199,7 +4403,11 @@ button.setrow{font-family:inherit}
|
|
|
4199
4403
|
} catch (e) { return []; }
|
|
4200
4404
|
}
|
|
4201
4405
|
function saveQueue() {
|
|
4202
|
-
|
|
4406
|
+
/* 没连上就不知道自己是哪台环境:此时宁可不写,也不能把「认不出归属」的行
|
|
4407
|
+
当成自己的冲掉(那正是旧实现丢别的环境队列的方式)。 */
|
|
4408
|
+
var mine = queueEnvKey();
|
|
4409
|
+
if (!mine) return;
|
|
4410
|
+
var others = readQueueAll().filter(function (p) { return rowEnv(p) !== mine; });
|
|
4203
4411
|
/* 只落**持久**字段:hide / gone 是这一次页面会话里的显示态,存下去会让「关掉 App 时
|
|
4204
4412
|
正好还没到 300ms」的那条永远隐藏 —— 队列里有它、列表上却看不见,最难查的一种。
|
|
4205
4413
|
id 保留:恢复时据此与内存里已有的条目去重。 */
|
|
@@ -4212,13 +4420,41 @@ button.setrow{font-family:inherit}
|
|
|
4212
4420
|
/* owner 刻意不落盘:重启后所有残留条目都由 restoreQueue 交给宿主
|
|
4213
4421
|
(enqueueDispatch),语义与 'host' 一致 —— 存下来反而会让「上次是
|
|
4214
4422
|
组件排队」的那条永远等不到组件接管。 */
|
|
4215
|
-
dev: p.dev, at: p.at };
|
|
4423
|
+
env: mine, dev: p.dev, at: p.at };
|
|
4216
4424
|
});
|
|
4217
4425
|
try { localStorage.setItem(QUEUE_KEY, JSON.stringify(others.concat(mineRows))); } catch (e) {}
|
|
4426
|
+
queueBroadcast();
|
|
4218
4427
|
}
|
|
4219
4428
|
function loadQueue() {
|
|
4220
|
-
var mine =
|
|
4221
|
-
|
|
4429
|
+
var mine = queueEnvKey();
|
|
4430
|
+
if (!mine) return [];
|
|
4431
|
+
return readQueueAll().filter(function (p) { return p && rowEnv(p) === mine && p.prompt; });
|
|
4432
|
+
}
|
|
4433
|
+
/* 同源多标签页:队列变了就互相知会一声(真正的一致性由服务端负责,
|
|
4434
|
+
这里只是让列表不用等下一次轮询才更新)。 */
|
|
4435
|
+
var QUEUE_BC = null;
|
|
4436
|
+
try { if (window.BroadcastChannel) QUEUE_BC = new BroadcastChannel('magic.queue'); } catch (e) { QUEUE_BC = null; }
|
|
4437
|
+
function queueBroadcast() {
|
|
4438
|
+
if (!QUEUE_BC) return;
|
|
4439
|
+
try { QUEUE_BC.postMessage({ env: queueEnvKey(), at: Date.now() }); } catch (e) {}
|
|
4440
|
+
}
|
|
4441
|
+
function syncPendingFromDisk() {
|
|
4442
|
+
var disk = loadQueue();
|
|
4443
|
+
var keep = [];
|
|
4444
|
+
PENDING.forEach(function (p) { if (p && (p.dispatching || p.owner === 'conv')) keep.push(p); });
|
|
4445
|
+
var ids = {};
|
|
4446
|
+
keep.forEach(function (p) { ids[p.id] = 1; });
|
|
4447
|
+
PENDING = disk.filter(function (p) { return !ids[p.id]; }).concat(keep);
|
|
4448
|
+
renderSessions(LAST_SESSIONS);
|
|
4449
|
+
}
|
|
4450
|
+
if (QUEUE_BC) {
|
|
4451
|
+
try {
|
|
4452
|
+
QUEUE_BC.onmessage = function (ev) {
|
|
4453
|
+
var d = ev && ev.data;
|
|
4454
|
+
if (!d || d.env !== queueEnvKey()) return;
|
|
4455
|
+
try { syncPendingFromDisk(); } catch (e) {}
|
|
4456
|
+
};
|
|
4457
|
+
} catch (e) {}
|
|
4222
4458
|
}
|
|
4223
4459
|
/* 某条排队任务被用户放弃(划掉 / 删除):磁盘上也要删,否则下次启动它又冒出来。 */
|
|
4224
4460
|
function purgeQueue(id) {
|
|
@@ -4226,6 +4462,7 @@ button.setrow{font-family:inherit}
|
|
|
4226
4462
|
var rest = readQueueAll().filter(function (p) { return p.id !== id; });
|
|
4227
4463
|
localStorage.setItem(QUEUE_KEY, JSON.stringify(rest));
|
|
4228
4464
|
} catch (e) {}
|
|
4465
|
+
queueBroadcast();
|
|
4229
4466
|
}
|
|
4230
4467
|
/* 占位何时能还:等登记表里该空间真的出现这一条(= 它的 core 已落盘)。
|
|
4231
4468
|
判据不能只看 `running`:几十毫秒就收工的任务,落盘那一刻起就不是 running 了,
|
|
@@ -4346,6 +4583,15 @@ button.setrow{font-family:inherit}
|
|
|
4346
4583
|
function gateAsk(info) {
|
|
4347
4584
|
if (TASK_MODE !== 'serial' || !info) return Promise.resolve(true);
|
|
4348
4585
|
var ws = wsKey(info.workspace);
|
|
4586
|
+
/* 服务端有作业队列:真正的「谁先跑」由服务端裁决(多客户端同一 workspace 串行),
|
|
4587
|
+
宿主这里只把这条登记成本地行(关 App 后能恢复),然后立刻放行 ——
|
|
4588
|
+
组件照常发 /desk/ask,服务端会先排队再开跑,排队时推 `queued` 事件回来。 */
|
|
4589
|
+
if (SERVER_JOBS) {
|
|
4590
|
+
var pend = addPending(ws, info, true);
|
|
4591
|
+
ASK_WAITING.push(pend);
|
|
4592
|
+
pend.queuedCb = (typeof info.queued === 'function') ? info.queued : null;
|
|
4593
|
+
return Promise.resolve(true);
|
|
4594
|
+
}
|
|
4349
4595
|
var q = serialQ(ws);
|
|
4350
4596
|
/* 落库是同步写,很便宜;而「点了发送、网还没回、用户退出了」正是丢任务的高危窗口,
|
|
4351
4597
|
所以不等任何延时,先进队列进磁盘。 */
|
|
@@ -4405,6 +4651,19 @@ button.setrow{font-family:inherit}
|
|
|
4405
4651
|
if (!hit) return false;
|
|
4406
4652
|
hit.cancelled = true;
|
|
4407
4653
|
hit.gone = true;
|
|
4654
|
+
/* 服务端作业队列:排队中要把它从服务端撤掉,否则这边删了行、那边照样会开跑。
|
|
4655
|
+
有 job id 走 job 路由;还没拿到(queued 事件未到)就按 request_id 撤。 */
|
|
4656
|
+
if (SERVER_JOBS && (hit.job || hit.requestId || hit.id)) {
|
|
4657
|
+
try {
|
|
4658
|
+
var hdr = TOKEN ? { 'X-Magic-Token': TOKEN, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' };
|
|
4659
|
+
if (hit.job) {
|
|
4660
|
+
fetch(BASE + '/desk/jobs/' + encodeURIComponent(hit.job) + '/cancel', { method: 'POST', headers: hdr }).catch(function () {});
|
|
4661
|
+
} else {
|
|
4662
|
+
fetch(BASE + '/desk/jobs/cancel', { method: 'POST', headers: hdr,
|
|
4663
|
+
body: JSON.stringify({ request_id: hit.requestId || hit.id }) }).catch(function () {});
|
|
4664
|
+
}
|
|
4665
|
+
} catch (e) {}
|
|
4666
|
+
}
|
|
4408
4667
|
dropPending(id);
|
|
4409
4668
|
renderSessions(LAST_SESSIONS);
|
|
4410
4669
|
return true;
|
|
@@ -4454,7 +4713,62 @@ button.setrow{font-family:inherit}
|
|
|
4454
4713
|
list.forEach(function (p) { p.owner = 'host'; });
|
|
4455
4714
|
PENDING = list;
|
|
4456
4715
|
renderSessions(LAST_SESSIONS);
|
|
4457
|
-
|
|
4716
|
+
if (!SERVER_JOBS) {
|
|
4717
|
+
list.forEach(function (p) { enqueueDispatch(p); });
|
|
4718
|
+
return;
|
|
4719
|
+
}
|
|
4720
|
+
/* 服务端作业队列:直接按 request_id 重新提交(幂等),提交成功才算交给服务端。
|
|
4721
|
+
用 Web Locks 串行化「同源多标签页同时恢复」,避免两边各发一遍
|
|
4722
|
+
—— 即使没有 Web Locks,服务端的 request_id 幂等也能兜住。 */
|
|
4723
|
+
var submitAll = function () {
|
|
4724
|
+
list.forEach(function (p) { submitQueuedJob(p); });
|
|
4725
|
+
};
|
|
4726
|
+
try {
|
|
4727
|
+
if (navigator.locks && navigator.locks.request) {
|
|
4728
|
+
navigator.locks.request('magic.queue.restore.' + queueEnvKey(), submitAll);
|
|
4729
|
+
} else {
|
|
4730
|
+
submitAll();
|
|
4731
|
+
}
|
|
4732
|
+
} catch (e) { submitAll(); }
|
|
4733
|
+
}
|
|
4734
|
+
/* 服务端作业提交:受理即删本地行(服务端接管,之后刷新/重开 App 都不会重发)。
|
|
4735
|
+
失败则保留并标「未能启动」,下次连上由 restoreQueue 再试(request_id 不变 → 幂等)。 */
|
|
4736
|
+
function submitQueuedJob(p) {
|
|
4737
|
+
if (!connected()) return Promise.resolve(false);
|
|
4738
|
+
var body = { engine: p.engine, prompt: p.prompt, request_id: p.id, serial: (TASK_MODE === 'serial') };
|
|
4739
|
+
if (p.model) body.model = p.model;
|
|
4740
|
+
if (p.w) body.workspace = p.w;
|
|
4741
|
+
if (p.session) body.session = p.session;
|
|
4742
|
+
if (p.permission) body.permission = p.permission;
|
|
4743
|
+
if (p.team) body.team = p.team;
|
|
4744
|
+
if (p.hosted) body.hosted = true;
|
|
4745
|
+
if (p.attachments && p.attachments.length) body.attachments = p.attachments;
|
|
4746
|
+
var hdr = { 'Content-Type': 'application/json' };
|
|
4747
|
+
if (TOKEN) hdr['X-Magic-Token'] = TOKEN;
|
|
4748
|
+
return fetch(BASE + '/desk/jobs', { method: 'POST', headers: hdr, body: JSON.stringify(body) })
|
|
4749
|
+
.then(function (r) {
|
|
4750
|
+
if (!r.ok) {
|
|
4751
|
+
return r.text().then(function (txt) {
|
|
4752
|
+
var why = '';
|
|
4753
|
+
try { var j = JSON.parse(txt); why = (j && j.error) || ''; } catch (e) {}
|
|
4754
|
+
throw new Error(why || ('HTTP ' + r.status));
|
|
4755
|
+
});
|
|
4756
|
+
}
|
|
4757
|
+
return r.json();
|
|
4758
|
+
})
|
|
4759
|
+
.then(function (out) {
|
|
4760
|
+
if (!out || !out.job) return false;
|
|
4761
|
+
if (p.stalled) { p.stalled = false; p.stalledReason = ''; }
|
|
4762
|
+
dropPending(p.id);
|
|
4763
|
+
if (typeof loadSessions === 'function') loadSessions(true);
|
|
4764
|
+
return true;
|
|
4765
|
+
})
|
|
4766
|
+
.catch(function (e) {
|
|
4767
|
+
p.stalled = true;
|
|
4768
|
+
p.stalledReason = (e && e.message) || '网络错误';
|
|
4769
|
+
renderSessions(LAST_SESSIONS);
|
|
4770
|
+
return false;
|
|
4771
|
+
});
|
|
4458
4772
|
}
|
|
4459
4773
|
function enqueueDispatch(p) {
|
|
4460
4774
|
var ws = wsKey(p.w);
|
|
@@ -4761,13 +5075,19 @@ button.setrow{font-family:inherit}
|
|
|
4761
5075
|
var isCustom = CUSTOM_SPACES.indexOf(w) >= 0;
|
|
4762
5076
|
hRow(body, basename(w), isCustom ? '自定义' : null, function () { WS = w; saveWs(); applyWs(); closeHostSheet(); paintRunSettings(); }, false, WS === w);
|
|
4763
5077
|
});
|
|
4764
|
-
hRow(body, '自定义路径…', '+',
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
5078
|
+
hRow(body, '自定义路径…', '+', openCustomPathSheet);
|
|
5079
|
+
});
|
|
5080
|
+
}
|
|
5081
|
+
/* 自定义路径:复用单行输入抽屉(见 openTextInputSheet 上方的 prompt 说明)。 */
|
|
5082
|
+
function openCustomPathSheet() {
|
|
5083
|
+
openTextInputSheet({
|
|
5084
|
+
title: '自定义路径',
|
|
5085
|
+
value: SPACES_ROOT || DW || '',
|
|
5086
|
+
placeholder: '/绝对路径',
|
|
5087
|
+
emptyHint: '请输入工作空间的绝对路径'
|
|
5088
|
+
}, function (p) {
|
|
5089
|
+
if (CUSTOM_SPACES.indexOf(p) < 0) { CUSTOM_SPACES.push(p); saveSpaces(); }
|
|
5090
|
+
WS = p; saveWs(); applyWs(); loadSessions(); paintRunSettings();
|
|
4771
5091
|
});
|
|
4772
5092
|
}
|
|
4773
5093
|
|
|
@@ -5186,9 +5506,9 @@ button.setrow{font-family:inherit}
|
|
|
5186
5506
|
return el;
|
|
5187
5507
|
}
|
|
5188
5508
|
/* 「我的 → 建议引擎」:单列,首项「不开启」。不展示模型 —— 建议是纯文本短任务,
|
|
5189
|
-
|
|
5509
|
+
选模型只会让界面更复杂;建议生成对 llm 会用不思考的变体作默认值。 */
|
|
5190
5510
|
/* 「我的 → 建议引擎 / 模型」:与默认引擎同一个二列抽屉(左引擎 · 右模型)。
|
|
5191
|
-
首项「不开启」;点左列只浏览、点右列才落定。llm
|
|
5511
|
+
首项「不开启」;点左列只浏览、点右列才落定。llm 放最前,模型为空时建议生成用 no-think 变体作默认值。 */
|
|
5192
5512
|
function openSuggestEnginePicker() {
|
|
5193
5513
|
openHostSheet('建议引擎 / 模型', function (body) {
|
|
5194
5514
|
body.innerHTML = '';
|
|
@@ -5218,12 +5538,12 @@ button.setrow{font-family:inherit}
|
|
|
5218
5538
|
pick: pickSuggestPref,
|
|
5219
5539
|
offLabel: '不开启',
|
|
5220
5540
|
offDesc: '不生成下一轮建议(默认)',
|
|
5221
|
-
modelDefaultDesc: '
|
|
5541
|
+
modelDefaultDesc: '不指定模型(建议生成用 no-think 变体)',
|
|
5222
5542
|
});
|
|
5223
5543
|
/* 建议生成一律不思考。各引擎都走官方关闭参数;某些模型路由不支持时
|
|
5224
5544
|
才在超时/报错后降级 llm,不预先吞掉用户的选择。 */
|
|
5225
5545
|
var nk = hEl('div', 'history__hint',
|
|
5226
|
-
'建议生成固定关闭思考:claude --thinking disabled、codex effort none、llm no-think
|
|
5546
|
+
'建议生成固定关闭思考:claude --thinking disabled、codex effort none、llm reasoning_effort=none(显式模型不换;空模型用 no-think 变体)、codebuddy alwaysThinkingEnabled=false、dsh reasoningEffort=off;引擎失败才降级 llm。');
|
|
5227
5547
|
nk.style.textAlign = 'left';
|
|
5228
5548
|
body.appendChild(nk);
|
|
5229
5549
|
}, function (e) {
|
|
@@ -5654,7 +5974,9 @@ button.setrow{font-family:inherit}
|
|
|
5654
5974
|
var tok = (lastUrl === injUrl && injectedToken)
|
|
5655
5975
|
? injectedToken : (sc.token || injectedToken);
|
|
5656
5976
|
if (!tok) break;
|
|
5657
|
-
BASE = sc.url; TOKEN = tok;
|
|
5977
|
+
BASE = sc.url; TOKEN = tok;
|
|
5978
|
+
sc = ensureEnvId(sc) || sc; // 老记录补 eid,并把按地址分区的旧队列/配置槽迁过来
|
|
5979
|
+
ACTIVE_ID = sc.id;
|
|
5658
5980
|
showHistory(sc.host || connHost(sc.url));
|
|
5659
5981
|
restoreQueue();
|
|
5660
5982
|
resumed = true;
|
|
@@ -5667,6 +5989,7 @@ button.setrow{font-family:inherit}
|
|
|
5667
5989
|
/* 把当前这条(壳注入的)连接落成环境,名字默认「公司远程」——
|
|
5668
5990
|
否则它只是一条改不了名、也留不住的「未命名」。 */
|
|
5669
5991
|
seedCurrentConn();
|
|
5992
|
+
ensureEnvId(activeConn()); // 种出来的这条也补 eid;seedoff 没落盘时按老键回退
|
|
5670
5993
|
showHistory(injectedBase ? injectedBase.replace(/^https?:\/\//, '') : '本机');
|
|
5671
5994
|
restoreQueue(); // App 重启:上次没轮到的排队任务接着排(见 restoreQueue)
|
|
5672
5995
|
} else if (!resumed) {
|
|
@@ -5677,6 +6000,7 @@ button.setrow{font-family:inherit}
|
|
|
5677
6000
|
all.sort(function (a, b) { return (b.at || 0) - (a.at || 0); });
|
|
5678
6001
|
var pick = all[0];
|
|
5679
6002
|
if (pick) {
|
|
6003
|
+
pick = ensureEnvId(pick) || pick;
|
|
5680
6004
|
BASE = pick.url; TOKEN = pick.token || ''; ACTIVE_ID = pick.id;
|
|
5681
6005
|
try { localStorage.setItem(ACTIVE_ENV_KEY, pick.url); } catch (e) {}
|
|
5682
6006
|
showHistory(pick.host || connHost(pick.url));
|