dsh-linked-workspace 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.en.md +4 -0
- package/lib/client.js +40 -11
- package/lib/index.js +61 -3
- package/package.json +1 -1
- package/scripts/doc_tool.py +17 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.2 — 2026-11-05
|
|
4
|
+
|
|
5
|
+
- Fixed Markdown/document JSON transport failing with a Windows GBK codec error for characters such as the superscript minus (U+207B); the Python bridge now emits UTF-8/ASCII-safe JSON and configures UTF-8 stdout.
|
|
6
|
+
- PDF files now load lazily: only the current page is rendered; other pages are fetched on demand (`docs.pdfPage`), and a large PDF is downloaded/opened once and cached. This removes the long “加载中” hang on large (30 MB+) PDFs.
|
|
7
|
+
- PDF page navigation, rotation, deletion and metadata remain available.
|
|
8
|
+
|
|
3
9
|
## 0.6.1 — 2026-11-05
|
|
4
10
|
|
|
5
11
|
- Fixed the linked-workspace directory tab disappearing for a new or blank linked-workspace session: the client session store is now consulted for cwd/workspaceId before falling back to session.info, so the tab and the start-page option are registered immediately when the workspace session opens.
|
package/README.en.md
CHANGED
|
@@ -115,6 +115,10 @@ Old “Remote directories” tabs are restored after refresh through hidden lega
|
|
|
115
115
|
|
|
116
116
|
See [CHANGELOG.md](CHANGELOG.md) for the full version history.
|
|
117
117
|
|
|
118
|
+
### 0.6.2
|
|
119
|
+
|
|
120
|
+
Fix Windows GBK JSON transport errors for Markdown characters such as the superscript minus; PDF pages now load lazily and the source PDF is cached, avoiding long loading hangs on large files.
|
|
121
|
+
|
|
118
122
|
### 0.6.1
|
|
119
123
|
|
|
120
124
|
Fix the linked-workspace directory tab and start-page entry disappearing in a new blank linked-workspace session by consulting the client session store for cwd and workspaceId first.
|
package/lib/client.js
CHANGED
|
@@ -4430,7 +4430,7 @@ function fileIconSpec(name, isDir) {
|
|
|
4430
4430
|
var stateRef = useState({
|
|
4431
4431
|
loading: true, error: '', data: null, text: '', dirty: false, saving: false,
|
|
4432
4432
|
preview: false, html: '', previewLoading: false, page: 0, sheet: 0, slide: 0,
|
|
4433
|
-
updates: {}, pptxUpdates: {}, rotations: {}, deleted: {}, notice: '',
|
|
4433
|
+
updates: {}, pptxUpdates: {}, rotations: {}, deleted: {}, pdfLoading: false, notice: '',
|
|
4434
4434
|
});
|
|
4435
4435
|
var state = stateRef[0];
|
|
4436
4436
|
var setState = stateRef[1];
|
|
@@ -4451,7 +4451,7 @@ function fileIconSpec(name, isDir) {
|
|
|
4451
4451
|
loading: false, error: '', data: result,
|
|
4452
4452
|
text: result.kind === 'markdown' ? String(result.text || '') : '',
|
|
4453
4453
|
preview: false, html: '', page: 0, sheet: 0, slide: 0,
|
|
4454
|
-
updates: {}, pptxUpdates: {}, rotations: {}, deleted: {}, dirty: false,
|
|
4454
|
+
updates: {}, pptxUpdates: {}, rotations: {}, deleted: {}, pdfLoading: false, dirty: false,
|
|
4455
4455
|
});
|
|
4456
4456
|
}).catch(function (error) {
|
|
4457
4457
|
patch({ loading: false, error: messageOf(error, t, locale) });
|
|
@@ -4538,6 +4538,35 @@ function fileIconSpec(name, isDir) {
|
|
|
4538
4538
|
patch({ pptxUpdates: updates, dirty: true });
|
|
4539
4539
|
}
|
|
4540
4540
|
|
|
4541
|
+
function goPdfPage(index) {
|
|
4542
|
+
if (index < 0 || index >= (state.data.pageCount || 0)) return;
|
|
4543
|
+
var cached = (state.data.pages || []).filter(function (page) { return page.index === index; })[0];
|
|
4544
|
+
if (cached) {
|
|
4545
|
+
patch({ page: index });
|
|
4546
|
+
return;
|
|
4547
|
+
}
|
|
4548
|
+
patch({ pdfLoading: true, error: '' });
|
|
4549
|
+
apiCall('docs.pdfPage', params({ page: index })).then(function (result) {
|
|
4550
|
+
var pages = (state.data.pages || []).filter(function (page) { return page.index !== result.pages[0].index; }).concat(result.pages);
|
|
4551
|
+
pages.sort(function (left, right) { return left.index - right.index; });
|
|
4552
|
+
patch({ data: Object.assign({}, state.data, { pages: pages }), page: index, pdfLoading: false });
|
|
4553
|
+
}).catch(function (error) {
|
|
4554
|
+
patch({ pdfLoading: false, error: messageOf(error, t, locale) });
|
|
4555
|
+
});
|
|
4556
|
+
}
|
|
4557
|
+
|
|
4558
|
+
function stepPdfPage(delta) {
|
|
4559
|
+
var total = state.data.pageCount || 0;
|
|
4560
|
+
var next = state.page + delta;
|
|
4561
|
+
while (next >= 0 && next < total) {
|
|
4562
|
+
if (!state.deleted[next]) {
|
|
4563
|
+
goPdfPage(next);
|
|
4564
|
+
return;
|
|
4565
|
+
}
|
|
4566
|
+
next += delta;
|
|
4567
|
+
}
|
|
4568
|
+
}
|
|
4569
|
+
|
|
4541
4570
|
var toolbar = h('div', { className: 'rw-doc-toolbar' },
|
|
4542
4571
|
h('span', { className: 'rw-chip' }, kind.toUpperCase()),
|
|
4543
4572
|
h('span', { className: 'rw-hint', style: { flex: '1 1 auto', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: path }, path),
|
|
@@ -4567,9 +4596,8 @@ function fileIconSpec(name, isDir) {
|
|
|
4567
4596
|
onChange: function (event) { patch({ text: event.currentTarget.value, dirty: true, html: '' }); },
|
|
4568
4597
|
}));
|
|
4569
4598
|
} else if (kind === 'pdf' && state.data) {
|
|
4570
|
-
var
|
|
4571
|
-
var
|
|
4572
|
-
var current = visible[Math.min(state.page, Math.max(visible.length - 1, 0))] || null;
|
|
4599
|
+
var total = state.data.pageCount || 0;
|
|
4600
|
+
var current = (state.data.pages || []).filter(function (page) { return page.index === state.page; })[0] || null;
|
|
4573
4601
|
body = h('div', { className: 'rw-doc-body' },
|
|
4574
4602
|
h('div', { className: 'rw-doc-field' },
|
|
4575
4603
|
h('label', null, t('docPdfTitle')),
|
|
@@ -4578,13 +4606,14 @@ function fileIconSpec(name, isDir) {
|
|
|
4578
4606
|
h('input', { className: 'rw-doc-input', style: { flex: '1 1 auto' }, value: (state.data.metadata && state.data.metadata.author) || '', onChange: function (event) { patch({ data: Object.assign({}, state.data, { metadata: Object.assign({}, state.data.metadata, { author: event.currentTarget.value }) }), dirty: true }); } })),
|
|
4579
4607
|
h('div', { className: 'rw-inline', style: { justifyContent: 'space-between', margin: '6px 0' } },
|
|
4580
4608
|
h('div', { className: 'rw-inline' },
|
|
4581
|
-
h('button', { type: 'button', className: 'rw-btn rw-btn-sm', disabled: state.page <= 0, onClick: function () {
|
|
4582
|
-
h('span', { className: 'rw-hint' },
|
|
4583
|
-
h('button', { type: 'button', className: 'rw-btn rw-btn-sm', disabled: state.page >=
|
|
4609
|
+
h('button', { type: 'button', className: 'rw-btn rw-btn-sm', disabled: state.page <= 0, onClick: function () { stepPdfPage(-1); } }, '‹'),
|
|
4610
|
+
h('span', { className: 'rw-hint' }, t('docPageOf', { page: state.page + 1, total: total })),
|
|
4611
|
+
h('button', { type: 'button', className: 'rw-btn rw-btn-sm', disabled: state.page >= total - 1, onClick: function () { stepPdfPage(1); } }, '›')),
|
|
4584
4612
|
h('div', { className: 'rw-inline' },
|
|
4585
|
-
h('button', { type: 'button', className: 'rw-btn rw-btn-sm', disabled: !current, onClick: function () { var rotations = Object.assign({}, state.rotations); rotations[
|
|
4586
|
-
h('button', { type: 'button', className: 'rw-btn rw-btn-sm rw-btn-danger', disabled:
|
|
4587
|
-
|
|
4613
|
+
h('button', { type: 'button', className: 'rw-btn rw-btn-sm', disabled: !current, onClick: function () { var rotations = Object.assign({}, state.rotations); rotations[state.page] = ((rotations[state.page] || (current && current.rotation) || 0) + 90) % 360; patch({ rotations: rotations, dirty: true }); } }, t('docRotate')),
|
|
4614
|
+
h('button', { type: 'button', className: 'rw-btn rw-btn-sm rw-btn-danger', disabled: total <= 1, onClick: function () { var deleted = Object.assign({}, state.deleted); deleted[state.page] = true; var next = state.page + 1; while (next < total && deleted[next]) next += 1; if (next >= total) { next = state.page - 1; while (next >= 0 && deleted[next]) next -= 1; } var nextPage = next >= 0 ? next : state.page; patch({ deleted: deleted, dirty: true, page: nextPage }); if (next >= 0 && !((state.data.pages || []).some(function (page) { return page.index === next; }))) goPdfPage(next); } }, t('docDeletePage')))),
|
|
4615
|
+
state.pdfLoading ? h('div', { className: 'rw-editor-message' }, Spinner(14), t('loading')) : null,
|
|
4616
|
+
current ? h('img', { className: 'rw-doc-pdf-page', src: current.png, style: { transform: 'rotate(' + ((state.rotations[current.index] || current.rotation || 0) % 360) + 'deg)' } }) : null);
|
|
4588
4617
|
} else if (kind === 'docx' && state.data) {
|
|
4589
4618
|
var paragraphs = state.data.paragraphs || [];
|
|
4590
4619
|
var tables = state.data.tables || [];
|
package/lib/index.js
CHANGED
|
@@ -1505,7 +1505,8 @@ export function apply(ctx) {
|
|
|
1505
1505
|
return { data: readFileSync(current.path), mtime: info.mtimeMs, size: info.size }
|
|
1506
1506
|
}
|
|
1507
1507
|
const stat = await current.scope.remote.stat(current.path)
|
|
1508
|
-
|
|
1508
|
+
const isFile = target.remote ? stat.type === 'file' : stat.isFile()
|
|
1509
|
+
if (!isFile) throw new ApiError('FS_NOT_DIR', '只能打开文件')
|
|
1509
1510
|
if (stat.size > DOC_MAX_BYTES) throw new ApiError('DOC_TOO_LARGE', '文档超过 80 MB')
|
|
1510
1511
|
const read = await current.scope.remote.readFile(current.path, { maxBytes: DOC_MAX_BYTES })
|
|
1511
1512
|
return { data: read.data, mtime: stat.mtime, size: stat.size }
|
|
@@ -1524,6 +1525,44 @@ export function apply(ctx) {
|
|
|
1524
1525
|
return { path: target.path, size: result.size, mtime: result.mtime }
|
|
1525
1526
|
}
|
|
1526
1527
|
|
|
1528
|
+
const docFileCache = new Map()
|
|
1529
|
+
|
|
1530
|
+
async function ensureDocCache(target) {
|
|
1531
|
+
let stat
|
|
1532
|
+
try {
|
|
1533
|
+
stat = target.remote ? await target.scope.remote.stat(target.path) : await fsStat(target.path)
|
|
1534
|
+
} catch (error) {
|
|
1535
|
+
if (target.remote) throw error
|
|
1536
|
+
throw mapLocalFsError(error)
|
|
1537
|
+
}
|
|
1538
|
+
const isFile = target.remote ? stat.type === 'file' : stat.isFile()
|
|
1539
|
+
if (!isFile) throw new ApiError('FS_NOT_DIR', '只能打开文件')
|
|
1540
|
+
if (stat.size > DOC_MAX_BYTES) throw new ApiError('DOC_TOO_LARGE', '文档超过 80 MB')
|
|
1541
|
+
const mtime = target.remote ? stat.mtime : stat.mtimeMs
|
|
1542
|
+
const key = (target.remote ? String(target.scope.id) : 'local') + '|' + target.path + '|' + String(mtime)
|
|
1543
|
+
const existing = docFileCache.get(key)
|
|
1544
|
+
if (existing && existsSync(existing)) return { file: existing, size: stat.size, mtime }
|
|
1545
|
+
let data
|
|
1546
|
+
if (target.remote) {
|
|
1547
|
+
const read = await target.scope.remote.readFile(target.path, { maxBytes: DOC_MAX_BYTES })
|
|
1548
|
+
data = read.data
|
|
1549
|
+
} else {
|
|
1550
|
+
data = readFileSync(target.path)
|
|
1551
|
+
}
|
|
1552
|
+
const directory = join(tmpdir(), 'dsh-linked-workspace-docs')
|
|
1553
|
+
mkdirSync(directory, { recursive: true })
|
|
1554
|
+
const file = join(directory, randomUUID() + (extname(target.path) || '.bin'))
|
|
1555
|
+
writeFileSync(file, data)
|
|
1556
|
+
docFileCache.set(key, file)
|
|
1557
|
+
if (docFileCache.size > 20) {
|
|
1558
|
+
const oldestKey = docFileCache.keys().next().value
|
|
1559
|
+
const oldestFile = docFileCache.get(oldestKey)
|
|
1560
|
+
docFileCache.delete(oldestKey)
|
|
1561
|
+
try { rmSync(oldestFile, { force: true }) } catch { /* best effort */ }
|
|
1562
|
+
}
|
|
1563
|
+
return { file, size: stat.size, mtime }
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1527
1566
|
async function makeDocTemp() {
|
|
1528
1567
|
const directory = mkdtempSync(join(tmpdir(), 'dsh-doc-'))
|
|
1529
1568
|
return {
|
|
@@ -1733,6 +1772,11 @@ export function apply(ctx) {
|
|
|
1733
1772
|
async 'docs.read'(params = {}) {
|
|
1734
1773
|
const target = await docTarget(params)
|
|
1735
1774
|
const kind = assertDocKind(target.path)
|
|
1775
|
+
if (kind === 'pdf') {
|
|
1776
|
+
const cached = await ensureDocCache(target)
|
|
1777
|
+
const data = await runDocTool(['read', '--kind', 'pdf', '--file', cached.file])
|
|
1778
|
+
return { ...data, path: target.path, size: cached.size, mtime: cached.mtime, cached: true }
|
|
1779
|
+
}
|
|
1736
1780
|
const current = await readDocBytes(target, target)
|
|
1737
1781
|
if (kind === 'markdown') {
|
|
1738
1782
|
if (looksBinary(current.data)) throw new ApiError('DOC_BINARY', 'Markdown 文件包含二进制内容')
|
|
@@ -1749,6 +1793,15 @@ export function apply(ctx) {
|
|
|
1749
1793
|
}
|
|
1750
1794
|
},
|
|
1751
1795
|
|
|
1796
|
+
async 'docs.pdfPage'(params = {}) {
|
|
1797
|
+
const target = await docTarget(params)
|
|
1798
|
+
assertDocKind(target.path, 'pdf')
|
|
1799
|
+
const page = Number(params.page)
|
|
1800
|
+
if (!Number.isInteger(page) || page < 0) throw new ApiError('BAD_REQUEST', '页码无效')
|
|
1801
|
+
const cached = await ensureDocCache(target)
|
|
1802
|
+
return await runDocTool(['read', '--kind', 'pdf', '--file', cached.file, '--page', String(page)])
|
|
1803
|
+
},
|
|
1804
|
+
|
|
1752
1805
|
async 'docs.write'(params = {}) {
|
|
1753
1806
|
const target = await docTarget(params)
|
|
1754
1807
|
const kind = assertDocKind(target.path)
|
|
@@ -1765,13 +1818,18 @@ export function apply(ctx) {
|
|
|
1765
1818
|
const written = await writeDocBytes(target, Buffer.from(data.text, 'utf8'))
|
|
1766
1819
|
return { kind, ...written }
|
|
1767
1820
|
}
|
|
1768
|
-
|
|
1821
|
+
let sourceFile
|
|
1822
|
+
if (kind === 'pdf') {
|
|
1823
|
+
sourceFile = (await ensureDocCache(target)).file
|
|
1824
|
+
} else {
|
|
1825
|
+
sourceFile = (await readDocBytes(target, target)).data
|
|
1826
|
+
}
|
|
1769
1827
|
const temp = await makeDocTemp()
|
|
1770
1828
|
try {
|
|
1771
1829
|
const source = temp.file('source.' + kind)
|
|
1772
1830
|
const spec = temp.file('data.json')
|
|
1773
1831
|
const output = temp.file('output.' + kind)
|
|
1774
|
-
writeFileSync(source,
|
|
1832
|
+
writeFileSync(source, Buffer.isBuffer(sourceFile) ? sourceFile : readFileSync(sourceFile))
|
|
1775
1833
|
writeFileSync(spec, JSON.stringify(data))
|
|
1776
1834
|
await runDocTool(['write', '--kind', kind, '--file', source, '--data', spec, '--out', output])
|
|
1777
1835
|
const written = await writeDocBytes(target, readFileSync(output))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-linked-workspace",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Linked local ⇄ remote workspaces and SSH-backed remote directories for DeepSeek Harness: theme-aware split editor, file-type icons, and an agent tool that edits remote files directly.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
package/scripts/doc_tool.py
CHANGED
|
@@ -13,6 +13,12 @@ import os
|
|
|
13
13
|
import re
|
|
14
14
|
import sys
|
|
15
15
|
|
|
16
|
+
try:
|
|
17
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
18
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
19
|
+
except Exception:
|
|
20
|
+
pass
|
|
21
|
+
|
|
16
22
|
MAX_PDF_PAGES = 40
|
|
17
23
|
MAX_PDF_WIDTH = 1100
|
|
18
24
|
MAX_SHEET_ROWS = 200
|
|
@@ -21,12 +27,12 @@ MAX_PPTX_SLIDES = 80
|
|
|
21
27
|
|
|
22
28
|
|
|
23
29
|
def fail(message):
|
|
24
|
-
print(json.dumps({"ok": False, "error": str(message)}, ensure_ascii=
|
|
30
|
+
print(json.dumps({"ok": False, "error": str(message)}, ensure_ascii=True))
|
|
25
31
|
sys.exit(1)
|
|
26
32
|
|
|
27
33
|
|
|
28
34
|
def emit(data):
|
|
29
|
-
print(json.dumps({"ok": True, "data": data}, ensure_ascii=
|
|
35
|
+
print(json.dumps({"ok": True, "data": data}, ensure_ascii=True))
|
|
30
36
|
|
|
31
37
|
|
|
32
38
|
def json_value(value):
|
|
@@ -198,14 +204,16 @@ def write_pptx(source, data, out):
|
|
|
198
204
|
|
|
199
205
|
# ── PDF ───────────────────────────────────────────────────────────────────
|
|
200
206
|
|
|
201
|
-
def read_pdf(path):
|
|
207
|
+
def read_pdf(path, page_index=None):
|
|
202
208
|
import fitz
|
|
203
209
|
|
|
204
210
|
doc = fitz.open(path)
|
|
205
211
|
pages = []
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
212
|
+
indexes = [page_index] if isinstance(page_index, int) else [0]
|
|
213
|
+
for index in indexes:
|
|
214
|
+
if index < 0 or index >= doc.page_count:
|
|
215
|
+
continue
|
|
216
|
+
page = doc[index]
|
|
209
217
|
rect = page.rect
|
|
210
218
|
scale = min(1.5, MAX_PDF_WIDTH / max(rect.width, 1))
|
|
211
219
|
pixmap = page.get_pixmap(matrix=fitz.Matrix(scale, scale), alpha=False)
|
|
@@ -226,7 +234,7 @@ def read_pdf(path):
|
|
|
226
234
|
"subject": metadata.get("subject", ""),
|
|
227
235
|
},
|
|
228
236
|
"pages": pages,
|
|
229
|
-
"truncated": doc.page_count >
|
|
237
|
+
"truncated": doc.page_count > 1,
|
|
230
238
|
})
|
|
231
239
|
|
|
232
240
|
|
|
@@ -347,6 +355,7 @@ def main():
|
|
|
347
355
|
read = sub.add_parser("read")
|
|
348
356
|
read.add_argument("--kind", required=True)
|
|
349
357
|
read.add_argument("--file", required=True)
|
|
358
|
+
read.add_argument("--page", type=int, default=None)
|
|
350
359
|
|
|
351
360
|
write = sub.add_parser("write")
|
|
352
361
|
write.add_argument("--kind", required=True)
|
|
@@ -375,7 +384,7 @@ def main():
|
|
|
375
384
|
elif args.kind == "pptx":
|
|
376
385
|
read_pptx(args.file)
|
|
377
386
|
elif args.kind == "pdf":
|
|
378
|
-
read_pdf(args.file)
|
|
387
|
+
read_pdf(args.file, args.page)
|
|
379
388
|
else:
|
|
380
389
|
raise ValueError(f"unsupported kind: {args.kind}")
|
|
381
390
|
elif args.command == "write":
|