dsh-code-ui 1.5.2 → 1.6.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/README.md +2 -0
- package/icon.svg +6 -0
- package/lib/client.js +153 -19
- package/lib/index.js +436 -137
- package/package.json +17 -4
package/README.md
CHANGED
|
@@ -114,6 +114,8 @@ dsh-code-ui/
|
|
|
114
114
|
|
|
115
115
|
| 时间 | 版本 | 修复 / 新增 |
|
|
116
116
|
| --- | --- | --- |
|
|
117
|
+
| 2026-10-10 | v1.6.0 | 里程碑:**契约迁移到 DSH 官方 Typert Remote**——自定义 RPC 通道从 `connection.rpc.handle("/dsh-code-ui")`(0.1.5 起有 Cordis guard 405 bug,靠自挂 webServer 路由兜底)整体迁到 `/api` 通道:Host 侧 `CodeUiService` 经 `bindTypertRemote` + `@Remote` 标记注册并 `ctx.root.reflect.provide` 到宿主 root Context,api-gateway SRC 扫描自动发现(`internal/service` 事件失效缓存,插件后加载有效);浏览器侧 `rpc.call('/api', 'dsh-code-ui/<端点>', { args: { args: payload } })`,33 个端点名/参数/返回值全部不变;handle 调用、自挂路由、Host/Origin 认证门整套删除。同一套代码兼容宿主 0.1.5-rc.x 与 0.2.0-rc.x。配套:peer 增加 `@deepseek-ai/dsh >=0.1.5-rc.1 <0.3.0`(宿主安装门强制校验)、`dsh-typert-protocol`/`dsh-tools` 补声明,`dsh-settings` 范围扩到 `<0.3.0`(旧 `^0.1.x` 范围在 0.2.0 宿主会被拒载);`engines.dsh` 同步声明;`client.inject` 移除新版已不存在的 `dsh-client-runtime`;新增 `icon.svg` + `exports["./icon"]`(插件管理器不再回退默认图);卸载卡 NO_ENDPOINT 识别覆盖 gateway 新错误文案;新增 `tools/typert-smoke.mjs` 冒烟(真实 cordis 4.0.2 + typert-protocol 0.1.5-rc.2 上验证注册全链路 11/11) |
|
|
118
|
+
| 2026-09-18 | v1.5.3 | 新增:**插件自卸载**——配置卡新增「卸载插件」危险区(两击确认防误触,5 秒不点第二下自动回弹)。主路径调官方 CLI(`dsh plugin --profile <p> remove dsh-code-ui`,自动识别三种启动形态:源码入口带 execArgv 再唤起 / POSIX 直装 / Windows .cmd shim 经 cmd.exe 引号包装);CLI 不可用自动退路 pnpm remove(hoist 漂移先 pnpm install 重建再重试);全程实时滚动 pnpm 日志;收尾按**磁盘真相**核对依赖行 / bundles 清单行 / node_modules(#65 幽灵清单保护:文件已删而清单未清时原子补删清单行,tmp+rename,防 profile 起不来)。成功绿字明确告知**需重启 DSH 彻底生效**(重启前插件仍运行,浏览器建议 Ctrl+F5);幂等(已卸载再点显示已是未安装状态);失败红字留档:错误码(PROFILE_NOT_FOUND / PLUGIN_CMD_FAILED / PNPM_FAILED / TIMEOUT / MANIFEST_LOCKED / FILES_STILL_PRESENT)+ 原因 + 逐条手动处理指导。旧宿主无此端点时给出「先升级插件再卸载」的 NO_ENDPOINT 兜底提示 |
|
|
117
119
|
| 2026-09-11 | v1.5.2 | 修复:停靠面板拖拽分界线不再折行——编辑区去掉 min-width 下限,面板标题/按钮/状态栏等文本全部 nowrap + 省略号截断,拖到多窄只截断、不把文字竖排成一列;停靠面板左缘拖到宽度 < 80px 时自动切回 dsh UI(还原拖前宽度,下次展开不残留细条) |
|
|
118
120
|
| 2026-09-11 | v1.5.0 | 里程碑:**引用体系全面升级——行内引用 chip**。引用/翻译后标签不再挂在输入框上方,而是以 chip 直接嵌入输入框句中(`1:文件名:起行-止行`,翻译带 🌐 前缀),点击跳转编辑器、退格即删;`@` 菜单可把引用插入句中任意位置;发送时 chip 原位展开为 `- 代码引用 / - 翻译引用 文件名:行号(路径)` 引用行,正文由 AI 经 `get_referenced_snippets` 读取。实现:注册宿主 InputTrigger 触发源(candidates/onPick/codec)+ ReferenceChipNode 装饰器直插(shell.insertReference,实时取 `conversation.input.shell(sid).snapshot` 的 draft/draftRev);chip ref 自包含 JSON(抗刷新/重启)。配套:**引用一次性消耗**——发送即标记已使用、chip 清空、【引用/备注】留档可查;**允许重复引用**(chip 可见可删,取消 host 去重);**引用编号全链路贯通**——标签/消息指针/引用卡/AI 工具输出同号,对 AI 说「引用N」即指对应条目;【引用/备注】条目右键菜单(复制 / 翻译 / 引用给AI);消息区引用卡 + chip 文件图标/点击跳转;提示条 ✕ 手动关闭兜底;移除「引用消耗模式」配置项(固定一次性)与输入条上方 tag 条(被行内 chip 取代)。工程:发送补丁改挂 shell.submit 必经点(原生输入条回车/按钮走 keyboard.submit,原 inputActions 补丁只覆盖插件输入卡);纯 chip 携带的发送同样消耗 |
|
|
119
121
|
| 2026-09-09 | v1.4.1 | 修复:编辑器**每敲一个字符文本抖动一下**。根因:hljs 就绪后,每次击键高亮层经历「tokenizer 顶上 → 60ms 防抖后换 hljs」**两次全量 innerHTML 重写**,浏览器重写 innerHTML 时 `scrollTop`/`scrollLeft` 被重置为 0——凡滚动过(垂直或水平)的文件,每次击键可见文本都会位移。修复:≤32KB 的文件改为**同步走 hljs**(单帧渲染直接出最终结果,无交换帧、工作量减半),>32KB 保留 60ms 防抖路径;并在每次渲染提交后从 textarea 回同步高亮层与行号滚动位(兜底所有 innerHTML 重写场景)。新增:**编辑器状态按会话记忆**——切换对话时快照当前会话的标签页/激活项并恢复目标会话的(此前 `applyRoot` 换根即清空标签与缓冲,切会话打开的文件全消失);滚动位按文件路径记忆、标签恢复挂载即自动回滚;焦点不恢复,由用户自己点击决定;缓冲/脏标记以绝对路径为键全局保留,切走再切回未保存内容与 ● 状态原样还在,原「有脏文件暂不跟随根」守卫随之退役,根始终跟随新会话;**文件树展开状态同样按会话记忆**——目录展开与已加载子目录随会话快照/恢复(`applyRoot` 仅在 nodes 里没有该根节点时才重建树,同根换会话不再折叠),侧栏不再切一次对话就全部收起 |
|
package/icon.svg
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
2
|
+
<rect x="4" y="4" width="56" height="56" rx="12" fill="#1f6feb"/>
|
|
3
|
+
<path d="M22 20l-8 6 8 6" fill="none" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
<path d="M42 20l8 6-8 6" fill="none" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
|
+
<line x1="36" y1="18" x2="28" y2="46" stroke="#fff" stroke-width="4" stroke-linecap="round"/>
|
|
6
|
+
</svg>
|
package/lib/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Browser half of dsh-code-ui — standard bundle plugin.
|
|
2
2
|
// Hand-written JS, no build. Registered via __ModuleLoader__; host RPC goes
|
|
3
|
-
// through the
|
|
3
|
+
// through the official Typert Remote channel "/api" (namespace dsh-code-ui).
|
|
4
4
|
window.__ModuleLoader__.load({
|
|
5
5
|
id: "dsh-code-ui",
|
|
6
6
|
factory: (require) => {
|
|
@@ -15,7 +15,6 @@ window.__ModuleLoader__.load({
|
|
|
15
15
|
const primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
16
16
|
if (primitives && primitives.IconChevronDownOutline14) IconChevronDown = primitives.IconChevronDownOutline14;
|
|
17
17
|
} catch (e) {}
|
|
18
|
-
const CHANNEL = "/dsh-code-ui";
|
|
19
18
|
|
|
20
19
|
function __apply(ctx) {
|
|
21
20
|
// --- begin original apply body ---
|
|
@@ -26,10 +25,10 @@ window.__ModuleLoader__.load({
|
|
|
26
25
|
// 协议失败不会 reject(只有传输层 HTTP 失败才 throw)。这里统一解包成「裸值」,
|
|
27
26
|
// 协议错误折叠为 { error: message },让下方各调用点沿用自己的 res.error 判断
|
|
28
27
|
// (对齐 dsh-updater 的 res.ok / res.value 消费约定)。
|
|
29
|
-
//
|
|
30
|
-
//
|
|
28
|
+
// 端点名映射:方法只收单个 args 对象(Typert Remote SRC 约定:参数名即 wire 字段名),
|
|
29
|
+
// 所以 payload 再包一层 { args: payload } 作为 wire 字段的值。
|
|
31
30
|
function callHost(endpoint, payload) {
|
|
32
|
-
return rpc.call(
|
|
31
|
+
return rpc.call('/api', 'dsh-code-ui/' + endpoint, { args: payload === undefined ? {} : { args: payload } })
|
|
33
32
|
.then((res) => {
|
|
34
33
|
if (!res || typeof res !== 'object') return { error: 'RPC 返回异常' }
|
|
35
34
|
if (res.ok === false) {
|
|
@@ -45,6 +44,9 @@ window.__ModuleLoader__.load({
|
|
|
45
44
|
const slots = ctx.get('slots');
|
|
46
45
|
if (slots === undefined) return
|
|
47
46
|
const timer = ctx.get('timer')
|
|
47
|
+
// 宿主主题服务(cordis 键 theme,0.1.5-rc.2 起):snapshot.active.colorScheme 是当前
|
|
48
|
+
// 明暗权威,theme/change 事件(上方已监听)驱动重渲染;老宿主缺失时各处回退 body 属性
|
|
49
|
+
const themeService = ctx.get('theme')
|
|
48
50
|
const h = React.createElement
|
|
49
51
|
const vw = (typeof window !== 'undefined' && window.innerWidth) ? window.innerWidth : 1200
|
|
50
52
|
const vh = (typeof window !== 'undefined' && window.innerHeight) ? window.innerHeight : 800
|
|
@@ -254,7 +256,7 @@ window.__ModuleLoader__.load({
|
|
|
254
256
|
.cdex-dock-toggle.active{color:var(--dsw-alias-label-primary,#d4d4d4);border-color:var(--dsw-alias-label-dimmed,#6a6a70);}
|
|
255
257
|
/* dsh 模式(不挂调色板)时的反色按钮与 token 色:
|
|
256
258
|
反色按钮直接用 DSH 的 brand-primary(浅色=黑 / 深色=白),文字色取 bg-base(自动反色);
|
|
257
|
-
token 色由 tokensClass()
|
|
259
|
+
token 色由 tokensClass() 读宿主 theme 服务 snapshot 后挂 .cdex-tokens-light/dark */
|
|
258
260
|
.cdex-workbench,.cdex-modebar{--cdex-accent-bg:var(--dsw-alias-brand-primary,#0f1115);--cdex-accent-fg:var(--dsw-alias-bg-base,#ffffff);}
|
|
259
261
|
.cdex-tokens-light{--cdex-tk-kw:#af00db;--cdex-tk-str:#a31515;--cdex-tk-com:#008000;--cdex-tk-num:#098658;--cdex-tk-fn:#795e26;--cdex-tk-punc:#555555;}
|
|
260
262
|
.cdex-tokens-dark{--cdex-tk-kw:#c586c0;--cdex-tk-str:#ce9178;--cdex-tk-com:#6a9955;--cdex-tk-num:#b5cea8;--cdex-tk-fn:#dcdcaa;--cdex-tk-punc:#808080;}
|
|
@@ -412,12 +414,18 @@ window.__ModuleLoader__.load({
|
|
|
412
414
|
// system 模式用 matchMedia 监听操作系统外观切换,实时换肤。
|
|
413
415
|
let themeMode = 'dsh'
|
|
414
416
|
let mqDispose = null
|
|
415
|
-
let bodyObs = null
|
|
416
417
|
function systemPrefersDark() {
|
|
417
418
|
try { return !!(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) } catch (e) { return true }
|
|
418
419
|
}
|
|
419
|
-
// DSH
|
|
420
|
+
// DSH 自身外观:优先读宿主 theme 服务 snapshot 的 active.colorScheme(契约路径);
|
|
421
|
+
// 服务缺失时回退 body[data-ds-dark-theme](旧版宿主)
|
|
420
422
|
function dshPrefersDark() {
|
|
423
|
+
try {
|
|
424
|
+
const snap = themeService && (typeof themeService.getSnapshot === 'function' ? themeService.getSnapshot() : themeService.snapshot)
|
|
425
|
+
if (snap && snap.active && (snap.active.colorScheme === 'dark' || snap.active.colorScheme === 'light')) {
|
|
426
|
+
return snap.active.colorScheme === 'dark'
|
|
427
|
+
}
|
|
428
|
+
} catch (e) {}
|
|
421
429
|
try { return !!(document.body && document.body.hasAttribute && document.body.hasAttribute('data-ds-dark-theme')) } catch (e) { return true }
|
|
422
430
|
}
|
|
423
431
|
function themeClass() {
|
|
@@ -446,12 +454,7 @@ window.__ModuleLoader__.load({
|
|
|
446
454
|
mqDispose = () => mq.removeListener(onChange)
|
|
447
455
|
}
|
|
448
456
|
}
|
|
449
|
-
// dsh
|
|
450
|
-
if (bodyObs) { try { bodyObs.disconnect() } catch (e) {} bodyObs = null }
|
|
451
|
-
if (themeMode === 'dsh' && typeof MutationObserver !== 'undefined' && document.body) {
|
|
452
|
-
bodyObs = new MutationObserver(() => emit())
|
|
453
|
-
bodyObs.observe(document.body, { attributes: true, attributeFilter: ['data-ds-dark-theme'] })
|
|
454
|
-
}
|
|
457
|
+
// dsh 模式跟随宿主外观:theme/change 事件(§1 已监听)驱动重渲染,无需再观察 body 属性
|
|
455
458
|
} catch (e) {}
|
|
456
459
|
emit()
|
|
457
460
|
}
|
|
@@ -935,6 +938,38 @@ window.__ModuleLoader__.load({
|
|
|
935
938
|
} else { emit() }
|
|
936
939
|
}
|
|
937
940
|
// —— 标签页 / 编辑 / 保存 ——
|
|
941
|
+
// read-file 新契约(dsh ≥ 0.1.7-rc.1 二进制 Remote 结果):res.bytes 为 Uint8Array,
|
|
942
|
+
// 经 multipart 附件通道下发;0.1.5/0.1.6 老宿主无投影,bytes 走 JSON 落成下标对象,
|
|
943
|
+
// 这里重建为 Uint8Array,同一份代码两端兼容。content 字段为更老服务端的兼容退路。
|
|
944
|
+
// 返回 readOnly/reason:截断加载(>4MB 只取前 4MB)或二进制内容都禁写,防写坏磁盘文件。
|
|
945
|
+
function decodeReadFileRes(res) {
|
|
946
|
+
let bytes = res && res.bytes
|
|
947
|
+
if (bytes && !(bytes instanceof Uint8Array)) {
|
|
948
|
+
if (Array.isArray(bytes)) bytes = Uint8Array.from(bytes)
|
|
949
|
+
else if (typeof bytes === 'object') {
|
|
950
|
+
const keys = Object.keys(bytes)
|
|
951
|
+
const u = new Uint8Array(keys.length)
|
|
952
|
+
for (let k = 0; k < keys.length; k++) u[k] = Number(bytes[keys[k]]) & 255
|
|
953
|
+
bytes = u
|
|
954
|
+
} else bytes = null
|
|
955
|
+
}
|
|
956
|
+
let text
|
|
957
|
+
let readOnly = false
|
|
958
|
+
let reason = null
|
|
959
|
+
if (bytes) {
|
|
960
|
+
text = new TextDecoder('utf-8', { fatal: false }).decode(bytes)
|
|
961
|
+
if (res.truncated) {
|
|
962
|
+
readOnly = true
|
|
963
|
+
reason = '文件超过 4MB,已只读加载前 4MB(保存已禁用)'
|
|
964
|
+
} else if (text.indexOf('�') !== -1) {
|
|
965
|
+
readOnly = true
|
|
966
|
+
reason = '二进制文件,仅只读预览(保存已禁用)'
|
|
967
|
+
}
|
|
968
|
+
} else {
|
|
969
|
+
text = res && typeof res.content === 'string' ? res.content : ''
|
|
970
|
+
}
|
|
971
|
+
return { text, readOnly, reason }
|
|
972
|
+
}
|
|
938
973
|
async function openFile(path) {
|
|
939
974
|
const name = baseName(path)
|
|
940
975
|
if (!store.tabs.some((t) => t.path === path)) {
|
|
@@ -949,11 +984,17 @@ window.__ModuleLoader__.load({
|
|
|
949
984
|
emit()
|
|
950
985
|
const res = await callHost('read-file', { path })
|
|
951
986
|
if (res && res.error) { store.error = res.error; emit(); return }
|
|
952
|
-
|
|
953
|
-
store.
|
|
987
|
+
const decoded = decodeReadFileRes(res)
|
|
988
|
+
store.buffers[path] = decoded.text
|
|
989
|
+
store.savedContent[path] = decoded.text
|
|
990
|
+
store.readOnly = store.readOnly || {}
|
|
991
|
+
store.readOnly[path] = decoded.readOnly
|
|
992
|
+
store.readOnlyReason = store.readOnlyReason || {}
|
|
993
|
+
store.readOnlyReason[path] = decoded.reason
|
|
954
994
|
store.loadSeq[path] = (store.loadSeq[path] || 0) + 1
|
|
955
995
|
store.externalChanged[path] = false
|
|
956
996
|
emit()
|
|
997
|
+
if (decoded.reason) showToast(decoded.reason)
|
|
957
998
|
// 通知 host 建 fs.watch:外部修改进事件队列,编辑器 1 秒内自动同步
|
|
958
999
|
callHost('watch-file', { path }).then(() => {})
|
|
959
1000
|
} else {
|
|
@@ -1053,12 +1094,18 @@ window.__ModuleLoader__.load({
|
|
|
1053
1094
|
async function reloadFromFile(path) {
|
|
1054
1095
|
const res = await callHost('read-file', { path })
|
|
1055
1096
|
if (res && res.error) { showToast('读取失败:' + res.error); return }
|
|
1056
|
-
|
|
1057
|
-
store.
|
|
1097
|
+
const decoded = decodeReadFileRes(res)
|
|
1098
|
+
store.buffers[path] = decoded.text
|
|
1099
|
+
store.savedContent[path] = decoded.text
|
|
1058
1100
|
store.dirty[path] = false
|
|
1059
1101
|
store.externalChanged[path] = false
|
|
1102
|
+
store.readOnly = store.readOnly || {}
|
|
1103
|
+
store.readOnly[path] = decoded.readOnly
|
|
1104
|
+
store.readOnlyReason = store.readOnlyReason || {}
|
|
1105
|
+
store.readOnlyReason[path] = decoded.reason
|
|
1060
1106
|
store.loadSeq[path] = (store.loadSeq[path] || 0) + 1
|
|
1061
1107
|
emit()
|
|
1108
|
+
if (decoded.reason) showToast(decoded.reason)
|
|
1062
1109
|
}
|
|
1063
1110
|
// 每秒拉一次 host 的事件队列(fs.watch 真事件;无变化时返回空数组,开销极小)
|
|
1064
1111
|
async function drainFileEvents() {
|
|
@@ -1081,6 +1128,10 @@ window.__ModuleLoader__.load({
|
|
|
1081
1128
|
async function saveFile(path) {
|
|
1082
1129
|
const p = path || store.activePath
|
|
1083
1130
|
if (!p) return
|
|
1131
|
+
if (store.readOnly && store.readOnly[p]) {
|
|
1132
|
+
showToast('只读:' + (store.readOnlyReason[p] || '该文件不可写'))
|
|
1133
|
+
return
|
|
1134
|
+
}
|
|
1084
1135
|
const content = store.buffers[p]
|
|
1085
1136
|
const res = await callHost('write-file', { path: p, content })
|
|
1086
1137
|
if (res && res.error) { showToast('保存失败:' + res.error); return }
|
|
@@ -1829,7 +1880,7 @@ window.__ModuleLoader__.load({
|
|
|
1829
1880
|
if (!n.lineStart) return { path: n.path, language: langOf(n.path), lineStart: null, lineEnd: null, text: '' } // 整文件备注:按整文件引用处理
|
|
1830
1881
|
const res = await callHost('read-file', { path: n.path })
|
|
1831
1882
|
if (!res || res.error) { showToast('读取原文件失败:' + ((res && res.error) || '未知错误')); return null }
|
|
1832
|
-
const lines =
|
|
1883
|
+
const lines = decodeReadFileRes(res).text.split('\n')
|
|
1833
1884
|
const s = Math.max(1, n.lineStart)
|
|
1834
1885
|
const t = Math.min(lines.length, Math.max(s, n.lineEnd || s))
|
|
1835
1886
|
return { path: n.path, language: langOf(n.path), lineStart: s, lineEnd: t, text: lines.slice(s - 1, t).join('\n') }
|
|
@@ -1956,6 +2007,7 @@ window.__ModuleLoader__.load({
|
|
|
1956
2007
|
defaultValue: content,
|
|
1957
2008
|
spellCheck: false,
|
|
1958
2009
|
wrap: 'off',
|
|
2010
|
+
readOnly: !!(st.readOnly && st.readOnly[p]),
|
|
1959
2011
|
ref: (el) => {
|
|
1960
2012
|
if (!el) return
|
|
1961
2013
|
// 仅在真正挂载时恢复滚动(换标签/内容重挂);不抢焦点,光标由用户自己点击决定
|
|
@@ -2007,6 +2059,7 @@ window.__ModuleLoader__.load({
|
|
|
2007
2059
|
h('span', null, lines ? (lines + ' 行') : ''),
|
|
2008
2060
|
h('span', null, '引用 ' + st.references.length),
|
|
2009
2061
|
dirty ? h('span', { className: 'cdex-dirty' }, '● 未保存') : null,
|
|
2062
|
+
st.readOnly && st.readOnly[p] ? h('span', { className: 'cdex-dirty', title: st.readOnlyReason[p] || '' }, '🔒 只读') : null,
|
|
2010
2063
|
)
|
|
2011
2064
|
}
|
|
2012
2065
|
function ContextMenu() {
|
|
@@ -2723,6 +2776,63 @@ window.__ModuleLoader__.load({
|
|
|
2723
2776
|
})
|
|
2724
2777
|
}
|
|
2725
2778
|
|
|
2779
|
+
// —— 危险区:卸载插件(两击确认 → uninstall-self → 轮询 get-uninstall-progress)——
|
|
2780
|
+
const [uninst, setUninst] = React.useState(null)
|
|
2781
|
+
const [uninstBusy, setUninstBusy] = React.useState(false)
|
|
2782
|
+
const [uninstConfirm, setUninstConfirm] = React.useState(false)
|
|
2783
|
+
const uninstPollRef = React.useRef(null)
|
|
2784
|
+
function stopUninstPoll() {
|
|
2785
|
+
if (uninstPollRef.current) { try { uninstPollRef.current() } catch (e) {} uninstPollRef.current = null }
|
|
2786
|
+
}
|
|
2787
|
+
React.useEffect(() => stopUninstPoll, [])
|
|
2788
|
+
function pollUninstall() {
|
|
2789
|
+
callHost('get-uninstall-progress').then((r) => {
|
|
2790
|
+
if (!r || r.error) return
|
|
2791
|
+
setUninst(r)
|
|
2792
|
+
if (r.active) {
|
|
2793
|
+
if (timer && typeof timer.timeout === 'function') {
|
|
2794
|
+
stopUninstPoll()
|
|
2795
|
+
const d = timer.timeout(pollUninstall, 1200)
|
|
2796
|
+
uninstPollRef.current = (typeof d === 'function') ? d : null
|
|
2797
|
+
}
|
|
2798
|
+
} else {
|
|
2799
|
+
stopUninstPoll()
|
|
2800
|
+
setUninstBusy(false)
|
|
2801
|
+
if (r.done && r.ok && !r.alreadyGone) showToast('已从 profile 移除 dsh-code-ui,重启 DSH 后彻底生效')
|
|
2802
|
+
}
|
|
2803
|
+
})
|
|
2804
|
+
}
|
|
2805
|
+
function startUninstall() {
|
|
2806
|
+
if (uninstBusy) return
|
|
2807
|
+
setUninstBusy(true)
|
|
2808
|
+
setUninst({ active: true, done: false, phase: 'starting', lines: [] })
|
|
2809
|
+
callHost('uninstall-self').then((r) => {
|
|
2810
|
+
if (!r || r.error) {
|
|
2811
|
+
const msg = (r && r.error) || '无法启动卸载'
|
|
2812
|
+
const noEp = /未知端点|no active Remote method exports|invalid RPC target|invocation-unavailable/.test(msg)
|
|
2813
|
+
setUninst({ active: false, done: true, ok: false, code: noEp ? 'NO_ENDPOINT' : 'START_FAILED',
|
|
2814
|
+
error: msg,
|
|
2815
|
+
hint: noEp ? '运行中的 host 端是旧版本(无自卸载端点)。请先用「检查更新」升级插件(或本地包装新版),重启 DSH 后再卸载。' : '',
|
|
2816
|
+
manualSteps: noEp
|
|
2817
|
+
? ['先用本卡片的更新功能升级到 v1.5.3+,重启 DSH', '或手动卸载:dsh plugin --profile web remove dsh-code-ui']
|
|
2818
|
+
: ['稍后重试;反复失败请重启 DSH 后再试'] })
|
|
2819
|
+
setUninstBusy(false)
|
|
2820
|
+
return
|
|
2821
|
+
}
|
|
2822
|
+
pollUninstall()
|
|
2823
|
+
})
|
|
2824
|
+
}
|
|
2825
|
+
function onUninstallClick() {
|
|
2826
|
+
if (uninstBusy) return
|
|
2827
|
+
if (!uninstConfirm) {
|
|
2828
|
+
setUninstConfirm(true)
|
|
2829
|
+
setTimeout(() => setUninstConfirm(false), 5000) // 5 秒不点第二下自动回弹
|
|
2830
|
+
return
|
|
2831
|
+
}
|
|
2832
|
+
setUninstConfirm(false)
|
|
2833
|
+
startUninstall()
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2726
2836
|
const cur = (self && self.version) || (chk && chk.current) || '?'
|
|
2727
2837
|
const started = !!(run && !run.done)
|
|
2728
2838
|
// 更新已成功、等待重启:更新类按钮文案临时改为「需重启 DSH 生效」,用户一眼可见
|
|
@@ -2882,6 +2992,30 @@ window.__ModuleLoader__.load({
|
|
|
2882
2992
|
h('button', { type: 'button', className: 'cdx-pc-btn cdx-pc-btn-primary', onClick: () => { if (lastPayloadRef.current) startUpdate(lastPayloadRef.current) } }, '重试'),
|
|
2883
2993
|
),
|
|
2884
2994
|
) : null,
|
|
2995
|
+
// 危险区:卸载插件
|
|
2996
|
+
h('div', { className: 'cdx-pc-field' },
|
|
2997
|
+
h('div', { className: 'cdx-pc-head' },
|
|
2998
|
+
h('span', { className: 'cdx-pc-label' }, '卸载插件'),
|
|
2999
|
+
h('button', {
|
|
3000
|
+
type: 'button',
|
|
3001
|
+
className: 'cdx-pc-btn ' + (uninstConfirm ? 'cdx-pc-btn-primary' : 'cdx-pc-btn-outline'),
|
|
3002
|
+
disabled: uninstBusy || !!(uninst && uninst.done && uninst.ok),
|
|
3003
|
+
onClick: onUninstallClick,
|
|
3004
|
+
}, uninstBusy ? '正在卸载…'
|
|
3005
|
+
: (uninst && uninst.done && uninst.ok) ? '已卸载 · 重启 DSH 生效'
|
|
3006
|
+
: uninstConfirm ? '确认卸载(再点一次执行)' : '卸载插件…'),
|
|
3007
|
+
),
|
|
3008
|
+
h('p', { className: 'cdx-pc-note' }, '从当前 profile 移除 dsh-code-ui(依赖 + bundles 清单 + node_modules)。卸载完成后插件在本进程内仍运行到重启为止——需重启 DSH 彻底生效。'),
|
|
3009
|
+
uninst && ((uninst.lines || []).length || uninst.active) ? h('div', { className: 'cdx-pc-log' }, ((uninst.lines || []).slice(-14).join('\n')) || '正在启动…') : null,
|
|
3010
|
+
uninst && uninst.done && uninst.ok && uninst.alreadyGone ? h('p', { className: 'cdx-pc-note' }, '当前已是未安装状态(profile 中无此插件)。') : null,
|
|
3011
|
+
uninst && uninst.done && uninst.ok && !uninst.alreadyGone ? h('p', { className: 'cdx-pc-ok' },
|
|
3012
|
+
'✓ 卸载完成(' + (uninst.cliUsed === 'dsh' ? '官方 CLI' : 'pnpm') + ' 通道' + (uninst.manifestReconciled ? ',清单已按磁盘收尾' : '') + ')。请重启 DSH 彻底生效——重启前插件仍在运行;浏览器端建议 Ctrl+F5。') : null,
|
|
3013
|
+
uninst && uninst.done && !uninst.ok ? [
|
|
3014
|
+
h('p', { className: 'cdx-pc-err' }, '✗ 卸载失败' + (uninst.code ? '(' + uninst.code + ')' : '') + ':' + (uninst.error || '未知错误')),
|
|
3015
|
+
uninst.hint ? h('p', { className: 'cdx-pc-note' }, uninst.hint) : null,
|
|
3016
|
+
Array.isArray(uninst.manualSteps) && uninst.manualSteps.length ? h('div', { className: 'cdx-pc-log' }, '手动处理步骤:\n' + uninst.manualSteps.map((s, i) => (i + 1) + '. ' + s).join('\n')) : null,
|
|
3017
|
+
] : null,
|
|
3018
|
+
),
|
|
2885
3019
|
// 底部信息
|
|
2886
3020
|
h('div', { className: 'cdx-pc-field' },
|
|
2887
3021
|
self && self.profileRoot ? h('p', { className: 'cdx-pc-note' }, 'profile:' + self.profileRoot + ' · 检查源:' + (self.updateRegistry || 'https://registry.npmjs.org')) : null,
|
package/lib/index.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
// Host half of dsh-code-ui.
|
|
2
2
|
// Standard DeepSeek Harness bundle plugin — hand-written JS, no build step.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
3
|
+
// Exposes the editor UI's file / reference / note operations as a Typert Remote
|
|
4
|
+
// service (namespace dsh-code-ui on the official /api RPC channel), and a model
|
|
5
|
+
// tool get_referenced_snippets.
|
|
5
6
|
|
|
7
|
+
import { Remote, bindTypertRemote } from "@deepseek-ai/dsh-typert-protocol";
|
|
6
8
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
7
9
|
import z from "@deepseek-ai/schemastery";
|
|
8
10
|
import { spawn } from "node:child_process";
|
|
9
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
|
|
10
|
-
import { dirname, isAbsolute, join } from "node:path";
|
|
11
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
12
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
11
13
|
import { fileURLToPath } from "node:url";
|
|
12
14
|
|
|
13
|
-
const CHANNEL = "/dsh-code-ui";
|
|
14
|
-
|
|
15
15
|
// 引用与备注:模块级内存存储,由 RPC 读写。
|
|
16
16
|
const references = [];
|
|
17
17
|
const notes = [];
|
|
@@ -235,7 +235,8 @@ function feedLine(text) {
|
|
|
235
235
|
// pnpm --reporter=ndjson 逐行事件 -> 可读文本。字段名以 pnpm 11 实测为准:
|
|
236
236
|
// progress/fetching-progress 用 packageId,pnpm:stage 用 stage,_dependency_resolved 用 resolution;
|
|
237
237
|
// scope/context/execution-time 等无信息量 debug 事件直接丢弃;非 JSON 行(老版 pnpm)原样保留。
|
|
238
|
-
function makeLineFeeder() {
|
|
238
|
+
function makeLineFeeder(push) {
|
|
239
|
+
const emit = push || feedLine;
|
|
239
240
|
let buffer = "";
|
|
240
241
|
return (chunk) => {
|
|
241
242
|
buffer += chunk;
|
|
@@ -263,11 +264,11 @@ function makeLineFeeder() {
|
|
|
263
264
|
else if (n === "pnpm:summary") t = "[统计] 安装流程结束";
|
|
264
265
|
} catch (_) {}
|
|
265
266
|
if (parsed) {
|
|
266
|
-
if (t !== null)
|
|
267
|
+
if (t !== null) emit(t);
|
|
267
268
|
continue;
|
|
268
269
|
}
|
|
269
270
|
}
|
|
270
|
-
|
|
271
|
+
emit(line);
|
|
271
272
|
}
|
|
272
273
|
};
|
|
273
274
|
}
|
|
@@ -383,6 +384,282 @@ async function runSelfUpdate(spec, profileRoot, source) {
|
|
|
383
384
|
updateRun.phase = updateRun.ok ? "done" : "failed";
|
|
384
385
|
}
|
|
385
386
|
|
|
387
|
+
// —— 自卸载(v1.5.3):配置卡「卸载插件」——
|
|
388
|
+
// 官方依据:DSH 0.1.6-alpha.2「插件管理支持运行时卸载,请开发者检查插件加载和卸载逻辑」。
|
|
389
|
+
// 宿主 0.1.5-rc.2 无运行时卸载 → 卸载 = 从 profile 移除依赖 + dsh.profile.bundles 清单 + node_modules,
|
|
390
|
+
// 完成后必须告知用户重启 DSH(当前进程内插件仍运行到重启为止,这是预期行为不是 bug)。
|
|
391
|
+
// 策略:优先官方 CLI(dsh plugin --profile <p> remove <name>,exit 0 时官方 CLI 自己同步清单);
|
|
392
|
+
// CLI 不可用退 pnpm remove + 手动补删 bundles 行;失败按磁盘真相 reconcile
|
|
393
|
+
//(#65 写序保护:pnpm 可能在删完 node_modules 后、写 package.json 前被文件锁打断,留下幽灵清单行
|
|
394
|
+
// 会让整个 profile 下次起不来——所以磁盘已删而清单未清时必须补删清单行)。
|
|
395
|
+
const uninstallRun = {
|
|
396
|
+
active: false, startedAt: 0, phase: "idle", lines: [],
|
|
397
|
+
done: false, ok: false, timedOut: false, exitCode: null,
|
|
398
|
+
alreadyGone: false, cliUsed: null, manifestReconciled: false, filesGone: false,
|
|
399
|
+
error: null, code: null, hint: null, manualSteps: null,
|
|
400
|
+
};
|
|
401
|
+
function uFeedLine(text) {
|
|
402
|
+
uninstallRun.lines.push(String(text).slice(0, 300));
|
|
403
|
+
if (uninstallRun.lines.length > 150) uninstallRun.lines.shift();
|
|
404
|
+
uninstallRun.phase = String(text).slice(0, 160);
|
|
405
|
+
}
|
|
406
|
+
function pnpmRemove(profileRoot, extraArgs) {
|
|
407
|
+
return new Promise((resolveP) => {
|
|
408
|
+
const workspace = existsSync(join(profileRoot, "pnpm-workspace.yaml"));
|
|
409
|
+
const args = ["remove"].concat(workspace ? ["-w"] : [], [SELF_NAME, "--reporter=ndjson"], extraArgs);
|
|
410
|
+
uFeedLine("$ pnpm " + args.join(" "));
|
|
411
|
+
const stdio = ["ignore", "pipe", "pipe"];
|
|
412
|
+
let child;
|
|
413
|
+
if (!WIN_CMD) {
|
|
414
|
+
child = spawn("pnpm", args, { cwd: profileRoot, env: selfSpawnEnv(), stdio, shell: false });
|
|
415
|
+
} else {
|
|
416
|
+
const cmdline = ["pnpm"].concat(args).map(quoteCmdArg).join(" ");
|
|
417
|
+
child = spawn(COMSPEC, ["/d", "/s", "/c", '"' + cmdline + '"'], {
|
|
418
|
+
cwd: profileRoot, env: selfSpawnEnv(), stdio, shell: false, windowsVerbatimArguments: true,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
let timedOut = false;
|
|
422
|
+
const timer = setTimeout(() => { timedOut = true; killTree(child); }, 10 * 60 * 1000);
|
|
423
|
+
const feed = makeLineFeeder(uFeedLine);
|
|
424
|
+
if (child.stdout) child.stdout.on("data", feed);
|
|
425
|
+
if (child.stderr) child.stderr.on("data", feed);
|
|
426
|
+
child.on("error", (err) => {
|
|
427
|
+
clearTimeout(timer);
|
|
428
|
+
uFeedLine("[spawn-error] " + err.message);
|
|
429
|
+
resolveP({ ok: false, exitCode: 127, timedOut: false });
|
|
430
|
+
});
|
|
431
|
+
child.on("close", (code) => {
|
|
432
|
+
clearTimeout(timer);
|
|
433
|
+
resolveP({ ok: code === 0 && !timedOut, exitCode: code == null ? -1 : code, timedOut });
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
// 再唤起启动本宿主的 CLI 执行官方卸载。三种启动形态(照 dshmarket dshArgv 实战):
|
|
438
|
+
// ① 源码/本地入口启动(pnpm dsh web 等,argv[1] 指向 bin)→ node <execArgv> <entry> plugin --profile ...
|
|
439
|
+
// (必须带 execArgv 且 cwd=入口目录,否则 tsx/esm 导入解析不到,MODULE_NOT_FOUND)
|
|
440
|
+
// ② POSIX 直装 → 裸 dsh
|
|
441
|
+
// ③ Windows 直装 → dsh 是 .cmd shim,必须 cmd.exe /d /s /c + 逐 token 引号(同更新器 pnpm 套路)
|
|
442
|
+
function dshPluginRemove(profileName) {
|
|
443
|
+
return new Promise((resolveP) => {
|
|
444
|
+
const pluginArgs = ["plugin", "--profile", profileName, "remove", SELF_NAME];
|
|
445
|
+
const entry = process.argv[1];
|
|
446
|
+
let child;
|
|
447
|
+
if (entry && /[\\/](?:bin\.(?:js|ts)|dsh)$/.test(entry)) {
|
|
448
|
+
uFeedLine("$ node " + basename(entry) + " " + pluginArgs.join(" "));
|
|
449
|
+
child = spawn(process.execPath, process.execArgv.concat([resolve(entry)], pluginArgs), {
|
|
450
|
+
cwd: dirname(entry), env: selfSpawnEnv(), stdio: ["ignore", "pipe", "pipe"], shell: false,
|
|
451
|
+
});
|
|
452
|
+
} else if (!WIN_CMD) {
|
|
453
|
+
uFeedLine("$ dsh " + pluginArgs.join(" "));
|
|
454
|
+
child = spawn("dsh", pluginArgs, { env: selfSpawnEnv(), stdio: ["ignore", "pipe", "pipe"], shell: false });
|
|
455
|
+
} else {
|
|
456
|
+
const cmdline = ["dsh"].concat(pluginArgs).map(quoteCmdArg).join(" ");
|
|
457
|
+
uFeedLine("$ dsh " + pluginArgs.join(" ") + " (windows cmd shim)");
|
|
458
|
+
child = spawn(COMSPEC, ["/d", "/s", "/c", '"' + cmdline + '"'], {
|
|
459
|
+
env: selfSpawnEnv(), stdio: ["ignore", "pipe", "pipe"], shell: false, windowsVerbatimArguments: true,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
let timedOut = false;
|
|
463
|
+
let out = "";
|
|
464
|
+
const collect = (chunk) => { out = (out + chunk.toString()).slice(-8192); };
|
|
465
|
+
const timer = setTimeout(() => { timedOut = true; killTree(child); }, 10 * 60 * 1000);
|
|
466
|
+
const feed = makeLineFeeder(uFeedLine);
|
|
467
|
+
if (child.stdout) child.stdout.on("data", (c) => { collect(c); feed(c); });
|
|
468
|
+
if (child.stderr) child.stderr.on("data", (c) => { collect(c); feed(c); });
|
|
469
|
+
child.on("error", (err) => {
|
|
470
|
+
clearTimeout(timer);
|
|
471
|
+
uFeedLine("[spawn-error] " + err.message);
|
|
472
|
+
resolveP({ ok: false, exitCode: 127, timedOut: false, output: (out + "\n" + err.message).slice(-8192) });
|
|
473
|
+
});
|
|
474
|
+
child.on("close", (code) => {
|
|
475
|
+
clearTimeout(timer);
|
|
476
|
+
resolveP({ ok: code === 0 && !timedOut, exitCode: code == null ? -1 : code, timedOut, output: out });
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
// 磁盘真相:dep 有无 / bundles 有无 / node_modules 有无
|
|
481
|
+
function manifestUninstallState(profileRoot) {
|
|
482
|
+
const m = readJsonSafe(join(profileRoot, "package.json"));
|
|
483
|
+
const depPresent = !!(m && m.dependencies && Object.prototype.hasOwnProperty.call(m.dependencies, SELF_NAME));
|
|
484
|
+
const bundles = m && m.dsh && m.dsh.profile && Array.isArray(m.dsh.profile.bundles) ? m.dsh.profile.bundles : null;
|
|
485
|
+
const bundlePresent = !!bundles && bundles.indexOf(SELF_NAME) >= 0;
|
|
486
|
+
const filesPresent = existsSync(join(profileRoot, "node_modules", SELF_NAME, "package.json"));
|
|
487
|
+
return { manifestOk: !!m, depPresent: depPresent, bundlePresent: bundlePresent, filesPresent: filesPresent };
|
|
488
|
+
}
|
|
489
|
+
// 清单收尾(原子写:tmp + rename;Windows 目标被锁 → "locked")。dropDep=false 只删 bundles 行。
|
|
490
|
+
function dropManifestRows(profileRoot, dropDep) {
|
|
491
|
+
const file = join(profileRoot, "package.json");
|
|
492
|
+
const m = readJsonSafe(file);
|
|
493
|
+
if (!m) return "unreadable";
|
|
494
|
+
let touched = false;
|
|
495
|
+
if (dropDep && m.dependencies && m.dependencies[SELF_NAME] !== undefined) {
|
|
496
|
+
delete m.dependencies[SELF_NAME];
|
|
497
|
+
touched = true;
|
|
498
|
+
}
|
|
499
|
+
const bundles = m.dsh && m.dsh.profile && Array.isArray(m.dsh.profile.bundles) ? m.dsh.profile.bundles : null;
|
|
500
|
+
if (bundles && bundles.indexOf(SELF_NAME) >= 0) {
|
|
501
|
+
m.dsh.profile.bundles = bundles.filter((b) => b !== SELF_NAME);
|
|
502
|
+
touched = true;
|
|
503
|
+
}
|
|
504
|
+
if (!touched) return "clean";
|
|
505
|
+
try {
|
|
506
|
+
const tmp = file + ".code-ui-uninstall.tmp";
|
|
507
|
+
writeFileSync(tmp, JSON.stringify(m, null, 2) + "\n", "utf8");
|
|
508
|
+
renameSync(tmp, file);
|
|
509
|
+
return "removed";
|
|
510
|
+
} catch (_) {
|
|
511
|
+
return "locked";
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// 每个错误码对应可执行的手动处理步骤(用户照做就能完成卸载)
|
|
515
|
+
function manualStepsFor(code, profileName) {
|
|
516
|
+
const cli = "dsh plugin --profile " + profileName + " remove " + SELF_NAME;
|
|
517
|
+
const table = {
|
|
518
|
+
PROFILE_NOT_FOUND: [
|
|
519
|
+
"确认 DSH_HOME 位置(默认 ~/.dsh,Windows 为 C:\\Users\\<你>\\.dsh)",
|
|
520
|
+
"检查 profiles/web/package.json 是否存在且含 dsh.profile.bundles",
|
|
521
|
+
],
|
|
522
|
+
PLUGIN_CMD_FAILED: [
|
|
523
|
+
"关闭 DSH(文件被运行中的进程占用是最常见原因)",
|
|
524
|
+
"终端执行:" + cli,
|
|
525
|
+
"仍失败则进入 profile 目录执行:pnpm remove " + SELF_NAME,
|
|
526
|
+
"完成后重启 DSH",
|
|
527
|
+
],
|
|
528
|
+
PNPM_FAILED: [
|
|
529
|
+
"关闭 DSH(文件占用是最常见原因)",
|
|
530
|
+
"进入 profile 目录执行:pnpm remove " + SELF_NAME,
|
|
531
|
+
"若报 EPERM/EBUSY:结束所有 node 进程或重启电脑后重试",
|
|
532
|
+
"完成后重启 DSH",
|
|
533
|
+
],
|
|
534
|
+
TIMEOUT: [
|
|
535
|
+
"pnpm 卡住(锁/网络),稍后重试一次",
|
|
536
|
+
"仍卡住:删除 profile/node_modules/.pnpm 下的锁残留后重试",
|
|
537
|
+
"或手动执行:" + cli,
|
|
538
|
+
],
|
|
539
|
+
MANIFEST_LOCKED: [
|
|
540
|
+
"关闭 DSH",
|
|
541
|
+
"手动编辑 profile/package.json:删除 dependencies 里的 \"" + SELF_NAME + "\" 行,以及 dsh.profile.bundles 数组里的 \"" + SELF_NAME + "\" 项",
|
|
542
|
+
"进入 profile 目录执行:pnpm install",
|
|
543
|
+
"重启 DSH",
|
|
544
|
+
],
|
|
545
|
+
FILES_STILL_PRESENT: [
|
|
546
|
+
"清单已清但文件仍在(被运行中进程占用):关闭 DSH",
|
|
547
|
+
"手动删除 node_modules 下的 " + SELF_NAME + " 目录(及 node_modules/.pnpm 内对应目录)",
|
|
548
|
+
"或重启 DSH 后再卸载一次",
|
|
549
|
+
],
|
|
550
|
+
};
|
|
551
|
+
return table[code] || ["终端执行:" + cli, "或进入 profile 目录执行:pnpm remove " + SELF_NAME, "完成后重启 DSH"];
|
|
552
|
+
}
|
|
553
|
+
async function runSelfUninstall(profileRoot) {
|
|
554
|
+
const profileName = basename(profileRoot);
|
|
555
|
+
Object.assign(uninstallRun, {
|
|
556
|
+
active: true, startedAt: Date.now(), phase: "starting", lines: [],
|
|
557
|
+
done: false, ok: false, timedOut: false, exitCode: null,
|
|
558
|
+
alreadyGone: false, cliUsed: null, manifestReconciled: false, filesGone: false,
|
|
559
|
+
error: null, code: null, hint: null, manualSteps: null,
|
|
560
|
+
});
|
|
561
|
+
const verdict = (okFlag, code, message, hint) => {
|
|
562
|
+
uninstallRun.ok = okFlag;
|
|
563
|
+
if (code) {
|
|
564
|
+
uninstallRun.code = code;
|
|
565
|
+
uninstallRun.error = message || "";
|
|
566
|
+
uninstallRun.hint = hint || "";
|
|
567
|
+
uninstallRun.manualSteps = manualStepsFor(code, profileName);
|
|
568
|
+
}
|
|
569
|
+
uninstallRun.done = true;
|
|
570
|
+
uninstallRun.active = false;
|
|
571
|
+
uninstallRun.phase = okFlag ? "done" : "failed";
|
|
572
|
+
uFeedLine(okFlag ? "[完成] 已从 profile 移除,重启 DSH 后彻底生效" : "[失败] " + code + " " + (message || ""));
|
|
573
|
+
};
|
|
574
|
+
try {
|
|
575
|
+
let st = manifestUninstallState(profileRoot);
|
|
576
|
+
if (!st.manifestOk) return verdict(false, "PROFILE_NOT_FOUND", "profile package.json 不可读", "profile 目录异常");
|
|
577
|
+
if (!st.depPresent && !st.bundlePresent && !st.filesPresent) {
|
|
578
|
+
uninstallRun.alreadyGone = true;
|
|
579
|
+
uFeedLine("[code-ui] profile 中已无本插件,无需卸载");
|
|
580
|
+
return verdict(true, null, null);
|
|
581
|
+
}
|
|
582
|
+
// 主路径:官方 CLI(exit 0 时官方自己同步 dependencies + bundles)
|
|
583
|
+
let cli = await dshPluginRemove(profileName);
|
|
584
|
+
uninstallRun.exitCode = cli.exitCode;
|
|
585
|
+
uninstallRun.timedOut = cli.timedOut;
|
|
586
|
+
if (cli.ok) {
|
|
587
|
+
uninstallRun.cliUsed = "dsh";
|
|
588
|
+
} else if (cli.timedOut) {
|
|
589
|
+
return verdict(false, "TIMEOUT", "dsh plugin remove 超时,已终止", "可重试或按手动步骤执行");
|
|
590
|
+
} else if (cli.exitCode === 127 || /not recognized|不是内部或外部命令|command not found|cannot find/i.test(cli.output || "")) {
|
|
591
|
+
// CLI 不可用(拿不到 dsh 命令)→ 退路:pnpm remove
|
|
592
|
+
uFeedLine("[code-ui] dsh CLI 不可用,退路:pnpm remove");
|
|
593
|
+
let r = await pnpmRemove(profileRoot, []);
|
|
594
|
+
let out = uninstallRun.lines.join("\n");
|
|
595
|
+
if (!r.ok && !r.timedOut && /ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF|hoist/i.test(out)) {
|
|
596
|
+
uFeedLine("[code-ui] node_modules 由旧版 pnpm 创建,先重建再重试");
|
|
597
|
+
const rb = await spawnPnpmInstallRebuild(profileRoot);
|
|
598
|
+
if (rb.ok) r = await pnpmRemove(profileRoot, []);
|
|
599
|
+
}
|
|
600
|
+
uninstallRun.exitCode = r.exitCode;
|
|
601
|
+
uninstallRun.timedOut = r.timedOut;
|
|
602
|
+
if (r.ok) {
|
|
603
|
+
uninstallRun.cliUsed = "pnpm";
|
|
604
|
+
} else if (r.timedOut) {
|
|
605
|
+
return verdict(false, "TIMEOUT", "pnpm remove 超时,已终止", "可重试或按手动步骤执行");
|
|
606
|
+
} else {
|
|
607
|
+
return verdict(false, "PNPM_FAILED", "pnpm remove 失败(退出码 " + r.exitCode + ")", "详见输出与手动步骤");
|
|
608
|
+
}
|
|
609
|
+
} else {
|
|
610
|
+
// CLI 在但执行失败:多为文件占用;按磁盘真相收尾,能救则救
|
|
611
|
+
uFeedLine("[code-ui] CLI 退出码 " + cli.exitCode + ",按磁盘真相收尾");
|
|
612
|
+
}
|
|
613
|
+
// 收尾:按磁盘真相 reconcile(#65 幽灵清单保护)
|
|
614
|
+
st = manifestUninstallState(profileRoot);
|
|
615
|
+
if (!st.filesPresent && (st.depPresent || st.bundlePresent)) {
|
|
616
|
+
const r = dropManifestRows(profileRoot, st.depPresent);
|
|
617
|
+
uninstallRun.manifestReconciled = r === "removed";
|
|
618
|
+
if (r === "locked") return verdict(false, "MANIFEST_LOCKED", "清单写入失败(文件被占用)", "按手动步骤编辑 package.json");
|
|
619
|
+
if (r === "unreadable") return verdict(false, "MANIFEST_LOCKED", "package.json 不可读", "按手动步骤检查 profile");
|
|
620
|
+
st = manifestUninstallState(profileRoot);
|
|
621
|
+
} else if (st.filesPresent && uninstallRun.cliUsed === "pnpm") {
|
|
622
|
+
// pnpm 路径成功但 bundles 行只有官方 CLI 会同步——pnpm 路径需手动删 bundles 行
|
|
623
|
+
//(注意此时文件应已删除;若文件还在说明 pnpm 实际没删掉,走 FILES_STILL_PRESENT 判定)
|
|
624
|
+
const r = dropManifestRows(profileRoot, false);
|
|
625
|
+
uninstallRun.manifestReconciled = r === "removed";
|
|
626
|
+
}
|
|
627
|
+
st = manifestUninstallState(profileRoot);
|
|
628
|
+
uninstallRun.filesGone = !st.filesPresent;
|
|
629
|
+
if (!st.filesPresent && !st.depPresent && !st.bundlePresent) {
|
|
630
|
+
return verdict(true, null, null);
|
|
631
|
+
}
|
|
632
|
+
if (st.filesPresent) {
|
|
633
|
+
return verdict(false, "FILES_STILL_PRESENT", "插件文件仍在磁盘(多半被运行中的 DSH 占用)", "关闭 DSH 后按手动步骤删除,或重启后再试一次");
|
|
634
|
+
}
|
|
635
|
+
return verdict(false, "MANIFEST_LOCKED", "清单仍有残留", "按手动步骤编辑 package.json");
|
|
636
|
+
} catch (err) {
|
|
637
|
+
return verdict(false, "PLUGIN_CMD_FAILED", String((err && err.message) || err), "执行异常");
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// hoist 漂移重建(node_modules 由旧版 pnpm 创建时):pnpm install --no-frozen-lockfile(尽力而为)
|
|
641
|
+
function spawnPnpmInstallRebuild(profileRoot) {
|
|
642
|
+
return new Promise((resolveP) => {
|
|
643
|
+
const args = ["install", "--no-frozen-lockfile"];
|
|
644
|
+
uFeedLine("$ pnpm " + args.join(" ") + " (rebuild node_modules)");
|
|
645
|
+
let child;
|
|
646
|
+
if (!WIN_CMD) {
|
|
647
|
+
child = spawn("pnpm", args, { cwd: profileRoot, env: selfSpawnEnv(), stdio: ["ignore", "pipe", "pipe"], shell: false });
|
|
648
|
+
} else {
|
|
649
|
+
const cmdline = ["pnpm"].concat(args).map(quoteCmdArg).join(" ");
|
|
650
|
+
child = spawn(COMSPEC, ["/d", "/s", "/c", '"' + cmdline + '"'], {
|
|
651
|
+
cwd: profileRoot, env: selfSpawnEnv(), stdio: ["ignore", "pipe", "pipe"], shell: false, windowsVerbatimArguments: true,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
const timer = setTimeout(() => killTree(child), 10 * 60 * 1000);
|
|
655
|
+
const feed = makeLineFeeder(uFeedLine);
|
|
656
|
+
if (child.stdout) child.stdout.on("data", feed);
|
|
657
|
+
if (child.stderr) child.stderr.on("data", feed);
|
|
658
|
+
child.on("error", () => { clearTimeout(timer); resolveP({ ok: false }); });
|
|
659
|
+
child.on("close", (code) => { clearTimeout(timer); resolveP({ ok: code === 0 }); });
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
386
663
|
// —— RPC 处理:文件系统 + 引用/备注 ——
|
|
387
664
|
// -- 文件监听:对 Code UI 已打开的文件建 fs.watch,外部修改记入事件队列 --
|
|
388
665
|
// client 每 1 秒 take-file-events 拉空队列(插件 RPC 是单向请求/响应,没有 host->浏览器推送通道,
|
|
@@ -419,7 +696,48 @@ function unwatchAllFiles() {
|
|
|
419
696
|
}
|
|
420
697
|
|
|
421
698
|
function rpcHandler(ctx) {
|
|
422
|
-
|
|
699
|
+
// fs 服务延迟获取:插件 apply 可能早于 base bundle 的 fs-sandbox 行挂载,
|
|
700
|
+
// apply 时一次性 ctx.get("fs") 会永久取到 undefined(0.2.0-rc.2 实测:
|
|
701
|
+
// list-dir/stat-path/read-file 全部抛 "Cannot read properties of
|
|
702
|
+
// undefined (reading 'resolve')")。改为 inject 等待 + 调用时解析。
|
|
703
|
+
let fsSvc = null;
|
|
704
|
+
try { fsSvc = ctx.get("fs") || null; } catch (_) {}
|
|
705
|
+
try {
|
|
706
|
+
ctx.inject(["fs"], (c) => { fsSvc = c.fs || fsSvc; });
|
|
707
|
+
} catch (_) {}
|
|
708
|
+
// 宿主组合始终无 fs 服务时的原生兜底:与 fs-mkdir/write-file 等磁盘端点
|
|
709
|
+
// 同一权限模型(编辑器是用户显式操作面,本就读写真实磁盘)。
|
|
710
|
+
const nativeFsShim = {
|
|
711
|
+
resolve: async (p) => p,
|
|
712
|
+
stat: async (p) => {
|
|
713
|
+
try {
|
|
714
|
+
const s = statSync(p);
|
|
715
|
+
return { type: s.isDirectory() ? "directory" : s.isFile() ? "file" : "other" };
|
|
716
|
+
} catch (_) { return null; }
|
|
717
|
+
},
|
|
718
|
+
listDir: async (p) => {
|
|
719
|
+
const sep = String(p).includes("\\") ? "\\" : "/";
|
|
720
|
+
return readdirSync(p, { withFileTypes: true }).map((d) => ({
|
|
721
|
+
name: d.name,
|
|
722
|
+
path: String(p).replace(/[\\/]+$/, "") + sep + d.name,
|
|
723
|
+
isDir: d.isDirectory(),
|
|
724
|
+
}));
|
|
725
|
+
},
|
|
726
|
+
readBytes: async (p, _signal, maxBytes) => {
|
|
727
|
+
const b = readFileSync(p);
|
|
728
|
+
if (b.length > maxBytes) {
|
|
729
|
+
const e = new Error("file too large");
|
|
730
|
+
e.code = "FS_TOO_LARGE";
|
|
731
|
+
throw e;
|
|
732
|
+
}
|
|
733
|
+
return new Uint8Array(b.buffer, b.byteOffset, b.byteLength);
|
|
734
|
+
},
|
|
735
|
+
readByteRange: async (p, range) => {
|
|
736
|
+
const b = readFileSync(p);
|
|
737
|
+
return new Uint8Array(b.buffer, b.byteOffset, b.byteLength).slice(range.offset, range.offset + range.length);
|
|
738
|
+
},
|
|
739
|
+
};
|
|
740
|
+
const getFs = () => fsSvc || nativeFsShim;
|
|
423
741
|
let currentRootRef = detectWorkspaceRoot(ctx); // 最近一次解析的根:list-dir 的兜底
|
|
424
742
|
// 每会话根目录记忆:sessionId -> 手动设置的根(「以此文件夹为根」)。
|
|
425
743
|
// 按会话隔离——手动根只影响设置它的那个会话,不再全局污染其它会话的自动解析;
|
|
@@ -515,8 +833,9 @@ function rpcHandler(ctx) {
|
|
|
515
833
|
if (endpoint === "list-dir") {
|
|
516
834
|
const path = args.path || currentRootRef;
|
|
517
835
|
if (!path) return ok({ error: "no-root" });
|
|
518
|
-
const
|
|
519
|
-
const
|
|
836
|
+
const fsvc = getFs();
|
|
837
|
+
const target = await fsvc.resolve(path);
|
|
838
|
+
const entries = await fsvc.listDir(target);
|
|
520
839
|
const sep = String(path).includes("\\") ? "\\" : "/";
|
|
521
840
|
const list = entries.map((e) => {
|
|
522
841
|
let name = null;
|
|
@@ -543,8 +862,9 @@ function rpcHandler(ctx) {
|
|
|
543
862
|
|
|
544
863
|
if (endpoint === "stat-path") {
|
|
545
864
|
if (!args.path) return ok({ error: "no-path" });
|
|
546
|
-
const
|
|
547
|
-
const
|
|
865
|
+
const fsvc = getFs();
|
|
866
|
+
const target = await fsvc.resolve(args.path);
|
|
867
|
+
const info = await fsvc.stat(target);
|
|
548
868
|
// DSH fs.stat 返回 FsInfo { type: 'file' | 'directory' | 'other' },
|
|
549
869
|
// 没有 isDirectory/isFile 布尔字段;必须按 type 判定(原实现恒为 false,
|
|
550
870
|
// 导致目录路径被误报为「不存在」)。保留遗留字段兼容。
|
|
@@ -556,11 +876,22 @@ function rpcHandler(ctx) {
|
|
|
556
876
|
|
|
557
877
|
if (endpoint === "read-file") {
|
|
558
878
|
if (!args.path) return ok({ error: "no-path" });
|
|
559
|
-
const
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
879
|
+
const fsvc = getFs();
|
|
880
|
+
const target = await fsvc.resolve(args.path);
|
|
881
|
+
// 对齐 dsh ≥ 0.1.7-rc.1 的二进制 Remote 结果契约:字节经 Uint8Array 字段返回,
|
|
882
|
+
// gateway 自动投影为 multipart 附件下发;0.1.5/0.1.6 老宿主无投影则走 JSON
|
|
883
|
+
// (Uint8Array 落成下标对象,客户端负责重建),同一份代码两端兼容。
|
|
884
|
+
// 取代旧的 60 万字符文本截断——截断内容若被 Ctrl+S 原样写回会写坏大文件。
|
|
885
|
+
const MAX = 4 * 1024 * 1024; // 单次读取上限 4MB
|
|
886
|
+
try {
|
|
887
|
+
const bytes = await fsvc.readBytes(target, undefined, MAX);
|
|
888
|
+
return ok({ path: args.path, bytes, truncated: false, size: bytes.length });
|
|
889
|
+
} catch (err) {
|
|
890
|
+
if (!err || err.code !== "FS_TOO_LARGE" || typeof fsvc.readByteRange !== "function") throw err;
|
|
891
|
+
// 超限文件读前 4MB,标记 truncated;客户端据此禁写并提示
|
|
892
|
+
const bytes = await fsvc.readByteRange(target, { offset: 0, length: MAX });
|
|
893
|
+
return ok({ path: args.path, bytes, truncated: true });
|
|
894
|
+
}
|
|
564
895
|
}
|
|
565
896
|
|
|
566
897
|
if (endpoint === "write-file") {
|
|
@@ -894,6 +1225,28 @@ function rpcHandler(ctx) {
|
|
|
894
1225
|
});
|
|
895
1226
|
}
|
|
896
1227
|
|
|
1228
|
+
if (endpoint === "uninstall-self") {
|
|
1229
|
+
if (uninstallRun.active) return ok({ error: "卸载任务已在进行中" });
|
|
1230
|
+
const profileRoot = findProfileRoot();
|
|
1231
|
+
if (!profileRoot) {
|
|
1232
|
+
return ok({ ok: false, code: "PROFILE_NOT_FOUND", error: "未找到 profile 根目录(package.json 缺少 dsh.profile.bundles)", manualSteps: manualStepsFor("PROFILE_NOT_FOUND", "web") });
|
|
1233
|
+
}
|
|
1234
|
+
runSelfUninstall(profileRoot); // 异步执行;进度经 get-uninstall-progress 轮询(与更新同一模式)
|
|
1235
|
+
return ok({ started: true });
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
if (endpoint === "get-uninstall-progress") {
|
|
1239
|
+
return ok({
|
|
1240
|
+
active: uninstallRun.active, done: uninstallRun.done, ok: uninstallRun.ok,
|
|
1241
|
+
phase: uninstallRun.phase, alreadyGone: uninstallRun.alreadyGone,
|
|
1242
|
+
cliUsed: uninstallRun.cliUsed, manifestReconciled: uninstallRun.manifestReconciled,
|
|
1243
|
+
filesGone: uninstallRun.filesGone,
|
|
1244
|
+
error: uninstallRun.error, code: uninstallRun.code, hint: uninstallRun.hint,
|
|
1245
|
+
manualSteps: uninstallRun.manualSteps,
|
|
1246
|
+
lines: uninstallRun.lines.slice(-40),
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
|
|
897
1250
|
return fail("未知端点: " + endpoint);
|
|
898
1251
|
} catch (err) {
|
|
899
1252
|
return fail(String((err && err.message) || err));
|
|
@@ -908,6 +1261,57 @@ function fail(message) {
|
|
|
908
1261
|
return { ok: false, error: { code: "internal", message, details: {} } };
|
|
909
1262
|
}
|
|
910
1263
|
|
|
1264
|
+
// —— Typert Remote 服务:经官方 /api 通道对浏览器暴露上列全部端点 ——
|
|
1265
|
+
// api-gateway 扫描宿主 root Context 的 reflect.props,发现带 typertRemote 绑定且原型上有
|
|
1266
|
+
// Remote 方法标记的服务(SRC 发现模式,0.1.5-rc.2 与 0.2.0-rc.2 机制一致),把
|
|
1267
|
+
// 「dsh-code-ui/<端点>」分发到对应方法;gateway 监听 internal/service 事件做缓存失效,
|
|
1268
|
+
// 插件晚于 gateway 加载也能被发现。方法签名约束(SRC 派生):参数必须是简单标识符、
|
|
1269
|
+
// 不能解构/默认值/rest,参数名即 wire 字段名;客户端 call('/api', ep, { args: {...} }) 的
|
|
1270
|
+
// 内层对象按参数名取字段。本服务所有方法只收单个 args 对象,逐个委托给 rpcHandler 闭包,
|
|
1271
|
+
// 端点逻辑零改动。
|
|
1272
|
+
function applyRemote(cls, methodName, exportName) {
|
|
1273
|
+
// 无装饰器语法的手动调用:Remote(exportName) 返回标准方法装饰器,
|
|
1274
|
+
// addInitializer 里以 cls.prototype 为原型造对象,让标记写到类原型上。
|
|
1275
|
+
Remote(exportName)(cls.prototype[methodName], {
|
|
1276
|
+
kind: "method", name: methodName, static: false, private: false,
|
|
1277
|
+
addInitializer(fn) { fn.call(Object.create(cls.prototype)); },
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
class CodeUiService {
|
|
1282
|
+
constructor(invoke) {
|
|
1283
|
+
this.typertRemote = bindTypertRemote(this, "dsh-code-ui");
|
|
1284
|
+
this.__invoke = invoke;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// 端点清单(kebab-case,与 lib/client.js 调用点、rpcHandler 分支一一对应)
|
|
1289
|
+
const CODE_UI_ENDPOINTS = [
|
|
1290
|
+
"get-root", "set-root", "list-dir", "stat-path",
|
|
1291
|
+
"read-file", "write-file", "watch-file", "unwatch-file", "take-file-events",
|
|
1292
|
+
"fs-mkdir", "fs-createfile", "fs-rename", "fs-delete", "fs-reveal",
|
|
1293
|
+
"add-reference", "list-references", "use-references", "delete-reference",
|
|
1294
|
+
"add-note", "list-notes", "delete-note",
|
|
1295
|
+
"get-self", "set-toggle", "set-uimode", "set-layout", "set-theme",
|
|
1296
|
+
"set-syn-theme", "set-enabled",
|
|
1297
|
+
"check-update", "perform-update", "get-update-progress",
|
|
1298
|
+
"uninstall-self", "get-uninstall-progress",
|
|
1299
|
+
];
|
|
1300
|
+
for (const endpoint of CODE_UI_ENDPOINTS) {
|
|
1301
|
+
const method = endpoint.replace(/-([a-z])/g, (_, ch) => ch.toUpperCase());
|
|
1302
|
+
// 委托约定:rpcHandler 返回 RpcResult 封套;ok:false 抛出 Error 让 gateway 转成
|
|
1303
|
+
// { ok: false, error: { code: 'gateway/internal', message } },客户端 callHost 折叠为
|
|
1304
|
+
// { error: message }(与 handler 内 fail() 走到客户端的错误形态一致);ok:true 解出 value 原样返回。
|
|
1305
|
+
CodeUiService.prototype[method] = async function (args) {
|
|
1306
|
+
const r = await this.__invoke(endpoint, args && typeof args === "object" ? args : {});
|
|
1307
|
+
if (r && typeof r === "object" && r.ok === false) {
|
|
1308
|
+
throw new Error((r.error && (r.error.message || r.error.code)) || "RPC 调用失败");
|
|
1309
|
+
}
|
|
1310
|
+
return r && typeof r === "object" && "value" in r ? r.value : r;
|
|
1311
|
+
};
|
|
1312
|
+
applyRemote(CodeUiService, method, endpoint);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
911
1315
|
// —— 模型工具:读取用户标记的代码片段 ——
|
|
912
1316
|
function registerSnippetsTool(tools) {
|
|
913
1317
|
if (!tools || typeof tools.register !== "function") return;
|
|
@@ -952,69 +1356,6 @@ function registerSnippetsTool(tools) {
|
|
|
952
1356
|
export const name = "dsh-code-ui";
|
|
953
1357
|
export const inject = ["tools"];
|
|
954
1358
|
|
|
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
|
-
|
|
1018
1359
|
export function apply(ctx) {
|
|
1019
1360
|
// 注册 settings 命名空间(插件配置页按命名空间分发卡片;内部自带 ctx.inject(['settings']),
|
|
1020
1361
|
// 无 settings 服务的宿主上静默不运行,不影响其它功能)
|
|
@@ -1029,61 +1370,19 @@ export function apply(ctx) {
|
|
|
1029
1370
|
registerSnippetsTool(c.tools);
|
|
1030
1371
|
});
|
|
1031
1372
|
|
|
1032
|
-
//
|
|
1033
|
-
//
|
|
1034
|
-
//
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
if (connection === undefined) return;
|
|
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
|
-
}
|
|
1085
|
-
c.effect(() => () => unwatchAllFiles(), "dsh-code-ui: file watchers");
|
|
1086
|
-
});
|
|
1373
|
+
// 注册 Typert Remote 服务到宿主 root Context(api-gateway 只扫描它所在 Context 的
|
|
1374
|
+
// reflect.props;root 与 gateway 同层,0.1.5-rc.2 / 0.2.0-rc.2 一致)。服务生命周期
|
|
1375
|
+
// 挂到本插件 fiber:卸载时自动从 root 摘除,gateway 经 internal/service 事件失效缓存。
|
|
1376
|
+
const invoke = rpcHandler(ctx);
|
|
1377
|
+
ctx.effect(() => {
|
|
1378
|
+
const service = new CodeUiService(invoke);
|
|
1379
|
+
const dispose = ctx.root.reflect.provide("dsh-code-ui", service);
|
|
1380
|
+
console.log("[dsh-code-ui] Typert Remote 服务已注册(/api 通道):dsh-code-ui");
|
|
1381
|
+
return () => { Promise.resolve(dispose()).catch(() => {}); };
|
|
1382
|
+
}, "dsh-code-ui: typert remote service");
|
|
1383
|
+
|
|
1384
|
+
// 文件监听随插件卸载清理
|
|
1385
|
+
ctx.effect(() => () => unwatchAllFiles(), "dsh-code-ui: file watchers");
|
|
1087
1386
|
}
|
|
1088
1387
|
|
|
1089
1388
|
export default apply;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-code-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "Cursor 风格的 AI 代码编辑器工作台插件(DSH):文件树、多标签页编辑、语法高亮、引用/翻译行内标签给 AI、右侧停靠/悬浮窗口双布局",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -20,21 +20,25 @@
|
|
|
20
20
|
"default": "./lib/client.js"
|
|
21
21
|
},
|
|
22
22
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
23
|
+
"./icon": "./icon.svg",
|
|
23
24
|
"./package.json": "./package.json"
|
|
24
25
|
},
|
|
25
26
|
"files": [
|
|
26
27
|
"lib",
|
|
27
28
|
"cordis.patch.yml",
|
|
29
|
+
"icon.svg",
|
|
28
30
|
"README.md",
|
|
29
31
|
"LICENSE"
|
|
30
32
|
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"dsh": ">=0.1.5-rc.1 <0.3.0"
|
|
35
|
+
},
|
|
31
36
|
"dsh": {
|
|
32
37
|
"bundle": {
|
|
33
38
|
"patch": "./cordis.patch.yml"
|
|
34
39
|
},
|
|
35
40
|
"client": {
|
|
36
41
|
"inject": [
|
|
37
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
38
42
|
"@deepseek-ai/dsh-client-ui-slots",
|
|
39
43
|
"@deepseek-ai/dsh-client-ui-conversation",
|
|
40
44
|
"@deepseek-ai/dsh-client-ui-settings",
|
|
@@ -45,10 +49,19 @@
|
|
|
45
49
|
},
|
|
46
50
|
"peerDependencies": {
|
|
47
51
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
48
|
-
"@deepseek-ai/dsh
|
|
52
|
+
"@deepseek-ai/dsh": ">=0.1.5-rc.1 <0.3.0",
|
|
53
|
+
"@deepseek-ai/dsh-typert-protocol": ">=0.1.5-rc.1 <0.3.0",
|
|
54
|
+
"@deepseek-ai/dsh-tools": ">=0.1.5-rc.1 <0.3.0",
|
|
55
|
+
"@deepseek-ai/dsh-settings": ">=0.1.0-rc.7 <0.3.0",
|
|
49
56
|
"react": "^18.2.0"
|
|
50
57
|
},
|
|
51
58
|
"peerDependenciesMeta": {
|
|
59
|
+
"@deepseek-ai/dsh-typert-protocol": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"@deepseek-ai/dsh-tools": {
|
|
63
|
+
"optional": true
|
|
64
|
+
},
|
|
52
65
|
"@deepseek-ai/dsh-settings": {
|
|
53
66
|
"optional": true
|
|
54
67
|
}
|
|
@@ -57,7 +70,7 @@
|
|
|
57
70
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
58
71
|
},
|
|
59
72
|
"devDependencies": {
|
|
60
|
-
"@deepseek-ai/dsh-
|
|
73
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.2.0-rc.2",
|
|
61
74
|
"@types/node": "^26.2.0",
|
|
62
75
|
"@types/react": "~18.3.1",
|
|
63
76
|
"react": "^18.3.1",
|