@nimoxie/dsh-ui-tweaks 0.2.0 → 0.2.1
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/DEVELOPMENT.md +31 -3
- package/LICENSE +21 -21
- package/README.md +120 -157
- package/cordis.patch.yml +7 -7
- package/lib/client.js +738 -119
- package/lib/window-helper.cs +195 -195
- package/lib/window-helper.ps1 +276 -276
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/* dsh-ui-tweaks browser half.
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
* 1. tablefix —— 宽表格越界修复(纯 CSS
|
|
3
|
+
* 五个模块各自 guard,任一失败都不得拖垮其它:
|
|
4
|
+
* 1. tablefix —— 宽表格越界修复(纯 CSS 覆盖)
|
|
5
5
|
* 2. scrollbar —— 对话区左侧自绘滚动条(从 dsh-web-shell 移植,选择器适配 0.2.x)
|
|
6
6
|
* 3. updates —— 插件更新检查面板(不含 dsh 本体;remote.pluginManager/pluginInventory)
|
|
7
|
+
* 4. anim —— 动画修复(忽略 prefers-reduced-motion,恢复界面动画)
|
|
8
|
+
* 5. changes —— 回合尾改动卡:日志聚合 + 模块 5b 数据适配层(喂数据给官方卡)
|
|
9
|
+
* host 半区(lib/index.js)另有两项:窗口记忆(模块 6)、立即重启路由(模块 7)。
|
|
7
10
|
*
|
|
8
11
|
* 装载机制:shell 的 window.__ModuleLoader__.load + factory(require);
|
|
9
12
|
* apply(ctx) 由客户端 cordis 上下文调用(同 dsh-model-organizer 的结构)。
|
|
@@ -14,7 +17,7 @@ window.__ModuleLoader__.load({
|
|
|
14
17
|
const module = { exports: {} };
|
|
15
18
|
const exports = module.exports;
|
|
16
19
|
|
|
17
|
-
const BUILD = "0.2.
|
|
20
|
+
const BUILD = "0.2.1";
|
|
18
21
|
|
|
19
22
|
/* 每个模块的注册各套一层 guard:失败只记 warn(座位/模块失败模式是「静默消失」,
|
|
20
23
|
必须主动吭声;但不能让一个模块的异常打断同文件里其它模块的 apply)。 */
|
|
@@ -354,6 +357,9 @@ window.__ModuleLoader__.load({
|
|
|
354
357
|
* 全部走官方 remote API,不自建 host 路由:
|
|
355
358
|
* remote.pluginManager.listBundles() → 已装插件包(name/version/installed)
|
|
356
359
|
* remote.pluginManager.installBundle(spec, {enabled, requestId}) → 更新
|
|
360
|
+
* (开着 HMR 的运行时里,安装**已装过**的包会被拒 already-installed —— 与版本无关;
|
|
361
|
+
* 此时退回"先 removeBundle 再 installBundle",见 installBundleEx)
|
|
362
|
+
* remote.pluginManager.removeBundle(包名) → 卸载(两步更新时用)
|
|
357
363
|
* remote.pluginManager.waitForInstall(requestId) → 安装进度轮询
|
|
358
364
|
* remote.pluginManager.cancelInstall(requestId) → 取消
|
|
359
365
|
* registry 比对从浏览器直连(registry.npmjs.org 带 CORS *)。
|
|
@@ -1403,7 +1409,8 @@ window.__ModuleLoader__.load({
|
|
|
1403
1409
|
"bundle-in-use": "插件正在使用中,无法替换",
|
|
1404
1410
|
"stale-approval": "构建脚本授权已失效,需重新授权",
|
|
1405
1411
|
"incompatible-version": "版本与当前 dsh 不兼容",
|
|
1406
|
-
"operation-error": "安装命令执行失败(pnpm 非零退出)"
|
|
1412
|
+
"operation-error": "安装命令执行失败(pnpm 非零退出)",
|
|
1413
|
+
"already-installed": "该插件已安装,DSH 不允许原地覆盖版本(已自动尝试「先卸载再安装」,仍未成功)"
|
|
1407
1414
|
};
|
|
1408
1415
|
/* failureText 与 coolingFromResult 都在文件上方的「纯函数区」里(错误码表作为
|
|
1409
1416
|
参数传入),这里只保留这张码表;旧的长文案实现已删除,避免两份逻辑漂移。 */
|
|
@@ -1441,6 +1448,105 @@ window.__ModuleLoader__.load({
|
|
|
1441
1448
|
setRow(name, { state: "update", why: "" });
|
|
1442
1449
|
}
|
|
1443
1450
|
|
|
1451
|
+
/* ---- 更新 = 「先卸载、再安装」(DSH 安装接口的硬约束)--------------------
|
|
1452
|
+
官方 installBundle 在**开着 HMR(热重载)的运行时**里会直接拒绝:
|
|
1453
|
+
`already-installed` —— 判据是"这个包名已经在 profile 依赖里",与版本无关
|
|
1454
|
+
(@deepseek-ai/dsh-plugin-manager/lib/index.js 的 installBundle:
|
|
1455
|
+
`if (ownerContext.get("hmr") !== void 0 && hasOwn(before, name)) throw`)。
|
|
1456
|
+
官方工具说明给出的正路就是"先 removeBundle 再 installBundle"。因此:
|
|
1457
|
+
① 先按老办法直接装 —— 没有 HMR 的运行时这一步就成功,且不用碰卸载;
|
|
1458
|
+
② 只有被 already-installed 拒绝时,才退回"卸载 → 安装"两步。
|
|
1459
|
+
返回最终那次 installBundle 的信封;onRemoved 在"确实卸载过"时回调(供失败回滚)。 */
|
|
1460
|
+
function isAlreadyInstalledResult(res) {
|
|
1461
|
+
if (!res) { return false; }
|
|
1462
|
+
/* 形状①:远程 API 的拒绝信封(web 端 / 未来版本) */
|
|
1463
|
+
if (res.ok === false) { return String((res.error && res.error.code) || "") === "already-installed"; }
|
|
1464
|
+
/* 形状②:桌面端进程内管理器把它报成「应用层失败」—— true 信封里 value.application === "failed"
|
|
1465
|
+
且 value.error.code 就是那个码(failureText 正是这么取码的,实测就是这条路径) */
|
|
1466
|
+
var st = res.value;
|
|
1467
|
+
return !!(st && st.application === "failed" && String((st.error && st.error.code) || "") === "already-installed");
|
|
1468
|
+
}
|
|
1469
|
+
/* 结果信封到底是成功还是失败?**不能只看 ok**:桌面端进程内管理器把失败包成
|
|
1470
|
+
`{ok:true, value:{application:"failed", error:{code}}}`(0.2.1 第一版就栽在这,
|
|
1471
|
+
把"卸载被拒"当成卸载成功,于是"已回滚"是假话、"仍需卸载"这条真正的原因被吞掉)。
|
|
1472
|
+
返回空串=不是失败;否则返回失败码。 */
|
|
1473
|
+
function resultFailure(res) {
|
|
1474
|
+
if (!res) { return "empty-result"; }
|
|
1475
|
+
if (res.ok === false) { return String((res.error && res.error.code) || "unknown-error"); }
|
|
1476
|
+
var st = res.value;
|
|
1477
|
+
if (st && st.application === "failed") { return String((st.error && st.error.code) || "unknown-error"); }
|
|
1478
|
+
return "";
|
|
1479
|
+
}
|
|
1480
|
+
/* 卸载会触发一次 profile 重载。真机踩过的坑:卸载刚返回就立刻重装,新请求可能仍在
|
|
1481
|
+
上一代管理器里执行、读到的还是卸载前的清单 → 又被 already-installed 拒。
|
|
1482
|
+
所以两步之间要**等清单里真的没有它**再装,装的时候再按退避重试几次。 */
|
|
1483
|
+
var REMOVE_SETTLE_MS = 10000; // 等清单消失的上限
|
|
1484
|
+
var REMOVE_POLL_MS = 400; // 轮询间隔
|
|
1485
|
+
var INSTALL_RETRY_TIMES = 3; // 被 already-installed 拒后的重试次数
|
|
1486
|
+
var INSTALL_RETRY_DELAY_MS = 1500; // 每次重试前等多久
|
|
1487
|
+
function bundleStillListed(pm, nm) {
|
|
1488
|
+
return Promise.resolve(pm.listBundles()).then(function (r) {
|
|
1489
|
+
var list = r && r.ok === false ? null : (r && r.value) || [];
|
|
1490
|
+
if (!list || !list.length) { return false; } // 拿不到清单:当作"没有",别把更新卡死
|
|
1491
|
+
for (var i = 0; i < list.length; i++) {
|
|
1492
|
+
if (list[i] && list[i].name === nm) { return true; }
|
|
1493
|
+
}
|
|
1494
|
+
return false;
|
|
1495
|
+
}, function () { return false; });
|
|
1496
|
+
}
|
|
1497
|
+
function waitBundlesGone(pm, nm, timeoutMs) {
|
|
1498
|
+
if (typeof pm.listBundles !== "function") { return Promise.resolve(false); }
|
|
1499
|
+
var deadline = Date.now() + (timeoutMs || REMOVE_SETTLE_MS);
|
|
1500
|
+
function poll() {
|
|
1501
|
+
return bundleStillListed(pm, nm).then(function (still) {
|
|
1502
|
+
if (!still) { return true; }
|
|
1503
|
+
if (Date.now() >= deadline) { return false; }
|
|
1504
|
+
return new Promise(function (r) { setTimeout(r, REMOVE_POLL_MS); }).then(poll);
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
return poll();
|
|
1508
|
+
}
|
|
1509
|
+
function installRetrying(pm, spec, opts, left) {
|
|
1510
|
+
var n = left === undefined ? INSTALL_RETRY_TIMES : left;
|
|
1511
|
+
return Promise.resolve(pm.installBundle(spec, opts)).then(function (res) {
|
|
1512
|
+
if (!isAlreadyInstalledResult(res) || n <= 1) { return res; }
|
|
1513
|
+
return new Promise(function (r) { setTimeout(r, INSTALL_RETRY_DELAY_MS); })
|
|
1514
|
+
.then(function () { return installRetrying(pm, spec, opts, n - 1); });
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
function installBundleEx(pm, spec, nm, opts, onRemoved) {
|
|
1518
|
+
return Promise.resolve(pm.installBundle(spec, opts)).then(function (res) {
|
|
1519
|
+
if (!isAlreadyInstalledResult(res) || typeof pm.removeBundle !== "function") {
|
|
1520
|
+
return { result: res, removed: false, removeFailure: "" };
|
|
1521
|
+
}
|
|
1522
|
+
return Promise.resolve(pm.removeBundle(nm)).then(function (rm) {
|
|
1523
|
+
var rmFail = resultFailure(rm);
|
|
1524
|
+
/* 卸载被拒:把原始拒绝交回去,同时把"卸载失败"这个真实原因一起带出,别再吞掉 */
|
|
1525
|
+
if (rmFail) { return { result: res, removed: false, removeFailure: rmFail }; }
|
|
1526
|
+
if (onRemoved) { onRemoved(); }
|
|
1527
|
+
return waitBundlesGone(pm, nm)
|
|
1528
|
+
.then(function () { return installRetrying(pm, spec, opts); })
|
|
1529
|
+
.then(function (res2) { return { result: res2, removed: true, removeFailure: "" }; });
|
|
1530
|
+
});
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
/* 安装器返回的错误码翻成人话(这些码是稳定契约,优先用它,而不是甩一串英文) */
|
|
1534
|
+
function installCodeText(code) {
|
|
1535
|
+
var map = {
|
|
1536
|
+
"already-installed": "该插件已安装,DSH 不允许原地覆盖版本(已自动尝试「先卸载再安装」,仍未成功)",
|
|
1537
|
+
"incompatible-version": "这个版本与本机 dsh 不兼容,已被安装器拒绝",
|
|
1538
|
+
"invalid-spec": "安装参数不合法",
|
|
1539
|
+
"not-removable": "这个条目不允许卸载,因此没法用「卸载 + 安装」的方式更新",
|
|
1540
|
+
"bundle-in-use": "该插件正在使用中,安装器拒绝了卸载",
|
|
1541
|
+
"unknown-plugin": "安装器不认识这个插件,请点「重新检查」确认清单",
|
|
1542
|
+
"operation-error": "安装器执行失败",
|
|
1543
|
+
"stale-approval": "上一次安装的批准已过期,请重试",
|
|
1544
|
+
"ambiguous-install": "这次依赖变更没指明唯一插件,请点「重新检查」确认"
|
|
1545
|
+
};
|
|
1546
|
+
var t = map[String(code || "")];
|
|
1547
|
+
return t ? t + "(" + code + ")" : "";
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1444
1550
|
function startOne(name) {
|
|
1445
1551
|
var row = findRow(name);
|
|
1446
1552
|
busyName = name;
|
|
@@ -1452,6 +1558,7 @@ window.__ModuleLoader__.load({
|
|
|
1452
1558
|
var pm = pickService(null, "remote.pluginManager");
|
|
1453
1559
|
if (!pm) { finishOne(name, "failed", "remote.pluginManager 不可用"); return; }
|
|
1454
1560
|
var settled = false;
|
|
1561
|
+
var busyRemoved = false; // 本次更新走没走「先卸载再安装」:卸载过 → 失败必须回滚
|
|
1455
1562
|
/* 看门狗只作兜底:正常路径一次调用就定案,不等它。 */
|
|
1456
1563
|
var watchdog = setTimeout(function () {
|
|
1457
1564
|
finish("unknown", "更新超时(240 秒未结束);结果未知,请点「重新检查」确认");
|
|
@@ -1461,6 +1568,30 @@ window.__ModuleLoader__.load({
|
|
|
1461
1568
|
if (settled) { return; }
|
|
1462
1569
|
settled = true;
|
|
1463
1570
|
clearTimeout(watchdog);
|
|
1571
|
+
/* 卸载成功但重装没成(失败/被取消)→ 把旧版本装回去,绝不把插件留在"没了"的状态。
|
|
1572
|
+
只有 failed / cancelled 自动回滚:unknown 时安装可能仍在飞,硬回滚会打架,
|
|
1573
|
+
改为在文案里讲清楚"中途卸载过,确认它装回来了没有"。 */
|
|
1574
|
+
if (busyRemoved && busyBefore && (kind === "failed" || kind === "cancelled")) {
|
|
1575
|
+
var pmR = pickService(null, "remote.pluginManager");
|
|
1576
|
+
if (pmR && typeof pmR.installBundle === "function") {
|
|
1577
|
+
setRow(name, { state: "busy", why: "正在回滚到 " + busyBefore + "…" });
|
|
1578
|
+
emit();
|
|
1579
|
+
Promise.resolve(pmR.installBundle(name + "@" + busyBefore, { enabled: enabled })).then(function (r) {
|
|
1580
|
+
var rf = resultFailure(r);
|
|
1581
|
+
finishOne(name, kind, (why || "") +
|
|
1582
|
+
(rf ? ";回滚也失败了(" + rf + "),请手动重装 " + name + "@" + busyBefore : ";该插件已回滚到 " + busyBefore), meta);
|
|
1583
|
+
}, function () {
|
|
1584
|
+
finishOne(name, kind, (why || "") + ";回滚失败,请手动重装 " + name + "@" + busyBefore, meta);
|
|
1585
|
+
});
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
finishOne(name, kind, (why || "") + ";未能回滚,请手动重装 " + name + "@" + busyBefore, meta);
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
if (busyRemoved && kind === "unknown") {
|
|
1592
|
+
finishOne(name, kind, (why || "") + "(注意:本次更新中途卸载过该插件,请点「重新检查」确认它是否装回来了)", meta);
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1464
1595
|
finishOne(name, kind, why || "", meta);
|
|
1465
1596
|
}
|
|
1466
1597
|
function settle(st) {
|
|
@@ -1544,9 +1675,32 @@ window.__ModuleLoader__.load({
|
|
|
1544
1675
|
guardMaturity(name, targetVersion, function () { resolve(spec); });
|
|
1545
1676
|
});
|
|
1546
1677
|
}).then(function (spec) {
|
|
1547
|
-
return pm
|
|
1548
|
-
|
|
1549
|
-
|
|
1678
|
+
return installBundleEx(pm, spec, name, { enabled: enabled, requestId: busyReq }, function () {
|
|
1679
|
+
busyRemoved = true;
|
|
1680
|
+
/* 让用户看清这两步:HMR 运行时不允许原地覆盖已装的包,只能先卸再装 */
|
|
1681
|
+
setRow(name, { state: "busy", why: "该插件已装过:DSH 不允许原地覆盖版本,正在先卸载、再用 " + targetVersion + " 装回…" });
|
|
1682
|
+
emit();
|
|
1683
|
+
});
|
|
1684
|
+
}).then(function (out) {
|
|
1685
|
+
var res = out.result;
|
|
1686
|
+
if (!res || res.ok === false) {
|
|
1687
|
+
/* 明确的拒绝(带错误码)直接翻成人话给用户看;没有码才按"回包丢了"去追结果 */
|
|
1688
|
+
var code = res && res.error ? res.error.code : "";
|
|
1689
|
+
var human = installCodeText(code);
|
|
1690
|
+
if (human) { finish("failed", human); return; }
|
|
1691
|
+
lostReply(res); return;
|
|
1692
|
+
}
|
|
1693
|
+
var fail = resultFailure(res);
|
|
1694
|
+
if (fail) {
|
|
1695
|
+
var text = installCodeText(fail);
|
|
1696
|
+
if (!text) { lostReply(res); return; }
|
|
1697
|
+
if (out.removeFailure) {
|
|
1698
|
+
text += ";这一步必须「先卸载再安装」,但卸载被拒(" + out.removeFailure + "),所以没法用两步法更新它";
|
|
1699
|
+
} else if (!out.removed) {
|
|
1700
|
+
text += "(DSH 不允许原地覆盖已装的包,需要「先卸载再安装」)";
|
|
1701
|
+
}
|
|
1702
|
+
finish("failed", text); return;
|
|
1703
|
+
}
|
|
1550
1704
|
settle(res.value);
|
|
1551
1705
|
}).catch(function (e) { finish("failed", "安装请求失败:" + msgOf(e)); });
|
|
1552
1706
|
}
|
|
@@ -1984,7 +2138,7 @@ window.__ModuleLoader__.load({
|
|
|
1984
2138
|
}
|
|
1985
2139
|
function accessModeBar() {
|
|
1986
2140
|
if (!hasAutoReviewSession()) { return ""; }
|
|
1987
|
-
return "<div class=\"warn\">⚠
|
|
2141
|
+
return "<div class=\"warn\">⚠ 有会话在用「自动审查」:装/更新插件容易失败,工具调用也可能被审查拦掉。建议先切「工作区内修改」,装完再切回。</div>";
|
|
1988
2142
|
}
|
|
1989
2143
|
|
|
1990
2144
|
function paint() {
|
|
@@ -2203,34 +2357,69 @@ window.__ModuleLoader__.load({
|
|
|
2203
2357
|
}
|
|
2204
2358
|
|
|
2205
2359
|
/* ======================================================================
|
|
2206
|
-
* 模块 5
|
|
2360
|
+
* 模块 5:回合尾「本回合改动」——数据聚合 + 两种兜底情形下的自画卡
|
|
2207
2361
|
*
|
|
2208
|
-
* 缺陷(0.2.1-rc.2 实测):官方回合尾卡读的是 workspaceChanges
|
|
2209
|
-
*
|
|
2210
|
-
*
|
|
2211
|
-
*
|
|
2212
|
-
* 工具卡 diff 用的就是它(dsh-client-ui-tool 的 appliedDiffs)。所以本模块
|
|
2362
|
+
* 缺陷(0.2.1-rc.2 实测):官方回合尾卡读的是 workspaceChanges 服务,摘要与对比
|
|
2363
|
+
* 只活在 Host 内存 + 系统临时目录里,Session 一释放就 404,卡片直接不画。但每次
|
|
2364
|
+
* 文件工具调用都把前后全文写在工具结果事件的 meta.diffs:[{path,oldText,newText}]
|
|
2365
|
+
* 里,**随会话日志永久保存** —— 客户端画工具卡 diff 用的就是它。所以本模块
|
|
2213
2366
|
* **不新增任何持久化**:用 uiConversation 的 Turn Definition 把一回合内这些
|
|
2214
|
-
* meta.diffs
|
|
2215
|
-
*
|
|
2367
|
+
* meta.diffs 按文件聚合起来,注册到 conversation.chat.turnTail(官方声明的 list
|
|
2368
|
+
* 槽位),供两处使用:
|
|
2369
|
+
* 1. 模块 5b:喂给官方卡的数据(主路径,官方组件自己渲染);
|
|
2370
|
+
* 2. 本模块的自画卡:只在官方一定不会画的两种情形下出现(见下)。
|
|
2216
2371
|
*
|
|
2217
|
-
*
|
|
2372
|
+
* 0.2.1 修的两处(0.2.0 实测「卡片一次都没画出来」):
|
|
2373
|
+
* - **必须实现 buildLocationData**:会话装配器只为实现了它的 Definition 发布
|
|
2374
|
+
* Turn 作用域数据,而 changesCard 读的 `owner.turn.data.get(CHANGES_KIND)`
|
|
2375
|
+
* 正是这里发布的 value。0.2.0 缺了它 → state 恒为 null → 卡片永不画。
|
|
2376
|
+
* - **补位条件按「官方那侧会不会画」判断,不是只按「探测到 404」**:官方卡还受
|
|
2377
|
+
* 设置项「显示编码工具视图」约束(showCodeDiff = configForms.developerTools
|
|
2378
|
+
* .enabled);开关关着时路由照样 200,只按 404 判断会两边都不画。同理,该回合
|
|
2379
|
+
* 没有 workspace/changes 公告(seq 为 null)时官方没有可请求的摘要。
|
|
2380
|
+
*
|
|
2381
|
+
* 自画卡的边界(README 同步):
|
|
2218
2382
|
* - 只覆盖**文件工具**的改动;pwsh / npm 等造成的改动进不来(只有官方卡含)。
|
|
2219
2383
|
* - 嵌套调用(run_code 里的 edit)走 tool/ptc-dispatch 事件,该事件没有 turn 字段,
|
|
2220
2384
|
* 用 rootCallId → turn 的映射(来自 tool/call)挂回回合。
|
|
2221
2385
|
* - 探测结果只缓存在本次连接内(connection/reset 清空)。官方自己永久缓存 missing,
|
|
2222
2386
|
* 那会在宿主恢复后留下双份卡。
|
|
2387
|
+
* - 外观照官方骨架(60px 卡头 / 图标块 / 千分位统计 / 4 行折叠 / hover 迷你 diff),
|
|
2388
|
+
* 点击一律打开官方右侧 Review;没有任何内联展开的 diff。
|
|
2223
2389
|
* ====================================================================== */
|
|
2224
2390
|
var REACT = null;
|
|
2225
2391
|
try { REACT = require("react"); } catch (e) { REACT = null; }
|
|
2226
2392
|
|
|
2227
2393
|
var CHANGES_KIND = "ui-tweaks-changes";
|
|
2228
2394
|
var SUMMARY_ROUTE = "api/changes.summary"; /* 文档相对,勿加前导斜杠(同官方 CHANGED_FILES_ROUTE) */
|
|
2229
|
-
var DIFF_ROW_LIMIT = 400; /* 单个文件最多画多少行 diff */
|
|
2230
2395
|
var LCS_LINE_LIMIT = 1200; /* 超过就退化为「全删 + 全增」,避免 DP 吃内存 */
|
|
2231
2396
|
var probeCache = {}; /* sessionId:seq -> "ok" | "missing" */
|
|
2232
2397
|
var rootTurn = {}; /* rootCallId -> turn(ptc 事件没有 turn) */
|
|
2233
2398
|
|
|
2399
|
+
/* 官方卡还受设置项「显示编码工具视图」约束(官方 deliverables 的 showCodeDiff =
|
|
2400
|
+
configForms.developerTools.enabled)。关着时官方卡不画,但 /api/changes.summary
|
|
2401
|
+
照样 200 —— 只按探测结果判断会两边都不画。拿不到这个 store(注入未就绪)时按
|
|
2402
|
+
「可能开着」处理:仍然走探测,结果与 0.2.0 一致,不会多出一张卡。 */
|
|
2403
|
+
var devToolsStore = null;
|
|
2404
|
+
function useDevToolsEnabled() {
|
|
2405
|
+
var s = REACT.useState(function () {
|
|
2406
|
+
return devToolsStore && typeof devToolsStore.getSnapshot === "function"
|
|
2407
|
+
? devToolsStore.getSnapshot() === true
|
|
2408
|
+
: true;
|
|
2409
|
+
});
|
|
2410
|
+
var enabled = s[0], setEnabled = s[1];
|
|
2411
|
+
REACT.useEffect(function () {
|
|
2412
|
+
if (!devToolsStore || typeof devToolsStore.subscribe !== "function") { return undefined; }
|
|
2413
|
+
var sync = function () {
|
|
2414
|
+
try { setEnabled(devToolsStore.getSnapshot() === true); } catch (e) {}
|
|
2415
|
+
};
|
|
2416
|
+
sync();
|
|
2417
|
+
var off = devToolsStore.subscribe(sync);
|
|
2418
|
+
return function () { if (typeof off === "function") { off(); } };
|
|
2419
|
+
}, []);
|
|
2420
|
+
return enabled;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2234
2423
|
function splitLines(text) {
|
|
2235
2424
|
var s = text == null ? "" : String(text);
|
|
2236
2425
|
if (s === "") { return []; }
|
|
@@ -2274,26 +2463,6 @@ window.__ModuleLoader__.load({
|
|
|
2274
2463
|
}
|
|
2275
2464
|
return { added: added, deleted: deleted };
|
|
2276
2465
|
}
|
|
2277
|
-
/* 同一文件在回合内被改多次时按 old→new 链折成一份净变化;链断返回 null(分段显示)。 */
|
|
2278
|
-
function foldParts(parts) {
|
|
2279
|
-
if (!parts || parts.length === 0) { return null; }
|
|
2280
|
-
var before = parts[0][0] == null ? "" : String(parts[0][0]);
|
|
2281
|
-
var after = parts[0][1] == null ? "" : String(parts[0][1]);
|
|
2282
|
-
for (var i = 1; i < parts.length; i++) {
|
|
2283
|
-
var o = parts[i][0];
|
|
2284
|
-
if (o != null && String(o) !== after) { return null; }
|
|
2285
|
-
if (parts[i][1] != null) { after = String(parts[i][1]); }
|
|
2286
|
-
}
|
|
2287
|
-
return { before: before, after: after };
|
|
2288
|
-
}
|
|
2289
|
-
function countParts(parts) {
|
|
2290
|
-
var added = 0, deleted = 0;
|
|
2291
|
-
for (var i = 0; i < parts.length; i++) {
|
|
2292
|
-
var c = countRows(diffRows(parts[i][0], parts[i][1]));
|
|
2293
|
-
added += c.added; deleted += c.deleted;
|
|
2294
|
-
}
|
|
2295
|
-
return { added: added, deleted: deleted };
|
|
2296
|
-
}
|
|
2297
2466
|
function metaDiffs(data) {
|
|
2298
2467
|
if (!data || typeof data !== "object") { return null; }
|
|
2299
2468
|
var meta = data.meta;
|
|
@@ -2344,25 +2513,109 @@ window.__ModuleLoader__.load({
|
|
|
2344
2513
|
}
|
|
2345
2514
|
return null;
|
|
2346
2515
|
},
|
|
2347
|
-
start: function () {
|
|
2516
|
+
start: function (context, match) {
|
|
2517
|
+
var turn = match && match.event && match.event.data ? match.event.data.turn : undefined;
|
|
2518
|
+
return { turn: turn, seq: null, files: [] };
|
|
2519
|
+
},
|
|
2348
2520
|
update: function (context, match) {
|
|
2349
|
-
var prev = (context && context.state) || context || { seq: null, files: [] };
|
|
2350
|
-
var next = { seq: prev.seq == null ? null : prev.seq, files: prev.files || [] };
|
|
2521
|
+
var prev = (context && context.state) || context || { turn: undefined, seq: null, files: [] };
|
|
2522
|
+
var next = { turn: prev.turn, seq: prev.seq == null ? null : prev.seq, files: prev.files || [] };
|
|
2351
2523
|
var event = match && match.event;
|
|
2352
2524
|
if (!event) { return next; }
|
|
2353
|
-
if (event.type === "workspace/changes") { return { seq: event.seq, files: next.files }; }
|
|
2525
|
+
if (event.type === "workspace/changes") { return { turn: next.turn, seq: event.seq, files: next.files }; }
|
|
2354
2526
|
var list = metaDiffs(event.data || {});
|
|
2355
2527
|
if (!list) { return next; }
|
|
2356
2528
|
var files = next.files;
|
|
2357
2529
|
for (var i = 0; i < list.length; i++) { files = mergeDiff(files, list[i]); }
|
|
2358
|
-
return { seq: next.seq, files: files };
|
|
2530
|
+
return { turn: next.turn, seq: next.seq, files: files };
|
|
2531
|
+
},
|
|
2532
|
+
/* 卡片的取数通路(0.2.0 漏了它 → 卡片一次都没画):装配器只为实现了本方法的
|
|
2533
|
+
Definition 发布 Turn 作用域数据,changesCard 的 owner.turn.data.get(CHANGES_KIND)
|
|
2534
|
+
读到的就是这里的 value。约定:data.kind === scope、"key === 本 Definition 的 kind"、
|
|
2535
|
+
data.turn 是安全整数,任一条不满足装配器会直接抛错 —— 所以这里只做保守返回。 */
|
|
2536
|
+
buildLocationData: function (context, scope, previous) {
|
|
2537
|
+
if (scope !== "turn") { return null; }
|
|
2538
|
+
var state = context && context.state;
|
|
2539
|
+
var turn = state ? state.turn : undefined;
|
|
2540
|
+
if (typeof turn !== "number" || !Number.isSafeInteger(turn) || turn < 0) { return null; }
|
|
2541
|
+
if (previous && previous.kind === "turn" && previous.turn === turn && previous.key === CHANGES_KIND &&
|
|
2542
|
+
previous.value && previous.value.seq === state.seq && previous.value.files === state.files) {
|
|
2543
|
+
return previous; /* 引用不变 → 装配器判定未变化,卡片不跟着重绘 */
|
|
2544
|
+
}
|
|
2545
|
+
return { kind: "turn", turn: turn, key: CHANGES_KIND, value: { seq: state.seq, files: state.files } };
|
|
2359
2546
|
}
|
|
2360
2547
|
};
|
|
2361
2548
|
}
|
|
2362
|
-
|
|
2549
|
+
/* ---------- 自画卡对齐官方所需的取数/渲染辅助(只服务两种兜底情形) ----------
|
|
2550
|
+
|
|
2551
|
+
官方那张卡由 deliverables 自己渲染,我们抢不过来也不该抢;但有两种情形官方一定
|
|
2552
|
+
不画:①「显示编码工具视图」关着(源码里 announced = showCodeDiff ? … : null);
|
|
2553
|
+
② 该回合根本没有 workspace/changes 公告。这两处才由本卡补位,外观尽量贴官方:
|
|
2554
|
+
类名不硬编码(hash 每版会变),运行时从官方注入的 CSS module <style> 里读出前缀。 */
|
|
2555
|
+
var officialClassCache = null;
|
|
2556
|
+
function officialChangesClasses() {
|
|
2557
|
+
if (officialClassCache !== null) { return officialClassCache; }
|
|
2558
|
+
officialClassCache = false;
|
|
2559
|
+
try {
|
|
2560
|
+
var tag = document.querySelector("style[data-plugin-css$=\"ChangedFiles.module.css\"]");
|
|
2561
|
+
var text = tag && tag.textContent ? String(tag.textContent) : "";
|
|
2562
|
+
var hit = /\.([A-Za-z0-9_-]{4,12})_card\b/.exec(text);
|
|
2563
|
+
if (!hit) { return officialClassCache; }
|
|
2564
|
+
var k = hit[1];
|
|
2565
|
+
officialClassCache = {
|
|
2566
|
+
card: k + "_card", header: k + "_header", tile: k + "_tile",
|
|
2567
|
+
titles: k + "_titles", title: k + "_title", stat: k + "_stat",
|
|
2568
|
+
statCounts: k + "_statCounts", previewHint: k + "_previewHint",
|
|
2569
|
+
list: k + "_list", row: k + "_row", path: k + "_path",
|
|
2570
|
+
counts: k + "_counts", toggle: k + "_toggle",
|
|
2571
|
+
preview: k + "_preview", previewPath: k + "_previewPath"
|
|
2572
|
+
};
|
|
2573
|
+
} catch (e) { officialClassCache = false; }
|
|
2574
|
+
return officialClassCache;
|
|
2575
|
+
}
|
|
2576
|
+
/* 读不到官方样式表时的近似样式(只影响观感,不影响结构/交互)。 */
|
|
2577
|
+
var CHANGES_FALLBACK_STYLES = {
|
|
2578
|
+
card: { marginTop: "4px", border: "0.5px solid var(--dsw-alias-border-l2, rgba(128,128,128,.3))", borderRadius: "var(--dsw-radius-lg, 8px)", background: "var(--dsw-alias-bg-layer-1, transparent)", display: "flex", flexDirection: "column", overflow: "hidden" },
|
|
2579
|
+
header: { display: "flex", alignItems: "center", gap: "10px", width: "100%", boxSizing: "border-box", height: "60px", padding: "8px 10px", background: "var(--dsw-static-neutral-50, rgba(128,128,128,.08))", color: "inherit", textAlign: "left", border: "0", cursor: "pointer", font: "inherit" },
|
|
2580
|
+
tile: { display: "grid", placeItems: "center", width: "40px", height: "40px", flex: "none", borderRadius: "10px", border: "0.5px solid var(--dsw-alias-border-l1, rgba(128,128,128,.25))" },
|
|
2581
|
+
titles: { display: "flex", flexDirection: "column", flex: "1", minWidth: "0" },
|
|
2582
|
+
title: { fontSize: "13px", fontWeight: 500, lineHeight: "20px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
2583
|
+
stat: { display: "inline-flex", gap: "6px", fontSize: "10px", lineHeight: "16px", color: "var(--dsw-alias-label-tertiary, #888)" },
|
|
2584
|
+
list: { margin: "0", padding: "0", listStyle: "none", borderTop: "0.5px solid var(--dsw-alias-border-l2, rgba(128,128,128,.3))" },
|
|
2585
|
+
row: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "10px", width: "100%", boxSizing: "border-box", padding: "7px 18px 7px 14px", minHeight: "24px", background: "none", border: "0", cursor: "pointer", textAlign: "left", color: "var(--dsw-alias-label-tertiary, #888)", fontFamily: "var(--ds-font-family-code, monospace)", fontSize: "11px", lineHeight: "18px" },
|
|
2586
|
+
path: { minWidth: "0", fontFamily: "var(--dsw-font-family, inherit)", fontSize: "12px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
2587
|
+
counts: { display: "inline-flex", gap: "6px", flex: "none" },
|
|
2588
|
+
toggle: { display: "inline-flex", alignItems: "center", gap: "4px", width: "100%", padding: "10px 18px 10px 14px", background: "none", border: "0", cursor: "pointer", color: "var(--dsw-alias-label-tertiary, #888)", fontSize: "12px", lineHeight: "18px", textAlign: "left" }
|
|
2589
|
+
};
|
|
2590
|
+
var CHANGES_ADDED_STYLE = { color: "var(--dsw-alias-state-success-primary, #3fb950)" };
|
|
2591
|
+
var CHANGES_DELETED_STYLE = { color: "var(--dsw-alias-state-error-primary, #f85149)" };
|
|
2592
|
+
var changesNumberFormat = null;
|
|
2593
|
+
var changesHoverTimer = null;
|
|
2594
|
+
var sidebarRightForChanges = null;
|
|
2595
|
+
|
|
2596
|
+
function changesNumber(value) {
|
|
2597
|
+
try {
|
|
2598
|
+
changesNumberFormat = changesNumberFormat || new Intl.NumberFormat("en-US");
|
|
2599
|
+
return changesNumberFormat.format(value);
|
|
2600
|
+
} catch (e) { return String(value); }
|
|
2601
|
+
}
|
|
2602
|
+
function changesBaseName(p) {
|
|
2363
2603
|
var s = String(p);
|
|
2364
|
-
|
|
2604
|
+
var at = Math.max(s.lastIndexOf("/"), s.lastIndexOf("\\"));
|
|
2605
|
+
return at < 0 ? s : s.slice(at + 1);
|
|
2606
|
+
}
|
|
2607
|
+
/* 点卡头/行 → 官方右侧 Review(地址与 ReviewTab 注册的 tab 类型对得上)。 */
|
|
2608
|
+
function openOfficialChangesReview(sessionId, seq, turn, index) {
|
|
2609
|
+
try {
|
|
2610
|
+
var svc = sidebarRightForChanges || pickService(null, "sidebarRight");
|
|
2611
|
+
if (!svc || typeof svc.openResource !== "function" || turn == null) { return false; }
|
|
2612
|
+
var address = "dsh-resource://changes-review/session/" + encodeURIComponent(String(sessionId)) +
|
|
2613
|
+
"/" + String(seq) + "/" + String(turn);
|
|
2614
|
+
svc.openResource(address, { params: { index: index } });
|
|
2615
|
+
return true;
|
|
2616
|
+
} catch (e) { return false; }
|
|
2365
2617
|
}
|
|
2618
|
+
|
|
2366
2619
|
function changesCard(props) {
|
|
2367
2620
|
var owner = props || {};
|
|
2368
2621
|
var state = null;
|
|
@@ -2373,14 +2626,27 @@ window.__ModuleLoader__.load({
|
|
|
2373
2626
|
} catch (e) { state = null; }
|
|
2374
2627
|
var sessionId = owner.sessionId;
|
|
2375
2628
|
var seq = state && state.seq != null ? state.seq : null;
|
|
2629
|
+
/* 模块 5b:把这条坐标(sessionId + seq → 文件/统计/hunk)登记进适配层索引。
|
|
2630
|
+
必须在这里(渲染期)而不是 useEffect:React 先跑完所有 slot 的 render 才跑
|
|
2631
|
+
effect,官方那个组件正是在它的 useEffect 里发请求,所以登记必然早于请求。 */
|
|
2632
|
+
try {
|
|
2633
|
+
registerChangesCoordinates(sessionId, seq, state,
|
|
2634
|
+
(owner.turn && typeof owner.turn.turn === "number") ? owner.turn.turn : undefined);
|
|
2635
|
+
} catch (e) {}
|
|
2636
|
+
var devEnabled = useDevToolsEnabled();
|
|
2637
|
+
/* 官方卡成立的两个前提:①「显示编码工具视图」开着;②该回合有 workspace/changes
|
|
2638
|
+
公告(没公告就没有可请求的 seq,官方那侧一定不画)。缺任一个都不必探测。 */
|
|
2639
|
+
var officialMayRender = devEnabled === true && seq != null;
|
|
2376
2640
|
var key = (sessionId && seq != null) ? String(sessionId) + ":" + String(seq) : null;
|
|
2377
2641
|
var cached = key && probeCache[key] ? probeCache[key] : null;
|
|
2378
2642
|
var probeState = REACT.useState(cached || "pending");
|
|
2379
2643
|
var probe = probeState[0], setProbe = probeState[1];
|
|
2380
2644
|
var expState = REACT.useState(false);
|
|
2381
2645
|
var expanded = expState[0], setExpanded = expState[1];
|
|
2646
|
+
var hoverState = REACT.useState(null); /* {index, top, left} | null:hover 迷你 diff 的锚点 */
|
|
2647
|
+
var hover = hoverState[0], setHover = hoverState[1];
|
|
2382
2648
|
REACT.useEffect(function () {
|
|
2383
|
-
if (!key || cached) { return undefined; }
|
|
2649
|
+
if (!key || !officialMayRender || cached) { return undefined; }
|
|
2384
2650
|
var cancelled = false;
|
|
2385
2651
|
try {
|
|
2386
2652
|
fetch(SUMMARY_ROUTE + "?" + new URLSearchParams({ sessionId: String(sessionId), seq: String(seq) }),
|
|
@@ -2392,85 +2658,404 @@ window.__ModuleLoader__.load({
|
|
|
2392
2658
|
}).catch(function () {});
|
|
2393
2659
|
} catch (e) {}
|
|
2394
2660
|
return function () { cancelled = true; };
|
|
2395
|
-
}, [key, cached]);
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
var
|
|
2409
|
-
var
|
|
2410
|
-
var
|
|
2411
|
-
var
|
|
2412
|
-
var
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2661
|
+
}, [key, cached, officialMayRender]);
|
|
2662
|
+
if (!state || !state.files || state.files.length === 0) { return null; }
|
|
2663
|
+
/* 只在「官方那侧不会画」时补位:官方可能画且探测不是 missing(ok / pending / error)
|
|
2664
|
+
一律不画,避免双份卡。 */
|
|
2665
|
+
if (officialMayRender && probe !== "missing") { return null; }
|
|
2666
|
+
|
|
2667
|
+
/* 数据直接复用模块 5b 的入口:保证卡片上的数字/hunk 与喂给官方卡的完全是同一份
|
|
2668
|
+
(官方口径 = 回合首末净变化),不另算一套。无公告的回合用合成坐标 0 供 Review 取数。 */
|
|
2669
|
+
var turnNumber = (owner.turn && typeof owner.turn.turn === "number") ? owner.turn.turn : null;
|
|
2670
|
+
var seqForReview = seq != null ? seq : 0;
|
|
2671
|
+
try { registerChangesCoordinates(sessionId, seqForReview, state, turnNumber); } catch (e) {}
|
|
2672
|
+
var entry = changesEntryFor(sessionId, seqForReview);
|
|
2673
|
+
if (!entry || entry.files.length === 0) { return null; }
|
|
2674
|
+
var list = entry.files;
|
|
2675
|
+
var total = list.length;
|
|
2676
|
+
var single = total === 1 ? list[0] : null;
|
|
2677
|
+
var foldable = total > 4;
|
|
2678
|
+
var visible = foldable && !expanded ? list.slice(0, 4) : list;
|
|
2679
|
+
var totals = { added: 0, deleted: 0 }, i;
|
|
2680
|
+
for (i = 0; i < list.length; i++) { totals.added += list[i].added; totals.deleted += list[i].deleted; }
|
|
2681
|
+
|
|
2682
|
+
var classes = officialChangesClasses();
|
|
2683
|
+
var classOf = function (name) { return classes ? classes[name] : undefined; };
|
|
2684
|
+
var styleOf = function (name) {
|
|
2685
|
+
if (classes) { return undefined; }
|
|
2686
|
+
return CHANGES_FALLBACK_STYLES[name];
|
|
2687
|
+
};
|
|
2688
|
+
var hiddenNote = seq == null
|
|
2689
|
+
? "由 dsh-ui-tweaks 补位 · 该回合没有官方的改动记录"
|
|
2690
|
+
: (devEnabled === true ? null : "由 dsh-ui-tweaks 补位 · 显示编码工具视图已关闭");
|
|
2691
|
+
function beginHover(index, event) {
|
|
2692
|
+
try {
|
|
2693
|
+
var rect = event.currentTarget.getBoundingClientRect();
|
|
2694
|
+
if (changesHoverTimer) { clearTimeout(changesHoverTimer); }
|
|
2695
|
+
changesHoverTimer = setTimeout(function () {
|
|
2696
|
+
setHover({ index: index, top: rect.bottom + 6, left: Math.max(8, rect.left) });
|
|
2697
|
+
}, 500);
|
|
2698
|
+
} catch (e) {}
|
|
2699
|
+
}
|
|
2700
|
+
function endHover() {
|
|
2701
|
+
if (changesHoverTimer) { clearTimeout(changesHoverTimer); changesHoverTimer = null; }
|
|
2702
|
+
if (hover) { setHover(null); }
|
|
2703
|
+
}
|
|
2704
|
+
function countsOf(added, deleted) {
|
|
2705
|
+
return [
|
|
2706
|
+
REACT.createElement("span", { key: "a", style: CHANGES_ADDED_STYLE }, "+" + changesNumber(added)),
|
|
2707
|
+
REACT.createElement("span", { key: "d", style: CHANGES_DELETED_STYLE }, "−" + changesNumber(deleted))
|
|
2708
|
+
];
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
var header = REACT.createElement("button", {
|
|
2712
|
+
key: "header",
|
|
2713
|
+
type: "button",
|
|
2714
|
+
className: classOf("header"),
|
|
2715
|
+
style: styleOf("header"),
|
|
2716
|
+
title: hiddenNote || undefined,
|
|
2717
|
+
"aria-label": single === null ? "打开本回合改动审查" : ("查看 " + single.display + " 的逐行对比"),
|
|
2718
|
+
onClick: function () { openOfficialChangesReview(sessionId, seqForReview, turnNumber, 0); },
|
|
2719
|
+
onMouseEnter: single === null ? undefined : function (event) { beginHover(0, event); },
|
|
2720
|
+
onMouseLeave: single === null ? undefined : endHover
|
|
2417
2721
|
},
|
|
2418
|
-
REACT.createElement("span", {
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
));
|
|
2424
|
-
children.push(REACT.createElement("div", { key: "note", style: { opacity: 0.5, fontSize: 11, marginTop: 2 } },
|
|
2425
|
-
"(据会话日志重建 · 仅文件工具的改动)"));
|
|
2426
|
-
if (expanded) {
|
|
2427
|
-
for (i = 0; i < prepared.length; i++) {
|
|
2428
|
-
var f = prepared[i];
|
|
2429
|
-
var block = [];
|
|
2430
|
-
block.push(REACT.createElement("div", {
|
|
2431
|
-
key: "p", style: { display: "flex", alignItems: "center", gap: 6, marginTop: 6 }
|
|
2722
|
+
REACT.createElement("span", { key: "tile", className: classOf("tile"), style: styleOf("tile") },
|
|
2723
|
+
REACT.createElement("svg", {
|
|
2724
|
+
width: 20, height: 20, viewBox: "0 0 20 20", fill: "none",
|
|
2725
|
+
stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round",
|
|
2726
|
+
style: { color: "var(--dsw-alias-link, #6cb6ff)" }
|
|
2432
2727
|
},
|
|
2433
|
-
REACT.createElement("
|
|
2434
|
-
REACT.createElement("
|
|
2435
|
-
|
|
2436
|
-
))
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2728
|
+
REACT.createElement("path", { d: "M7.5 6 4 10l3.5 4" }),
|
|
2729
|
+
REACT.createElement("path", { d: "M12.5 6 16 10l-3.5 4" }))),
|
|
2730
|
+
REACT.createElement("span", { key: "titles", className: classOf("titles"), style: styleOf("titles") },
|
|
2731
|
+
REACT.createElement("span", { key: "title", className: classOf("title"), style: styleOf("title") },
|
|
2732
|
+
single === null ? ("已编辑 " + changesNumber(total) + " 个文件") : changesBaseName(single.path)),
|
|
2733
|
+
REACT.createElement("span", { key: "stat", className: classOf("stat"), style: styleOf("stat") },
|
|
2734
|
+
countsOf(totals.added, totals.deleted))));
|
|
2735
|
+
|
|
2736
|
+
var rows = [];
|
|
2737
|
+
for (i = 0; i < visible.length; i++) {
|
|
2738
|
+
rows.push((function (file, index) {
|
|
2739
|
+
return REACT.createElement("li", { key: "row:" + file.display, style: { display: "block", margin: 0, padding: 0 } },
|
|
2740
|
+
REACT.createElement("button", {
|
|
2741
|
+
type: "button",
|
|
2742
|
+
className: classOf("row"),
|
|
2743
|
+
style: styleOf("row"),
|
|
2744
|
+
title: file.path,
|
|
2745
|
+
"aria-label": "查看 " + file.display + " 的逐行对比",
|
|
2746
|
+
onClick: function () { openOfficialChangesReview(sessionId, seqForReview, turnNumber, index); },
|
|
2747
|
+
onMouseEnter: function (event) { beginHover(index, event); },
|
|
2748
|
+
onMouseLeave: endHover
|
|
2749
|
+
},
|
|
2750
|
+
REACT.createElement("span", { className: classOf("path"), style: styleOf("path") }, file.display),
|
|
2751
|
+
REACT.createElement("span", { className: classOf("counts"), style: styleOf("counts") },
|
|
2752
|
+
countsOf(file.added, file.deleted))));
|
|
2753
|
+
})(visible[i], i));
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
var preview = null;
|
|
2757
|
+
if (hover && list[hover.index]) {
|
|
2758
|
+
var hovered = list[hover.index];
|
|
2759
|
+
var previewLines = [];
|
|
2760
|
+
for (var h = 0; h < hovered.hunks.length; h++) {
|
|
2761
|
+
var hunk = hovered.hunks[h];
|
|
2762
|
+
if (h > 0) { previewLines.push(REACT.createElement("div", { key: "gap" + h, style: { height: "6px" } })); }
|
|
2763
|
+
var oldNo = hunk.oldStart, newNo = hunk.newStart;
|
|
2764
|
+
for (var n = 0; n < hunk.lines.length; n++) {
|
|
2765
|
+
var text = hunk.lines[n];
|
|
2766
|
+
var mark = text.charAt(0);
|
|
2767
|
+
/* 行号照官方:删除行给旧行号,新增行给新行号,上下文行给新行号。 */
|
|
2768
|
+
var shownNo = mark === "-" ? oldNo : newNo;
|
|
2769
|
+
if (mark === "-") { oldNo++; }
|
|
2770
|
+
else if (mark === "+") { newNo++; }
|
|
2771
|
+
else { oldNo++; newNo++; }
|
|
2772
|
+
previewLines.push(REACT.createElement("div", {
|
|
2773
|
+
key: "l" + h + "_" + n,
|
|
2774
|
+
style: {
|
|
2775
|
+
display: "flex", gap: "8px", padding: "0 8px 0 4px",
|
|
2776
|
+
fontFamily: "var(--ds-font-family-code, monospace)", fontSize: "11px", lineHeight: "16px",
|
|
2777
|
+
whiteSpace: "pre",
|
|
2778
|
+
background: mark === "+" ? "rgba(63,185,80,.16)" : (mark === "-" ? "rgba(248,81,73,.16)" : "transparent"),
|
|
2779
|
+
borderLeft: "2px solid " + (mark === "+"
|
|
2780
|
+
? "var(--dsw-alias-state-success-primary, #3fb950)"
|
|
2781
|
+
: (mark === "-" ? "var(--dsw-alias-state-error-primary, #f85149)" : "transparent")),
|
|
2782
|
+
color: mark === "+"
|
|
2783
|
+
? "var(--dsw-alias-state-success-primary, #3fb950)"
|
|
2784
|
+
: (mark === "-" ? "var(--dsw-alias-state-error-primary, #f85149)" : "inherit"),
|
|
2785
|
+
opacity: mark === " " ? 0.72 : 1
|
|
2463
2786
|
}
|
|
2464
|
-
}
|
|
2787
|
+
},
|
|
2788
|
+
REACT.createElement("span", { key: "no", style: { flex: "none", minWidth: "30px", textAlign: "right", opacity: 0.55 } }, String(shownNo)),
|
|
2789
|
+
REACT.createElement("span", { key: "mark", style: { flex: "none", width: "9px", opacity: 0.8 } }, mark === " " ? "" : mark),
|
|
2790
|
+
REACT.createElement("span", { key: "text", style: { flex: "1" } }, text.slice(1))));
|
|
2465
2791
|
}
|
|
2466
|
-
children.push(REACT.createElement("div", { key: "f" + i }, block));
|
|
2467
2792
|
}
|
|
2793
|
+
preview = REACT.createElement("div", {
|
|
2794
|
+
key: "preview",
|
|
2795
|
+
style: {
|
|
2796
|
+
position: "fixed", top: hover.top + "px", left: hover.left + "px", zIndex: 9999,
|
|
2797
|
+
maxWidth: "720px", maxHeight: "320px", overflow: "auto", pointerEvents: "none",
|
|
2798
|
+
background: "var(--dsw-alias-bg-layer-1, #1e1e1e)",
|
|
2799
|
+
border: "0.5px solid var(--dsw-alias-border-l2, rgba(128,128,128,.35))",
|
|
2800
|
+
borderRadius: "var(--dsw-radius-lg, 8px)", padding: "0 0 6px",
|
|
2801
|
+
boxShadow: "0 8px 24px rgba(0,0,0,.35)"
|
|
2802
|
+
},
|
|
2803
|
+
children: [
|
|
2804
|
+
REACT.createElement("div", {
|
|
2805
|
+
key: "path",
|
|
2806
|
+
style: { fontSize: "12px", lineHeight: "20px", color: "var(--dsw-alias-label-tertiary, #888)",
|
|
2807
|
+
fontFamily: "var(--ds-font-family-code, monospace)", padding: "6px 10px 4px", whiteSpace: "nowrap" }
|
|
2808
|
+
}, hovered.display)
|
|
2809
|
+
].concat(previewLines)
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
var children = [header];
|
|
2814
|
+
if (rows.length > 0) {
|
|
2815
|
+
children.push(REACT.createElement("ul", { key: "list", className: classOf("list"), style: styleOf("list") }, rows));
|
|
2816
|
+
}
|
|
2817
|
+
if (foldable) {
|
|
2818
|
+
children.push(REACT.createElement("button", {
|
|
2819
|
+
key: "toggle",
|
|
2820
|
+
type: "button",
|
|
2821
|
+
className: classOf("toggle"),
|
|
2822
|
+
style: styleOf("toggle"),
|
|
2823
|
+
"aria-expanded": expanded,
|
|
2824
|
+
onClick: function () { setExpanded(!expanded); }
|
|
2825
|
+
}, expanded ? "收起" : ("全部 " + changesNumber(total) + " 个")));
|
|
2826
|
+
}
|
|
2827
|
+
/* 只有「无公告」这种官方压根没有过记录的回合,才在卡内留一行可见小字;编码视图关
|
|
2828
|
+
的情形走 title(鼠标悬停可见),保持与官方外观一致。 */
|
|
2829
|
+
if (seq == null) {
|
|
2830
|
+
children.push(REACT.createElement("div", {
|
|
2831
|
+
key: "note",
|
|
2832
|
+
style: { padding: "0 14px 8px", fontSize: "11px", lineHeight: "16px", color: "var(--dsw-alias-label-tertiary, #888)" }
|
|
2833
|
+
}, hiddenNote));
|
|
2834
|
+
}
|
|
2835
|
+
if (preview) { children.push(preview); }
|
|
2836
|
+
|
|
2837
|
+
var cardProps = {
|
|
2838
|
+
className: classOf("card"),
|
|
2839
|
+
style: styleOf("card"),
|
|
2840
|
+
"data-changed-files": true,
|
|
2841
|
+
title: hiddenNote || undefined
|
|
2842
|
+
};
|
|
2843
|
+
if (single !== null) { cardProps["data-single"] = true; }
|
|
2844
|
+
return REACT.createElement("div", cardProps, children);
|
|
2845
|
+
}
|
|
2846
|
+
/* ======================================================================
|
|
2847
|
+
* 模块 5b:官方改动卡数据适配层(A 线)
|
|
2848
|
+
*
|
|
2849
|
+
* 官方那张卡不自己算数据,它向两条同源路由要东西:
|
|
2850
|
+
* GET api/changes.summary?sessionId=…&seq=…
|
|
2851
|
+
* GET api/changes.diff?sessionId=…&seq=…&index=…
|
|
2852
|
+
* 而这两条路由的数据只活在该 Session 的 Host 内存里,会话一释放就 404。本模块把
|
|
2853
|
+
* 会话日志里已聚合好的改动(meta.diffs)在**客户端**按官方形状重算一遍:官方 404 时
|
|
2854
|
+
* 由包装过的 fetch 直接顶上 200,于是**官方组件原生渲染** —— 卡头、hover 预览、
|
|
2855
|
+
* 右侧 Review 面板全是官方的,我们一个像素都不用画。
|
|
2856
|
+
*
|
|
2857
|
+
* 三条硬约束:
|
|
2858
|
+
* 1. 只拦这两个路径、且只对索引里存在的坐标生效;其余请求原样透传给原生 fetch。
|
|
2859
|
+
* 2. 坐标必须在官方组件发请求**之前**进索引 → 卡片在渲染期登记(见 changesCard)。
|
|
2860
|
+
* 3. 形状必须过官方 validator(deliverables 的 isChangesSummary / isChangesDiff):
|
|
2861
|
+
* summary = { turn, files:[{path, display, added, deleted}], total, added, deleted }
|
|
2862
|
+
* diff = { kind:"text", path, display, before, after, coarse,
|
|
2863
|
+
* hunks:[{ oldStart, oldLines, newStart, newLines, lines:["+…"] }] }
|
|
2864
|
+
* 口径差异(有意为之):官方是「回合开始快照 → 回合结束」的净变化,所以同文件多段
|
|
2865
|
+
* 改动链不上时,我们给首段 oldText → 末段 newText 的净变化,数字因此与旧卡的
|
|
2866
|
+
* 分段累加不同(例如 +16 −13 vs +2 −12)。
|
|
2867
|
+
* ====================================================================== */
|
|
2868
|
+
var ADAPTER_SUMMARY_SUFFIX = "/api/changes.summary";
|
|
2869
|
+
var ADAPTER_DIFF_SUFFIX = "/api/changes.diff";
|
|
2870
|
+
var ADAPTER_CONTEXT_LINES = 3;
|
|
2871
|
+
var ADAPTER_MAX_HUNK_LINES = 2000;
|
|
2872
|
+
|
|
2873
|
+
var changesCoordinates = {}; /* "sessionId:seq" -> entry */
|
|
2874
|
+
var adapterInstalled = false;
|
|
2875
|
+
var adapterOriginalFetch = null;
|
|
2876
|
+
var adapterPatchedFetch = null;
|
|
2877
|
+
var adapterCtx = null;
|
|
2878
|
+
|
|
2879
|
+
/* 显示口径照官方:工作目录内用相对路径,否则给绝对路径(家目录 ~ 归一化暂缺)。 */
|
|
2880
|
+
function changesDisplayOf(cwd, p) {
|
|
2881
|
+
var abs = String(p);
|
|
2882
|
+
if (cwd) {
|
|
2883
|
+
var base = String(cwd).replace(/[\\/]+$/, "");
|
|
2884
|
+
var low = abs.toLowerCase(), lowBase = base.toLowerCase();
|
|
2885
|
+
if (low === lowBase) { return abs; }
|
|
2886
|
+
if (low.indexOf(lowBase + "\\") === 0 || low.indexOf(lowBase + "/") === 0) {
|
|
2887
|
+
return abs.slice(base.length + 1).replace(/\//g, "\\");
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
return abs.replace(/\//g, "\\");
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
/* 行 → 官方 hunk(3 行上下文、1 起始行号、每行带 +/-/空格 前缀)。 */
|
|
2894
|
+
function changesRowsToHunks(rows) {
|
|
2895
|
+
var i, row, idx = [];
|
|
2896
|
+
for (i = 0; i < rows.length; i++) { if (rows[i].k !== " ") { idx[i] = 1; } }
|
|
2897
|
+
var changed = [];
|
|
2898
|
+
for (i = 0; i < rows.length; i++) { if (idx[i] === 1) { changed.push(i); } }
|
|
2899
|
+
if (changed.length === 0) { return []; }
|
|
2900
|
+
var ranges = [];
|
|
2901
|
+
var start = Math.max(0, changed[0] - ADAPTER_CONTEXT_LINES);
|
|
2902
|
+
var end = Math.min(rows.length - 1, changed[0] + ADAPTER_CONTEXT_LINES);
|
|
2903
|
+
for (i = 1; i < changed.length; i++) {
|
|
2904
|
+
if (changed[i] - ADAPTER_CONTEXT_LINES <= end + 1) {
|
|
2905
|
+
end = Math.min(rows.length - 1, changed[i] + ADAPTER_CONTEXT_LINES);
|
|
2906
|
+
} else {
|
|
2907
|
+
ranges.push([start, end]);
|
|
2908
|
+
start = Math.max(0, changed[i] - ADAPTER_CONTEXT_LINES);
|
|
2909
|
+
end = Math.min(rows.length - 1, changed[i] + ADAPTER_CONTEXT_LINES);
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
ranges.push([start, end]);
|
|
2913
|
+
var hunks = [], oldNo = 1, newNo = 1, cursor = 0, budget = ADAPTER_MAX_HUNK_LINES, at;
|
|
2914
|
+
for (var r = 0; r < ranges.length; r++) {
|
|
2915
|
+
var from = ranges[r][0], to = ranges[r][1];
|
|
2916
|
+
while (cursor < from) {
|
|
2917
|
+
row = rows[cursor];
|
|
2918
|
+
if (row.k !== "+") { oldNo++; }
|
|
2919
|
+
if (row.k !== "-") { newNo++; }
|
|
2920
|
+
cursor++;
|
|
2921
|
+
}
|
|
2922
|
+
var oldStart = oldNo, newStart = newNo, lines = [], oldLines = 0, newLines = 0;
|
|
2923
|
+
for (at = from; at <= to && budget > 0; at++, budget--) {
|
|
2924
|
+
row = rows[at];
|
|
2925
|
+
lines.push((row.k === " " ? " " : row.k) + row.t);
|
|
2926
|
+
if (row.k !== "+") { oldLines++; oldNo++; }
|
|
2927
|
+
if (row.k !== "-") { newLines++; newNo++; }
|
|
2928
|
+
}
|
|
2929
|
+
cursor = to + 1;
|
|
2930
|
+
hunks.push({ oldStart: oldStart, oldLines: oldLines, newStart: newStart, newLines: newLines, lines: lines });
|
|
2931
|
+
if (budget <= 0) { break; }
|
|
2932
|
+
}
|
|
2933
|
+
return hunks;
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
function buildChangesEntry(sessionId, seq, turn, files, cwd) {
|
|
2937
|
+
var out = [], i, file, parts, before, after, rows, counts;
|
|
2938
|
+
for (i = 0; i < files.length; i++) {
|
|
2939
|
+
file = files[i] || {};
|
|
2940
|
+
parts = Array.isArray(file.parts) ? file.parts : [];
|
|
2941
|
+
before = parts.length > 0 ? parts[0][0] : null;
|
|
2942
|
+
after = parts.length > 0 ? parts[parts.length - 1][1] : null;
|
|
2943
|
+
rows = diffRows(before, after);
|
|
2944
|
+
counts = countRows(rows);
|
|
2945
|
+
out.push({
|
|
2946
|
+
path: String(file.path),
|
|
2947
|
+
display: changesDisplayOf(cwd, file.path),
|
|
2948
|
+
added: counts.added,
|
|
2949
|
+
deleted: counts.deleted,
|
|
2950
|
+
before: before != null,
|
|
2951
|
+
after: after != null,
|
|
2952
|
+
hunks: changesRowsToHunks(rows)
|
|
2953
|
+
});
|
|
2954
|
+
}
|
|
2955
|
+
out.sort(function (left, right) { return left.display < right.display ? -1 : (left.display > right.display ? 1 : 0); });
|
|
2956
|
+
return { sessionId: String(sessionId), seq: String(seq), turn: turn, files: out, stateFiles: files };
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
/* 会话 cwd:官方 display 的基准。取不到就退化为绝对路径(不影响校验,只是显示不同)。 */
|
|
2960
|
+
function changesCwdOf(sessionId) {
|
|
2961
|
+
try {
|
|
2962
|
+
var svc = (adapterCtx && typeof adapterCtx.get === "function") ? adapterCtx.get("sessions") : null;
|
|
2963
|
+
if (!svc) { svc = pickService(null, "sessions"); }
|
|
2964
|
+
var store = svc && svc.list;
|
|
2965
|
+
var snap = store && typeof store.getSnapshot === "function" ? store.getSnapshot() : null;
|
|
2966
|
+
var meta = snap && snap.byId ? snap.byId[sessionId] : null;
|
|
2967
|
+
return meta && typeof meta.cwd === "string" ? meta.cwd : null;
|
|
2968
|
+
} catch (e) { return null; }
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
/* 渲染期登记:必须早于官方组件的 useEffect(React 先跑完所有 slot 的 render)。
|
|
2972
|
+
turn 来自 slot props 里的 TurnLocation(owner.turn.turn),Definition 发布的状态里
|
|
2973
|
+
只有 {seq, files} —— 所以这里显式收 turn,缺了就退回 state.turn。 */
|
|
2974
|
+
function registerChangesCoordinates(sessionId, seq, state, turn) {
|
|
2975
|
+
if (!sessionId || seq == null || !state || !Array.isArray(state.files) || state.files.length === 0) { return; }
|
|
2976
|
+
var turnNumber = (typeof turn === "number" && Number.isSafeInteger(turn) && turn >= 1) ? turn
|
|
2977
|
+
: (typeof state.turn === "number" && Number.isSafeInteger(state.turn) && state.turn >= 1 ? state.turn : null);
|
|
2978
|
+
if (turnNumber === null) { return; } /* 官方 validator 要求 turn 是 ≥1 的整数,缺了宁可不拦 */
|
|
2979
|
+
var key = String(sessionId) + ":" + String(seq);
|
|
2980
|
+
var current = changesCoordinates[key];
|
|
2981
|
+
if (current && current.stateFiles === state.files && current.turn === turnNumber) { return; }
|
|
2982
|
+
changesCoordinates[key] = buildChangesEntry(sessionId, seq, turnNumber, state.files, changesCwdOf(sessionId));
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
function changesEntryFor(sessionId, seq) {
|
|
2986
|
+
if (!sessionId || seq == null) { return null; }
|
|
2987
|
+
return changesCoordinates[String(sessionId) + ":" + String(seq)] || null;
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
function changesSummaryPayload(entry) {
|
|
2991
|
+
var files = [], added = 0, deleted = 0, i, f;
|
|
2992
|
+
for (i = 0; i < entry.files.length; i++) {
|
|
2993
|
+
f = entry.files[i];
|
|
2994
|
+
files.push({ path: f.path, display: f.display, added: f.added, deleted: f.deleted });
|
|
2995
|
+
added += f.added;
|
|
2996
|
+
deleted += f.deleted;
|
|
2997
|
+
}
|
|
2998
|
+
return { turn: entry.turn, files: files, total: files.length, added: added, deleted: deleted };
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
function changesDiffPayload(entry, index) {
|
|
3002
|
+
var f = entry.files[index];
|
|
3003
|
+
if (!f) { return null; }
|
|
3004
|
+
return { kind: "text", path: f.path, display: f.display, before: f.before, after: f.after, coarse: false, hunks: f.hunks };
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
function changesJsonResponse(payload) {
|
|
3008
|
+
try {
|
|
3009
|
+
return new Response(JSON.stringify(payload), {
|
|
3010
|
+
status: 200,
|
|
3011
|
+
headers: { "content-type": "application/json", "cache-control": "no-store" }
|
|
3012
|
+
});
|
|
3013
|
+
} catch (e) { return null; }
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
function installChangesFetchAdapter() {
|
|
3017
|
+
if (adapterInstalled) { return; }
|
|
3018
|
+
if (typeof window === "undefined" || typeof window.fetch !== "function") { return; }
|
|
3019
|
+
var original = window.fetch;
|
|
3020
|
+
var patched = function (input, init) {
|
|
3021
|
+
var raw = "";
|
|
3022
|
+
try { raw = typeof input === "string" ? input : ((input && input.url) || ""); } catch (e0) { raw = ""; }
|
|
3023
|
+
var url = null;
|
|
3024
|
+
try { url = new URL(raw, (typeof location !== "undefined" && location.href) ? location.href : "http://localhost/"); } catch (e1) { url = null; }
|
|
3025
|
+
if (!url) { return original.apply(this, arguments); }
|
|
3026
|
+
var path = url.pathname || "";
|
|
3027
|
+
var isSummary = path.length >= ADAPTER_SUMMARY_SUFFIX.length && path.slice(-ADAPTER_SUMMARY_SUFFIX.length) === ADAPTER_SUMMARY_SUFFIX;
|
|
3028
|
+
var isDiff = path.length >= ADAPTER_DIFF_SUFFIX.length && path.slice(-ADAPTER_DIFF_SUFFIX.length) === ADAPTER_DIFF_SUFFIX;
|
|
3029
|
+
if (!isSummary && !isDiff) { return original.apply(this, arguments); }
|
|
3030
|
+
var sessionId = url.searchParams.get("sessionId");
|
|
3031
|
+
var seq = url.searchParams.get("seq");
|
|
3032
|
+
var entry = changesEntryFor(sessionId, seq);
|
|
3033
|
+
if (!entry) { return original.apply(this, arguments); }
|
|
3034
|
+
var payload = isSummary ? changesSummaryPayload(entry) : changesDiffPayload(entry, Number(url.searchParams.get("index")));
|
|
3035
|
+
if (payload === null) { return original.apply(this, arguments); }
|
|
3036
|
+
var response = changesJsonResponse(payload);
|
|
3037
|
+
if (!response) { return original.apply(this, arguments); }
|
|
3038
|
+
try {
|
|
3039
|
+
console.info("[dsh-ui-tweaks:" + BUILD + "] 改动卡数据由会话日志重建 · " + (isSummary ? "summary" : "diff #" + url.searchParams.get("index")) + " · turn " + entry.turn);
|
|
3040
|
+
} catch (e2) {}
|
|
3041
|
+
return Promise.resolve(response);
|
|
3042
|
+
};
|
|
3043
|
+
adapterOriginalFetch = original;
|
|
3044
|
+
adapterPatchedFetch = patched;
|
|
3045
|
+
window.fetch = patched;
|
|
3046
|
+
adapterInstalled = true;
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
function uninstallChangesFetchAdapter() {
|
|
3050
|
+
if (!adapterInstalled) { return; }
|
|
3051
|
+
if (typeof window !== "undefined" && window.fetch === adapterPatchedFetch && adapterOriginalFetch) {
|
|
3052
|
+
window.fetch = adapterOriginalFetch;
|
|
2468
3053
|
}
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
}, children);
|
|
3054
|
+
adapterInstalled = false;
|
|
3055
|
+
adapterPatchedFetch = null;
|
|
3056
|
+
adapterOriginalFetch = null;
|
|
2473
3057
|
}
|
|
3058
|
+
|
|
2474
3059
|
function installChangesCard(scope, ctx) {
|
|
2475
3060
|
if (!REACT) { throw new Error("ui-tweaks: require(\"react\") 不可用,跳过改动补位卡"); }
|
|
2476
3061
|
var s = scope || ctx || {};
|
|
@@ -2482,7 +3067,41 @@ window.__ModuleLoader__.load({
|
|
|
2482
3067
|
if (!slots || typeof slots.register !== "function" || typeof slots.inject !== "function") {
|
|
2483
3068
|
throw new Error("ui-tweaks: slots 服务不可用");
|
|
2484
3069
|
}
|
|
2485
|
-
|
|
3070
|
+
var definition = changesDefinition();
|
|
3071
|
+
/* 模块 5b:先装数据适配层。装了之后,已释放会话里官方卡会自己长出来(我们这张卡
|
|
3072
|
+
因为探测同样被答成 200 而自动让位);活会话不受影响(原生 200 优先,我们不拦)。 */
|
|
3073
|
+
adapterCtx = ctx || null;
|
|
3074
|
+
guard("changes-adapter", installChangesFetchAdapter);
|
|
3075
|
+
try {
|
|
3076
|
+
if (ctx && typeof ctx.effect === "function") {
|
|
3077
|
+
ctx.effect(function () { return uninstallChangesFetchAdapter; });
|
|
3078
|
+
}
|
|
3079
|
+
} catch (e) {}
|
|
3080
|
+
/* 回归闸:buildLocationData 是卡片唯一的取数通路,缺了它 state 恒 null、卡片静默
|
|
3081
|
+
不画(0.2.0 的真实缺陷)。这里宁可显式报错,也不要再静默消失一次。 */
|
|
3082
|
+
if (typeof definition.buildLocationData !== "function") {
|
|
3083
|
+
throw new Error("ui-tweaks: changesDefinition 缺 buildLocationData,补位卡不会渲染");
|
|
3084
|
+
}
|
|
3085
|
+
conv.events.register(definition);
|
|
3086
|
+
/* 官方卡的开关(「显示编码工具视图」)。拿不到就保持 null → useDevToolsEnabled 按
|
|
3087
|
+
「可能开着」走探测,行为与 0.2.0 一致。configForms 缺失时 ctx.inject 的回调不会
|
|
3088
|
+
执行(Cordis 语义),所以这里不当作错误。 */
|
|
3089
|
+
try {
|
|
3090
|
+
ctx.inject(["configForms"], function (cfg) {
|
|
3091
|
+
try {
|
|
3092
|
+
var cf = cfg && cfg.configForms ? cfg.configForms : cfg;
|
|
3093
|
+
if (cf && cf.developerTools && cf.developerTools.enabled) { devToolsStore = cf.developerTools.enabled; }
|
|
3094
|
+
} catch (e) {}
|
|
3095
|
+
});
|
|
3096
|
+
} catch (e) {}
|
|
3097
|
+
try {
|
|
3098
|
+
ctx.inject(["sidebarRight"], function (right) {
|
|
3099
|
+
try {
|
|
3100
|
+
var svc = right && right.sidebarRight ? right.sidebarRight : right;
|
|
3101
|
+
if (svc && typeof svc.openResource === "function") { sidebarRightForChanges = svc; }
|
|
3102
|
+
} catch (e) {}
|
|
3103
|
+
});
|
|
3104
|
+
} catch (e) {}
|
|
2486
3105
|
slots.inject("conversation.chat.turnTail", function () {
|
|
2487
3106
|
return slots.register({
|
|
2488
3107
|
name: "conversation.chat.turnTail",
|