@michengai/dsh-codex-ui 0.2.110 → 0.2.112

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 CHANGED
@@ -4,6 +4,65 @@
4
4
 
5
5
  This changelog records recent releases of DSH Codex UI and its one-click installer. Earlier changes remain available in the [Git history](https://github.com/MichengAI/dsh-codex-ui/commits/main).
6
6
 
7
+ ## Unreleased
8
+
9
+ ## 0.2.112 - 2026-09-08
10
+
11
+ ### Release automation
12
+
13
+ - Fixed Chromium installation steps in release workflows and added YAML structure and prerequisite validation.
14
+ - Installer releases now generate and commit bilingual CHANGELOG entries before extracting release notes; missing either language blocks publication.
15
+
16
+ ### Breaking changes
17
+
18
+ - Removed the public `crossSiteRequest` export. Consumers must use the host connection’s `requestRejection()` contract. Business GET routes now require authentication, including loopback requests (401 when signed out); missing authentication services return 503. The old REST paths have no compatibility aliases. Requires `@deepseek-ai/dsh-client-connection >=0.1.2-rc.1` and Cordis `>=4.0.2`.
19
+
20
+ ### Fixed
21
+
22
+ - Added localized 401/403/503 feedback for business requests and an explicit local-cache notice when workspace preferences cannot sync.
23
+ - Consolidated empty-chat padding and ignored stale invisible drop markers; conflicting distinct drop positions now emit a deduplicated diagnostic.
24
+
25
+ - Stopped applying custom-group spacing between projects in an ungrouped project list, keeping the empty-conversation label vertically centered between project rows.
26
+ - Centered empty-group text between adjacent group headers, accounting for the external group gap, and unified both empty-state labels at 13px/18px with the same muted color.
27
+ - Standardized the Chinese empty-conversation label to "暂无聊天".
28
+ - Aligned empty-group text with the group title without changing project or conversation indentation.
29
+ - Matched the Codex sidebar reorder marker's 8px hollow ring and 2px rounded stroke, extending the line toward its start and removing the ring-to-line gap.
30
+ - Unified group, project, and conversation reorder indicators around the midpoint between visible rows, including scrolling and collapsed content, instead of fixed offsets from wrapper elements.
31
+ - Added 4px below each project's conversation list, matching its top gap so the last conversation no longer touches the next project row; the spacing collapses with the list.
32
+ - Prevented the group reorder indicator above the first sidebar group from being clipped by the expanded Projects section.
33
+
34
+ ### Changed
35
+
36
+ - Increased group labels to 14px/600 with 32px rows and 12px spacing; project folders now switch between equally sized open and closed icons according to expansion state.
37
+ - Strengthened sidebar groups with persistent disclosure indicators, medium-weight labels, subtle backgrounds, and spacing while preserving existing project and conversation indentation and row widths. Expansion uses the arrow state without resembling a selected conversation; improved group touch targets and keyboard focus styling.
38
+ - Moved Codex UI's business REST routes from `/api/michengai/codex-ui/*` to `/api/dsh-codex-ui/*`, aligning their naming with the Skills Manager and IM Connect business endpoints; the old routes are no longer registered. The host baseline now requires `@deepseek-ai/dsh-client-connection >=0.1.2-rc.1` and Cordis `>=4.0.2`.
39
+
40
+ ### Security
41
+
42
+ - Delegated all five business REST routes to the DSH host's `requestRejection()` checks for trusted hosts, origins, and login cookies. Requests now fail closed when authentication is unavailable without blocking legitimate reverse-proxy origins through a loopback-only check.
43
+
44
+ ### Tests
45
+
46
+ - Run Chromium layout checks in `pnpm test` and CI without screenshots, including exact tertiary colors and empty-state padding. Added cross-section drag cleanup and pinned-host-package trust-boundary regression coverage with login stubbed as authenticated.
47
+
48
+ - Added business-route registration and authentication regression coverage for the new and removed paths, 401, 403, unavailable authentication, unchanged request-header forwarding, and an external-origin write authorized by the host boundary.
49
+
50
+ ## suite-installer-v0.1.29 - 2026-09-08
51
+
52
+ - Updated the suite to Codex UI `0.2.111` and IM Connect `0.1.38`, keeping the channel sidebar compatible with the authenticated `/api/dsh-im-connect/channels` endpoint.
53
+ - Refreshed exact member versions from the official npm registry for reproducible one-click installation.
54
+
55
+ ## 0.2.111 - 2026-09-08
56
+
57
+ ### Fixed
58
+
59
+ - Updated the channel sidebar to use `/api/dsh-im-connect/channels`, matching IM Connect `0.1.38` after removal of the old management endpoint.
60
+
61
+ ## suite-installer-v0.1.28 - 2026-09-08
62
+
63
+ - Updated the bundled Codex UI to `0.2.110`, including standalone DSH Web plugin-update recovery guidance.
64
+ - Refreshed exact member versions from the official npm registry for reproducible one-click installation.
65
+
7
66
  ## 0.2.110 - 2026-09-08
8
67
 
9
68
  ### Fixed
@@ -4,6 +4,65 @@
4
4
 
5
5
  本日志记录 DSH Codex UI 及其一键安装器的最近发布;更早的变更可查看 [Git 提交历史](https://github.com/MichengAI/dsh-codex-ui/commits/main)。
6
6
 
7
+ ## 未发布
8
+
9
+ ## 0.2.112 - 2026-09-08
10
+
11
+ ### 发布流程
12
+
13
+ - 修复发布工作流的 Chromium 安装步骤位置,增加 YAML 结构及测试前置条件校验。
14
+ - 安装器自动发布先生成并提交双语 CHANGELOG,再提取 Release 说明;缺少任一语言时阻止发布。
15
+
16
+ ### 不兼容变更
17
+
18
+ - 移除公共导出 `crossSiteRequest`,调用方应改用宿主 connection 的 `requestRejection()` 契约。业务 GET 现在必须认证,包括回环请求(未登录返回 401);缺少认证服务返回 503。旧 REST 地址没有兼容入口。要求 `@deepseek-ai/dsh-client-connection >=0.1.2-rc.1` 与 Cordis `>=4.0.2`。
19
+
20
+ ### 修复
21
+
22
+ - 业务请求增加 401/403/503 专属本地化提示;项目偏好无法同步时明确提示正在使用本地缓存。
23
+ - 合并空聊天四边间距声明,跳过不可见的陈旧拖拽标记;多个不同落点冲突时输出去重诊断。
24
+
25
+ - 普通项目列表不再误用自定义分组的组间距,使“暂无聊天”在上下项目胶囊之间保持居中。
26
+ - 将外部组间距纳入空分组文案的垂直居中计算;“暂无项目”和“暂无聊天”统一为 13px 字号、18px 行高及相同灰色。
27
+ - 空项目文案由“没有聊天”统一为“暂无聊天”。
28
+ - 分组内“暂无项目”与分组标题文字左对齐,不改变项目和会话胶囊的缩进。
29
+ - 参考 Codex 侧栏插入线,采用 8px 空心圆环与 2px 圆头线,向起点延长直线并消除圆环与线段的断口。
30
+ - 分组、项目与会话排序蓝线统一按相邻可见行的边界中点定位,适配滚动与折叠内容,不再依赖外层容器的固定偏移。
31
+ - 项目内会话列表底部补齐与顶部一致的 4px 留白,避免最后一条会话贴住下一个项目胶囊;留白随列表一起折叠。
32
+ - 修复分组拖到侧栏第一组之前时,顶部蓝色插入线被项目分区容器裁剪的问题。
33
+
34
+ ### 变更
35
+
36
+ - 分组标题增强至 14px/600、32px 行高和 12px 组间距;项目文件夹随展开、收起切换等尺寸图标。
37
+ - 强化侧栏分组的常驻箭头、标题字重、轻底色与组间留白;保留项目和会话原有缩进及胶囊宽度,展开状态仅由箭头表达,避免与会话选中混淆;补充分组触摸目标与键盘焦点样式。
38
+ - 将 Codex UI 自有业务 REST 从 `/api/michengai/codex-ui/*` 统一迁移到 `/api/dsh-codex-ui/*`,与 Skills Manager 和 IM Connect 的业务 endpoint 命名保持一致;旧地址不再注册。宿主依赖基线相应要求 `@deepseek-ai/dsh-client-connection >=0.1.2-rc.1` 与 Cordis `>=4.0.2`。
39
+
40
+ ### 安全
41
+
42
+ - 五个业务 REST 全部委托 DSH 宿主 `requestRejection()` 校验可信 Host、Origin 和登录 Cookie;认证服务不可用时关闭失败,不再用仅限回环地址的判断阻断合法反向代理访问。
43
+
44
+ ### 测试
45
+
46
+ - 将无截图的 Chromium 布局检查纳入 `pnpm test` 和 CI,精确检查三级灰与空状态间距;补齐跨区拖拽清理及锁定宿主发布包的信任边界回归,登录层固定为已登录。
47
+
48
+ - 新增业务路由注册和认证回归测试,覆盖新旧路径、401、403、认证不可用、请求头原样转交及宿主认证放行的外部 Origin 写请求。
49
+
50
+ ## suite-installer-v0.1.29 - 2026-09-08
51
+
52
+ - 将套件更新至 Codex UI `0.2.111` 和 IM Connect `0.1.38`,保证频道侧栏兼容已认证的 `/api/dsh-im-connect/channels` 接口。
53
+ - 从官方 npm 刷新成员精确版本,保证一键安装可复现。
54
+
55
+ ## 0.2.111 - 2026-09-08
56
+
57
+ ### 修复
58
+
59
+ - 频道侧栏改用 `/api/dsh-im-connect/channels`,与删除旧管理地址后的 IM Connect `0.1.38` 保持一致。
60
+
61
+ ## suite-installer-v0.1.28 - 2026-09-08
62
+
63
+ - 将套件中的 Codex UI 更新至 `0.2.110`,包含独立 DSH Web 的插件更新恢复指引。
64
+ - 从官方 npm 刷新成员精确版本,保证一键安装可复现。
65
+
7
66
  ## 0.2.110 - 2026-09-08
8
67
 
9
68
  ### 修复
package/lib/client.js CHANGED
@@ -270,7 +270,7 @@ window.__ModuleLoader__.load({
270
270
  }
271
271
  //#endregion
272
272
  //#region src/client/channel-api.ts
273
- const CHANNELS_ENDPOINT = "/dsh-im-connect/api/channels";
273
+ const CHANNELS_ENDPOINT = "/api/dsh-im-connect/channels";
274
274
  /** DSH 会话头不能写 origin=im,频道会话只靠 id 前缀。 */
275
275
  function isChannelSession(id) {
276
276
  return id.startsWith("im:");
@@ -848,6 +848,32 @@ window.__ModuleLoader__.load({
848
848
  function pendingInteractionForSession(sessionId, pendingInteractions, summaryKind) {
849
849
  return visiblePendingKind(summaryKind) ?? visiblePendingKind(pendingInteractions.get(sessionId)?.kind);
850
850
  }
851
+ //#endregion
852
+ //#region src/business-api.ts
853
+ const CODEX_UI_API_BASE = "/api/dsh-codex-ui";
854
+ const CODEX_UI_API_ENDPOINTS = Object.freeze({
855
+ connectors: `${CODEX_UI_API_BASE}/connectors`,
856
+ dependencies: `${CODEX_UI_API_BASE}/dependencies`,
857
+ openInExplorer: `${CODEX_UI_API_BASE}/open-in-explorer`,
858
+ preferences: `${CODEX_UI_API_BASE}/preferences`,
859
+ sessionMove: `${CODEX_UI_API_BASE}/session-move`
860
+ });
861
+ //#endregion
862
+ //#region src/client/business-request-error.ts
863
+ var BusinessRequestError = class extends Error {
864
+ status;
865
+ constructor(status) {
866
+ super(`业务请求失败:HTTP ${status}`);
867
+ this.status = status;
868
+ this.name = "BusinessRequestError";
869
+ }
870
+ };
871
+ function businessRequestErrorKey(error) {
872
+ if (!(error instanceof BusinessRequestError)) return void 0;
873
+ if (error.status === 401) return "errors.unauthorized";
874
+ if (error.status === 403) return "errors.forbidden";
875
+ if (error.status === 503) return "errors.serviceUnavailable";
876
+ }
851
877
  const WORKSPACE_GROUP_ERROR_MESSAGES = {
852
878
  "group-invalid": "分组信息无效。",
853
879
  "workspace-invalid": "项目标识无效。",
@@ -1001,7 +1027,7 @@ window.__ModuleLoader__.load({
1001
1027
  /** 浏览器本地持久化键;用于工作区偏好的首帧恢复与 Host 故障兜底。 */
1002
1028
  const PINNED_WORKSPACES_STORAGE_KEY = "dsh-codex-ui.pinned-workspace-ids";
1003
1029
  const WORKSPACE_GROUPS_STORAGE_KEY = "dsh-codex-ui.workspace-groups.v1";
1004
- const WORKSPACE_PREFERENCES_ENDPOINT = "/api/michengai/codex-ui/preferences";
1030
+ const WORKSPACE_PREFERENCES_ENDPOINT = CODEX_UI_API_ENDPOINTS.preferences;
1005
1031
  /** 清理无效或重复的工作区标识。 */
1006
1032
  function normalizePinnedWorkspaceIds(ids) {
1007
1033
  return [...new Set(ids.filter((id) => id.trim() !== ""))];
@@ -1040,7 +1066,7 @@ window.__ModuleLoader__.load({
1040
1066
  cache: "no-store",
1041
1067
  signal: AbortSignal.timeout(5e3)
1042
1068
  });
1043
- if (!response.ok) throw new Error(`读取置顶偏好失败:HTTP ${response.status}`);
1069
+ if (!response.ok) throw new BusinessRequestError(response.status);
1044
1070
  const payload = await response.json();
1045
1071
  if (payload === null || typeof payload !== "object") throw new Error("置顶偏好响应格式无效。");
1046
1072
  const record = payload;
@@ -1067,7 +1093,7 @@ window.__ModuleLoader__.load({
1067
1093
  }),
1068
1094
  signal: AbortSignal.timeout(5e3)
1069
1095
  });
1070
- if (!response.ok) throw new Error(`保存置顶偏好失败:HTTP ${response.status}`);
1096
+ if (!response.ok) throw new BusinessRequestError(response.status);
1071
1097
  }
1072
1098
  /** 兼容旧调用方;新代码应同时写入分组。 */
1073
1099
  async function readHostPinnedWorkspaceIds(fetcher = fetch) {
@@ -2009,6 +2035,8 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2009
2035
  }
2010
2036
  /** 将业务错误映射为当前语言,未知底层错误统一脱敏。 */
2011
2037
  function userErrorText(error, t) {
2038
+ const requestKey = businessRequestErrorKey(error);
2039
+ if (requestKey !== void 0) return t(requestKey);
2012
2040
  if (error instanceof UserFacingError) return error.message;
2013
2041
  if (error instanceof WorkspaceGroupError) return t(WORKSPACE_GROUP_ERROR_KEYS[error.code]);
2014
2042
  if (error instanceof HostActionError) return t(hostActionErrorKey(error));
@@ -2016,16 +2044,20 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2016
2044
  }
2017
2045
  /** 安装错误保留用户可执行的处理建议,但不直接展示 Host 原文。 */
2018
2046
  function installErrorText(error, t) {
2047
+ const requestKey = businessRequestErrorKey(error);
2048
+ if (requestKey !== void 0) return t(requestKey);
2019
2049
  const message = error instanceof Error ? error.message : String(error);
2020
2050
  return t(INSTALL_ERROR_KEYS.find(([pattern]) => pattern.test(message))?.[1] ?? "about.installFailed");
2021
2051
  }
2022
2052
  //#endregion
2023
2053
  //#region src/client/session-move.ts
2024
- const SESSION_MOVE_ENDPOINT = "/api/michengai/codex-ui/session-move";
2054
+ const SESSION_MOVE_ENDPOINT = CODEX_UI_API_ENDPOINTS.sessionMove;
2025
2055
  const MOVE_ACTION_PREFIX = "move-session:";
2026
2056
  const SESSION_MOVE_ERROR_KEYS = {
2027
2057
  "session-move/unavailable": "sessions.moveUnavailable",
2028
2058
  "session-move/service-unavailable": "sessions.moveUnavailable",
2059
+ "session-move/unauthorized": "sessions.moveUnauthorized",
2060
+ "session-move/forbidden": "sessions.moveForbidden",
2029
2061
  "session-move/session-not-found": "sessions.moveNotFound",
2030
2062
  "session-move/workspace-not-found": "sessions.moveNotFound",
2031
2063
  "session-move/subagent-unsupported": "sessions.moveSubagent",
@@ -2101,6 +2133,85 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2101
2133
  for (const sessionId of new Set(sessionIds)) await archiveSession(sessionId);
2102
2134
  }
2103
2135
  //#endregion
2136
+ //#region src/client/workspace-drop-indicator.ts
2137
+ const ROWS = ".dcu-wb-section-head,.dcu-wb-collection-head,.dcu-wb-project-head,.dcu-wb-session,.dcu-wb-nochat,.dcu-wb-empty";
2138
+ const TARGETS = ".dcu-wb-drop,.dcu-wb-group-order-drop";
2139
+ function measureWorkspaceDropIndicator(root, onTargets) {
2140
+ const measured = Array.from(root.querySelectorAll(TARGETS)).filter((node) => node.closest("[data-open=false]") === null).map((target) => ({
2141
+ target,
2142
+ rect: measureTarget(root, target)
2143
+ })).filter((item) => item.rect !== void 0);
2144
+ const unique = measured.filter((item, index) => !measured.slice(0, index).some((previous) => previous.rect.top === item.rect.top && previous.rect.left === item.rect.left && previous.rect.width === item.rect.width));
2145
+ onTargets?.(unique.map((item) => item.target));
2146
+ return unique[0]?.rect;
2147
+ }
2148
+ function measureTarget(root, target) {
2149
+ const section = target.closest(".dcu-wb-section") ?? root;
2150
+ const rows = Array.from(section.querySelectorAll(ROWS)).filter((node) => node.closest("[data-open=false]") === null).map((node) => ({
2151
+ node,
2152
+ rect: node.getBoundingClientRect()
2153
+ })).filter((row) => row.rect.height > 0 && row.rect.width > 0);
2154
+ const owned = rows.filter((row) => row.node === target || target.contains(row.node));
2155
+ const after = target.classList.contains("dcu-wb-drop-after");
2156
+ let upper;
2157
+ let lower;
2158
+ let anchor = target.getBoundingClientRect();
2159
+ if (owned.length > 0) {
2160
+ const edge = after ? owned[owned.length - 1] : owned[0];
2161
+ const index = rows.indexOf(edge);
2162
+ anchor = edge.rect;
2163
+ upper = after ? edge.rect.bottom : rows[index - 1]?.rect.bottom;
2164
+ lower = after ? rows[index + 1]?.rect.top : edge.rect.top;
2165
+ } else if (target.matches(".dcu-wb-pin-start,.dcu-wb-pin-end")) {
2166
+ if (anchor.width <= 0 || anchor.height <= 0) return void 0;
2167
+ upper = rows.filter((row) => row.rect.bottom <= anchor.top).at(-1)?.rect.bottom;
2168
+ lower = rows.find((row) => row.rect.top >= anchor.bottom)?.rect.top;
2169
+ if (lower === void 0) lower = anchor.bottom;
2170
+ if (upper === void 0) upper = anchor.top;
2171
+ } else return void 0;
2172
+ const center = upper === void 0 ? lower - 4 : lower === void 0 ? upper + 4 : (upper + lower) / 2;
2173
+ const box = root.getBoundingClientRect();
2174
+ return {
2175
+ top: center - box.top - root.clientTop + root.scrollTop - 4,
2176
+ left: anchor.left - box.left - root.clientLeft + root.scrollLeft + 4,
2177
+ width: Math.max(0, anchor.width - 12)
2178
+ };
2179
+ }
2180
+ /** 仅拖拽期间刷新,滚动、折叠动画和尺寸变化都使用当前帧的布局;结束时完整清理。 */
2181
+ function mountWorkspaceDropIndicator(root) {
2182
+ const line = root.ownerDocument.createElement("div");
2183
+ line.className = "dcu-wb-drop-indicator";
2184
+ line.setAttribute("aria-hidden", "true");
2185
+ root.append(line);
2186
+ root.dataset.dropIndicator = "measured";
2187
+ const view = root.ownerDocument.defaultView;
2188
+ let frame = 0;
2189
+ let previous = "";
2190
+ let previousConflict = [];
2191
+ const update = () => {
2192
+ const rect = measureWorkspaceDropIndicator(root, (targets) => {
2193
+ const conflict = targets.length > 1 ? targets : [];
2194
+ if (conflict.length > 1 && (conflict.length !== previousConflict.length || conflict.some((target, index) => target !== previousConflict[index]))) console.warn("Codex UI 拖拽存在多个有效落点,请检查跨区状态清理。", { count: conflict.length });
2195
+ previousConflict = conflict;
2196
+ });
2197
+ const next = rect === void 0 ? "" : `${rect.top},${rect.left},${rect.width}`;
2198
+ line.hidden = rect === void 0;
2199
+ if (rect !== void 0 && next !== previous) {
2200
+ line.style.top = `${rect.top}px`;
2201
+ line.style.left = `${rect.left}px`;
2202
+ line.style.width = `${rect.width}px`;
2203
+ }
2204
+ previous = next;
2205
+ frame = view.requestAnimationFrame(update);
2206
+ };
2207
+ update();
2208
+ return () => {
2209
+ view.cancelAnimationFrame(frame);
2210
+ line.remove();
2211
+ delete root.dataset.dropIndicator;
2212
+ };
2213
+ }
2214
+ //#endregion
2104
2215
  //#region src/client/CodexWorkspaceBrowser.tsx
2105
2216
  const DISCLOSURE_EXIT_MS = 180;
2106
2217
  function NewSessionIcon() {
@@ -2259,8 +2370,40 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2259
2370
  .dcu-wb-rename-input:focus{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 1px color-mix(in srgb,var(--dsw-alias-state-business-primary) 28%,transparent)}
2260
2371
  `;
2261
2372
  const runningStyles = `.dcu-wb-running{position:absolute;right:10px;top:50%;flex:none;width:12px;height:12px;margin-top:-6px;border:2px solid color-mix(in srgb,var(--dcu-sidebar-secondary) 25%,transparent);border-top-color:var(--dcu-sidebar-secondary);border-radius:50%;animation:dcu-wb-spin .8s linear infinite}@keyframes dcu-wb-spin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){.dcu-wb-running{animation:none}}`;
2262
- const typographyStyles = `.dcu-wb{--dcu-wb-disclosure-duration:180ms;--dcu-wb-disclosure-ease:cubic-bezier(.16,1,.3,1);font:14px/20px var(--dcu-font,var(--dsw-font-family))}.dcu-wb-section-label{color:var(--dcu-sidebar-secondary);font:13px/20px var(--dcu-font,var(--dsw-font-family));font-weight:400;letter-spacing:0;padding-left:0}.dcu-wb-section-caret{transition:opacity var(--dcu-wb-disclosure-duration) var(--dcu-wb-disclosure-ease),transform var(--dcu-wb-disclosure-duration) var(--dcu-wb-disclosure-ease)}.dcu-wb-section-head .dcu-wb-section-caret{position:static;left:auto;top:auto;opacity:.78}.dcu-wb-section-body,.dcu-wb-project-body,.dcu-wb-collection-body{display:block;min-height:0;height:auto;overflow:clip;opacity:1;transform:none;visibility:visible}.dcu-wb-section-body[data-open=false],.dcu-wb-project-body[data-open=false],.dcu-wb-collection-body[data-open=false]{display:block;height:0;opacity:0;transform:translateY(-2px);pointer-events:none}.dcu-wb-section-body[data-open=true]:has(.dcu-wb-drop),.dcu-wb-project-body[data-open=true]:has(.dcu-wb-drop),.dcu-wb-collection-body[data-open=true]:has(.dcu-wb-drop){overflow:visible}.dcu-wb-section-body[data-open=true]>div{animation:none}@media (prefers-reduced-motion:reduce){.dcu-wb-section-caret{transition:none}}.dcu-wb-project-title{font-size:14px;line-height:20px;font-weight:400;color:var(--dcu-sidebar-primary)}.dcu-wb-session-title{font-size:14px;line-height:20px;font-weight:400;color:var(--dcu-sidebar-secondary)}.dcu-wb-session.dcu-wb-selected .dcu-wb-session-title,.dcu-wb-session:hover .dcu-wb-session-title{color:var(--dcu-sidebar-primary)}.dcu-wb-empty{color:var(--dcu-sidebar-tertiary);font-size:13px;line-height:18px}.dcu-wb-nochat{padding:0 8px 4px 28px;color:var(--dcu-sidebar-tertiary);font-size:14px;line-height:20px}`;
2263
- const collectionLayoutStyles = `.dcu-wb-collection-body{padding-left:0}.dcu-wb-collection-body .dcu-wb-project-head{padding-left:8px}.dcu-wb-collection-body::before,.dcu-wb-group-member::after{display:none}.dcu-wb-collection-head,.dcu-wb-collection-head:focus-within{background:color-mix(in srgb,var(--dcu-sidebar-hover) 28%,transparent)}.dcu-wb-collection-head:hover{background:var(--dcu-sidebar-hover)}.dcu-wb-collection-head.dcu-wb-group-drop{background:var(--dcu-sidebar-hover)}.dcu-wb-workspace-move-drop>.dcu-wb-collection-head{background:transparent}`;
2373
+ const typographyStyles = `.dcu-wb{--dcu-wb-disclosure-duration:180ms;--dcu-wb-disclosure-ease:cubic-bezier(.16,1,.3,1);font:14px/20px var(--dcu-font,var(--dsw-font-family))}.dcu-wb-section-label{color:var(--dcu-sidebar-secondary);font:13px/20px var(--dcu-font,var(--dsw-font-family));font-weight:400;letter-spacing:0;padding-left:0}.dcu-wb-section-caret{transition:opacity var(--dcu-wb-disclosure-duration) var(--dcu-wb-disclosure-ease),transform var(--dcu-wb-disclosure-duration) var(--dcu-wb-disclosure-ease)}.dcu-wb-section-head .dcu-wb-section-caret{position:static;left:auto;top:auto;opacity:.78}.dcu-wb-section-body,.dcu-wb-project-body,.dcu-wb-collection-body{display:block;min-height:0;height:auto;overflow:clip;opacity:1;transform:none;visibility:visible}.dcu-wb-section-body[data-open=false],.dcu-wb-project-body[data-open=false],.dcu-wb-collection-body[data-open=false]{display:block;height:0;opacity:0;transform:translateY(-2px);pointer-events:none}.dcu-wb-section-body[data-open=true]:has(.dcu-wb-drop),.dcu-wb-project-body[data-open=true]:has(.dcu-wb-drop),.dcu-wb-collection-body[data-open=true]:has(.dcu-wb-drop){overflow:visible}.dcu-wb-section-body[data-open=true]>div{animation:none}@media (prefers-reduced-motion:reduce){.dcu-wb-section-caret{transition:none}}.dcu-wb-project-title{font-size:14px;line-height:20px;font-weight:400;color:var(--dcu-sidebar-primary)}.dcu-wb-session-title{font-size:14px;line-height:20px;font-weight:400;color:var(--dcu-sidebar-secondary)}.dcu-wb-session.dcu-wb-selected .dcu-wb-session-title,.dcu-wb-session:hover .dcu-wb-session-title{color:var(--dcu-sidebar-primary)}.dcu-wb-empty{color:var(--dcu-sidebar-tertiary);font-size:13px;line-height:18px}`;
2374
+ const collectionLayoutStyles = `
2375
+ .dcu-wb-tree{position:relative}
2376
+ .dcu-wb-tree[data-drop-indicator=measured] .dcu-wb-drop::before,.dcu-wb-tree[data-drop-indicator=measured] .dcu-wb-group-order-drop::before{display:none}
2377
+ .dcu-wb-drop-indicator{position:absolute;z-index:3;height:8px;pointer-events:none;color:var(--dsw-alias-state-business-primary)}
2378
+ .dcu-wb-drop-indicator::before{content:"";position:absolute;left:6px;right:0;top:3px;height:2px;border-radius:999px;background:currentColor}
2379
+ .dcu-wb-drop-indicator::after{content:"";position:absolute;box-sizing:border-box;left:0;top:0;width:8px;height:8px;border:2px solid currentColor;border-radius:50%}
2380
+ .dcu-wb-project-body:has(>.dcu-wb-session)::after{content:"";display:block;height:4px;pointer-events:none}
2381
+ .dcu-wb-section-body[data-open=true]:has(.dcu-wb-group-order-drop){overflow:visible}
2382
+ .dcu-wb-collection-body{padding-left:0}
2383
+ .dcu-wb-collection-body>.dcu-wb-group-member:first-child{padding-top:4px}
2384
+ .dcu-wb-collection-body .dcu-wb-project-head{padding-left:8px}
2385
+ .dcu-wb-collection-body>.dcu-wb-empty{padding:8px 8px 8px 26px}
2386
+ .dcu-wb-collection-body>.dcu-wb-empty,.dcu-wb-nochat{font-size:13px;line-height:18px;color:var(--dcu-sidebar-tertiary)}
2387
+ .dcu-wb-nochat{padding:1px 8px 5px 28px}
2388
+ .dcu-wb-collection:has(+.dcu-wb-collection)>.dcu-wb-collection-body>.dcu-wb-empty,.dcu-wb-collection:has(+.dcu-wb-ungrouped)>.dcu-wb-collection-body>.dcu-wb-empty{padding-top:14px;padding-bottom:2px}
2389
+ .dcu-wb-collection-body::before,.dcu-wb-group-member::after{display:none}
2390
+ .dcu-wb-collections{gap:12px}
2391
+ .dcu-wb-collections:has(>.dcu-wb-project){gap:0}
2392
+ .dcu-wb-collection-head{min-height:32px}
2393
+ .dcu-wb-collection-head:hover{background:var(--dcu-sidebar-hover)}
2394
+ .dcu-wb-collection-head.dcu-wb-group-drop{background:var(--dcu-sidebar-hover)}
2395
+ .dcu-wb-collection-head{background:color-mix(in srgb,var(--dcu-sidebar-hover) 55%,transparent)}
2396
+ .dcu-wb-collection-label{gap:4px;min-height:32px;font-size:14px;font-weight:600}
2397
+ .dcu-wb-collection-label .dcu-wb-section-caret{width:14px;height:14px;opacity:1;color:var(--dcu-sidebar-primary)}
2398
+ .dcu-wb-collection-label .dcu-wb-section-caret svg{width:14px;height:14px}
2399
+ .dcu-wb-collection-label .dcu-wb-section-caret svg path{stroke-width:2}
2400
+ .dcu-wb-collection-count{flex:none;font-size:12px;font-weight:400;padding-left:8px}
2401
+ .dcu-wb-ungrouped .dcu-wb-collection-label{color:var(--dcu-sidebar-secondary)}
2402
+ .dcu-wb-collection-head:focus-within,.dcu-wb-collection-head:has(.dcu-wb-more[aria-expanded=true]){background:var(--dcu-sidebar-hover)}
2403
+ .dcu-wb-workspace-move-drop>.dcu-wb-collection-head{background:transparent}
2404
+ .dcu-wb-collection-label:focus-visible,.dcu-wb-project-head:focus-visible,.dcu-wb-session:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px;border-radius:6px}
2405
+ @media (pointer:coarse){.dcu-wb-collection-head,.dcu-wb-collection-label{min-height:36px}.dcu-wb-collection-actions{width:28px}.dcu-wb-collection-actions .dcu-wb-more{width:28px;height:28px}}
2406
+ `;
2264
2407
  const WORKSPACE_TREE_STYLE = stylesheet$4 + runningStyles + typographyStyles + collectionLayoutStyles;
2265
2408
  function storage$1() {
2266
2409
  return browserStorage();
@@ -2296,6 +2439,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2296
2439
  const pinnedHostDirtyRef = (0, react.useRef)(false);
2297
2440
  const pinnedHostSkipWriteRef = (0, react.useRef)();
2298
2441
  const pinnedHostWriteRef = (0, react.useRef)(Promise.resolve());
2442
+ const [preferencesFailure, setPreferencesFailure] = (0, react.useState)();
2299
2443
  const workspaceBaselineRef = (0, react.useRef)({
2300
2444
  ready: false,
2301
2445
  validIds: []
@@ -2314,12 +2458,15 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2314
2458
  };
2315
2459
  const pending = pinnedHostWriteRef.current.then(async () => {
2316
2460
  await writeHostWorkspacePreferences(snapshot.pinnedWorkspaceIds, snapshot.workspaceGroups);
2461
+ setPreferencesFailure(void 0);
2317
2462
  if (!pinnedHostSupportsWorkspaceGroupsRef.current) return;
2318
2463
  if (!sameIds(pinnedWorkspaceIdsRef.current, snapshot.pinnedWorkspaceIds) || !sameWorkspaceGroups(workspaceGroupsRef.current, snapshot.workspaceGroups)) return;
2319
2464
  workspaceGroupsPendingHostSyncRef.current = false;
2320
2465
  saveWorkspaceGroupsCache(storage$1(), snapshot.workspaceGroups, false);
2321
2466
  });
2322
- pinnedHostWriteRef.current = pending.catch(() => void 0);
2467
+ pinnedHostWriteRef.current = pending.catch((reason) => {
2468
+ setPreferencesFailure(reason);
2469
+ });
2323
2470
  };
2324
2471
  const setPinnedWorkspaceIds = (update) => {
2325
2472
  pinnedHostDirtyRef.current = true;
@@ -2427,7 +2574,9 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2427
2574
  }
2428
2575
  savePinnedWorkspaceIds(storage$1(), ids);
2429
2576
  if (writeHost) queuePinnedHostWrite(ids, groups);
2430
- }).catch(() => {
2577
+ }).catch((reason) => {
2578
+ if (!alive) return;
2579
+ setPreferencesFailure(reason);
2431
2580
  pinnedHostHydratedRef.current = true;
2432
2581
  });
2433
2582
  return () => {
@@ -2980,7 +3129,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2980
3129
  top: box.top
2981
3130
  }, { immediate: true });
2982
3131
  },
2983
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderClose16, { size: 16 })
3132
+ children: isExpanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpenOutline16, { size: 16 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderClose16, { size: 16 })
2984
3133
  }),
2985
3134
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2986
3135
  className: "dcu-wb-project-title",
@@ -3592,6 +3741,11 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3592
3741
  };
3593
3742
  const treeRef = (0, react.useRef)(null);
3594
3743
  useSectionDisclosureMotion(treeRef);
3744
+ const sorting = workspaceDragId !== void 0 || workspaceGroupDragId !== void 0 || sessionDrag !== void 0;
3745
+ (0, react.useLayoutEffect)(() => {
3746
+ if (!wide || !sorting || treeRef.current === null) return;
3747
+ return mountWorkspaceDropIndicator(treeRef.current);
3748
+ }, [wide, sorting]);
3595
3749
  if (!wide) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3596
3750
  className: "dcu-wb dcu-wb-rail",
3597
3751
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: stylesheet$4 })
@@ -3606,6 +3760,11 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3606
3760
  typographyStyles,
3607
3761
  collectionLayoutStyles
3608
3762
  ] }),
3763
+ preferencesFailure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3764
+ className: "dcu-wb-error",
3765
+ role: "status",
3766
+ children: [t("workspace.preferencesLocalOnly"), businessRequestErrorKey(preferencesFailure) !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" ", userErrorText(preferencesFailure, t)] })]
3767
+ }),
3609
3768
  error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3610
3769
  className: "dcu-wb-error",
3611
3770
  role: "alert",
@@ -6139,7 +6298,7 @@ html[data-dcu-official-turn-navigator-supported=true] .dcu-turn-navigator,html:h
6139
6298
  "linking",
6140
6299
  "building"
6141
6300
  ]);
6142
- const endpoint = "/api/michengai/codex-ui/dependencies";
6301
+ const endpoint = CODEX_UI_API_ENDPOINTS.dependencies;
6143
6302
  const stylesheet$2 = `
6144
6303
  .dcu-about{color:var(--dsw-alias-label-primary)}.dcu-about h2{margin:0;font-size:20px;line-height:28px}.dcu-about-intro{margin:6px 0 22px;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}.dcu-about h3{margin:24px 0 8px;font-size:14px;line-height:20px}.dcu-about-features{margin:0;padding-left:20px;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:24px}.dcu-about-dependencies-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:24px}.dcu-about-dependencies-heading h3{margin:0}.dcu-about-dependencies-heading+.dcu-about-intro{margin-bottom:14px}.dcu-about-dependencies{overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:10px}.dcu-about-dependency{display:flex;align-items:center;gap:12px;min-height:64px;padding:12px;border-bottom:1px solid var(--dsw-alias-border-l2)}.dcu-about-dependency:last-child{border-bottom:0}.dcu-about-copy{min-width:0;flex:1}.dcu-about-name{font-size:13px;line-height:20px;font-weight:600}.dcu-about-package{overflow:hidden;margin-top:2px;color:var(--dsw-alias-label-tertiary);font-size:12px;text-overflow:ellipsis;white-space:nowrap}.dcu-about-status{display:flex;align-items:center;gap:5px;font-size:12px}.dcu-about-status[data-installed=true]{color:var(--dsw-alias-state-success-primary)}.dcu-about-status[data-installed=false]{color:var(--dsw-alias-state-error-primary)}.dcu-about-status[data-update=true]{color:var(--dsw-alias-state-warning-primary)}.dcu-about-install{display:inline-flex;align-items:center;justify-content:center;gap:5px;min-height:32px;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;padding:6px 9px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;cursor:pointer}.dcu-about-install:hover:not(:disabled){background:var(--dsw-specific-menu-item-hover)}.dcu-about-install:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}.dcu-about-install:disabled{cursor:wait;opacity:.65}.dcu-about-update-all{min-width:92px}.dcu-about-message{margin:10px 0 0;border-radius:8px;padding:9px 10px;background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,transparent);color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}.dcu-about-message[data-error=true]{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent);color:var(--dsw-alias-state-error-primary)}.dcu-about-progress{display:grid;grid-template-columns:auto 1fr auto;gap:8px 10px;align-items:center;margin-top:10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:9px 10px}.dcu-about-progress code{min-width:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dcu-about-progress-pct{color:var(--dsw-alias-label-tertiary);font-size:12px;font-variant-numeric:tabular-nums}.dcu-about-progress-bar{grid-column:1/-1;height:4px;overflow:hidden;border-radius:99px;background:var(--dsw-alias-border-l2)}.dcu-about-progress-fill{height:100%;background:var(--dsw-alias-state-business-primary);transition:width .2s ease}.dcu-about-progress-fill[data-wave=true]{width:28%;animation:dcu-about-progress-wave 1.2s ease-in-out infinite}@keyframes dcu-about-progress-wave{0%{transform:translateX(-60%)}100%{transform:translateX(280%)}}
6145
6304
  .dcu-about-title-row{display:flex;align-items:center;gap:8px;min-width:0}.dcu-about-links{display:flex;align-items:center;gap:4px}.dcu-about-external-link{display:inline-flex;align-items:center;gap:5px;min-height:28px;padding:0 8px;color:var(--dsw-alias-label-secondary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;font-size:12px;font-weight:500;line-height:18px;text-decoration:none;white-space:nowrap}.dcu-about-external-link:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.dcu-about-external-link:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}.dcu-about-external-link svg{flex:none}@media(max-width:720px){.dcu-about-title-row{flex-wrap:wrap}}
@@ -6190,6 +6349,7 @@ html[data-dcu-official-turn-navigator-supported=true] .dcu-turn-navigator,html:h
6190
6349
  const [progress, setProgress] = (0, react.useState)();
6191
6350
  const [message, setMessage] = (0, react.useState)();
6192
6351
  const [refreshFailed, setRefreshFailed] = (0, react.useState)(false);
6352
+ const [loadFailure, setLoadFailure] = (0, react.useState)();
6193
6353
  const alive = (0, react.useRef)(true);
6194
6354
  const root = (0, react.useRef)(null);
6195
6355
  const stateRef = (0, react.useRef)("loading");
@@ -6210,14 +6370,17 @@ html[data-dcu-official-turn-navigator-supported=true] .dcu-turn-navigator,html:h
6210
6370
  cache: "no-store",
6211
6371
  signal
6212
6372
  });
6373
+ if (!response.ok) throw new BusinessRequestError(response.status);
6213
6374
  const payload = await response.json();
6214
6375
  if (signal?.aborted || currentRequest !== requestId.current) return;
6215
- if (!response.ok || !Array.isArray(payload.dependencies) || !payload.dependencies.every(isDependencyStatus)) throw new Error();
6376
+ if (!Array.isArray(payload.dependencies) || !payload.dependencies.every(isDependencyStatus)) throw new Error();
6216
6377
  setDependencies(payload.dependencies);
6217
6378
  setState("ready");
6218
6379
  setRefreshFailed(false);
6380
+ setLoadFailure(void 0);
6219
6381
  } catch (error) {
6220
6382
  if (signal?.aborted || error instanceof DOMException && error.name === "AbortError" || currentRequest !== requestId.current) return;
6383
+ setLoadFailure(error);
6221
6384
  if (stateRef.current === "ready") setRefreshFailed(true);
6222
6385
  else setState("failed");
6223
6386
  }
@@ -6282,6 +6445,11 @@ html[data-dcu-official-turn-navigator-supported=true] .dcu-turn-navigator,html:h
6282
6445
  setMessage(void 0);
6283
6446
  try {
6284
6447
  const response = await fetch(`${endpoint}?dependency=${encodeURIComponent(id)}`, { method: "POST" });
6448
+ if ([
6449
+ 401,
6450
+ 403,
6451
+ 503
6452
+ ].includes(response.status)) throw new BusinessRequestError(response.status);
6285
6453
  const payload = await response.json();
6286
6454
  if (!response.ok || !Array.isArray(payload.dependencies) || !payload.dependencies.every(isDependencyStatus)) throw new Error(typeof payload.error === "string" ? payload.error : t("about.installFailed"));
6287
6455
  if (!alive.current) return;
@@ -6310,6 +6478,11 @@ html[data-dcu-official-turn-navigator-supported=true] .dcu-turn-navigator,html:h
6310
6478
  setMessage(void 0);
6311
6479
  try {
6312
6480
  const response = await fetch(`${endpoint}?action=update-all`, { method: "POST" });
6481
+ if ([
6482
+ 401,
6483
+ 403,
6484
+ 503
6485
+ ].includes(response.status)) throw new BusinessRequestError(response.status);
6313
6486
  const payload = await response.json();
6314
6487
  if (!response.ok || !Array.isArray(payload.dependencies) || !payload.dependencies.every(isDependencyStatus)) throw new Error(typeof payload.error === "string" ? payload.error : t("about.installFailed"));
6315
6488
  if (!alive.current) return;
@@ -6405,7 +6578,7 @@ html[data-dcu-official-turn-navigator-supported=true] .dcu-turn-navigator,html:h
6405
6578
  className: "dcu-about-message",
6406
6579
  "data-error": "true",
6407
6580
  role: "alert",
6408
- children: t("about.statusFailed")
6581
+ children: t(businessRequestErrorKey(loadFailure) ?? "about.statusFailed")
6409
6582
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6410
6583
  className: "dcu-about-dependencies",
6411
6584
  "aria-busy": installing !== void 0,
@@ -6468,7 +6641,7 @@ html[data-dcu-official-turn-navigator-supported=true] .dcu-turn-navigator,html:h
6468
6641
  className: "dcu-about-message",
6469
6642
  "data-error": "true",
6470
6643
  role: "status",
6471
- children: t("about.refreshFailed")
6644
+ children: t(businessRequestErrorKey(loadFailure) ?? "about.refreshFailed")
6472
6645
  }),
6473
6646
  message !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6474
6647
  className: "dcu-about-message",
@@ -6560,6 +6733,7 @@ html[data-dcu-official-turn-navigator-supported=true] .dcu-turn-navigator,html:h
6560
6733
  const sessionId = (0, react.useSyncExternalStore)(sessionStore.subscribe, () => sessionStore.getSnapshot().current);
6561
6734
  const [connectors, setConnectors] = (0, react.useState)([]);
6562
6735
  const [state, setState] = (0, react.useState)("loading");
6736
+ const [failure, setFailure] = (0, react.useState)();
6563
6737
  (0, react.useEffect)(() => {
6564
6738
  if (sessionId === void 0) {
6565
6739
  setConnectors([]);
@@ -6568,15 +6742,18 @@ html[data-dcu-official-turn-navigator-supported=true] .dcu-turn-navigator,html:h
6568
6742
  }
6569
6743
  const controller = new AbortController();
6570
6744
  setState("loading");
6571
- fetch(`/api/michengai/codex-ui/connectors?sessionId=${encodeURIComponent(sessionId)}`, { signal: controller.signal }).then(async (response) => {
6572
- if (!response.ok) throw new Error("连接器目录暂不可用。");
6745
+ fetch(`${CODEX_UI_API_ENDPOINTS.connectors}?sessionId=${encodeURIComponent(sessionId)}`, { signal: controller.signal }).then(async (response) => {
6746
+ if (!response.ok) throw new BusinessRequestError(response.status);
6573
6747
  const payload = await response.json();
6574
6748
  if (!Array.isArray(payload.connectors) || !payload.connectors.every(isConnector)) throw new Error("连接器目录返回格式无效。");
6575
6749
  if (!controller.signal.aborted) setConnectors(payload.connectors);
6576
6750
  }).then(() => {
6577
6751
  if (!controller.signal.aborted) setState("ready");
6578
- }).catch(() => {
6579
- if (!controller.signal.aborted) setState("failed");
6752
+ }).catch((error) => {
6753
+ if (!controller.signal.aborted) {
6754
+ setFailure(error);
6755
+ setState("failed");
6756
+ }
6580
6757
  });
6581
6758
  return () => {
6582
6759
  controller.abort();
@@ -6592,7 +6769,8 @@ html[data-dcu-official-turn-navigator-supported=true] .dcu-turn-navigator,html:h
6592
6769
  });
6593
6770
  if (state === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6594
6771
  className: "dcu-connector-empty",
6595
- children: t("connectors.failed")
6772
+ role: "alert",
6773
+ children: t(businessRequestErrorKey(failure) ?? "connectors.failed")
6596
6774
  });
6597
6775
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6598
6776
  className: "dcu-connector-list",
@@ -6722,7 +6900,7 @@ html[data-dcu-official-turn-navigator-supported=true] .dcu-turn-navigator,html:h
6722
6900
  "workspace.renameGroup": "重命名分组",
6723
6901
  "workspace.recent": "最近",
6724
6902
  "workspace.recentEmpty": "无聊天",
6725
- "workspace.noChat": "没有聊天",
6903
+ "workspace.noChat": "暂无聊天",
6726
6904
  "workspace.empty": "暂无项目",
6727
6905
  "workspace.newSession": "新建会话",
6728
6906
  "workspace.rename": "重命名项目",
@@ -6770,6 +6948,8 @@ html[data-dcu-official-turn-navigator-supported=true] .dcu-turn-navigator,html:h
6770
6948
  "sessions.openPath": "在资源管理器中打开",
6771
6949
  "sessions.moveWorkspace": "项目",
6772
6950
  "sessions.moveUnavailable": "会话移动服务暂不可用,请重启 DSH 后重试。",
6951
+ "sessions.moveUnauthorized": "登录状态已失效,请刷新页面并重新登录。",
6952
+ "sessions.moveForbidden": "当前访问地址不受信任,或请求来源已被拒绝。",
6773
6953
  "sessions.moveNotFound": "会话或目标项目已不存在,请刷新后重试。",
6774
6954
  "sessions.moveSubagent": "子代理会话不能移动到其他项目。",
6775
6955
  "sessions.moveBusy": "该会话正在移动,请稍后重试。",
@@ -6859,6 +7039,10 @@ html[data-dcu-official-turn-navigator-supported=true] .dcu-turn-navigator,html:h
6859
7039
  "time.minutes": "{count}分",
6860
7040
  "time.hours": "{count}小时",
6861
7041
  "time.days": "{count}天",
7042
+ "errors.unauthorized": "请先登录 DSH,再重试。",
7043
+ "errors.forbidden": "请求被安全检查拒绝,请从可信的 DSH 地址重试。",
7044
+ "errors.serviceUnavailable": "服务暂不可用,请稍后重试。",
7045
+ "workspace.preferencesLocalOnly": "项目偏好尚未同步,当前使用本地缓存。",
6862
7046
  "errors.generic": "操作未完成,请重试。",
6863
7047
  "errors.groupInvalid": "分组信息无效。",
6864
7048
  "errors.workspaceInvalid": "项目标识无效。",
@@ -6976,6 +7160,8 @@ html[data-dcu-official-turn-navigator-supported=true] .dcu-turn-navigator,html:h
6976
7160
  "sessions.openPath": "Open in file explorer",
6977
7161
  "sessions.moveWorkspace": "Project",
6978
7162
  "sessions.moveUnavailable": "Conversation moving is unavailable. Restart DSH and try again.",
7163
+ "sessions.moveUnauthorized": "Your login session has expired. Refresh the page and sign in again.",
7164
+ "sessions.moveForbidden": "The current address is not trusted, or the request origin was rejected.",
6979
7165
  "sessions.moveNotFound": "The conversation or target project no longer exists. Refresh and try again.",
6980
7166
  "sessions.moveSubagent": "Subagent conversations cannot be moved to another project.",
6981
7167
  "sessions.moveBusy": "This conversation is already being moved. Try again shortly.",
@@ -7065,6 +7251,10 @@ html[data-dcu-official-turn-navigator-supported=true] .dcu-turn-navigator,html:h
7065
7251
  "time.minutes": "{count}m",
7066
7252
  "time.hours": "{count}h",
7067
7253
  "time.days": "{count}d",
7254
+ "errors.unauthorized": "Sign in to DSH, then try again.",
7255
+ "errors.forbidden": "The security check rejected this request. Try again from a trusted DSH address.",
7256
+ "errors.serviceUnavailable": "The service is unavailable. Try again later.",
7257
+ "workspace.preferencesLocalOnly": "Workspace preferences are not synced. Using the local cache.",
7068
7258
  "errors.generic": "The action could not be completed. Try again.",
7069
7259
  "errors.groupInvalid": "The group information is invalid.",
7070
7260
  "errors.workspaceInvalid": "The project identifier is invalid.",
@@ -7074,7 +7264,7 @@ html[data-dcu-official-turn-navigator-supported=true] .dcu-turn-navigator,html:h
7074
7264
  };
7075
7265
  //#endregion
7076
7266
  //#region src/client/host-open-path.ts
7077
- const OPEN_IN_EXPLORER_ENDPOINT = "/api/michengai/codex-ui/open-in-explorer";
7267
+ const OPEN_IN_EXPLORER_ENDPOINT = CODEX_UI_API_ENDPOINTS.openInExplorer;
7078
7268
  /** “在资源管理器中打开”属于明确的系统动作,不应被侧边栏文件预览器接管。 */
7079
7269
  async function openPathInHost(connection, path, fetcher = fetch) {
7080
7270
  let foregroundResponse;
@@ -7086,6 +7276,11 @@ html[data-dcu-official-turn-navigator-supported=true] .dcu-turn-navigator,html:h
7086
7276
  });
7087
7277
  } catch {}
7088
7278
  if (foregroundResponse?.ok) return;
7279
+ if (foregroundResponse !== void 0 && [
7280
+ 401,
7281
+ 403,
7282
+ 503
7283
+ ].includes(foregroundResponse.status)) throw new BusinessRequestError(foregroundResponse.status);
7089
7284
  if (foregroundResponse !== void 0 && foregroundResponse.status !== 404 && foregroundResponse.status !== 501) {
7090
7285
  const payload = await foregroundResponse.json().catch(() => ({}));
7091
7286
  throw new Error(typeof payload.error === "string" ? payload.error : "无法在前台打开资源管理器。");
@@ -7326,7 +7521,7 @@ html[data-dcu-official-turn-navigator-supported=true] .dcu-turn-navigator,html:h
7326
7521
  const OFFICIAL_TURN_MARK_ATTRIBUTE = "data-dcu-official-turn-mark";
7327
7522
  const OFFICIAL_TURN_TOOLTIP_ATTRIBUTE = "data-dcu-official-turn-tooltip";
7328
7523
  const OFFICIAL_TURN_NAVIGATOR_SUPPORTED_ATTRIBUTE = "data-dcu-official-turn-navigator-supported";
7329
- const CAPABILITIES_ENDPOINT = "/api/michengai/codex-ui/dependencies?action=capabilities";
7524
+ const CAPABILITIES_ENDPOINT = `${CODEX_UI_API_ENDPOINTS.dependencies}?action=capabilities`;
7330
7525
  function isOfficialTurnNavigator(element) {
7331
7526
  return element instanceof HTMLElement && element.tagName === "NAV" && element.style.getPropertyValue("--turn-natural-height") !== "" && element.querySelector("button[type=\"button\"][aria-label]") !== null;
7332
7527
  }
package/lib/index.d.mts CHANGED
@@ -4,16 +4,8 @@ type HostRequest = {
4
4
  method?: string;
5
5
  url?: string;
6
6
  headers?: Record<string, string | string[] | undefined>;
7
- socket?: {
8
- remoteAddress?: string;
9
- };
10
7
  [Symbol.asyncIterator]?: () => AsyncIterator<Uint8Array | string>;
11
8
  };
12
- /**
13
- * 依赖安装会改写用户配置并拉起子进程,只允许回环地址上的同源浏览器请求。
14
- * Origin、Host 和 Sec-Fetch-Site 都可被非浏览器客户端伪造,不能单独作为授权依据。
15
- */
16
- declare function crossSiteRequest(request: HostRequest): boolean;
17
9
  /** 把安装错误收成可给浏览器看的文案:我们自己的中文说明保留,带本地路径的底层错误脱敏。 */
18
10
  declare function publicDependencyError(error: unknown): string;
19
11
  declare class RequestBodyTooLargeError extends Error {}
@@ -23,4 +15,4 @@ declare const inject: string[];
23
15
  /** 提供不泄露地址、命令和凭证的连接器目录。 */
24
16
  declare function apply(ctx: Context): void;
25
17
  //#endregion
26
- export { RequestBodyTooLargeError, apply, crossSiteRequest, inject, publicDependencyError, readRequestBody };
18
+ export { RequestBodyTooLargeError, apply, inject, publicDependencyError, readRequestBody };
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 = "/api/michengai/codex-ui/connectors";
1562
- const dependenciesEndpoint = "/api/michengai/codex-ui/dependencies";
1563
- const explorerEndpoint = "/api/michengai/codex-ui/open-in-explorer";
1564
- const preferencesEndpoint = "/api/michengai/codex-ui/preferences";
1565
- const sessionMoveEndpoint = "/api/michengai/codex-ui/session-move";
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, crossSiteRequest, inject, publicDependencyError, readRequestBody };
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.110",
3
+ "version": "0.2.112",
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"
66
67
  },
67
68
  "peerDependencies": {
68
- "@deepseek-ai/cordis": ">=4.0.1 <5.0.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"