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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-doc-assistant",
3
- "version": "0.1.14",
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": [
@@ -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
- throw new Error((data && data.error) || `请求失败:${path}`);
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], [{ 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', { revisionId });
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 = Array.isArray(list) ? list.slice() : [];
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
- revision.error
909
- ? h('div', { key: 'err', className: P + 'Err' }, `错误:${revision.error}`)
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 || outdated,
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, false)
957
+ ? button('restore', '恢复为待处理', p.onRestore, null, p.writeBusy)
917
958
  : null,
918
959
  status === 'accepted' && groupSize <= 1
919
- ? button('restore', '撤回', p.onRestore, null, false, '撤回这次写入并还原文件')
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 || outdated) : null,
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-brand-primary);border-color:var(--dsw-alias-brand-primary);
1084
- color:#fff}
1085
- .${P}BtnPrimary:hover:not(:disabled){background:var(--dsw-alias-brand-primary);color:#fff;
1086
- filter:brightness(.92)}
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
  `;
@@ -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.14';
37
+ const CLIENT_VERSION = '0.1.18';
38
38
 
39
39
  /** 设置窗口的分区。 */
40
40
  const SECTIONS = [