dsh-code-ui 1.5.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +6 -1
- package/lib/index.js +113 -3
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -893,7 +893,12 @@ window.__ModuleLoader__.load({
|
|
|
893
893
|
if (!store.rootPath || (sid && sid !== store.rootSessionId)) {
|
|
894
894
|
const r = await callHost('get-root', sid ? { sessionId: sid } : {})
|
|
895
895
|
const root = (r && r.root) || null
|
|
896
|
-
if (!root) {
|
|
896
|
+
if (!root) {
|
|
897
|
+
// 透出真实失败原因(transport failure / RPC 协议错误),不再只给笼统红字:
|
|
898
|
+
// 通道整体 405(请求落 SPA 静态兜底)这类传输层问题一眼可辨,避免误判成根目录解析问题
|
|
899
|
+
store.error = (r && r.error) ? ('获取根目录失败:' + r.error) : '未找到工作区根目录'
|
|
900
|
+
emit(); return
|
|
901
|
+
}
|
|
897
902
|
await applyRoot(root)
|
|
898
903
|
store.rootSessionId = sid
|
|
899
904
|
} else {
|
package/lib/index.js
CHANGED
|
@@ -952,6 +952,69 @@ function registerSnippetsTool(tools) {
|
|
|
952
952
|
export const name = "dsh-code-ui";
|
|
953
953
|
export const inject = ["tools"];
|
|
954
954
|
|
|
955
|
+
// —— 兼容兜底:宿主 connection.rpc.handle 不可用时的自挂通道路由 ——
|
|
956
|
+
// 背景(2026-09-11 405 事故):宿主 connection 的 rpc-host.ts register() 直接访问 owner.webServer,
|
|
957
|
+
// 而 owner 是 connection 插件自己的 ctx(inject 只有 credentials),Cordis guard 抛
|
|
958
|
+
// "cannot get property webServer without inject" → 插件通道路由永不注册 → 客户端全部 405
|
|
959
|
+
//(POST 落 webServer 的 SPA 静态兜底),文件树/更新器等 RPC 功能全挂。
|
|
960
|
+
// 宿主修复前的版本上,插件侧唯一可行的适配就是自己挂同路径前缀路由:
|
|
961
|
+
// ① 信任/认证门复用宿主公开 API connection.requestRejection(HostConnectionHandle 文档明示
|
|
962
|
+
// 用于「Apply Connection's Host/Origin checks and browser authentication to another Web route」),
|
|
963
|
+
// 因此 Host/Origin 围栏与浏览器会话 cookie 校验和宿主通道完全同策略,不降低安全性;
|
|
964
|
+
// ② 信封协议(client-request / server-response + rpcId 关联)按 Connection 契约复刻。
|
|
965
|
+
// 宿主修复上线后此兜底不再触发(仅当 handle() 抛错时才启用),可随最低支持版本提升而删除。
|
|
966
|
+
function selfHostedChannelRoute(connection, channel, handler) {
|
|
967
|
+
const ENDPOINT_SEGMENT = /^[A-Za-z0-9_$.-]+$/;
|
|
968
|
+
return async (req, res) => {
|
|
969
|
+
try {
|
|
970
|
+
const rejection = connection.requestRejection(req);
|
|
971
|
+
if (rejection !== undefined) {
|
|
972
|
+
res.writeHead(rejection);
|
|
973
|
+
res.end(rejection === 401 ? "unauthorized" : "forbidden");
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
const url = new URL(req.url || "/", "http://dsh.invalid");
|
|
977
|
+
const endpoint = url.pathname.startsWith(channel + "/") ? url.pathname.slice(channel.length + 1) : null;
|
|
978
|
+
const segments = endpoint === null ? [] : endpoint.split("/");
|
|
979
|
+
if (req.method !== "POST" || !endpoint
|
|
980
|
+
|| segments.some((s) => !s || s === "." || s === ".." || !ENDPOINT_SEGMENT.test(s))) {
|
|
981
|
+
res.writeHead(404);
|
|
982
|
+
res.end("not found");
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (String(req.headers["content-type"] || "").split(";")[0].trim().toLowerCase() !== "application/json") {
|
|
986
|
+
res.writeHead(415);
|
|
987
|
+
res.end("content type must be application/json");
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
const chunks = [];
|
|
991
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
992
|
+
let message;
|
|
993
|
+
try { message = JSON.parse(Buffer.concat(chunks).toString("utf8")); }
|
|
994
|
+
catch (_) { res.writeHead(400); res.end("body is not JSON"); return; }
|
|
995
|
+
const rpcId = message && typeof message.rpcId === "string" ? message.rpcId : "invalid-request";
|
|
996
|
+
const send = (result) => {
|
|
997
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
998
|
+
res.end(JSON.stringify({ type: "server-response", rpcId, result }));
|
|
999
|
+
};
|
|
1000
|
+
if (!message || message.type !== "client-request" || typeof message.method !== "string") {
|
|
1001
|
+
send({ ok: false, error: { code: "gateway/bad-request", message: "invalid client-request message", details: {} } });
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
if (message.method !== endpoint) {
|
|
1005
|
+
send({ ok: false, error: { code: "gateway/bad-request", message: "method does not match endpoint", details: {} } });
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
let result;
|
|
1009
|
+
try { result = await handler(endpoint, message.payload, undefined); }
|
|
1010
|
+
catch (err) { res.writeHead(500); res.end("handler failure: " + String(err)); return; }
|
|
1011
|
+
send(result);
|
|
1012
|
+
} catch (err) {
|
|
1013
|
+
try { res.writeHead(500); res.end("channel failure: " + String(err)); } catch (_) {}
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
|
|
955
1018
|
export function apply(ctx) {
|
|
956
1019
|
// 注册 settings 命名空间(插件配置页按命名空间分发卡片;内部自带 ctx.inject(['settings']),
|
|
957
1020
|
// 无 settings 服务的宿主上静默不运行,不影响其它功能)
|
|
@@ -966,12 +1029,59 @@ export function apply(ctx) {
|
|
|
966
1029
|
registerSnippetsTool(c.tools);
|
|
967
1030
|
});
|
|
968
1031
|
|
|
969
|
-
// 等待 Connection 服务就绪后注册 RPC
|
|
1032
|
+
// 等待 Connection 服务就绪后注册 RPC 通道。
|
|
1033
|
+
// 注:2026-09-11 的 405 事故根因在宿主侧——connection 的 rpc-host.ts register() 曾直接访问
|
|
1034
|
+
// owner.webServer,而 owner 是 connection 插件自己的 ctx(inject 列表只有 credentials),
|
|
1035
|
+
// Cordis guard 抛 "cannot get property webServer without inject" → 前缀路由永不注册 →
|
|
1036
|
+
// 所有插件 RPC 405(POST 落到 SPA 静态兜底;dsh-context 等其它插件同样中招)。
|
|
1037
|
+
// 宿主已修:改经 connection 自己的 webServer 作用域挂载(与 /api 路由同路),
|
|
1038
|
+
// 因此插件侧无需注入 webServer(实测调用方声明也救不了)。
|
|
1039
|
+
// 防御性注册:不同 dsh-client-connection 版本的 handle() 对第三参(options)宽容度不同
|
|
1040
|
+
//(0.1.5-rc.2 为 (channel, handler) 两参、多余参数被忽略;其它版本可能校验/依赖 options)。
|
|
1041
|
+
// 带参注册抛错时回退两参注册;仍失败则向宿主控制台大声报错——通道注册失败时客户端
|
|
1042
|
+
// 所有 RPC 都会 405(POST 落到 SPA 静态兜底),不留日志极难定位。
|
|
970
1043
|
ctx.inject(["connection"], (c) => {
|
|
971
1044
|
const connection = c.connection;
|
|
972
1045
|
if (connection === undefined) return;
|
|
973
|
-
const
|
|
974
|
-
|
|
1046
|
+
const handler = rpcHandler(ctx);
|
|
1047
|
+
let dispose;
|
|
1048
|
+
let hostChannel = false;
|
|
1049
|
+
try {
|
|
1050
|
+
dispose = connection.rpc.handle(CHANNEL, handler, { authority: "loopback" });
|
|
1051
|
+
hostChannel = true;
|
|
1052
|
+
} catch (err) {
|
|
1053
|
+
console.error("[dsh-code-ui] 带 options 注册 RPC 通道失败,回退无 options 注册:" + ((err && err.message) || err));
|
|
1054
|
+
try {
|
|
1055
|
+
dispose = connection.rpc.handle(CHANNEL, handler);
|
|
1056
|
+
hostChannel = true;
|
|
1057
|
+
} catch (err2) {
|
|
1058
|
+
console.error("[dsh-code-ui] 宿主 RPC 通道注册失败(将启用插件自挂路由兜底):" + ((err2 && err2.message) || err2));
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
if (hostChannel) {
|
|
1062
|
+
console.log("[dsh-code-ui] RPC 通道已注册(宿主 connection 通道):" + CHANNEL);
|
|
1063
|
+
c.effect(() => dispose, "dsh-code-ui: rpc channel");
|
|
1064
|
+
} else if (typeof connection.requestRejection !== "function") {
|
|
1065
|
+
// 安全护栏:拿不到宿主的信任/认证门就绝不自挂路由——否则等于开一条绕过
|
|
1066
|
+
// Host/Origin 围栏与浏览器会话校验的裸路由。
|
|
1067
|
+
console.error("[dsh-code-ui] 宿主通道不可用且无 requestRejection,已放弃自挂路由兜底;请更新 DSH 宿主以修复通道注册 bug");
|
|
1068
|
+
} else {
|
|
1069
|
+
// 兜底:自挂同路径前缀路由(与 vendor 静态路由同款机制),复用 requestRejection 认证门。
|
|
1070
|
+
// 仅在宿主通道不可用时启用,宿主修复后不会走到这里。
|
|
1071
|
+
c.inject(["webServer"], (w) => {
|
|
1072
|
+
try {
|
|
1073
|
+
const disposeSelf = w.webServer.register({
|
|
1074
|
+
kind: "prefix",
|
|
1075
|
+
path: CHANNEL,
|
|
1076
|
+
handler: selfHostedChannelRoute(connection, CHANNEL, handler),
|
|
1077
|
+
});
|
|
1078
|
+
c.effect(() => disposeSelf, "dsh-code-ui: self-hosted rpc channel");
|
|
1079
|
+
console.log("[dsh-code-ui] 已启用插件自挂路由兜底:" + CHANNEL);
|
|
1080
|
+
} catch (err3) {
|
|
1081
|
+
console.error("[dsh-code-ui] 兜底通道路由注册失败:" + ((err3 && err3.message) || err3));
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
975
1085
|
c.effect(() => () => unwatchAllFiles(), "dsh-code-ui: file watchers");
|
|
976
1086
|
});
|
|
977
1087
|
}
|