dsh-doc-assistant 0.1.14 → 0.1.18
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 +27 -2
- package/lib/client.js +590 -174
- package/package.json +3 -3
- package/src/client/20-api.js +2 -0
- package/src/client/40-revisions.js +67 -24
- package/src/client/50-library.js +1 -1
- package/src/client/60-reader.js +505 -146
- package/src/client/70-shelf.js +14 -2
- package/src/host/chat.js +43 -13
- package/src/host/covers.js +56 -15
- package/src/host/docir.js +26 -3
- package/src/host/legacyWord.js +91 -0
- package/src/host/modelVision.js +44 -0
- package/src/host/ocr.js +120 -210
- package/src/host/ocr_vision.js +39 -0
- package/src/host/ocr_windows.ps1 +89 -0
- package/src/host/office.js +3 -1
- package/src/host/officeImages.js +269 -0
- package/src/host/office_tool.py +451 -29
- package/src/host/preview.js +132 -28
- package/src/host/revise.js +146 -21
- package/src/host/revisionBoundary.js +45 -0
- package/src/host/revisionRebase.js +123 -0
- package/src/host/revisions.js +20 -6
- package/src/host/routes.js +18 -5
- package/src/host/tree.js +1 -0
- package/src/host/upload.js +113 -11
- package/src/host/visionOcr.js +101 -0
- package/src/host/visualContext.js +248 -0
- package/src/host/wordLayout.js +131 -0
- package/src/host/wordSelection.js +114 -0
- package/src/host/writeback.js +108 -19
- package/src/host/writers/index.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-doc-assistant",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"author": "stone086",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "企业文档助手 (Enterprise Document Assistant) — a DSH plugin that turns the harness into a document workbench: project-scoped document library, an e-reader style three-column page (tree | document | AI chat), selection-to-ask, and AI rewrite shown as tracked-change revisions with accept/revert.",
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "node dev/build-client.mjs",
|
|
26
26
|
"test": "node dev/harness.mjs && npm run test:regression",
|
|
27
|
-
"test:regression": "node --test dev/review-regression.mjs dev/review-tool-regression.mjs dev/runtime-regression.mjs dev/word-selection-regression.mjs dev/expansion-regression.mjs dev/chat-revision-regression.mjs dev/office-editor-regression.mjs dev/document-watch-regression.mjs",
|
|
27
|
+
"test:regression": "node --test dev/review-regression.mjs dev/review-tool-regression.mjs dev/runtime-regression.mjs dev/word-selection-regression.mjs dev/expansion-regression.mjs dev/chat-revision-regression.mjs dev/office-editor-regression.mjs dev/document-watch-regression.mjs dev/ocr-regression.mjs dev/cover-regression.mjs dev/visual-context-regression.mjs dev/revision-boundary-regression.mjs dev/word-multipart-regression.mjs dev/compound-word-api-regression.mjs dev/legacy-word-regression.mjs dev/revision-rebase-regression.mjs dev/parallel-generation-host-regression.mjs dev/word-layout-regression.mjs dev/layout-warning-regression.mjs",
|
|
28
28
|
"test:render": "node dev/render-test.mjs",
|
|
29
|
-
"test:ui": "node dev/review-ui-regression.mjs && node dev/spike-reader-e2e.mjs && node dev/spike-reader.mjs && node dev/interaction-regression.mjs && node dev/drag-regression.mjs && node dev/spike-library-render.mjs",
|
|
29
|
+
"test:ui": "node dev/review-ui-regression.mjs && node dev/spike-reader-e2e.mjs && node dev/spike-reader.mjs && node dev/interaction-regression.mjs && node dev/drag-regression.mjs && node dev/spike-library-render.mjs && node dev/image-reader-regression.mjs && node dev/ppt-navigation-regression.mjs && node dev/word-multipart-ui-regression.mjs && node dev/legacy-word-ui-regression.mjs && node dev/parallel-generation-ui-regression.mjs",
|
|
30
30
|
"spike": "node dev/spike-client-view.mjs"
|
|
31
31
|
},
|
|
32
32
|
"keywords": [
|
package/src/client/20-api.js
CHANGED
|
@@ -239,6 +239,7 @@ __define('./api', (module, exports, require) => {
|
|
|
239
239
|
|
|
240
240
|
/** GET /doc/ir?projectId=&path= -> { doc } */
|
|
241
241
|
docIR: (first, second) => call('/doc/ir', { query: pairObject(first, second) }),
|
|
242
|
+
convertWord: (first, second) => call('/doc/convert-word', { body: pairObject(first, second) }),
|
|
242
243
|
|
|
243
244
|
/** GET /covers?projectId= -> { covers } —— 批量,书架一次拿全(CONTRACT §15.3)。 */
|
|
244
245
|
getCovers: (projectId) => call('/covers', { query: asObject(projectId, 'projectId') }),
|
|
@@ -281,6 +282,7 @@ __define('./api', (module, exports, require) => {
|
|
|
281
282
|
/** POST /chat/clear { projectId, path } -> { messages: [] }(§13.3) */
|
|
282
283
|
chatClear: (first, second) => call('/chat/clear', { body: pairObject(first, second) }),
|
|
283
284
|
chatModels: () => call('/chat/models'),
|
|
285
|
+
contextStatus: (first, second) => call('/doc/context-status', { query: pairObject(first, second) }),
|
|
284
286
|
docVersion: (first, second) => call('/doc/version', { query: pairObject(first, second) }),
|
|
285
287
|
openOffice: (first, second) => call('/doc/open-office', { body: pairObject(first, second) }),
|
|
286
288
|
|
|
@@ -289,7 +289,9 @@ __define('./revisions', (module, exports, require) => {
|
|
|
289
289
|
throw new Error(`宿主返回的不是 JSON:${path}`);
|
|
290
290
|
}
|
|
291
291
|
if (!data || data.ok !== true) {
|
|
292
|
-
|
|
292
|
+
const error = new Error((data && data.error) || `请求失败:${path}`);
|
|
293
|
+
error.body = data;
|
|
294
|
+
throw error;
|
|
293
295
|
}
|
|
294
296
|
return data;
|
|
295
297
|
}
|
|
@@ -375,6 +377,16 @@ __define('./revisions', (module, exports, require) => {
|
|
|
375
377
|
async chatModels() {
|
|
376
378
|
return typeof api.chatModels === 'function' ? invokeApi(api.chatModels, [[]]) : getJSON('/chat/models');
|
|
377
379
|
},
|
|
380
|
+
async contextStatus(projectId, path) {
|
|
381
|
+
const data = typeof api.contextStatus === 'function' ? await invokeApi(api.contextStatus, [[{ projectId, path }]])
|
|
382
|
+
: await getJSON('/doc/context-status', { projectId, path });
|
|
383
|
+
return data.progress || { stage: 'idle' };
|
|
384
|
+
},
|
|
385
|
+
async convertWord(projectId, path) {
|
|
386
|
+
const data = typeof api.convertWord === 'function' ? await invokeApi(api.convertWord, [[{ projectId, path }]])
|
|
387
|
+
: await postJSON('/doc/convert-word', { projectId, path });
|
|
388
|
+
return bodyOf(data, ['entry']);
|
|
389
|
+
},
|
|
378
390
|
async openOffice(projectId, path) {
|
|
379
391
|
return typeof api.openOffice === 'function' ? invokeApi(api.openOffice, [[{ projectId, path }]]) : postJSON('/doc/open-office', { projectId, path });
|
|
380
392
|
},
|
|
@@ -407,12 +419,13 @@ __define('./revisions', (module, exports, require) => {
|
|
|
407
419
|
return bodyOf(await postJSON('/revise/iterate', input), ['revision']);
|
|
408
420
|
},
|
|
409
421
|
|
|
410
|
-
async acceptRevision(revisionId) {
|
|
422
|
+
async acceptRevision(revisionId, options = {}) {
|
|
423
|
+
const input = { revisionId, ...(options.layoutRiskToken ? { layoutRiskToken: options.layoutRiskToken } : {}) };
|
|
411
424
|
if (typeof api.acceptRevision === 'function') {
|
|
412
|
-
const out = await invokeApi(api.acceptRevision, [[revisionId], [
|
|
425
|
+
const out = await invokeApi(api.acceptRevision, options.layoutRiskToken ? [[input]] : [[revisionId], [input]]);
|
|
413
426
|
return { revision: bodyOf(out, ['revision']), written: out && out.written ? out.written : null };
|
|
414
427
|
}
|
|
415
|
-
const data = await postJSON('/revision/accept',
|
|
428
|
+
const data = await postJSON('/revision/accept', input);
|
|
416
429
|
return { revision: data.revision || null, written: data.written || null };
|
|
417
430
|
},
|
|
418
431
|
|
|
@@ -558,6 +571,8 @@ __define('./revisions', (module, exports, require) => {
|
|
|
558
571
|
this.listeners = [];
|
|
559
572
|
this.lastWrite = null;
|
|
560
573
|
this.seq = 0;
|
|
574
|
+
this.changeSeq = 0;
|
|
575
|
+
this.itemChanges = new Map();
|
|
561
576
|
}
|
|
562
577
|
|
|
563
578
|
subscribe(listener) {
|
|
@@ -617,6 +632,7 @@ __define('./revisions', (module, exports, require) => {
|
|
|
617
632
|
if (!changed) return this.all();
|
|
618
633
|
this.seq += 1;
|
|
619
634
|
this.items = [];
|
|
635
|
+
this.itemChanges = new Map();
|
|
620
636
|
this.error = '';
|
|
621
637
|
this.loading = false;
|
|
622
638
|
this.emit();
|
|
@@ -633,11 +649,13 @@ __define('./revisions', (module, exports, require) => {
|
|
|
633
649
|
this.loading = true;
|
|
634
650
|
this.error = '';
|
|
635
651
|
this.emit();
|
|
636
|
-
const seq = ++this.seq;
|
|
652
|
+
const seq = ++this.seq, changeSeq = this.changeSeq;
|
|
637
653
|
try {
|
|
638
654
|
const list = await this.transport.listRevisions(this.projectId, this.docPath);
|
|
639
655
|
if (seq !== this.seq) return this.all();
|
|
640
|
-
this.items
|
|
656
|
+
const recent = this.items.filter(item => item?.id && (this.itemChanges.get(item.id) || 0) > changeSeq);
|
|
657
|
+
const recentIds = new Set(recent.map(item => item.id));
|
|
658
|
+
this.items = recent.concat((Array.isArray(list) ? list : []).filter(item => !recentIds.has(item?.id)));
|
|
641
659
|
this.loading = false;
|
|
642
660
|
this.emit();
|
|
643
661
|
return this.all();
|
|
@@ -652,11 +670,14 @@ __define('./revisions', (module, exports, require) => {
|
|
|
652
670
|
|
|
653
671
|
_upsert(revision) {
|
|
654
672
|
if (!revision || typeof revision !== 'object') return null;
|
|
673
|
+
if ((revision.projectId && revision.projectId !== this.projectId)
|
|
674
|
+
|| (revision.docPath && revision.docPath !== this.docPath)) return null;
|
|
655
675
|
if (!revision.id) {
|
|
656
676
|
this.items = [revision].concat(this.items);
|
|
657
677
|
this.emit();
|
|
658
678
|
return revision;
|
|
659
679
|
}
|
|
680
|
+
this.itemChanges.set(revision.id, ++this.changeSeq);
|
|
660
681
|
const index = this.items.findIndex((item) => item && item.id === revision.id);
|
|
661
682
|
if (index >= 0) {
|
|
662
683
|
this.items[index] = Object.assign({}, this.items[index], revision);
|
|
@@ -690,10 +711,12 @@ __define('./revisions', (module, exports, require) => {
|
|
|
690
711
|
return revision;
|
|
691
712
|
}
|
|
692
713
|
|
|
693
|
-
async accept(revisionId) {
|
|
714
|
+
async accept(revisionId, options) {
|
|
715
|
+
const projectId = this.projectId, docPath = this.docPath;
|
|
694
716
|
this.setBusy(`rev:${revisionId}`, true);
|
|
695
717
|
try {
|
|
696
|
-
const out = await this.transport.acceptRevision(revisionId);
|
|
718
|
+
const out = await this.transport.acceptRevision(revisionId, options);
|
|
719
|
+
if (this.projectId !== projectId || this.docPath !== docPath) return out;
|
|
697
720
|
if (out && out.revision && typeof out.revision === 'object') this._upsert(out.revision);
|
|
698
721
|
else {
|
|
699
722
|
const target = this.items.find((item) => item && item.id === revisionId);
|
|
@@ -702,15 +725,20 @@ __define('./revisions', (module, exports, require) => {
|
|
|
702
725
|
}
|
|
703
726
|
if (out && out.written) this.lastWrite = out.written;
|
|
704
727
|
return out;
|
|
728
|
+
} catch (error) {
|
|
729
|
+
if (this.projectId === projectId && this.docPath === docPath && error?.body?.revision) this._upsert(error.body.revision);
|
|
730
|
+
throw error;
|
|
705
731
|
} finally {
|
|
706
732
|
this.setBusy(`rev:${revisionId}`, false);
|
|
707
733
|
}
|
|
708
734
|
}
|
|
709
735
|
|
|
710
736
|
async reject(revisionId) {
|
|
737
|
+
const projectId = this.projectId, docPath = this.docPath;
|
|
711
738
|
this.setBusy(`rev:${revisionId}`, true);
|
|
712
739
|
try {
|
|
713
740
|
const out = await this.transport.rejectRevision(revisionId);
|
|
741
|
+
if (this.projectId !== projectId || this.docPath !== docPath) return out;
|
|
714
742
|
if (out && out.revision && typeof out.revision === 'object') this._upsert(out.revision);
|
|
715
743
|
else {
|
|
716
744
|
const target = this.items.find((item) => item && item.id === revisionId);
|
|
@@ -724,9 +752,11 @@ __define('./revisions', (module, exports, require) => {
|
|
|
724
752
|
}
|
|
725
753
|
|
|
726
754
|
async restore(revisionId) {
|
|
755
|
+
const projectId = this.projectId, docPath = this.docPath;
|
|
727
756
|
this.setBusy(`rev:${revisionId}`, true);
|
|
728
757
|
try {
|
|
729
758
|
const out = await this.transport.restoreRevision(revisionId);
|
|
759
|
+
if (this.projectId !== projectId || this.docPath !== docPath) return out;
|
|
730
760
|
if (out && out.revision && typeof out.revision === 'object') this._upsert(out.revision);
|
|
731
761
|
return out;
|
|
732
762
|
} finally {
|
|
@@ -759,15 +789,17 @@ __define('./revisions', (module, exports, require) => {
|
|
|
759
789
|
* (单条撤回会破坏共用快照的语义)。accept / reject 仍可逐条兜底。
|
|
760
790
|
*/
|
|
761
791
|
async bulk(ids, action) {
|
|
792
|
+
const projectId = this.projectId, docPath = this.docPath;
|
|
793
|
+
const isCurrent = () => this.projectId === projectId && this.docPath === docPath;
|
|
762
794
|
const list = (Array.isArray(ids) ? ids : []).filter(Boolean);
|
|
763
795
|
if (list.length === 0) return [];
|
|
764
796
|
try {
|
|
765
797
|
const results = await this.transport.bulkRevisions(list, action);
|
|
766
|
-
await this.load();
|
|
798
|
+
if (isCurrent()) await this.load();
|
|
767
799
|
return results;
|
|
768
800
|
} catch (error) {
|
|
769
801
|
if (action === 'restore') {
|
|
770
|
-
await this.load().catch(() => {});
|
|
802
|
+
if (isCurrent()) await this.load().catch(() => {});
|
|
771
803
|
throw error;
|
|
772
804
|
}
|
|
773
805
|
const results = [];
|
|
@@ -781,7 +813,7 @@ __define('./revisions', (module, exports, require) => {
|
|
|
781
813
|
results.push({ revisionId: id, ok: false, error: normalizeError(inner) });
|
|
782
814
|
}
|
|
783
815
|
}
|
|
784
|
-
await this.load();
|
|
816
|
+
if (isCurrent()) await this.load();
|
|
785
817
|
return results;
|
|
786
818
|
}
|
|
787
819
|
}
|
|
@@ -874,8 +906,13 @@ __define('./revisions', (module, exports, require) => {
|
|
|
874
906
|
const ops = opsFor(revision);
|
|
875
907
|
const busy = p.busy === true || (p.busyIds || []).indexOf(revision.id) >= 0;
|
|
876
908
|
const canDecide = status === 'pending';
|
|
909
|
+
const canAcceptLayoutRisk = canDecide && typeof revision.layoutWarning?.token === 'string';
|
|
910
|
+
const savedError = p.acceptError || revision.error || '';
|
|
911
|
+
// Older saved cards may still recommend the removed copy feature.
|
|
912
|
+
const acceptError = /流式(?:编辑)?副本/.test(savedError)
|
|
913
|
+
? '此修订可能造成文字重叠或版式异常。修订尚未写入。请点击文档标题右侧的 Office 图标,手动打开文档调整排版。' : savedError;
|
|
914
|
+
const layoutWarning = canDecide && /手动打开文档调整排版/.test(acceptError);
|
|
877
915
|
const unsafe = /\.docx$/i.test(revision.docPath || '') && revision.selectionVersion !== 'word-selection-v2' && status !== 'accepted';
|
|
878
|
-
const outdated = status === 'pending' && p.currentHash && revision.docHash && p.currentHash !== revision.docHash;
|
|
879
916
|
const group = typeof p.writeGroup === 'function' ? p.writeGroup(revision) : writeGroupOf([revision], revision);
|
|
880
917
|
const groupSize = Array.isArray(group) ? group.length : 1;
|
|
881
918
|
|
|
@@ -905,18 +942,22 @@ __define('./revisions', (module, exports, require) => {
|
|
|
905
942
|
collapsed ? h('span', { key: 'status', className: P + 'RevCollapseStatus' }, STATUS_TEXT[status] || status) : null,
|
|
906
943
|
]),
|
|
907
944
|
unsafe ? h('div', { key: 'unsafe', className: P + 'Err' }, '旧版选区可能漏字,请重新选择文字后生成修订。') : null,
|
|
908
|
-
|
|
909
|
-
? h('div', { key: 'err', className: P + 'Err' },
|
|
945
|
+
acceptError
|
|
946
|
+
? h('div', { key: 'err', className: P + 'Err', 'data-revision-layout-warning': layoutWarning ? '1' : undefined }, `${layoutWarning ? '提示' : '错误'}:${acceptError}`)
|
|
947
|
+
: null,
|
|
948
|
+
status === 'accepted' && revision.written?.layoutRiskAccepted
|
|
949
|
+
? h('div', { key: 'layout-reminder', className: P + 'Hint', role: 'status', 'data-revision-layout-warning': '1' },
|
|
950
|
+
'已写入修订,可能存在文字重叠或版式异常。请点击文档标题右侧的 Office 图标,手动打开文档调整排版。')
|
|
910
951
|
: null,
|
|
952
|
+
p.progress ? h('div', { key: 'progress', 'data-revision-write-progress': '1' }, p.progress) : null,
|
|
911
953
|
!collapsed ? h('div', { key: 'diff', className: P + 'RevDiffBox', 'aria-label': '词级差异' }, h(InlineDiff, { ops })) : null,
|
|
912
954
|
!collapsed ? h('div', { key: 'actions', className: P + 'RevActions' }, [
|
|
913
|
-
canDecide ? button('accept', '接受', p.onAccept, P + 'BtnPrimary', unsafe ||
|
|
914
|
-
outdated ? '文档已更新,请重新选择文字后生成修订' : undefined) : null,
|
|
955
|
+
canDecide ? button('accept', canAcceptLayoutRisk ? '仍然接受并写入' : '接受', p.onAccept, P + 'BtnPrimary', unsafe || p.writeBusy) : null,
|
|
915
956
|
status === 'rejected'
|
|
916
|
-
? button('restore', '恢复为待处理', p.onRestore, null,
|
|
957
|
+
? button('restore', '恢复为待处理', p.onRestore, null, p.writeBusy)
|
|
917
958
|
: null,
|
|
918
959
|
status === 'accepted' && groupSize <= 1
|
|
919
|
-
? button('restore', '撤回', p.onRestore, null,
|
|
960
|
+
? button('restore', '撤回', p.onRestore, null, p.writeBusy, '撤回这次写入并还原文件')
|
|
920
961
|
: null,
|
|
921
962
|
status === 'accepted' && groupSize > 1
|
|
922
963
|
? h('button', {
|
|
@@ -924,11 +965,11 @@ __define('./revisions', (module, exports, require) => {
|
|
|
924
965
|
type: 'button',
|
|
925
966
|
className: P + 'Btn',
|
|
926
967
|
title: '这几条修订共用一次写回快照,必须整组还原',
|
|
927
|
-
disabled: busy || typeof p.onRestoreGroup !== 'function',
|
|
968
|
+
disabled: busy || p.writeBusy || typeof p.onRestoreGroup !== 'function',
|
|
928
969
|
onClick: typeof p.onRestoreGroup === 'function' ? () => p.onRestoreGroup(group) : undefined,
|
|
929
970
|
}, `撤回本次批量写入(${groupSize} 条)`)
|
|
930
971
|
: null,
|
|
931
|
-
status !== 'accepted' ? button('regen', '重新生成', p.onRegenerate, null, unsafe
|
|
972
|
+
status !== 'accepted' ? button('regen', '重新生成', p.onRegenerate, null, unsafe) : null,
|
|
932
973
|
button('delete', '删除', p.onDelete, null, false, '删除这张修订卡片,不撤销已写入文档的修改'),
|
|
933
974
|
]) : null,
|
|
934
975
|
]);
|
|
@@ -1080,10 +1121,12 @@ __define('./revisions', (module, exports, require) => {
|
|
|
1080
1121
|
border-radius:var(--dsw-radius-sm,5px);cursor:pointer}
|
|
1081
1122
|
.${P}Btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
1082
1123
|
.${P}Btn:disabled{opacity:.5;cursor:default}
|
|
1083
|
-
.${P}BtnPrimary{background:var(--dsw-alias-
|
|
1084
|
-
color
|
|
1085
|
-
|
|
1086
|
-
|
|
1124
|
+
.${P}BtnPrimary{background:var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary));
|
|
1125
|
+
border-color:var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary));
|
|
1126
|
+
color:var(--dsw-alias-label-primary-foreground,#fff)}
|
|
1127
|
+
.${P}BtnPrimary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary)));
|
|
1128
|
+
border-color:var(--dsw-alias-button-primary-hover,var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary)));
|
|
1129
|
+
color:var(--dsw-alias-label-primary-foreground,#fff)}
|
|
1087
1130
|
.${P}BtnGhost{border-color:transparent;background:transparent;color:var(--dsw-alias-label-secondary)}
|
|
1088
1131
|
.${P}Err{color:var(--dsw-alias-state-error-primary);font-size:12px;margin-top:4px}
|
|
1089
1132
|
`;
|
package/src/client/50-library.js
CHANGED
|
@@ -34,7 +34,7 @@ __define('./library', (module, exports, require) => {
|
|
|
34
34
|
const DISPLAY_NAME = '企业文档助手';
|
|
35
35
|
|
|
36
36
|
/** 客户端版本号,与 package.json 的 version 同步维护(`/state` 不回传版本时用它)。 */
|
|
37
|
-
const CLIENT_VERSION = '0.1.
|
|
37
|
+
const CLIENT_VERSION = '0.1.18';
|
|
38
38
|
|
|
39
39
|
/** 设置窗口的分区。 */
|
|
40
40
|
const SECTIONS = [
|