dsh-vscode-mode 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -5
- package/lib/client.js +1613 -1317
- package/lib/client.js.map +2 -2
- package/lib/index.js +7028 -6516
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/downloadFile.ts +36 -6
- package/src/client/downloadSink.ts +162 -0
- package/src/client/reviewBar.ts +3 -0
- package/src/client/reviewBatch.ts +9 -1
- package/src/client/reviewKeepAll.ts +72 -0
- package/src/client/state/diffSummary.ts +34 -7
- package/src/client/ui/DownloadPicker.ts +2 -1
- package/src/client/ui/EditorView.ts +117 -29
- package/src/client/ui/ReviewAllConfirm.ts +1 -1
- package/src/index.ts +1 -1
- package/src/pending/persist.ts +94 -5
- package/src/pending/store.ts +76 -1
- package/src/pendingCompat.ts +80 -9
- package/src/pendingRpc.ts +7 -6
- package/src/reviewDb.ts +119 -8
- package/src/reviewRpc.ts +110 -3
- package/src/rpc.ts +14 -10
- package/src/shared/review.ts +21 -0
- package/src/shared/rpc.ts +3 -1
- package/src/store.ts +44 -0
package/lib/client.js
CHANGED
|
@@ -3686,7 +3686,15 @@ window.__ModuleLoader__.load({
|
|
|
3686
3686
|
if (page.nextCursor !== null || page.items.length !== page.pending) throw new Error("待审记录数量不完整,未执行批量操作");
|
|
3687
3687
|
return page.items;
|
|
3688
3688
|
}
|
|
3689
|
-
/**
|
|
3689
|
+
/**
|
|
3690
|
+
* @public
|
|
3691
|
+
* @author ddj 2026年10月27号
|
|
3692
|
+
* @description: 逐条决策引擎,现仅供 Undo All 使用(Keep 已改走 `reviewKeepAll.ts` 的宿主批量通道);
|
|
3693
|
+
* 保留 reject 形态以便对照既有契约与测试。
|
|
3694
|
+
* @param port Guarded existing per-file decision adapter.
|
|
3695
|
+
* @param reject Undo instead of Keep.
|
|
3696
|
+
* @returns Precise partial result; Undo stops at first conflict, never claims atomic multi-file rollback.
|
|
3697
|
+
*/
|
|
3690
3698
|
async function runReviewBatch(port, reject) {
|
|
3691
3699
|
const result = {
|
|
3692
3700
|
completed: 0,
|
|
@@ -3721,6 +3729,1278 @@ window.__ModuleLoader__.load({
|
|
|
3721
3729
|
return result;
|
|
3722
3730
|
}
|
|
3723
3731
|
//#endregion
|
|
3732
|
+
//#region src/client/reviewKeepAll.ts
|
|
3733
|
+
/**
|
|
3734
|
+
* @private
|
|
3735
|
+
* @author ddj 2026年11月03号
|
|
3736
|
+
* @description: 取一次完整待审快照;完整性校验失败时重取一次,避免瞬时漂移被当成不可恢复错误。
|
|
3737
|
+
* @param port 已鉴权的 RPC 适配器
|
|
3738
|
+
* @returns 冻结的批量目标
|
|
3739
|
+
* @throws 两次都取不到一致快照时抛出原始原因
|
|
3740
|
+
*/
|
|
3741
|
+
async function snapshotOnce(port) {
|
|
3742
|
+
try {
|
|
3743
|
+
return toItems(await loadPending(port));
|
|
3744
|
+
} catch {
|
|
3745
|
+
port.guard();
|
|
3746
|
+
return toItems(await loadPending(port));
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
/**
|
|
3750
|
+
* @private
|
|
3751
|
+
* @author ddj 2026年11月03号
|
|
3752
|
+
* @description: 把快照条目裁成批量目标,只带 host 需要的身份与版本线索。
|
|
3753
|
+
* @param items 完整待审集
|
|
3754
|
+
* @returns 批量目标
|
|
3755
|
+
*/
|
|
3756
|
+
function toItems(items) {
|
|
3757
|
+
return items.map((item) => ({
|
|
3758
|
+
id: item.id,
|
|
3759
|
+
display: item.display,
|
|
3760
|
+
seq: item.seq,
|
|
3761
|
+
revision: item.revision
|
|
3762
|
+
}));
|
|
3763
|
+
}
|
|
3764
|
+
/**
|
|
3765
|
+
* @public
|
|
3766
|
+
* @author ddj 2026年11月03号
|
|
3767
|
+
* @description: 批量保留全部待审记录(Keep 不改写磁盘)。一次往返完成,跳过项只进 skipped,
|
|
3768
|
+
* 段级异常才写 error;guard 失效时保留已完成计数交给调用方呈现。
|
|
3769
|
+
* @param port 已鉴权的 RPC 适配器
|
|
3770
|
+
* @returns 与 BatchResult 同构的批量结果(含跳过明细)
|
|
3771
|
+
*/
|
|
3772
|
+
async function runKeepAll(port) {
|
|
3773
|
+
const result = {
|
|
3774
|
+
completed: 0,
|
|
3775
|
+
total: 0,
|
|
3776
|
+
paths: [],
|
|
3777
|
+
skipped: [],
|
|
3778
|
+
error: ""
|
|
3779
|
+
};
|
|
3780
|
+
try {
|
|
3781
|
+
port.guard();
|
|
3782
|
+
const items = await snapshotOnce(port);
|
|
3783
|
+
result.total = items.length;
|
|
3784
|
+
if (!items.length) return result;
|
|
3785
|
+
port.guard();
|
|
3786
|
+
const reply = await port.keepAll(items, crypto.randomUUID());
|
|
3787
|
+
if (!reply.ok) {
|
|
3788
|
+
result.error = reply.error;
|
|
3789
|
+
return result;
|
|
3790
|
+
}
|
|
3791
|
+
return reply.result;
|
|
3792
|
+
} catch (cause) {
|
|
3793
|
+
result.error = cause instanceof Error ? cause.message : String(cause);
|
|
3794
|
+
return result;
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
//#endregion
|
|
3798
|
+
//#region src/shared/externalOpen.ts
|
|
3799
|
+
/**
|
|
3800
|
+
* dsh-vscode-mode shared — 外部深链 URL 契约(Windows launcher / Unity 包 / client 共用)。
|
|
3801
|
+
* 形态:?<FLAG>=1&<PATHS_KEY>=<enc1>[,<enc2>…] [&<LINE_KEY>=N] [&<COLUMN_KEY>=M]
|
|
3802
|
+
* 每个路径独立 percent-encode(encodeURIComponent / Uri.EscapeDataString)后以 `,` 连接:
|
|
3803
|
+
* 编码结果不含裸逗号(Windows 路径本身不允许逗号,编码兜底其余字符),URLSearchParams
|
|
3804
|
+
* 取值做一次标准解码即得 `enc1,enc2`,按 `,` split 即路径,无需二次 decode。
|
|
3805
|
+
* Purity rule: no node/react imports(URLSearchParams 为标准全局)。
|
|
3806
|
+
* 作者 ddj 2026-09-07
|
|
3807
|
+
*/
|
|
3808
|
+
/** 深链开关参数名(值恒为 '1')。 */
|
|
3809
|
+
const EDRV_OPEN_FLAG = "edrvOpen";
|
|
3810
|
+
/** 路径列表参数名(逗号分隔的已编码路径)。 */
|
|
3811
|
+
const EDRV_PATHS_KEY = "edrvPaths";
|
|
3812
|
+
/** 目标行参数名(1-based,可选,作用于首个路径)。 */
|
|
3813
|
+
const EDRV_LINE_KEY = "edrvLine";
|
|
3814
|
+
/** 目标列参数名(1-based,可选,作用于首个路径)。 */
|
|
3815
|
+
const EDRV_COLUMN_KEY = "edrvColumn";
|
|
3816
|
+
/** 深链参数键全集(URL 清理用)。 */
|
|
3817
|
+
const EDRV_PARAM_KEYS = [
|
|
3818
|
+
EDRV_OPEN_FLAG,
|
|
3819
|
+
EDRV_PATHS_KEY,
|
|
3820
|
+
EDRV_LINE_KEY,
|
|
3821
|
+
EDRV_COLUMN_KEY
|
|
3822
|
+
];
|
|
3823
|
+
/**
|
|
3824
|
+
* 解析深链查询串:非深链/路径为空 → null;行列仅接受正整数,非法忽略。
|
|
3825
|
+
* @author ddj 2026年09月07号
|
|
3826
|
+
* @param search location.search(含或不含前导 ? 均可)
|
|
3827
|
+
* @returns 深链参数或 null
|
|
3828
|
+
*/
|
|
3829
|
+
function parseOpenParams(search) {
|
|
3830
|
+
const params = new URLSearchParams(search ?? "");
|
|
3831
|
+
if (params.get("edrvOpen") !== "1") return null;
|
|
3832
|
+
const paths = (params.get("edrvPaths") ?? "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
3833
|
+
if (!paths.length) return null;
|
|
3834
|
+
return {
|
|
3835
|
+
paths,
|
|
3836
|
+
line: positiveIntOf(params.get(EDRV_LINE_KEY)),
|
|
3837
|
+
column: positiveIntOf(params.get(EDRV_COLUMN_KEY))
|
|
3838
|
+
};
|
|
3839
|
+
}
|
|
3840
|
+
/**
|
|
3841
|
+
* 正整数解析(非法/非正 → undefined)。
|
|
3842
|
+
* @author ddj 2026年09月07号
|
|
3843
|
+
* @param raw 参数原文(可空)
|
|
3844
|
+
* @returns 正整数或 undefined
|
|
3845
|
+
*/
|
|
3846
|
+
function positiveIntOf(raw) {
|
|
3847
|
+
const value = Number(raw);
|
|
3848
|
+
return Number.isInteger(value) && value > 0 ? value : void 0;
|
|
3849
|
+
}
|
|
3850
|
+
//#endregion
|
|
3851
|
+
//#region src/client/chooseDialog.ts
|
|
3852
|
+
/**
|
|
3853
|
+
* dsh-vscode-mode client — 工作区选择弹窗(深链打开规则 1)。
|
|
3854
|
+
* 复用与「添加 MCP」一致的 vsm-mcp-modal / vsm-mcp-dialog 模态(页面居中卡片 + 遮罩 + 主题
|
|
3855
|
+
* token),React portal 挂 body;Esc / 遮罩点击 / 取消 → null(中止流程不改任何状态)。
|
|
3856
|
+
* 作者 ddj 2026-09-07
|
|
3857
|
+
*/
|
|
3858
|
+
/** 选择弹窗(vsm-mcp-modal 遮罩 + vsm-mcp-dialog 卡片,Esc/遮罩点击取消)。 */
|
|
3859
|
+
function ChoiceModal({ title, folder, done }) {
|
|
3860
|
+
react$1.default.useEffect(() => {
|
|
3861
|
+
const onKey = (event) => {
|
|
3862
|
+
if (event.key === "Escape") done(null);
|
|
3863
|
+
};
|
|
3864
|
+
window.addEventListener("keydown", onKey);
|
|
3865
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
3866
|
+
}, []);
|
|
3867
|
+
return react$1.default.createElement("div", {
|
|
3868
|
+
className: "vsm-mcp-modal",
|
|
3869
|
+
onMouseDown: (event) => {
|
|
3870
|
+
if (event.target === event.currentTarget) done(null);
|
|
3871
|
+
}
|
|
3872
|
+
}, react$1.default.createElement("div", { className: "vsm-mcp-dialog" }, react$1.default.createElement("h3", null, "在 DSH 中打开"), react$1.default.createElement("label", null, "文件夹 " + folder + " 位于已注册的工作区「" + title + "」内,选择打开方式:"), react$1.default.createElement("div", { className: "vsm-mcp-dialog-actions" }, react$1.default.createElement("button", { onClick: () => done(null) }, "取消"), react$1.default.createElement("button", { onClick: () => done("create") }, "新建工作区"), react$1.default.createElement("button", {
|
|
3873
|
+
className: "vsm-primary",
|
|
3874
|
+
onClick: () => done("recent")
|
|
3875
|
+
}, "使用最近的工作区"))));
|
|
3876
|
+
}
|
|
3877
|
+
/**
|
|
3878
|
+
* 弹出工作区选择弹窗(深链打开规则 1)。
|
|
3879
|
+
* @author ddj 2026年09月07号
|
|
3880
|
+
* @param title 匹配到的工作区标题
|
|
3881
|
+
* @param folder 文件夹路径
|
|
3882
|
+
* @returns 用户选择;null = 取消/中止
|
|
3883
|
+
*/
|
|
3884
|
+
function chooseWorkspace(title, folder) {
|
|
3885
|
+
return new Promise((resolve) => {
|
|
3886
|
+
const host = document.createElement("div");
|
|
3887
|
+
document.body.appendChild(host);
|
|
3888
|
+
const root = (0, react_dom_client.createRoot)(host);
|
|
3889
|
+
const done = (choice) => {
|
|
3890
|
+
root.unmount();
|
|
3891
|
+
host.remove();
|
|
3892
|
+
resolve(choice);
|
|
3893
|
+
};
|
|
3894
|
+
root.render(react$1.default.createElement(ChoiceModal, {
|
|
3895
|
+
title,
|
|
3896
|
+
folder,
|
|
3897
|
+
done
|
|
3898
|
+
}));
|
|
3899
|
+
});
|
|
3900
|
+
}
|
|
3901
|
+
//#endregion
|
|
3902
|
+
//#region src/client/addToConversation.ts
|
|
3903
|
+
/**
|
|
3904
|
+
* dsh-vscode-mode client — 把文件/选中内容注入会话对话输入框(composer draft)。
|
|
3905
|
+
* 复用 DSH 官方输入门面:ctx.conversation.input.for(sessions.scope(sessionId)) → SessionInput。
|
|
3906
|
+
* 引用 chip:insertReference(reference source,发送时序列化为 @path 文本);忙态降级为纯文本 setDraft。
|
|
3907
|
+
* 纯函数部分 node 可单测。
|
|
3908
|
+
* 作者 ddj 2026-08-25
|
|
3909
|
+
*/
|
|
3910
|
+
/** 把追加引用结果映射为可读文案(ok 用 okText;busy 提示已降级纯文本;unavailable 提示不可用)。
|
|
3911
|
+
* @author ddj 2026年09月03号 / 2026年09月11号
|
|
3912
|
+
* @param outcome 追加结果状态
|
|
3913
|
+
* @param okText 成功文案(如「已添加文件引用」/「已添加文件夹引用」)
|
|
3914
|
+
* @returns 状态栏/通知用文案
|
|
3915
|
+
*/
|
|
3916
|
+
function statusOfAdd(outcome, okText) {
|
|
3917
|
+
if (outcome === "ok") return okText;
|
|
3918
|
+
if (outcome === "busy") return okText + "(已降级纯文本)";
|
|
3919
|
+
if (outcome === "failed") return "添加引用失败(输入框忙或未就绪,请重试)";
|
|
3920
|
+
return "无法添加到对话(无会话或输入框不可用)";
|
|
3921
|
+
}
|
|
3922
|
+
/** DSH reference source 名(dsh-client-ui-reference 注册的 @file/@session 统一源)。 */
|
|
3923
|
+
const REF_SOURCE = "reference";
|
|
3924
|
+
/**
|
|
3925
|
+
* 把 clipboard 投影的末尾折算成 **detect 投影**的末尾。
|
|
3926
|
+
*
|
|
3927
|
+
* ⚠️ 两个投影对 chip 的计长不同,混用会直接毁掉已插入的引用(真实缺陷,勿改回 `draft.length`):
|
|
3928
|
+
* DSH 的 `$composerLayout()` 中 chip 在 detect 投影里只贡献 **1 个 U+FFFC**,
|
|
3929
|
+
* 在 clipboard 投影里贡献 `clipboardText` 全文
|
|
3930
|
+
* (`pushLeaf('chip', kid, '', kid.getTextContent())`)。
|
|
3931
|
+
* 而 `insertReference(ref, span)` 的 span 走 detect 坐标并经 `selectSpan` 校验:
|
|
3932
|
+
* `span.end > layout.detectLength` 时返回 null → 插入失败。
|
|
3933
|
+
*
|
|
3934
|
+
* 旧实现直接用 `draft.length`(clipboard 长度)当 detect 起点:草稿里一旦有 chip,
|
|
3935
|
+
* 该值就超出 detect 长度 → 插入被拒 → 落入 `setDraft` 兜底,
|
|
3936
|
+
* 而 `setDraft` 会整篇重建并剔除 U+FFFC(`text.replace(REFERENCE_PLACEHOLDER_RE, '')`),
|
|
3937
|
+
* 于是**既有 chip 被销毁、引用退化为重复的纯文本**(即「连续添加多个引用显示异常」)。
|
|
3938
|
+
*
|
|
3939
|
+
* 换算:每个 chip 在 clipboard 中多算了 `length - 1`,逐个扣回即得 detect 末尾。
|
|
3940
|
+
* 无 occurrences(旧版 DSH)时退化为 `draft.length`,与旧行为一致。
|
|
3941
|
+
* @author ddj 2026年09月11号
|
|
3942
|
+
* @param snapshot 输入快照
|
|
3943
|
+
* @returns detect 投影中的文档末尾偏移
|
|
3944
|
+
*/
|
|
3945
|
+
function detectEndOf(snapshot) {
|
|
3946
|
+
const total = String(snapshot?.draft ?? "").length;
|
|
3947
|
+
const occurrences = snapshot?.occurrences;
|
|
3948
|
+
if (!Array.isArray(occurrences) || !occurrences.length) return total;
|
|
3949
|
+
let clipped = 0;
|
|
3950
|
+
for (const occurrence of occurrences) {
|
|
3951
|
+
const length = Number(occurrence?.length ?? 0);
|
|
3952
|
+
if (!Number.isFinite(length) || length <= 1) continue;
|
|
3953
|
+
clipped += length - 1;
|
|
3954
|
+
}
|
|
3955
|
+
return Math.max(0, total - clipped);
|
|
3956
|
+
}
|
|
3957
|
+
/** chip 列表按 clipboard 偏移升序(官方已排序,此处防御性归一,丢弃非法项)。 */
|
|
3958
|
+
function chipsOf(snapshot) {
|
|
3959
|
+
const occurrences = snapshot?.occurrences;
|
|
3960
|
+
if (!Array.isArray(occurrences) || !occurrences.length) return [];
|
|
3961
|
+
const out = [];
|
|
3962
|
+
for (const occurrence of occurrences) {
|
|
3963
|
+
const offset = Number(occurrence?.offset ?? NaN);
|
|
3964
|
+
const length = Number(occurrence?.length ?? NaN);
|
|
3965
|
+
if (!Number.isFinite(offset) || !Number.isFinite(length) || length <= 1) continue;
|
|
3966
|
+
out.push({
|
|
3967
|
+
offset: Math.max(0, offset),
|
|
3968
|
+
length
|
|
3969
|
+
});
|
|
3970
|
+
}
|
|
3971
|
+
return out.sort((a, b) => a.offset - b.offset);
|
|
3972
|
+
}
|
|
3973
|
+
/**
|
|
3974
|
+
* detect 偏移 → clipboard 偏移(paddingAt 用:draft 是 clipboard 投影,不能直接按下标取字符)。
|
|
3975
|
+
* 每个位于该偏移之前的 chip 在 clipboard 中多占 `length − 1`,累计补上即可。
|
|
3976
|
+
* @author ddj 2026年09月11号
|
|
3977
|
+
* @param snapshot 输入快照
|
|
3978
|
+
* @param detectOffset detect 投影偏移
|
|
3979
|
+
* @returns clipboard 投影偏移(已夹到 draft 范围内)
|
|
3980
|
+
*/
|
|
3981
|
+
function detectToClipboardOf(snapshot, detectOffset) {
|
|
3982
|
+
const total = String(snapshot?.draft ?? "").length;
|
|
3983
|
+
const target = Math.max(0, Math.min(Number(detectOffset) || 0, detectEndOf(snapshot)));
|
|
3984
|
+
let shift = 0;
|
|
3985
|
+
for (const chip of chipsOf(snapshot)) {
|
|
3986
|
+
if (chip.offset - shift >= target) break;
|
|
3987
|
+
shift += chip.length - 1;
|
|
3988
|
+
}
|
|
3989
|
+
return Math.max(0, Math.min(target + shift, total));
|
|
3990
|
+
}
|
|
3991
|
+
/**
|
|
3992
|
+
* 插入点两侧是否需要补空格(仅纯文本通道需要;chip 由 facade 自行补尾随空格)。
|
|
3993
|
+
*
|
|
3994
|
+
* 在 detect 坐标的任意位置求值,故**末尾追加的行为与旧实现逐字符一致**
|
|
3995
|
+
* (末尾前为非空白 → 前后各补一个空格;末尾前已是空白 → 只补尾随空格),
|
|
3996
|
+
* 同时正确支持「插在文本中间」不再贴字。
|
|
3997
|
+
* @author ddj 2026年09月11号
|
|
3998
|
+
* @param snapshot 输入快照
|
|
3999
|
+
* @param detectOffset detect 投影插入点
|
|
4000
|
+
* @returns 是否需要前导/尾随空格
|
|
4001
|
+
*/
|
|
4002
|
+
function paddingAt(snapshot, detectOffset) {
|
|
4003
|
+
const draft = String(snapshot?.draft ?? "");
|
|
4004
|
+
const at = detectToClipboardOf(snapshot, detectOffset);
|
|
4005
|
+
const before = at > 0 ? draft.slice(at - 1, at) : "";
|
|
4006
|
+
const after = at < draft.length ? draft.slice(at, at + 1) : "";
|
|
4007
|
+
return {
|
|
4008
|
+
lead: before !== "" && !/\s/.test(before),
|
|
4009
|
+
tail: after === "" || !/\s/.test(after)
|
|
4010
|
+
};
|
|
4011
|
+
}
|
|
4012
|
+
/**
|
|
4013
|
+
* 解析插入点(detect 坐标):优先用输入框**当前光标/选区**(`caretSpan()`,
|
|
4014
|
+
* 官方契约即 detect 坐标,无选区时回落文档末尾),并夹到 `[0, detectLength]`。
|
|
4015
|
+
*
|
|
4016
|
+
* 选区非塌缩时取 `end`(右边界)—— 插入而非替换,绝不删除用户已选中的内容。
|
|
4017
|
+
* caret 缺失/非法(旧版 facade 无该方法、抛异常、NaN 等)一律回落到文档末尾。
|
|
4018
|
+
* @author ddj 2026年09月11号
|
|
4019
|
+
* @param caret caretSpan() 的返回值(可为 null/非法)
|
|
4020
|
+
* @param detectLength detect 投影文档长度
|
|
4021
|
+
* @returns detect 投影插入偏移
|
|
4022
|
+
*/
|
|
4023
|
+
function insertOffsetOf(caret, detectLength) {
|
|
4024
|
+
const max = Math.max(0, Number(detectLength) || 0);
|
|
4025
|
+
if (!caret || typeof caret !== "object") return max;
|
|
4026
|
+
const span = caret;
|
|
4027
|
+
const raw = Number(span.end);
|
|
4028
|
+
const value = Number.isFinite(raw) ? raw : Number(span.start);
|
|
4029
|
+
if (!Number.isFinite(value)) return max;
|
|
4030
|
+
return Math.max(0, Math.min(value, max));
|
|
4031
|
+
}
|
|
4032
|
+
/**
|
|
4033
|
+
* 生成 DSH @file 语法引用串:cwd 相对化、\ → /、含空白时按 @"path" 语法加引号。
|
|
4034
|
+
* cwd 外/无法相对化的路径回退原路径。
|
|
4035
|
+
* @author ddj 2026年08月25号
|
|
4036
|
+
* @param path 打开的文件路径(相对 cwd 或绝对)
|
|
4037
|
+
* @param cwd 会话工作区目录(可选)
|
|
4038
|
+
* @returns 引用串(如 @src/index.ts 或 @"a b.ts")
|
|
4039
|
+
*/
|
|
4040
|
+
function mentionOf(path, cwd) {
|
|
4041
|
+
const raw = String(path);
|
|
4042
|
+
let rel = raw;
|
|
4043
|
+
if (cwd) {
|
|
4044
|
+
const base = String(cwd).replace(/[\\/]+$/, "");
|
|
4045
|
+
const up = raw.replace(/\\/g, "/");
|
|
4046
|
+
const baseUp = base.replace(/\\/g, "/");
|
|
4047
|
+
if (up.startsWith(baseUp + "/")) rel = up.slice(baseUp.length + 1);
|
|
4048
|
+
else if (up === baseUp) rel = "";
|
|
4049
|
+
}
|
|
4050
|
+
rel = rel.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
4051
|
+
if (!rel) rel = raw.replace(/\\/g, "/");
|
|
4052
|
+
if (/[\u0000-\u001f\u007f-\u009f"]/u.test(rel)) return "@" + raw;
|
|
4053
|
+
return /\s/u.test(rel) ? `@"${rel}"` : `@${rel}`;
|
|
4054
|
+
}
|
|
4055
|
+
/** 引用标签:文件名 + 可选行区间(chip 内展示,不带 @ 前缀)。
|
|
4056
|
+
* @author ddj 2026年08月25号 */
|
|
4057
|
+
function labelOf(path, range) {
|
|
4058
|
+
const base = String(path).split(/[\\/]/).pop() || String(path);
|
|
4059
|
+
if (!range || range.endLine < range.startLine) return base;
|
|
4060
|
+
return range.startLine === range.endLine ? `${base} L${range.startLine}` : `${base} L${range.startLine}-${range.endLine}`;
|
|
4061
|
+
}
|
|
4062
|
+
/** 引用 ref:引用串 + 可选行区间(序列化直出,agent 据此读取对应片段)。
|
|
4063
|
+
* @author ddj 2026年08月25号 */
|
|
4064
|
+
function refOf(mention, range) {
|
|
4065
|
+
if (!range || range.endLine < range.startLine) return mention;
|
|
4066
|
+
return range.startLine === range.endLine ? `${mention} L${range.startLine}` : `${mention} L${range.startLine}-${range.endLine}`;
|
|
4067
|
+
}
|
|
4068
|
+
/** 构造 DSH 文件引用插入载荷,模型文本、剪贴板投影与降级文本共用完整行区间。
|
|
4069
|
+
* @public
|
|
4070
|
+
* @author ddj 2026年10月28号
|
|
4071
|
+
* @returns 引用载荷与含可选行区间的完整引用文本
|
|
4072
|
+
* @param path 目标路径(相对 cwd 或绝对)
|
|
4073
|
+
* @param cwd 会话工作区目录(可选)
|
|
4074
|
+
* @param range 行区间(可选)
|
|
4075
|
+
* @param appearance 引用外观:'file'(默认)| 'folder'
|
|
4076
|
+
*/
|
|
4077
|
+
function buildFileRef(path, cwd, range, appearance = "file") {
|
|
4078
|
+
const mention = refOf(mentionOf(path, cwd), range);
|
|
4079
|
+
return {
|
|
4080
|
+
mention,
|
|
4081
|
+
reference: {
|
|
4082
|
+
source: REF_SOURCE,
|
|
4083
|
+
ref: mention,
|
|
4084
|
+
label: labelOf(path, range),
|
|
4085
|
+
appearance,
|
|
4086
|
+
clipboardText: mention
|
|
4087
|
+
}
|
|
4088
|
+
};
|
|
4089
|
+
}
|
|
4090
|
+
/** @private @author ddj 2026年10月29号 @param receipt Raw selected-channel receipt. @returns Explicit admission evidence and the historical boolean. */
|
|
4091
|
+
function taskReceipt(receipt) {
|
|
4092
|
+
const ok = receipt?.ok;
|
|
4093
|
+
if (ok === true) return {
|
|
4094
|
+
status: "accepted",
|
|
4095
|
+
legacyOk: true
|
|
4096
|
+
};
|
|
4097
|
+
if (ok === false) return {
|
|
4098
|
+
status: "rejected",
|
|
4099
|
+
legacyOk: false
|
|
4100
|
+
};
|
|
4101
|
+
return {
|
|
4102
|
+
status: "unknown",
|
|
4103
|
+
legacyOk: false
|
|
4104
|
+
};
|
|
4105
|
+
}
|
|
4106
|
+
/** @private @author ddj 2026年10月29号 @param ctx Lifecycle owner. @param sessions Public Controller. @param id Fixed target. @param text Task body. @returns Admission evidence without switching transport. @sideEffects Retains and releases exactly one generation. */
|
|
4107
|
+
async function sendOwned(ctx, sessions, id, text) {
|
|
4108
|
+
const controller = new AbortController();
|
|
4109
|
+
const reference = sessions.retain(id, {
|
|
4110
|
+
source: "controllerOperation",
|
|
4111
|
+
signal: controller.signal
|
|
4112
|
+
});
|
|
4113
|
+
let released = false;
|
|
4114
|
+
let cleanupFailed = false;
|
|
4115
|
+
/** @private @author ddj 2026年10月29号 Cancels and releases this exact generation once; remembers unload cleanup failures. */
|
|
4116
|
+
function release() {
|
|
4117
|
+
if (released) return;
|
|
4118
|
+
released = true;
|
|
4119
|
+
controller.abort();
|
|
4120
|
+
try {
|
|
4121
|
+
reference.release();
|
|
4122
|
+
} catch (error) {
|
|
4123
|
+
cleanupFailed = true;
|
|
4124
|
+
throw error;
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
let remove;
|
|
4128
|
+
let started = false;
|
|
4129
|
+
let result = {
|
|
4130
|
+
status: "unavailable",
|
|
4131
|
+
legacyOk: false
|
|
4132
|
+
};
|
|
4133
|
+
try {
|
|
4134
|
+
remove = ctx.effect?.(() => release, "vscode-mode: task session reference");
|
|
4135
|
+
const binding = await reference.ready;
|
|
4136
|
+
if (controller.signal.aborted) result = {
|
|
4137
|
+
status: "cancelled",
|
|
4138
|
+
legacyOk: false
|
|
4139
|
+
};
|
|
4140
|
+
else {
|
|
4141
|
+
started = true;
|
|
4142
|
+
result = taskReceipt(await binding.session.prompt([{
|
|
4143
|
+
type: "text",
|
|
4144
|
+
text
|
|
4145
|
+
}], "queue", controller.signal));
|
|
4146
|
+
}
|
|
4147
|
+
} catch {
|
|
4148
|
+
result = {
|
|
4149
|
+
status: started ? "unknown" : "unavailable",
|
|
4150
|
+
legacyOk: false
|
|
4151
|
+
};
|
|
4152
|
+
}
|
|
4153
|
+
const cancelled = controller.signal.aborted;
|
|
4154
|
+
try {
|
|
4155
|
+
release();
|
|
4156
|
+
} catch {
|
|
4157
|
+
cleanupFailed = true;
|
|
4158
|
+
}
|
|
4159
|
+
try {
|
|
4160
|
+
remove?.();
|
|
4161
|
+
} catch {
|
|
4162
|
+
cleanupFailed = true;
|
|
4163
|
+
}
|
|
4164
|
+
if (cleanupFailed) result = {
|
|
4165
|
+
status: started ? "unknown" : "unavailable",
|
|
4166
|
+
legacyOk: false
|
|
4167
|
+
};
|
|
4168
|
+
if (cancelled) result = {
|
|
4169
|
+
status: "cancelled",
|
|
4170
|
+
legacyOk: result.legacyOk
|
|
4171
|
+
};
|
|
4172
|
+
return result;
|
|
4173
|
+
}
|
|
4174
|
+
/** @private @author ddj 2026年10月29号 @param ctx Service owner. @param id Fixed target. @param text Task body. @returns One legacy-channel attempt; no cross-channel retry. */
|
|
4175
|
+
async function sendLegacy(ctx, id, text) {
|
|
4176
|
+
const input = inputFor(ctx, id);
|
|
4177
|
+
if (typeof input?.setDraft === "function" && typeof input.submit === "function") {
|
|
4178
|
+
let submitted = false;
|
|
4179
|
+
try {
|
|
4180
|
+
input.setDraft(text);
|
|
4181
|
+
submitted = true;
|
|
4182
|
+
input.submit();
|
|
4183
|
+
return {
|
|
4184
|
+
status: "submitted",
|
|
4185
|
+
legacyOk: true
|
|
4186
|
+
};
|
|
4187
|
+
} catch {
|
|
4188
|
+
return {
|
|
4189
|
+
status: submitted ? "unknown" : "unavailable",
|
|
4190
|
+
legacyOk: false
|
|
4191
|
+
};
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
const remote = ctx.get("remote.session");
|
|
4195
|
+
if (typeof remote?.prompt !== "function") return httpTaskResult(id, text);
|
|
4196
|
+
try {
|
|
4197
|
+
const payload = {
|
|
4198
|
+
sessionId: id,
|
|
4199
|
+
mode: "queue",
|
|
4200
|
+
content: [{
|
|
4201
|
+
type: "text",
|
|
4202
|
+
text
|
|
4203
|
+
}]
|
|
4204
|
+
};
|
|
4205
|
+
const receipt = await remote.prompt(slotArg(payload));
|
|
4206
|
+
return taskReceipt(receipt?.result ?? receipt);
|
|
4207
|
+
} catch {
|
|
4208
|
+
return {
|
|
4209
|
+
status: "unknown",
|
|
4210
|
+
legacyOk: false
|
|
4211
|
+
};
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
/** apiproxy wire 端点(POST /api/<method>;浏览器同源 fetch 自带 cookie 鉴权;实证 401=未鉴权而非 404)。 */
|
|
4215
|
+
const SESSION_CREATE_PATH = "/api/session/create";
|
|
4216
|
+
/** apiproxy wire 端点:发送任务(session/prompt;点击即发送方案)。 */
|
|
4217
|
+
const SESSION_PROMPT_PATH = "/api/session/prompt";
|
|
4218
|
+
/** mint RPC 请求 id(RpcId 品牌类型运行时即普通字符串)。 */
|
|
4219
|
+
function mintRpcId() {
|
|
4220
|
+
try {
|
|
4221
|
+
return globalThis.crypto?.randomUUID?.() ?? "rpc-" + Date.now();
|
|
4222
|
+
} catch {
|
|
4223
|
+
return "rpc-" + Date.now();
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
4226
|
+
/**
|
|
4227
|
+
* wire 槽实参组装(gateway `invoke` 铁证:`request.args[parameter.wire]` 具名槽,
|
|
4228
|
+
* `session/prompt` 的槽名 = "request",值 = RpcRequest `{rpcId, payload}`)。
|
|
4229
|
+
* @author ddj 2026年09月24号
|
|
4230
|
+
* @param payload 业务载荷
|
|
4231
|
+
* @returns 具名槽实参 `{ args: { request: RpcRequest } }`
|
|
4232
|
+
*/
|
|
4233
|
+
function slotArg(payload) {
|
|
4234
|
+
return { args: { request: {
|
|
4235
|
+
rpcId: mintRpcId(),
|
|
4236
|
+
payload
|
|
4237
|
+
} } };
|
|
4238
|
+
}
|
|
4239
|
+
/**
|
|
4240
|
+
* RPC 返回解析(照官方 dsh-client-runtime 调用形态:`const { result } = await api.sessions.create(...)`)。
|
|
4241
|
+
* 官方 api.sessions 面返回**信封** `{ rpcId, result: RpcResult }`——须解 .result;
|
|
4242
|
+
* 兼容裸 RpcResult 形态(个别补丁面直接回 {ok, value})。
|
|
4243
|
+
* @author ddj 2026年09月23号
|
|
4244
|
+
* @param res 信封或裸 RpcResult
|
|
4245
|
+
* @returns 归一后的 { ok, value }
|
|
4246
|
+
*/
|
|
4247
|
+
function rpcOk(res) {
|
|
4248
|
+
const r = res?.result ?? res;
|
|
4249
|
+
return {
|
|
4250
|
+
ok: r?.ok === true,
|
|
4251
|
+
value: r?.value
|
|
4252
|
+
};
|
|
4253
|
+
}
|
|
4254
|
+
/**
|
|
4255
|
+
* wire 直调 session/create(主路径):ClientRequest 信封 → ServerResponse.result(RpcResult 形态)。
|
|
4256
|
+
* 契约以当前安装版 dsh-host-apiproxy 为准(api/sessions.d.ts「Method signatures are the
|
|
4257
|
+
* source of truth」):`create({cwd}) → {sessionId}`;信封 = `{type:'client-request', rpcId,
|
|
4258
|
+
* method, payload}` → `{result: {ok:true, value:{sessionId}} | {ok:false, error}}`。
|
|
4259
|
+
* @author ddj 2026年09月23号
|
|
4260
|
+
* @param cwd 新会话工作区目录
|
|
4261
|
+
* @returns 新会话 id(失败空串)
|
|
4262
|
+
*/
|
|
4263
|
+
async function apiSessionCreate(cwd) {
|
|
4264
|
+
const json = await (await fetch(appUrl(SESSION_CREATE_PATH), {
|
|
4265
|
+
method: "POST",
|
|
4266
|
+
headers: { "content-type": "application/json" },
|
|
4267
|
+
body: JSON.stringify({
|
|
4268
|
+
type: "client-request",
|
|
4269
|
+
rpcId: mintRpcId(),
|
|
4270
|
+
method: "session/create",
|
|
4271
|
+
payload: cwd ? { cwd } : {}
|
|
4272
|
+
})
|
|
4273
|
+
})).json().catch(() => null);
|
|
4274
|
+
const id = json?.result?.value?.sessionId;
|
|
4275
|
+
return json?.result?.ok === true && typeof id === "string" ? id : "";
|
|
4276
|
+
}
|
|
4277
|
+
/** @private @author ddj 2026年10月29号 @param sessionId Fixed target. @param text Task body. @returns HTTP receipt evidence; errors after request entry are never safe rejection. */
|
|
4278
|
+
async function httpTaskResult(sessionId, text) {
|
|
4279
|
+
try {
|
|
4280
|
+
const payload = {
|
|
4281
|
+
sessionId,
|
|
4282
|
+
mode: "queue",
|
|
4283
|
+
content: [{
|
|
4284
|
+
type: "text",
|
|
4285
|
+
text
|
|
4286
|
+
}]
|
|
4287
|
+
};
|
|
4288
|
+
return taskReceipt((await (await fetch(appUrl(SESSION_PROMPT_PATH), {
|
|
4289
|
+
method: "POST",
|
|
4290
|
+
headers: { "content-type": "application/json" },
|
|
4291
|
+
body: JSON.stringify({
|
|
4292
|
+
type: "client-request",
|
|
4293
|
+
rpcId: mintRpcId(),
|
|
4294
|
+
method: "session/prompt",
|
|
4295
|
+
payload: slotArg(payload)
|
|
4296
|
+
})
|
|
4297
|
+
})).json().catch(() => null))?.result);
|
|
4298
|
+
} catch {
|
|
4299
|
+
return {
|
|
4300
|
+
status: "unknown",
|
|
4301
|
+
legacyOk: false
|
|
4302
|
+
};
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
/**
|
|
4306
|
+
* 新建独立会话承载旁路任务(02-deep-session-prompt:深度分析不污染当前对话)。
|
|
4307
|
+
* 优先公开 sessions.create;旧 Host 按能力选择 remote 或 wire,调用后不换通道重试。
|
|
4308
|
+
* 受理状态不明时返回 ok:false,不重复创建会话。
|
|
4309
|
+
* @author ddj 2026年10月28号
|
|
4310
|
+
* @author ddj 2026年09月23号
|
|
4311
|
+
* @param ctx 客户端服务上下文(remote.session + sessions,仅回落用)
|
|
4312
|
+
* @param cwd 新会话工作区目录(与当前会话同工作区)
|
|
4313
|
+
* @returns 新会话结果(含 id)
|
|
4314
|
+
*/
|
|
4315
|
+
async function newDraftSession(ctx, cwd) {
|
|
4316
|
+
try {
|
|
4317
|
+
const sessions = ctx.get("sessions");
|
|
4318
|
+
if (typeof sessions?.create === "function") {
|
|
4319
|
+
const created = await sessions.create(cwd ? { cwd } : void 0);
|
|
4320
|
+
const id = typeof created === "string" && created ? created : typeof created?.id === "string" ? created.id : typeof created?.sessionId === "string" ? created.sessionId : typeof created?.session?.id === "string" ? created.session.id : "";
|
|
4321
|
+
if (id) return {
|
|
4322
|
+
ok: true,
|
|
4323
|
+
id
|
|
4324
|
+
};
|
|
4325
|
+
}
|
|
4326
|
+
if (typeof sessions?.create === "function") return { ok: false };
|
|
4327
|
+
} catch {
|
|
4328
|
+
return { ok: false };
|
|
4329
|
+
}
|
|
4330
|
+
try {
|
|
4331
|
+
const remote = ctx.get("remote.session");
|
|
4332
|
+
if (typeof remote?.create === "function") {
|
|
4333
|
+
const payload = cwd ? { cwd } : {};
|
|
4334
|
+
const receipt = rpcOk(await remote.create(slotArg(payload)));
|
|
4335
|
+
const created = receipt.value;
|
|
4336
|
+
if (receipt.ok && typeof created?.sessionId === "string") return {
|
|
4337
|
+
ok: true,
|
|
4338
|
+
id: created.sessionId
|
|
4339
|
+
};
|
|
4340
|
+
return { ok: false };
|
|
4341
|
+
}
|
|
4342
|
+
} catch {
|
|
4343
|
+
return { ok: false };
|
|
4344
|
+
}
|
|
4345
|
+
try {
|
|
4346
|
+
const id = await apiSessionCreate(cwd);
|
|
4347
|
+
if (id) return {
|
|
4348
|
+
ok: true,
|
|
4349
|
+
id
|
|
4350
|
+
};
|
|
4351
|
+
} catch {}
|
|
4352
|
+
return { ok: false };
|
|
4353
|
+
}
|
|
4354
|
+
/**
|
|
4355
|
+
* 解析会话输入门面;缺失时返回 undefined(动作侧守卫降级)。
|
|
4356
|
+
* @author ddj 2026年08月25号
|
|
4357
|
+
* @param ctx 客户端服务上下文
|
|
4358
|
+
* @param sessionId 会话 id
|
|
4359
|
+
* @returns SessionInput 或 undefined
|
|
4360
|
+
*/
|
|
4361
|
+
function inputFor(ctx, sessionId) {
|
|
4362
|
+
if (!sessionId) return void 0;
|
|
4363
|
+
try {
|
|
4364
|
+
const sessions = ctx.get("sessions");
|
|
4365
|
+
const conversation = ctx.get("conversation");
|
|
4366
|
+
const actx = typeof sessions?.binding === "function" ? sessions.binding(sessionId)?.ctx : sessions?.scope?.(sessionId);
|
|
4367
|
+
const shell = actx && conversation?.input?.for?.(actx);
|
|
4368
|
+
return shell && typeof shell.insertReference === "function" ? shell : void 0;
|
|
4369
|
+
} catch {
|
|
4370
|
+
return;
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
/** 取当前草稿快照(长度/版本/chip 列表);无输入门面时返回 null。
|
|
4374
|
+
* @author ddj 2026年08月25号 / 2026年09月11号 */
|
|
4375
|
+
function draftCursor(input) {
|
|
4376
|
+
try {
|
|
4377
|
+
const s = input?.state?.getSnapshot?.();
|
|
4378
|
+
if (!s) return null;
|
|
4379
|
+
return {
|
|
4380
|
+
draft: String(s.draft ?? ""),
|
|
4381
|
+
draftRev: Number(s.draftRev ?? 0),
|
|
4382
|
+
occurrences: Array.isArray(s.occurrences) ? s.occurrences : void 0
|
|
4383
|
+
};
|
|
4384
|
+
} catch {
|
|
4385
|
+
return null;
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
/** 从 sessions 列表快照读会话工作区目录(与 index.ts 既有读取方式一致)。
|
|
4389
|
+
* @author ddj 2026年08月25号 */
|
|
4390
|
+
function cwdOf(ctx, sessionId) {
|
|
4391
|
+
if (!sessionId) return void 0;
|
|
4392
|
+
try {
|
|
4393
|
+
return ctx.get("sessions")?.list?.getSnapshot?.()?.byId?.[sessionId]?.cwd;
|
|
4394
|
+
} catch {
|
|
4395
|
+
return;
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
/** 输入门面插入引用(拦截异常视作未应用,走降级)。
|
|
4399
|
+
* @author ddj 2026年08月25号 */
|
|
4400
|
+
function safeInsert(input, reference, span) {
|
|
4401
|
+
try {
|
|
4402
|
+
return input.insertReference(reference, span) === true;
|
|
4403
|
+
} catch {
|
|
4404
|
+
return false;
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
4407
|
+
/** 输入门面插入纯文本(官方 plain-text 通道;不存在或异常视作未应用)。
|
|
4408
|
+
* @author ddj 2026年09月11号 */
|
|
4409
|
+
function safeInsertText(input, text, span) {
|
|
4410
|
+
if (typeof input.insertText !== "function") return false;
|
|
4411
|
+
try {
|
|
4412
|
+
return input.insertText(text, span) === true;
|
|
4413
|
+
} catch {
|
|
4414
|
+
return false;
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
/**
|
|
4418
|
+
* 读输入框当前光标(detect 坐标);门面缺失/异常返回 null(由调用方回落到文档末尾)。
|
|
4419
|
+
* @author ddj 2026年09月11号
|
|
4420
|
+
* @param input 输入门面
|
|
4421
|
+
* @returns caretSpan 结果或 null
|
|
4422
|
+
*/
|
|
4423
|
+
function caretOf(input) {
|
|
4424
|
+
if (typeof input.caretSpan !== "function") return null;
|
|
4425
|
+
try {
|
|
4426
|
+
return input.caretSpan() ?? null;
|
|
4427
|
+
} catch {
|
|
4428
|
+
return null;
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4431
|
+
/**
|
|
4432
|
+
* 创建「添加到对话」动作集(apply 阶段构建一次,随 props 传给 EditorView)。
|
|
4433
|
+
*
|
|
4434
|
+
* 插入位置 = 输入框**当前光标**(`caretSpan()`,detect 坐标;无光标提示时回落文档末尾),
|
|
4435
|
+
* 不再固定追加到最末尾。坐标一律用 **detect 投影**(见 detectEndOf 的缺陷说明);
|
|
4436
|
+
* 两条写入通道都失败时**不写任何内容**并返回 'failed' —— 旧实现在此处退回 `setDraft`
|
|
4437
|
+
* 整篇重建,会把既有 chip 全部销毁(连续添加多个引用即触发)。
|
|
4438
|
+
* @author ddj 2026年08月25号 / 2026年09月11号
|
|
4439
|
+
* @param ctx 客户端服务上下文(sessions + conversation)
|
|
4440
|
+
* @returns 动作集
|
|
4441
|
+
*/
|
|
4442
|
+
function createAddToConversation(ctx) {
|
|
4443
|
+
const appendReference = async (sessionId, path, range, appearance) => {
|
|
4444
|
+
const input = inputFor(ctx, sessionId);
|
|
4445
|
+
if (!input) return "unavailable";
|
|
4446
|
+
const cur = draftCursor(input);
|
|
4447
|
+
if (!cur) return "unavailable";
|
|
4448
|
+
const { reference, mention } = buildFileRef(path, cwdOf(ctx, sessionId), range, appearance);
|
|
4449
|
+
const at = insertOffsetOf(caretOf(input), detectEndOf(cur));
|
|
4450
|
+
const span = {
|
|
4451
|
+
start: at,
|
|
4452
|
+
end: at,
|
|
4453
|
+
draftRev: cur.draftRev
|
|
4454
|
+
};
|
|
4455
|
+
if (safeInsert(input, reference, span)) return "ok";
|
|
4456
|
+
const pad = paddingAt(cur, at);
|
|
4457
|
+
if (safeInsertText(input, (pad.lead ? " " : "") + mention + (pad.tail ? " " : ""), span)) return "busy";
|
|
4458
|
+
return "failed";
|
|
4459
|
+
};
|
|
4460
|
+
/**
|
|
4461
|
+
* 追加纯文本(AI 深度分析任务注入):走官方 plain-text 通道插到当前光标。
|
|
4462
|
+
* 失败语义与 appendReference 一致:宁可不写也不整篇重建(保护既有 chip)。
|
|
4463
|
+
* @author ddj 2026年09月23号
|
|
4464
|
+
* @param sessionId 会话 id
|
|
4465
|
+
* @param text 待注入文本
|
|
4466
|
+
* @returns 注入结果(ok / unavailable / failed)
|
|
4467
|
+
*/
|
|
4468
|
+
const appendText = async (sessionId, text) => {
|
|
4469
|
+
const input = inputFor(ctx, sessionId);
|
|
4470
|
+
if (!input) return "unavailable";
|
|
4471
|
+
const cur = draftCursor(input);
|
|
4472
|
+
if (!cur) return "unavailable";
|
|
4473
|
+
const at = insertOffsetOf(caretOf(input), detectEndOf(cur));
|
|
4474
|
+
return safeInsertText(input, text, {
|
|
4475
|
+
start: at,
|
|
4476
|
+
end: at,
|
|
4477
|
+
draftRev: cur.draftRev
|
|
4478
|
+
}) ? "ok" : "failed";
|
|
4479
|
+
};
|
|
4480
|
+
/**
|
|
4481
|
+
* 新建独立会话(02-deep-session-prompt:旁路任务不污染当前对话)。
|
|
4482
|
+
* 闭包持有真服务 ctx(面板 SidebarCtx 无 .get,会话操作必须经此门面);
|
|
4483
|
+
* 新版仅返回已登记身份,视图选择由宿主负责;旧版保留可用的 open/select。
|
|
4484
|
+
* @author ddj 2026年10月28号
|
|
4485
|
+
* @author ddj 2026年09月23号
|
|
4486
|
+
* @param cwd 新会话工作区目录(与当前会话同工作区)
|
|
4487
|
+
* @returns 新会话结果(失败供降级)
|
|
4488
|
+
*/
|
|
4489
|
+
const startDraftSession = async (cwd) => {
|
|
4490
|
+
const result = await newDraftSession(ctx, cwd);
|
|
4491
|
+
if (typeof ctx.get("sessions")?.retain === "function") return result;
|
|
4492
|
+
if (result.ok && result.id) try {
|
|
4493
|
+
const sessions = ctx.get("sessions");
|
|
4494
|
+
sessions?.materializeScope?.(result.id);
|
|
4495
|
+
const title = "AI 深度分析 · " + (String(cwd ?? "").split(/[\\/]/).pop() || "SVN 变更");
|
|
4496
|
+
sessions?.manager?.rename?.(result.id, title);
|
|
4497
|
+
sessions?.manager?.select?.(result.id);
|
|
4498
|
+
sessions?.open?.(result.id);
|
|
4499
|
+
} catch {}
|
|
4500
|
+
return result;
|
|
4501
|
+
};
|
|
4502
|
+
/**
|
|
4503
|
+
* 发送任务给目标会话 agent(点击即发送方案:任务落地会话即非 blank,列表可见)。
|
|
4504
|
+
* 新版持有目标 Controller 到 prompt 结束;旧版按能力选择唯一通道,不跨通道重发。
|
|
4505
|
+
* @author ddj 2026年10月29号
|
|
4506
|
+
* @param sessionId 目标会话 id
|
|
4507
|
+
* @param text 任务文本
|
|
4508
|
+
* @returns 同一次调用的历史boolean语义,不能作为受理事实
|
|
4509
|
+
*/
|
|
4510
|
+
const sendTask = async (sessionId, text) => (await sendTaskResult(sessionId, text)).legacyOk;
|
|
4511
|
+
/** @private @author ddj 2026年10月29号 @param sessionId Fixed target. @param text Task body. @returns Detailed evidence from exactly one selected transport. */
|
|
4512
|
+
async function sendTaskResult(sessionId, text) {
|
|
4513
|
+
if (!sessionId) return {
|
|
4514
|
+
status: "unavailable",
|
|
4515
|
+
legacyOk: false
|
|
4516
|
+
};
|
|
4517
|
+
try {
|
|
4518
|
+
const sessions = ctx.get("sessions");
|
|
4519
|
+
if (typeof sessions?.retain === "function") return await sendOwned(ctx, sessions, sessionId, text);
|
|
4520
|
+
return await sendLegacy(ctx, sessionId, text);
|
|
4521
|
+
} catch {
|
|
4522
|
+
return {
|
|
4523
|
+
status: "unavailable",
|
|
4524
|
+
legacyOk: false
|
|
4525
|
+
};
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4528
|
+
return {
|
|
4529
|
+
appendReference,
|
|
4530
|
+
appendText,
|
|
4531
|
+
startDraftSession,
|
|
4532
|
+
sendTask,
|
|
4533
|
+
sendTaskResult
|
|
4534
|
+
};
|
|
4535
|
+
}
|
|
4536
|
+
//#endregion
|
|
4537
|
+
//#region src/client/openFlow.ts
|
|
4538
|
+
/**
|
|
4539
|
+
* dsh-vscode-mode client — 深链打开规则引擎(资源管理器右键 / Unity 外部编辑器落地)。
|
|
4540
|
+
* 规则:文件夹 ⊂ 某已注册工作区 → 页面内弹窗二选一(使用最近的工作区 / 新建工作区);
|
|
4541
|
+
* 文件夹不在任何工作区 → 直达最近对话所在工作区新增;文件 → 最近对话 + 编辑器展开
|
|
4542
|
+
* (无任何会话 → 以文件父目录注册工作区引导创建)。首路径驱动规则,其余路径:文件进
|
|
4543
|
+
* 编辑器、文件夹补引用。纯函数可单测;编排依赖经 OpenDeps 注入(mock 可测)。
|
|
4544
|
+
* 作者 ddj 2026-09-07
|
|
4545
|
+
*/
|
|
4546
|
+
/** 等列表/输入就绪的节奏。 */
|
|
4547
|
+
const READY_TIMEOUT_MS = 15e3;
|
|
4548
|
+
const READY_TICK_MS = 300;
|
|
4549
|
+
const REF_RETRY_MAX = 20;
|
|
4550
|
+
const REF_RETRY_MS = 300;
|
|
4551
|
+
/**
|
|
4552
|
+
* 路径规范化:`\→/`、小写、去尾分隔符(Windows 大小写不敏感前缀匹配用)。
|
|
4553
|
+
* @author ddj 2026年09月07号
|
|
4554
|
+
*/
|
|
4555
|
+
function normPath(p) {
|
|
4556
|
+
return String(p ?? "").replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
4557
|
+
}
|
|
4558
|
+
/**
|
|
4559
|
+
* 目录包含判定:child === root 或 child 位于 root 之下。
|
|
4560
|
+
* @author ddj 2026年09月07号
|
|
4561
|
+
*/
|
|
4562
|
+
function containsPath(root, child) {
|
|
4563
|
+
const r = normPath(root);
|
|
4564
|
+
const c = normPath(child);
|
|
4565
|
+
return c === r || c.startsWith(r + "/");
|
|
4566
|
+
}
|
|
4567
|
+
/**
|
|
4568
|
+
* 父目录(文件引导创建工作区用;根路径回退自身)。
|
|
4569
|
+
* @author ddj 2026年09月07号
|
|
4570
|
+
*/
|
|
4571
|
+
function dirnameOf(p) {
|
|
4572
|
+
const norm = String(p ?? "").replace(/[\\/]+$/, "");
|
|
4573
|
+
const cut = Math.max(norm.lastIndexOf("/"), norm.lastIndexOf("\\"));
|
|
4574
|
+
return cut > 0 ? norm.slice(0, cut) : norm;
|
|
4575
|
+
}
|
|
4576
|
+
/**
|
|
4577
|
+
* 最近一次对话:排除子代理行,updatedAt 最新优先。
|
|
4578
|
+
* @author ddj 2026年09月07号
|
|
4579
|
+
*/
|
|
4580
|
+
function recentSession(byId, ids) {
|
|
4581
|
+
return (ids ?? Object.keys(byId)).map((id) => byId[id]).filter((row) => row && row.origin !== "subagent").sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))[0];
|
|
4582
|
+
}
|
|
4583
|
+
/**
|
|
4584
|
+
* 文件夹命中的已注册工作区(前缀包含),updatedAt 新者优先。
|
|
4585
|
+
* @author ddj 2026年09月07号
|
|
4586
|
+
*/
|
|
4587
|
+
function matchWorkspaces(items, folder) {
|
|
4588
|
+
return items.filter((ws) => containsPath(ws.path, folder));
|
|
4589
|
+
}
|
|
4590
|
+
/**
|
|
4591
|
+
* 规则分派:目录+命中工作区 → choose;目录未命中 → folderNew(以该文件夹为根注册新工作区,
|
|
4592
|
+
* 用户确认:一律取右键的文件夹本身);文件有会话 → fileRecent;文件无会话 → bootstrap;
|
|
4593
|
+
* 路径缺失 → abort。
|
|
4594
|
+
* @author ddj 2026年09月08号
|
|
4595
|
+
*/
|
|
4596
|
+
function pickRule(kind, matched, hasSession) {
|
|
4597
|
+
if (kind === "missing") return "abort";
|
|
4598
|
+
if (kind === "directory") return matched ? "choose" : "folderNew";
|
|
4599
|
+
return hasSession ? "fileRecent" : "bootstrap";
|
|
4600
|
+
}
|
|
4601
|
+
/**
|
|
4602
|
+
* 深链打开编排入口:等列表就绪 → 判型 → 规则分派 → 编排 create/open/引用/编辑器。
|
|
4603
|
+
* @author ddj 2026年09月07号
|
|
4604
|
+
* @param ctx 客户端根上下文(sessions/workspaces 服务)
|
|
4605
|
+
* @param params 深链参数(首路径驱动规则)
|
|
4606
|
+
* @param overrides 依赖覆盖(测试注入)
|
|
4607
|
+
*/
|
|
4608
|
+
async function openDeepLink(ctx, params, overrides = {}) {
|
|
4609
|
+
const deps = realDeps(ctx, overrides);
|
|
4610
|
+
if (!await waitListsReady(deps)) {
|
|
4611
|
+
deps.notify("DSH 会话列表未就绪,请稍后重试");
|
|
4612
|
+
return false;
|
|
4613
|
+
}
|
|
4614
|
+
const primary = params.paths[0];
|
|
4615
|
+
const kind = await deps.stat(primary).catch(() => "missing");
|
|
4616
|
+
const sess = deps.sessions();
|
|
4617
|
+
const rule = pickRule(kind, matchWorkspaces(deps.workspaces().items, primary).length > 0, Boolean(recentSession(sess.byId, sess.ids)));
|
|
4618
|
+
try {
|
|
4619
|
+
if (rule === "abort") {
|
|
4620
|
+
deps.notify("路径不存在:" + primary);
|
|
4621
|
+
return false;
|
|
4622
|
+
}
|
|
4623
|
+
if (rule === "choose") await runChoose(deps, primary, params);
|
|
4624
|
+
else if (rule === "folderNew") await runFolderNew(deps, primary, params);
|
|
4625
|
+
else if (rule === "fileRecent") await runFileRecent(deps, primary, params);
|
|
4626
|
+
else await runFileBootstrap(deps, primary, params);
|
|
4627
|
+
return true;
|
|
4628
|
+
} catch (error) {
|
|
4629
|
+
deps.notify("打开失败:" + String(error?.message ?? error));
|
|
4630
|
+
return false;
|
|
4631
|
+
}
|
|
4632
|
+
}
|
|
4633
|
+
/** 等待会话与工作区列表就绪(上限 15s;超时按当前态判定)。 */
|
|
4634
|
+
async function waitListsReady(deps) {
|
|
4635
|
+
for (let waited = 0; waited < READY_TIMEOUT_MS; waited += READY_TICK_MS) {
|
|
4636
|
+
if (deps.sessions().ready && deps.workspaces().ready) return true;
|
|
4637
|
+
await sleep(deps, READY_TICK_MS);
|
|
4638
|
+
}
|
|
4639
|
+
return deps.sessions().ready && deps.workspaces().ready;
|
|
4640
|
+
}
|
|
4641
|
+
/** 规则 1:命中工作区 → 弹窗分派(取消即中止)。 */
|
|
4642
|
+
async function runChoose(deps, folder, params) {
|
|
4643
|
+
const best = bestMatch(deps, matchWorkspaces(deps.workspaces().items, folder));
|
|
4644
|
+
const choice = await deps.choose(best?.title ?? "工作区", folder);
|
|
4645
|
+
if (choice === null) throw new Error("用户取消打开");
|
|
4646
|
+
if (choice === "create") return runFolderNew(deps, folder, params);
|
|
4647
|
+
const sessionId = await spawnInWorkspace(deps, (best ?? deps.workspaces().items[0]).workspaceId, void 0);
|
|
4648
|
+
await settleRef(deps, sessionId, folder, "folder");
|
|
4649
|
+
await openChecked(deps, sessionId, null);
|
|
4650
|
+
await openRest(deps, sessionId, params, 1);
|
|
4651
|
+
}
|
|
4652
|
+
/** 规则 3a:文件 → 所属工作区优先(有会话打开其最近会话;无会话新建 + 文件引用);无匹配 → 最近对话。 */
|
|
4653
|
+
async function runFileRecent(deps, file, params) {
|
|
4654
|
+
const ws = pickFileWorkspace(deps, file);
|
|
4655
|
+
if (ws) {
|
|
4656
|
+
const anchor = recentSessionIn(deps, ws);
|
|
4657
|
+
if (anchor) {
|
|
4658
|
+
deps.openSession(anchor.id);
|
|
4659
|
+
await openChecked(deps, anchor.id, file, params.line, params.column);
|
|
4660
|
+
await openRest(deps, anchor.id, params, 1);
|
|
4661
|
+
return;
|
|
4662
|
+
}
|
|
4663
|
+
const sessionId = await spawnInWorkspace(deps, ws.workspaceId, void 0);
|
|
4664
|
+
await settleRef(deps, sessionId, file, "file");
|
|
4665
|
+
await openChecked(deps, sessionId, file, params.line, params.column);
|
|
4666
|
+
await openRest(deps, sessionId, params, 1);
|
|
4667
|
+
return;
|
|
4668
|
+
}
|
|
4669
|
+
const anchor = recentSession(deps.sessions().byId, deps.sessions().ids);
|
|
4670
|
+
if (!anchor) return runFileBootstrap(deps, file, params);
|
|
4671
|
+
deps.openSession(anchor.id);
|
|
4672
|
+
await openChecked(deps, anchor.id, file, params.line, params.column);
|
|
4673
|
+
await openRest(deps, anchor.id, params, 1);
|
|
4674
|
+
}
|
|
4675
|
+
/**
|
|
4676
|
+
* 文件所属工作区:前缀命中的工作区里,优先取有会话且最近活跃的;都无会话取路径最具体的。
|
|
4677
|
+
* @author ddj 2026年09月08号
|
|
4678
|
+
*/
|
|
4679
|
+
function pickFileWorkspace(deps, file) {
|
|
4680
|
+
const matches = matchWorkspaces(deps.workspaces().items, file);
|
|
4681
|
+
if (!matches.length) return void 0;
|
|
4682
|
+
const withSessions = matches.filter((ws) => (ws.sessionIds ?? []).some((id) => Boolean(deps.sessions().byId[id])));
|
|
4683
|
+
if (!withSessions.length) return [...matches].sort((a, b) => normPath(b.path).length - normPath(a.path).length)[0];
|
|
4684
|
+
return withSessions.sort((a, b) => latestSessionAt(deps, b) - latestSessionAt(deps, a))[0];
|
|
4685
|
+
}
|
|
4686
|
+
/** 工作区内会话的最近活跃时间(无成员 → 0)。 */
|
|
4687
|
+
function latestSessionAt(deps, ws) {
|
|
4688
|
+
const byId = deps.sessions().byId;
|
|
4689
|
+
return (ws.sessionIds ?? []).reduce((max, id) => Math.max(max, byId[id]?.updatedAt ?? 0), 0);
|
|
4690
|
+
}
|
|
4691
|
+
/** 工作区内最近一次会话(排除子代理与不在列表中的陈旧行)。 */
|
|
4692
|
+
function recentSessionIn(deps, ws) {
|
|
4693
|
+
const byId = deps.sessions().byId;
|
|
4694
|
+
return (ws.sessionIds ?? []).map((id) => byId[id]).filter((row) => row && row.origin !== "subagent").sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))[0];
|
|
4695
|
+
}
|
|
4696
|
+
/** 规则 3b:文件 + 无任何会话 → 父目录注册工作区 + 新对话 + 文件引用 + 编辑器打开。 */
|
|
4697
|
+
async function runFileBootstrap(deps, file, params) {
|
|
4698
|
+
const sessionId = await spawnInWorkspace(deps, (await deps.createWorkspace(dirnameOf(file))).workspaceId, void 0);
|
|
4699
|
+
await settleRef(deps, sessionId, file, "file");
|
|
4700
|
+
await openChecked(deps, sessionId, file, params.line, params.column);
|
|
4701
|
+
await openRest(deps, sessionId, params, 1);
|
|
4702
|
+
}
|
|
4703
|
+
/** 规则 1.2:以文件夹注册新工作区 + 新对话 + 文件夹引用 + 编辑页。 */
|
|
4704
|
+
async function runFolderNew(deps, folder, params) {
|
|
4705
|
+
const sessionId = await spawnInWorkspace(deps, (await deps.createWorkspace(folder)).workspaceId, void 0);
|
|
4706
|
+
await settleRef(deps, sessionId, folder, "folder");
|
|
4707
|
+
await openChecked(deps, sessionId, null);
|
|
4708
|
+
await openRest(deps, sessionId, params, 1);
|
|
4709
|
+
}
|
|
4710
|
+
/** 在指定工作区(或回退 cwd)新建对话并置为当前;返回新会话 id。 */
|
|
4711
|
+
async function spawnInWorkspace(deps, workspaceId, fallbackCwd) {
|
|
4712
|
+
const sessionId = await deps.createSession(workspaceId ? { workspaceId } : fallbackCwd ? { cwd: fallbackCwd } : {});
|
|
4713
|
+
deps.openSession(sessionId);
|
|
4714
|
+
return sessionId;
|
|
4715
|
+
}
|
|
4716
|
+
/** 命中工作区集中「最近」的一个(其成员会话 updatedAt 最新者优先)。 */
|
|
4717
|
+
function bestMatch(deps, matches) {
|
|
4718
|
+
const byId = deps.sessions().byId;
|
|
4719
|
+
const score = (ws) => {
|
|
4720
|
+
const sessionLatest = (ws.sessionIds ?? []).reduce((max, id) => Math.max(max, byId[id]?.updatedAt ?? 0), 0);
|
|
4721
|
+
return Math.max(sessionLatest, Date.parse(ws.updatedAt ?? "") || 0);
|
|
4722
|
+
};
|
|
4723
|
+
return [...matches].sort((a, b) => score(b) - score(a))[0];
|
|
4724
|
+
}
|
|
4725
|
+
/** 引用插入轮询:输入门面就绪前每 300ms 重试(appendReference 忙态已降级视为成功)。 */
|
|
4726
|
+
async function settleRef(deps, sessionId, path, appearance) {
|
|
4727
|
+
for (let attempt = 0; attempt < REF_RETRY_MAX; attempt++) {
|
|
4728
|
+
if (await deps.reference(sessionId, path, appearance)) return;
|
|
4729
|
+
await sleep(deps, REF_RETRY_MS);
|
|
4730
|
+
}
|
|
4731
|
+
throw new Error("引用插入未完成");
|
|
4732
|
+
}
|
|
4733
|
+
/** 其余路径:文件进编辑器(步进),文件夹补文件夹引用。 */
|
|
4734
|
+
async function openRest(deps, sessionId, params, from) {
|
|
4735
|
+
const rest = params.paths.slice(from);
|
|
4736
|
+
for (const path of rest) {
|
|
4737
|
+
const kind = await deps.stat(path).catch(() => "missing");
|
|
4738
|
+
if (kind === "file") await openChecked(deps, sessionId ?? void 0, path);
|
|
4739
|
+
else if (kind === "directory" && sessionId) await settleRef(deps, sessionId, path, "folder");
|
|
4740
|
+
else throw new Error("路径不存在:" + path);
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4743
|
+
/** 可注入 sleep。 */
|
|
4744
|
+
function sleep(deps, ms) {
|
|
4745
|
+
return new Promise((resolve) => deps.schedule(resolve, ms));
|
|
4746
|
+
}
|
|
4747
|
+
/** 真实依赖装配(全部来自 ctx 注入服务 + 既有 RPC/弹窗/编辑器入口)。 */
|
|
4748
|
+
function realDeps(ctx, overrides = {}) {
|
|
4749
|
+
const service = (name) => {
|
|
4750
|
+
const get = ctx?.get;
|
|
4751
|
+
return typeof get === "function" ? get.call(ctx, name) : void 0;
|
|
4752
|
+
};
|
|
4753
|
+
const add = createAddToConversation(ctx);
|
|
4754
|
+
return {
|
|
4755
|
+
stat: async (path) => {
|
|
4756
|
+
const result = await rpc("edrv.externalStat", { path });
|
|
4757
|
+
return result.ok ? result.kind : "missing";
|
|
4758
|
+
},
|
|
4759
|
+
workspaces: () => {
|
|
4760
|
+
const snap = service("workspaces")?.list?.getSnapshot?.() ?? {};
|
|
4761
|
+
return {
|
|
4762
|
+
items: snap.items ?? [],
|
|
4763
|
+
ready: snap.phase === "ready"
|
|
4764
|
+
};
|
|
4765
|
+
},
|
|
4766
|
+
sessions: () => {
|
|
4767
|
+
const snap = service("sessions")?.list?.getSnapshot?.() ?? {};
|
|
4768
|
+
return {
|
|
4769
|
+
ids: snap.ids ?? [],
|
|
4770
|
+
byId: snap.byId ?? {},
|
|
4771
|
+
ready: snap.phase !== "pending"
|
|
4772
|
+
};
|
|
4773
|
+
},
|
|
4774
|
+
createSession: async (opts) => {
|
|
4775
|
+
const sessions = service("sessions");
|
|
4776
|
+
if (typeof sessions?.create === "function") try {
|
|
4777
|
+
const created = await sessions.create(opts);
|
|
4778
|
+
const id = typeof created === "string" && created ? created : typeof created?.id === "string" ? created.id : typeof created?.sessionId === "string" ? created.sessionId : "";
|
|
4779
|
+
if (id) return id;
|
|
4780
|
+
} catch {}
|
|
4781
|
+
const draft = await newDraftSession(ctx, opts.cwd);
|
|
4782
|
+
if (draft.ok && draft.id) return draft.id;
|
|
4783
|
+
throw new Error("新建会话失败");
|
|
4784
|
+
},
|
|
4785
|
+
createWorkspace: async (path) => {
|
|
4786
|
+
const ws = await service("workspaces").create({ path });
|
|
4787
|
+
return {
|
|
4788
|
+
workspaceId: ws.workspaceId,
|
|
4789
|
+
title: ws.title
|
|
4790
|
+
};
|
|
4791
|
+
},
|
|
4792
|
+
openSession: (id) => {
|
|
4793
|
+
try {
|
|
4794
|
+
const sessions = service("sessions");
|
|
4795
|
+
sessions?.materializeScope?.(id);
|
|
4796
|
+
sessions?.manager?.select?.(id);
|
|
4797
|
+
sessions?.open?.(id);
|
|
4798
|
+
} catch {}
|
|
4799
|
+
},
|
|
4800
|
+
reference: async (sessionId, path, appearance) => {
|
|
4801
|
+
const outcome = await add.appendReference(sessionId, path, void 0, appearance);
|
|
4802
|
+
return outcome === "ok" || outcome === "busy";
|
|
4803
|
+
},
|
|
4804
|
+
choose: (title, folder) => chooseWorkspace(title, folder),
|
|
4805
|
+
openEditor: requestOpen,
|
|
4806
|
+
schedule: (fn, ms) => setTimeout(fn, ms),
|
|
4807
|
+
notify: (text) => toastDom(text),
|
|
4808
|
+
...overrides
|
|
4809
|
+
};
|
|
4810
|
+
}
|
|
4811
|
+
/** @private @author ddj 2026年09月28号
|
|
4812
|
+
* Await actual editor completion and propagate failure to the request result.
|
|
4813
|
+
* @param deps Open dependencies.
|
|
4814
|
+
* @param sessionId Target session.
|
|
4815
|
+
* @param path Target file or editor mount.
|
|
4816
|
+
* @param line Optional line.
|
|
4817
|
+
* @param column Optional column.
|
|
4818
|
+
*/
|
|
4819
|
+
async function openChecked(deps, sessionId, path, line, column) {
|
|
4820
|
+
if (!await deps.openEditor(path, line, column, sessionId)) throw new Error("编辑器打开未完成:" + (path ?? "工作区"));
|
|
4821
|
+
}
|
|
4822
|
+
//#endregion
|
|
4823
|
+
//#region src/client/externalOpen.ts
|
|
4824
|
+
/**
|
|
4825
|
+
* dsh-vscode-mode client — 外部深链落地(Windows 右键菜单 / Unity 外部编辑器)。
|
|
4826
|
+
* 启动时解析 location.search 的 edrvOpen 深链:跨源 referrer 守卫防外部网页诱导;
|
|
4827
|
+
* 处理完剥离 URL 参数防刷新重开;执行委托 openFlow 打开规则引擎(工作区/对话智能路由)。
|
|
4828
|
+
* 另挂 3s 移交轮询(兼活跃心跳):launcher 投递的打开请求由已打开页面就地执行,避免重复开新页。
|
|
4829
|
+
* 作者 ddj 2026-09-07 / 2026-09-08
|
|
4830
|
+
*/
|
|
4831
|
+
/** toast 自隐时长。 */
|
|
4832
|
+
const TOAST_MS = 5e3;
|
|
4833
|
+
/** 深链移交轮询周期(兼活跃心跳)。 */
|
|
4834
|
+
const HANDOFF_POLL_MS = 3e3;
|
|
4835
|
+
/**
|
|
4836
|
+
* 页面隐藏时的降频倍数(3s × 5 = 15s)。
|
|
4837
|
+
* 必须仍短于 host 租约(src/externalHandoff.ts 的 LEASE_MS = 30s):心跳一旦超期,
|
|
4838
|
+
* host 判定该页面已死,launcher 会重复开新页——比多几次 RPC 更糟。
|
|
4839
|
+
*/
|
|
4840
|
+
const HIDDEN_POLL_EVERY = 5;
|
|
4841
|
+
/**
|
|
4842
|
+
* 装配外部深链:深链参数缺失时仅保持移交轮询(零额外开销)。
|
|
4843
|
+
* @author ddj 2026年09月07号
|
|
4844
|
+
* @param ctx 客户端根上下文(sessions/workspaces 服务)
|
|
4845
|
+
* @param options 可注入选项
|
|
4846
|
+
*/
|
|
4847
|
+
function setupExtOpen(ctx, options = {}) {
|
|
4848
|
+
const dispose = startPresencePoll(ctx);
|
|
4849
|
+
const params = parseOpenParams(options.search ?? (typeof location !== "undefined" ? location.search : ""));
|
|
4850
|
+
if (!params) return dispose;
|
|
4851
|
+
const referrer = options.referrer ?? (typeof document !== "undefined" ? document.referrer : "");
|
|
4852
|
+
if (!referrerAllowed(referrer, options.origin ?? (typeof location !== "undefined" ? location.origin : ""))) {
|
|
4853
|
+
log$2.warn("已忽略跨源深链请求(referrer=" + referrer + ")");
|
|
4854
|
+
return dispose;
|
|
4855
|
+
}
|
|
4856
|
+
stripCurrentUrl();
|
|
4857
|
+
openDeepLink(ctx, params).catch((error) => {
|
|
4858
|
+
log$2.warn("深链打开失败:" + String(error));
|
|
4859
|
+
toastDom("深链打开失败:" + String(error?.message ?? error));
|
|
4860
|
+
});
|
|
4861
|
+
return dispose;
|
|
4862
|
+
}
|
|
4863
|
+
/**
|
|
4864
|
+
* 深链移交轮询:可见时每 3s 向 host 领取待打开请求(兼活跃心跳,launcher 据此决定是否开新页);
|
|
4865
|
+
* 页面隐藏时降到 15s 一次——无人看页面时移交不必保持 3s 精度,但心跳不能停(超期会让 host 判页面已死)。
|
|
4866
|
+
* 页面级单例(window 标记防 HMR 重复);领取到即执行打开规则。
|
|
4867
|
+
* @author ddj 2026年09月08号 / 2026年10月15号
|
|
4868
|
+
* @param ctx 客户端根上下文
|
|
4869
|
+
*/
|
|
4870
|
+
function startPresencePoll(ctx) {
|
|
4871
|
+
if (typeof window === "undefined") return () => {};
|
|
4872
|
+
const marker = window;
|
|
4873
|
+
if (typeof marker.__edrvExtPoll === "function") marker.__edrvExtPoll();
|
|
4874
|
+
const generatedId = createRequestId();
|
|
4875
|
+
if (!generatedId) {
|
|
4876
|
+
log$2.warn("安全随机源不可用,已停用外部打开心跳");
|
|
4877
|
+
return () => {};
|
|
4878
|
+
}
|
|
4879
|
+
const clientId = generatedId;
|
|
4880
|
+
let disposed = false;
|
|
4881
|
+
let polling = false;
|
|
4882
|
+
let executing = false;
|
|
4883
|
+
let ticks = 0;
|
|
4884
|
+
/**
|
|
4885
|
+
* 页面隐藏时按倍数跳轮;可见时每次都跑并清零计数。
|
|
4886
|
+
* @author ddj 2026年10月15号
|
|
4887
|
+
* @returns 是否跳过本轮
|
|
4888
|
+
*/
|
|
4889
|
+
function hiddenSkip() {
|
|
4890
|
+
if (typeof document === "undefined" || document.visibilityState !== "hidden") {
|
|
4891
|
+
ticks = 0;
|
|
4892
|
+
return false;
|
|
4893
|
+
}
|
|
4894
|
+
ticks += 1;
|
|
4895
|
+
return ticks % HIDDEN_POLL_EVERY !== 0;
|
|
4896
|
+
}
|
|
4897
|
+
/** @private @author ddj 2026年09月28号 Open all requested paths before acknowledging this exact lease. */
|
|
4898
|
+
async function execute(open) {
|
|
4899
|
+
executing = true;
|
|
4900
|
+
try {
|
|
4901
|
+
const success = await openDeepLink(ctx, open);
|
|
4902
|
+
if (!disposed) await rpc("edrv.external.ack", {
|
|
4903
|
+
token: open.token,
|
|
4904
|
+
leaseId: open.leaseId,
|
|
4905
|
+
clientId,
|
|
4906
|
+
success,
|
|
4907
|
+
error: success ? void 0 : "编辑器打开失败或取消"
|
|
4908
|
+
});
|
|
4909
|
+
} finally {
|
|
4910
|
+
executing = false;
|
|
4911
|
+
}
|
|
4912
|
+
}
|
|
4913
|
+
/** @private @author ddj 2026年09月28号 Poll while opening to renew the lease without accepting a second request. */
|
|
4914
|
+
async function tick() {
|
|
4915
|
+
if (disposed || polling) return;
|
|
4916
|
+
polling = true;
|
|
4917
|
+
try {
|
|
4918
|
+
const result = await rpc("edrv.external.pending", {
|
|
4919
|
+
clientId,
|
|
4920
|
+
busy: executing
|
|
4921
|
+
});
|
|
4922
|
+
if (!disposed && result.ok && result.open && !executing) execute(result.open).catch(() => {});
|
|
4923
|
+
} finally {
|
|
4924
|
+
polling = false;
|
|
4925
|
+
}
|
|
4926
|
+
}
|
|
4927
|
+
const timer = window.setInterval(() => {
|
|
4928
|
+
if (hiddenSkip()) return;
|
|
4929
|
+
tick().catch(() => {});
|
|
4930
|
+
}, HANDOFF_POLL_MS);
|
|
4931
|
+
/** @private @author ddj 2026年09月28号 Stop heartbeats and fail outstanding editor waits on plugin disposal. */
|
|
4932
|
+
function dispose() {
|
|
4933
|
+
disposed = true;
|
|
4934
|
+
window.clearInterval(timer);
|
|
4935
|
+
disposeOpenWait();
|
|
4936
|
+
if (marker.__edrvExtPoll === dispose) delete marker.__edrvExtPoll;
|
|
4937
|
+
}
|
|
4938
|
+
marker.__edrvExtPoll = dispose;
|
|
4939
|
+
tick().catch(() => {});
|
|
4940
|
+
return dispose;
|
|
4941
|
+
}
|
|
4942
|
+
/**
|
|
4943
|
+
* 跨源守卫:referrer 为空(launcher/Unity 直开)或同源 → 允许。
|
|
4944
|
+
* @author ddj 2026年09月07号
|
|
4945
|
+
* @param referrer 文档 referrer
|
|
4946
|
+
* @param origin 当前页面源
|
|
4947
|
+
* @returns 是否允许处理深链
|
|
4948
|
+
*/
|
|
4949
|
+
function referrerAllowed(referrer, origin) {
|
|
4950
|
+
if (!referrer) return true;
|
|
4951
|
+
try {
|
|
4952
|
+
return new URL(referrer).origin === origin;
|
|
4953
|
+
} catch {
|
|
4954
|
+
return false;
|
|
4955
|
+
}
|
|
4956
|
+
}
|
|
4957
|
+
/**
|
|
4958
|
+
* 剥离 URL 中的深链参数(保留其余参数与 hash,防刷新重开)。
|
|
4959
|
+
* @author ddj 2026年09月07号
|
|
4960
|
+
* @param href 完整 URL
|
|
4961
|
+
* @returns 清理后的 URL(pathname+search+hash)
|
|
4962
|
+
*/
|
|
4963
|
+
function stripOpenHref(href) {
|
|
4964
|
+
const url = new URL(href);
|
|
4965
|
+
for (const key of EDRV_PARAM_KEYS) url.searchParams.delete(key);
|
|
4966
|
+
return url.pathname + url.search + url.hash;
|
|
4967
|
+
}
|
|
4968
|
+
/** 清理当前页面 URL(best-effort,失败不阻塞打开)。 */
|
|
4969
|
+
function stripCurrentUrl() {
|
|
4970
|
+
try {
|
|
4971
|
+
if (typeof location === "undefined" || typeof history === "undefined") return;
|
|
4972
|
+
const next = stripOpenHref(location.href);
|
|
4973
|
+
if (next !== location.pathname + location.search + location.hash) history.replaceState(null, "", next);
|
|
4974
|
+
} catch {}
|
|
4975
|
+
}
|
|
4976
|
+
/**
|
|
4977
|
+
* 轻量 toast(编辑器未挂载也能提示;5s 自隐;openFlow 通知与错误呈现共用)。
|
|
4978
|
+
* @author ddj 2026年09月07号
|
|
4979
|
+
* @param text 提示文本
|
|
4980
|
+
*/
|
|
4981
|
+
function toastDom(text) {
|
|
4982
|
+
if (typeof document === "undefined") return;
|
|
4983
|
+
const toast = document.createElement("div");
|
|
4984
|
+
toast.textContent = text;
|
|
4985
|
+
toast.setAttribute("style", [
|
|
4986
|
+
"position:fixed",
|
|
4987
|
+
"left:50%",
|
|
4988
|
+
"bottom:28px",
|
|
4989
|
+
"transform:translateX(-50%)",
|
|
4990
|
+
"z-index:99999",
|
|
4991
|
+
"padding:8px 14px",
|
|
4992
|
+
"border-radius:8px",
|
|
4993
|
+
"max-width:80vw",
|
|
4994
|
+
"background:#2a2a2a",
|
|
4995
|
+
"color:#ddd",
|
|
4996
|
+
"border:1px solid #444",
|
|
4997
|
+
"font-size:12px",
|
|
4998
|
+
"box-shadow:0 4px 16px rgba(0,0,0,.35)"
|
|
4999
|
+
].join(";"));
|
|
5000
|
+
document.body?.appendChild(toast);
|
|
5001
|
+
setTimeout(() => toast.remove(), TOAST_MS);
|
|
5002
|
+
}
|
|
5003
|
+
//#endregion
|
|
3724
5004
|
//#region src/client/ui/ReviewAllConfirm.ts
|
|
3725
5005
|
/**
|
|
3726
5006
|
* @public
|
|
@@ -3731,7 +5011,7 @@ window.__ModuleLoader__.load({
|
|
|
3731
5011
|
*/
|
|
3732
5012
|
function ReviewAllConfirm(props) {
|
|
3733
5013
|
const action = props.reject ? "Undo All" : "Keep All";
|
|
3734
|
-
const warning = props.reject ? "撤销会改写磁盘;逐项安全校验,遇到冲突停止,已完成项不会自动恢复。" : "
|
|
5014
|
+
const warning = props.reject ? "撤销会改写磁盘;逐项安全校验,遇到冲突停止,已完成项不会自动恢复。" : "只标记待审修改为保留,不改写文件;个别条目若已变化或需在活跃端判定会被跳过并留在列表里,可逐个处理。";
|
|
3735
5015
|
const footer = react$1.default.createElement(react$1.default.Fragment, null, react$1.default.createElement(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
3736
5016
|
variant: "ghost",
|
|
3737
5017
|
onClick: props.onCancel,
|
|
@@ -4214,6 +5494,34 @@ window.__ModuleLoader__.load({
|
|
|
4214
5494
|
return !!page?.overview?.coveredPaths.some((candidate) => diffPathKey(candidate) === key);
|
|
4215
5495
|
}
|
|
4216
5496
|
/**
|
|
5497
|
+
* @author ddj 2026年11月3号
|
|
5498
|
+
* @description: 本页待审条目占用的路径键:路径本身与其身份各一份。
|
|
5499
|
+
* @param page 审查页
|
|
5500
|
+
* @returns 待审覆盖的路径键集合
|
|
5501
|
+
*/
|
|
5502
|
+
function pendingCoverKeys(page) {
|
|
5503
|
+
const keys = /* @__PURE__ */ new Set();
|
|
5504
|
+
for (const item of page?.items ?? []) {
|
|
5505
|
+
if (!item.id.startsWith("pending:")) continue;
|
|
5506
|
+
keys.add(diffPathKey(item.currentPath || item.path));
|
|
5507
|
+
keys.add(diffPathKey(item.id.slice(8)));
|
|
5508
|
+
}
|
|
5509
|
+
return keys;
|
|
5510
|
+
}
|
|
5511
|
+
/**
|
|
5512
|
+
* @public
|
|
5513
|
+
* @author ddj 2026年11月3号
|
|
5514
|
+
* @description: 取本页"已有权威通道在管"的路径键:官方汇总覆盖集 + 待审条目。
|
|
5515
|
+
* 编辑器与差异汇总必须同口径,否则同一改动在列表里被压掉、在编辑器里却仍然画得出来并可以点。
|
|
5516
|
+
* @param page 审查页
|
|
5517
|
+
* @returns 路径比较键集合
|
|
5518
|
+
*/
|
|
5519
|
+
function coveredPathKeys(page) {
|
|
5520
|
+
const keys = pendingCoverKeys(page);
|
|
5521
|
+
for (const path of page?.overview?.coveredPaths ?? []) keys.add(diffPathKey(path));
|
|
5522
|
+
return keys;
|
|
5523
|
+
}
|
|
5524
|
+
/**
|
|
4217
5525
|
* @author ddj 2026年10月30号
|
|
4218
5526
|
* @description: 取出本页待审条目(id 带待审前缀)并折算成文件行。
|
|
4219
5527
|
* @param page 审查页
|
|
@@ -4221,12 +5529,9 @@ window.__ModuleLoader__.load({
|
|
|
4221
5529
|
*/
|
|
4222
5530
|
function pendingRowsOf(page) {
|
|
4223
5531
|
const rows = [];
|
|
4224
|
-
const covered = /* @__PURE__ */ new Set();
|
|
4225
5532
|
for (const item of page?.items ?? []) {
|
|
4226
5533
|
if (!item.id.startsWith("pending:")) continue;
|
|
4227
5534
|
const path = item.currentPath || item.path;
|
|
4228
|
-
covered.add(diffPathKey(path));
|
|
4229
|
-
covered.add(diffPathKey(item.id.slice(8)));
|
|
4230
5535
|
rows.push({
|
|
4231
5536
|
path,
|
|
4232
5537
|
label: item.display || path,
|
|
@@ -4239,7 +5544,7 @@ window.__ModuleLoader__.load({
|
|
|
4239
5544
|
}
|
|
4240
5545
|
return {
|
|
4241
5546
|
rows,
|
|
4242
|
-
covered
|
|
5547
|
+
covered: pendingCoverKeys(page)
|
|
4243
5548
|
};
|
|
4244
5549
|
}
|
|
4245
5550
|
/** @public @author ddj 2026年10月26号 @param records Tool-call history. @param page Official file navigation, never only its first 50 rows. @returns Pending first, then official; legacy paths remain explicitly labelled. */
|
|
@@ -145912,10 +147217,10 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
145912
147217
|
var require_useMergeRef = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
145913
147218
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
145914
147219
|
exports.useMergeRefs = void 0;
|
|
145915
|
-
var React$
|
|
147220
|
+
var React$68 = (init_tslib_es6(), __toCommonJS(tslib_es6_exports)).__importStar(require("react"));
|
|
145916
147221
|
var assignRef_1 = require_assignRef();
|
|
145917
147222
|
var useRef_1 = require_useRef();
|
|
145918
|
-
var useIsomorphicLayoutEffect = typeof window !== "undefined" ? React$
|
|
147223
|
+
var useIsomorphicLayoutEffect = typeof window !== "undefined" ? React$68.useLayoutEffect : React$68.useEffect;
|
|
145919
147224
|
var currentValues = /* @__PURE__ */ new WeakMap();
|
|
145920
147225
|
/**
|
|
145921
147226
|
* Merges two or more refs together providing a single interface to set their value
|
|
@@ -146195,7 +147500,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
146195
147500
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
146196
147501
|
exports.sidecar = void 0;
|
|
146197
147502
|
var tslib_1 = (init_tslib_es6(), __toCommonJS(tslib_es6_exports));
|
|
146198
|
-
var React$
|
|
147503
|
+
var React$67 = tslib_1.__importStar(require("react"));
|
|
146199
147504
|
var hook_1 = require_hook$1();
|
|
146200
147505
|
function sidecar(importer, errorComponent) {
|
|
146201
147506
|
var ErrorCase = function() {
|
|
@@ -146204,7 +147509,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
146204
147509
|
return function Sidecar(props) {
|
|
146205
147510
|
var _a = (0, hook_1.useSidecar)(importer, props.sideCar), Car = _a[0];
|
|
146206
147511
|
if (_a[1] && errorComponent) return ErrorCase;
|
|
146207
|
-
return Car ? React$
|
|
147512
|
+
return Car ? React$67.createElement(Car, tslib_1.__assign({}, props)) : null;
|
|
146208
147513
|
};
|
|
146209
147514
|
}
|
|
146210
147515
|
exports.sidecar = sidecar;
|
|
@@ -146319,7 +147624,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
146319
147624
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
146320
147625
|
exports.renderCar = void 0;
|
|
146321
147626
|
var tslib_1 = (init_tslib_es6(), __toCommonJS(tslib_es6_exports));
|
|
146322
|
-
var React$
|
|
147627
|
+
var React$66 = tslib_1.__importStar(require("react"));
|
|
146323
147628
|
var react_1 = require("react");
|
|
146324
147629
|
function renderCar(WrappedComponent, defaults) {
|
|
146325
147630
|
function State(_a) {
|
|
@@ -146332,9 +147637,9 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
146332
147637
|
});
|
|
146333
147638
|
return null;
|
|
146334
147639
|
}, []);
|
|
146335
|
-
return React$
|
|
147640
|
+
return React$66.createElement(WrappedComponent, tslib_1.__assign({}, props, { children: renderTarget }));
|
|
146336
147641
|
}
|
|
146337
|
-
var Children = React$
|
|
147642
|
+
var Children = React$66.memo(function(_a) {
|
|
146338
147643
|
var stateRef = _a.stateRef, defaultState = _a.defaultState, children = _a.children;
|
|
146339
147644
|
var _b = (0, react_1.useState)(defaultState.current), state = _b[0], setState = _b[1];
|
|
146340
147645
|
(0, react_1.useEffect)(function() {
|
|
@@ -146345,14 +147650,14 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
146345
147650
|
return true;
|
|
146346
147651
|
});
|
|
146347
147652
|
return function Combiner(props) {
|
|
146348
|
-
var defaultState = React$
|
|
146349
|
-
var ref = React$
|
|
147653
|
+
var defaultState = React$66.useRef(defaults(props));
|
|
147654
|
+
var ref = React$66.useRef(function(state) {
|
|
146350
147655
|
return defaultState.current = state;
|
|
146351
147656
|
});
|
|
146352
|
-
return React$
|
|
147657
|
+
return React$66.createElement(React$66.Fragment, null, React$66.createElement(State, {
|
|
146353
147658
|
stateRef: ref,
|
|
146354
147659
|
props
|
|
146355
|
-
}), React$
|
|
147660
|
+
}), React$66.createElement(Children, {
|
|
146356
147661
|
stateRef: ref,
|
|
146357
147662
|
defaultState,
|
|
146358
147663
|
children: props.children
|
|
@@ -146367,13 +147672,13 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
146367
147672
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
146368
147673
|
exports.exportSidecar = void 0;
|
|
146369
147674
|
var tslib_1 = (init_tslib_es6(), __toCommonJS(tslib_es6_exports));
|
|
146370
|
-
var React$
|
|
147675
|
+
var React$65 = tslib_1.__importStar(require("react"));
|
|
146371
147676
|
var SideCar = function(_a) {
|
|
146372
147677
|
var sideCar = _a.sideCar, rest = tslib_1.__rest(_a, ["sideCar"]);
|
|
146373
147678
|
if (!sideCar) throw new Error("Sidecar: please provide `sideCar` property to import the right car");
|
|
146374
147679
|
var Target = sideCar.read();
|
|
146375
147680
|
if (!Target) throw new Error("Sidecar medium not found");
|
|
146376
|
-
return React$
|
|
147681
|
+
return React$65.createElement(Target, tslib_1.__assign({}, rest));
|
|
146377
147682
|
};
|
|
146378
147683
|
SideCar.isSideCarExport = true;
|
|
146379
147684
|
function exportSidecar(medium, exported) {
|
|
@@ -146449,7 +147754,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
146449
147754
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
146450
147755
|
exports.RemoveScroll = void 0;
|
|
146451
147756
|
var tslib_1 = (init_tslib_es6(), __toCommonJS(tslib_es6_exports));
|
|
146452
|
-
var React$
|
|
147757
|
+
var React$64 = tslib_1.__importStar(require("react"));
|
|
146453
147758
|
var constants_1 = require_constants();
|
|
146454
147759
|
var use_callback_ref_1 = require_es5$5();
|
|
146455
147760
|
var medium_1 = require_medium();
|
|
@@ -146457,9 +147762,9 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
146457
147762
|
/**
|
|
146458
147763
|
* Removes scrollbar from the page and contain the scroll within the Lock
|
|
146459
147764
|
*/
|
|
146460
|
-
var RemoveScroll = React$
|
|
146461
|
-
var ref = React$
|
|
146462
|
-
var _a = React$
|
|
147765
|
+
var RemoveScroll = React$64.forwardRef(function(props, parentRef) {
|
|
147766
|
+
var ref = React$64.useRef(null);
|
|
147767
|
+
var _a = React$64.useState({
|
|
146463
147768
|
onScrollCapture: nothing,
|
|
146464
147769
|
onWheelCapture: nothing,
|
|
146465
147770
|
onTouchMoveCapture: nothing
|
|
@@ -146482,7 +147787,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
146482
147787
|
var SideCar = sideCar;
|
|
146483
147788
|
var containerRef = (0, use_callback_ref_1.useMergeRefs)([ref, parentRef]);
|
|
146484
147789
|
var containerProps = tslib_1.__assign(tslib_1.__assign({}, rest), callbacks);
|
|
146485
|
-
return React$
|
|
147790
|
+
return React$64.createElement(React$64.Fragment, null, enabled && React$64.createElement(SideCar, {
|
|
146486
147791
|
sideCar: medium_1.effectCar,
|
|
146487
147792
|
removeScrollBar,
|
|
146488
147793
|
shards,
|
|
@@ -146493,7 +147798,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
146493
147798
|
allowPinchZoom: !!allowPinchZoom,
|
|
146494
147799
|
lockRef: ref,
|
|
146495
147800
|
gapMode
|
|
146496
|
-
}), forwardProps ? React$
|
|
147801
|
+
}), forwardProps ? React$64.cloneElement(React$64.Children.only(children), tslib_1.__assign(tslib_1.__assign({}, containerProps), { ref: containerRef })) : React$64.createElement(Container, tslib_1.__assign({}, containerProps, {
|
|
146497
147802
|
className,
|
|
146498
147803
|
ref: containerRef
|
|
146499
147804
|
}), children));
|
|
@@ -146572,7 +147877,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
146572
147877
|
var require_hook = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
146573
147878
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
146574
147879
|
exports.styleHookSingleton = void 0;
|
|
146575
|
-
var React$
|
|
147880
|
+
var React$63 = (init_tslib_es6(), __toCommonJS(tslib_es6_exports)).__importStar(require("react"));
|
|
146576
147881
|
var singleton_1 = require_singleton();
|
|
146577
147882
|
/**
|
|
146578
147883
|
* creates a hook to control style singleton
|
|
@@ -146586,7 +147891,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
146586
147891
|
var styleHookSingleton = function() {
|
|
146587
147892
|
var sheet = (0, singleton_1.stylesheetSingleton)();
|
|
146588
147893
|
return function(styles, isDynamic) {
|
|
146589
|
-
React$
|
|
147894
|
+
React$63.useEffect(function() {
|
|
146590
147895
|
sheet.add(styles);
|
|
146591
147896
|
return function() {
|
|
146592
147897
|
sheet.remove();
|
|
@@ -146691,7 +147996,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
146691
147996
|
var require_component = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
146692
147997
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
146693
147998
|
exports.RemoveScrollBar = exports.useLockAttribute = exports.lockAttribute = void 0;
|
|
146694
|
-
var React$
|
|
147999
|
+
var React$62 = (init_tslib_es6(), __toCommonJS(tslib_es6_exports)).__importStar(require("react"));
|
|
146695
148000
|
var react_style_singleton_1 = require_es5$2();
|
|
146696
148001
|
var constants_1 = require_constants();
|
|
146697
148002
|
var utils_1 = require_utils();
|
|
@@ -146711,7 +148016,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
146711
148016
|
return isFinite(counter) ? counter : 0;
|
|
146712
148017
|
};
|
|
146713
148018
|
var useLockAttribute = function() {
|
|
146714
|
-
React$
|
|
148019
|
+
React$62.useEffect(function() {
|
|
146715
148020
|
document.body.setAttribute(exports.lockAttribute, (getCurrentUseCounter() + 1).toString());
|
|
146716
148021
|
return function() {
|
|
146717
148022
|
var newCounter = getCurrentUseCounter() - 1;
|
|
@@ -146727,10 +148032,10 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
146727
148032
|
var RemoveScrollBar = function(_a) {
|
|
146728
148033
|
var noRelative = _a.noRelative, noImportant = _a.noImportant, _b = _a.gapMode, gapMode = _b === void 0 ? "margin" : _b;
|
|
146729
148034
|
(0, exports.useLockAttribute)();
|
|
146730
|
-
var gap = React$
|
|
148035
|
+
var gap = React$62.useMemo(function() {
|
|
146731
148036
|
return (0, utils_1.getGapWidth)(gapMode);
|
|
146732
148037
|
}, [gapMode]);
|
|
146733
|
-
return React$
|
|
148038
|
+
return React$62.createElement(Style, { styles: getStyles(gap, !noRelative, gapMode, !noImportant ? "!important" : "") });
|
|
146734
148039
|
};
|
|
146735
148040
|
exports.RemoveScrollBar = RemoveScrollBar;
|
|
146736
148041
|
}));
|
|
@@ -146892,7 +148197,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
146892
148197
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
146893
148198
|
exports.RemoveScrollSideCar = exports.getDeltaXY = exports.getTouchXY = void 0;
|
|
146894
148199
|
var tslib_1 = (init_tslib_es6(), __toCommonJS(tslib_es6_exports));
|
|
146895
|
-
var React$
|
|
148200
|
+
var React$61 = tslib_1.__importStar(require("react"));
|
|
146896
148201
|
var react_remove_scroll_bar_1 = require_es5$1();
|
|
146897
148202
|
var react_style_singleton_1 = require_es5$2();
|
|
146898
148203
|
var aggresiveCapture_1 = require_aggresiveCapture();
|
|
@@ -146917,16 +148222,16 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
146917
148222
|
var idCounter = 0;
|
|
146918
148223
|
var lockStack = [];
|
|
146919
148224
|
function RemoveScrollSideCar(props) {
|
|
146920
|
-
var shouldPreventQueue = React$
|
|
146921
|
-
var touchStartRef = React$
|
|
146922
|
-
var activeAxis = React$
|
|
146923
|
-
var id = React$
|
|
146924
|
-
var Style = React$
|
|
146925
|
-
var lastProps = React$
|
|
146926
|
-
React$
|
|
148225
|
+
var shouldPreventQueue = React$61.useRef([]);
|
|
148226
|
+
var touchStartRef = React$61.useRef([0, 0]);
|
|
148227
|
+
var activeAxis = React$61.useRef();
|
|
148228
|
+
var id = React$61.useState(idCounter++)[0];
|
|
148229
|
+
var Style = React$61.useState(react_style_singleton_1.styleSingleton)[0];
|
|
148230
|
+
var lastProps = React$61.useRef(props);
|
|
148231
|
+
React$61.useEffect(function() {
|
|
146927
148232
|
lastProps.current = props;
|
|
146928
148233
|
}, [props]);
|
|
146929
|
-
React$
|
|
148234
|
+
React$61.useEffect(function() {
|
|
146930
148235
|
if (props.inert) {
|
|
146931
148236
|
document.body.classList.add("block-interactivity-".concat(id));
|
|
146932
148237
|
var allow_1 = tslib_1.__spreadArray([props.lockRef.current], (props.shards || []).map(extractRef), true).filter(Boolean);
|
|
@@ -146945,7 +148250,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
146945
148250
|
props.lockRef.current,
|
|
146946
148251
|
props.shards
|
|
146947
148252
|
]);
|
|
146948
|
-
var shouldCancelEvent = React$
|
|
148253
|
+
var shouldCancelEvent = React$61.useCallback(function(event, parent) {
|
|
146949
148254
|
if ("touches" in event && event.touches.length === 2 || event.type === "wheel" && event.ctrlKey) return !lastProps.current.allowPinchZoom;
|
|
146950
148255
|
var touch = (0, exports.getTouchXY)(event);
|
|
146951
148256
|
var touchStart = touchStartRef.current;
|
|
@@ -146971,7 +148276,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
146971
148276
|
var cancelingAxis = activeAxis.current || currentAxis;
|
|
146972
148277
|
return (0, handleScroll_1.handleScroll)(cancelingAxis, parent, event, cancelingAxis === "h" ? deltaX : deltaY, true);
|
|
146973
148278
|
}, []);
|
|
146974
|
-
var shouldPrevent = React$
|
|
148279
|
+
var shouldPrevent = React$61.useCallback(function(_event) {
|
|
146975
148280
|
var event = _event;
|
|
146976
148281
|
if (!lockStack.length || lockStack[lockStack.length - 1] !== Style) return;
|
|
146977
148282
|
var delta = "deltaY" in event ? (0, exports.getDeltaXY)(event) : (0, exports.getTouchXY)(event);
|
|
@@ -146991,7 +148296,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
146991
148296
|
}
|
|
146992
148297
|
}
|
|
146993
148298
|
}, []);
|
|
146994
|
-
var shouldCancel = React$
|
|
148299
|
+
var shouldCancel = React$61.useCallback(function(name, delta, target, should) {
|
|
146995
148300
|
var event = {
|
|
146996
148301
|
name,
|
|
146997
148302
|
delta,
|
|
@@ -147006,17 +148311,17 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
147006
148311
|
});
|
|
147007
148312
|
}, 1);
|
|
147008
148313
|
}, []);
|
|
147009
|
-
var scrollTouchStart = React$
|
|
148314
|
+
var scrollTouchStart = React$61.useCallback(function(event) {
|
|
147010
148315
|
touchStartRef.current = (0, exports.getTouchXY)(event);
|
|
147011
148316
|
activeAxis.current = void 0;
|
|
147012
148317
|
}, []);
|
|
147013
|
-
var scrollWheel = React$
|
|
148318
|
+
var scrollWheel = React$61.useCallback(function(event) {
|
|
147014
148319
|
shouldCancel(event.type, (0, exports.getDeltaXY)(event), event.target, shouldCancelEvent(event, props.lockRef.current));
|
|
147015
148320
|
}, []);
|
|
147016
|
-
var scrollTouchMove = React$
|
|
148321
|
+
var scrollTouchMove = React$61.useCallback(function(event) {
|
|
147017
148322
|
shouldCancel(event.type, (0, exports.getTouchXY)(event), event.target, shouldCancelEvent(event, props.lockRef.current));
|
|
147018
148323
|
}, []);
|
|
147019
|
-
React$
|
|
148324
|
+
React$61.useEffect(function() {
|
|
147020
148325
|
lockStack.push(Style);
|
|
147021
148326
|
props.setCallbacks({
|
|
147022
148327
|
onScrollCapture: scrollWheel,
|
|
@@ -147036,7 +148341,7 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
147036
148341
|
};
|
|
147037
148342
|
}, []);
|
|
147038
148343
|
var removeScrollBar = props.removeScrollBar, inert = props.inert;
|
|
147039
|
-
return React$
|
|
148344
|
+
return React$61.createElement(React$61.Fragment, null, inert ? React$61.createElement(Style, { styles: generateStyle(id) }) : null, removeScrollBar ? React$61.createElement(react_remove_scroll_bar_1.RemoveScrollBar, {
|
|
147040
148345
|
noRelative: props.noRelative,
|
|
147041
148346
|
gapMode: props.gapMode
|
|
147042
148347
|
}) : null);
|
|
@@ -147068,11 +148373,11 @@ For more info please visit our website: https://redi.wzhu.dev/en-US/docs/faq#imp
|
|
|
147068
148373
|
var require_Combination = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
147069
148374
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
147070
148375
|
var tslib_1 = (init_tslib_es6(), __toCommonJS(tslib_es6_exports));
|
|
147071
|
-
var React$
|
|
148376
|
+
var React$60 = tslib_1.__importStar(require("react"));
|
|
147072
148377
|
var UI_1 = require_UI();
|
|
147073
148378
|
var sidecar_1 = tslib_1.__importDefault(require_sidecar());
|
|
147074
|
-
var ReactRemoveScroll = React$
|
|
147075
|
-
return React$
|
|
148379
|
+
var ReactRemoveScroll = React$60.forwardRef(function(props, ref) {
|
|
148380
|
+
return React$60.createElement(UI_1.RemoveScroll, tslib_1.__assign({}, props, {
|
|
147076
148381
|
ref,
|
|
147077
148382
|
sideCar: sidecar_1.default
|
|
147078
148383
|
}));
|
|
@@ -382739,6 +384044,8 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
382739
384044
|
const SIDECAR_KEEP_TITLE = "标记这些历史改动为已看过:只改记录,不改写磁盘文件,也不影响官方审查。";
|
|
382740
384045
|
/** 非官方 Undo 的作用域说明:走原有回滚校验写回磁盘,但不影响官方记录。 */
|
|
382741
384046
|
const SIDECAR_UNDO_TITLE = "撤销这些历史改动(写回受回滚校验保护):只影响这份历史记录,不影响官方审查。";
|
|
384047
|
+
/** 非官方 Keep 之后的完成态:该通道 Keep 不写盘,不能沿用「已采纳」这种会让人以为改过文件的措辞。 */
|
|
384048
|
+
const SIDECAR_KEEP_DONE = "已标记为看过(未改动文件)";
|
|
382742
384049
|
/** 当前端没有该会话时,差异栏里给中性说明而不是红色报错。 */
|
|
382743
384050
|
const SESSION_DOWN_NOTICE = "这个会话没在当前端激活,官方审查数据暂时读不到;下面的历史改动仍可查看、Keep、Undo。";
|
|
382744
384051
|
/**
|
|
@@ -384506,641 +385813,6 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
384506
385813
|
localStorage.setItem(key, text);
|
|
384507
385814
|
} catch (error) {}
|
|
384508
385815
|
}
|
|
384509
|
-
//#endregion
|
|
384510
|
-
//#region src/client/addToConversation.ts
|
|
384511
|
-
/**
|
|
384512
|
-
* dsh-vscode-mode client — 把文件/选中内容注入会话对话输入框(composer draft)。
|
|
384513
|
-
* 复用 DSH 官方输入门面:ctx.conversation.input.for(sessions.scope(sessionId)) → SessionInput。
|
|
384514
|
-
* 引用 chip:insertReference(reference source,发送时序列化为 @path 文本);忙态降级为纯文本 setDraft。
|
|
384515
|
-
* 纯函数部分 node 可单测。
|
|
384516
|
-
* 作者 ddj 2026-08-25
|
|
384517
|
-
*/
|
|
384518
|
-
/** 把追加引用结果映射为可读文案(ok 用 okText;busy 提示已降级纯文本;unavailable 提示不可用)。
|
|
384519
|
-
* @author ddj 2026年09月03号 / 2026年09月11号
|
|
384520
|
-
* @param outcome 追加结果状态
|
|
384521
|
-
* @param okText 成功文案(如「已添加文件引用」/「已添加文件夹引用」)
|
|
384522
|
-
* @returns 状态栏/通知用文案
|
|
384523
|
-
*/
|
|
384524
|
-
function statusOfAdd(outcome, okText) {
|
|
384525
|
-
if (outcome === "ok") return okText;
|
|
384526
|
-
if (outcome === "busy") return okText + "(已降级纯文本)";
|
|
384527
|
-
if (outcome === "failed") return "添加引用失败(输入框忙或未就绪,请重试)";
|
|
384528
|
-
return "无法添加到对话(无会话或输入框不可用)";
|
|
384529
|
-
}
|
|
384530
|
-
/** DSH reference source 名(dsh-client-ui-reference 注册的 @file/@session 统一源)。 */
|
|
384531
|
-
const REF_SOURCE = "reference";
|
|
384532
|
-
/**
|
|
384533
|
-
* 把 clipboard 投影的末尾折算成 **detect 投影**的末尾。
|
|
384534
|
-
*
|
|
384535
|
-
* ⚠️ 两个投影对 chip 的计长不同,混用会直接毁掉已插入的引用(真实缺陷,勿改回 `draft.length`):
|
|
384536
|
-
* DSH 的 `$composerLayout()` 中 chip 在 detect 投影里只贡献 **1 个 U+FFFC**,
|
|
384537
|
-
* 在 clipboard 投影里贡献 `clipboardText` 全文
|
|
384538
|
-
* (`pushLeaf('chip', kid, '', kid.getTextContent())`)。
|
|
384539
|
-
* 而 `insertReference(ref, span)` 的 span 走 detect 坐标并经 `selectSpan` 校验:
|
|
384540
|
-
* `span.end > layout.detectLength` 时返回 null → 插入失败。
|
|
384541
|
-
*
|
|
384542
|
-
* 旧实现直接用 `draft.length`(clipboard 长度)当 detect 起点:草稿里一旦有 chip,
|
|
384543
|
-
* 该值就超出 detect 长度 → 插入被拒 → 落入 `setDraft` 兜底,
|
|
384544
|
-
* 而 `setDraft` 会整篇重建并剔除 U+FFFC(`text.replace(REFERENCE_PLACEHOLDER_RE, '')`),
|
|
384545
|
-
* 于是**既有 chip 被销毁、引用退化为重复的纯文本**(即「连续添加多个引用显示异常」)。
|
|
384546
|
-
*
|
|
384547
|
-
* 换算:每个 chip 在 clipboard 中多算了 `length - 1`,逐个扣回即得 detect 末尾。
|
|
384548
|
-
* 无 occurrences(旧版 DSH)时退化为 `draft.length`,与旧行为一致。
|
|
384549
|
-
* @author ddj 2026年09月11号
|
|
384550
|
-
* @param snapshot 输入快照
|
|
384551
|
-
* @returns detect 投影中的文档末尾偏移
|
|
384552
|
-
*/
|
|
384553
|
-
function detectEndOf(snapshot) {
|
|
384554
|
-
const total = String(snapshot?.draft ?? "").length;
|
|
384555
|
-
const occurrences = snapshot?.occurrences;
|
|
384556
|
-
if (!Array.isArray(occurrences) || !occurrences.length) return total;
|
|
384557
|
-
let clipped = 0;
|
|
384558
|
-
for (const occurrence of occurrences) {
|
|
384559
|
-
const length = Number(occurrence?.length ?? 0);
|
|
384560
|
-
if (!Number.isFinite(length) || length <= 1) continue;
|
|
384561
|
-
clipped += length - 1;
|
|
384562
|
-
}
|
|
384563
|
-
return Math.max(0, total - clipped);
|
|
384564
|
-
}
|
|
384565
|
-
/** chip 列表按 clipboard 偏移升序(官方已排序,此处防御性归一,丢弃非法项)。 */
|
|
384566
|
-
function chipsOf(snapshot) {
|
|
384567
|
-
const occurrences = snapshot?.occurrences;
|
|
384568
|
-
if (!Array.isArray(occurrences) || !occurrences.length) return [];
|
|
384569
|
-
const out = [];
|
|
384570
|
-
for (const occurrence of occurrences) {
|
|
384571
|
-
const offset = Number(occurrence?.offset ?? NaN);
|
|
384572
|
-
const length = Number(occurrence?.length ?? NaN);
|
|
384573
|
-
if (!Number.isFinite(offset) || !Number.isFinite(length) || length <= 1) continue;
|
|
384574
|
-
out.push({
|
|
384575
|
-
offset: Math.max(0, offset),
|
|
384576
|
-
length
|
|
384577
|
-
});
|
|
384578
|
-
}
|
|
384579
|
-
return out.sort((a, b) => a.offset - b.offset);
|
|
384580
|
-
}
|
|
384581
|
-
/**
|
|
384582
|
-
* detect 偏移 → clipboard 偏移(paddingAt 用:draft 是 clipboard 投影,不能直接按下标取字符)。
|
|
384583
|
-
* 每个位于该偏移之前的 chip 在 clipboard 中多占 `length − 1`,累计补上即可。
|
|
384584
|
-
* @author ddj 2026年09月11号
|
|
384585
|
-
* @param snapshot 输入快照
|
|
384586
|
-
* @param detectOffset detect 投影偏移
|
|
384587
|
-
* @returns clipboard 投影偏移(已夹到 draft 范围内)
|
|
384588
|
-
*/
|
|
384589
|
-
function detectToClipboardOf(snapshot, detectOffset) {
|
|
384590
|
-
const total = String(snapshot?.draft ?? "").length;
|
|
384591
|
-
const target = Math.max(0, Math.min(Number(detectOffset) || 0, detectEndOf(snapshot)));
|
|
384592
|
-
let shift = 0;
|
|
384593
|
-
for (const chip of chipsOf(snapshot)) {
|
|
384594
|
-
if (chip.offset - shift >= target) break;
|
|
384595
|
-
shift += chip.length - 1;
|
|
384596
|
-
}
|
|
384597
|
-
return Math.max(0, Math.min(target + shift, total));
|
|
384598
|
-
}
|
|
384599
|
-
/**
|
|
384600
|
-
* 插入点两侧是否需要补空格(仅纯文本通道需要;chip 由 facade 自行补尾随空格)。
|
|
384601
|
-
*
|
|
384602
|
-
* 在 detect 坐标的任意位置求值,故**末尾追加的行为与旧实现逐字符一致**
|
|
384603
|
-
* (末尾前为非空白 → 前后各补一个空格;末尾前已是空白 → 只补尾随空格),
|
|
384604
|
-
* 同时正确支持「插在文本中间」不再贴字。
|
|
384605
|
-
* @author ddj 2026年09月11号
|
|
384606
|
-
* @param snapshot 输入快照
|
|
384607
|
-
* @param detectOffset detect 投影插入点
|
|
384608
|
-
* @returns 是否需要前导/尾随空格
|
|
384609
|
-
*/
|
|
384610
|
-
function paddingAt(snapshot, detectOffset) {
|
|
384611
|
-
const draft = String(snapshot?.draft ?? "");
|
|
384612
|
-
const at = detectToClipboardOf(snapshot, detectOffset);
|
|
384613
|
-
const before = at > 0 ? draft.slice(at - 1, at) : "";
|
|
384614
|
-
const after = at < draft.length ? draft.slice(at, at + 1) : "";
|
|
384615
|
-
return {
|
|
384616
|
-
lead: before !== "" && !/\s/.test(before),
|
|
384617
|
-
tail: after === "" || !/\s/.test(after)
|
|
384618
|
-
};
|
|
384619
|
-
}
|
|
384620
|
-
/**
|
|
384621
|
-
* 解析插入点(detect 坐标):优先用输入框**当前光标/选区**(`caretSpan()`,
|
|
384622
|
-
* 官方契约即 detect 坐标,无选区时回落文档末尾),并夹到 `[0, detectLength]`。
|
|
384623
|
-
*
|
|
384624
|
-
* 选区非塌缩时取 `end`(右边界)—— 插入而非替换,绝不删除用户已选中的内容。
|
|
384625
|
-
* caret 缺失/非法(旧版 facade 无该方法、抛异常、NaN 等)一律回落到文档末尾。
|
|
384626
|
-
* @author ddj 2026年09月11号
|
|
384627
|
-
* @param caret caretSpan() 的返回值(可为 null/非法)
|
|
384628
|
-
* @param detectLength detect 投影文档长度
|
|
384629
|
-
* @returns detect 投影插入偏移
|
|
384630
|
-
*/
|
|
384631
|
-
function insertOffsetOf(caret, detectLength) {
|
|
384632
|
-
const max = Math.max(0, Number(detectLength) || 0);
|
|
384633
|
-
if (!caret || typeof caret !== "object") return max;
|
|
384634
|
-
const span = caret;
|
|
384635
|
-
const raw = Number(span.end);
|
|
384636
|
-
const value = Number.isFinite(raw) ? raw : Number(span.start);
|
|
384637
|
-
if (!Number.isFinite(value)) return max;
|
|
384638
|
-
return Math.max(0, Math.min(value, max));
|
|
384639
|
-
}
|
|
384640
|
-
/**
|
|
384641
|
-
* 生成 DSH @file 语法引用串:cwd 相对化、\ → /、含空白时按 @"path" 语法加引号。
|
|
384642
|
-
* cwd 外/无法相对化的路径回退原路径。
|
|
384643
|
-
* @author ddj 2026年08月25号
|
|
384644
|
-
* @param path 打开的文件路径(相对 cwd 或绝对)
|
|
384645
|
-
* @param cwd 会话工作区目录(可选)
|
|
384646
|
-
* @returns 引用串(如 @src/index.ts 或 @"a b.ts")
|
|
384647
|
-
*/
|
|
384648
|
-
function mentionOf(path, cwd) {
|
|
384649
|
-
const raw = String(path);
|
|
384650
|
-
let rel = raw;
|
|
384651
|
-
if (cwd) {
|
|
384652
|
-
const base = String(cwd).replace(/[\\/]+$/, "");
|
|
384653
|
-
const up = raw.replace(/\\/g, "/");
|
|
384654
|
-
const baseUp = base.replace(/\\/g, "/");
|
|
384655
|
-
if (up.startsWith(baseUp + "/")) rel = up.slice(baseUp.length + 1);
|
|
384656
|
-
else if (up === baseUp) rel = "";
|
|
384657
|
-
}
|
|
384658
|
-
rel = rel.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
384659
|
-
if (!rel) rel = raw.replace(/\\/g, "/");
|
|
384660
|
-
if (/[\u0000-\u001f\u007f-\u009f"]/u.test(rel)) return "@" + raw;
|
|
384661
|
-
return /\s/u.test(rel) ? `@"${rel}"` : `@${rel}`;
|
|
384662
|
-
}
|
|
384663
|
-
/** 引用标签:文件名 + 可选行区间(chip 内展示,不带 @ 前缀)。
|
|
384664
|
-
* @author ddj 2026年08月25号 */
|
|
384665
|
-
function labelOf(path, range) {
|
|
384666
|
-
const base = String(path).split(/[\\/]/).pop() || String(path);
|
|
384667
|
-
if (!range || range.endLine < range.startLine) return base;
|
|
384668
|
-
return range.startLine === range.endLine ? `${base} L${range.startLine}` : `${base} L${range.startLine}-${range.endLine}`;
|
|
384669
|
-
}
|
|
384670
|
-
/** 引用 ref:引用串 + 可选行区间(序列化直出,agent 据此读取对应片段)。
|
|
384671
|
-
* @author ddj 2026年08月25号 */
|
|
384672
|
-
function refOf(mention, range) {
|
|
384673
|
-
if (!range || range.endLine < range.startLine) return mention;
|
|
384674
|
-
return range.startLine === range.endLine ? `${mention} L${range.startLine}` : `${mention} L${range.startLine}-${range.endLine}`;
|
|
384675
|
-
}
|
|
384676
|
-
/** 构造 DSH 文件引用插入载荷,模型文本、剪贴板投影与降级文本共用完整行区间。
|
|
384677
|
-
* @public
|
|
384678
|
-
* @author ddj 2026年10月28号
|
|
384679
|
-
* @returns 引用载荷与含可选行区间的完整引用文本
|
|
384680
|
-
* @param path 目标路径(相对 cwd 或绝对)
|
|
384681
|
-
* @param cwd 会话工作区目录(可选)
|
|
384682
|
-
* @param range 行区间(可选)
|
|
384683
|
-
* @param appearance 引用外观:'file'(默认)| 'folder'
|
|
384684
|
-
*/
|
|
384685
|
-
function buildFileRef(path, cwd, range, appearance = "file") {
|
|
384686
|
-
const mention = refOf(mentionOf(path, cwd), range);
|
|
384687
|
-
return {
|
|
384688
|
-
mention,
|
|
384689
|
-
reference: {
|
|
384690
|
-
source: REF_SOURCE,
|
|
384691
|
-
ref: mention,
|
|
384692
|
-
label: labelOf(path, range),
|
|
384693
|
-
appearance,
|
|
384694
|
-
clipboardText: mention
|
|
384695
|
-
}
|
|
384696
|
-
};
|
|
384697
|
-
}
|
|
384698
|
-
/** @private @author ddj 2026年10月29号 @param receipt Raw selected-channel receipt. @returns Explicit admission evidence and the historical boolean. */
|
|
384699
|
-
function taskReceipt(receipt) {
|
|
384700
|
-
const ok = receipt?.ok;
|
|
384701
|
-
if (ok === true) return {
|
|
384702
|
-
status: "accepted",
|
|
384703
|
-
legacyOk: true
|
|
384704
|
-
};
|
|
384705
|
-
if (ok === false) return {
|
|
384706
|
-
status: "rejected",
|
|
384707
|
-
legacyOk: false
|
|
384708
|
-
};
|
|
384709
|
-
return {
|
|
384710
|
-
status: "unknown",
|
|
384711
|
-
legacyOk: false
|
|
384712
|
-
};
|
|
384713
|
-
}
|
|
384714
|
-
/** @private @author ddj 2026年10月29号 @param ctx Lifecycle owner. @param sessions Public Controller. @param id Fixed target. @param text Task body. @returns Admission evidence without switching transport. @sideEffects Retains and releases exactly one generation. */
|
|
384715
|
-
async function sendOwned(ctx, sessions, id, text) {
|
|
384716
|
-
const controller = new AbortController();
|
|
384717
|
-
const reference = sessions.retain(id, {
|
|
384718
|
-
source: "controllerOperation",
|
|
384719
|
-
signal: controller.signal
|
|
384720
|
-
});
|
|
384721
|
-
let released = false;
|
|
384722
|
-
let cleanupFailed = false;
|
|
384723
|
-
/** @private @author ddj 2026年10月29号 Cancels and releases this exact generation once; remembers unload cleanup failures. */
|
|
384724
|
-
function release() {
|
|
384725
|
-
if (released) return;
|
|
384726
|
-
released = true;
|
|
384727
|
-
controller.abort();
|
|
384728
|
-
try {
|
|
384729
|
-
reference.release();
|
|
384730
|
-
} catch (error) {
|
|
384731
|
-
cleanupFailed = true;
|
|
384732
|
-
throw error;
|
|
384733
|
-
}
|
|
384734
|
-
}
|
|
384735
|
-
let remove;
|
|
384736
|
-
let started = false;
|
|
384737
|
-
let result = {
|
|
384738
|
-
status: "unavailable",
|
|
384739
|
-
legacyOk: false
|
|
384740
|
-
};
|
|
384741
|
-
try {
|
|
384742
|
-
remove = ctx.effect?.(() => release, "vscode-mode: task session reference");
|
|
384743
|
-
const binding = await reference.ready;
|
|
384744
|
-
if (controller.signal.aborted) result = {
|
|
384745
|
-
status: "cancelled",
|
|
384746
|
-
legacyOk: false
|
|
384747
|
-
};
|
|
384748
|
-
else {
|
|
384749
|
-
started = true;
|
|
384750
|
-
result = taskReceipt(await binding.session.prompt([{
|
|
384751
|
-
type: "text",
|
|
384752
|
-
text
|
|
384753
|
-
}], "queue", controller.signal));
|
|
384754
|
-
}
|
|
384755
|
-
} catch {
|
|
384756
|
-
result = {
|
|
384757
|
-
status: started ? "unknown" : "unavailable",
|
|
384758
|
-
legacyOk: false
|
|
384759
|
-
};
|
|
384760
|
-
}
|
|
384761
|
-
const cancelled = controller.signal.aborted;
|
|
384762
|
-
try {
|
|
384763
|
-
release();
|
|
384764
|
-
} catch {
|
|
384765
|
-
cleanupFailed = true;
|
|
384766
|
-
}
|
|
384767
|
-
try {
|
|
384768
|
-
remove?.();
|
|
384769
|
-
} catch {
|
|
384770
|
-
cleanupFailed = true;
|
|
384771
|
-
}
|
|
384772
|
-
if (cleanupFailed) result = {
|
|
384773
|
-
status: started ? "unknown" : "unavailable",
|
|
384774
|
-
legacyOk: false
|
|
384775
|
-
};
|
|
384776
|
-
if (cancelled) result = {
|
|
384777
|
-
status: "cancelled",
|
|
384778
|
-
legacyOk: result.legacyOk
|
|
384779
|
-
};
|
|
384780
|
-
return result;
|
|
384781
|
-
}
|
|
384782
|
-
/** @private @author ddj 2026年10月29号 @param ctx Service owner. @param id Fixed target. @param text Task body. @returns One legacy-channel attempt; no cross-channel retry. */
|
|
384783
|
-
async function sendLegacy(ctx, id, text) {
|
|
384784
|
-
const input = inputFor(ctx, id);
|
|
384785
|
-
if (typeof input?.setDraft === "function" && typeof input.submit === "function") {
|
|
384786
|
-
let submitted = false;
|
|
384787
|
-
try {
|
|
384788
|
-
input.setDraft(text);
|
|
384789
|
-
submitted = true;
|
|
384790
|
-
input.submit();
|
|
384791
|
-
return {
|
|
384792
|
-
status: "submitted",
|
|
384793
|
-
legacyOk: true
|
|
384794
|
-
};
|
|
384795
|
-
} catch {
|
|
384796
|
-
return {
|
|
384797
|
-
status: submitted ? "unknown" : "unavailable",
|
|
384798
|
-
legacyOk: false
|
|
384799
|
-
};
|
|
384800
|
-
}
|
|
384801
|
-
}
|
|
384802
|
-
const remote = ctx.get("remote.session");
|
|
384803
|
-
if (typeof remote?.prompt !== "function") return httpTaskResult(id, text);
|
|
384804
|
-
try {
|
|
384805
|
-
const payload = {
|
|
384806
|
-
sessionId: id,
|
|
384807
|
-
mode: "queue",
|
|
384808
|
-
content: [{
|
|
384809
|
-
type: "text",
|
|
384810
|
-
text
|
|
384811
|
-
}]
|
|
384812
|
-
};
|
|
384813
|
-
const receipt = await remote.prompt(slotArg(payload));
|
|
384814
|
-
return taskReceipt(receipt?.result ?? receipt);
|
|
384815
|
-
} catch {
|
|
384816
|
-
return {
|
|
384817
|
-
status: "unknown",
|
|
384818
|
-
legacyOk: false
|
|
384819
|
-
};
|
|
384820
|
-
}
|
|
384821
|
-
}
|
|
384822
|
-
/** apiproxy wire 端点(POST /api/<method>;浏览器同源 fetch 自带 cookie 鉴权;实证 401=未鉴权而非 404)。 */
|
|
384823
|
-
const SESSION_CREATE_PATH = "/api/session/create";
|
|
384824
|
-
/** apiproxy wire 端点:发送任务(session/prompt;点击即发送方案)。 */
|
|
384825
|
-
const SESSION_PROMPT_PATH = "/api/session/prompt";
|
|
384826
|
-
/** mint RPC 请求 id(RpcId 品牌类型运行时即普通字符串)。 */
|
|
384827
|
-
function mintRpcId() {
|
|
384828
|
-
try {
|
|
384829
|
-
return globalThis.crypto?.randomUUID?.() ?? "rpc-" + Date.now();
|
|
384830
|
-
} catch {
|
|
384831
|
-
return "rpc-" + Date.now();
|
|
384832
|
-
}
|
|
384833
|
-
}
|
|
384834
|
-
/**
|
|
384835
|
-
* wire 槽实参组装(gateway `invoke` 铁证:`request.args[parameter.wire]` 具名槽,
|
|
384836
|
-
* `session/prompt` 的槽名 = "request",值 = RpcRequest `{rpcId, payload}`)。
|
|
384837
|
-
* @author ddj 2026年09月24号
|
|
384838
|
-
* @param payload 业务载荷
|
|
384839
|
-
* @returns 具名槽实参 `{ args: { request: RpcRequest } }`
|
|
384840
|
-
*/
|
|
384841
|
-
function slotArg(payload) {
|
|
384842
|
-
return { args: { request: {
|
|
384843
|
-
rpcId: mintRpcId(),
|
|
384844
|
-
payload
|
|
384845
|
-
} } };
|
|
384846
|
-
}
|
|
384847
|
-
/**
|
|
384848
|
-
* RPC 返回解析(照官方 dsh-client-runtime 调用形态:`const { result } = await api.sessions.create(...)`)。
|
|
384849
|
-
* 官方 api.sessions 面返回**信封** `{ rpcId, result: RpcResult }`——须解 .result;
|
|
384850
|
-
* 兼容裸 RpcResult 形态(个别补丁面直接回 {ok, value})。
|
|
384851
|
-
* @author ddj 2026年09月23号
|
|
384852
|
-
* @param res 信封或裸 RpcResult
|
|
384853
|
-
* @returns 归一后的 { ok, value }
|
|
384854
|
-
*/
|
|
384855
|
-
function rpcOk(res) {
|
|
384856
|
-
const r = res?.result ?? res;
|
|
384857
|
-
return {
|
|
384858
|
-
ok: r?.ok === true,
|
|
384859
|
-
value: r?.value
|
|
384860
|
-
};
|
|
384861
|
-
}
|
|
384862
|
-
/**
|
|
384863
|
-
* wire 直调 session/create(主路径):ClientRequest 信封 → ServerResponse.result(RpcResult 形态)。
|
|
384864
|
-
* 契约以当前安装版 dsh-host-apiproxy 为准(api/sessions.d.ts「Method signatures are the
|
|
384865
|
-
* source of truth」):`create({cwd}) → {sessionId}`;信封 = `{type:'client-request', rpcId,
|
|
384866
|
-
* method, payload}` → `{result: {ok:true, value:{sessionId}} | {ok:false, error}}`。
|
|
384867
|
-
* @author ddj 2026年09月23号
|
|
384868
|
-
* @param cwd 新会话工作区目录
|
|
384869
|
-
* @returns 新会话 id(失败空串)
|
|
384870
|
-
*/
|
|
384871
|
-
async function apiSessionCreate(cwd) {
|
|
384872
|
-
const json = await (await fetch(appUrl(SESSION_CREATE_PATH), {
|
|
384873
|
-
method: "POST",
|
|
384874
|
-
headers: { "content-type": "application/json" },
|
|
384875
|
-
body: JSON.stringify({
|
|
384876
|
-
type: "client-request",
|
|
384877
|
-
rpcId: mintRpcId(),
|
|
384878
|
-
method: "session/create",
|
|
384879
|
-
payload: cwd ? { cwd } : {}
|
|
384880
|
-
})
|
|
384881
|
-
})).json().catch(() => null);
|
|
384882
|
-
const id = json?.result?.value?.sessionId;
|
|
384883
|
-
return json?.result?.ok === true && typeof id === "string" ? id : "";
|
|
384884
|
-
}
|
|
384885
|
-
/** @private @author ddj 2026年10月29号 @param sessionId Fixed target. @param text Task body. @returns HTTP receipt evidence; errors after request entry are never safe rejection. */
|
|
384886
|
-
async function httpTaskResult(sessionId, text) {
|
|
384887
|
-
try {
|
|
384888
|
-
const payload = {
|
|
384889
|
-
sessionId,
|
|
384890
|
-
mode: "queue",
|
|
384891
|
-
content: [{
|
|
384892
|
-
type: "text",
|
|
384893
|
-
text
|
|
384894
|
-
}]
|
|
384895
|
-
};
|
|
384896
|
-
return taskReceipt((await (await fetch(appUrl(SESSION_PROMPT_PATH), {
|
|
384897
|
-
method: "POST",
|
|
384898
|
-
headers: { "content-type": "application/json" },
|
|
384899
|
-
body: JSON.stringify({
|
|
384900
|
-
type: "client-request",
|
|
384901
|
-
rpcId: mintRpcId(),
|
|
384902
|
-
method: "session/prompt",
|
|
384903
|
-
payload: slotArg(payload)
|
|
384904
|
-
})
|
|
384905
|
-
})).json().catch(() => null))?.result);
|
|
384906
|
-
} catch {
|
|
384907
|
-
return {
|
|
384908
|
-
status: "unknown",
|
|
384909
|
-
legacyOk: false
|
|
384910
|
-
};
|
|
384911
|
-
}
|
|
384912
|
-
}
|
|
384913
|
-
/**
|
|
384914
|
-
* 新建独立会话承载旁路任务(02-deep-session-prompt:深度分析不污染当前对话)。
|
|
384915
|
-
* 优先公开 sessions.create;旧 Host 按能力选择 remote 或 wire,调用后不换通道重试。
|
|
384916
|
-
* 受理状态不明时返回 ok:false,不重复创建会话。
|
|
384917
|
-
* @author ddj 2026年10月28号
|
|
384918
|
-
* @author ddj 2026年09月23号
|
|
384919
|
-
* @param ctx 客户端服务上下文(remote.session + sessions,仅回落用)
|
|
384920
|
-
* @param cwd 新会话工作区目录(与当前会话同工作区)
|
|
384921
|
-
* @returns 新会话结果(含 id)
|
|
384922
|
-
*/
|
|
384923
|
-
async function newDraftSession(ctx, cwd) {
|
|
384924
|
-
try {
|
|
384925
|
-
const sessions = ctx.get("sessions");
|
|
384926
|
-
if (typeof sessions?.create === "function") {
|
|
384927
|
-
const created = await sessions.create(cwd ? { cwd } : void 0);
|
|
384928
|
-
const id = typeof created === "string" && created ? created : typeof created?.id === "string" ? created.id : typeof created?.sessionId === "string" ? created.sessionId : typeof created?.session?.id === "string" ? created.session.id : "";
|
|
384929
|
-
if (id) return {
|
|
384930
|
-
ok: true,
|
|
384931
|
-
id
|
|
384932
|
-
};
|
|
384933
|
-
}
|
|
384934
|
-
if (typeof sessions?.create === "function") return { ok: false };
|
|
384935
|
-
} catch {
|
|
384936
|
-
return { ok: false };
|
|
384937
|
-
}
|
|
384938
|
-
try {
|
|
384939
|
-
const remote = ctx.get("remote.session");
|
|
384940
|
-
if (typeof remote?.create === "function") {
|
|
384941
|
-
const payload = cwd ? { cwd } : {};
|
|
384942
|
-
const receipt = rpcOk(await remote.create(slotArg(payload)));
|
|
384943
|
-
const created = receipt.value;
|
|
384944
|
-
if (receipt.ok && typeof created?.sessionId === "string") return {
|
|
384945
|
-
ok: true,
|
|
384946
|
-
id: created.sessionId
|
|
384947
|
-
};
|
|
384948
|
-
return { ok: false };
|
|
384949
|
-
}
|
|
384950
|
-
} catch {
|
|
384951
|
-
return { ok: false };
|
|
384952
|
-
}
|
|
384953
|
-
try {
|
|
384954
|
-
const id = await apiSessionCreate(cwd);
|
|
384955
|
-
if (id) return {
|
|
384956
|
-
ok: true,
|
|
384957
|
-
id
|
|
384958
|
-
};
|
|
384959
|
-
} catch {}
|
|
384960
|
-
return { ok: false };
|
|
384961
|
-
}
|
|
384962
|
-
/**
|
|
384963
|
-
* 解析会话输入门面;缺失时返回 undefined(动作侧守卫降级)。
|
|
384964
|
-
* @author ddj 2026年08月25号
|
|
384965
|
-
* @param ctx 客户端服务上下文
|
|
384966
|
-
* @param sessionId 会话 id
|
|
384967
|
-
* @returns SessionInput 或 undefined
|
|
384968
|
-
*/
|
|
384969
|
-
function inputFor(ctx, sessionId) {
|
|
384970
|
-
if (!sessionId) return void 0;
|
|
384971
|
-
try {
|
|
384972
|
-
const sessions = ctx.get("sessions");
|
|
384973
|
-
const conversation = ctx.get("conversation");
|
|
384974
|
-
const actx = typeof sessions?.binding === "function" ? sessions.binding(sessionId)?.ctx : sessions?.scope?.(sessionId);
|
|
384975
|
-
const shell = actx && conversation?.input?.for?.(actx);
|
|
384976
|
-
return shell && typeof shell.insertReference === "function" ? shell : void 0;
|
|
384977
|
-
} catch {
|
|
384978
|
-
return;
|
|
384979
|
-
}
|
|
384980
|
-
}
|
|
384981
|
-
/** 取当前草稿快照(长度/版本/chip 列表);无输入门面时返回 null。
|
|
384982
|
-
* @author ddj 2026年08月25号 / 2026年09月11号 */
|
|
384983
|
-
function draftCursor(input) {
|
|
384984
|
-
try {
|
|
384985
|
-
const s = input?.state?.getSnapshot?.();
|
|
384986
|
-
if (!s) return null;
|
|
384987
|
-
return {
|
|
384988
|
-
draft: String(s.draft ?? ""),
|
|
384989
|
-
draftRev: Number(s.draftRev ?? 0),
|
|
384990
|
-
occurrences: Array.isArray(s.occurrences) ? s.occurrences : void 0
|
|
384991
|
-
};
|
|
384992
|
-
} catch {
|
|
384993
|
-
return null;
|
|
384994
|
-
}
|
|
384995
|
-
}
|
|
384996
|
-
/** 从 sessions 列表快照读会话工作区目录(与 index.ts 既有读取方式一致)。
|
|
384997
|
-
* @author ddj 2026年08月25号 */
|
|
384998
|
-
function cwdOf(ctx, sessionId) {
|
|
384999
|
-
if (!sessionId) return void 0;
|
|
385000
|
-
try {
|
|
385001
|
-
return ctx.get("sessions")?.list?.getSnapshot?.()?.byId?.[sessionId]?.cwd;
|
|
385002
|
-
} catch {
|
|
385003
|
-
return;
|
|
385004
|
-
}
|
|
385005
|
-
}
|
|
385006
|
-
/** 输入门面插入引用(拦截异常视作未应用,走降级)。
|
|
385007
|
-
* @author ddj 2026年08月25号 */
|
|
385008
|
-
function safeInsert(input, reference, span) {
|
|
385009
|
-
try {
|
|
385010
|
-
return input.insertReference(reference, span) === true;
|
|
385011
|
-
} catch {
|
|
385012
|
-
return false;
|
|
385013
|
-
}
|
|
385014
|
-
}
|
|
385015
|
-
/** 输入门面插入纯文本(官方 plain-text 通道;不存在或异常视作未应用)。
|
|
385016
|
-
* @author ddj 2026年09月11号 */
|
|
385017
|
-
function safeInsertText(input, text, span) {
|
|
385018
|
-
if (typeof input.insertText !== "function") return false;
|
|
385019
|
-
try {
|
|
385020
|
-
return input.insertText(text, span) === true;
|
|
385021
|
-
} catch {
|
|
385022
|
-
return false;
|
|
385023
|
-
}
|
|
385024
|
-
}
|
|
385025
|
-
/**
|
|
385026
|
-
* 读输入框当前光标(detect 坐标);门面缺失/异常返回 null(由调用方回落到文档末尾)。
|
|
385027
|
-
* @author ddj 2026年09月11号
|
|
385028
|
-
* @param input 输入门面
|
|
385029
|
-
* @returns caretSpan 结果或 null
|
|
385030
|
-
*/
|
|
385031
|
-
function caretOf(input) {
|
|
385032
|
-
if (typeof input.caretSpan !== "function") return null;
|
|
385033
|
-
try {
|
|
385034
|
-
return input.caretSpan() ?? null;
|
|
385035
|
-
} catch {
|
|
385036
|
-
return null;
|
|
385037
|
-
}
|
|
385038
|
-
}
|
|
385039
|
-
/**
|
|
385040
|
-
* 创建「添加到对话」动作集(apply 阶段构建一次,随 props 传给 EditorView)。
|
|
385041
|
-
*
|
|
385042
|
-
* 插入位置 = 输入框**当前光标**(`caretSpan()`,detect 坐标;无光标提示时回落文档末尾),
|
|
385043
|
-
* 不再固定追加到最末尾。坐标一律用 **detect 投影**(见 detectEndOf 的缺陷说明);
|
|
385044
|
-
* 两条写入通道都失败时**不写任何内容**并返回 'failed' —— 旧实现在此处退回 `setDraft`
|
|
385045
|
-
* 整篇重建,会把既有 chip 全部销毁(连续添加多个引用即触发)。
|
|
385046
|
-
* @author ddj 2026年08月25号 / 2026年09月11号
|
|
385047
|
-
* @param ctx 客户端服务上下文(sessions + conversation)
|
|
385048
|
-
* @returns 动作集
|
|
385049
|
-
*/
|
|
385050
|
-
function createAddToConversation(ctx) {
|
|
385051
|
-
const appendReference = async (sessionId, path, range, appearance) => {
|
|
385052
|
-
const input = inputFor(ctx, sessionId);
|
|
385053
|
-
if (!input) return "unavailable";
|
|
385054
|
-
const cur = draftCursor(input);
|
|
385055
|
-
if (!cur) return "unavailable";
|
|
385056
|
-
const { reference, mention } = buildFileRef(path, cwdOf(ctx, sessionId), range, appearance);
|
|
385057
|
-
const at = insertOffsetOf(caretOf(input), detectEndOf(cur));
|
|
385058
|
-
const span = {
|
|
385059
|
-
start: at,
|
|
385060
|
-
end: at,
|
|
385061
|
-
draftRev: cur.draftRev
|
|
385062
|
-
};
|
|
385063
|
-
if (safeInsert(input, reference, span)) return "ok";
|
|
385064
|
-
const pad = paddingAt(cur, at);
|
|
385065
|
-
if (safeInsertText(input, (pad.lead ? " " : "") + mention + (pad.tail ? " " : ""), span)) return "busy";
|
|
385066
|
-
return "failed";
|
|
385067
|
-
};
|
|
385068
|
-
/**
|
|
385069
|
-
* 追加纯文本(AI 深度分析任务注入):走官方 plain-text 通道插到当前光标。
|
|
385070
|
-
* 失败语义与 appendReference 一致:宁可不写也不整篇重建(保护既有 chip)。
|
|
385071
|
-
* @author ddj 2026年09月23号
|
|
385072
|
-
* @param sessionId 会话 id
|
|
385073
|
-
* @param text 待注入文本
|
|
385074
|
-
* @returns 注入结果(ok / unavailable / failed)
|
|
385075
|
-
*/
|
|
385076
|
-
const appendText = async (sessionId, text) => {
|
|
385077
|
-
const input = inputFor(ctx, sessionId);
|
|
385078
|
-
if (!input) return "unavailable";
|
|
385079
|
-
const cur = draftCursor(input);
|
|
385080
|
-
if (!cur) return "unavailable";
|
|
385081
|
-
const at = insertOffsetOf(caretOf(input), detectEndOf(cur));
|
|
385082
|
-
return safeInsertText(input, text, {
|
|
385083
|
-
start: at,
|
|
385084
|
-
end: at,
|
|
385085
|
-
draftRev: cur.draftRev
|
|
385086
|
-
}) ? "ok" : "failed";
|
|
385087
|
-
};
|
|
385088
|
-
/**
|
|
385089
|
-
* 新建独立会话(02-deep-session-prompt:旁路任务不污染当前对话)。
|
|
385090
|
-
* 闭包持有真服务 ctx(面板 SidebarCtx 无 .get,会话操作必须经此门面);
|
|
385091
|
-
* 新版仅返回已登记身份,视图选择由宿主负责;旧版保留可用的 open/select。
|
|
385092
|
-
* @author ddj 2026年10月28号
|
|
385093
|
-
* @author ddj 2026年09月23号
|
|
385094
|
-
* @param cwd 新会话工作区目录(与当前会话同工作区)
|
|
385095
|
-
* @returns 新会话结果(失败供降级)
|
|
385096
|
-
*/
|
|
385097
|
-
const startDraftSession = async (cwd) => {
|
|
385098
|
-
const result = await newDraftSession(ctx, cwd);
|
|
385099
|
-
if (typeof ctx.get("sessions")?.retain === "function") return result;
|
|
385100
|
-
if (result.ok && result.id) try {
|
|
385101
|
-
const sessions = ctx.get("sessions");
|
|
385102
|
-
sessions?.materializeScope?.(result.id);
|
|
385103
|
-
const title = "AI 深度分析 · " + (String(cwd ?? "").split(/[\\/]/).pop() || "SVN 变更");
|
|
385104
|
-
sessions?.manager?.rename?.(result.id, title);
|
|
385105
|
-
sessions?.manager?.select?.(result.id);
|
|
385106
|
-
sessions?.open?.(result.id);
|
|
385107
|
-
} catch {}
|
|
385108
|
-
return result;
|
|
385109
|
-
};
|
|
385110
|
-
/**
|
|
385111
|
-
* 发送任务给目标会话 agent(点击即发送方案:任务落地会话即非 blank,列表可见)。
|
|
385112
|
-
* 新版持有目标 Controller 到 prompt 结束;旧版按能力选择唯一通道,不跨通道重发。
|
|
385113
|
-
* @author ddj 2026年10月29号
|
|
385114
|
-
* @param sessionId 目标会话 id
|
|
385115
|
-
* @param text 任务文本
|
|
385116
|
-
* @returns 同一次调用的历史boolean语义,不能作为受理事实
|
|
385117
|
-
*/
|
|
385118
|
-
const sendTask = async (sessionId, text) => (await sendTaskResult(sessionId, text)).legacyOk;
|
|
385119
|
-
/** @private @author ddj 2026年10月29号 @param sessionId Fixed target. @param text Task body. @returns Detailed evidence from exactly one selected transport. */
|
|
385120
|
-
async function sendTaskResult(sessionId, text) {
|
|
385121
|
-
if (!sessionId) return {
|
|
385122
|
-
status: "unavailable",
|
|
385123
|
-
legacyOk: false
|
|
385124
|
-
};
|
|
385125
|
-
try {
|
|
385126
|
-
const sessions = ctx.get("sessions");
|
|
385127
|
-
if (typeof sessions?.retain === "function") return await sendOwned(ctx, sessions, sessionId, text);
|
|
385128
|
-
return await sendLegacy(ctx, sessionId, text);
|
|
385129
|
-
} catch {
|
|
385130
|
-
return {
|
|
385131
|
-
status: "unavailable",
|
|
385132
|
-
legacyOk: false
|
|
385133
|
-
};
|
|
385134
|
-
}
|
|
385135
|
-
}
|
|
385136
|
-
return {
|
|
385137
|
-
appendReference,
|
|
385138
|
-
appendText,
|
|
385139
|
-
startDraftSession,
|
|
385140
|
-
sendTask,
|
|
385141
|
-
sendTaskResult
|
|
385142
|
-
};
|
|
385143
|
-
}
|
|
385144
385816
|
/**
|
|
385145
385817
|
* 由选区文本推导搜索种子。
|
|
385146
385818
|
* @author ddj 2026年09月18号
|
|
@@ -394041,10 +394713,11 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
394041
394713
|
const layoutRef = react$1.default.useRef(layout);
|
|
394042
394714
|
layoutRef.current = layout;
|
|
394043
394715
|
if (!diffRendererRef.current) diffRendererRef.current = createDiffRenderer((sid, t) => dbg(sid, t));
|
|
394716
|
+
const coveredPaths = react$1.default.useMemo(() => coveredPathKeys(review.page), [review.page]);
|
|
394044
394717
|
const currentRecords = react$1.default.useMemo(() => {
|
|
394045
394718
|
const list = [];
|
|
394046
394719
|
for (const rec of Object.values(records)) {
|
|
394047
|
-
if (rec.archived === true ||
|
|
394720
|
+
if (rec.archived === true || coveredPaths.has(diffPathKey(rec.path))) continue;
|
|
394048
394721
|
if (sameFile(rec.path, active)) list.push(rec);
|
|
394049
394722
|
}
|
|
394050
394723
|
return list;
|
|
@@ -394052,7 +394725,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
394052
394725
|
records,
|
|
394053
394726
|
active,
|
|
394054
394727
|
cwd,
|
|
394055
|
-
|
|
394728
|
+
coveredPaths
|
|
394056
394729
|
]);
|
|
394057
394730
|
const reviewOpsRef = react$1.default.useRef(/* @__PURE__ */ new Map());
|
|
394058
394731
|
const settledRef = react$1.default.useRef(/* @__PURE__ */ new Set());
|
|
@@ -397043,11 +397716,13 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
397043
397716
|
dbg(sessionId, `[DEBUG diffAction] stage=reply trace=${traceId} method=decideBatch items=${items.length} rpcMs=${(performance.now() - start).toFixed(1)} ok=${res?.ok === true}`);
|
|
397044
397717
|
} catch (e) {}
|
|
397045
397718
|
if (!res || !res.ok || !Array.isArray(res.results)) {
|
|
397046
|
-
|
|
397719
|
+
const failure = res?.error ? String(res.error) : "批量处理失败";
|
|
397720
|
+
setError(failure);
|
|
397047
397721
|
return {
|
|
397048
397722
|
ok: 0,
|
|
397049
397723
|
fail: items.length,
|
|
397050
|
-
stale: 0
|
|
397724
|
+
stale: 0,
|
|
397725
|
+
error: failure
|
|
397051
397726
|
};
|
|
397052
397727
|
}
|
|
397053
397728
|
let ok = 0;
|
|
@@ -397066,11 +397741,13 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
397066
397741
|
stale
|
|
397067
397742
|
};
|
|
397068
397743
|
}).catch((e) => {
|
|
397069
|
-
|
|
397744
|
+
const failure = "批量处理异常:" + String(e);
|
|
397745
|
+
setError(failure);
|
|
397070
397746
|
return {
|
|
397071
397747
|
ok: 0,
|
|
397072
397748
|
fail: items.length,
|
|
397073
|
-
stale: 0
|
|
397749
|
+
stale: 0,
|
|
397750
|
+
error: failure
|
|
397074
397751
|
};
|
|
397075
397752
|
});
|
|
397076
397753
|
};
|
|
@@ -397239,6 +397916,27 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
397239
397916
|
if (lifetime?.live) setReviewBatchBusy(false);
|
|
397240
397917
|
}
|
|
397241
397918
|
}
|
|
397919
|
+
/**
|
|
397920
|
+
* 批量失败提示:Host 原文优先,只有拿不到原因时才按计数兜底。
|
|
397921
|
+
* @author ddj 2026年11月3号
|
|
397922
|
+
* @param result actMany 的结果
|
|
397923
|
+
* @returns 提示文案;没有失败返回 null
|
|
397924
|
+
*/
|
|
397925
|
+
const batchError = (result) => {
|
|
397926
|
+
if (!result.error) return batchFailText(result);
|
|
397927
|
+
if (isSessionMissingError(result.error)) return "这个会话没在当前端激活,Keep/Undo 没有执行(记录未改动);在会话里发一条消息激活后重试";
|
|
397928
|
+
return result.error;
|
|
397929
|
+
};
|
|
397930
|
+
/**
|
|
397931
|
+
* 批量结果没有 Host 原文时的计数兜底文案。
|
|
397932
|
+
* @author ddj 2026年11月3号
|
|
397933
|
+
* @param result actMany 的结果
|
|
397934
|
+
* @returns 提示文案;没有失败返回 null
|
|
397935
|
+
*/
|
|
397936
|
+
const batchFailText = (result) => {
|
|
397937
|
+
if (!result.fail) return null;
|
|
397938
|
+
return result.stale ? `${result.fail} 处未处理(其中 ${result.stale} 处已不在文件中,只更新了记录),可刷新后重试` : `${result.fail} 处未处理,可刷新后重试`;
|
|
397939
|
+
};
|
|
397242
397940
|
const acceptFile = () => {
|
|
397243
397941
|
if (missingNow && !officialDetail) return decideMissing(false);
|
|
397244
397942
|
if (officialDetail) return decideOfficial(false);
|
|
@@ -397252,8 +397950,9 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
397252
397950
|
paths: active ? [active] : void 0,
|
|
397253
397951
|
kind: "structure"
|
|
397254
397952
|
});
|
|
397255
|
-
setStatus(decideStatus("已采纳 ", result));
|
|
397256
|
-
|
|
397953
|
+
setStatus(result.ok ? `${SIDECAR_KEEP_DONE}(${result.ok} 处)` : decideStatus("已采纳 ", result));
|
|
397954
|
+
const message = batchError(result);
|
|
397955
|
+
if (message) setError(message);
|
|
397257
397956
|
}).catch(() => {
|
|
397258
397957
|
batchBusyRef.current = false;
|
|
397259
397958
|
});
|
|
@@ -397272,7 +397971,8 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
397272
397971
|
kind: "structure"
|
|
397273
397972
|
});
|
|
397274
397973
|
setStatus(decideStatus("已不采纳 ", result));
|
|
397275
|
-
|
|
397974
|
+
const message = batchError(result);
|
|
397975
|
+
if (message) setError(message);
|
|
397276
397976
|
}).catch(() => {
|
|
397277
397977
|
batchBusyRef.current = false;
|
|
397278
397978
|
});
|
|
@@ -397280,7 +397980,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
397280
397980
|
const allPending = react$1.default.useMemo(() => {
|
|
397281
397981
|
const out = [];
|
|
397282
397982
|
for (const rec of Object.values(records)) {
|
|
397283
|
-
if (rec.superseded === true ||
|
|
397983
|
+
if (rec.superseded === true || coveredPaths.has(diffPathKey(rec.path))) continue;
|
|
397284
397984
|
const perHunk = Array.isArray(rec.decisions?.perHunk) ? rec.decisions.perHunk : [];
|
|
397285
397985
|
const hunks = Array.isArray(rec.hunks) ? rec.hunks : [];
|
|
397286
397986
|
for (let i = 0; i < hunks.length; i++) if ((perHunk.length ? perHunk[i] : rec.decisions.call) === "pending" && !noopHunk(rec, hunks[i])) out.push({
|
|
@@ -397369,7 +398069,8 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
397369
398069
|
kind: "structure"
|
|
397370
398070
|
});
|
|
397371
398071
|
setStatus(decideStatus(reject ? "已不采纳 " : "已采纳 ", result) + `;删除文件 ${handled.size} 个`);
|
|
397372
|
-
|
|
398072
|
+
const message = batchError(result);
|
|
398073
|
+
if (message) setError(message);
|
|
397373
398074
|
} catch (cause) {
|
|
397374
398075
|
if (lifetime?.live) setError(String(cause));
|
|
397375
398076
|
} finally {
|
|
@@ -397414,6 +398115,88 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
397414
398115
|
setStatus(`官方待审列表正在载入,载入完成后请再次点击${action}`);
|
|
397415
398116
|
return false;
|
|
397416
398117
|
}
|
|
398118
|
+
/** @private @author ddj 2026年11月03号 @description 批量通道的一次性完整快照(分页口径的 pending 只是页内计数)。 @returns 完整待审页 */
|
|
398119
|
+
async function batchSnapshot() {
|
|
398120
|
+
const res = await rpc("edrv.review.list", {
|
|
398121
|
+
sessionId,
|
|
398122
|
+
all: true
|
|
398123
|
+
});
|
|
398124
|
+
if (!res.ok) throw new Error(res.error);
|
|
398125
|
+
return res.page;
|
|
398126
|
+
}
|
|
398127
|
+
/** @private @author ddj 2026年11月03号 @param token 起批时的会话寿命 @returns 逐条 Undo 的精确部分完成(冲突处停止) */
|
|
398128
|
+
function undoAllBatch(token) {
|
|
398129
|
+
return runReviewBatch({
|
|
398130
|
+
snapshot: batchSnapshot,
|
|
398131
|
+
detail: async (id) => {
|
|
398132
|
+
const res = await rpc("edrv.review.detail", {
|
|
398133
|
+
sessionId,
|
|
398134
|
+
id
|
|
398135
|
+
});
|
|
398136
|
+
if (!res.ok) throw new Error(res.error);
|
|
398137
|
+
return res.detail;
|
|
398138
|
+
},
|
|
398139
|
+
decide: (detail, undo, operationId) => rpc("edrv.review.decide", {
|
|
398140
|
+
sessionId,
|
|
398141
|
+
id: detail.item.id,
|
|
398142
|
+
seq: detail.item.seq,
|
|
398143
|
+
revision: detail.item.revision,
|
|
398144
|
+
operationId,
|
|
398145
|
+
scope: "file",
|
|
398146
|
+
decision: undo ? "rejected" : "accepted"
|
|
398147
|
+
}),
|
|
398148
|
+
guard: () => guardReviewAll(true, token),
|
|
398149
|
+
operation: batchOperation
|
|
398150
|
+
}, true);
|
|
398151
|
+
}
|
|
398152
|
+
/** @private @author ddj 2026年11月03号 @param token 起批时的会话寿命 @returns 一次 RPC 的批量 Keep 结果(跳过项不算失败) */
|
|
398153
|
+
function keepAllBatch(token) {
|
|
398154
|
+
return runKeepAll({
|
|
398155
|
+
snapshot: batchSnapshot,
|
|
398156
|
+
keepAll: (items, batchId) => rpc("edrv.review.keepAll", {
|
|
398157
|
+
sessionId,
|
|
398158
|
+
batchId,
|
|
398159
|
+
items
|
|
398160
|
+
}),
|
|
398161
|
+
guard: () => guardReviewAll(false, token)
|
|
398162
|
+
});
|
|
398163
|
+
}
|
|
398164
|
+
/**
|
|
398165
|
+
* @private
|
|
398166
|
+
* @author ddj 2026年11月03号
|
|
398167
|
+
* @description: 批量结果落界面:刷新列表、状态栏计数;Keep 的跳过只做提示不当错误,
|
|
398168
|
+
* Undo 保持「遇冲突停止」口径。
|
|
398169
|
+
* @param result 批量结果
|
|
398170
|
+
* @param reject 本次是 Undo 还是 Keep
|
|
398171
|
+
* @param undoPath 起批时的活动文件
|
|
398172
|
+
* @param undoModel 起批时的编辑器模型
|
|
398173
|
+
* @param undoVersion 起批时的模型版本
|
|
398174
|
+
* @returns 官方段是否完全成功(legacy 通道是否继续)
|
|
398175
|
+
*/
|
|
398176
|
+
function finishReviewAll(result, reject, undoPath, undoModel, undoVersion) {
|
|
398177
|
+
refreshReview(sessionId);
|
|
398178
|
+
if (result.paths.length) emitRefresh({
|
|
398179
|
+
scope,
|
|
398180
|
+
paths: result.paths,
|
|
398181
|
+
kind: "structure"
|
|
398182
|
+
});
|
|
398183
|
+
if (reject) {
|
|
398184
|
+
if (result.paths.some((path) => sameFile(path, undoPath)) && activeRef.current === undoPath && !dirtyRef.current[undoPath] && !savingRef.current.has(undoPath) && undoModel?.getVersionId?.() === undoVersion) reloadFile(true);
|
|
398185
|
+
setStatus(`Undo All:官方记录已完成 ${result.completed} / ${result.total}`);
|
|
398186
|
+
if (result.error) setError(`批量已停止,已完成 ${result.completed} 项,其余未处理。${result.error}`);
|
|
398187
|
+
return !result.error;
|
|
398188
|
+
}
|
|
398189
|
+
const skipped = result.skipped ?? [];
|
|
398190
|
+
setStatus(`Keep All:已保留 ${result.completed} / ${result.total}` + (skipped.length ? `,跳过 ${skipped.length} 条` : ""));
|
|
398191
|
+
if (result.error) setError(`Keep All 未全部完成:${result.error}`);
|
|
398192
|
+
if (skipped.length) toastDom(skippedTip(skipped));
|
|
398193
|
+
return !result.error;
|
|
398194
|
+
}
|
|
398195
|
+
/** @private @author ddj 2026年11月03号 @param skipped 被跳过的条目 @returns 最多 3 条、余量汇总的提示文案 */
|
|
398196
|
+
function skippedTip(skipped) {
|
|
398197
|
+
const head = skipped.slice(0, 3).map((item) => `${item.display}:${item.reason}`).join(";");
|
|
398198
|
+
return skipped.length > 3 ? `${head};等 ${skipped.length} 条` : head;
|
|
398199
|
+
}
|
|
397417
398200
|
/** @private @author ddj 2026年10月27号 @param reject Undo or Keep all workspace pending records, including previous rounds. @returns Explicit partial results; legacy proceeds only when official stage fully succeeds. */
|
|
397418
398201
|
async function actReviewAll(reject) {
|
|
397419
398202
|
if (batchBusyRef.current) return;
|
|
@@ -397427,48 +398210,9 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
397427
398210
|
setReviewBatchBusy(true);
|
|
397428
398211
|
setStatus(reject ? "正在检查并 Undo 全部待审记录…" : "正在 Keep 全部待审记录…");
|
|
397429
398212
|
try {
|
|
397430
|
-
const result = await
|
|
397431
|
-
snapshot: async () => {
|
|
397432
|
-
const res = await rpc("edrv.review.list", {
|
|
397433
|
-
sessionId,
|
|
397434
|
-
all: true
|
|
397435
|
-
});
|
|
397436
|
-
if (!res.ok) throw new Error(res.error);
|
|
397437
|
-
return res.page;
|
|
397438
|
-
},
|
|
397439
|
-
detail: async (id) => {
|
|
397440
|
-
const res = await rpc("edrv.review.detail", {
|
|
397441
|
-
sessionId,
|
|
397442
|
-
id
|
|
397443
|
-
});
|
|
397444
|
-
if (!res.ok) throw new Error(res.error);
|
|
397445
|
-
return res.detail;
|
|
397446
|
-
},
|
|
397447
|
-
decide: (detail, undo, operationId) => rpc("edrv.review.decide", {
|
|
397448
|
-
sessionId,
|
|
397449
|
-
id: detail.item.id,
|
|
397450
|
-
seq: detail.item.seq,
|
|
397451
|
-
revision: detail.item.revision,
|
|
397452
|
-
operationId,
|
|
397453
|
-
scope: "file",
|
|
397454
|
-
decision: undo ? "rejected" : "accepted"
|
|
397455
|
-
}),
|
|
397456
|
-
guard: () => guardReviewAll(reject, token),
|
|
397457
|
-
operation: batchOperation
|
|
397458
|
-
}, reject);
|
|
398213
|
+
const result = reject ? await undoAllBatch(token) : await keepAllBatch(token);
|
|
397459
398214
|
if (!token?.live) return;
|
|
397460
|
-
|
|
397461
|
-
if (reject && result.paths.some((path) => sameFile(path, undoPath)) && activeRef.current === undoPath && !dirtyRef.current[undoPath] && !savingRef.current.has(undoPath) && undoModel?.getVersionId?.() === undoVersion) reloadFile(true);
|
|
397462
|
-
if (result.paths.length) emitRefresh({
|
|
397463
|
-
scope,
|
|
397464
|
-
paths: result.paths,
|
|
397465
|
-
kind: "structure"
|
|
397466
|
-
});
|
|
397467
|
-
setStatus(`${reject ? "Undo All" : "Keep All"}:官方记录已完成 ${result.completed} / ${result.total}`);
|
|
397468
|
-
if (result.error) {
|
|
397469
|
-
setError(`批量已停止,已完成 ${result.completed} 项,其余未处理。${result.error}`);
|
|
397470
|
-
return;
|
|
397471
|
-
}
|
|
398215
|
+
if (!finishReviewAll(result, reject, undoPath, undoModel, undoVersion)) return;
|
|
397472
398216
|
guardReviewAll(reject, token);
|
|
397473
398217
|
if (allPending.length) {
|
|
397474
398218
|
batchBusyRef.current = false;
|
|
@@ -403456,577 +404200,6 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
403456
404200
|
}
|
|
403457
404201
|
}
|
|
403458
404202
|
//#endregion
|
|
403459
|
-
//#region src/shared/externalOpen.ts
|
|
403460
|
-
/**
|
|
403461
|
-
* dsh-vscode-mode shared — 外部深链 URL 契约(Windows launcher / Unity 包 / client 共用)。
|
|
403462
|
-
* 形态:?<FLAG>=1&<PATHS_KEY>=<enc1>[,<enc2>…] [&<LINE_KEY>=N] [&<COLUMN_KEY>=M]
|
|
403463
|
-
* 每个路径独立 percent-encode(encodeURIComponent / Uri.EscapeDataString)后以 `,` 连接:
|
|
403464
|
-
* 编码结果不含裸逗号(Windows 路径本身不允许逗号,编码兜底其余字符),URLSearchParams
|
|
403465
|
-
* 取值做一次标准解码即得 `enc1,enc2`,按 `,` split 即路径,无需二次 decode。
|
|
403466
|
-
* Purity rule: no node/react imports(URLSearchParams 为标准全局)。
|
|
403467
|
-
* 作者 ddj 2026-09-07
|
|
403468
|
-
*/
|
|
403469
|
-
/** 深链开关参数名(值恒为 '1')。 */
|
|
403470
|
-
const EDRV_OPEN_FLAG = "edrvOpen";
|
|
403471
|
-
/** 路径列表参数名(逗号分隔的已编码路径)。 */
|
|
403472
|
-
const EDRV_PATHS_KEY = "edrvPaths";
|
|
403473
|
-
/** 目标行参数名(1-based,可选,作用于首个路径)。 */
|
|
403474
|
-
const EDRV_LINE_KEY = "edrvLine";
|
|
403475
|
-
/** 目标列参数名(1-based,可选,作用于首个路径)。 */
|
|
403476
|
-
const EDRV_COLUMN_KEY = "edrvColumn";
|
|
403477
|
-
/** 深链参数键全集(URL 清理用)。 */
|
|
403478
|
-
const EDRV_PARAM_KEYS = [
|
|
403479
|
-
EDRV_OPEN_FLAG,
|
|
403480
|
-
EDRV_PATHS_KEY,
|
|
403481
|
-
EDRV_LINE_KEY,
|
|
403482
|
-
EDRV_COLUMN_KEY
|
|
403483
|
-
];
|
|
403484
|
-
/**
|
|
403485
|
-
* 解析深链查询串:非深链/路径为空 → null;行列仅接受正整数,非法忽略。
|
|
403486
|
-
* @author ddj 2026年09月07号
|
|
403487
|
-
* @param search location.search(含或不含前导 ? 均可)
|
|
403488
|
-
* @returns 深链参数或 null
|
|
403489
|
-
*/
|
|
403490
|
-
function parseOpenParams(search) {
|
|
403491
|
-
const params = new URLSearchParams(search ?? "");
|
|
403492
|
-
if (params.get("edrvOpen") !== "1") return null;
|
|
403493
|
-
const paths = (params.get("edrvPaths") ?? "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
403494
|
-
if (!paths.length) return null;
|
|
403495
|
-
return {
|
|
403496
|
-
paths,
|
|
403497
|
-
line: positiveIntOf(params.get(EDRV_LINE_KEY)),
|
|
403498
|
-
column: positiveIntOf(params.get(EDRV_COLUMN_KEY))
|
|
403499
|
-
};
|
|
403500
|
-
}
|
|
403501
|
-
/**
|
|
403502
|
-
* 正整数解析(非法/非正 → undefined)。
|
|
403503
|
-
* @author ddj 2026年09月07号
|
|
403504
|
-
* @param raw 参数原文(可空)
|
|
403505
|
-
* @returns 正整数或 undefined
|
|
403506
|
-
*/
|
|
403507
|
-
function positiveIntOf(raw) {
|
|
403508
|
-
const value = Number(raw);
|
|
403509
|
-
return Number.isInteger(value) && value > 0 ? value : void 0;
|
|
403510
|
-
}
|
|
403511
|
-
//#endregion
|
|
403512
|
-
//#region src/client/chooseDialog.ts
|
|
403513
|
-
/**
|
|
403514
|
-
* dsh-vscode-mode client — 工作区选择弹窗(深链打开规则 1)。
|
|
403515
|
-
* 复用与「添加 MCP」一致的 vsm-mcp-modal / vsm-mcp-dialog 模态(页面居中卡片 + 遮罩 + 主题
|
|
403516
|
-
* token),React portal 挂 body;Esc / 遮罩点击 / 取消 → null(中止流程不改任何状态)。
|
|
403517
|
-
* 作者 ddj 2026-09-07
|
|
403518
|
-
*/
|
|
403519
|
-
/** 选择弹窗(vsm-mcp-modal 遮罩 + vsm-mcp-dialog 卡片,Esc/遮罩点击取消)。 */
|
|
403520
|
-
function ChoiceModal({ title, folder, done }) {
|
|
403521
|
-
react$1.default.useEffect(() => {
|
|
403522
|
-
const onKey = (event) => {
|
|
403523
|
-
if (event.key === "Escape") done(null);
|
|
403524
|
-
};
|
|
403525
|
-
window.addEventListener("keydown", onKey);
|
|
403526
|
-
return () => window.removeEventListener("keydown", onKey);
|
|
403527
|
-
}, []);
|
|
403528
|
-
return react$1.default.createElement("div", {
|
|
403529
|
-
className: "vsm-mcp-modal",
|
|
403530
|
-
onMouseDown: (event) => {
|
|
403531
|
-
if (event.target === event.currentTarget) done(null);
|
|
403532
|
-
}
|
|
403533
|
-
}, react$1.default.createElement("div", { className: "vsm-mcp-dialog" }, react$1.default.createElement("h3", null, "在 DSH 中打开"), react$1.default.createElement("label", null, "文件夹 " + folder + " 位于已注册的工作区「" + title + "」内,选择打开方式:"), react$1.default.createElement("div", { className: "vsm-mcp-dialog-actions" }, react$1.default.createElement("button", { onClick: () => done(null) }, "取消"), react$1.default.createElement("button", { onClick: () => done("create") }, "新建工作区"), react$1.default.createElement("button", {
|
|
403534
|
-
className: "vsm-primary",
|
|
403535
|
-
onClick: () => done("recent")
|
|
403536
|
-
}, "使用最近的工作区"))));
|
|
403537
|
-
}
|
|
403538
|
-
/**
|
|
403539
|
-
* 弹出工作区选择弹窗(深链打开规则 1)。
|
|
403540
|
-
* @author ddj 2026年09月07号
|
|
403541
|
-
* @param title 匹配到的工作区标题
|
|
403542
|
-
* @param folder 文件夹路径
|
|
403543
|
-
* @returns 用户选择;null = 取消/中止
|
|
403544
|
-
*/
|
|
403545
|
-
function chooseWorkspace(title, folder) {
|
|
403546
|
-
return new Promise((resolve) => {
|
|
403547
|
-
const host = document.createElement("div");
|
|
403548
|
-
document.body.appendChild(host);
|
|
403549
|
-
const root = (0, react_dom_client.createRoot)(host);
|
|
403550
|
-
const done = (choice) => {
|
|
403551
|
-
root.unmount();
|
|
403552
|
-
host.remove();
|
|
403553
|
-
resolve(choice);
|
|
403554
|
-
};
|
|
403555
|
-
root.render(react$1.default.createElement(ChoiceModal, {
|
|
403556
|
-
title,
|
|
403557
|
-
folder,
|
|
403558
|
-
done
|
|
403559
|
-
}));
|
|
403560
|
-
});
|
|
403561
|
-
}
|
|
403562
|
-
//#endregion
|
|
403563
|
-
//#region src/client/openFlow.ts
|
|
403564
|
-
/**
|
|
403565
|
-
* dsh-vscode-mode client — 深链打开规则引擎(资源管理器右键 / Unity 外部编辑器落地)。
|
|
403566
|
-
* 规则:文件夹 ⊂ 某已注册工作区 → 页面内弹窗二选一(使用最近的工作区 / 新建工作区);
|
|
403567
|
-
* 文件夹不在任何工作区 → 直达最近对话所在工作区新增;文件 → 最近对话 + 编辑器展开
|
|
403568
|
-
* (无任何会话 → 以文件父目录注册工作区引导创建)。首路径驱动规则,其余路径:文件进
|
|
403569
|
-
* 编辑器、文件夹补引用。纯函数可单测;编排依赖经 OpenDeps 注入(mock 可测)。
|
|
403570
|
-
* 作者 ddj 2026-09-07
|
|
403571
|
-
*/
|
|
403572
|
-
/** 等列表/输入就绪的节奏。 */
|
|
403573
|
-
const READY_TIMEOUT_MS = 15e3;
|
|
403574
|
-
const READY_TICK_MS = 300;
|
|
403575
|
-
const REF_RETRY_MAX = 20;
|
|
403576
|
-
const REF_RETRY_MS = 300;
|
|
403577
|
-
/**
|
|
403578
|
-
* 路径规范化:`\→/`、小写、去尾分隔符(Windows 大小写不敏感前缀匹配用)。
|
|
403579
|
-
* @author ddj 2026年09月07号
|
|
403580
|
-
*/
|
|
403581
|
-
function normPath(p) {
|
|
403582
|
-
return String(p ?? "").replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
403583
|
-
}
|
|
403584
|
-
/**
|
|
403585
|
-
* 目录包含判定:child === root 或 child 位于 root 之下。
|
|
403586
|
-
* @author ddj 2026年09月07号
|
|
403587
|
-
*/
|
|
403588
|
-
function containsPath(root, child) {
|
|
403589
|
-
const r = normPath(root);
|
|
403590
|
-
const c = normPath(child);
|
|
403591
|
-
return c === r || c.startsWith(r + "/");
|
|
403592
|
-
}
|
|
403593
|
-
/**
|
|
403594
|
-
* 父目录(文件引导创建工作区用;根路径回退自身)。
|
|
403595
|
-
* @author ddj 2026年09月07号
|
|
403596
|
-
*/
|
|
403597
|
-
function dirnameOf(p) {
|
|
403598
|
-
const norm = String(p ?? "").replace(/[\\/]+$/, "");
|
|
403599
|
-
const cut = Math.max(norm.lastIndexOf("/"), norm.lastIndexOf("\\"));
|
|
403600
|
-
return cut > 0 ? norm.slice(0, cut) : norm;
|
|
403601
|
-
}
|
|
403602
|
-
/**
|
|
403603
|
-
* 最近一次对话:排除子代理行,updatedAt 最新优先。
|
|
403604
|
-
* @author ddj 2026年09月07号
|
|
403605
|
-
*/
|
|
403606
|
-
function recentSession(byId, ids) {
|
|
403607
|
-
return (ids ?? Object.keys(byId)).map((id) => byId[id]).filter((row) => row && row.origin !== "subagent").sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))[0];
|
|
403608
|
-
}
|
|
403609
|
-
/**
|
|
403610
|
-
* 文件夹命中的已注册工作区(前缀包含),updatedAt 新者优先。
|
|
403611
|
-
* @author ddj 2026年09月07号
|
|
403612
|
-
*/
|
|
403613
|
-
function matchWorkspaces(items, folder) {
|
|
403614
|
-
return items.filter((ws) => containsPath(ws.path, folder));
|
|
403615
|
-
}
|
|
403616
|
-
/**
|
|
403617
|
-
* 规则分派:目录+命中工作区 → choose;目录未命中 → folderNew(以该文件夹为根注册新工作区,
|
|
403618
|
-
* 用户确认:一律取右键的文件夹本身);文件有会话 → fileRecent;文件无会话 → bootstrap;
|
|
403619
|
-
* 路径缺失 → abort。
|
|
403620
|
-
* @author ddj 2026年09月08号
|
|
403621
|
-
*/
|
|
403622
|
-
function pickRule(kind, matched, hasSession) {
|
|
403623
|
-
if (kind === "missing") return "abort";
|
|
403624
|
-
if (kind === "directory") return matched ? "choose" : "folderNew";
|
|
403625
|
-
return hasSession ? "fileRecent" : "bootstrap";
|
|
403626
|
-
}
|
|
403627
|
-
/**
|
|
403628
|
-
* 深链打开编排入口:等列表就绪 → 判型 → 规则分派 → 编排 create/open/引用/编辑器。
|
|
403629
|
-
* @author ddj 2026年09月07号
|
|
403630
|
-
* @param ctx 客户端根上下文(sessions/workspaces 服务)
|
|
403631
|
-
* @param params 深链参数(首路径驱动规则)
|
|
403632
|
-
* @param overrides 依赖覆盖(测试注入)
|
|
403633
|
-
*/
|
|
403634
|
-
async function openDeepLink(ctx, params, overrides = {}) {
|
|
403635
|
-
const deps = realDeps(ctx, overrides);
|
|
403636
|
-
if (!await waitListsReady(deps)) {
|
|
403637
|
-
deps.notify("DSH 会话列表未就绪,请稍后重试");
|
|
403638
|
-
return false;
|
|
403639
|
-
}
|
|
403640
|
-
const primary = params.paths[0];
|
|
403641
|
-
const kind = await deps.stat(primary).catch(() => "missing");
|
|
403642
|
-
const sess = deps.sessions();
|
|
403643
|
-
const rule = pickRule(kind, matchWorkspaces(deps.workspaces().items, primary).length > 0, Boolean(recentSession(sess.byId, sess.ids)));
|
|
403644
|
-
try {
|
|
403645
|
-
if (rule === "abort") {
|
|
403646
|
-
deps.notify("路径不存在:" + primary);
|
|
403647
|
-
return false;
|
|
403648
|
-
}
|
|
403649
|
-
if (rule === "choose") await runChoose(deps, primary, params);
|
|
403650
|
-
else if (rule === "folderNew") await runFolderNew(deps, primary, params);
|
|
403651
|
-
else if (rule === "fileRecent") await runFileRecent(deps, primary, params);
|
|
403652
|
-
else await runFileBootstrap(deps, primary, params);
|
|
403653
|
-
return true;
|
|
403654
|
-
} catch (error) {
|
|
403655
|
-
deps.notify("打开失败:" + String(error?.message ?? error));
|
|
403656
|
-
return false;
|
|
403657
|
-
}
|
|
403658
|
-
}
|
|
403659
|
-
/** 等待会话与工作区列表就绪(上限 15s;超时按当前态判定)。 */
|
|
403660
|
-
async function waitListsReady(deps) {
|
|
403661
|
-
for (let waited = 0; waited < READY_TIMEOUT_MS; waited += READY_TICK_MS) {
|
|
403662
|
-
if (deps.sessions().ready && deps.workspaces().ready) return true;
|
|
403663
|
-
await sleep(deps, READY_TICK_MS);
|
|
403664
|
-
}
|
|
403665
|
-
return deps.sessions().ready && deps.workspaces().ready;
|
|
403666
|
-
}
|
|
403667
|
-
/** 规则 1:命中工作区 → 弹窗分派(取消即中止)。 */
|
|
403668
|
-
async function runChoose(deps, folder, params) {
|
|
403669
|
-
const best = bestMatch(deps, matchWorkspaces(deps.workspaces().items, folder));
|
|
403670
|
-
const choice = await deps.choose(best?.title ?? "工作区", folder);
|
|
403671
|
-
if (choice === null) throw new Error("用户取消打开");
|
|
403672
|
-
if (choice === "create") return runFolderNew(deps, folder, params);
|
|
403673
|
-
const sessionId = await spawnInWorkspace(deps, (best ?? deps.workspaces().items[0]).workspaceId, void 0);
|
|
403674
|
-
await settleRef(deps, sessionId, folder, "folder");
|
|
403675
|
-
await openChecked(deps, sessionId, null);
|
|
403676
|
-
await openRest(deps, sessionId, params, 1);
|
|
403677
|
-
}
|
|
403678
|
-
/** 规则 3a:文件 → 所属工作区优先(有会话打开其最近会话;无会话新建 + 文件引用);无匹配 → 最近对话。 */
|
|
403679
|
-
async function runFileRecent(deps, file, params) {
|
|
403680
|
-
const ws = pickFileWorkspace(deps, file);
|
|
403681
|
-
if (ws) {
|
|
403682
|
-
const anchor = recentSessionIn(deps, ws);
|
|
403683
|
-
if (anchor) {
|
|
403684
|
-
deps.openSession(anchor.id);
|
|
403685
|
-
await openChecked(deps, anchor.id, file, params.line, params.column);
|
|
403686
|
-
await openRest(deps, anchor.id, params, 1);
|
|
403687
|
-
return;
|
|
403688
|
-
}
|
|
403689
|
-
const sessionId = await spawnInWorkspace(deps, ws.workspaceId, void 0);
|
|
403690
|
-
await settleRef(deps, sessionId, file, "file");
|
|
403691
|
-
await openChecked(deps, sessionId, file, params.line, params.column);
|
|
403692
|
-
await openRest(deps, sessionId, params, 1);
|
|
403693
|
-
return;
|
|
403694
|
-
}
|
|
403695
|
-
const anchor = recentSession(deps.sessions().byId, deps.sessions().ids);
|
|
403696
|
-
if (!anchor) return runFileBootstrap(deps, file, params);
|
|
403697
|
-
deps.openSession(anchor.id);
|
|
403698
|
-
await openChecked(deps, anchor.id, file, params.line, params.column);
|
|
403699
|
-
await openRest(deps, anchor.id, params, 1);
|
|
403700
|
-
}
|
|
403701
|
-
/**
|
|
403702
|
-
* 文件所属工作区:前缀命中的工作区里,优先取有会话且最近活跃的;都无会话取路径最具体的。
|
|
403703
|
-
* @author ddj 2026年09月08号
|
|
403704
|
-
*/
|
|
403705
|
-
function pickFileWorkspace(deps, file) {
|
|
403706
|
-
const matches = matchWorkspaces(deps.workspaces().items, file);
|
|
403707
|
-
if (!matches.length) return void 0;
|
|
403708
|
-
const withSessions = matches.filter((ws) => (ws.sessionIds ?? []).some((id) => Boolean(deps.sessions().byId[id])));
|
|
403709
|
-
if (!withSessions.length) return [...matches].sort((a, b) => normPath(b.path).length - normPath(a.path).length)[0];
|
|
403710
|
-
return withSessions.sort((a, b) => latestSessionAt(deps, b) - latestSessionAt(deps, a))[0];
|
|
403711
|
-
}
|
|
403712
|
-
/** 工作区内会话的最近活跃时间(无成员 → 0)。 */
|
|
403713
|
-
function latestSessionAt(deps, ws) {
|
|
403714
|
-
const byId = deps.sessions().byId;
|
|
403715
|
-
return (ws.sessionIds ?? []).reduce((max, id) => Math.max(max, byId[id]?.updatedAt ?? 0), 0);
|
|
403716
|
-
}
|
|
403717
|
-
/** 工作区内最近一次会话(排除子代理与不在列表中的陈旧行)。 */
|
|
403718
|
-
function recentSessionIn(deps, ws) {
|
|
403719
|
-
const byId = deps.sessions().byId;
|
|
403720
|
-
return (ws.sessionIds ?? []).map((id) => byId[id]).filter((row) => row && row.origin !== "subagent").sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))[0];
|
|
403721
|
-
}
|
|
403722
|
-
/** 规则 3b:文件 + 无任何会话 → 父目录注册工作区 + 新对话 + 文件引用 + 编辑器打开。 */
|
|
403723
|
-
async function runFileBootstrap(deps, file, params) {
|
|
403724
|
-
const sessionId = await spawnInWorkspace(deps, (await deps.createWorkspace(dirnameOf(file))).workspaceId, void 0);
|
|
403725
|
-
await settleRef(deps, sessionId, file, "file");
|
|
403726
|
-
await openChecked(deps, sessionId, file, params.line, params.column);
|
|
403727
|
-
await openRest(deps, sessionId, params, 1);
|
|
403728
|
-
}
|
|
403729
|
-
/** 规则 1.2:以文件夹注册新工作区 + 新对话 + 文件夹引用 + 编辑页。 */
|
|
403730
|
-
async function runFolderNew(deps, folder, params) {
|
|
403731
|
-
const sessionId = await spawnInWorkspace(deps, (await deps.createWorkspace(folder)).workspaceId, void 0);
|
|
403732
|
-
await settleRef(deps, sessionId, folder, "folder");
|
|
403733
|
-
await openChecked(deps, sessionId, null);
|
|
403734
|
-
await openRest(deps, sessionId, params, 1);
|
|
403735
|
-
}
|
|
403736
|
-
/** 在指定工作区(或回退 cwd)新建对话并置为当前;返回新会话 id。 */
|
|
403737
|
-
async function spawnInWorkspace(deps, workspaceId, fallbackCwd) {
|
|
403738
|
-
const sessionId = await deps.createSession(workspaceId ? { workspaceId } : fallbackCwd ? { cwd: fallbackCwd } : {});
|
|
403739
|
-
deps.openSession(sessionId);
|
|
403740
|
-
return sessionId;
|
|
403741
|
-
}
|
|
403742
|
-
/** 命中工作区集中「最近」的一个(其成员会话 updatedAt 最新者优先)。 */
|
|
403743
|
-
function bestMatch(deps, matches) {
|
|
403744
|
-
const byId = deps.sessions().byId;
|
|
403745
|
-
const score = (ws) => {
|
|
403746
|
-
const sessionLatest = (ws.sessionIds ?? []).reduce((max, id) => Math.max(max, byId[id]?.updatedAt ?? 0), 0);
|
|
403747
|
-
return Math.max(sessionLatest, Date.parse(ws.updatedAt ?? "") || 0);
|
|
403748
|
-
};
|
|
403749
|
-
return [...matches].sort((a, b) => score(b) - score(a))[0];
|
|
403750
|
-
}
|
|
403751
|
-
/** 引用插入轮询:输入门面就绪前每 300ms 重试(appendReference 忙态已降级视为成功)。 */
|
|
403752
|
-
async function settleRef(deps, sessionId, path, appearance) {
|
|
403753
|
-
for (let attempt = 0; attempt < REF_RETRY_MAX; attempt++) {
|
|
403754
|
-
if (await deps.reference(sessionId, path, appearance)) return;
|
|
403755
|
-
await sleep(deps, REF_RETRY_MS);
|
|
403756
|
-
}
|
|
403757
|
-
throw new Error("引用插入未完成");
|
|
403758
|
-
}
|
|
403759
|
-
/** 其余路径:文件进编辑器(步进),文件夹补文件夹引用。 */
|
|
403760
|
-
async function openRest(deps, sessionId, params, from) {
|
|
403761
|
-
const rest = params.paths.slice(from);
|
|
403762
|
-
for (const path of rest) {
|
|
403763
|
-
const kind = await deps.stat(path).catch(() => "missing");
|
|
403764
|
-
if (kind === "file") await openChecked(deps, sessionId ?? void 0, path);
|
|
403765
|
-
else if (kind === "directory" && sessionId) await settleRef(deps, sessionId, path, "folder");
|
|
403766
|
-
else throw new Error("路径不存在:" + path);
|
|
403767
|
-
}
|
|
403768
|
-
}
|
|
403769
|
-
/** 可注入 sleep。 */
|
|
403770
|
-
function sleep(deps, ms) {
|
|
403771
|
-
return new Promise((resolve) => deps.schedule(resolve, ms));
|
|
403772
|
-
}
|
|
403773
|
-
/** 真实依赖装配(全部来自 ctx 注入服务 + 既有 RPC/弹窗/编辑器入口)。 */
|
|
403774
|
-
function realDeps(ctx, overrides = {}) {
|
|
403775
|
-
const service = (name) => {
|
|
403776
|
-
const get = ctx?.get;
|
|
403777
|
-
return typeof get === "function" ? get.call(ctx, name) : void 0;
|
|
403778
|
-
};
|
|
403779
|
-
const add = createAddToConversation(ctx);
|
|
403780
|
-
return {
|
|
403781
|
-
stat: async (path) => {
|
|
403782
|
-
const result = await rpc("edrv.externalStat", { path });
|
|
403783
|
-
return result.ok ? result.kind : "missing";
|
|
403784
|
-
},
|
|
403785
|
-
workspaces: () => {
|
|
403786
|
-
const snap = service("workspaces")?.list?.getSnapshot?.() ?? {};
|
|
403787
|
-
return {
|
|
403788
|
-
items: snap.items ?? [],
|
|
403789
|
-
ready: snap.phase === "ready"
|
|
403790
|
-
};
|
|
403791
|
-
},
|
|
403792
|
-
sessions: () => {
|
|
403793
|
-
const snap = service("sessions")?.list?.getSnapshot?.() ?? {};
|
|
403794
|
-
return {
|
|
403795
|
-
ids: snap.ids ?? [],
|
|
403796
|
-
byId: snap.byId ?? {},
|
|
403797
|
-
ready: snap.phase !== "pending"
|
|
403798
|
-
};
|
|
403799
|
-
},
|
|
403800
|
-
createSession: async (opts) => {
|
|
403801
|
-
const sessions = service("sessions");
|
|
403802
|
-
if (typeof sessions?.create === "function") try {
|
|
403803
|
-
const created = await sessions.create(opts);
|
|
403804
|
-
const id = typeof created === "string" && created ? created : typeof created?.id === "string" ? created.id : typeof created?.sessionId === "string" ? created.sessionId : "";
|
|
403805
|
-
if (id) return id;
|
|
403806
|
-
} catch {}
|
|
403807
|
-
const draft = await newDraftSession(ctx, opts.cwd);
|
|
403808
|
-
if (draft.ok && draft.id) return draft.id;
|
|
403809
|
-
throw new Error("新建会话失败");
|
|
403810
|
-
},
|
|
403811
|
-
createWorkspace: async (path) => {
|
|
403812
|
-
const ws = await service("workspaces").create({ path });
|
|
403813
|
-
return {
|
|
403814
|
-
workspaceId: ws.workspaceId,
|
|
403815
|
-
title: ws.title
|
|
403816
|
-
};
|
|
403817
|
-
},
|
|
403818
|
-
openSession: (id) => {
|
|
403819
|
-
try {
|
|
403820
|
-
const sessions = service("sessions");
|
|
403821
|
-
sessions?.materializeScope?.(id);
|
|
403822
|
-
sessions?.manager?.select?.(id);
|
|
403823
|
-
sessions?.open?.(id);
|
|
403824
|
-
} catch {}
|
|
403825
|
-
},
|
|
403826
|
-
reference: async (sessionId, path, appearance) => {
|
|
403827
|
-
const outcome = await add.appendReference(sessionId, path, void 0, appearance);
|
|
403828
|
-
return outcome === "ok" || outcome === "busy";
|
|
403829
|
-
},
|
|
403830
|
-
choose: (title, folder) => chooseWorkspace(title, folder),
|
|
403831
|
-
openEditor: requestOpen,
|
|
403832
|
-
schedule: (fn, ms) => setTimeout(fn, ms),
|
|
403833
|
-
notify: (text) => toastDom(text),
|
|
403834
|
-
...overrides
|
|
403835
|
-
};
|
|
403836
|
-
}
|
|
403837
|
-
/** @private @author ddj 2026年09月28号
|
|
403838
|
-
* Await actual editor completion and propagate failure to the request result.
|
|
403839
|
-
* @param deps Open dependencies.
|
|
403840
|
-
* @param sessionId Target session.
|
|
403841
|
-
* @param path Target file or editor mount.
|
|
403842
|
-
* @param line Optional line.
|
|
403843
|
-
* @param column Optional column.
|
|
403844
|
-
*/
|
|
403845
|
-
async function openChecked(deps, sessionId, path, line, column) {
|
|
403846
|
-
if (!await deps.openEditor(path, line, column, sessionId)) throw new Error("编辑器打开未完成:" + (path ?? "工作区"));
|
|
403847
|
-
}
|
|
403848
|
-
//#endregion
|
|
403849
|
-
//#region src/client/externalOpen.ts
|
|
403850
|
-
/**
|
|
403851
|
-
* dsh-vscode-mode client — 外部深链落地(Windows 右键菜单 / Unity 外部编辑器)。
|
|
403852
|
-
* 启动时解析 location.search 的 edrvOpen 深链:跨源 referrer 守卫防外部网页诱导;
|
|
403853
|
-
* 处理完剥离 URL 参数防刷新重开;执行委托 openFlow 打开规则引擎(工作区/对话智能路由)。
|
|
403854
|
-
* 另挂 3s 移交轮询(兼活跃心跳):launcher 投递的打开请求由已打开页面就地执行,避免重复开新页。
|
|
403855
|
-
* 作者 ddj 2026-09-07 / 2026-09-08
|
|
403856
|
-
*/
|
|
403857
|
-
/** toast 自隐时长。 */
|
|
403858
|
-
const TOAST_MS = 5e3;
|
|
403859
|
-
/** 深链移交轮询周期(兼活跃心跳)。 */
|
|
403860
|
-
const HANDOFF_POLL_MS = 3e3;
|
|
403861
|
-
/**
|
|
403862
|
-
* 页面隐藏时的降频倍数(3s × 5 = 15s)。
|
|
403863
|
-
* 必须仍短于 host 租约(src/externalHandoff.ts 的 LEASE_MS = 30s):心跳一旦超期,
|
|
403864
|
-
* host 判定该页面已死,launcher 会重复开新页——比多几次 RPC 更糟。
|
|
403865
|
-
*/
|
|
403866
|
-
const HIDDEN_POLL_EVERY = 5;
|
|
403867
|
-
/**
|
|
403868
|
-
* 装配外部深链:深链参数缺失时仅保持移交轮询(零额外开销)。
|
|
403869
|
-
* @author ddj 2026年09月07号
|
|
403870
|
-
* @param ctx 客户端根上下文(sessions/workspaces 服务)
|
|
403871
|
-
* @param options 可注入选项
|
|
403872
|
-
*/
|
|
403873
|
-
function setupExtOpen(ctx, options = {}) {
|
|
403874
|
-
const dispose = startPresencePoll(ctx);
|
|
403875
|
-
const params = parseOpenParams(options.search ?? (typeof location !== "undefined" ? location.search : ""));
|
|
403876
|
-
if (!params) return dispose;
|
|
403877
|
-
const referrer = options.referrer ?? (typeof document !== "undefined" ? document.referrer : "");
|
|
403878
|
-
if (!referrerAllowed(referrer, options.origin ?? (typeof location !== "undefined" ? location.origin : ""))) {
|
|
403879
|
-
log$2.warn("已忽略跨源深链请求(referrer=" + referrer + ")");
|
|
403880
|
-
return dispose;
|
|
403881
|
-
}
|
|
403882
|
-
stripCurrentUrl();
|
|
403883
|
-
openDeepLink(ctx, params).catch((error) => {
|
|
403884
|
-
log$2.warn("深链打开失败:" + String(error));
|
|
403885
|
-
toastDom("深链打开失败:" + String(error?.message ?? error));
|
|
403886
|
-
});
|
|
403887
|
-
return dispose;
|
|
403888
|
-
}
|
|
403889
|
-
/**
|
|
403890
|
-
* 深链移交轮询:可见时每 3s 向 host 领取待打开请求(兼活跃心跳,launcher 据此决定是否开新页);
|
|
403891
|
-
* 页面隐藏时降到 15s 一次——无人看页面时移交不必保持 3s 精度,但心跳不能停(超期会让 host 判页面已死)。
|
|
403892
|
-
* 页面级单例(window 标记防 HMR 重复);领取到即执行打开规则。
|
|
403893
|
-
* @author ddj 2026年09月08号 / 2026年10月15号
|
|
403894
|
-
* @param ctx 客户端根上下文
|
|
403895
|
-
*/
|
|
403896
|
-
function startPresencePoll(ctx) {
|
|
403897
|
-
if (typeof window === "undefined") return () => {};
|
|
403898
|
-
const marker = window;
|
|
403899
|
-
if (typeof marker.__edrvExtPoll === "function") marker.__edrvExtPoll();
|
|
403900
|
-
const generatedId = createRequestId();
|
|
403901
|
-
if (!generatedId) {
|
|
403902
|
-
log$2.warn("安全随机源不可用,已停用外部打开心跳");
|
|
403903
|
-
return () => {};
|
|
403904
|
-
}
|
|
403905
|
-
const clientId = generatedId;
|
|
403906
|
-
let disposed = false;
|
|
403907
|
-
let polling = false;
|
|
403908
|
-
let executing = false;
|
|
403909
|
-
let ticks = 0;
|
|
403910
|
-
/**
|
|
403911
|
-
* 页面隐藏时按倍数跳轮;可见时每次都跑并清零计数。
|
|
403912
|
-
* @author ddj 2026年10月15号
|
|
403913
|
-
* @returns 是否跳过本轮
|
|
403914
|
-
*/
|
|
403915
|
-
function hiddenSkip() {
|
|
403916
|
-
if (typeof document === "undefined" || document.visibilityState !== "hidden") {
|
|
403917
|
-
ticks = 0;
|
|
403918
|
-
return false;
|
|
403919
|
-
}
|
|
403920
|
-
ticks += 1;
|
|
403921
|
-
return ticks % HIDDEN_POLL_EVERY !== 0;
|
|
403922
|
-
}
|
|
403923
|
-
/** @private @author ddj 2026年09月28号 Open all requested paths before acknowledging this exact lease. */
|
|
403924
|
-
async function execute(open) {
|
|
403925
|
-
executing = true;
|
|
403926
|
-
try {
|
|
403927
|
-
const success = await openDeepLink(ctx, open);
|
|
403928
|
-
if (!disposed) await rpc("edrv.external.ack", {
|
|
403929
|
-
token: open.token,
|
|
403930
|
-
leaseId: open.leaseId,
|
|
403931
|
-
clientId,
|
|
403932
|
-
success,
|
|
403933
|
-
error: success ? void 0 : "编辑器打开失败或取消"
|
|
403934
|
-
});
|
|
403935
|
-
} finally {
|
|
403936
|
-
executing = false;
|
|
403937
|
-
}
|
|
403938
|
-
}
|
|
403939
|
-
/** @private @author ddj 2026年09月28号 Poll while opening to renew the lease without accepting a second request. */
|
|
403940
|
-
async function tick() {
|
|
403941
|
-
if (disposed || polling) return;
|
|
403942
|
-
polling = true;
|
|
403943
|
-
try {
|
|
403944
|
-
const result = await rpc("edrv.external.pending", {
|
|
403945
|
-
clientId,
|
|
403946
|
-
busy: executing
|
|
403947
|
-
});
|
|
403948
|
-
if (!disposed && result.ok && result.open && !executing) execute(result.open).catch(() => {});
|
|
403949
|
-
} finally {
|
|
403950
|
-
polling = false;
|
|
403951
|
-
}
|
|
403952
|
-
}
|
|
403953
|
-
const timer = window.setInterval(() => {
|
|
403954
|
-
if (hiddenSkip()) return;
|
|
403955
|
-
tick().catch(() => {});
|
|
403956
|
-
}, HANDOFF_POLL_MS);
|
|
403957
|
-
/** @private @author ddj 2026年09月28号 Stop heartbeats and fail outstanding editor waits on plugin disposal. */
|
|
403958
|
-
function dispose() {
|
|
403959
|
-
disposed = true;
|
|
403960
|
-
window.clearInterval(timer);
|
|
403961
|
-
disposeOpenWait();
|
|
403962
|
-
if (marker.__edrvExtPoll === dispose) delete marker.__edrvExtPoll;
|
|
403963
|
-
}
|
|
403964
|
-
marker.__edrvExtPoll = dispose;
|
|
403965
|
-
tick().catch(() => {});
|
|
403966
|
-
return dispose;
|
|
403967
|
-
}
|
|
403968
|
-
/**
|
|
403969
|
-
* 跨源守卫:referrer 为空(launcher/Unity 直开)或同源 → 允许。
|
|
403970
|
-
* @author ddj 2026年09月07号
|
|
403971
|
-
* @param referrer 文档 referrer
|
|
403972
|
-
* @param origin 当前页面源
|
|
403973
|
-
* @returns 是否允许处理深链
|
|
403974
|
-
*/
|
|
403975
|
-
function referrerAllowed(referrer, origin) {
|
|
403976
|
-
if (!referrer) return true;
|
|
403977
|
-
try {
|
|
403978
|
-
return new URL(referrer).origin === origin;
|
|
403979
|
-
} catch {
|
|
403980
|
-
return false;
|
|
403981
|
-
}
|
|
403982
|
-
}
|
|
403983
|
-
/**
|
|
403984
|
-
* 剥离 URL 中的深链参数(保留其余参数与 hash,防刷新重开)。
|
|
403985
|
-
* @author ddj 2026年09月07号
|
|
403986
|
-
* @param href 完整 URL
|
|
403987
|
-
* @returns 清理后的 URL(pathname+search+hash)
|
|
403988
|
-
*/
|
|
403989
|
-
function stripOpenHref(href) {
|
|
403990
|
-
const url = new URL(href);
|
|
403991
|
-
for (const key of EDRV_PARAM_KEYS) url.searchParams.delete(key);
|
|
403992
|
-
return url.pathname + url.search + url.hash;
|
|
403993
|
-
}
|
|
403994
|
-
/** 清理当前页面 URL(best-effort,失败不阻塞打开)。 */
|
|
403995
|
-
function stripCurrentUrl() {
|
|
403996
|
-
try {
|
|
403997
|
-
if (typeof location === "undefined" || typeof history === "undefined") return;
|
|
403998
|
-
const next = stripOpenHref(location.href);
|
|
403999
|
-
if (next !== location.pathname + location.search + location.hash) history.replaceState(null, "", next);
|
|
404000
|
-
} catch {}
|
|
404001
|
-
}
|
|
404002
|
-
/**
|
|
404003
|
-
* 轻量 toast(编辑器未挂载也能提示;5s 自隐;openFlow 通知与错误呈现共用)。
|
|
404004
|
-
* @author ddj 2026年09月07号
|
|
404005
|
-
* @param text 提示文本
|
|
404006
|
-
*/
|
|
404007
|
-
function toastDom(text) {
|
|
404008
|
-
if (typeof document === "undefined") return;
|
|
404009
|
-
const toast = document.createElement("div");
|
|
404010
|
-
toast.textContent = text;
|
|
404011
|
-
toast.setAttribute("style", [
|
|
404012
|
-
"position:fixed",
|
|
404013
|
-
"left:50%",
|
|
404014
|
-
"bottom:28px",
|
|
404015
|
-
"transform:translateX(-50%)",
|
|
404016
|
-
"z-index:99999",
|
|
404017
|
-
"padding:8px 14px",
|
|
404018
|
-
"border-radius:8px",
|
|
404019
|
-
"max-width:80vw",
|
|
404020
|
-
"background:#2a2a2a",
|
|
404021
|
-
"color:#ddd",
|
|
404022
|
-
"border:1px solid #444",
|
|
404023
|
-
"font-size:12px",
|
|
404024
|
-
"box-shadow:0 4px 16px rgba(0,0,0,.35)"
|
|
404025
|
-
].join(";"));
|
|
404026
|
-
document.body?.appendChild(toast);
|
|
404027
|
-
setTimeout(() => toast.remove(), TOAST_MS);
|
|
404028
|
-
}
|
|
404029
|
-
//#endregion
|
|
404030
404203
|
//#region src/client/compatSummary.ts
|
|
404031
404204
|
/**
|
|
404032
404205
|
* Classify the API generation without importing the host's node-only version probe.
|
|
@@ -411139,6 +411312,107 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
411139
411312
|
function clearFileClip() {
|
|
411140
411313
|
clip = null;
|
|
411141
411314
|
}
|
|
411315
|
+
//#endregion
|
|
411316
|
+
//#region src/client/downloadSink.ts
|
|
411317
|
+
/**
|
|
411318
|
+
* 让用户选择保存目录。
|
|
411319
|
+
* @author ddj 2026年11月03号
|
|
411320
|
+
* @returns 所选目录 / 回落原因 / 'cancelled'(用户取消,整批终止)
|
|
411321
|
+
* @sideEffects 弹出系统目录选择器,成功后可能触发一次写权限请求
|
|
411322
|
+
*/
|
|
411323
|
+
async function pickSaveSink() {
|
|
411324
|
+
if (globalThis.isSecureContext === false) return {
|
|
411325
|
+
kind: "browser",
|
|
411326
|
+
reason: "insecure"
|
|
411327
|
+
};
|
|
411328
|
+
const picker = pickerOf();
|
|
411329
|
+
if (!picker) return {
|
|
411330
|
+
kind: "browser",
|
|
411331
|
+
reason: "unsupported"
|
|
411332
|
+
};
|
|
411333
|
+
let dir;
|
|
411334
|
+
try {
|
|
411335
|
+
dir = await picker({
|
|
411336
|
+
id: "vsm-download",
|
|
411337
|
+
mode: "readwrite",
|
|
411338
|
+
startIn: "downloads"
|
|
411339
|
+
});
|
|
411340
|
+
} catch (error) {
|
|
411341
|
+
return isAbort(error) ? "cancelled" : {
|
|
411342
|
+
kind: "browser",
|
|
411343
|
+
reason: "denied"
|
|
411344
|
+
};
|
|
411345
|
+
}
|
|
411346
|
+
return await ensureWritable(dir) ? {
|
|
411347
|
+
kind: "dir",
|
|
411348
|
+
dir
|
|
411349
|
+
} : {
|
|
411350
|
+
kind: "browser",
|
|
411351
|
+
reason: "denied"
|
|
411352
|
+
};
|
|
411353
|
+
}
|
|
411354
|
+
/**
|
|
411355
|
+
* 取回 `showDirectoryPicker`(不安全上下文视为没有)。
|
|
411356
|
+
* @author ddj 2026年11月03号
|
|
411357
|
+
* @returns 绑定到 globalThis 的选目录函数;不可用为 undefined
|
|
411358
|
+
*/
|
|
411359
|
+
function pickerOf() {
|
|
411360
|
+
const win = globalThis;
|
|
411361
|
+
if (win.isSecureContext === false) return void 0;
|
|
411362
|
+
return typeof win.showDirectoryPicker === "function" ? win.showDirectoryPicker.bind(win) : void 0;
|
|
411363
|
+
}
|
|
411364
|
+
/**
|
|
411365
|
+
* 确认目录可写(部分浏览器选择后仍需显式请求一次 readwrite 权限)。
|
|
411366
|
+
* @author ddj 2026年11月03号
|
|
411367
|
+
* @param dir 目录句柄
|
|
411368
|
+
* @returns 已获得写权限为 true
|
|
411369
|
+
* @throws 权限查询本身抛错时向上透传(由调用方按回落处理)
|
|
411370
|
+
*/
|
|
411371
|
+
async function ensureWritable(dir) {
|
|
411372
|
+
if (!dir.queryPermission) return true;
|
|
411373
|
+
if (await dir.queryPermission({ mode: "readwrite" }) === "granted") return true;
|
|
411374
|
+
if (!dir.requestPermission) return false;
|
|
411375
|
+
return await dir.requestPermission({ mode: "readwrite" }) === "granted";
|
|
411376
|
+
}
|
|
411377
|
+
/**
|
|
411378
|
+
* 把字节写到所选目录下的相对路径处(自动逐级建子目录)。
|
|
411379
|
+
* @author ddj 2026年11月03号
|
|
411380
|
+
* @param dir 目标目录句柄
|
|
411381
|
+
* @param relPath 工作区相对路径(分隔符 `/`;越界段 `..` 会被丢弃)
|
|
411382
|
+
* @param bytes 原始字节
|
|
411383
|
+
* @sideEffects 在用户所选目录内创建目录与文件并覆盖同名文件
|
|
411384
|
+
* @throws 磁盘写入失败或文件名非法时透传底层错误
|
|
411385
|
+
*/
|
|
411386
|
+
async function writeFileToDir(dir, relPath, bytes) {
|
|
411387
|
+
const parts = pathSegments(relPath);
|
|
411388
|
+
const fileName = parts.pop() ?? "download";
|
|
411389
|
+
let parent = dir;
|
|
411390
|
+
for (const part of parts) parent = await parent.getDirectoryHandle(part, { create: true });
|
|
411391
|
+
const writable = await (await parent.getFileHandle(fileName, { create: true })).createWritable();
|
|
411392
|
+
try {
|
|
411393
|
+
await writable.write(bytes);
|
|
411394
|
+
} finally {
|
|
411395
|
+
await writable.close();
|
|
411396
|
+
}
|
|
411397
|
+
}
|
|
411398
|
+
/**
|
|
411399
|
+
* 相对路径切段(过滤空段、`.` 与越界 `..`,兼容反斜杠)。
|
|
411400
|
+
* @author ddj 2026年11月03号
|
|
411401
|
+
* @param relPath 工作区相对路径
|
|
411402
|
+
* @returns 安全的路径段(末段为文件名)
|
|
411403
|
+
*/
|
|
411404
|
+
function pathSegments(relPath) {
|
|
411405
|
+
return relPath.split(/[\\/]+/).map((part) => part.trim()).filter((part) => part !== "" && part !== "." && part !== "..");
|
|
411406
|
+
}
|
|
411407
|
+
/**
|
|
411408
|
+
* 判断是否用户取消(AbortError),不依赖 DOMException 构造函数的存在。
|
|
411409
|
+
* @author ddj 2026年11月03号
|
|
411410
|
+
* @param error 捕获值
|
|
411411
|
+
* @returns 取消为 true
|
|
411412
|
+
*/
|
|
411413
|
+
function isAbort(error) {
|
|
411414
|
+
return typeof error === "object" && error !== null && error.name === "AbortError";
|
|
411415
|
+
}
|
|
411142
411416
|
/**
|
|
411143
411417
|
* 初始状态:根目录默认展开,等待首层 listDir 填充。
|
|
411144
411418
|
* @author ddj 2026年11月03号
|
|
@@ -411513,7 +411787,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
411513
411787
|
dialogClass: "edrv-dl-dialog",
|
|
411514
411788
|
width: "min(640px, calc(100vw - 40px))",
|
|
411515
411789
|
onClose
|
|
411516
|
-
}, react$1.default.createElement("h3", null, "下载到本机"), react$1.default.createElement("div", { className: "edrv-dl-sub" }, root ? "/" + root : "工作区根目录"), renderTree({
|
|
411790
|
+
}, react$1.default.createElement("h3", null, "下载到本机"), react$1.default.createElement("div", { className: "edrv-dl-sub" }, root ? "/" + root : "工作区根目录"), react$1.default.createElement("div", { className: "edrv-dl-hint" }, "点下载后会先选择保存文件夹,文件按相对路径写入"), renderTree({
|
|
411517
411791
|
state,
|
|
411518
411792
|
root,
|
|
411519
411793
|
rows,
|
|
@@ -411565,7 +411839,7 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
411565
411839
|
className: "edrv-dl-go",
|
|
411566
411840
|
disabled: !picked.length,
|
|
411567
411841
|
onClick: () => onConfirm(picked)
|
|
411568
|
-
}, "
|
|
411842
|
+
}, "选择位置并下载 " + picked.length + " 个文件"));
|
|
411569
411843
|
}
|
|
411570
411844
|
/**
|
|
411571
411845
|
* 目录行:三角展开 / 收起,名称后带加载或错误提示。
|
|
@@ -411723,16 +411997,17 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
411723
411997
|
};
|
|
411724
411998
|
}
|
|
411725
411999
|
/**
|
|
411726
|
-
*
|
|
412000
|
+
* 下载单个文件并落盘(所选目录则按相对路径写入,否则交给浏览器 `<a download>`)。
|
|
411727
412001
|
* @author ddj 2026年11月03号
|
|
411728
412002
|
* @param path 工作区相对路径
|
|
411729
|
-
* @param opts 会话 /
|
|
412003
|
+
* @param opts 会话 / 取消 / 落盘选项
|
|
411730
412004
|
* @returns 路径与字节数
|
|
411731
|
-
* @throws
|
|
412005
|
+
* @throws 读取失败透传主机错误;目录写入失败透传底层错误
|
|
411732
412006
|
*/
|
|
411733
412007
|
async function downloadOne(path, opts = {}) {
|
|
411734
412008
|
const file = await fetchDownload(path, opts.sessionId);
|
|
411735
|
-
|
|
412009
|
+
if (opts.sink?.kind === "dir") await writeFileToDir(opts.sink.dir, path, file.bytes);
|
|
412010
|
+
else downloadBytes(file.bytes, baseNameOf$1(path) || "download", file.mime);
|
|
411736
412011
|
return {
|
|
411737
412012
|
path,
|
|
411738
412013
|
size: file.size
|
|
@@ -411787,22 +412062,43 @@ td.UniverTableCell, th.UniverTableCell{mso-padding-alt:0cm 5.4pt 0cm 5.4pt;}
|
|
|
411787
412062
|
});
|
|
411788
412063
|
}
|
|
411789
412064
|
/**
|
|
411790
|
-
*
|
|
412065
|
+
* 批量下载并统一反馈(先选保存目录,再逐文件落盘;开始一条、结束一条,避免逐文件刷屏)。
|
|
411791
412066
|
* @author ddj 2026年11月03号
|
|
411792
412067
|
* @param paths 勾选路径
|
|
411793
412068
|
* @param ctx 面板上下文
|
|
412069
|
+
* @sideEffects 弹出目录选择器;用户取消则整批中止
|
|
411794
412070
|
*/
|
|
411795
412071
|
async function runBatch(paths, ctx) {
|
|
411796
412072
|
if (!paths.length) return;
|
|
412073
|
+
const sink = await pickSaveSink();
|
|
412074
|
+
if (sink === "cancelled") {
|
|
412075
|
+
ctx?.notify?.("已取消下载");
|
|
412076
|
+
return;
|
|
412077
|
+
}
|
|
412078
|
+
if (sink.kind === "browser") ctx?.notify?.(fallbackText(sink.reason));
|
|
411797
412079
|
if (paths.length > 1) ctx?.notify?.("正在下载 " + paths.length + " 个文件…");
|
|
411798
412080
|
try {
|
|
411799
|
-
const out = await downloadMany(paths, {
|
|
412081
|
+
const out = await downloadMany(paths, {
|
|
412082
|
+
sessionId: ctx?.sessionId,
|
|
412083
|
+
sink
|
|
412084
|
+
});
|
|
411800
412085
|
ctx?.notify?.(batchText(out));
|
|
411801
412086
|
} catch (error) {
|
|
411802
412087
|
ctx?.notify?.("下载失败:" + textOfError(error));
|
|
411803
412088
|
}
|
|
411804
412089
|
}
|
|
411805
412090
|
/**
|
|
412091
|
+
* 目录选择不可用时的提示文案(说明文件去了浏览器默认下载目录)。
|
|
412092
|
+
* @author ddj 2026年11月03号
|
|
412093
|
+
* @param reason 回落原因
|
|
412094
|
+
* @returns 单行提示文案
|
|
412095
|
+
*/
|
|
412096
|
+
function fallbackText(reason) {
|
|
412097
|
+
if (reason === "unsupported") return "浏览器不支持选择保存目录,已保存到默认下载目录";
|
|
412098
|
+
if (reason === "insecure") return "当前页面不是安全上下文(需 https 或 localhost),已保存到默认下载目录";
|
|
412099
|
+
return "未获得目录写入权限,已保存到默认下载目录";
|
|
412100
|
+
}
|
|
412101
|
+
/**
|
|
411806
412102
|
* 批量结果口播文案。
|
|
411807
412103
|
* @author ddj 2026年11月03号
|
|
411808
412104
|
* @param out 批量结果
|