dsh-vscode-mode 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/lib/client.debug.js +934 -0
- package/lib/client.debug.js.map +1 -0
- package/lib/client.js +848 -1676
- package/lib/client.js.map +1 -1
- package/lib/client.rules.js +793 -0
- package/lib/client.rules.js.map +1 -0
- package/lib/index.js +236 -42
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +9 -0
- package/src/client/lazyPanel.ts +68 -0
- package/src/client/sidebar/PanelBoundary.ts +57 -0
- package/src/client/sidebar/SidebarView.ts +58 -21
- package/src/client/sidebar/panels/FileExplorer.ts +80 -27
- package/src/client/sidebar/panels/SvnPanel.ts +24 -8
- package/src/client/sidebar/panels/index.ts +3 -4
- package/src/client/sidebar/panels/treeWindow.ts +106 -0
- package/src/client/sidebar/panels/useExplorerReveal.ts +5 -2
- package/src/client/sidebar/registry.ts +4 -1
- package/src/client/styles/editor.css +8 -0
- package/src/client/textPages.ts +135 -0
- package/src/client/ui/EditorView.ts +71 -11
- package/src/client/ui/LargeTextView.ts +94 -0
- package/src/rpc.ts +29 -7
- package/src/shared/rpc.ts +3 -1
- package/src/shared/textPages.ts +28 -0
- package/src/textPages.ts +149 -0
- package/src/treeIndex.ts +62 -37
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.rules.js","names":["React"],"sources":["../src/client/paths.ts","../src/client/rulesMdc.ts","../src/client/sidebar/panels/RulesPanel.ts"],"sourcesContent":["/**\n * dsh-vscode-mode client — 浏览器端缓存键/路径统一出口(PathConst)。\n * 统一 localStorage 键前缀(各缓存模块/编辑区/搜索面板共用),避免内联散落;\n * 键带 schema 版本号,未来版本递增时经 sweepLegacyKeys 自动清旧。\n * 浏览器不涉 ~/.dsh 文件系统,这里只管键名 + shared 路由常量。\n * 作者 ddj 2026-09-01\n */\n\n/** localStorage 键前缀表(按功能隔离;与 scopeStore 作用域键拼接:工作区优先,无 cwd 回退会话)。 */\nexport const CACHE_KEY = {\n /** 展开状态(explorerCache v1,当前使用)。 */\n expanded: 'edrv.cache.explorer.v1.',\n /** 目录条目 SWR 缓存(explorerEntriesCache v2,当前使用)。 */\n entries: 'edrv.cache.entries.v2.',\n /** 编辑区视图状态(viewStateCache v1)。 */\n viewstate: 'edrv.cache.viewstate.v1.',\n /** 编辑器页签/活动文件(EditorView v3:页签带 pinned 固定标记)。 */\n editor: 'edrv.editor.v3.',\n /** Multi-group split tree and floating window geometry. */\n editorLayout: 'edrv.editorLayout.v1.',\n /** 旧编辑器页签键(v2:页签为纯路径数组)——仅用于一次性回读迁移,不再写入。 */\n editorLegacy: 'edrv.editor.v2.',\n /** 侧边栏状态(EditorView;按布局 side/central 追加段)。 */\n sidebar: 'edrv.sidebar.',\n /** 搜索面板条件(SearchPanel v1)。 */\n search: 'edrv.search.v1.',\n /** 侧边栏提示已关闭标记(全局,无作用域)。 */\n sideHint: 'edrv.side-hint-dismissed',\n /** 「性能优化」页工作区栏目展开状态(workspaceFoldCache v1,全局不按会话)。 */\n workspaceFold: 'edrv.ws-fold.v1.',\n /** 规则面板 UI 状态(RulesPanel v1:Tab 与项目工作区选择,全局不按会话)。 */\n rules: 'edrv.rules.v1.',\n /** SVN 变更面板 UI 状态(SvnPanel v1:未版本控制/忽略项显示开关,按作用域隔离)。 */\n svn: 'edrv.svn.v1.',\n /** Markdown 预览态(EditorView v1:处于预览态的 Markdown 路径集合,按作用域隔离)。 */\n mdPreview: 'edrv.md-preview.v1.',\n /** 官方侧边栏「文件编辑」Tab 展开/激活态(officialSidebar v1:重启后自动回到编辑 Tab)。 */\n editorTab: 'edrv.editor-tab.v1.',\n /** 编辑器导航历史双栈(navStateCache v1:重启后前进/后退与当前位置仍在,按作用域隔离)。 */\n navHistory: 'edrv.nav-history.v1.',\n} as const\n\n/** 按前缀拼作用域键(scopeStore 产出作用域;侧边栏/编辑器等需要布局段时自行拼接后传入)。 */\nexport function cacheKey(prefix: string, sessionId: string): string {\n return prefix + sessionId\n}\n\n/** 旧版本键清理入口:未来前缀版本递增时在此登记旧前缀并清空(当前无实际旧键)。 */\nexport function sweepLegacyKeys(): void {\n // 例:entries v2 取代 v1 时 → localStorage.removeItem('edrv.cache.entries.v1.' + sid)\n // 当前各功能均为现役版本,无待清旧键。\n}\n","/**\n * dsh-vscode-mode client — 规则 frontmatter 纯函数(解析 + 按类型改写)。\n * 与 host parseRuleMdc/ruleTypeOf 语义对齐的浏览器端实现:编辑器类型下拉与\n * 原始 .mdc 文本域双向同步共用;纯字符串运算,禁 node/react 导入。\n * 作者 ddj 2026年09月07号\n */\n\n/** 规则生效类型(与 shared/rules.ts RuleType 一致,此处独立声明避免引入 host 依赖)。 */\nexport type RuleFmType = 'always' | 'auto' | 'manual'\n\n/** 一条 .mdc 文本的解析结果(改写 frontmatter 所需的全部上下文)。 */\nexport interface RuleFmParsed {\n type: RuleFmType\n description: string\n alwaysApply: boolean\n globs: string[]\n enabled: boolean\n /** 是否存在规则 frontmatter(首行 --- 且有闭合)。 */\n hasFm: boolean\n /** 闭合 --- 之后的正文(按 \\n 归一,与 host 解析一致)。 */\n body: string\n /** 已知键之外的 frontmatter 原行(如 updatedAt),改写时原样保留。 */\n fmExtra: string[]\n /** 原文是否带 BOM(改写后回填)。 */\n bom: boolean\n /** 原文 frontmatter 是否显式写了 enabled 行(改写时保持该行存在与否)。 */\n hasEnabledLine: boolean\n}\n\n/** 类型改写入参:下拉/描述/globs 控件的最新值。 */\nexport interface RuleMetaNext {\n type: RuleFmType\n description: string\n globs: string[]\n}\n\n/**\n * 去除值两侧成对引号(与 host stripQuotes 一致)。\n * @author ddj 2026年09月07号\n * @param raw 原始值\n * @returns 去引号后的值\n */\nfunction stripQuotes(raw: string): string {\n const value = raw.trim()\n if (value.length >= 2 && ((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\")))) {\n return value.slice(1, -1)\n }\n return value\n}\n\n// --region 解析\n/**\n * 解析 .mdc 全文:首行 --- 且 100 行内有闭合 --- 才视为 frontmatter;\n * 已知键 description/alwaysApply/globs/enabled,其余原行进 fmExtra;永不抛错。\n * @author ddj 2026年09月07号\n * @param text 文件全文\n * @returns 解析结果\n */\nexport function parseRuleFm(text: string): RuleFmParsed {\n const bom = text.startsWith('\\uFEFF')\n const lines = text.replace(/^\\uFEFF/, '').split(/\\r?\\n/)\n const none: RuleFmParsed = { type: 'manual', description: '', alwaysApply: false, globs: [], enabled: true, hasFm: false, body: text.replace(/^\\uFEFF/, ''), fmExtra: [], bom, hasEnabledLine: false }\n if (lines[0]?.trim() !== '---') return none\n let close = -1\n for (let i = 1; i < lines.length && i <= 101; i++) {\n if (lines[i].trim() === '---') { close = i; break }\n }\n if (close < 0) return none\n const parsed: RuleFmParsed = { type: 'manual', description: '', alwaysApply: false, globs: [], enabled: true, hasFm: true, body: lines.slice(close + 1).join('\\n'), fmExtra: [], bom, hasEnabledLine: false }\n for (let i = 1; i < close; i++) {\n const match = /^([A-Za-z_-]+)[ \\t]*:[ \\t]?(.*)$/.exec(lines[i])\n if (!match) { parsed.fmExtra.push(lines[i]); continue }\n const [, key, raw] = match\n if (key === 'description') parsed.description = stripQuotes(raw)\n else if (key === 'alwaysApply') parsed.alwaysApply = stripQuotes(raw).toLowerCase() === 'true'\n else if (key === 'enabled') { parsed.enabled = stripQuotes(raw).toLowerCase() !== 'false'; parsed.hasEnabledLine = true }\n else if (key === 'globs') parsed.globs = parseGlobsValue(raw, lines, i, close)\n else parsed.fmExtra.push(lines[i])\n }\n parsed.type = fmTypeOf(parsed)\n return parsed\n}\n\n/**\n * 解析 globs 值:内联逗号分隔串,或空值后接 YAML 短横列表(与 host parseGlobs 一致)。\n * @author ddj 2026年09月07号\n * @param value 内联值\n * @param lines 全文行\n * @param start globs 行下标\n * @param close 闭合 --- 下标\n * @returns glob 数组\n */\nfunction parseGlobsValue(value: string, lines: string[], start: number, close: number): string[] {\n const inline = stripQuotes(value)\n if (inline) return inline.split(',').map((item) => stripQuotes(item)).filter(Boolean)\n const list: string[] = []\n for (let j = start + 1; j < close; j++) {\n const item = /^[ \\t]*-[ \\t]*(.+)$/.exec(lines[j])\n if (!item) break\n list.push(stripQuotes(item[1]))\n }\n return list\n}\n\n/**\n * 由 frontmatter 推导类型:alwaysApply=true → 总是;有 globs → 自动;否则手动。\n * @author ddj 2026年09月07号\n * @param parsed 解析结果\n * @returns 规则类型\n */\nfunction fmTypeOf(parsed: RuleFmParsed): RuleFmType {\n if (parsed.alwaysApply) return 'always'\n if (parsed.globs.length) return 'auto'\n return 'manual'\n}\n// --endregion\n\n// --region 改写\n/**\n * 按最新控件值重写 frontmatter:known 键固定顺序 + fmExtra + 闭合 --- + 原 body;\n * 无 frontmatter 且目标为手动时:有描述则新建仅含描述的 frontmatter,否则原文返回。\n * @author ddj 2026年09月07号\n * @param parsed parseRuleFm 的结果(必须来自同一文本)\n * @param next 控件最新值\n * @returns 重写后的全文\n */\nexport function applyRuleMeta(parsed: RuleFmParsed, next: RuleMetaNext): string {\n if (!parsed.hasFm && next.type === 'manual') {\n if (!next.description.trim()) return (parsed.bom ? '\\uFEFF' : '') + parsed.body\n return (parsed.bom ? '\\uFEFF' : '') + ['---', 'description: ' + next.description.trim(), '---'].join('\\n') + '\\n' + parsed.body\n }\n const lines: string[] = ['---']\n lines.push('description: ' + next.description.trim())\n if (next.type === 'always') lines.push('alwaysApply: true')\n if (next.type === 'auto') lines.push('globs: ' + next.globs.join(', '))\n if (parsed.hasEnabledLine) lines.push('enabled: ' + (parsed.enabled ? 'true' : 'false'))\n lines.push(...parsed.fmExtra)\n lines.push('---')\n return (parsed.bom ? '\\uFEFF' : '') + lines.join('\\n') + '\\n' + parsed.body\n}\n// --endregion\n","// @ts-nocheck\n/**\n * dsh-vscode-mode client — 侧边栏「规则」面板(Codebuddy 规则管理形态)。\n * 双 Tab:用户规则(~/.dsh/rules/)与项目规则(<工作区>/.dsh/rules/);\n * 列表行 = 文件名 + 相对提示,第二行类型徽标与描述,右侧 编辑/删除 图标 + 滑动启用开关;\n * 新建/编辑 = 顶部面包屑 + 规则类型下拉(总是/自动/手动)+ 描述/globs 控件,与原始 .mdc\n * 文本域双向同步(控件改写 frontmatter,文本域回读控件),保存走 rules.save。\n * 作者 ddj 2026年09月03号(2026年09月07号 交互性调整)\n */\nimport React from 'react'\nimport { rpc } from '../../rpc.js'\nimport type { SidebarCtx } from '../types.js'\nimport { CACHE_KEY } from '../../paths.js'\nimport { parseRuleFm, applyRuleMeta } from '../../rulesMdc.js'\n\n/** 新建规则模板(frontmatter 字段与 host parseRuleMdc 对齐)。 */\nconst NEW_TEMPLATE = '---\\ndescription: \\nalwaysApply: true\\n---\\n\\n'\nconst TYPE_LABEL = { always: '总是', auto: '自动', manual: '手动' }\n/** 列表行描述的基础显示上限(字符数;宽面板下的封顶值,超出省略号)。 */\nconst DESC_MAX = 24\n/** 动态字数下限(极窄面板的保底可见字符数)。 */\nconst DESC_CAP_MIN = 8\n/** 动态字数估算:描述之外的行内固定占用 px(动作区/类型标签/徽标/间距/省略余量)。 */\nconst DESC_RESERVE_PX = 210\n/** 动态字数估算:单字符平均宽度 px(11px 字号,中英混排)。 */\nconst DESC_CHAR_PX = 10\n/** 窄面板阈值:低于此宽度隐藏路径提示(编辑界面面包屑与悬停仍有完整路径)。 */\nconst NARROW_W = 420\n\n/**\n * 读 localStorage(损坏/不可用安全)。\n * @author ddj 2026年09月03号\n * @param key 键\n * @returns 值或 null\n */\nfunction readLocal(key) {\n try { return localStorage.getItem(key) } catch (e) { return null }\n}\n\n/**\n * 写 localStorage(不可用安全)。\n * @author ddj 2026年09月03号\n * @param key 键\n * @param value 值\n */\nfunction writeLocal(key, value) {\n try { localStorage.setItem(key, value) } catch (e) { /* 忽略 */ }\n}\n\n/**\n * 类型徽标(总是=绿 / 自动=蓝 / 手动=灰)。\n * @author ddj 2026年09月03号\n * @param type 规则类型\n * @returns 徽标元素\n */\nfunction typeBadge(type) {\n return React.createElement('span', { className: 'edrv-rules-type edrv-rules-type-' + type }, TYPE_LABEL[type] ?? type)\n}\n\n/**\n * 按字符上限截断列表行描述,超出以省略号收尾(上限随面板宽度动态变化;\n * 窄面板下 CSS 省略号仍会按像素提前截断,二者取更小)。\n * @author ddj 2026年09月07号\n * @param text 完整描述\n * @param cap 本帧字符上限\n * @returns 截断后的展示文本\n */\nfunction clampDesc(text, cap) {\n const chars = Array.from(String(text ?? '')) // 按码位切分,避免把 emoji 等代理对从中间截断\n const limit = Math.max(4, Number(cap) || DESC_MAX)\n return chars.length > limit ? chars.slice(0, limit).join('') + '…' : chars.join('')\n}\n\n/**\n * 由面板宽度估算描述可用字符数:随宽度动态变化,并与基础上限取小、极窄时保底。\n * @author ddj 2026年09月07号\n * @param panelWidth 面板内容宽度(px)\n * @returns 本帧描述字符上限\n */\nfunction descCapOf(panelWidth) {\n const byWidth = Math.floor((Number(panelWidth) - DESC_RESERVE_PX) / DESC_CHAR_PX)\n return Math.max(DESC_CAP_MIN, Math.min(DESC_MAX, byWidth))\n}\n\n/**\n * 编辑图标(线性铅笔,与 DiffBox chevron 同风格:currentColor 描边装饰图)。\n * @author ddj 2026年09月07号\n * @returns 13×13 装饰性 SVG\n */\nfunction pencilIcon() {\n return React.createElement('svg', {\n width: 13, height: 13, viewBox: '0 0 24 24', fill: 'none', 'aria-hidden': true, focusable: false,\n }, React.createElement('path', {\n d: 'M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z',\n stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',\n }))\n}\n\n/**\n * 删除图标(线性垃圾桶,风格同 pencilIcon)。\n * @author ddj 2026年09月07号\n * @returns 13×13 装饰性 SVG\n */\nfunction trashIcon() {\n return React.createElement('svg', {\n width: 13, height: 13, viewBox: '0 0 24 24', fill: 'none', 'aria-hidden': true, focusable: false,\n }, React.createElement('path', {\n d: 'M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14zM10 11v6M14 11v6',\n stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',\n }))\n}\n\n/**\n * 逗号分隔的 globs 输入串 → 数组(去空项)。\n * @author ddj 2026年09月07号\n * @param text 输入串\n * @returns glob 数组\n */\nfunction globList(text) {\n return String(text ?? '').split(',').map((item) => item.trim()).filter(Boolean)\n}\n\n/**\n * 由控件值重建 .mdc 全文:frontmatter 按类型/描述/globs 重写,正文与其余键不动。\n * @author ddj 2026年09月07号\n * @param base 当前全文\n * @param fmType 目标类型(always/auto/manual)\n * @param desc 描述文本\n * @param globsText globs 输入串\n * @returns 重写后的全文\n */\nfunction rebuildContent(base, fmType, desc, globsText) {\n return applyRuleMeta(parseRuleFm(base), { type: fmType, description: desc, globs: globList(globsText) })\n}\n\n// --region 列表行\n/**\n * 渲染一条规则行:左侧两行(文件名+相对提示 / 类型徽标+描述),右侧 编辑/删除 图标 + 滑动开关。\n * @author ddj 2026年09月03号\n * @param props.rule 规则元信息\n * @param props.onEdit 编辑回调\n * @param props.onRemove 删除回调\n * @param props.onToggle 开关回调\n * @returns 行元素\n */\nfunction RuleRow(props) {\n const rule = props.rule\n const desc = [rule.error ? '解析失败:' + rule.error : null, rule.description || (rule.type === 'auto' ? 'globs: ' + rule.globs.join(', ') : '')]\n .filter(Boolean).join(' · ')\n return React.createElement('div', { className: 'edrv-rules-row' + (rule.enabled ? '' : ' off') },\n React.createElement('div', { className: 'edrv-rules-main', onClick: () => props.onEdit(rule), title: rule.absPath },\n React.createElement('div', { className: 'edrv-rules-line1' },\n React.createElement('span', { className: 'edrv-rules-file' }, rule.file),\n !props.narrow ? React.createElement('span', { className: 'edrv-rules-path' }, '[' + rule.relHint + rule.file + ']') : null,\n props.narrow && rule.scope === 'user' && rule.origin === 'legacy'\n ? React.createElement('span', { className: 'edrv-rules-path', title: rule.absPath }, '旧目录') : null,\n !rule.enabled ? React.createElement('span', { className: 'edrv-rules-offtag' }, '已停用') : null),\n React.createElement('div', { className: 'edrv-rules-sub' },\n React.createElement('span', { className: 'edrv-rules-sublabel' }, '类型:'),\n typeBadge(rule.type),\n desc ? React.createElement('span', { className: 'edrv-rules-desc', title: desc }, clampDesc(desc, props.descCap)) : null)),\n React.createElement('div', { className: 'edrv-rules-actions' },\n React.createElement('button', { className: 'edrv-rules-act', title: '编辑', onClick: () => props.onEdit(rule) }, pencilIcon()),\n React.createElement('button', { className: 'edrv-rules-act', title: '删除', onClick: () => props.onRemove(rule) }, trashIcon()),\n React.createElement('button', {\n className: 'edrv-rules-switch' + (rule.enabled ? ' on' : ''), role: 'switch', 'aria-checked': rule.enabled,\n title: rule.enabled ? '已启用(点击停用)' : '已停用(点击启用)',\n onClick: () => props.onToggle(rule),\n }, React.createElement('span', { className: 'edrv-rules-knob' }))))\n}\n// --endregion\n\n// --region 内嵌编辑器\n/**\n * 新建/编辑表单:面包屑头部 + 规则类型下拉/描述/globs 控件行 + 原始 .mdc 文本域 + 保存/取消。\n * @author ddj 2026年09月03号\n * @param props.form 表单状态(mode/scope/workspacePath/relHint/file/content/fmType/desc/globs/error/saving)\n * @param props.onType 类型下拉变更回调\n * @param props.onDesc 描述输入回调\n * @param props.onGlobs globs 输入回调\n * @param props.onBody 文本域编辑回调\n * @param props.onSave 保存回调\n * @param props.onCancel 取消回调\n * @returns 表单元素\n */\nfunction RuleEditor(props) {\n const form = props.form\n const isNew = form.mode === 'new'\n const crumb = (form.scope === 'project' ? '项目规则' : '用户规则') + ' > ' + form.relHint + ' > ' + (form.file || '新建规则.mdc')\n return React.createElement('div', { className: 'edrv-rules-editor' },\n React.createElement('div', { className: 'edrv-rules-editor-head' },\n React.createElement('span', { className: 'edrv-rules-crumb', title: crumb }, crumb),\n React.createElement('button', { className: 'edrv-rules-act', title: '取消', onClick: props.onCancel }, '✕')),\n React.createElement('div', { className: 'edrv-rules-editor-ctl' },\n React.createElement('label', { className: 'edrv-rules-ctl-label' }, '规则类型'),\n React.createElement('select', { className: 'edrv-rules-select', value: form.fmType, onChange: props.onType },\n Object.keys(TYPE_LABEL).map((key) => React.createElement('option', { key, value: key }, TYPE_LABEL[key]))),\n React.createElement('label', { className: 'edrv-rules-ctl-label' }, '描述'),\n React.createElement('input', {\n className: 'edrv-rules-input edrv-rules-desc-input', value: form.desc, spellCheck: false,\n placeholder: isNew ? '规则用途说明(写入 frontmatter description)' : '规则用途说明', onChange: props.onDesc,\n })),\n form.fmType === 'auto' ? React.createElement('div', { className: 'edrv-rules-editor-ctl' },\n React.createElement('label', { className: 'edrv-rules-ctl-label' }, 'globs'),\n React.createElement('input', {\n className: 'edrv-rules-input', value: form.globs, spellCheck: false,\n placeholder: 'src/**/*.ts, *.md(逗号分隔,命中才注入)', onChange: props.onGlobs,\n })) : null,\n React.createElement('textarea', {\n className: 'edrv-rules-body', spellCheck: false,\n value: form.content,\n onChange: props.onBody,\n }),\n React.createElement('div', { className: 'edrv-rules-editor-hint' },\n '类型/描述/globs 与 frontmatter 双向同步;enabled 与其余键原样保留;无 frontmatter 的纯 markdown 视为「手动」规则'),\n form.error ? React.createElement('div', { className: 'edrv-rules-err' }, form.error) : null,\n React.createElement('div', { className: 'edrv-rules-editor-foot' },\n React.createElement('button', { className: 'edrv-rules-save', disabled: form.saving, onClick: props.onSave }, form.saving ? '保存中…' : '保存')))\n}\n// --endregion\n\n/**\n * 规则面板主体。\n * @param props.ctx 面板共享上下文(notify 可选)\n */\nexport function RulesPanel(props) {\n const ctx = props?.ctx\n const notify = ctx?.notify ?? ((message) => {})\n // 当前会话工作区(EditorView 从 sessions 快照注入;项目 Tab 自动匹配,不提供手动切换)\n const cwd = ctx?.cwd ?? null\n const [tab, setTab] = React.useState(() => (readLocal(CACHE_KEY.rules + 'tab') === 'project' ? 'project' : 'user'))\n const [data, setData] = React.useState(null) // { user: RuleInfo[], projects: RuleProject[] }\n const [error, setError] = React.useState('')\n const panelRef = React.useRef(null)\n const [panelW, setPanelW] = React.useState(0) // 面板内容宽度(ResizeObserver 跟踪;0=未测得)\n const [form, setForm] = React.useState(null) // 编辑器表单状态;null=列表态\n const mountedRef = React.useRef(true)\n\n /**\n * 当前工作区对应的项目聚合(按 ctx.cwd 自动匹配;无会话/未注册返回 null)。\n * @returns RuleProject 或 null\n */\n const activeProject = () => {\n if (!cwd) return null\n return (data?.projects ?? []).find((p) => p.workspacePath === cwd) ?? null\n }\n\n /** 拉取规则总列表(rules.list)。 */\n const load = () => {\n rpc('rules.list', {}).then((res) => {\n if (!mountedRef.current) return\n if (res && res.ok) { setData(res); setError('') } else setError(res?.error ?? '读取规则失败')\n }).catch((e) => { if (mountedRef.current) setError('读取规则异常: ' + String(e)) })\n }\n\n React.useEffect(() => {\n mountedRef.current = true\n load()\n return () => { mountedRef.current = false }\n }, [])\n\n // 面板宽度跟踪:描述字数上限与窄面板模式都随宽度动态变化\n React.useEffect(() => {\n const el = panelRef.current\n if (!el || typeof ResizeObserver === 'undefined') return undefined\n const observer = new ResizeObserver(() => setPanelW(el.clientWidth))\n observer.observe(el)\n setPanelW(el.clientWidth)\n return () => observer.disconnect()\n }, [])\n\n // 描述字数上限与窄面板模式(未测得宽度前用基础上限/完整展示)\n const descCap = panelW > 0 ? descCapOf(panelW) : DESC_MAX\n const narrow = panelW > 0 && panelW < NARROW_W\n\n /**\n * 切换 Tab(记忆到 localStorage)。\n * @param next 目标 Tab\n */\n const switchTab = (next) => {\n setTab(next)\n writeLocal(CACHE_KEY.rules + 'tab', next)\n }\n\n /**\n * 打开新建表单(按当前 Tab 决定作用域;项目规则跟随当前会话工作区)。\n */\n const openNew = () => {\n const scope = tab === 'project' ? 'project' : 'user'\n if (scope === 'project' && !cwd) { notify('当前会话没有工作区,无法新建项目规则'); return }\n const parsed = parseRuleFm(NEW_TEMPLATE)\n setForm({\n mode: 'new', scope, origin: scope === 'user' ? 'current' : undefined, workspacePath: scope === 'project' ? cwd : undefined,\n relHint: scope === 'project' ? '.dsh/rules/' : 'rules/', file: '', content: NEW_TEMPLATE,\n fmType: parsed.type, desc: parsed.description, globs: parsed.globs.join(', '), error: '', saving: false,\n })\n }\n\n /**\n * 打开编辑表单:先拉全文,再解析 frontmatter 回填类型/描述/globs 控件。\n * @param rule 目标规则\n */\n const openEdit = (rule) => {\n rpc('rules.read', { scope: rule.scope, origin: rule.origin, workspacePath: rule.scope === 'project' ? rule.workspacePath : undefined, file: rule.file })\n .then((res) => {\n if (!res || !res.ok) { notify(res?.error ?? '读取规则失败'); return }\n const parsed = parseRuleFm(res.content)\n setForm({\n mode: 'edit', scope: rule.scope, origin: rule.origin, workspacePath: rule.workspacePath, relHint: rule.relHint,\n file: rule.file, content: res.content,\n fmType: parsed.type, desc: parsed.description, globs: parsed.globs.join(', '), error: '', saving: false,\n })\n }).catch((e) => notify('读取规则异常: ' + String(e)))\n }\n\n /** 类型下拉变更:frontmatter 按新类型重写,控件同步。 */\n const onTypeChange = (e) => {\n const nextType = e.target.value\n setForm((prev) => ({ ...prev, fmType: nextType, content: rebuildContent(prev.content, nextType, prev.desc, prev.globs), error: '' }))\n }\n\n /** 描述输入变更:同步 frontmatter description 行。 */\n const onDescChange = (e) => {\n const nextDesc = e.target.value\n setForm((prev) => ({ ...prev, desc: nextDesc, content: rebuildContent(prev.content, prev.fmType, nextDesc, prev.globs), error: '' }))\n }\n\n /** globs 输入变更(自动规则):同步 frontmatter globs 行。 */\n const onGlobsChange = (e) => {\n const nextGlobs = e.target.value\n setForm((prev) => ({ ...prev, globs: nextGlobs, content: rebuildContent(prev.content, prev.fmType, prev.desc, nextGlobs), error: '' }))\n }\n\n /** 文本域直接编辑:回读 frontmatter,同步类型/描述/globs 控件(文本域是唯一真源)。 */\n const onBodyChange = (e) => {\n const parsed = parseRuleFm(e.target.value)\n setForm((prev) => ({ ...prev, content: e.target.value, fmType: parsed.type, desc: parsed.description, globs: parsed.globs.join(', ') }))\n }\n\n /** 保存表单(rules.save;文件名非空 + 自动规则必须有 globs)。 */\n const saveForm = () => {\n const next = { ...form, saving: true, error: '' }\n const file = String(form.file ?? '').trim()\n if (!file) { setForm({ ...next, saving: false, error: '文件名不能为空' }); return }\n if (form.fmType === 'auto' && globList(form.globs).length === 0) { setForm({ ...next, saving: false, error: '自动规则需要至少一个 glob(逗号分隔)' }); return }\n setForm(next)\n rpc('rules.save', { scope: form.scope, origin: form.origin, workspacePath: form.workspacePath, file, content: form.content })\n .then((res) => {\n if (res && res.ok) {\n setForm(null)\n notify('规则已保存:' + file)\n load()\n } else setForm((prev) => ({ ...prev, saving: false, error: res?.error ?? '保存失败' }))\n }).catch((e) => setForm((prev) => ({ ...prev, saving: false, error: '保存异常: ' + String(e) })))\n }\n\n /**\n * 删除规则(confirm 后走 rules.remove)。\n * @param rule 目标规则\n */\n const removeRule = (rule) => {\n const reveal = rule.scope === 'user' && rule.origin !== 'legacy'\n ? '\\n若旧目录有同名规则,删除后它将重新生效。' : ''\n if (!window.confirm('删除规则 ' + rule.absPath + '?(不可恢复)' + reveal)) return\n rpc('rules.remove', { scope: rule.scope, origin: rule.origin, workspacePath: rule.workspacePath, file: rule.file })\n .then((res) => {\n if (res && res.ok) { notify('已删除:' + rule.file); load() } else notify(res?.error ?? '删除失败')\n }).catch((e) => notify('删除异常: ' + String(e)))\n }\n\n /**\n * 切换启用开关:本地乐观翻转(按 workspacePath+file 定位,失败回滚)。\n * @param rule 目标规则(project 已注入 workspacePath)\n */\n const toggleRule = (rule) => {\n const enabled = !rule.enabled\n const applyLocal = (on) => setData((prev) => prev ? {\n ...prev,\n user: prev.user.map((r) => (rule.scope === 'user' && r.file === rule.file && (r.origin ?? 'current') === (rule.origin ?? 'current') ? { ...r, enabled: on } : r)),\n projects: prev.projects.map((p) => (p.workspacePath === rule.workspacePath\n ? { ...p, rules: p.rules.map((r) => (r.file === rule.file ? { ...r, enabled: on } : r)) }\n : p)),\n } : prev)\n applyLocal(enabled)\n rpc('rules.toggle', { scope: rule.scope, origin: rule.origin, workspacePath: rule.workspacePath, file: rule.file, enabled })\n .then((res) => {\n if (res && res.ok) { applyLocal(res.rule?.enabled === true); notify(res.rule?.enabled === false ? '已停用:' + rule.file : '已启用:' + rule.file) }\n else { applyLocal(!enabled); notify(res?.error ?? '切换失败') }\n }).catch((e) => { applyLocal(!enabled); notify('切换异常: ' + String(e)) })\n }\n\n /** 当前列表数据(按 Tab 取用户规则或当前工作区的项目规则)。 */\n const rowsOf = () => {\n if (!data) return []\n if (tab === 'user') return data.user\n return activeProject()?.rules ?? []\n }\n\n const renderTabs = () => React.createElement('div', { className: 'edrv-rules-tabs' },\n React.createElement('button', { className: 'edrv-rules-tab' + (tab === 'user' ? ' on' : ''), onClick: () => switchTab('user') }, '用户规则'),\n React.createElement('button', { className: 'edrv-rules-tab' + (tab === 'project' ? ' on' : ''), onClick: () => switchTab('project') }, '项目规则'),\n React.createElement('button', { className: 'edrv-rules-new', title: '新建规则', onClick: openNew }, '+ 新建规则'))\n\n /** 项目 Tab 的工作区展示(不可编辑,自动跟随当前会话)。 */\n const renderWsLine = () => {\n if (tab !== 'project') return null\n return React.createElement('div', { className: 'edrv-rules-ws-line', title: cwd ?? '' },\n '工作区:' + (cwd ?? '当前会话未打开工作区'))\n }\n\n const renderEmpty = () => {\n if (tab === 'project') {\n if (!cwd) return '当前会话没有工作区,项目规则不可用(项目规则跟随会话所在工作区)'\n if (!data?.projects?.length) return '当前 DSH 没有已注册的工作区;在目标工作区打开一次会话后再来管理'\n if (!activeProject()) return '当前工作区未注册为 DSH workspace,暂不能管理其项目规则'\n return '<工作区>/.dsh/rules/ 还没有规则,点击「+ 新建规则」创建'\n }\n return '还没有用户规则(当前 DSH_HOME/rules/ 或旧目录 ~/.dsh/rules/),点击「+ 新建规则」创建'\n }\n\n const renderBody = () => {\n if (form) {\n return React.createElement(RuleEditor, {\n form, onType: onTypeChange, onDesc: onDescChange, onGlobs: onGlobsChange, onBody: onBodyChange,\n onSave: saveForm, onCancel: () => setForm(null),\n })\n }\n if (!data) return React.createElement('div', { className: 'edrv-rules-empty' }, '加载中…')\n if (error) return React.createElement('div', { className: 'edrv-rules-err' }, error)\n const rows = rowsOf()\n if (!rows.length) return React.createElement('div', { className: 'edrv-rules-empty' }, renderEmpty())\n const common = {\n descCap,\n narrow,\n onEdit: (rule) => openEdit({ ...rule, workspacePath: rule.scope === 'project' ? cwd : undefined }),\n onRemove: (rule) => removeRule({ ...rule, workspacePath: rule.scope === 'project' ? cwd : undefined }),\n onToggle: (rule) => toggleRule({ ...rule, workspacePath: rule.scope === 'project' ? cwd : undefined }),\n }\n return React.createElement('div', { className: 'edrv-rules-list' },\n rows.map((rule) => React.createElement(RuleRow, { key: rule.scope + ':' + (rule.origin ?? 'project') + ':' + rule.file, rule, ...common })))\n }\n\n return React.createElement('div', { className: 'edrv-rules-panel', ref: panelRef },\n renderTabs(),\n renderWsLine(),\n React.createElement('div', { className: 'edrv-rules-hint' },\n tab === 'user' ? '读取 DSH_HOME/rules/ 与 ~/.dsh/rules/;同名时当前目录优先,新建保存到当前目录' : '项目规则仅对所在工作区的会话生效(存放于 <工作区>/.dsh/rules/)'),\n renderBody())\n}\n"],"mappings":"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"}
|
package/lib/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { chmod, copyFile, cp, lstat, mkdir, open, readFile, readdir, realpath, r
|
|
|
3
3
|
import { basename, dirname, extname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
|
|
4
4
|
import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
+
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
7
7
|
import { homedir, tmpdir } from "node:os";
|
|
8
8
|
import { DatabaseSync } from "node:sqlite";
|
|
9
9
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
@@ -6551,13 +6551,6 @@ const PERSIST_DEBOUNCE_MS = 1e4;
|
|
|
6551
6551
|
const HEAL_DEBOUNCE_MS = 3e3;
|
|
6552
6552
|
/** cwd → 索引状态(随会话销毁清理)。 */
|
|
6553
6553
|
const states = /* @__PURE__ */ new Map();
|
|
6554
|
-
/** 单调时钟:连续 put 的 ts 严格递增(同毫秒不并列,逐出/裁剪顺序可预期)。 */
|
|
6555
|
-
let clock = 0;
|
|
6556
|
-
/** 取下一时间戳(不小于真实时间且严格递增)。 */
|
|
6557
|
-
function nextTs() {
|
|
6558
|
-
clock = Math.max(Date.now(), clock + 1);
|
|
6559
|
-
return clock;
|
|
6560
|
-
}
|
|
6561
6554
|
/** 条目原始值 → 合法条目(非法返回 null)。 */
|
|
6562
6555
|
function entryOf(raw) {
|
|
6563
6556
|
if (!raw || typeof raw !== "object") return null;
|
|
@@ -6628,10 +6621,10 @@ function indexParse(text) {
|
|
|
6628
6621
|
type: e.type
|
|
6629
6622
|
});
|
|
6630
6623
|
}
|
|
6631
|
-
|
|
6624
|
+
const full = dir.full === true && entries.length === dir.entries.length;
|
|
6632
6625
|
dirs[rel] = {
|
|
6633
6626
|
ts: typeof dir.ts === "number" ? dir.ts : 0,
|
|
6634
|
-
full
|
|
6627
|
+
full,
|
|
6635
6628
|
entries
|
|
6636
6629
|
};
|
|
6637
6630
|
}
|
|
@@ -6650,11 +6643,12 @@ function trimPersist(data, maxDirs = 400, maxEntries = PERSIST_DIR_ENTRIES) {
|
|
|
6650
6643
|
const sorted = Object.entries(data.dirs).sort((a, b) => b[1].ts - a[1].ts);
|
|
6651
6644
|
for (const [rel, dir] of sorted) {
|
|
6652
6645
|
if (Object.keys(dirs).length >= maxDirs) break;
|
|
6653
|
-
const
|
|
6646
|
+
const truncated = dir.entries.length > maxEntries;
|
|
6647
|
+
const full = dir.full && !truncated;
|
|
6654
6648
|
dirs[rel] = {
|
|
6655
6649
|
ts: dir.ts,
|
|
6656
6650
|
full,
|
|
6657
|
-
entries:
|
|
6651
|
+
entries: truncated ? dir.entries.slice(0, maxEntries) : dir.entries
|
|
6658
6652
|
};
|
|
6659
6653
|
}
|
|
6660
6654
|
return {
|
|
@@ -6663,7 +6657,7 @@ function trimPersist(data, maxDirs = 400, maxEntries = PERSIST_DIR_ENTRIES) {
|
|
|
6663
6657
|
dirs
|
|
6664
6658
|
};
|
|
6665
6659
|
}
|
|
6666
|
-
/** 取(或建)某 cwd
|
|
6660
|
+
/** 取(或建)某 cwd 的索引状态并惰性加载快照;加载期间真实列取优先,不被旧快照覆盖。 */
|
|
6667
6661
|
function ensureState(ctx, cwd) {
|
|
6668
6662
|
const existing = states.get(cwd);
|
|
6669
6663
|
if (existing) return existing;
|
|
@@ -6676,6 +6670,7 @@ function ensureState(ctx, cwd) {
|
|
|
6676
6670
|
dirs: {}
|
|
6677
6671
|
},
|
|
6678
6672
|
stale: /* @__PURE__ */ new Set(),
|
|
6673
|
+
lru: /* @__PURE__ */ new Map(),
|
|
6679
6674
|
dirty: false,
|
|
6680
6675
|
persistTimer: null,
|
|
6681
6676
|
healTimer: null,
|
|
@@ -6703,44 +6698,68 @@ async function loadIndex(state) {
|
|
|
6703
6698
|
try {
|
|
6704
6699
|
const parsed = indexParse(await readFile(treeCacheFile(state.cwd), "utf8"));
|
|
6705
6700
|
if (!parsed) return;
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
if (
|
|
6701
|
+
const snapshots = Object.entries(parsed.dirs).sort((a, b) => a[1].ts - b[1].ts);
|
|
6702
|
+
for (const [rel, dir] of snapshots) {
|
|
6703
|
+
if (state.data.dirs[rel]) continue;
|
|
6704
|
+
state.data.dirs[rel] = dir;
|
|
6705
|
+
touchDir(state, rel);
|
|
6709
6706
|
}
|
|
6710
6707
|
if (parsed.root && !state.data.root) state.data.root = parsed.root;
|
|
6711
6708
|
} catch (error) {} finally {
|
|
6712
6709
|
state.loading = false;
|
|
6713
6710
|
}
|
|
6714
6711
|
}
|
|
6715
|
-
/**
|
|
6712
|
+
/**
|
|
6713
|
+
* @private
|
|
6714
|
+
* @author ddj 2026年10月22号
|
|
6715
|
+
* @description 更新运行时访问顺序,超限时同步清理目录及失效标记。
|
|
6716
|
+
* @param state 工作区索引状态
|
|
6717
|
+
* @param rel 本次访问的相对目录
|
|
6718
|
+
*/
|
|
6719
|
+
function touchDir(state, rel) {
|
|
6720
|
+
state.lru.delete(rel);
|
|
6721
|
+
state.lru.set(rel, true);
|
|
6722
|
+
if (state.lru.size <= 2e3) return;
|
|
6723
|
+
const oldest = state.lru.keys().next().value;
|
|
6724
|
+
if (oldest === void 0) return;
|
|
6725
|
+
state.lru.delete(oldest);
|
|
6726
|
+
state.stale.delete(oldest);
|
|
6727
|
+
delete state.data.dirs[oldest];
|
|
6728
|
+
}
|
|
6729
|
+
/**
|
|
6730
|
+
* @public
|
|
6731
|
+
* @author ddj 2026年10月22号
|
|
6732
|
+
* @description 完整、新鲜且未失效才命中;访问仅更新 LRU,不续期硬 TTL。
|
|
6733
|
+
* @param cwd 工作区根
|
|
6734
|
+
* @param rel 相对目录
|
|
6735
|
+
* @returns 完整条目,或未命中时的 null
|
|
6736
|
+
*/
|
|
6716
6737
|
function hitIndex(cwd, rel) {
|
|
6717
6738
|
const state = states.get(cwd);
|
|
6718
6739
|
if (!state) return null;
|
|
6719
6740
|
const dir = state.data.dirs[rel];
|
|
6720
|
-
if (!dir || state.stale.has(rel)) return null;
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
dir.ts = now;
|
|
6741
|
+
if (!dir || !dir.full || state.stale.has(rel)) return null;
|
|
6742
|
+
if (Date.now() - dir.ts >= 6e4) return null;
|
|
6743
|
+
touchDir(state, rel);
|
|
6724
6744
|
return dir.entries;
|
|
6725
6745
|
}
|
|
6726
|
-
/**
|
|
6746
|
+
/**
|
|
6747
|
+
* @public
|
|
6748
|
+
* @author ddj 2026年10月22号
|
|
6749
|
+
* @description 保存完整实列结果与确认时间,更新 LRU、撤销 stale 并防抖落盘。
|
|
6750
|
+
* @param ctx 工作区上下文
|
|
6751
|
+
* @param cwd 工作区根
|
|
6752
|
+
* @param rel 相对目录
|
|
6753
|
+
* @param entries 完整实列条目
|
|
6754
|
+
*/
|
|
6727
6755
|
function putIndex(ctx, cwd, rel, entries) {
|
|
6728
6756
|
const state = ensureState(ctx, cwd);
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
let oldestTs = Infinity;
|
|
6733
|
-
for (const [k, v] of Object.entries(dirs)) if (v.ts < oldestTs) {
|
|
6734
|
-
oldestTs = v.ts;
|
|
6735
|
-
oldest = k;
|
|
6736
|
-
}
|
|
6737
|
-
delete dirs[oldest];
|
|
6738
|
-
}
|
|
6739
|
-
dirs[rel] = {
|
|
6740
|
-
ts: nextTs(),
|
|
6741
|
-
full: entries.length <= PERSIST_DIR_ENTRIES,
|
|
6757
|
+
state.data.dirs[rel] = {
|
|
6758
|
+
ts: Date.now(),
|
|
6759
|
+
full: true,
|
|
6742
6760
|
entries
|
|
6743
6761
|
};
|
|
6762
|
+
touchDir(state, rel);
|
|
6744
6763
|
state.stale.delete(rel);
|
|
6745
6764
|
state.dirty = true;
|
|
6746
6765
|
schedulePersist(state);
|
|
@@ -6805,6 +6824,7 @@ function disposeIndex(cwd) {
|
|
|
6805
6824
|
state.healTimer = null;
|
|
6806
6825
|
}
|
|
6807
6826
|
if (state.dirty) persistNow(state);
|
|
6827
|
+
state.lru.clear();
|
|
6808
6828
|
states.delete(cwd);
|
|
6809
6829
|
}
|
|
6810
6830
|
/** 错误 → 用户文案(DirListError 用自身 message,其余带上下文)。 */
|
|
@@ -6857,6 +6877,7 @@ async function listDirCached(ctx, cwd, rel, force) {
|
|
|
6857
6877
|
if (error instanceof DirListError && error.code === "not-found") {
|
|
6858
6878
|
if (state.data.dirs[rel]) {
|
|
6859
6879
|
delete state.data.dirs[rel];
|
|
6880
|
+
state.lru.delete(rel);
|
|
6860
6881
|
state.dirty = true;
|
|
6861
6882
|
schedulePersist(state);
|
|
6862
6883
|
}
|
|
@@ -11959,7 +11980,140 @@ function settingsDocPath(configuredPath, requestedPath, platform = process.platf
|
|
|
11959
11980
|
}
|
|
11960
11981
|
}
|
|
11961
11982
|
//#endregion
|
|
11983
|
+
//#region src/shared/textPages.ts
|
|
11984
|
+
/** Bounded, read-only text paging protocol. @author ddj 2026年10月22号 */
|
|
11985
|
+
const TEXT_PAGE_BYTES = 262144;
|
|
11986
|
+
//#endregion
|
|
11987
|
+
//#region src/textPages.ts
|
|
11988
|
+
/**
|
|
11989
|
+
* Create a Host-owned pager with process-local authenticated cursors.
|
|
11990
|
+
* @public @author ddj 2026年10月22号
|
|
11991
|
+
* @returns A bounded reader requiring an independently authorized range capability.
|
|
11992
|
+
*/
|
|
11993
|
+
function createTextPages() {
|
|
11994
|
+
const secret = randomBytes(32);
|
|
11995
|
+
/** @private @author ddj 2026年10月22号 @param payload Authenticated state. @returns Signed cursor. */
|
|
11996
|
+
function signCursor(payload) {
|
|
11997
|
+
const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
11998
|
+
return body + "." + createHmac("sha256", secret).update(body).digest("base64url");
|
|
11999
|
+
}
|
|
12000
|
+
/** @private @author ddj 2026年10月22号 @param token Untrusted cursor. @returns Verified state or null. */
|
|
12001
|
+
function parseCursor(token) {
|
|
12002
|
+
if (typeof token !== "string" || token.length > 8192) return null;
|
|
12003
|
+
const parts = token.split(".");
|
|
12004
|
+
if (parts.length !== 2 || !/^[A-Za-z0-9_-]+$/.test(parts[0])) return null;
|
|
12005
|
+
const signature = Buffer.from(parts[1], "base64url");
|
|
12006
|
+
const expected = createHmac("sha256", secret).update(parts[0]).digest();
|
|
12007
|
+
if (signature.length !== expected.length || !timingSafeEqual(signature, expected)) return null;
|
|
12008
|
+
try {
|
|
12009
|
+
const payload = JSON.parse(Buffer.from(parts[0], "base64url").toString("utf8"));
|
|
12010
|
+
if (typeof payload.identity !== "string" || typeof payload.version !== "string" || typeof payload.sessionId !== "string" || typeof payload.path !== "string" || typeof payload.pendingCR !== "boolean" || !Number.isSafeInteger(payload.offset) || payload.offset <= 0) return null;
|
|
12011
|
+
return payload;
|
|
12012
|
+
} catch {
|
|
12013
|
+
return null;
|
|
12014
|
+
}
|
|
12015
|
+
}
|
|
12016
|
+
/**
|
|
12017
|
+
* Read at most 256 KiB, validating freshness before and after IO.
|
|
12018
|
+
* @public @author ddj 2026年10月22号
|
|
12019
|
+
* @param source Authorized range capability; absent means unsupported.
|
|
12020
|
+
* @param args Request identity and optional signed cursor.
|
|
12021
|
+
* @returns Strict UTF-8 text or an explicit fail-closed result; never partial success on mutation.
|
|
12022
|
+
*/
|
|
12023
|
+
async function readTextPage(source, args) {
|
|
12024
|
+
if (!source) return failure("unsupported", "Host does not expose authorized range reads");
|
|
12025
|
+
const cursor = args.cursor === void 0 ? void 0 : parseCursor(args.cursor);
|
|
12026
|
+
if (cursor === null || cursor && (cursor.identity !== source.identity || cursor.sessionId !== (args.sessionId ?? "") || cursor.path !== args.path)) return failure("invalid-cursor", "Cursor does not belong to this file and session");
|
|
12027
|
+
try {
|
|
12028
|
+
const before = await source.stat();
|
|
12029
|
+
if (!before || before.type !== "file" || typeof before.version !== "string" || !Number.isSafeInteger(before.size) || before.size < 0) return failure("unsupported", "Regular file size and opaque version are required");
|
|
12030
|
+
if (cursor && (cursor.version !== before.version || cursor.offset >= before.size)) return failure("stale", "File changed; restart text preview");
|
|
12031
|
+
const offset = cursor?.offset ?? 0;
|
|
12032
|
+
const length = Math.min(TEXT_PAGE_BYTES, before.size - offset);
|
|
12033
|
+
const bytes = await source.readRange(offset, length);
|
|
12034
|
+
const after = await source.stat();
|
|
12035
|
+
if (!after || after.version !== before.version || after.size !== before.size || after.type !== "file") return failure("stale", "File changed during text read");
|
|
12036
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength !== length) return failure("read-failed", "Range reader returned an incomplete or oversized page");
|
|
12037
|
+
return decodePage(bytes, before, offset, cursor?.pendingCR ?? false, args, source.identity, signCursor);
|
|
12038
|
+
} catch (error) {
|
|
12039
|
+
return failure("read-failed", String(error));
|
|
12040
|
+
}
|
|
12041
|
+
}
|
|
12042
|
+
return readTextPage;
|
|
12043
|
+
}
|
|
12044
|
+
/** @private @author ddj 2026年10月22号 @param reason Failure discriminator. @param error User-visible error. @returns Failure result. */
|
|
12045
|
+
function failure(reason, error) {
|
|
12046
|
+
return {
|
|
12047
|
+
ok: false,
|
|
12048
|
+
reason,
|
|
12049
|
+
error
|
|
12050
|
+
};
|
|
12051
|
+
}
|
|
12052
|
+
/**
|
|
12053
|
+
* Keep an incomplete final UTF-8 sequence for a bounded re-read on the next page.
|
|
12054
|
+
* @private @author ddj 2026年10月22号
|
|
12055
|
+
* @param bytes Raw page. @returns Complete prefix length; invalid sequences remain for fatal decoding.
|
|
12056
|
+
*/
|
|
12057
|
+
function utf8Prefix(bytes) {
|
|
12058
|
+
let start = bytes.length - 1;
|
|
12059
|
+
while (start >= 0 && (bytes[start] & 192) === 128) start--;
|
|
12060
|
+
if (start < 0) return bytes.length;
|
|
12061
|
+
const lead = bytes[start];
|
|
12062
|
+
let width = 1;
|
|
12063
|
+
if (lead >= 194 && lead <= 223) width = 2;
|
|
12064
|
+
else if (lead >= 224 && lead <= 239) width = 3;
|
|
12065
|
+
else if (lead >= 240 && lead <= 244) width = 4;
|
|
12066
|
+
return bytes.length - start < width ? start : bytes.length;
|
|
12067
|
+
}
|
|
12068
|
+
/**
|
|
12069
|
+
* Decode one page preserving original line endings without buffering a long line.
|
|
12070
|
+
* @private @author ddj 2026年10月22号
|
|
12071
|
+
* @param bytes Raw page. @param info File snapshot. @param offset Current offset.
|
|
12072
|
+
* @param pendingCR Previous trailing CR. @param args Request identity. @param identity Authorized target identity.
|
|
12073
|
+
* @param signCursor Host cursor signer. @returns Page or strict decoding failure.
|
|
12074
|
+
*/
|
|
12075
|
+
function decodePage(bytes, info, offset, pendingCR, args, identity, signCursor) {
|
|
12076
|
+
const consumed = offset + bytes.length === info.size ? bytes.length : utf8Prefix(bytes);
|
|
12077
|
+
let text;
|
|
12078
|
+
try {
|
|
12079
|
+
text = new TextDecoder("utf-8", {
|
|
12080
|
+
fatal: true,
|
|
12081
|
+
ignoreBOM: true
|
|
12082
|
+
}).decode(bytes.subarray(0, consumed));
|
|
12083
|
+
} catch {
|
|
12084
|
+
return failure("not-text", "File is not valid UTF-8");
|
|
12085
|
+
}
|
|
12086
|
+
if (text.includes("\0")) return failure("not-text", "Binary text preview is unsupported");
|
|
12087
|
+
if (offset === 0 && text.startsWith("")) text = text.slice(1);
|
|
12088
|
+
if (pendingCR) text = "\r" + text;
|
|
12089
|
+
const nextOffset = offset + consumed;
|
|
12090
|
+
const eof = nextOffset === info.size;
|
|
12091
|
+
const trailingCR = !eof && text.endsWith("\r");
|
|
12092
|
+
if (trailingCR) text = text.slice(0, -1);
|
|
12093
|
+
const nextCursor = eof ? null : signCursor({
|
|
12094
|
+
identity,
|
|
12095
|
+
sessionId: args.sessionId ?? "",
|
|
12096
|
+
path: args.path,
|
|
12097
|
+
version: info.version,
|
|
12098
|
+
offset: nextOffset,
|
|
12099
|
+
pendingCR: trailingCR
|
|
12100
|
+
});
|
|
12101
|
+
return {
|
|
12102
|
+
ok: true,
|
|
12103
|
+
text,
|
|
12104
|
+
version: info.version,
|
|
12105
|
+
offset: nextOffset,
|
|
12106
|
+
size: info.size,
|
|
12107
|
+
eof,
|
|
12108
|
+
nextCursor,
|
|
12109
|
+
encoding: "utf-8",
|
|
12110
|
+
partialLine: !eof && !/[\r\n]$/.test(text)
|
|
12111
|
+
};
|
|
12112
|
+
}
|
|
12113
|
+
//#endregion
|
|
11962
12114
|
//#region src/rpc.ts
|
|
12115
|
+
/** Process-local cursor signer; reads must stay on the Host filesystem capability. */
|
|
12116
|
+
const readTextPage = createTextPages();
|
|
11963
12117
|
/** cwd → 上次 stale 自动清理时间:全量轮询(EditorView/DiffBadge/Dock 各自 5s)节流,避免每轮都读文件算指纹。 */
|
|
11964
12118
|
const staleCheckedAt = /* @__PURE__ */ new Map();
|
|
11965
12119
|
/** stale 自动清理最小间隔。 */
|
|
@@ -12049,7 +12203,10 @@ async function readTargetOf(ctx, sc, path) {
|
|
|
12049
12203
|
err: "文件不存在",
|
|
12050
12204
|
resolvedPath: fs.processPath(target)
|
|
12051
12205
|
};
|
|
12052
|
-
if ((info.size ?? 0) > 33554432) return {
|
|
12206
|
+
if ((info.size ?? 0) > 33554432) return {
|
|
12207
|
+
err: "文件过大(>32MB),不支持整文件预览",
|
|
12208
|
+
code: "file-too-large"
|
|
12209
|
+
};
|
|
12053
12210
|
return {
|
|
12054
12211
|
target,
|
|
12055
12212
|
fs,
|
|
@@ -12066,13 +12223,11 @@ async function readTargetOf(ctx, sc, path) {
|
|
|
12066
12223
|
* @returns { ok:false } 形态载荷
|
|
12067
12224
|
*/
|
|
12068
12225
|
function targetErrOf(prep) {
|
|
12069
|
-
return
|
|
12226
|
+
return {
|
|
12070
12227
|
ok: false,
|
|
12071
12228
|
error: prep.err,
|
|
12072
|
-
resolvedPath: prep.resolvedPath
|
|
12073
|
-
|
|
12074
|
-
ok: false,
|
|
12075
|
-
error: prep.err
|
|
12229
|
+
...prep.resolvedPath !== void 0 ? { resolvedPath: prep.resolvedPath } : {},
|
|
12230
|
+
...prep.code ? { code: prep.code } : {}
|
|
12076
12231
|
};
|
|
12077
12232
|
}
|
|
12078
12233
|
/**
|
|
@@ -12601,6 +12756,44 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
12601
12756
|
results: await applyDecisions(ctx, sc.session, sc.cwd, bucket, Array.isArray(args.items) ? args.items : [])
|
|
12602
12757
|
};
|
|
12603
12758
|
},
|
|
12759
|
+
/** @author ddj 2026年10月22号 @param args Read-only request. @returns Bounded official range read, or unsupported on older providers; never bypasses Host IO. */
|
|
12760
|
+
"edrv.textPage": async (args) => {
|
|
12761
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
12762
|
+
if ("err" in sc) return {
|
|
12763
|
+
ok: false,
|
|
12764
|
+
error: sc.err,
|
|
12765
|
+
reason: "read-failed"
|
|
12766
|
+
};
|
|
12767
|
+
try {
|
|
12768
|
+
const target = await resolveTarget(ctx, sc.session, args.path);
|
|
12769
|
+
const fs = ctx.get("fs");
|
|
12770
|
+
if (typeof fs?.readByteRange !== "function" || typeof fs.stat !== "function") return readTextPage(void 0, args);
|
|
12771
|
+
const targetKey = typeof target === "string" ? target : target.targetKey;
|
|
12772
|
+
if (typeof targetKey !== "string") return readTextPage(void 0, args);
|
|
12773
|
+
/** @author ddj 2026年10月22号 @returns Provider metadata used for before/after freshness checks. */
|
|
12774
|
+
function statPage() {
|
|
12775
|
+
return fs.stat(target);
|
|
12776
|
+
}
|
|
12777
|
+
/** @author ddj 2026年10月22号 @param offset Verified cursor position. @param length Bounded byte count. @returns Bytes through the official provider only. */
|
|
12778
|
+
function readRange(offset, length) {
|
|
12779
|
+
return fs.readByteRange(target, {
|
|
12780
|
+
offset,
|
|
12781
|
+
length
|
|
12782
|
+
});
|
|
12783
|
+
}
|
|
12784
|
+
return readTextPage({
|
|
12785
|
+
identity: JSON.stringify([sc.cwd, targetKey]),
|
|
12786
|
+
stat: statPage,
|
|
12787
|
+
readRange
|
|
12788
|
+
}, args);
|
|
12789
|
+
} catch (error) {
|
|
12790
|
+
return {
|
|
12791
|
+
ok: false,
|
|
12792
|
+
error: "读取失败:" + String(error),
|
|
12793
|
+
reason: "read-failed"
|
|
12794
|
+
};
|
|
12795
|
+
}
|
|
12796
|
+
},
|
|
12604
12797
|
"edrv.read": async (args) => {
|
|
12605
12798
|
const settingsTarget = settingsTargetOf(ctx, args.path);
|
|
12606
12799
|
if (settingsTarget && args.encoding !== "base64") try {
|
|
@@ -12665,7 +12858,8 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
12665
12858
|
}
|
|
12666
12859
|
if ((prep.info.size ?? 0) > 8388608) return {
|
|
12667
12860
|
ok: false,
|
|
12668
|
-
error: "文件过大(>8MB),不支持整文件预览"
|
|
12861
|
+
error: "文件过大(>8MB),不支持整文件预览",
|
|
12862
|
+
code: "file-too-large"
|
|
12669
12863
|
};
|
|
12670
12864
|
const content = await prep.fs.readText(prep.target);
|
|
12671
12865
|
return {
|