@lyhue1991/dsh-soup 0.5.2 → 0.5.3

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.
@@ -0,0 +1,33 @@
1
+ import * as commands from '@codemirror/commands'
2
+ import * as css from '@codemirror/lang-css'
3
+ import * as html from '@codemirror/lang-html'
4
+ import * as javascript from '@codemirror/lang-javascript'
5
+ import * as json from '@codemirror/lang-json'
6
+ import * as markdown from '@codemirror/lang-markdown'
7
+ import * as python from '@codemirror/lang-python'
8
+ import * as sql from '@codemirror/lang-sql'
9
+ import * as xml from '@codemirror/lang-xml'
10
+ import * as language from '@codemirror/language'
11
+ import * as highlight from '@lezer/highlight'
12
+ import * as state from '@codemirror/state'
13
+ import * as view from '@codemirror/view'
14
+
15
+ const modules = {
16
+ '@codemirror/commands': commands,
17
+ '@codemirror/lang-css': css,
18
+ '@codemirror/lang-html': html,
19
+ '@codemirror/lang-javascript': javascript,
20
+ '@codemirror/lang-json': json,
21
+ '@codemirror/lang-markdown': markdown,
22
+ '@codemirror/lang-python': python,
23
+ '@codemirror/lang-sql': sql,
24
+ '@codemirror/lang-xml': xml,
25
+ '@codemirror/language': language,
26
+ '@lezer/highlight': highlight,
27
+ '@codemirror/state': state,
28
+ '@codemirror/view': view,
29
+ }
30
+
31
+ for (const [id, exports] of Object.entries(modules)) {
32
+ window.__ModuleLoader__.load({ id, factory: () => exports })
33
+ }
package/lib/index.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * 仅 open/trash 这类"唤起系统"的动作按 `process.platform` 分支选命令。
12
12
  */
13
13
 
14
- import { rename, mkdir, writeFile, appendFile, open as openFile, stat, realpath } from 'node:fs/promises'
14
+ import { rename, mkdir, writeFile, appendFile, open as openFile, rm, stat, realpath } from 'node:fs/promises'
15
15
  import { createReadStream } from 'node:fs'
16
16
  import { randomBytes } from 'node:crypto'
17
17
  import path from 'node:path'
@@ -390,7 +390,7 @@ export function apply(ctx) {
390
390
  }
391
391
 
392
392
  // ------------------------------------------------------------------
393
- // 文件只读预览:read(文本/图片/二进制三态);无 write——预览-only 不提供保存
393
+ // 文件预览/编辑:read(文本/图片/二进制三态)+ write(mtime/size 防覆盖保存)
394
394
  // ------------------------------------------------------------------
395
395
  /** 文本读取上限(超过则截断标记 truncated)。8 MB——HTML 常含内联 base64 图片,2 MB 太紧。 */
396
396
  const TEXT_MAX_BYTES = 8 * 1024 * 1024
@@ -413,7 +413,49 @@ export function apply(ctx) {
413
413
  }
414
414
 
415
415
  /**
416
- * 读取文件供 tab 展示:图片返回 base64,文本返回内容(可截断),
416
+ * 保存预览编辑:mtime/size 防覆盖 + 同目录临时文件原子替换。
417
+ * fsync 无法通过 node:fs/promises 便携获得;rename 前先 close 已落盘,
418
+ * 已满足 UI 编辑场景的崩溃一致性下限,并保留写失败时原文件不变。
419
+ */
420
+ async function writeFileForEditor(pathArg, contentArg, expectedMtime, expectedSize, sessionIdArg) {
421
+ const rp = await confine(pathArg, '保存路径', sessionIdArg)
422
+ const content = typeof contentArg === 'string' ? contentArg : ''
423
+ const bytes = Buffer.from(content, 'utf8')
424
+ if (bytes.length > TEXT_MAX_BYTES) {
425
+ return { ok: false, error: `内容超过 ${TEXT_MAX_BYTES} 字节,禁止保存` }
426
+ }
427
+ let before
428
+ try {
429
+ before = await stat(rp)
430
+ } catch (err) {
431
+ if (err && err.code === 'ENOENT') return { ok: false, conflict: true, error: '文件已被删除,保存已取消' }
432
+ return { ok: false, error: `无法读取原文件: ${String((err && err.message) || err)}` }
433
+ }
434
+ if (before.isDirectory()) return { ok: false, error: '目标是目录,不支持保存' }
435
+ if (expectedMtime !== undefined && (Number(expectedMtime) !== before.mtimeMs
436
+ || Number(expectedSize) !== before.size)) {
437
+ return {
438
+ ok: false,
439
+ conflict: true,
440
+ error: '文件已被其他程序修改,请重新加载后再保存',
441
+ mtime: before.mtimeMs,
442
+ size: before.size,
443
+ }
444
+ }
445
+ const tempPath = `${rp}.dsh-soup-${process.pid}-${Date.now()}-${randomBytes(4).toString('hex')}.tmp`
446
+ try {
447
+ await writeFile(tempPath, bytes)
448
+ await rename(tempPath, rp)
449
+ const after = await stat(rp)
450
+ return { ok: true, size: after.size, mtime: after.mtimeMs }
451
+ } catch (err) {
452
+ await rm(tempPath, { force: true })
453
+ return { ok: false, error: `保存失败: ${String((err && err.message) || err)}` }
454
+ }
455
+ }
456
+
457
+ /**
458
+ * 读取文件供 tab 展示:图片返回 base64,文本返回内容与版本(可截断),
417
459
  * 二进制只返回元信息。统一走 node:fs/promises(跨平台、无 shell 注入面)。
418
460
  */
419
461
  async function readFileForView(path) {
@@ -715,6 +757,15 @@ export function apply(ctx) {
715
757
  const rp = await confine(args.path, '文件路径', sessionIdArg)
716
758
  return readFileForView(rp)
717
759
  }
760
+ case 'write': {
761
+ return writeFileForEditor(
762
+ args.path,
763
+ args.content,
764
+ args.expectedMtime,
765
+ args.expectedSize,
766
+ sessionIdArg,
767
+ )
768
+ }
718
769
  case 'download': {
719
770
  // 流式下载签发:围栏校验通过后发一张一次性、2 分钟时效的票据 URL,
720
771
  // 真正的字节由 /api/dsh-soup/dl 流式发送——任意大小文件零内存放大。
package/package.json CHANGED
@@ -1,15 +1,38 @@
1
1
  {
2
2
  "name": "@lyhue1991/dsh-soup",
3
3
  "description": "DSH 项目资源管理器:右侧 details 列文件树(并列网格、宽度对齐宿主 details 合同 300-520px),有 Session log 时在其右侧、空白新会话在输入框上方右端提供 📁 切换按钮;支持多选、双击展开/打开、右键菜单(打开/系统打开/重命名/废纸篓/新建/复制路径)、拖拽移动、上传。文件「打开」在会话区新增「文件」标签页(原生 conversation.view,与对话/轨迹同级):文本可编辑 ⌘S 保存、图片预览、二进制提示。附带模型吞吐徽标:输入框正上方显示「正在等待模型...」与彩色 t/s 徽标(数据来自 llm/stream 真实流)。附带 GoalBar 多行展示:把原生单行截断 GoalBar 放开为多行完整显示,编辑用 textarea,工具与数据仍走原生 goal。",
4
- "version": "0.5.2",
4
+ "version": "0.5.3",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
+ "scripts": {
8
+ "build": "esbuild lib/client.entry.js --bundle --format=iife --platform=browser --target=es2020 --outfile=lib/client.bundle.js",
9
+ "prepublishOnly": "npm run build",
10
+ "test": "npm run build && node --test test/client-goal.spec.mjs test/host.spec.mjs"
11
+ },
7
12
  "exports": {
8
13
  ".": "./lib/index.js",
9
- "./client": "./lib/client.js",
14
+ "./client": "./lib/client.bundle.js",
10
15
  "./cordis.patch.yml": "./cordis.patch.yml",
11
16
  "./package.json": "./package.json"
12
17
  },
18
+ "dependencies": {
19
+ "@codemirror/commands": "^6.10.2",
20
+ "@codemirror/lang-css": "^6.3.1",
21
+ "@codemirror/lang-html": "^6.4.11",
22
+ "@codemirror/lang-javascript": "^6.2.4",
23
+ "@codemirror/lang-json": "^6.0.2",
24
+ "@codemirror/lang-markdown": "^6.5.0",
25
+ "@codemirror/lang-python": "^6.2.1",
26
+ "@codemirror/lang-sql": "^6.10.0",
27
+ "@codemirror/lang-xml": "^6.1.0",
28
+ "@codemirror/language": "^6.12.1",
29
+ "@codemirror/state": "^6.5.4",
30
+ "@codemirror/view": "^6.39.14",
31
+ "@lezer/highlight": "^1.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "esbuild": "^0.25.12"
35
+ },
13
36
  "dsh": {
14
37
  "bundle": {
15
38
  "patch": "./cordis.patch.yml"