@a3s-lab/office 0.7.3 → 0.8.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.
@@ -22,9 +22,9 @@ which tracks editor capability parity with WPS.
22
22
  the local binding origin, so one participant cannot undo another
23
23
  participant's operation.
24
24
  - `view`, `comment`, `suggest`, and `edit` are explicit session modes. A format
25
- enables a mode only after its durable model exists. Document now enables
26
- durable review-only `comment`; unsupported mode/format pairs remain
27
- read-only.
25
+ enables a mode only after its durable model exists. Document enables durable
26
+ review-only `comment` plus actor-attributed `suggest`; unsupported
27
+ mode/format pairs remain read-only.
28
28
  - Editors never collaborate by repeatedly replacing one serialized OOXML file
29
29
  or one universal JSON blob. Each format gets a typed, conflict-local model.
30
30
 
@@ -44,6 +44,7 @@ Protocol v1 reserves the `a3s.office` namespace.
44
44
  | `document.content` | `Y.XmlFragment` | ProseMirror document, including section layout, comment anchors, and tracked-change marks. |
45
45
  | `document.options` | `Y.Map` | Document-level editable options such as page color and tracking mode. |
46
46
  | `document.comments` / `document.comment-order` | `Y.Map` / `Y.Array` | ID-keyed durable comment/reply records and deterministic presentation order. |
47
+ | `document.change-decisions` / `document.change-decision-order` | `Y.Map` / `Y.Array` | Immutable accept/reject audit records and deterministic decision order. |
47
48
  | `document.bibliography*` | Typed maps/arrays | Bibliography settings and ID-keyed citation sources. |
48
49
  | `spreadsheet.*` | Typed maps/arrays | Sheet order, field-addressed sparse cells, formulas, styles, objects, names, and print state. |
49
50
  | `presentation.*` | Typed maps/arrays | Slide/master/layout order, ID-keyed scene objects, notes, transitions, and comments. |
@@ -92,11 +93,11 @@ Status: implemented in the browser library.
92
93
  Origin validation, permission enforcement, WebSocket rooms, durable Yrs
93
94
  persistence, ephemeral Awareness relay, reconnect repair, and service-owned
94
95
  durable update polling. Version-2 tickets bind actor display name and the
95
- durable store semantically authorizes Document comment-mode updates under
96
- the room lock before persistence or broadcast. Its browser adapter converts
97
- bounded base64 wire payloads back to the package's typed `Uint8Array`
98
- transport envelope and verifies the ready identity against the local
99
- session.
96
+ durable store semantically authorizes Document comment- and suggestion-mode
97
+ updates under the room lock before persistence or broadcast. Its browser
98
+ adapter converts bounded base64 wire payloads back to the package's typed
99
+ `Uint8Array` transport envelope and verifies the ready identity against the
100
+ local session.
100
101
  - Convergence, offline/reconnect, transport-boundary, presence, permission,
101
102
  StrictMode, framework-parity, ownership, backend persistence, room broadcast,
102
103
  ticket tamper, and read-only authorization tests.
@@ -109,9 +110,9 @@ Redis/NATS fan-out plus a distributed writer/lock policy.
109
110
 
110
111
  Status: browser collaboration foundation, participant roster, remote
111
112
  selection/caret projection, participant navigation, durable review-only
112
- comment mode, and native text/options/bounded structural paragraph/comment
113
- mutations implemented; rich native parity and suggestion authorization are
114
- pending.
113
+ comment and suggestion modes, immutable suggestion-decision audit, and native
114
+ text/options/bounded structural paragraph/comment mutations implemented; rich
115
+ native mutation parity remains pending.
115
116
 
116
117
  - TipTap is bound to `document.content` through
117
118
  `@tiptap/extension-collaboration`; StarterKit undo/redo is disabled and the
@@ -152,6 +153,19 @@ pending.
152
153
  immutable creation claim; removing an anchor through an authorized content
153
154
  edit retains a detached thread. Remote review changes do not enter local undo
154
155
  history.
156
+ - Authenticated Document `suggest` sessions create attributed insertion,
157
+ deletion, and replacement proposals as `documentChange` marks. Their
158
+ canonical projection is unchanged until an `edit` participant decides the
159
+ proposal. Suggesters may withdraw their own insertion proposals, but cannot
160
+ change canonical text, structure, non-suggestion formatting, options,
161
+ comments, bibliography, another actor's proposal, or a deletion proposal's
162
+ targeted canonical text.
163
+ - Accepting or rejecting a suggestion applies the tracked change and appends
164
+ one immutable actor-attributed record to `document.change-decisions` plus its
165
+ deterministic order root in the same transaction. Identical offline retries
166
+ converge, a second conflicting final decision fails closed, old Documents
167
+ without the additive roots remain readable, and a final decision clears
168
+ local history that could otherwise resurrect the decided mark.
155
169
  - Rust, CLI, MCP, and A3S Code expose `document-comment-create`,
156
170
  `document-comment-reply`, `document-comment-set-resolved`, and
157
171
  `document-comment-delete`. Projection schema v2 returns attributable threads,
@@ -159,16 +173,20 @@ pending.
159
173
  offsets, resolution, and detached state. Native and browser fixtures cover
160
174
  restart, reordered delivery, stale anchor guards, idempotent retries,
161
175
  ownership failures, and cross-language convergence.
162
- - The A3S Boot backend permits `edit` content/review updates and semantically
163
- validated Document `comment` review-only updates. Signed actor display name
164
- must match the session actor and every new record author. Forged roots,
165
- content, structure, options, authorship, order, claims, anchors, or foreign
166
- deletion fail before the durable store changes.
176
+ - The A3S Boot backend permits `edit` content/review updates, semantically
177
+ validated Document `comment` review-only updates, and semantically validated
178
+ Document `suggest` proposal updates. Signed actor identity and display name
179
+ must match every new comment, reply, or suggestion. Forged roots, canonical
180
+ content, structure, options, non-suggestion formatting, authorship, order,
181
+ claims, anchors, foreign deletion, or foreign suggestion changes fail before
182
+ the durable store changes.
167
183
 
168
184
  Remaining:
169
185
 
170
- - Add suggestion-only authorization and durable accept/reject decision audit
171
- records; `suggest` remains read-only until that model exists.
186
+ - Add closed typed native projection and mutation variants for creating
187
+ Document suggestions and deciding them through CLI/MCP/A3S Code. The current
188
+ typed native mutation set can synchronize the resulting Yjs state but does
189
+ not originate these two workflows.
172
190
  - Prove concurrent full-table, list-restructure, section, comment, and revision
173
191
  workflows, plus DOCX import/export after merged edits.
174
192
  - Expand native convergence from bounded paragraph edits to complete table,
@@ -369,11 +387,12 @@ destructive actions remain attributable, reviewable, and non-retryable.
369
387
  ### Phase 6: CLI, MCP, and coding agents
370
388
 
371
389
  Status: native Yrs replica store, resumable CLI/MCP event streams, `a3s code`
372
- projection, a host-injected live CLI transport session, and typed Markdown,
373
- Document content/comment, Spreadsheet cell, Presentation scene-element, PDF
374
- annotation/form-value, and PDF redaction/page-operation review mutation
375
- surfaces are implemented; the remaining format mutations and native presence
376
- projection are pending.
390
+ projection, a host-injected live CLI transport session, authenticated browser
391
+ suggestion-update authorization, and typed Markdown, Document content/comment,
392
+ Spreadsheet cell, Presentation scene-element, PDF annotation/form-value, and
393
+ PDF redaction/page-operation review mutation surfaces are implemented; typed
394
+ native Document suggestion/decision mutations, the remaining format mutations,
395
+ and native presence projection are pending.
377
396
 
378
397
  - Yrs `0.27.3` now uses 53-bit Yjs-compatible client IDs and exchanges standard
379
398
  Yjs v1 updates, state vectors, and y-sync `SyncStep1`/`Update` messages with
@@ -422,6 +441,13 @@ projection are pending.
422
441
  require `edit`; Document review mutations accept `edit` or `comment`, with
423
442
  ownership-restricted deletion in `comment`. Raw remote updates remain
424
443
  receivable in every mode so read-only peers still converge.
444
+ - Host-authenticated Document `suggest` transport updates are semantically
445
+ checked in Yrs before commit: only attributed insertion/deletion/replacement
446
+ proposals owned by the authenticated actor may be added or withdrawn, while
447
+ canonical state and every non-suggestion root remain protected. This is a
448
+ transport authorization path, not a `NativeOfficeCollaborationMutation`;
449
+ `collab mutate`, `office_collaboration_mutate`, and projection v2 do not yet
450
+ expose native typed suggestion creation or final-decision operations.
425
451
  - Rust `project`, `collab read`, and `office_collaboration_read` interpret the
426
452
  Office-owned browser schema inside Office rather than in a product host.
427
453
  Markdown returns its exact canonical source. Document returns bounded
@@ -483,8 +509,9 @@ Remaining:
483
509
 
484
510
  - Extend typed format-model mutations to Spreadsheet and remaining Presentation
485
511
  structural/rich-text operations plus PDF signatures; deepen Document
486
- mutations to additional nested structures and tracked suggestion decisions
487
- before enabling `suggest` for local mutation.
512
+ mutations to additional nested structures and add typed suggestion creation,
513
+ suggestion projection, and tracked final-decision operations for native
514
+ clients.
488
515
  - Project editor-visible presence and selection state while keeping Awareness
489
516
  ephemeral and outside native replica persistence.
490
517
 
@@ -506,5 +533,5 @@ valid update delivery orders.
506
533
  - Shipping a hosted collaboration backend inside the component package.
507
534
  - Treating awareness as authorization or durable audit history.
508
535
  - Hiding provider synchronization behind editor mount.
509
- - Claiming comments or suggestions are supported by a format until its durable
510
- review schema and accept/reject semantics are implemented.
536
+ - Claiming comments or suggestions are supported by a format that lacks a
537
+ durable review schema and, for suggestions, explicit accept/reject semantics.
package/README.md CHANGED
@@ -46,6 +46,9 @@ workflows, and a separate Rust automation plane.
46
46
  permissions, collaboration, and model providers.
47
47
  - **Collaborate across every editor** with shared Yjs/Yrs content, Awareness
48
48
  participants and remote locations, plus browser, CLI, MCP, and A3S Code peers.
49
+ Document reviewers can submit attributed insertions, deletions, and
50
+ replacements without directly changing canonical text; editors accept or
51
+ reject each proposal with an immutable shared decision record.
49
52
  - **Automate deterministically** through the native CLI, standard MCP server,
50
53
  or packaged Office Skill.
51
54
 
@@ -162,7 +165,15 @@ bindings, local undo, validated presence, and conflict-local typed mutations.
162
165
  An authenticated Document `comment` session can select text, create a durable
163
166
  thread, reply, resolve or reopen it, and delete only review records owned by
164
167
  its actor while canonical content remains read-only. The server independently
165
- validates that review-only boundary before persistence and broadcast.
168
+ validates that review-only boundary before persistence and broadcast. An
169
+ authenticated Document `suggest` session can submit attributed insertions,
170
+ deletions, and replacements while canonical text, structure, formatting,
171
+ options, comments, and other actors' suggestions remain protected. An `edit`
172
+ participant accepts or rejects a proposal and appends the final actor-attributed
173
+ decision to the immutable `document.change-decisions` audit trail. The A3S Boot
174
+ service validates both comment and suggestion semantics under the durable room
175
+ lock before it persists or broadcasts caller-supplied Yjs bytes. `suggest` on
176
+ non-Document formats remains receive-only.
166
177
  See the bilingual [real-time collaboration guide](https://a3s-lab.github.io/Office/docs/components/collaboration.html)
167
178
  for React, Vue, Web Component, reconnect, security, and native-agent setup.
168
179
  The repository also ships a runnable
@@ -1258,6 +1269,7 @@ without hard-coded return URLs.
1258
1269
 
1259
1270
  - [Live Playground](https://a3s-lab.github.io/Office/)
1260
1271
  - [Documentation center](https://a3s-lab.github.io/Office/docs/)
1272
+ - [A3S Office 0.8.0 documentation](https://a3s-lab.github.io/Office/docs/0.8.0/)
1261
1273
  - [A3S Office 0.7.3 documentation](https://a3s-lab.github.io/Office/docs/0.7.3/)
1262
1274
  - [A3S Office 0.7.2 documentation](https://a3s-lab.github.io/Office/docs/0.7.2/)
1263
1275
  - [A3S Office 0.7.1 documentation](https://a3s-lab.github.io/Office/docs/0.7.1/)
@@ -605,7 +605,7 @@ function DocumentTaskPane({ ariaLabel, title, description, closeLabel, className
605
605
  const DOCUMENT_CHANGE_ITEM_HEIGHT = 84;
606
606
  const DOCUMENT_CHANGE_ITEM_GAP = 7;
607
607
  const DOCUMENT_CHANGE_LIST_PADDING_TOP = 10;
608
- function DocumentChangesPanel({ editor, changes, trackChanges, onTrackChangesChange, onClose }) {
608
+ function DocumentChangesPanel({ editor, changes, decisions = [], trackChanges, suggestionOnly = false, onDecideChanges, onTrackChangesChange, onClose }) {
609
609
  const officeDialog = useOfficeDialog();
610
610
  const decisionButtonRefs = useRef(new Map());
611
611
  const pendingFocusRef = useRef(null);
@@ -654,7 +654,9 @@ function DocumentChangesPanel({ editor, changes, trackChanges, onTrackChangesCha
654
654
  decision
655
655
  };
656
656
  setRovingChangeKey(nextChangeKey);
657
- const handled = 'accept' === decision ? editor.commands.acceptDocumentChange(change.id) : editor.commands.rejectDocumentChange(change.id);
657
+ const handled = onDecideChanges ? onDecideChanges([
658
+ change
659
+ ], decision) : 'accept' === decision ? editor.commands.acceptDocumentChange(change.id) : editor.commands.rejectDocumentChange(change.id);
658
660
  if (!handled) {
659
661
  pendingFocusRef.current = null;
660
662
  setRovingChangeKey(currentChangeKey);
@@ -673,7 +675,8 @@ function DocumentChangesPanel({ editor, changes, trackChanges, onTrackChangesCha
673
675
  confirmLabel: '全部接受'
674
676
  });
675
677
  if (confirmed && !editor.isDestroyed) {
676
- editor.commands.acceptAllDocumentChanges();
678
+ if (onDecideChanges) onDecideChanges(changes, 'accept');
679
+ else editor.commands.acceptAllDocumentChanges();
677
680
  requestAnimationFrame(()=>{
678
681
  if (!editor.isDestroyed) editor.view.dom.focus();
679
682
  });
@@ -687,7 +690,8 @@ function DocumentChangesPanel({ editor, changes, trackChanges, onTrackChangesCha
687
690
  confirmTone: 'danger'
688
691
  });
689
692
  if (confirmed && !editor.isDestroyed) {
690
- editor.commands.rejectAllDocumentChanges();
693
+ if (onDecideChanges) onDecideChanges(changes, 'reject');
694
+ else editor.commands.rejectAllDocumentChanges();
691
695
  requestAnimationFrame(()=>{
692
696
  if (!editor.isDestroyed) editor.view.dom.focus();
693
697
  });
@@ -698,11 +702,11 @@ function DocumentChangesPanel({ editor, changes, trackChanges, onTrackChangesCha
698
702
  /*#__PURE__*/ jsxs(DocumentTaskPane, {
699
703
  className: "work-document-changes-panel",
700
704
  title: "修订审阅",
701
- description: changes.length ? `${changes.length} 项待处理` : '没有待处理的修订',
705
+ description: changes.length ? `${changes.length} 项待处理 · ${decisions.length} 项已决定` : decisions.length ? `没有待处理的修订 · ${decisions.length} 项已决定` : '没有待处理的修订',
702
706
  closeLabel: "关闭修订审阅",
703
707
  onClose: onClose,
704
708
  children: [
705
- changes.length > 0 && /*#__PURE__*/ jsxs("div", {
709
+ changes.length > 0 && !suggestionOnly && /*#__PURE__*/ jsxs("div", {
706
710
  className: "work-document-changes-bulk-actions",
707
711
  children: [
708
712
  /*#__PURE__*/ jsxs(button_Button, {
@@ -778,7 +782,7 @@ function DocumentChangesPanel({ editor, changes, trackChanges, onTrackChangesCha
778
782
  })
779
783
  ]
780
784
  }),
781
- /*#__PURE__*/ jsxs("div", {
785
+ !suggestionOnly && /*#__PURE__*/ jsxs("div", {
782
786
  children: [
783
787
  /*#__PURE__*/ jsxs(button_Button, {
784
788
  ref: (element)=>{
@@ -822,7 +826,7 @@ function DocumentChangesPanel({ editor, changes, trackChanges, onTrackChangesCha
822
826
  children: /*#__PURE__*/ jsx(CollectionState, {
823
827
  className: "work-document-changes-empty",
824
828
  icon: /*#__PURE__*/ jsx(FileDiff, {}),
825
- actions: /*#__PURE__*/ jsx(button_Button, {
829
+ actions: suggestionOnly ? void 0 : /*#__PURE__*/ jsx(button_Button, {
826
830
  size: "compact",
827
831
  tone: trackChanges ? 'quiet' : 'primary',
828
832
  onClick: ()=>onTrackChangesChange(!trackChanges),
@@ -830,8 +834,11 @@ function DocumentChangesPanel({ editor, changes, trackChanges, onTrackChangesCha
830
834
  }),
831
835
  tone: trackChanges ? 'info' : 'neutral',
832
836
  role: "status",
833
- children: trackChanges ? '正在记录新的改动。' : '当前没有记录新的改动。'
837
+ children: suggestionOnly ? '建议模式会自动记录带身份的新改动。' : trackChanges ? '正在记录新的改动。' : '当前没有记录新的改动。'
834
838
  })
839
+ }),
840
+ decisions.length > 0 && /*#__PURE__*/ jsx(DocumentChangeDecisionHistory, {
841
+ decisions: decisions
835
842
  })
836
843
  ]
837
844
  }),
@@ -839,6 +846,49 @@ function DocumentChangesPanel({ editor, changes, trackChanges, onTrackChangesCha
839
846
  ]
840
847
  });
841
848
  }
849
+ function DocumentChangeDecisionHistory({ decisions }) {
850
+ const visible = decisions.slice(-20).reverse();
851
+ return /*#__PURE__*/ jsxs("section", {
852
+ className: "work-document-change-decisions",
853
+ "aria-label": "修订决定记录",
854
+ children: [
855
+ /*#__PURE__*/ jsxs("header", {
856
+ children: [
857
+ /*#__PURE__*/ jsx("strong", {
858
+ children: "决定记录"
859
+ }),
860
+ /*#__PURE__*/ jsxs("span", {
861
+ children: [
862
+ decisions.length,
863
+ " 项"
864
+ ]
865
+ })
866
+ ]
867
+ }),
868
+ /*#__PURE__*/ jsx("ol", {
869
+ children: visible.map((decision)=>/*#__PURE__*/ jsxs("li", {
870
+ "data-document-change-decision": decision.decision,
871
+ children: [
872
+ /*#__PURE__*/ jsx("span", {
873
+ children: 'accept' === decision.decision ? '已接受' : '已拒绝'
874
+ }),
875
+ /*#__PURE__*/ jsx("strong", {
876
+ children: decision.text.trim() || '(空白字符)'
877
+ }),
878
+ /*#__PURE__*/ jsxs("small", {
879
+ children: [
880
+ decision.suggestedBy,
881
+ " → ",
882
+ decision.decidedBy,
883
+ decision.decidedAt ? ` · ${formatChangeDate(decision.decidedAt)}` : ''
884
+ ]
885
+ })
886
+ ]
887
+ }, decision.id))
888
+ })
889
+ ]
890
+ });
891
+ }
842
892
  function documentChangeDecisionKey(changeKey, decision) {
843
893
  return `${changeKey}:${decision}`;
844
894
  }
@@ -11696,7 +11746,7 @@ function headingLevelForShortcut(code, key) {
11696
11746
  if ('3' === value) return 3;
11697
11747
  return null;
11698
11748
  }
11699
- function DocumentToolbar({ editor, defaultRibbonCollapsed = false, reviewOnly = false, history, layoutOpen, layout, layoutFonts = [], navigationOpen, pageColor, showPageNumbers, showRulers, spellcheckEnabled, viewMode, zoom, pageChromeEditor, pageChromeEditingPart, pageChromeShowPageNumber, onRequestImage, onPageChromeEditingPartChange, onClosePageChrome, onTogglePageChromePageNumber, onToggleLayout, onLayoutChange, onOpenLayout, onToggleNavigation, onTogglePageNumbers, onToggleRulers, onPageColorChange, onToggleSpellcheck, onViewModeChange, onZoomChange, onZoomFit, onInsertSection, onInsertNote, onInsertCaption, onInsertCrossReference, citationsOpen, citationSourceCount, onToggleCitations, onInsertField, onRefreshFields, canInsertComment, onInsertComment, commentsOpen, commentCount, onToggleComments, trackChanges, changesOpen, changeCount, findReplaceMode, fileActions, onRibbonTabChange, onToggleTrackChanges, onToggleChanges, onOpenWordCount, onOpenFindReplace }) {
11749
+ function DocumentToolbar({ editor, defaultRibbonCollapsed = false, reviewOnly = false, suggestionOnly = false, history, layoutOpen, layout, layoutFonts = [], navigationOpen, pageColor, showPageNumbers, showRulers, spellcheckEnabled, viewMode, zoom, pageChromeEditor, pageChromeEditingPart, pageChromeShowPageNumber, onRequestImage, onPageChromeEditingPartChange, onClosePageChrome, onTogglePageChromePageNumber, onToggleLayout, onLayoutChange, onOpenLayout, onToggleNavigation, onTogglePageNumbers, onToggleRulers, onPageColorChange, onToggleSpellcheck, onViewModeChange, onZoomChange, onZoomFit, onInsertSection, onInsertNote, onInsertCaption, onInsertCrossReference, citationsOpen, citationSourceCount, onToggleCitations, onInsertField, onRefreshFields, canInsertComment, onInsertComment, commentsOpen, commentCount, onToggleComments, trackChanges, changesOpen, changeCount, findReplaceMode, fileActions, onRibbonTabChange, onToggleTrackChanges, onToggleChanges, onDecideChange, onOpenWordCount, onOpenFindReplace }) {
11700
11750
  const [activeTab, setActiveTab] = useState(reviewOnly ? 'review' : 'home');
11701
11751
  const officeDialog = useOfficeDialog();
11702
11752
  const prompt = officeDialog.prompt;
@@ -11901,7 +11951,7 @@ function DocumentToolbar({ editor, defaultRibbonCollapsed = false, reviewOnly =
11901
11951
  changes[index + 1]?.id,
11902
11952
  changes[index - 1]?.id
11903
11953
  ].filter((id)=>Boolean(id));
11904
- const handled = 'accept' === decision ? editor.commands.acceptDocumentChange(change.id) : editor.commands.rejectDocumentChange(change.id);
11954
+ const handled = onDecideChange ? onDecideChange(change, decision) : 'accept' === decision ? editor.commands.acceptDocumentChange(change.id) : editor.commands.rejectDocumentChange(change.id);
11905
11955
  if (!handled) return;
11906
11956
  const remaining = collectDocumentChanges(editor.state.doc);
11907
11957
  const next = nextIds.map((id)=>remaining.find((candidate)=>candidate.id === id)).find((candidate)=>Boolean(candidate));
@@ -11925,7 +11975,7 @@ function DocumentToolbar({ editor, defaultRibbonCollapsed = false, reviewOnly =
11925
11975
  adaptive: true,
11926
11976
  collapsible: true,
11927
11977
  fileActions: fileActions,
11928
- quickAccessActions: reviewOnly ? [] : [
11978
+ quickAccessActions: reviewOnly && !suggestionOnly ? [] : [
11929
11979
  {
11930
11980
  id: undoCommand.id,
11931
11981
  label: undoCommand.label,
@@ -12180,7 +12230,7 @@ function DocumentToolbar({ editor, defaultRibbonCollapsed = false, reviewOnly =
12180
12230
  })
12181
12231
  })
12182
12232
  }),
12183
- /*#__PURE__*/ jsxs(document_toolbar_RibbonGroup, {
12233
+ !suggestionOnly && /*#__PURE__*/ jsxs(document_toolbar_RibbonGroup, {
12184
12234
  label: "批注",
12185
12235
  priority: "high",
12186
12236
  children: [
@@ -12207,18 +12257,20 @@ function DocumentToolbar({ editor, defaultRibbonCollapsed = false, reviewOnly =
12207
12257
  })
12208
12258
  ]
12209
12259
  }),
12210
- !reviewOnly && /*#__PURE__*/ jsxs(Fragment, {
12260
+ (!reviewOnly || suggestionOnly) && /*#__PURE__*/ jsxs(Fragment, {
12211
12261
  children: [
12212
12262
  /*#__PURE__*/ jsxs(document_toolbar_RibbonGroup, {
12213
12263
  label: "修订",
12214
12264
  priority: "high",
12215
12265
  children: [
12216
12266
  /*#__PURE__*/ jsx(document_toolbar_ToolbarButton, {
12217
- label: "修订模式",
12267
+ label: suggestionOnly ? '建议模式' : '修订模式',
12218
12268
  displayLabel: true,
12219
12269
  shortcut: trackChangesCommand.shortcut?.label,
12220
12270
  ariaKeyShortcuts: trackChangesCommand.shortcut?.aria,
12221
- active: trackChanges,
12271
+ active: suggestionOnly || trackChanges,
12272
+ disabled: suggestionOnly,
12273
+ title: suggestionOnly ? '建议模式始终记录带身份的文字修订' : void 0,
12222
12274
  onClick: onToggleTrackChanges,
12223
12275
  children: /*#__PURE__*/ jsx(FileDiff, {
12224
12276
  size: 19
@@ -12235,7 +12287,7 @@ function DocumentToolbar({ editor, defaultRibbonCollapsed = false, reviewOnly =
12235
12287
  })
12236
12288
  ]
12237
12289
  }),
12238
- /*#__PURE__*/ jsxs(document_toolbar_RibbonGroup, {
12290
+ !suggestionOnly && /*#__PURE__*/ jsxs(document_toolbar_RibbonGroup, {
12239
12291
  label: "更改",
12240
12292
  priority: "high",
12241
12293
  children: [
@@ -14186,10 +14238,13 @@ function DocumentReviewConflictNotice({ conflicts, onDismiss }) {
14186
14238
  }
14187
14239
  const EMPTY_DOCUMENT_LAYOUT_FONTS = [];
14188
14240
  const EMPTY_DOCUMENT_EXTENSIONS = [];
14189
- function createTrackedDocumentChange(_kind) {
14241
+ function createTrackedDocumentChange(_kind, actor) {
14190
14242
  return {
14191
14243
  id: createWorkId('change'),
14192
- author: 'A3S Work 用户',
14244
+ ...actor ? {
14245
+ actorId: actor.id
14246
+ } : {},
14247
+ author: actor?.name ?? 'A3S Work 用户',
14193
14248
  date: new Date().toISOString()
14194
14249
  };
14195
14250
  }
@@ -14212,7 +14267,7 @@ function CollaborativeDocumentEditor(props) {
14212
14267
  if (!initial.current) initial.current = readWorkOfficeDocumentCollaboration(collaboration);
14213
14268
  const bridge = useRef({
14214
14269
  getContent: ()=>initial.current,
14215
- isTracking: ()=>Boolean(initial.current?.trackChanges),
14270
+ isTracking: ()=>'suggest' === collaboration.mode || Boolean(initial.current?.trackChanges),
14216
14271
  onContentChange: ()=>void 0,
14217
14272
  onTrackingChange: ()=>void 0
14218
14273
  });
@@ -14226,7 +14281,7 @@ function CollaborativeDocumentEditor(props) {
14226
14281
  workExtensions: {
14227
14282
  getContent: ()=>bridge.current.getContent(),
14228
14283
  isTracking: ()=>bridge.current.isTracking(),
14229
- createChange: createTrackedDocumentChange,
14284
+ createChange: (kind)=>createTrackedDocumentChange(kind, collaboration.actor),
14230
14285
  onContentChange: (content)=>bridge.current.onContentChange(content),
14231
14286
  onTrackingChange: (enabled)=>bridge.current.onTrackingChange(enabled)
14232
14287
  }
@@ -14256,11 +14311,13 @@ function CollaborativeDocumentEditor(props) {
14256
14311
  });
14257
14312
  }
14258
14313
  function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, collaborationBinding, collaborationBridge, collaborationInitialContent, content, extensions = EMPTY_DOCUMENT_EXTENSIONS, preview: requestedPreview, defaultRibbonCollapsed = false, saveStatus = '已自动保存', kernelWasmUrl, layoutFonts = EMPTY_DOCUMENT_LAYOUT_FONTS, fileActions, getSelectionMenuItems, onChange, onAgentRequest, onReviewConflict }) {
14259
- const reviewOnly = collaboration?.mode === 'comment' && !requestedPreview;
14260
- const collaborationInteractive = !collaboration || 'edit' === collaboration.mode || 'comment' === collaboration.mode;
14314
+ const commentOnly = collaboration?.mode === 'comment' && !requestedPreview;
14315
+ const suggestionOnly = collaboration?.mode === 'suggest' && Boolean(collaboration.actor) && !requestedPreview;
14316
+ const collaborationInteractive = !collaboration || 'edit' === collaboration.mode || 'comment' === collaboration.mode || 'suggest' === collaboration.mode && Boolean(collaboration.actor);
14261
14317
  const preview = requestedPreview || !collaborationInteractive;
14262
14318
  const canEditDocument = !preview && (!collaboration || 'edit' === collaboration.mode);
14263
14319
  const canCommentDocument = !preview && (!collaboration || 'edit' === collaboration.mode || 'comment' === collaboration.mode && Boolean(collaboration.actor));
14320
+ const canReviewDocumentChanges = canEditDocument || suggestionOnly;
14264
14321
  const readOnly = preview;
14265
14322
  const initialContentRef = useRef(collaborationInitialContent ?? content);
14266
14323
  const effectiveContent = collaboration ? initialContentRef.current : content;
@@ -14277,7 +14334,7 @@ function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, co
14277
14334
  const statisticsInvokerRef = useRef(null);
14278
14335
  const contentRef = useRef(effectiveContent);
14279
14336
  const onChangeRef = useRef(onChange);
14280
- const trackChangesRef = useRef(Boolean(effectiveContent.trackChanges));
14337
+ const trackChangesRef = useRef(suggestionOnly || Boolean(effectiveContent.trackChanges));
14281
14338
  const collaborationBindingRef = useRef(collaborationBinding);
14282
14339
  const normalizedContent = useMemo(()=>normalizeDocumentHtml(effectiveContent), [
14283
14340
  effectiveContent
@@ -14312,7 +14369,7 @@ function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, co
14312
14369
  const loadedLayoutFontIds = useDocumentLayoutFonts(layoutFonts);
14313
14370
  if (!collaboration) contentRef.current = content;
14314
14371
  onChangeRef.current = onChange;
14315
- trackChangesRef.current = Boolean(contentRef.current.trackChanges);
14372
+ trackChangesRef.current = suggestionOnly || Boolean(contentRef.current.trackChanges);
14316
14373
  const commitContentChange = useCallback((next)=>{
14317
14374
  const previous = contentRef.current;
14318
14375
  const binding = collaborationBindingRef.current;
@@ -14323,9 +14380,10 @@ function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, co
14323
14380
  collaborationBindingRef.current = collaborationBinding;
14324
14381
  if (collaborationBridge) collaborationBridge.current = {
14325
14382
  getContent: ()=>contentRef.current,
14326
- isTracking: ()=>trackChangesRef.current,
14383
+ isTracking: ()=>suggestionOnly || trackChangesRef.current,
14327
14384
  onContentChange: (next)=>commitContentChange(next),
14328
14385
  onTrackingChange: (trackChanges)=>{
14386
+ if (suggestionOnly) return;
14329
14387
  trackChangesRef.current = trackChanges;
14330
14388
  commitContentChange({
14331
14389
  ...contentRef.current,
@@ -14337,11 +14395,12 @@ function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, co
14337
14395
  ...collaborationBinding?.extensions ?? createWorkDocumentExtensions({
14338
14396
  getContent: ()=>contentRef.current,
14339
14397
  isTracking: ()=>trackChangesRef.current,
14340
- createChange: createTrackedDocumentChange,
14398
+ createChange: (kind)=>createTrackedDocumentChange(kind, collaboration?.actor),
14341
14399
  onContentChange: (next)=>{
14342
14400
  commitContentChange(next);
14343
14401
  },
14344
14402
  onTrackingChange: (trackChanges)=>{
14403
+ if (suggestionOnly) return;
14345
14404
  trackChangesRef.current = trackChanges;
14346
14405
  const next = {
14347
14406
  ...contentRef.current,
@@ -14364,12 +14423,12 @@ function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, co
14364
14423
  attributes: {
14365
14424
  'aria-label': '文档正文',
14366
14425
  'aria-multiline': 'true',
14367
- 'aria-readonly': reviewOnly ? 'true' : 'false',
14426
+ 'aria-readonly': commentOnly ? 'true' : 'false',
14368
14427
  role: 'textbox',
14369
14428
  spellcheck: 'true'
14370
14429
  }
14371
14430
  }), [
14372
- reviewOnly
14431
+ commentOnly
14373
14432
  ]);
14374
14433
  const editor = useEditor({
14375
14434
  extensions: editorExtensions,
@@ -14450,7 +14509,7 @@ function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, co
14450
14509
  const documentComments = useDocumentComments({
14451
14510
  actor: collaboration?.actor,
14452
14511
  contentRef,
14453
- deleteOwnOnly: reviewOnly,
14512
+ deleteOwnOnly: commentOnly,
14454
14513
  editor,
14455
14514
  enabled: canCommentDocument,
14456
14515
  onBeforeDraft: ()=>setTaskPane(null)
@@ -14463,7 +14522,7 @@ function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, co
14463
14522
  const unsubscribeChange = collaborationBinding.subscribe((change)=>{
14464
14523
  pendingCollaborationContentRef.current = change.content;
14465
14524
  contentRef.current = change.content;
14466
- trackChangesRef.current = Boolean(change.content.trackChanges);
14525
+ trackChangesRef.current = suggestionOnly || Boolean(change.content.trackChanges);
14467
14526
  setCollaborationVersion((value)=>value + 1);
14468
14527
  });
14469
14528
  const unsubscribeError = collaborationBinding.subscribeError((error)=>{
@@ -14488,7 +14547,7 @@ function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, co
14488
14547
  const next = pendingCollaborationContentRef.current;
14489
14548
  pendingCollaborationContentRef.current = void 0;
14490
14549
  contentRef.current = next;
14491
- trackChangesRef.current = Boolean(next.trackChanges);
14550
+ trackChangesRef.current = suggestionOnly || Boolean(next.trackChanges);
14492
14551
  onChangeRef.current(next);
14493
14552
  }, [
14494
14553
  collaborationVersion
@@ -14689,7 +14748,7 @@ function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, co
14689
14748
  editor.setEditable(!readOnly);
14690
14749
  const editorDom = editor.view.dom;
14691
14750
  editorDom.setAttribute('role', preview ? 'document' : 'textbox');
14692
- editorDom.setAttribute('aria-readonly', String(reviewOnly || preview));
14751
+ editorDom.setAttribute('aria-readonly', String(commentOnly || preview));
14693
14752
  if (preview) editorDom.removeAttribute('aria-multiline');
14694
14753
  else editorDom.setAttribute('aria-multiline', 'true');
14695
14754
  };
@@ -14699,10 +14758,10 @@ function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, co
14699
14758
  editor.off('mount', applyEditableState);
14700
14759
  };
14701
14760
  }, [
14761
+ commentOnly,
14702
14762
  editor,
14703
14763
  preview,
14704
- readOnly,
14705
- reviewOnly
14764
+ readOnly
14706
14765
  ]);
14707
14766
  useEffect(()=>{
14708
14767
  if (!editor) return;
@@ -14810,6 +14869,11 @@ function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, co
14810
14869
  });
14811
14870
  const finalPageNumber = pagination.pages.at(-1)?.pageNumber ?? Math.max(1, layout.pageNumberStart ?? 1) + pageCount - 1;
14812
14871
  const changes = collectDocumentChanges(editor.state.doc);
14872
+ const decideDocumentChanges = (selectedChanges, decision)=>{
14873
+ if (collaborationBinding) return collaborationBinding.decideChanges(editor, selectedChanges.map(({ id })=>id), decision);
14874
+ const ids = selectedChanges.map(({ id })=>id);
14875
+ return 'accept' === decision ? editor.commands.acceptDocumentChanges(ids) : editor.commands.rejectDocumentChanges(ids);
14876
+ };
14813
14877
  const citationCount = documentCitationCount(editor);
14814
14878
  const textStatistics = documentTextStatistics(editor);
14815
14879
  const paragraphIndent = documentParagraphIndent(editor);
@@ -14943,8 +15007,9 @@ function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, co
14943
15007
  }) : /*#__PURE__*/ jsx(DocumentToolbar, {
14944
15008
  editor: editor,
14945
15009
  defaultRibbonCollapsed: defaultRibbonCollapsed,
14946
- reviewOnly: reviewOnly,
14947
- history: collaborationBinding && !reviewOnly ? {
15010
+ reviewOnly: commentOnly || suggestionOnly,
15011
+ suggestionOnly: suggestionOnly,
15012
+ history: collaborationBinding && !commentOnly ? {
14948
15013
  canRedo: collaborationBinding.canRedo(),
14949
15014
  canUndo: collaborationBinding.canUndo(),
14950
15015
  redo: ()=>collaborationBinding.redo(),
@@ -14996,7 +15061,7 @@ function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, co
14996
15061
  commentsOpen: documentComments.open,
14997
15062
  commentCount: documentComments.comments.length,
14998
15063
  onToggleComments: ()=>void toggleCommentsPanel(),
14999
- trackChanges: Boolean(currentContent.trackChanges),
15064
+ trackChanges: suggestionOnly || Boolean(currentContent.trackChanges),
15000
15065
  changesOpen: changesOpen,
15001
15066
  changeCount: changes.length,
15002
15067
  findReplaceMode: findReplaceMode,
@@ -15005,10 +15070,14 @@ function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, co
15005
15070
  return requestEditorViewChange(keepCurrentPane ? taskPane : null, 'review' !== tab);
15006
15071
  },
15007
15072
  onToggleTrackChanges: ()=>{
15073
+ if (suggestionOnly) return;
15008
15074
  editor.commands.toggleDocumentTrackChanges();
15009
15075
  restoreDocumentBodyFocus();
15010
15076
  },
15011
15077
  onToggleChanges: ()=>void toggleTaskPane('changes'),
15078
+ onDecideChange: (change, decision)=>decideDocumentChanges([
15079
+ change
15080
+ ], decision),
15012
15081
  onOpenWordCount: openDocumentStatistics,
15013
15082
  onOpenFindReplace: openFindReplace
15014
15083
  }),
@@ -15174,12 +15243,12 @@ function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, co
15174
15243
  editor: editor,
15175
15244
  kind: "document"
15176
15245
  }),
15177
- !preview && /*#__PURE__*/ jsx(DocumentSelectionToolbar, {
15246
+ !preview && (canEditDocument || canCommentDocument) && /*#__PURE__*/ jsx(DocumentSelectionToolbar, {
15178
15247
  editor: editor,
15179
15248
  canInsertComment: documentComments.canInsert,
15180
15249
  layoutFonts: layoutFonts,
15181
15250
  onInsertComment: ()=>void startCommentDraft(),
15182
- reviewOnly: reviewOnly
15251
+ reviewOnly: commentOnly
15183
15252
  })
15184
15253
  ]
15185
15254
  }),
@@ -15259,11 +15328,14 @@ function DocumentEditorSurface({ artifactId, autoFocus = true, collaboration, co
15259
15328
  onClose: closeTaskPane,
15260
15329
  onDirtyChange: setCitationsDirty
15261
15330
  }),
15262
- canEditDocument && changesOpen && /*#__PURE__*/ jsx(DocumentChangesPanel, {
15331
+ canReviewDocumentChanges && changesOpen && /*#__PURE__*/ jsx(DocumentChangesPanel, {
15263
15332
  editor: editor,
15264
15333
  changes: changes,
15265
- trackChanges: Boolean(currentContent.trackChanges),
15266
- onTrackChangesChange: (enabled)=>editor.commands.setDocumentTrackChanges(enabled),
15334
+ decisions: currentContent.changeDecisions,
15335
+ trackChanges: suggestionOnly || Boolean(currentContent.trackChanges),
15336
+ suggestionOnly: suggestionOnly,
15337
+ onDecideChanges: canEditDocument ? decideDocumentChanges : void 0,
15338
+ onTrackChangesChange: (enabled)=>suggestionOnly ? false : editor.commands.setDocumentTrackChanges(enabled),
15267
15339
  onClose: closeTaskPane
15268
15340
  }),
15269
15341
  canEditDocument && layoutOpen && section && /*#__PURE__*/ jsx(DocumentLayoutPanel, {