@michengai/dsh-codex-ui 0.2.111 → 0.2.113
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/CHANGELOG.md +66 -0
- package/CHANGELOG.zh-CN.md +66 -0
- package/README.md +65 -71
- package/README.zh-CN.md +64 -73
- package/lib/client.js +241 -29
- package/lib/index.d.mts +1 -9
- package/lib/index.mjs +57 -66
- package/package.json +8 -4
package/lib/index.mjs
CHANGED
|
@@ -5,6 +5,16 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
8
|
+
//#region src/business-api.ts
|
|
9
|
+
const CODEX_UI_API_BASE = "/api/dsh-codex-ui";
|
|
10
|
+
const CODEX_UI_API_ENDPOINTS = Object.freeze({
|
|
11
|
+
connectors: `${CODEX_UI_API_BASE}/connectors`,
|
|
12
|
+
dependencies: `${CODEX_UI_API_BASE}/dependencies`,
|
|
13
|
+
openInExplorer: `${CODEX_UI_API_BASE}/open-in-explorer`,
|
|
14
|
+
preferences: `${CODEX_UI_API_BASE}/preferences`,
|
|
15
|
+
sessionMove: `${CODEX_UI_API_BASE}/session-move`
|
|
16
|
+
});
|
|
17
|
+
//#endregion
|
|
8
18
|
//#region src/dependencies.ts
|
|
9
19
|
const SUITE_PACKAGE = "@michengai/dsh-codex-suite";
|
|
10
20
|
const SUITE_MEMBER_PACKAGES = [
|
|
@@ -1558,41 +1568,17 @@ function writeWorkspacePreferences(pinnedWorkspaceIds, workspaceGroups = [], pat
|
|
|
1558
1568
|
}
|
|
1559
1569
|
//#endregion
|
|
1560
1570
|
//#region src/index.ts
|
|
1561
|
-
const connectorsEndpoint =
|
|
1562
|
-
const dependenciesEndpoint =
|
|
1563
|
-
const explorerEndpoint =
|
|
1564
|
-
const preferencesEndpoint =
|
|
1565
|
-
const sessionMoveEndpoint =
|
|
1571
|
+
const connectorsEndpoint = CODEX_UI_API_ENDPOINTS.connectors;
|
|
1572
|
+
const dependenciesEndpoint = CODEX_UI_API_ENDPOINTS.dependencies;
|
|
1573
|
+
const explorerEndpoint = CODEX_UI_API_ENDPOINTS.openInExplorer;
|
|
1574
|
+
const preferencesEndpoint = CODEX_UI_API_ENDPOINTS.preferences;
|
|
1575
|
+
const sessionMoveEndpoint = CODEX_UI_API_ENDPOINTS.sessionMove;
|
|
1566
1576
|
const maxPreferencesBodyBytes = 32768;
|
|
1567
|
-
function isLoopbackAddress(value) {
|
|
1568
|
-
const address = value?.toLowerCase().replace(/^\[|\]$/g, "");
|
|
1569
|
-
return address === "localhost" || address === "localhost." || address === "::1" || address?.startsWith("127.") === true || address?.startsWith("::ffff:127.") === true;
|
|
1570
|
-
}
|
|
1571
1577
|
function headerValue(headers, name) {
|
|
1572
1578
|
const value = headers[name];
|
|
1573
1579
|
if (typeof value === "string") return value;
|
|
1574
1580
|
if (Array.isArray(value)) return value[0];
|
|
1575
1581
|
}
|
|
1576
|
-
/**
|
|
1577
|
-
* 依赖安装会改写用户配置并拉起子进程,只允许回环地址上的同源浏览器请求。
|
|
1578
|
-
* Origin、Host 和 Sec-Fetch-Site 都可被非浏览器客户端伪造,不能单独作为授权依据。
|
|
1579
|
-
*/
|
|
1580
|
-
function crossSiteRequest(request) {
|
|
1581
|
-
if (!isLoopbackAddress(request.socket?.remoteAddress)) return true;
|
|
1582
|
-
const headers = request.headers;
|
|
1583
|
-
if (headers === void 0) return true;
|
|
1584
|
-
const site = headerValue(headers, "sec-fetch-site");
|
|
1585
|
-
if (site !== void 0 && site !== "same-origin") return true;
|
|
1586
|
-
const origin = headerValue(headers, "origin");
|
|
1587
|
-
const host = headerValue(headers, "host");
|
|
1588
|
-
if (origin === void 0 || host === void 0) return true;
|
|
1589
|
-
try {
|
|
1590
|
-
const url = new URL(origin);
|
|
1591
|
-
return url.protocol !== "http:" && url.protocol !== "https:" || !isLoopbackAddress(url.hostname) || url.host !== host;
|
|
1592
|
-
} catch {
|
|
1593
|
-
return true;
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
1582
|
/** 把安装错误收成可给浏览器看的文案:我们自己的中文说明保留,带本地路径的底层错误脱敏。 */
|
|
1597
1583
|
function publicDependencyError(error) {
|
|
1598
1584
|
const message = error instanceof Error ? error.message : "依赖管理暂不可用。";
|
|
@@ -1615,6 +1601,35 @@ async function readRequestBody(request, maxBytes = maxPreferencesBodyBytes) {
|
|
|
1615
1601
|
}
|
|
1616
1602
|
return Buffer.concat(chunks).toString("utf8");
|
|
1617
1603
|
}
|
|
1604
|
+
/**
|
|
1605
|
+
* 所有业务 REST 依赖 connection >=0.1.2-rc.1 的 Host/Origin/Fetch Metadata 与登录检查。
|
|
1606
|
+
* 必须在读取请求体及执行副作用前调用;不得将 undefined 以外的拒绝结果当作放行。
|
|
1607
|
+
* 发布包信任边界由 tests/business-rest-auth.assert.ts 的真实宿主契约用例验证。
|
|
1608
|
+
*/
|
|
1609
|
+
function authenticateBusinessRequest(ctx, request, response, errorBody = (_status, message) => ({ error: message })) {
|
|
1610
|
+
const reject = (status, message) => {
|
|
1611
|
+
response.writeHead(status, {
|
|
1612
|
+
"content-type": "application/json; charset=utf-8",
|
|
1613
|
+
"cache-control": "no-store"
|
|
1614
|
+
});
|
|
1615
|
+
response.end(JSON.stringify(errorBody(status, message)));
|
|
1616
|
+
return false;
|
|
1617
|
+
};
|
|
1618
|
+
try {
|
|
1619
|
+
const connection = ctx.get("connection");
|
|
1620
|
+
if (typeof connection?.requestRejection !== "function") {
|
|
1621
|
+
ctx.logger.warn("Codex UI 业务 REST 认证不可用:缺少 connection.requestRejection。");
|
|
1622
|
+
return reject(503, "宿主认证服务暂不可用。");
|
|
1623
|
+
}
|
|
1624
|
+
const rejection = connection.requestRejection(request);
|
|
1625
|
+
if (rejection !== void 0) return reject(rejection, rejection === 401 ? "请先登录 DSH。" : "已拒绝不可信或跨站请求。");
|
|
1626
|
+
return true;
|
|
1627
|
+
} catch (error) {
|
|
1628
|
+
const reason = error instanceof Error ? `${error.name}: ${error.message}` : typeof error;
|
|
1629
|
+
ctx.logger.warn("Codex UI 业务 REST 认证调用失败:%s", reason);
|
|
1630
|
+
return reject(503, "宿主认证服务暂不可用。");
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1618
1633
|
const inject = [
|
|
1619
1634
|
"webServer",
|
|
1620
1635
|
"agents",
|
|
@@ -1650,6 +1665,13 @@ function publicSessionMoveError(error) {
|
|
|
1650
1665
|
}[code] ?? "暂时无法移动该会话,请稍后重试。"
|
|
1651
1666
|
};
|
|
1652
1667
|
}
|
|
1668
|
+
function sessionMoveAuthenticationError(status, error) {
|
|
1669
|
+
return {
|
|
1670
|
+
ok: false,
|
|
1671
|
+
code: status === 401 ? "session-move/unauthorized" : status === 403 ? "session-move/forbidden" : "session-move/service-unavailable",
|
|
1672
|
+
error
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1653
1675
|
/** 提供不泄露地址、命令和凭证的连接器目录。 */
|
|
1654
1676
|
function apply(ctx) {
|
|
1655
1677
|
const host = hostServices(ctx);
|
|
@@ -1660,6 +1682,7 @@ function apply(ctx) {
|
|
|
1660
1682
|
kind: "exact",
|
|
1661
1683
|
path: connectorsEndpoint,
|
|
1662
1684
|
handler: async (request, response) => {
|
|
1685
|
+
if (!authenticateBusinessRequest(ctx, request, response)) return;
|
|
1663
1686
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
1664
1687
|
response.writeHead(405);
|
|
1665
1688
|
response.end();
|
|
@@ -1702,6 +1725,7 @@ function apply(ctx) {
|
|
|
1702
1725
|
kind: "exact",
|
|
1703
1726
|
path: dependenciesEndpoint,
|
|
1704
1727
|
handler: async (request, response) => {
|
|
1728
|
+
if (!authenticateBusinessRequest(ctx, request, response)) return;
|
|
1705
1729
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
1706
1730
|
try {
|
|
1707
1731
|
if (request.method === "GET") {
|
|
@@ -1731,14 +1755,6 @@ function apply(ctx) {
|
|
|
1731
1755
|
return;
|
|
1732
1756
|
}
|
|
1733
1757
|
if (request.method === "POST") {
|
|
1734
|
-
if (crossSiteRequest(request)) {
|
|
1735
|
-
response.writeHead(403, {
|
|
1736
|
-
"content-type": "application/json; charset=utf-8",
|
|
1737
|
-
"cache-control": "no-store"
|
|
1738
|
-
});
|
|
1739
|
-
response.end(JSON.stringify({ error: "已拒绝非本机同源请求。" }));
|
|
1740
|
-
return;
|
|
1741
|
-
}
|
|
1742
1758
|
if (url.searchParams.get("action") === "update-all") {
|
|
1743
1759
|
const runtime = resolveDependencyRuntime(ctx);
|
|
1744
1760
|
const notifyParent = canRequestParentReload(runtime);
|
|
@@ -1800,6 +1816,7 @@ function apply(ctx) {
|
|
|
1800
1816
|
kind: "exact",
|
|
1801
1817
|
path: preferencesEndpoint,
|
|
1802
1818
|
handler: async (request, response) => {
|
|
1819
|
+
if (!authenticateBusinessRequest(ctx, request, response)) return;
|
|
1803
1820
|
try {
|
|
1804
1821
|
if (request.method === "GET" || request.method === "HEAD") {
|
|
1805
1822
|
const preferences = await readWorkspacePreferences();
|
|
@@ -1811,14 +1828,6 @@ function apply(ctx) {
|
|
|
1811
1828
|
return;
|
|
1812
1829
|
}
|
|
1813
1830
|
if (request.method === "PUT") {
|
|
1814
|
-
if (crossSiteRequest(request)) {
|
|
1815
|
-
response.writeHead(403, {
|
|
1816
|
-
"content-type": "application/json; charset=utf-8",
|
|
1817
|
-
"cache-control": "no-store"
|
|
1818
|
-
});
|
|
1819
|
-
response.end(JSON.stringify({ error: "已拒绝跨站请求。" }));
|
|
1820
|
-
return;
|
|
1821
|
-
}
|
|
1822
1831
|
const body = JSON.parse(await readRequestBody(request));
|
|
1823
1832
|
const record = body !== null && typeof body === "object" ? body : void 0;
|
|
1824
1833
|
const pinnedWorkspaceIds = record === void 0 ? void 0 : parsePinnedWorkspaceIds(record.pinnedWorkspaceIds);
|
|
@@ -1878,23 +1887,12 @@ function apply(ctx) {
|
|
|
1878
1887
|
kind: "exact",
|
|
1879
1888
|
path: sessionMoveEndpoint,
|
|
1880
1889
|
handler: async (request, response) => {
|
|
1890
|
+
if (!authenticateBusinessRequest(ctx, request, response, sessionMoveAuthenticationError)) return;
|
|
1881
1891
|
if (request.method !== "POST") {
|
|
1882
1892
|
response.writeHead(405, { allow: "POST" });
|
|
1883
1893
|
response.end();
|
|
1884
1894
|
return;
|
|
1885
1895
|
}
|
|
1886
|
-
if (crossSiteRequest(request)) {
|
|
1887
|
-
response.writeHead(403, {
|
|
1888
|
-
"content-type": "application/json; charset=utf-8",
|
|
1889
|
-
"cache-control": "no-store"
|
|
1890
|
-
});
|
|
1891
|
-
response.end(JSON.stringify({
|
|
1892
|
-
ok: false,
|
|
1893
|
-
code: "session-move/cross-site",
|
|
1894
|
-
error: "已拒绝跨站请求。"
|
|
1895
|
-
}));
|
|
1896
|
-
return;
|
|
1897
|
-
}
|
|
1898
1896
|
try {
|
|
1899
1897
|
const body = JSON.parse(await readRequestBody(request));
|
|
1900
1898
|
const record = body !== null && typeof body === "object" ? body : void 0;
|
|
@@ -1951,19 +1949,12 @@ function apply(ctx) {
|
|
|
1951
1949
|
kind: "exact",
|
|
1952
1950
|
path: explorerEndpoint,
|
|
1953
1951
|
handler: async (request, response) => {
|
|
1952
|
+
if (!authenticateBusinessRequest(ctx, request, response)) return;
|
|
1954
1953
|
if (request.method !== "POST") {
|
|
1955
1954
|
response.writeHead(405, { allow: "POST" });
|
|
1956
1955
|
response.end();
|
|
1957
1956
|
return;
|
|
1958
1957
|
}
|
|
1959
|
-
if (crossSiteRequest(request)) {
|
|
1960
|
-
response.writeHead(403, {
|
|
1961
|
-
"content-type": "application/json; charset=utf-8",
|
|
1962
|
-
"cache-control": "no-store"
|
|
1963
|
-
});
|
|
1964
|
-
response.end(JSON.stringify({ error: "已拒绝跨站请求。" }));
|
|
1965
|
-
return;
|
|
1966
|
-
}
|
|
1967
1958
|
if (process.platform !== "win32") {
|
|
1968
1959
|
response.writeHead(501, {
|
|
1969
1960
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -2022,4 +2013,4 @@ function apply(ctx) {
|
|
|
2022
2013
|
}, "michengai-codex-ui: catalogs");
|
|
2023
2014
|
}
|
|
2024
2015
|
//#endregion
|
|
2025
|
-
export { RequestBodyTooLargeError, apply,
|
|
2016
|
+
export { RequestBodyTooLargeError, apply, inject, publicDependencyError, readRequestBody };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@michengai/dsh-codex-ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.113",
|
|
4
4
|
"description": "以 Codex 风格重构 DSH Web 侧栏的独立客户端插件",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"publishConfig": {
|
|
@@ -62,10 +62,12 @@
|
|
|
62
62
|
"scripts": {
|
|
63
63
|
"build": "tsc --noEmit && tsdown",
|
|
64
64
|
"typecheck": "tsc --noEmit",
|
|
65
|
-
"test": "pnpm run typecheck && vitest run && tsdown && node tests/run-assertions.mjs"
|
|
65
|
+
"test": "pnpm run typecheck && vitest run && tsdown && node tests/run-assertions.mjs && pnpm run test:layout",
|
|
66
|
+
"test:layout": "node scripts/verify-workspace-drop-layout.mjs --run && node scripts/verify-sidebar-transition.mjs"
|
|
66
67
|
},
|
|
67
68
|
"peerDependencies": {
|
|
68
|
-
"@deepseek-ai/cordis": ">=4.0.
|
|
69
|
+
"@deepseek-ai/cordis": ">=4.0.2 <5.0.0",
|
|
70
|
+
"@deepseek-ai/dsh-client-connection": ">=0.1.2-rc.1 <0.2.0",
|
|
69
71
|
"@deepseek-ai/dsh-client-locale": ">=0.1.0-rc.5 <0.2.0",
|
|
70
72
|
"@deepseek-ai/dsh-client-runtime": ">=0.1.0-rc.5 <0.2.0",
|
|
71
73
|
"@deepseek-ai/dsh-client-ui-conversation": ">=0.1.0-rc.5 <0.2.0",
|
|
@@ -119,12 +121,14 @@
|
|
|
119
121
|
"@types/node": "^26.0.0",
|
|
120
122
|
"@types/react": "~18.3.1",
|
|
121
123
|
"jsdom": "^29.1.1",
|
|
124
|
+
"playwright": "1.63.0",
|
|
122
125
|
"react": "^18.3.1",
|
|
123
126
|
"react-dom": "^18.3.1",
|
|
124
127
|
"tsdown": "^0.22.2",
|
|
125
128
|
"tsx": "^4.22.4",
|
|
126
129
|
"typescript": "^6.0.3",
|
|
127
|
-
"vitest": "^4.1.8"
|
|
130
|
+
"vitest": "^4.1.8",
|
|
131
|
+
"workflow-yaml": "npm:yaml@2.9.0"
|
|
128
132
|
},
|
|
129
133
|
"dependencies": {
|
|
130
134
|
"lucide-react": "^1.39.0"
|