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/lib/client.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* ./shelf <- src/client/70-shelf.js
|
|
13
13
|
* ./boot <- src/client/90-boot.js
|
|
14
14
|
*
|
|
15
|
-
* Source digest:
|
|
15
|
+
* Source digest: d5cb4638b3c119ba
|
|
16
16
|
*
|
|
17
17
|
* This is the hand-written lazy-CJS bundle the DSH client module system
|
|
18
18
|
* expects: it calls `window.__ModuleLoader__.load({ id, factory })` with the
|
|
@@ -656,6 +656,7 @@ window.__ModuleLoader__.load({
|
|
|
656
656
|
|
|
657
657
|
/** GET /doc/ir?projectId=&path= -> { doc } */
|
|
658
658
|
docIR: (first, second) => call('/doc/ir', { query: pairObject(first, second) }),
|
|
659
|
+
convertWord: (first, second) => call('/doc/convert-word', { body: pairObject(first, second) }),
|
|
659
660
|
|
|
660
661
|
/** GET /covers?projectId= -> { covers } —— 批量,书架一次拿全(CONTRACT §15.3)。 */
|
|
661
662
|
getCovers: (projectId) => call('/covers', { query: asObject(projectId, 'projectId') }),
|
|
@@ -698,6 +699,7 @@ window.__ModuleLoader__.load({
|
|
|
698
699
|
/** POST /chat/clear { projectId, path } -> { messages: [] }(§13.3) */
|
|
699
700
|
chatClear: (first, second) => call('/chat/clear', { body: pairObject(first, second) }),
|
|
700
701
|
chatModels: () => call('/chat/models'),
|
|
702
|
+
contextStatus: (first, second) => call('/doc/context-status', { query: pairObject(first, second) }),
|
|
701
703
|
docVersion: (first, second) => call('/doc/version', { query: pairObject(first, second) }),
|
|
702
704
|
openOffice: (first, second) => call('/doc/open-office', { body: pairObject(first, second) }),
|
|
703
705
|
|
|
@@ -1290,7 +1292,9 @@ window.__ModuleLoader__.load({
|
|
|
1290
1292
|
throw new Error(`宿主返回的不是 JSON:${path}`);
|
|
1291
1293
|
}
|
|
1292
1294
|
if (!data || data.ok !== true) {
|
|
1293
|
-
|
|
1295
|
+
const error = new Error((data && data.error) || `请求失败:${path}`);
|
|
1296
|
+
error.body = data;
|
|
1297
|
+
throw error;
|
|
1294
1298
|
}
|
|
1295
1299
|
return data;
|
|
1296
1300
|
}
|
|
@@ -1376,6 +1380,16 @@ window.__ModuleLoader__.load({
|
|
|
1376
1380
|
async chatModels() {
|
|
1377
1381
|
return typeof api.chatModels === 'function' ? invokeApi(api.chatModels, [[]]) : getJSON('/chat/models');
|
|
1378
1382
|
},
|
|
1383
|
+
async contextStatus(projectId, path) {
|
|
1384
|
+
const data = typeof api.contextStatus === 'function' ? await invokeApi(api.contextStatus, [[{ projectId, path }]])
|
|
1385
|
+
: await getJSON('/doc/context-status', { projectId, path });
|
|
1386
|
+
return data.progress || { stage: 'idle' };
|
|
1387
|
+
},
|
|
1388
|
+
async convertWord(projectId, path) {
|
|
1389
|
+
const data = typeof api.convertWord === 'function' ? await invokeApi(api.convertWord, [[{ projectId, path }]])
|
|
1390
|
+
: await postJSON('/doc/convert-word', { projectId, path });
|
|
1391
|
+
return bodyOf(data, ['entry']);
|
|
1392
|
+
},
|
|
1379
1393
|
async openOffice(projectId, path) {
|
|
1380
1394
|
return typeof api.openOffice === 'function' ? invokeApi(api.openOffice, [[{ projectId, path }]]) : postJSON('/doc/open-office', { projectId, path });
|
|
1381
1395
|
},
|
|
@@ -1408,12 +1422,13 @@ window.__ModuleLoader__.load({
|
|
|
1408
1422
|
return bodyOf(await postJSON('/revise/iterate', input), ['revision']);
|
|
1409
1423
|
},
|
|
1410
1424
|
|
|
1411
|
-
async acceptRevision(revisionId) {
|
|
1425
|
+
async acceptRevision(revisionId, options = {}) {
|
|
1426
|
+
const input = { revisionId, ...(options.layoutRiskToken ? { layoutRiskToken: options.layoutRiskToken } : {}) };
|
|
1412
1427
|
if (typeof api.acceptRevision === 'function') {
|
|
1413
|
-
const out = await invokeApi(api.acceptRevision, [[revisionId], [
|
|
1428
|
+
const out = await invokeApi(api.acceptRevision, options.layoutRiskToken ? [[input]] : [[revisionId], [input]]);
|
|
1414
1429
|
return { revision: bodyOf(out, ['revision']), written: out && out.written ? out.written : null };
|
|
1415
1430
|
}
|
|
1416
|
-
const data = await postJSON('/revision/accept',
|
|
1431
|
+
const data = await postJSON('/revision/accept', input);
|
|
1417
1432
|
return { revision: data.revision || null, written: data.written || null };
|
|
1418
1433
|
},
|
|
1419
1434
|
|
|
@@ -1559,6 +1574,8 @@ window.__ModuleLoader__.load({
|
|
|
1559
1574
|
this.listeners = [];
|
|
1560
1575
|
this.lastWrite = null;
|
|
1561
1576
|
this.seq = 0;
|
|
1577
|
+
this.changeSeq = 0;
|
|
1578
|
+
this.itemChanges = new Map();
|
|
1562
1579
|
}
|
|
1563
1580
|
|
|
1564
1581
|
subscribe(listener) {
|
|
@@ -1618,6 +1635,7 @@ window.__ModuleLoader__.load({
|
|
|
1618
1635
|
if (!changed) return this.all();
|
|
1619
1636
|
this.seq += 1;
|
|
1620
1637
|
this.items = [];
|
|
1638
|
+
this.itemChanges = new Map();
|
|
1621
1639
|
this.error = '';
|
|
1622
1640
|
this.loading = false;
|
|
1623
1641
|
this.emit();
|
|
@@ -1634,11 +1652,13 @@ window.__ModuleLoader__.load({
|
|
|
1634
1652
|
this.loading = true;
|
|
1635
1653
|
this.error = '';
|
|
1636
1654
|
this.emit();
|
|
1637
|
-
const seq = ++this.seq;
|
|
1655
|
+
const seq = ++this.seq, changeSeq = this.changeSeq;
|
|
1638
1656
|
try {
|
|
1639
1657
|
const list = await this.transport.listRevisions(this.projectId, this.docPath);
|
|
1640
1658
|
if (seq !== this.seq) return this.all();
|
|
1641
|
-
this.items
|
|
1659
|
+
const recent = this.items.filter(item => item?.id && (this.itemChanges.get(item.id) || 0) > changeSeq);
|
|
1660
|
+
const recentIds = new Set(recent.map(item => item.id));
|
|
1661
|
+
this.items = recent.concat((Array.isArray(list) ? list : []).filter(item => !recentIds.has(item?.id)));
|
|
1642
1662
|
this.loading = false;
|
|
1643
1663
|
this.emit();
|
|
1644
1664
|
return this.all();
|
|
@@ -1653,11 +1673,14 @@ window.__ModuleLoader__.load({
|
|
|
1653
1673
|
|
|
1654
1674
|
_upsert(revision) {
|
|
1655
1675
|
if (!revision || typeof revision !== 'object') return null;
|
|
1676
|
+
if ((revision.projectId && revision.projectId !== this.projectId)
|
|
1677
|
+
|| (revision.docPath && revision.docPath !== this.docPath)) return null;
|
|
1656
1678
|
if (!revision.id) {
|
|
1657
1679
|
this.items = [revision].concat(this.items);
|
|
1658
1680
|
this.emit();
|
|
1659
1681
|
return revision;
|
|
1660
1682
|
}
|
|
1683
|
+
this.itemChanges.set(revision.id, ++this.changeSeq);
|
|
1661
1684
|
const index = this.items.findIndex((item) => item && item.id === revision.id);
|
|
1662
1685
|
if (index >= 0) {
|
|
1663
1686
|
this.items[index] = Object.assign({}, this.items[index], revision);
|
|
@@ -1691,10 +1714,12 @@ window.__ModuleLoader__.load({
|
|
|
1691
1714
|
return revision;
|
|
1692
1715
|
}
|
|
1693
1716
|
|
|
1694
|
-
async accept(revisionId) {
|
|
1717
|
+
async accept(revisionId, options) {
|
|
1718
|
+
const projectId = this.projectId, docPath = this.docPath;
|
|
1695
1719
|
this.setBusy(`rev:${revisionId}`, true);
|
|
1696
1720
|
try {
|
|
1697
|
-
const out = await this.transport.acceptRevision(revisionId);
|
|
1721
|
+
const out = await this.transport.acceptRevision(revisionId, options);
|
|
1722
|
+
if (this.projectId !== projectId || this.docPath !== docPath) return out;
|
|
1698
1723
|
if (out && out.revision && typeof out.revision === 'object') this._upsert(out.revision);
|
|
1699
1724
|
else {
|
|
1700
1725
|
const target = this.items.find((item) => item && item.id === revisionId);
|
|
@@ -1703,15 +1728,20 @@ window.__ModuleLoader__.load({
|
|
|
1703
1728
|
}
|
|
1704
1729
|
if (out && out.written) this.lastWrite = out.written;
|
|
1705
1730
|
return out;
|
|
1731
|
+
} catch (error) {
|
|
1732
|
+
if (this.projectId === projectId && this.docPath === docPath && error?.body?.revision) this._upsert(error.body.revision);
|
|
1733
|
+
throw error;
|
|
1706
1734
|
} finally {
|
|
1707
1735
|
this.setBusy(`rev:${revisionId}`, false);
|
|
1708
1736
|
}
|
|
1709
1737
|
}
|
|
1710
1738
|
|
|
1711
1739
|
async reject(revisionId) {
|
|
1740
|
+
const projectId = this.projectId, docPath = this.docPath;
|
|
1712
1741
|
this.setBusy(`rev:${revisionId}`, true);
|
|
1713
1742
|
try {
|
|
1714
1743
|
const out = await this.transport.rejectRevision(revisionId);
|
|
1744
|
+
if (this.projectId !== projectId || this.docPath !== docPath) return out;
|
|
1715
1745
|
if (out && out.revision && typeof out.revision === 'object') this._upsert(out.revision);
|
|
1716
1746
|
else {
|
|
1717
1747
|
const target = this.items.find((item) => item && item.id === revisionId);
|
|
@@ -1725,9 +1755,11 @@ window.__ModuleLoader__.load({
|
|
|
1725
1755
|
}
|
|
1726
1756
|
|
|
1727
1757
|
async restore(revisionId) {
|
|
1758
|
+
const projectId = this.projectId, docPath = this.docPath;
|
|
1728
1759
|
this.setBusy(`rev:${revisionId}`, true);
|
|
1729
1760
|
try {
|
|
1730
1761
|
const out = await this.transport.restoreRevision(revisionId);
|
|
1762
|
+
if (this.projectId !== projectId || this.docPath !== docPath) return out;
|
|
1731
1763
|
if (out && out.revision && typeof out.revision === 'object') this._upsert(out.revision);
|
|
1732
1764
|
return out;
|
|
1733
1765
|
} finally {
|
|
@@ -1760,15 +1792,17 @@ window.__ModuleLoader__.load({
|
|
|
1760
1792
|
* (单条撤回会破坏共用快照的语义)。accept / reject 仍可逐条兜底。
|
|
1761
1793
|
*/
|
|
1762
1794
|
async bulk(ids, action) {
|
|
1795
|
+
const projectId = this.projectId, docPath = this.docPath;
|
|
1796
|
+
const isCurrent = () => this.projectId === projectId && this.docPath === docPath;
|
|
1763
1797
|
const list = (Array.isArray(ids) ? ids : []).filter(Boolean);
|
|
1764
1798
|
if (list.length === 0) return [];
|
|
1765
1799
|
try {
|
|
1766
1800
|
const results = await this.transport.bulkRevisions(list, action);
|
|
1767
|
-
await this.load();
|
|
1801
|
+
if (isCurrent()) await this.load();
|
|
1768
1802
|
return results;
|
|
1769
1803
|
} catch (error) {
|
|
1770
1804
|
if (action === 'restore') {
|
|
1771
|
-
await this.load().catch(() => {});
|
|
1805
|
+
if (isCurrent()) await this.load().catch(() => {});
|
|
1772
1806
|
throw error;
|
|
1773
1807
|
}
|
|
1774
1808
|
const results = [];
|
|
@@ -1782,7 +1816,7 @@ window.__ModuleLoader__.load({
|
|
|
1782
1816
|
results.push({ revisionId: id, ok: false, error: normalizeError(inner) });
|
|
1783
1817
|
}
|
|
1784
1818
|
}
|
|
1785
|
-
await this.load();
|
|
1819
|
+
if (isCurrent()) await this.load();
|
|
1786
1820
|
return results;
|
|
1787
1821
|
}
|
|
1788
1822
|
}
|
|
@@ -1875,8 +1909,13 @@ window.__ModuleLoader__.load({
|
|
|
1875
1909
|
const ops = opsFor(revision);
|
|
1876
1910
|
const busy = p.busy === true || (p.busyIds || []).indexOf(revision.id) >= 0;
|
|
1877
1911
|
const canDecide = status === 'pending';
|
|
1912
|
+
const canAcceptLayoutRisk = canDecide && typeof revision.layoutWarning?.token === 'string';
|
|
1913
|
+
const savedError = p.acceptError || revision.error || '';
|
|
1914
|
+
// Older saved cards may still recommend the removed copy feature.
|
|
1915
|
+
const acceptError = /流式(?:编辑)?副本/.test(savedError)
|
|
1916
|
+
? '此修订可能造成文字重叠或版式异常。修订尚未写入。请点击文档标题右侧的 Office 图标,手动打开文档调整排版。' : savedError;
|
|
1917
|
+
const layoutWarning = canDecide && /手动打开文档调整排版/.test(acceptError);
|
|
1878
1918
|
const unsafe = /\.docx$/i.test(revision.docPath || '') && revision.selectionVersion !== 'word-selection-v2' && status !== 'accepted';
|
|
1879
|
-
const outdated = status === 'pending' && p.currentHash && revision.docHash && p.currentHash !== revision.docHash;
|
|
1880
1919
|
const group = typeof p.writeGroup === 'function' ? p.writeGroup(revision) : writeGroupOf([revision], revision);
|
|
1881
1920
|
const groupSize = Array.isArray(group) ? group.length : 1;
|
|
1882
1921
|
|
|
@@ -1906,18 +1945,22 @@ window.__ModuleLoader__.load({
|
|
|
1906
1945
|
collapsed ? h('span', { key: 'status', className: P + 'RevCollapseStatus' }, STATUS_TEXT[status] || status) : null,
|
|
1907
1946
|
]),
|
|
1908
1947
|
unsafe ? h('div', { key: 'unsafe', className: P + 'Err' }, '旧版选区可能漏字,请重新选择文字后生成修订。') : null,
|
|
1909
|
-
|
|
1910
|
-
? h('div', { key: 'err', className: P + 'Err' },
|
|
1948
|
+
acceptError
|
|
1949
|
+
? h('div', { key: 'err', className: P + 'Err', 'data-revision-layout-warning': layoutWarning ? '1' : undefined }, `${layoutWarning ? '提示' : '错误'}:${acceptError}`)
|
|
1911
1950
|
: null,
|
|
1951
|
+
status === 'accepted' && revision.written?.layoutRiskAccepted
|
|
1952
|
+
? h('div', { key: 'layout-reminder', className: P + 'Hint', role: 'status', 'data-revision-layout-warning': '1' },
|
|
1953
|
+
'已写入修订,可能存在文字重叠或版式异常。请点击文档标题右侧的 Office 图标,手动打开文档调整排版。')
|
|
1954
|
+
: null,
|
|
1955
|
+
p.progress ? h('div', { key: 'progress', 'data-revision-write-progress': '1' }, p.progress) : null,
|
|
1912
1956
|
!collapsed ? h('div', { key: 'diff', className: P + 'RevDiffBox', 'aria-label': '词级差异' }, h(InlineDiff, { ops })) : null,
|
|
1913
1957
|
!collapsed ? h('div', { key: 'actions', className: P + 'RevActions' }, [
|
|
1914
|
-
canDecide ? button('accept', '接受', p.onAccept, P + 'BtnPrimary', unsafe ||
|
|
1915
|
-
outdated ? '文档已更新,请重新选择文字后生成修订' : undefined) : null,
|
|
1958
|
+
canDecide ? button('accept', canAcceptLayoutRisk ? '仍然接受并写入' : '接受', p.onAccept, P + 'BtnPrimary', unsafe || p.writeBusy) : null,
|
|
1916
1959
|
status === 'rejected'
|
|
1917
|
-
? button('restore', '恢复为待处理', p.onRestore, null,
|
|
1960
|
+
? button('restore', '恢复为待处理', p.onRestore, null, p.writeBusy)
|
|
1918
1961
|
: null,
|
|
1919
1962
|
status === 'accepted' && groupSize <= 1
|
|
1920
|
-
? button('restore', '撤回', p.onRestore, null,
|
|
1963
|
+
? button('restore', '撤回', p.onRestore, null, p.writeBusy, '撤回这次写入并还原文件')
|
|
1921
1964
|
: null,
|
|
1922
1965
|
status === 'accepted' && groupSize > 1
|
|
1923
1966
|
? h('button', {
|
|
@@ -1925,11 +1968,11 @@ window.__ModuleLoader__.load({
|
|
|
1925
1968
|
type: 'button',
|
|
1926
1969
|
className: P + 'Btn',
|
|
1927
1970
|
title: '这几条修订共用一次写回快照,必须整组还原',
|
|
1928
|
-
disabled: busy || typeof p.onRestoreGroup !== 'function',
|
|
1971
|
+
disabled: busy || p.writeBusy || typeof p.onRestoreGroup !== 'function',
|
|
1929
1972
|
onClick: typeof p.onRestoreGroup === 'function' ? () => p.onRestoreGroup(group) : undefined,
|
|
1930
1973
|
}, `撤回本次批量写入(${groupSize} 条)`)
|
|
1931
1974
|
: null,
|
|
1932
|
-
status !== 'accepted' ? button('regen', '重新生成', p.onRegenerate, null, unsafe
|
|
1975
|
+
status !== 'accepted' ? button('regen', '重新生成', p.onRegenerate, null, unsafe) : null,
|
|
1933
1976
|
button('delete', '删除', p.onDelete, null, false, '删除这张修订卡片,不撤销已写入文档的修改'),
|
|
1934
1977
|
]) : null,
|
|
1935
1978
|
]);
|
|
@@ -2081,10 +2124,12 @@ window.__ModuleLoader__.load({
|
|
|
2081
2124
|
border-radius:var(--dsw-radius-sm,5px);cursor:pointer}
|
|
2082
2125
|
.${P}Btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
2083
2126
|
.${P}Btn:disabled{opacity:.5;cursor:default}
|
|
2084
|
-
.${P}BtnPrimary{background:var(--dsw-alias-
|
|
2085
|
-
color
|
|
2086
|
-
|
|
2087
|
-
|
|
2127
|
+
.${P}BtnPrimary{background:var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary));
|
|
2128
|
+
border-color:var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary));
|
|
2129
|
+
color:var(--dsw-alias-label-primary-foreground,#fff)}
|
|
2130
|
+
.${P}BtnPrimary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary)));
|
|
2131
|
+
border-color:var(--dsw-alias-button-primary-hover,var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary)));
|
|
2132
|
+
color:var(--dsw-alias-label-primary-foreground,#fff)}
|
|
2088
2133
|
.${P}BtnGhost{border-color:transparent;background:transparent;color:var(--dsw-alias-label-secondary)}
|
|
2089
2134
|
.${P}Err{color:var(--dsw-alias-state-error-primary);font-size:12px;margin-top:4px}
|
|
2090
2135
|
`;
|
|
@@ -2142,7 +2187,7 @@ window.__ModuleLoader__.load({
|
|
|
2142
2187
|
const DISPLAY_NAME = '企业文档助手';
|
|
2143
2188
|
|
|
2144
2189
|
/** 客户端版本号,与 package.json 的 version 同步维护(`/state` 不回传版本时用它)。 */
|
|
2145
|
-
const CLIENT_VERSION = '0.1.
|
|
2190
|
+
const CLIENT_VERSION = '0.1.18';
|
|
2146
2191
|
|
|
2147
2192
|
/** 设置窗口的分区。 */
|
|
2148
2193
|
const SECTIONS = [
|
|
@@ -3626,7 +3671,8 @@ window.__ModuleLoader__.load({
|
|
|
3626
3671
|
for (const item of items) {
|
|
3627
3672
|
const expand = item.action === 'expand' || /^扩写/.test(item.instruction || '');
|
|
3628
3673
|
const key = expand ? JSON.stringify([item.docHash, item.original, item.anchor?.kind,
|
|
3629
|
-
item.anchor?.blockId, item.anchor?.srcStart, item.anchor?.srcEnd, item.anchor?.sheet, item.anchor?.cell
|
|
3674
|
+
item.anchor?.blockId, item.anchor?.srcStart, item.anchor?.srcEnd, item.anchor?.sheet, item.anchor?.cell,
|
|
3675
|
+
item.anchor?.segments?.map(segment => [segment.blockId, segment.srcStart, segment.srcEnd])]) : item.id;
|
|
3630
3676
|
let group = byKey.get(key);
|
|
3631
3677
|
if (!group) { group = { key, items: [] }; byKey.set(key, group); groups.push(group); }
|
|
3632
3678
|
group.items.push(item);
|
|
@@ -3637,14 +3683,19 @@ window.__ModuleLoader__.load({
|
|
|
3637
3683
|
|
|
3638
3684
|
function comparisonChoices(group, currentHash) {
|
|
3639
3685
|
if (group.items.some(item => item.status === 'accepted')) return [];
|
|
3640
|
-
return group.items.filter(item => item.status === 'pending' && !item.deletedAt
|
|
3641
|
-
&& (!currentHash || !item.docHash || item.docHash === currentHash));
|
|
3686
|
+
return group.items.filter(item => item.status === 'pending' && !item.deletedAt);
|
|
3642
3687
|
}
|
|
3643
3688
|
|
|
3644
3689
|
function captureRequest(value) {
|
|
3645
3690
|
const request = JSON.parse(JSON.stringify(value));
|
|
3646
|
-
|
|
3647
|
-
|
|
3691
|
+
const freeze = item => {
|
|
3692
|
+
if (item && typeof item === 'object') {
|
|
3693
|
+
Object.values(item).forEach(freeze);
|
|
3694
|
+
Object.freeze(item);
|
|
3695
|
+
}
|
|
3696
|
+
return item;
|
|
3697
|
+
};
|
|
3698
|
+
return freeze(request);
|
|
3648
3699
|
}
|
|
3649
3700
|
|
|
3650
3701
|
// =========================================================================
|
|
@@ -4224,7 +4275,7 @@ window.__ModuleLoader__.load({
|
|
|
4224
4275
|
}
|
|
4225
4276
|
|
|
4226
4277
|
const LAYOUT_FORMATS = { docx: true, pptx: true, xlsx: true, pdf: true };
|
|
4227
|
-
const LAYOUT_ONLY_FORMATS = { docx: true, pptx: true, pdf: true };
|
|
4278
|
+
const LAYOUT_ONLY_FORMATS = { doc: true, docx: true, pptx: true, pdf: true };
|
|
4228
4279
|
|
|
4229
4280
|
function mediaUrl(route, projectId, path, extra) {
|
|
4230
4281
|
const q = new URLSearchParams();
|
|
@@ -4343,13 +4394,39 @@ window.__ModuleLoader__.load({
|
|
|
4343
4394
|
return { picked, matched, srcStart: matched[0], srcEnd: matched[matched.length - 1] + 1 };
|
|
4344
4395
|
}
|
|
4345
4396
|
|
|
4397
|
+
function wordLineGeometry(line) {
|
|
4398
|
+
const box = line && line.layoutBox;
|
|
4399
|
+
return box && box.w > 0 ? { x: (line.x - box.x) / box.w, w: line.w / box.w }
|
|
4400
|
+
: { x: Number(line && line.x), w: Number(line && line.w) };
|
|
4401
|
+
}
|
|
4402
|
+
|
|
4403
|
+
/** Column membership supplies layout context only; source matching stays exact. */
|
|
4404
|
+
function sameWordColumn(a, b) {
|
|
4405
|
+
const first = wordLineGeometry(a), next = wordLineGeometry(b);
|
|
4406
|
+
if (![first.x, first.w, next.x, next.w].every(Number.isFinite) || first.w <= 0 || next.w <= 0) return false;
|
|
4407
|
+
const overlap = Math.min(first.x + first.w, next.x + next.w) - Math.max(first.x, next.x);
|
|
4408
|
+
return Math.abs(first.x - next.x) <= .065 && overlap > Math.min(first.w, next.w) * .45;
|
|
4409
|
+
}
|
|
4410
|
+
|
|
4411
|
+
function exactWordGlyphs(line) {
|
|
4412
|
+
if (!line || line.ocr || line.precise !== true || !Array.isArray(line.glyphs) || !line.glyphs.length) return false;
|
|
4413
|
+
let cursor = 0;
|
|
4414
|
+
for (const glyph of line.glyphs) {
|
|
4415
|
+
if (glyph.precise === false || !Number.isInteger(glyph.from) || !Number.isInteger(glyph.to)
|
|
4416
|
+
|| glyph.from !== cursor || glyph.to <= cursor || !Number.isFinite(glyph.x) || !Number.isFinite(glyph.w) || glyph.w < 0) return false;
|
|
4417
|
+
cursor = glyph.to;
|
|
4418
|
+
}
|
|
4419
|
+
return cursor === String(line.text || '').length;
|
|
4420
|
+
}
|
|
4421
|
+
|
|
4346
4422
|
function bindWordLayoutLines(blocks, lines) {
|
|
4347
4423
|
const parts = sourceParts({ blocks });
|
|
4348
4424
|
const source = parts.map((part) => part.text).join('\n');
|
|
4349
4425
|
const index = wordTextIndex(source);
|
|
4350
4426
|
const beginnings = parts.map(part => wordTextIndex(part.text).compact);
|
|
4351
4427
|
const list = Array.isArray(lines) ? lines : [];
|
|
4352
|
-
const entries = list.map((
|
|
4428
|
+
const entries = list.map((rawLine, order) => {
|
|
4429
|
+
const { srcStart, srcEnd, srcMap, blockId, wordCell, wordParagraphs, wordPrefix, wordLayout, ...line } = rawLine || {};
|
|
4353
4430
|
const text = String(line && line.text || '');
|
|
4354
4431
|
let prefix = 0;
|
|
4355
4432
|
let picked = wordTextIndex(text);
|
|
@@ -4394,16 +4471,56 @@ window.__ModuleLoader__.load({
|
|
|
4394
4471
|
window.forEach((entry, i) => { entry.at = placements[i]; });
|
|
4395
4472
|
}
|
|
4396
4473
|
}
|
|
4474
|
+
// Multi-column reports interleave author details with body lines in PDF
|
|
4475
|
+
// reading order. Match a unique, exact continuation in its own column,
|
|
4476
|
+
// and require any already mapped neighbour to agree with the placement.
|
|
4477
|
+
for (const entry of ordered) {
|
|
4478
|
+
if (entry.at >= 0 || !exactWordGlyphs(entry.line)) continue;
|
|
4479
|
+
const column = ordered.filter(other => exactWordGlyphs(other.line)
|
|
4480
|
+
&& Number(other.line.page || 1) === Number(entry.line.page || 1) && sameWordColumn(entry.line, other.line));
|
|
4481
|
+
const center = column.indexOf(entry), placements = new Set();
|
|
4482
|
+
for (let size = 2; size <= Math.min(8, column.length); size += 1) {
|
|
4483
|
+
for (let from = Math.max(0, center - size + 1); from <= center && from + size <= column.length; from += 1) {
|
|
4484
|
+
const window = column.slice(from, from + size);
|
|
4485
|
+
if (!window.some(other => other !== entry && other.at >= 0)) continue;
|
|
4486
|
+
const needle = window.map(other => other.picked.compact).join('');
|
|
4487
|
+
if (needle.length > 2048) continue;
|
|
4488
|
+
let offset = 0;
|
|
4489
|
+
const anchored = new Set();
|
|
4490
|
+
for (const other of window) {
|
|
4491
|
+
if (other.at >= 0) anchored.add(other.at - offset);
|
|
4492
|
+
offset += other.picked.compact.length;
|
|
4493
|
+
}
|
|
4494
|
+
if (anchored.size !== 1) continue;
|
|
4495
|
+
const at = anchored.values().next().value;
|
|
4496
|
+
if (at < 0 || index.compact.slice(at, at + needle.length) !== needle) continue;
|
|
4497
|
+
let cursor = at, wanted = -1, consistent = true;
|
|
4498
|
+
for (const other of window) {
|
|
4499
|
+
if (other.at >= 0 && other.at !== cursor) consistent = false;
|
|
4500
|
+
if (other === entry) wanted = cursor;
|
|
4501
|
+
cursor += other.picked.compact.length;
|
|
4502
|
+
}
|
|
4503
|
+
if (consistent && !ordered.some(other => other !== entry && other.at >= 0
|
|
4504
|
+
&& other.at < wanted + entry.picked.compact.length && other.at + other.picked.compact.length > wanted)) placements.add(wanted);
|
|
4505
|
+
}
|
|
4506
|
+
}
|
|
4507
|
+
if (placements.size === 1) entry.at = placements.values().next().value;
|
|
4508
|
+
}
|
|
4397
4509
|
}
|
|
4398
4510
|
return entries.map(entry => {
|
|
4399
4511
|
const { line, text, picked, prefix, at } = entry;
|
|
4400
|
-
if (at < 0) return { ...line, text };
|
|
4512
|
+
if (at < 0) return { ...line, text, wordLayout: true };
|
|
4401
4513
|
const matched = index.positions.slice(at, at + picked.compact.length);
|
|
4402
4514
|
const srcStart = matched[0];
|
|
4403
4515
|
const srcEnd = matched[matched.length - 1] + 1;
|
|
4404
4516
|
const part = parts.find(item => srcStart >= item.start && srcStart < item.start + item.text.length);
|
|
4405
|
-
|
|
4406
|
-
|
|
4517
|
+
// PDF-to-Word documents can put a bullet and its text in two separate
|
|
4518
|
+
// paragraphs while painting them as one PDF line. Native glyphs prove
|
|
4519
|
+
// that exact combined span; OCR and table-cell crossings stay refused.
|
|
4520
|
+
const crossed = parts.filter(item => item.start < srcEnd && item.start + item.text.length > srcStart);
|
|
4521
|
+
if (!part || (srcEnd > part.start + part.text.length
|
|
4522
|
+
&& (!exactWordGlyphs(line) || crossed.some(item => item.block.type === 'table')))) return { ...line, text, wordLayout: true };
|
|
4523
|
+
const bound = { ...line, text, wordLayout: true, blockId: part.block.id, srcStart, srcEnd };
|
|
4407
4524
|
if (part.block.type === 'table') {
|
|
4408
4525
|
for (let row = 0; row < (part.block.rows || []).length; row++) {
|
|
4409
4526
|
const cells = part.block.rows[row].cells || [];
|
|
@@ -4502,6 +4619,45 @@ window.__ModuleLoader__.load({
|
|
|
4502
4619
|
return ranges;
|
|
4503
4620
|
}
|
|
4504
4621
|
|
|
4622
|
+
function wordListMarker(text) {
|
|
4623
|
+
return /^[\s⚫•●▪◦‣⁃∙○◉◎■□◆◇◼◾⬤*\-]+$/u.test(String(text || ''))
|
|
4624
|
+
&& /[⚫•●▪◦‣⁃∙○◉◎■□◆◇◼◾⬤*\-]/u.test(String(text || ''));
|
|
4625
|
+
}
|
|
4626
|
+
|
|
4627
|
+
/** Separate Word paragraphs remain exact source segments of one selection. */
|
|
4628
|
+
function wordCompoundAnchor(doc, parts) {
|
|
4629
|
+
const source = sourceParts(doc).map(part => part.text).join('\n');
|
|
4630
|
+
if (!Array.isArray(parts) || !parts.length || parts.some(part => !part?.anchor
|
|
4631
|
+
|| !Number.isInteger(part.anchor.srcStart) || !Number.isInteger(part.anchor.srcEnd)
|
|
4632
|
+
|| part.anchor.srcStart < 0 || part.anchor.srcEnd <= part.anchor.srcStart || part.anchor.srcEnd > source.length
|
|
4633
|
+
|| source.slice(part.anchor.srcStart, part.anchor.srcEnd) !== part.text)) return null;
|
|
4634
|
+
if (parts.length === 1) return { ...parts[0].anchor };
|
|
4635
|
+
const segments = parts.map(part => ({ blockId: part.anchor.blockId, srcStart: part.anchor.srcStart,
|
|
4636
|
+
srcEnd: part.anchor.srcEnd, selectionProof: part.anchor.selectionProof || 'pdf-glyphs-v1' }));
|
|
4637
|
+
if (segments.some((segment, i) => i > 0 && segment.srcStart < segments[i - 1].srcEnd)) return null;
|
|
4638
|
+
const anchor = { kind: 'range', blockId: segments[0].blockId, segments };
|
|
4639
|
+
const from = segments[0].srcStart, to = segments.at(-1).srcEnd;
|
|
4640
|
+
if (segments.every((segment, i) => !i || /^\s*$/.test(source.slice(segments[i - 1].srcEnd, segment.srcStart)))) {
|
|
4641
|
+
anchor.srcStart = from; anchor.srcEnd = to;
|
|
4642
|
+
}
|
|
4643
|
+
return anchor;
|
|
4644
|
+
}
|
|
4645
|
+
|
|
4646
|
+
function wordAnchorText(doc, anchor) {
|
|
4647
|
+
const source = sourceParts(doc).map(part => part.text).join('\n');
|
|
4648
|
+
const segments = Array.isArray(anchor?.segments) ? anchor.segments : [anchor];
|
|
4649
|
+
if (!segments.length || segments.some(segment => !Number.isInteger(segment?.srcStart) || !Number.isInteger(segment?.srcEnd)
|
|
4650
|
+
|| segment.srcStart < 0 || segment.srcEnd <= segment.srcStart || segment.srcEnd > source.length)) return undefined;
|
|
4651
|
+
return segments.map(segment => source.slice(segment.srcStart, segment.srcEnd)).join('\n');
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4654
|
+
function continuousWordAnchor(doc, anchor) {
|
|
4655
|
+
const source = sourceParts(doc).map(part => part.text).join('\n');
|
|
4656
|
+
const segments = Array.isArray(anchor?.segments) ? anchor.segments : [anchor];
|
|
4657
|
+
return wordAnchorText(doc, anchor) !== undefined && segments.every((segment, i) => !i
|
|
4658
|
+
|| (segment.srcStart >= segments[i - 1].srcEnd && /^\s*$/.test(source.slice(segments[i - 1].srcEnd, segment.srcStart))));
|
|
4659
|
+
}
|
|
4660
|
+
|
|
4505
4661
|
/** The physical highlight, quote and rewrite all use this same exact source. */
|
|
4506
4662
|
function wordNativeSelection(doc, lines) {
|
|
4507
4663
|
const selected = sortReadingOrder(lines);
|
|
@@ -4515,8 +4671,9 @@ window.__ModuleLoader__.load({
|
|
|
4515
4671
|
if (!original.trim() || wordTextIndex(original).compact !== wordTextIndex(picked).compact) return fallback;
|
|
4516
4672
|
const parts = wordRanges(doc, from, to);
|
|
4517
4673
|
if (!parts.length) return fallback;
|
|
4518
|
-
|
|
4519
|
-
|
|
4674
|
+
const anchor = wordCompoundAnchor(doc, parts);
|
|
4675
|
+
if (!anchor) return fallback;
|
|
4676
|
+
return { anchor, text: wordAnchorText(doc, anchor), blockId: parts[0].blockId, parts, verified: true };
|
|
4520
4677
|
}
|
|
4521
4678
|
|
|
4522
4679
|
/** A rectangle can select separate source slices; verify each before merging only whitespace gaps. */
|
|
@@ -4537,8 +4694,9 @@ window.__ModuleLoader__.load({
|
|
|
4537
4694
|
}
|
|
4538
4695
|
if (!groups.length) return null;
|
|
4539
4696
|
const parts = groups.flatMap(group => wordRanges(doc, group.from, group.to));
|
|
4540
|
-
|
|
4541
|
-
|
|
4697
|
+
const anchor = wordCompoundAnchor(doc, parts);
|
|
4698
|
+
if (!anchor) return { anchor: { kind: 'range' }, text: joinOcrText(lines), wordUnmapped: true };
|
|
4699
|
+
return { anchor, text: wordAnchorText(doc, anchor), parts, blockId: parts[0].blockId, verified: true };
|
|
4542
4700
|
}
|
|
4543
4701
|
|
|
4544
4702
|
function uniqueAt(haystack, needle) {
|
|
@@ -5008,12 +5166,26 @@ window.__ModuleLoader__.load({
|
|
|
5008
5166
|
* @param {{ x: number, y: number }} b
|
|
5009
5167
|
*/
|
|
5010
5168
|
function selectionSpans(lines, a, b) {
|
|
5011
|
-
|
|
5169
|
+
let list = sortReadingOrder((Array.isArray(lines) ? lines : []).filter((line) => (
|
|
5012
5170
|
line && (line.ocr === true || line.nativeSelection) && String(line.text || '').trim()
|
|
5013
5171
|
)));
|
|
5014
5172
|
if (!a || !b || list.length === 0) return [];
|
|
5015
|
-
|
|
5016
|
-
|
|
5173
|
+
let ia = indexAtPoint(list, Number(a.x) || 0, Number(a.y) || 0);
|
|
5174
|
+
let ib = indexAtPoint(list, Number(b.x) || 0, Number(b.y) || 0);
|
|
5175
|
+
const first = list[ia >= 0 ? ia : ib], last = list[ib >= 0 ? ib : ia];
|
|
5176
|
+
if (first && first.wordLayout && !first.wordCell && (!last || sameWordColumn(first, last))) {
|
|
5177
|
+
const edge = wordLineGeometry(first);
|
|
5178
|
+
// Restrict an ordinary drag inside a clearly separate Word column. A
|
|
5179
|
+
// rectangle remains available for an intentional multi-column selection.
|
|
5180
|
+
const hasOtherColumn = list.some(line => line.wordLayout && Number(line.page || 1) === Number(first.page || 1)
|
|
5181
|
+
&& Math.abs(wordLineGeometry(line).x - edge.x) > .18
|
|
5182
|
+
&& (wordLineGeometry(line).x >= edge.x + edge.w || wordLineGeometry(line).x + wordLineGeometry(line).w <= edge.x));
|
|
5183
|
+
if (hasOtherColumn) {
|
|
5184
|
+
list = list.filter(line => sameWordColumn(first, line));
|
|
5185
|
+
ia = indexAtPoint(list, Number(a.x) || 0, Number(a.y) || 0);
|
|
5186
|
+
ib = indexAtPoint(list, Number(b.x) || 0, Number(b.y) || 0);
|
|
5187
|
+
}
|
|
5188
|
+
}
|
|
5017
5189
|
if (ia < 0 && ib < 0) {
|
|
5018
5190
|
// 指针停在行外时,仍按拖过的矩形把跨到的行切开,避免一松手选区被清掉。
|
|
5019
5191
|
const hits = hitsInRect(list, { x0: a.x, y0: a.y, x1: b.x, y1: b.y });
|
|
@@ -5512,7 +5684,7 @@ window.__ModuleLoader__.load({
|
|
|
5512
5684
|
function revisionAllowed(doc) {
|
|
5513
5685
|
if (!doc || typeof doc !== 'object') return true;
|
|
5514
5686
|
const format = typeof doc.format === 'string' ? doc.format : '';
|
|
5515
|
-
if (format === 'pdf' || format === 'image') return false;
|
|
5687
|
+
if (format === 'pdf' || format === 'image' || format === 'doc') return false;
|
|
5516
5688
|
return !(doc.meta && doc.meta.writable === false);
|
|
5517
5689
|
}
|
|
5518
5690
|
|
|
@@ -5574,6 +5746,18 @@ window.__ModuleLoader__.load({
|
|
|
5574
5746
|
return `dshDoc:scroll:${projectId}:${path}`;
|
|
5575
5747
|
}
|
|
5576
5748
|
|
|
5749
|
+
function slideStorageKey(projectId, path) {
|
|
5750
|
+
return `dshDoc:slide:${projectId}:${path}`;
|
|
5751
|
+
}
|
|
5752
|
+
|
|
5753
|
+
function storedSlide(projectId, path) {
|
|
5754
|
+
try {
|
|
5755
|
+
const value = Number(currentWindow()?.localStorage?.getItem(slideStorageKey(projectId, path)));
|
|
5756
|
+
if (Number.isSafeInteger(value) && value >= 1) return value;
|
|
5757
|
+
} catch { /* A private browser still opens the first page. */ }
|
|
5758
|
+
return 1;
|
|
5759
|
+
}
|
|
5760
|
+
|
|
5577
5761
|
function isTypingTarget(target) {
|
|
5578
5762
|
if (!target || typeof target.tagName !== 'string') return false;
|
|
5579
5763
|
const tag = target.tagName;
|
|
@@ -5590,7 +5774,9 @@ window.__ModuleLoader__.load({
|
|
|
5590
5774
|
const [findOpen, setFindOpen] = React.useState(false);
|
|
5591
5775
|
const [findQ, setFindQ] = React.useState('');
|
|
5592
5776
|
const [findAt, setFindAt] = React.useState(0);
|
|
5593
|
-
const [slide, setSlide] = React.useState(
|
|
5777
|
+
const [slide, setSlide] = React.useState(() => storedSlide(p.projectId, p.path));
|
|
5778
|
+
const [slideInput, setSlideInput] = React.useState(() => String(storedSlide(p.projectId, p.path)));
|
|
5779
|
+
const [slideJumpError, setSlideJumpError] = React.useState('');
|
|
5594
5780
|
const [slideCount, setSlideCount] = React.useState(0);
|
|
5595
5781
|
const [slideReady, setSlideReady] = React.useState(false);
|
|
5596
5782
|
const [slideError, setSlideError] = React.useState('');
|
|
@@ -5599,6 +5785,7 @@ window.__ModuleLoader__.load({
|
|
|
5599
5785
|
const [pageFailed, setPageFailed] = React.useState(false);
|
|
5600
5786
|
const [readyPages, setReadyPages] = React.useState({});
|
|
5601
5787
|
const [ocrByPage, setOcrByPage] = React.useState({});
|
|
5788
|
+
const [pageTextNotice, setPageTextNotice] = React.useState(null);
|
|
5602
5789
|
const [rasterPages, setRasterPages] = React.useState(0);
|
|
5603
5790
|
const [rasterAspect, setRasterAspect] = React.useState(0);
|
|
5604
5791
|
const [ocrOn, setOcrOn] = React.useState(null);
|
|
@@ -5610,6 +5797,7 @@ window.__ModuleLoader__.load({
|
|
|
5610
5797
|
const bodyRef = React.useRef(null);
|
|
5611
5798
|
const wrapRef = React.useRef(null);
|
|
5612
5799
|
const slideCountRef = React.useRef(0);
|
|
5800
|
+
const slideStorageRef = React.useRef({ target: '', ready: false });
|
|
5613
5801
|
const missTick = React.useRef(0);
|
|
5614
5802
|
const gestureRef = React.useRef(null);
|
|
5615
5803
|
const selectionScrollRef = React.useRef(null);
|
|
@@ -5628,7 +5816,9 @@ window.__ModuleLoader__.load({
|
|
|
5628
5816
|
const actions = React.useMemo(() => (
|
|
5629
5817
|
canRevise ? baseActions : baseActions.filter((action) => !action || action.kind !== 'revise')
|
|
5630
5818
|
), [baseActions, canRevise]);
|
|
5631
|
-
const
|
|
5819
|
+
const sourceFormat = doc && typeof doc.format === 'string' ? doc.format : '';
|
|
5820
|
+
const format = sourceFormat === 'doc' ? 'docx' : sourceFormat;
|
|
5821
|
+
const visionModelKey = JSON.stringify(p.visionRoute || {});
|
|
5632
5822
|
const canLayout = Boolean(p.projectId && p.path && LAYOUT_FORMATS[format]);
|
|
5633
5823
|
// Word / PPT / PDF 只显示版式,旧的文本偏好也不能把它们带回抽取正文。
|
|
5634
5824
|
const view = (LAYOUT_ONLY_FORMATS[format] && canLayout) || (p.view === 'layout' && canLayout) ? 'layout' : 'text';
|
|
@@ -5641,9 +5831,8 @@ window.__ModuleLoader__.load({
|
|
|
5641
5831
|
kind: 'pick',
|
|
5642
5832
|
title: '列出这一页的文本框,选一段润色、扩写,或自己改字',
|
|
5643
5833
|
}])
|
|
5644
|
-
: actions)
|
|
5645
|
-
|
|
5646
|
-
), [actions, format, canRevise, showLayout, p.busyAction]);
|
|
5834
|
+
: actions)
|
|
5835
|
+
), [actions, format, canRevise, showLayout]);
|
|
5647
5836
|
const showImage = format === 'image' && !showLayout;
|
|
5648
5837
|
const reason = doc && doc.meta && typeof doc.meta.reason === 'string' ? doc.meta.reason : '';
|
|
5649
5838
|
const previewRev = Number(p.previewEpoch) || 0;
|
|
@@ -5666,6 +5855,9 @@ window.__ModuleLoader__.load({
|
|
|
5666
5855
|
}
|
|
5667
5856
|
function docMedia(route, extra) {
|
|
5668
5857
|
const query = extra ? { ...extra } : {};
|
|
5858
|
+
if (route === 'page-text' && query.page && p.visionRoute?.provider && p.visionRoute?.model) {
|
|
5859
|
+
query.provider = p.visionRoute.provider; query.model = p.visionRoute.model;
|
|
5860
|
+
}
|
|
5669
5861
|
// 旧地址没有 v,浏览器会把改写前的页图缓存五分钟。每次接受都换一个 v。
|
|
5670
5862
|
query.v = previewRev;
|
|
5671
5863
|
return mediaUrl(route, p.projectId, p.path, query);
|
|
@@ -5678,10 +5870,15 @@ window.__ModuleLoader__.load({
|
|
|
5678
5870
|
setFindOpen(false);
|
|
5679
5871
|
setFindQ('');
|
|
5680
5872
|
setFindAt(0);
|
|
5681
|
-
|
|
5873
|
+
const restoredSlide = storedSlide(p.projectId, p.path);
|
|
5874
|
+
slideStorageRef.current = { target: `${p.projectId}\u0000${p.path}`, ready: false };
|
|
5875
|
+
setSlide(restoredSlide);
|
|
5876
|
+
setSlideInput(String(restoredSlide));
|
|
5877
|
+
setSlideJumpError('');
|
|
5682
5878
|
setSlideCount(0);
|
|
5683
5879
|
setSlideReady(false);
|
|
5684
5880
|
setSlideError('');
|
|
5881
|
+
setPageTextNotice(null);
|
|
5685
5882
|
setSlideRatio(16 / 9);
|
|
5686
5883
|
setPageModel(null);
|
|
5687
5884
|
setPageFailed(false);
|
|
@@ -5733,23 +5930,30 @@ window.__ModuleLoader__.load({
|
|
|
5733
5930
|
fetch(docMedia('pages'), { signal: ac.signal })
|
|
5734
5931
|
.then((res) => res.json())
|
|
5735
5932
|
.then((body) => {
|
|
5933
|
+
if (ac.signal.aborted) return;
|
|
5736
5934
|
if (!body || body.ok === false) {
|
|
5737
5935
|
setSlideError((body && body.error) || '无法读取幻灯片页数');
|
|
5738
5936
|
return;
|
|
5739
5937
|
}
|
|
5740
5938
|
const pages = Number(body.pages);
|
|
5939
|
+
const count = Number.isInteger(pages) && pages > 0 ? pages : 1;
|
|
5741
5940
|
setSlideError('');
|
|
5742
|
-
|
|
5941
|
+
slideStorageRef.current = { target: `${p.projectId}\u0000${p.path}`, ready: true };
|
|
5942
|
+
setSlideCount(count);
|
|
5943
|
+
setSlide(value => Math.min(count, Math.max(1, value)));
|
|
5743
5944
|
})
|
|
5744
5945
|
.catch((error) => {
|
|
5745
|
-
if (error && error.name === 'AbortError') return;
|
|
5946
|
+
if (ac.signal.aborted || error && error.name === 'AbortError') return;
|
|
5746
5947
|
setSlideError('无法读取幻灯片页数');
|
|
5747
5948
|
});
|
|
5748
5949
|
return () => ac.abort();
|
|
5749
|
-
}, [format, showLayout, p.projectId, p.path]);
|
|
5950
|
+
}, [format, showLayout, p.projectId, p.path, previewRev]);
|
|
5750
5951
|
|
|
5751
5952
|
React.useEffect(() => {
|
|
5953
|
+
setSlideInput(String(slide));
|
|
5954
|
+
setSlideJumpError('');
|
|
5752
5955
|
setSlideReady(false);
|
|
5956
|
+
setSlideError('');
|
|
5753
5957
|
setSelection(null);
|
|
5754
5958
|
setOcrOn(null);
|
|
5755
5959
|
setBoxPick(null);
|
|
@@ -5758,8 +5962,16 @@ window.__ModuleLoader__.load({
|
|
|
5758
5962
|
}, [slide, p.projectId, p.path]);
|
|
5759
5963
|
|
|
5760
5964
|
React.useEffect(() => {
|
|
5761
|
-
|
|
5762
|
-
|
|
5965
|
+
if (format !== 'pptx' || !slideCount || slide < 1 || slide > slideCount
|
|
5966
|
+
|| !slideStorageRef.current.ready || slideStorageRef.current.target !== `${p.projectId}\u0000${p.path}`) return;
|
|
5967
|
+
try { currentWindow()?.localStorage?.setItem(slideStorageKey(p.projectId, p.path), String(slide)); }
|
|
5968
|
+
catch { /* Navigation does not depend on local persistence. */ }
|
|
5969
|
+
if (typeof p.onLayoutPage === 'function') p.onLayoutPage(slide);
|
|
5970
|
+
}, [format, p.projectId, p.path, slide, slideCount]);
|
|
5971
|
+
|
|
5972
|
+
React.useEffect(() => {
|
|
5973
|
+
// 幻灯片逐页读取;打开图片型 PPTX 不预先转换整册。
|
|
5974
|
+
const want = Boolean(format !== 'pptx' && p.projectId && p.path && (showLayout || format === 'pdf'));
|
|
5763
5975
|
if (!want) return undefined;
|
|
5764
5976
|
const ac = new AbortController();
|
|
5765
5977
|
fetch(docMedia('page-text'), { signal: ac.signal })
|
|
@@ -5808,7 +6020,7 @@ window.__ModuleLoader__.load({
|
|
|
5808
6020
|
if (!nativePages.has(i)) queue.push(i);
|
|
5809
6021
|
}
|
|
5810
6022
|
}
|
|
5811
|
-
const tokens = queue.map((page) => `${p.projectId}:${p.path}:${page}:${previewRev}`);
|
|
6023
|
+
const tokens = queue.map((page) => `${p.projectId}:${p.path}:${page}:${previewRev}:${visionModelKey}`);
|
|
5812
6024
|
(async () => {
|
|
5813
6025
|
for (let i = 0; i < queue.length; i += 1) {
|
|
5814
6026
|
if (cancelled) return;
|
|
@@ -5816,23 +6028,32 @@ window.__ModuleLoader__.load({
|
|
|
5816
6028
|
const token = tokens[i];
|
|
5817
6029
|
if (ocrTried.current[token]) continue;
|
|
5818
6030
|
ocrTried.current[token] = 'pending';
|
|
6031
|
+
if (format === 'pptx') setPageTextNotice({ page, pending: true, text: '正在识别本页文字,完成后即可选中…' });
|
|
5819
6032
|
try {
|
|
5820
6033
|
const res = await fetch(docMedia('page-text', { page }), { signal: ac.signal });
|
|
5821
6034
|
const body = await res.json();
|
|
5822
6035
|
if (cancelled) return;
|
|
5823
6036
|
ocrTried.current[token] = 'done';
|
|
5824
|
-
if (!body || body.ok === false)
|
|
6037
|
+
if (!body || body.ok === false) {
|
|
6038
|
+
if (format === 'pptx') setPageTextNotice({ page, text: body?.error || '本页文字读取失败,请刷新后重试。' });
|
|
6039
|
+
continue;
|
|
6040
|
+
}
|
|
6041
|
+
if (format === 'pptx') setPageTextNotice(body.lines?.length
|
|
6042
|
+
? (body.ocr?.warning ? { page, text: body.ocr.warning + (body.ocr.visionError ? ` ${body.ocr.visionError}` : '') } : null)
|
|
6043
|
+
: { page, text: body.ocr?.visionError || body.ocr?.error || '本页未识别到文字;可选用支持图片的模型进行文档提问。' });
|
|
5825
6044
|
const pages = Number(body.pages);
|
|
5826
6045
|
const aspect = Number(body.aspect);
|
|
5827
6046
|
// 第一页可能全是图片;页数仍用于继续读取后面的文字页。
|
|
5828
6047
|
if (pages > 0) setRasterPages((prev) => (pages > prev ? pages : prev));
|
|
5829
6048
|
if (aspect > 0) setRasterAspect((prev) => (prev > 0 ? prev : aspect));
|
|
5830
6049
|
if (!Array.isArray(body.lines) || body.lines.length === 0) continue;
|
|
5831
|
-
if (
|
|
6050
|
+
if (body.source === 'pdf' || body.source === 'mixed') {
|
|
5832
6051
|
setPageModel((prev) => ({
|
|
5833
6052
|
pages: Math.max(pages || 1, prev && prev.pages || 1),
|
|
5834
6053
|
aspect: aspect > 0 ? aspect : (prev && prev.aspect || 0.772),
|
|
5835
|
-
lines: (prev ? prev.lines.filter((line) => Number(line.page) !== page) : []).concat(body.lines
|
|
6054
|
+
lines: (prev ? prev.lines.filter((line) => Number(line.page) !== page) : []).concat(body.lines.map(line => ({ ...line, page,
|
|
6055
|
+
...(line.ocr !== true && line.precise && Array.isArray(line.glyphs) ? { nativeSelection: true } : {}),
|
|
6056
|
+
}))),
|
|
5836
6057
|
}));
|
|
5837
6058
|
continue;
|
|
5838
6059
|
}
|
|
@@ -5843,7 +6064,10 @@ window.__ModuleLoader__.load({
|
|
|
5843
6064
|
}));
|
|
5844
6065
|
} catch (error) {
|
|
5845
6066
|
if (error && error.name === 'AbortError') return;
|
|
5846
|
-
if (!cancelled)
|
|
6067
|
+
if (!cancelled) {
|
|
6068
|
+
ocrTried.current[token] = 'done';
|
|
6069
|
+
if (format === 'pptx') setPageTextNotice({ page, text: normalizeError(error) });
|
|
6070
|
+
}
|
|
5847
6071
|
}
|
|
5848
6072
|
}
|
|
5849
6073
|
})();
|
|
@@ -5854,7 +6078,7 @@ window.__ModuleLoader__.load({
|
|
|
5854
6078
|
if (ocrTried.current[tokens[i]] === 'pending') delete ocrTried.current[tokens[i]];
|
|
5855
6079
|
}
|
|
5856
6080
|
};
|
|
5857
|
-
}, [format, showLayout, slide, pageModel, pageFailed, rasterPages, p.projectId, p.path, previewRev]);
|
|
6081
|
+
}, [format, showLayout, slide, pageModel, pageFailed, rasterPages, p.projectId, p.path, previewRev, visionModelKey]);
|
|
5858
6082
|
|
|
5859
6083
|
const boundLines = React.useMemo(
|
|
5860
6084
|
() => (format === 'docx' ? bindWordLayoutLines : bindLayoutLines)(blocks, pageModel && pageModel.lines),
|
|
@@ -6298,14 +6522,15 @@ window.__ModuleLoader__.load({
|
|
|
6298
6522
|
if (target && typeof target.closest === 'function' && target.closest('button')) return;
|
|
6299
6523
|
if (startPageGesture(event, event.currentTarget.closest?.('[data-reader-pages]'))) return;
|
|
6300
6524
|
const native = format === 'docx' && (lines || []).some(line => line.precise && Array.isArray(line.glyphs));
|
|
6301
|
-
|
|
6525
|
+
const mixedSlide = format === 'pptx' && (lines || []).some(line => line.nativeSelection);
|
|
6526
|
+
if (!native && !mixedSlide && target && typeof target.closest === 'function') {
|
|
6302
6527
|
const layer = target.closest('[data-text-layer]');
|
|
6303
6528
|
if (layer && layer.getAttribute('data-ocr') !== '1') {
|
|
6304
6529
|
ocrHold.current = false;
|
|
6305
6530
|
return;
|
|
6306
6531
|
}
|
|
6307
6532
|
}
|
|
6308
|
-
let ocrLines = (lines || []).filter(line => line && (native ? line.precise && Array.isArray(line.glyphs) : line.ocr === true))
|
|
6533
|
+
let ocrLines = (lines || []).filter(line => line && (native ? line.precise && Array.isArray(line.glyphs) : (line.ocr === true || line.nativeSelection)))
|
|
6309
6534
|
.map(line => native ? { ...line, nativeSelection: true } : line);
|
|
6310
6535
|
if (ocrLines.length === 0) return;
|
|
6311
6536
|
const plane = event.currentTarget;
|
|
@@ -6550,6 +6775,18 @@ window.__ModuleLoader__.load({
|
|
|
6550
6775
|
});
|
|
6551
6776
|
};
|
|
6552
6777
|
|
|
6778
|
+
const jumpSlide = () => {
|
|
6779
|
+
const value = slideInput.trim(), number = Number(value), max = slideCountRef.current;
|
|
6780
|
+
if (!max) { setSlideJumpError('页数尚未读取完成,请稍后重试。'); return; }
|
|
6781
|
+
if (!/^\d+$/.test(value) || !Number.isSafeInteger(number) || number < 1 || number > max) {
|
|
6782
|
+
setSlideJumpError(`请输入 1–${max} 之间的整数页码。`);
|
|
6783
|
+
return;
|
|
6784
|
+
}
|
|
6785
|
+
setSlideJumpError('');
|
|
6786
|
+
setSlideInput(String(number));
|
|
6787
|
+
setSlide(number);
|
|
6788
|
+
};
|
|
6789
|
+
|
|
6553
6790
|
const slideMode = showLayout && format === 'pptx';
|
|
6554
6791
|
const pdfLineCount = pageModel && Array.isArray(pageModel.lines) ? pageModel.lines.length : 0;
|
|
6555
6792
|
const ocrLineCount = Object.keys(ocrBound).reduce((sum, key) => sum + (Array.isArray(ocrBound[key]) ? ocrBound[key].length : 0), 0);
|
|
@@ -6588,7 +6825,7 @@ window.__ModuleLoader__.load({
|
|
|
6588
6825
|
return;
|
|
6589
6826
|
}
|
|
6590
6827
|
const target = event.target;
|
|
6591
|
-
if (target && typeof target.closest === 'function' && (target.closest('button') || target.closest('[data-text-layer]'))) return;
|
|
6828
|
+
if (target && typeof target.closest === 'function' && (target.closest('button,input,select,textarea') || target.closest('[data-text-layer]'))) return;
|
|
6592
6829
|
const selected = currentWindow();
|
|
6593
6830
|
const live = selected && typeof selected.getSelection === 'function' ? selected.getSelection() : null;
|
|
6594
6831
|
if (live && !live.isCollapsed) return;
|
|
@@ -6673,7 +6910,25 @@ window.__ModuleLoader__.load({
|
|
|
6673
6910
|
disabled: slideCount === 0 || slide >= slideCount,
|
|
6674
6911
|
onClick: () => turnSlide(1),
|
|
6675
6912
|
}, '›'),
|
|
6676
|
-
h('div', { key: 'count', className: P + 'SlideCount'
|
|
6913
|
+
h('div', { key: 'count', className: P + 'SlideCount', 'data-slide-navigation': '1' }, [
|
|
6914
|
+
h('div', { key: 'controls', className: P + 'SlideCountControls' }, [
|
|
6915
|
+
h('input', {
|
|
6916
|
+
key: 'page', type: 'number', inputMode: 'numeric', min: 1, max: slideCount || undefined, step: 1,
|
|
6917
|
+
value: slideInput, disabled: !slideCount, className: P + 'SlidePageInput',
|
|
6918
|
+
'aria-label': '跳转页码', 'aria-invalid': slideJumpError ? 'true' : undefined, 'data-slide-page-input': '1',
|
|
6919
|
+
onChange: event => { setSlideInput(event.target.value); setSlideJumpError(''); },
|
|
6920
|
+
onKeyDown: event => {
|
|
6921
|
+
if (event.key === 'Enter') { event.preventDefault(); event.stopPropagation(); jumpSlide(); }
|
|
6922
|
+
else if (event.key === 'Escape') { setSlideInput(String(slide)); setSlideJumpError(''); }
|
|
6923
|
+
},
|
|
6924
|
+
}),
|
|
6925
|
+
h('span', { key: 'total', 'data-slide-total': '1' }, slideCount ? `/ ${slideCount}` : '/ …'),
|
|
6926
|
+
h('button', { key: 'jump', type: 'button', className: P + 'SlideJumpButton', disabled: !slideCount,
|
|
6927
|
+
'aria-label': '跳转到指定页', 'data-slide-jump': '1', onClick: jumpSlide }, '跳转'),
|
|
6928
|
+
]),
|
|
6929
|
+
slideJumpError ? h('div', { key: 'error', className: P + 'SlideJumpError', role: 'alert',
|
|
6930
|
+
'data-slide-jump-error': '1' }, slideJumpError) : null,
|
|
6931
|
+
]),
|
|
6677
6932
|
]) : null;
|
|
6678
6933
|
|
|
6679
6934
|
const shownPages = usePages ? Math.min(pageCount, 80) : 0;
|
|
@@ -6731,6 +6986,12 @@ window.__ModuleLoader__.load({
|
|
|
6731
6986
|
|
|
6732
6987
|
return h('div', { className: P + 'Wrap', ref: wrapRef, 'data-reader-wrap': '1', 'data-reader-view': view }, [
|
|
6733
6988
|
viewBar,
|
|
6989
|
+
sourceFormat === 'doc' ? h('div', { key: 'legacy-word', className: P + 'SelectionNotice', role: 'status', 'data-legacy-word-notice': '1' }, [
|
|
6990
|
+
p.convertingWord ? h(Processing, { key: 'progress', text: '正在转换为可修订的 Word 文档…' })
|
|
6991
|
+
: h('span', { key: 'text' }, '旧版 DOC 可以阅读和提问;转换为 DOCX 副本后即可润色、扩写和接受修订。'),
|
|
6992
|
+
h('button', { key: 'convert', type: 'button', className: P + 'Btn', 'data-convert-word': '1',
|
|
6993
|
+
disabled: p.convertingWord || p.conversionDisabled, onClick: p.onConvertWord }, '转换并编辑'),
|
|
6994
|
+
]) : null,
|
|
6734
6995
|
p.officeNotice ? h('div', { key: 'office', className: P + 'SelectionNotice', role: 'alert', 'data-office-notice': '1' }, [
|
|
6735
6996
|
h('span', { key: 'message' }, p.officeNotice.message),
|
|
6736
6997
|
p.officeNotice.helpUrl ? h('a', { key: 'help', href: '/doc-assistant/office/help', target: '_blank', rel: 'noopener noreferrer' }, ' 查看安装指引') : null,
|
|
@@ -6741,6 +7002,9 @@ window.__ModuleLoader__.load({
|
|
|
6741
7002
|
showLayout && p.selectionNotice
|
|
6742
7003
|
? h('div', { key: 'selection-notice', className: P + 'SelectionNotice', role: 'status', 'data-layout-selection-notice': '1' }, p.selectionNotice)
|
|
6743
7004
|
: null,
|
|
7005
|
+
showLayout && format === 'pptx' && pageTextNotice?.page === slide
|
|
7006
|
+
? h('div', { key: 'page-text-notice', className: P + 'SelectionNotice', role: 'status', 'data-page-text-status': '1' },
|
|
7007
|
+
pageTextNotice.pending ? h(Processing, { text: pageTextNotice.text }) : pageTextNotice.text) : null,
|
|
6744
7008
|
findBar,
|
|
6745
7009
|
h('main', {
|
|
6746
7010
|
key: 'main',
|
|
@@ -7017,6 +7281,7 @@ window.__ModuleLoader__.load({
|
|
|
7017
7281
|
const [rightOpen, setRightOpen] = React.useState(true);
|
|
7018
7282
|
const [status, setStatus] = React.useState('');
|
|
7019
7283
|
const [busyAction, setBusyAction] = React.useState('');
|
|
7284
|
+
const [acceptingRevisionId, setAcceptingRevisionId] = React.useState('');
|
|
7020
7285
|
const [revisionTick, setRevisionTick] = React.useState(0);
|
|
7021
7286
|
// 接受写回后幻灯片图的地址要变,否则浏览器会继续显示缓存的旧页。
|
|
7022
7287
|
const [previewEpoch, setPreviewEpoch] = React.useState(0);
|
|
@@ -7029,6 +7294,8 @@ window.__ModuleLoader__.load({
|
|
|
7029
7294
|
const readerStateRef = React.useRef(null);
|
|
7030
7295
|
const reloadDocumentRef = React.useRef(null);
|
|
7031
7296
|
const [officeBusy, setOfficeBusy] = React.useState(false);
|
|
7297
|
+
const [legacyConverting, setLegacyConverting] = React.useState(false);
|
|
7298
|
+
const legacyConvertRef = React.useRef(null);
|
|
7032
7299
|
const officeLaunchRef = React.useRef('');
|
|
7033
7300
|
const [officeNotice, setOfficeNotice] = React.useState(null);
|
|
7034
7301
|
const [booted, setBooted] = React.useState(false);
|
|
@@ -7050,16 +7317,26 @@ window.__ModuleLoader__.load({
|
|
|
7050
7317
|
const [readerView, setReaderView] = React.useState('text');
|
|
7051
7318
|
const [selectionNotice, setSelectionNotice] = React.useState('');
|
|
7052
7319
|
const [noChangeNotice, setNoChangeNotice] = React.useState('');
|
|
7053
|
-
|
|
7054
|
-
|
|
7320
|
+
// Every request owns its immutable selection/model snapshot. Updating a job
|
|
7321
|
+
// replaces the Map so independent completions cannot overwrite each other.
|
|
7322
|
+
const [generationJobs, setGenerationJobs] = React.useState(() => new Map());
|
|
7323
|
+
const generationJobsRef = React.useRef(new Map());
|
|
7324
|
+
const generationSequenceRef = React.useRef(0);
|
|
7325
|
+
const generationAliveRef = React.useRef(true);
|
|
7326
|
+
const writeBusyRef = React.useRef(false);
|
|
7327
|
+
const [revisionErrors, setRevisionErrors] = React.useState(() => new Map());
|
|
7055
7328
|
const generationTargetRef = React.useRef('');
|
|
7056
7329
|
generationTargetRef.current = `${projectId}\u0000${docPath}`;
|
|
7057
|
-
React.
|
|
7330
|
+
const updateGenerationJobs = React.useCallback(update => {
|
|
7331
|
+
const next = update(generationJobsRef.current);
|
|
7332
|
+
generationJobsRef.current = next;
|
|
7333
|
+
if (generationAliveRef.current) setGenerationJobs(next);
|
|
7334
|
+
}, []);
|
|
7058
7335
|
React.useEffect(() => {
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
}, [projectId, docPath]);
|
|
7336
|
+
generationAliveRef.current = true;
|
|
7337
|
+
return () => { generationAliveRef.current = false; };
|
|
7338
|
+
}, []);
|
|
7339
|
+
React.useEffect(() => { setNoChangeNotice(''); }, [projectId, docPath]);
|
|
7063
7340
|
const [layoutPage, setLayoutPage] = React.useState(0);
|
|
7064
7341
|
const [layoutFocus, setLayoutFocus] = React.useState(null);
|
|
7065
7342
|
const [readPct, setReadPct] = React.useState(0);
|
|
@@ -7080,6 +7357,8 @@ window.__ModuleLoader__.load({
|
|
|
7080
7357
|
const [messages, setMessages] = React.useState([]);
|
|
7081
7358
|
const [chatBusy, setChatBusy] = React.useState(false);
|
|
7082
7359
|
const [chatKind, setChatKind] = React.useState('chat');
|
|
7360
|
+
const [chatProgress, setChatProgress] = React.useState('');
|
|
7361
|
+
const chatStartedRef = React.useRef(0);
|
|
7083
7362
|
const [chatError, setChatError] = React.useState('');
|
|
7084
7363
|
const [chatModels, setChatModels] = React.useState([]);
|
|
7085
7364
|
const [chatDefaultModel, setChatDefaultModel] = React.useState(null);
|
|
@@ -7122,7 +7401,7 @@ window.__ModuleLoader__.load({
|
|
|
7122
7401
|
// Word / PPT / PDF 打开即版式;Excel 打开即表格文本,版式按需再转。
|
|
7123
7402
|
React.useEffect(() => {
|
|
7124
7403
|
const fmt = doc && doc.format;
|
|
7125
|
-
if (fmt === 'docx' || fmt === 'pptx' || fmt === 'pdf') setReaderView('layout');
|
|
7404
|
+
if (fmt === 'doc' || fmt === 'docx' || fmt === 'pptx' || fmt === 'pdf') setReaderView('layout');
|
|
7126
7405
|
else setReaderView('text');
|
|
7127
7406
|
setLayoutPage(0);
|
|
7128
7407
|
setLayoutFocus(null);
|
|
@@ -7255,7 +7534,7 @@ window.__ModuleLoader__.load({
|
|
|
7255
7534
|
uploadBusyRef.current = true;
|
|
7256
7535
|
setUploadOutcome(null);
|
|
7257
7536
|
setStatus(`正在导入 ${list.length} 个文件…`);
|
|
7258
|
-
let done = 0;
|
|
7537
|
+
let done = 0, converted = 0;
|
|
7259
7538
|
let currentFile = '';
|
|
7260
7539
|
try {
|
|
7261
7540
|
for (const file of list) {
|
|
@@ -7264,11 +7543,12 @@ window.__ModuleLoader__.load({
|
|
|
7264
7543
|
setUploadProgress({ ...progress, stage: '正在读取文件…' });
|
|
7265
7544
|
const dataBase64 = await shelfModule.readFileBase64(file);
|
|
7266
7545
|
// The response also waits for best-effort cover preparation.
|
|
7267
|
-
setUploadProgress({ ...progress, stage: '正在保存文档并准备封面,请稍候…' });
|
|
7268
|
-
await transport.uploadDocs(targetProject, '', [{ name: file.name, dataBase64 }]);
|
|
7546
|
+
setUploadProgress({ ...progress, stage: /\.doc$/i.test(file.name) ? '正在转换为可修订的 Word 文档并准备封面…' : '正在保存文档并准备封面,请稍候…' });
|
|
7547
|
+
const imported = await transport.uploadDocs(targetProject, '', [{ name: file.name, dataBase64 }]);
|
|
7548
|
+
converted += (Array.isArray(imported) ? imported : imported?.entries || []).filter(entry => entry.convertedFrom === 'doc').length;
|
|
7269
7549
|
done += 1;
|
|
7270
7550
|
}
|
|
7271
|
-
const message = `已导入 ${done}
|
|
7551
|
+
const message = `已导入 ${done} 个文件${converted ? `,其中 ${converted} 个 DOC 已转换为可修订的 DOCX` : ''}`;
|
|
7272
7552
|
setStatus(message);
|
|
7273
7553
|
setUploadOutcome({ message, error: false });
|
|
7274
7554
|
} catch (error) {
|
|
@@ -7364,6 +7644,21 @@ window.__ModuleLoader__.load({
|
|
|
7364
7644
|
if (draftReference && rightOpen && rightTab === 'chat' && !chatBusy && chatInputRef.current) chatInputRef.current.focus();
|
|
7365
7645
|
}, [draftReference, rightOpen, rightTab, chatBusy]);
|
|
7366
7646
|
|
|
7647
|
+
React.useEffect(() => {
|
|
7648
|
+
if (!chatBusy || !projectId || !docPath || typeof transport.contextStatus !== 'function' || !chatStartedRef.current) return undefined;
|
|
7649
|
+
let alive = true, timer;
|
|
7650
|
+
const started = chatStartedRef.current;
|
|
7651
|
+
const poll = async () => {
|
|
7652
|
+
try {
|
|
7653
|
+
const state = await transport.contextStatus(projectId, docPath);
|
|
7654
|
+
if (alive && Number(state?.at) >= started && ['reading', 'vision', 'ocr', 'ready'].includes(state.stage)) setChatProgress(state.message || '');
|
|
7655
|
+
} catch { /* The original chat request reports any real failure. */ }
|
|
7656
|
+
if (alive) timer = setTimeout(poll, 1000);
|
|
7657
|
+
};
|
|
7658
|
+
poll();
|
|
7659
|
+
return () => { alive = false; clearTimeout(timer); };
|
|
7660
|
+
}, [chatBusy, projectId, docPath, transport]);
|
|
7661
|
+
|
|
7367
7662
|
// ---- 新消息滚到底
|
|
7368
7663
|
React.useEffect(() => {
|
|
7369
7664
|
const el = chatListRef.current;
|
|
@@ -7397,6 +7692,8 @@ window.__ModuleLoader__.load({
|
|
|
7397
7692
|
setDraft('');
|
|
7398
7693
|
setDraftReference(null);
|
|
7399
7694
|
}
|
|
7695
|
+
chatStartedRef.current = Date.now();
|
|
7696
|
+
setChatProgress('');
|
|
7400
7697
|
setChatBusy(true);
|
|
7401
7698
|
setChatKind(kind);
|
|
7402
7699
|
setChatError('');
|
|
@@ -7435,6 +7732,7 @@ window.__ModuleLoader__.load({
|
|
|
7435
7732
|
if (chatInFlightRef.current && chatInFlightRef.current.target === target) return;
|
|
7436
7733
|
const request = ++chatRequestRef.current;
|
|
7437
7734
|
chatInFlightRef.current = { target, request };
|
|
7735
|
+
chatStartedRef.current = 0; setChatProgress('');
|
|
7438
7736
|
setChatBusy(true);
|
|
7439
7737
|
setChatError('');
|
|
7440
7738
|
try {
|
|
@@ -7499,57 +7797,66 @@ window.__ModuleLoader__.load({
|
|
|
7499
7797
|
const payload = extra?.request ? { ...extra.request } : { projectId, path: docPath, anchor, instruction, action: actionId };
|
|
7500
7798
|
if (!extra?.request && typeof extra?.revised !== 'string') Object.assign(payload, chosenModel(chatModel));
|
|
7501
7799
|
if (!extra?.request && doc && doc.format === 'docx') {
|
|
7502
|
-
|
|
7503
|
-
payload.expectedOriginal = source.slice(anchor.srcStart, anchor.srcEnd);
|
|
7800
|
+
payload.expectedOriginal = wordAnchorText(doc, anchor);
|
|
7504
7801
|
if (doc.meta?.documentHash) payload.expectedDocumentHash = doc.meta.documentHash;
|
|
7505
7802
|
}
|
|
7506
7803
|
if (extra && typeof extra.revised === 'string') payload.revised = extra.revised;
|
|
7507
7804
|
const snapshot = captureRequest(payload);
|
|
7508
7805
|
const target = `${snapshot.projectId}\u0000${snapshot.path}`;
|
|
7509
|
-
|
|
7510
|
-
const
|
|
7511
|
-
|
|
7512
|
-
|
|
7806
|
+
const range = snapshot.anchor || {};
|
|
7807
|
+
const ranges = Array.isArray(range.segments) ? range.segments : [range];
|
|
7808
|
+
const key = JSON.stringify([target, snapshot.action || actionId,
|
|
7809
|
+
snapshot.expectedDocumentHash || doc?.meta?.documentHash, snapshot.expectedOriginal ?? wordAnchorText(doc, snapshot.anchor), ranges.map(part => [part.kind,
|
|
7810
|
+
part.srcStart, part.srcEnd, part.blockId, part.sheet, part.cell, part.row, part.col])]);
|
|
7811
|
+
if (Array.from(generationJobsRef.current.values()).some(job => job.key === key && job.state === 'running')) return;
|
|
7812
|
+
const id = extra?.jobId || `generation-${++generationSequenceRef.current}`;
|
|
7813
|
+
const operation = Object.freeze({ id, key, target, snapshot, state: 'running', replaceId: extra?.replaceId,
|
|
7814
|
+
original: snapshot.expectedOriginal ?? wordAnchorText(doc, snapshot.anchor),
|
|
7815
|
+
documentHash: snapshot.expectedDocumentHash || doc?.meta?.documentHash,
|
|
7816
|
+
label: actionId === 'expand' ? '扩写' : actionId === 'manual' ? '修改' : '润色' });
|
|
7817
|
+
updateGenerationJobs(previous => { const next = new Map(previous); next.set(id, operation); return next; });
|
|
7818
|
+
const isCurrent = () => generationAliveRef.current && generationTargetRef.current === target
|
|
7819
|
+
&& store.projectId === snapshot.projectId && store.docPath === snapshot.path;
|
|
7820
|
+
const finishJob = changes => updateGenerationJobs(previous => {
|
|
7821
|
+
if (previous.get(id) !== operation) return previous;
|
|
7822
|
+
const next = new Map(previous);
|
|
7823
|
+
if (changes) next.set(id, Object.freeze({ ...operation, ...changes }));
|
|
7824
|
+
else next.delete(id);
|
|
7825
|
+
return next;
|
|
7826
|
+
});
|
|
7513
7827
|
let replaced = false;
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
// 生成后让用户直接看到结果:展开右栏并切到「修订」标签。
|
|
7520
|
-
setRightOpen(true);
|
|
7521
|
-
setRightTab('revise');
|
|
7828
|
+
if (isCurrent()) {
|
|
7829
|
+
setStatus(extra?.replaceId ? '正在重新生成…' : '正在生成修订…');
|
|
7830
|
+
setSelectionNotice(''); setNoChangeNotice('');
|
|
7831
|
+
setRightOpen(true); setRightTab('revise');
|
|
7832
|
+
}
|
|
7522
7833
|
try {
|
|
7523
7834
|
if (extra?.replaceId) {
|
|
7524
7835
|
await store.delete(extra.replaceId, { optimistic: true });
|
|
7525
7836
|
replaced = true;
|
|
7526
7837
|
}
|
|
7527
7838
|
const revision = await store.generate(snapshot);
|
|
7528
|
-
if (!isCurrent()) return revision;
|
|
7529
7839
|
if (revision?.unchanged) {
|
|
7530
|
-
const message = `${
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
setStatus(message);
|
|
7534
|
-
}
|
|
7840
|
+
const message = `${operation.label}结果与原文一致,内容无变化。`;
|
|
7841
|
+
finishJob({ state: 'unchanged', message, replaceId: undefined });
|
|
7842
|
+
if (isCurrent() && !extra?.batch) setStatus(message);
|
|
7535
7843
|
return revision;
|
|
7536
7844
|
}
|
|
7537
|
-
|
|
7538
|
-
|
|
7845
|
+
finishJob(null);
|
|
7846
|
+
if (isCurrent()) {
|
|
7847
|
+
const note = extra && extra.note ? `${extra.note} ` : '';
|
|
7848
|
+
setStatus(`${note}${revision && revision.id ? `已生成修订 ${revision.id}` : '已生成修订'}`);
|
|
7849
|
+
}
|
|
7539
7850
|
return revision;
|
|
7540
7851
|
} catch (error) {
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
setStatus(message);
|
|
7544
|
-
setGenerationError({ message, request: snapshot, replaceId: !replaced ? extra?.replaceId : undefined });
|
|
7545
|
-
} finally {
|
|
7546
|
-
if (isCurrent()) setBusyAction('');
|
|
7547
|
-
if (generationRef.current === operation) generationRef.current = null;
|
|
7852
|
+
const message = `${extra?.replaceId ? '重新生成' : operation.label}失败:${normalizeError(error)}`;
|
|
7853
|
+
finishJob({ state: 'failed', message, replaceId: !replaced ? extra?.replaceId : undefined });
|
|
7854
|
+
if (isCurrent()) setStatus(message);
|
|
7548
7855
|
}
|
|
7549
|
-
}, [store, projectId, docPath, doc, chatModel]);
|
|
7856
|
+
}, [store, projectId, docPath, doc, chatModel, updateGenerationJobs]);
|
|
7550
7857
|
|
|
7551
7858
|
const refreshDoc = React.useCallback(() => {
|
|
7552
|
-
if (docPath) openDoc(docPath, { keepStatus: true });
|
|
7859
|
+
if (docPath) return openDoc(docPath, { keepStatus: true });
|
|
7553
7860
|
}, [docPath, openDoc]);
|
|
7554
7861
|
|
|
7555
7862
|
const reloadDocument = async (automatic = false) => {
|
|
@@ -7598,83 +7905,86 @@ window.__ModuleLoader__.load({
|
|
|
7598
7905
|
};
|
|
7599
7906
|
|
|
7600
7907
|
const acceptRevision = React.useCallback(async (revision) => {
|
|
7601
|
-
if (!revision) return;
|
|
7602
|
-
|
|
7908
|
+
if (!revision || writeBusyRef.current) return;
|
|
7909
|
+
const target = `${store.projectId}\u0000${store.docPath}`;
|
|
7910
|
+
const isCurrent = () => generationTargetRef.current === target && `${store.projectId}\u0000${store.docPath}` === target;
|
|
7911
|
+
writeBusyRef.current = true; setBusyAction(`rev:${revision.id}`); setAcceptingRevisionId(revision.id);
|
|
7912
|
+
setStatus(/\.docx$/i.test(revision.docPath || '') ? '正在核对版式并写回…' : '正在写回修订…');
|
|
7913
|
+
setRevisionErrors(previous => { const next = new Map(previous); next.delete(revision.id); return next; });
|
|
7603
7914
|
try {
|
|
7604
|
-
const out = await store.accept(revision.id
|
|
7915
|
+
const out = await store.accept(revision.id, revision.layoutWarning?.token
|
|
7916
|
+
? { layoutRiskToken: revision.layoutWarning.token } : undefined);
|
|
7917
|
+
if (!isCurrent()) return;
|
|
7605
7918
|
const written = out && out.written;
|
|
7606
|
-
setStatus(written
|
|
7607
|
-
?
|
|
7608
|
-
: '已接受修订');
|
|
7609
|
-
setPreviewEpoch(
|
|
7610
|
-
refreshDoc();
|
|
7919
|
+
setStatus(written?.layoutRiskAccepted
|
|
7920
|
+
? '已接受并写入修订,请点击文档标题右侧的 Office 图标,手动打开文档调整排版。'
|
|
7921
|
+
: written && written.backup ? `已接受并写回(${written.bytes || 0} 字节,备份:${written.backup})` : '已接受修订');
|
|
7922
|
+
setPreviewEpoch(value => value + 1);
|
|
7923
|
+
await Promise.all([refreshDoc(), store.load()]);
|
|
7611
7924
|
} catch (error) {
|
|
7612
|
-
|
|
7925
|
+
if (!isCurrent()) return;
|
|
7926
|
+
const message = normalizeError(error);
|
|
7927
|
+
setStatus(`${error?.body?.revision?.layoutWarning ? '排版提示' : '接受失败'}:${message}`);
|
|
7928
|
+
setRevisionErrors(previous => { const next = new Map(previous); next.set(revision.id, message); return next; });
|
|
7613
7929
|
} finally {
|
|
7614
|
-
setBusyAction('');
|
|
7930
|
+
writeBusyRef.current = false; setBusyAction(''); setAcceptingRevisionId('');
|
|
7615
7931
|
}
|
|
7616
7932
|
}, [store, refreshDoc]);
|
|
7617
7933
|
|
|
7618
7934
|
const rejectRevision = React.useCallback(async (revision) => {
|
|
7619
7935
|
if (!revision) return;
|
|
7620
|
-
|
|
7936
|
+
const target = `${store.projectId}\u0000${store.docPath}`;
|
|
7621
7937
|
try {
|
|
7622
7938
|
await store.reject(revision.id);
|
|
7623
|
-
setStatus('已撤回修订');
|
|
7939
|
+
if (generationTargetRef.current === target) setStatus('已撤回修订');
|
|
7624
7940
|
} catch (error) {
|
|
7625
|
-
setStatus(`撤回失败:${normalizeError(error)}`);
|
|
7626
|
-
} finally {
|
|
7627
|
-
setBusyAction('');
|
|
7941
|
+
if (generationTargetRef.current === target) setStatus(`撤回失败:${normalizeError(error)}`);
|
|
7628
7942
|
}
|
|
7629
7943
|
}, [store]);
|
|
7630
7944
|
|
|
7631
7945
|
const deleteRevision = React.useCallback(async (revision) => {
|
|
7632
7946
|
if (!revision) return;
|
|
7633
|
-
|
|
7947
|
+
const target = `${store.projectId}\u0000${store.docPath}`;
|
|
7634
7948
|
try {
|
|
7635
|
-
await store.delete(revision.id);
|
|
7636
|
-
setStatus(revision.status === 'accepted'
|
|
7949
|
+
await store.delete(revision.id, { optimistic: true });
|
|
7950
|
+
if (generationTargetRef.current === target) setStatus(revision.status === 'accepted'
|
|
7951
|
+
? '已删除修订卡片,文档中已写入的修改保留。' : '已删除修订卡片');
|
|
7637
7952
|
} catch (error) {
|
|
7638
|
-
setStatus(`删除修订失败:${normalizeError(error)}`);
|
|
7639
|
-
}
|
|
7953
|
+
if (generationTargetRef.current === target) setStatus(`删除修订失败:${normalizeError(error)}`);
|
|
7954
|
+
}
|
|
7640
7955
|
}, [store]);
|
|
7641
7956
|
|
|
7642
7957
|
const restoreRevision = React.useCallback(async (revision) => {
|
|
7643
|
-
if (!revision) return;
|
|
7644
|
-
|
|
7958
|
+
if (!revision || writeBusyRef.current) return;
|
|
7959
|
+
const target = `${store.projectId}\u0000${store.docPath}`;
|
|
7960
|
+
writeBusyRef.current = true; setBusyAction(`rev:${revision.id}`);
|
|
7645
7961
|
try {
|
|
7646
7962
|
await store.restore(revision.id);
|
|
7963
|
+
if (generationTargetRef.current !== target) return;
|
|
7647
7964
|
setStatus(revision.status === 'accepted' ? '已撤销写入并还原文件' : '已恢复为待处理');
|
|
7648
|
-
setPreviewEpoch(
|
|
7649
|
-
refreshDoc();
|
|
7650
|
-
await store.load();
|
|
7965
|
+
setPreviewEpoch(value => value + 1);
|
|
7966
|
+
await Promise.all([refreshDoc(), store.load()]);
|
|
7651
7967
|
} catch (error) {
|
|
7652
|
-
setStatus(`恢复失败:${normalizeError(error)}`);
|
|
7653
|
-
} finally {
|
|
7654
|
-
setBusyAction('');
|
|
7655
|
-
}
|
|
7968
|
+
if (generationTargetRef.current === target) setStatus(`恢复失败:${normalizeError(error)}`);
|
|
7969
|
+
} finally { writeBusyRef.current = false; setBusyAction(''); }
|
|
7656
7970
|
}, [store, refreshDoc]);
|
|
7657
7971
|
|
|
7658
7972
|
const restoreWriteGroup = React.useCallback(async (group) => {
|
|
7659
|
-
const list = Array.isArray(group) ? group.filter(
|
|
7660
|
-
if (list.length
|
|
7661
|
-
|
|
7973
|
+
const list = Array.isArray(group) ? group.filter(item => item && item.id) : [];
|
|
7974
|
+
if (!list.length || writeBusyRef.current) return;
|
|
7975
|
+
const target = `${store.projectId}\u0000${store.docPath}`;
|
|
7976
|
+
writeBusyRef.current = true; setBusyAction(`rev:${list[0].id}`);
|
|
7662
7977
|
try {
|
|
7663
|
-
const results = await store.bulk(list.map(
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
return;
|
|
7668
|
-
}
|
|
7978
|
+
const results = await store.bulk(list.map(item => item.id), 'restore');
|
|
7979
|
+
if (generationTargetRef.current !== target) return;
|
|
7980
|
+
const failed = (results || []).filter(item => item && item.ok === false);
|
|
7981
|
+
if (failed.length) { setStatus(`整组撤回失败:${failed[0].error || ''}`); return; }
|
|
7669
7982
|
setStatus(`已撤回本次批量写入(${list.length} 条)`);
|
|
7670
|
-
setPreviewEpoch(
|
|
7671
|
-
refreshDoc();
|
|
7672
|
-
await store.load();
|
|
7983
|
+
setPreviewEpoch(value => value + 1);
|
|
7984
|
+
await Promise.all([refreshDoc(), store.load()]);
|
|
7673
7985
|
} catch (error) {
|
|
7674
|
-
setStatus(`整组撤回失败:${normalizeError(error)}`);
|
|
7675
|
-
} finally {
|
|
7676
|
-
setBusyAction('');
|
|
7677
|
-
}
|
|
7986
|
+
if (generationTargetRef.current === target) setStatus(`整组撤回失败:${normalizeError(error)}`);
|
|
7987
|
+
} finally { writeBusyRef.current = false; setBusyAction(''); }
|
|
7678
7988
|
}, [store, refreshDoc]);
|
|
7679
7989
|
|
|
7680
7990
|
const regenerate = React.useCallback(async (revision) => {
|
|
@@ -7693,15 +8003,21 @@ window.__ModuleLoader__.load({
|
|
|
7693
8003
|
}, [generate, projectId, docPath, chatModel]);
|
|
7694
8004
|
|
|
7695
8005
|
const acceptAll = React.useCallback(async () => {
|
|
8006
|
+
if (writeBusyRef.current) return;
|
|
7696
8007
|
if (groupRevisionVariants(store.all()).some(group => group.items.filter(item => item.status === 'pending').length > 1)) {
|
|
7697
8008
|
setStatus('同一选区有多份扩写,请选择其中一份接受。');
|
|
7698
8009
|
return;
|
|
7699
8010
|
}
|
|
8011
|
+
const target = `${store.projectId}\u0000${store.docPath}`;
|
|
8012
|
+
writeBusyRef.current = true;
|
|
7700
8013
|
setBusyAction('accept-all');
|
|
7701
8014
|
setStatus('正在批量接受…');
|
|
7702
8015
|
try {
|
|
7703
8016
|
const results = await store.acceptAll();
|
|
8017
|
+
if (generationTargetRef.current !== target) return;
|
|
7704
8018
|
const failed = (results || []).filter((item) => item && item.ok === false);
|
|
8019
|
+
if (failed.length) setRevisionErrors(previous => { const next = new Map(previous);
|
|
8020
|
+
failed.forEach(item => next.set(item.revisionId, item.error || '接受失败')); return next; });
|
|
7705
8021
|
const wrote = (results || []).some((item) => item && item.ok !== false);
|
|
7706
8022
|
setStatus(failed.length > 0
|
|
7707
8023
|
? `接受完成,${failed.length} 条失败:${failed[0].error || ''}`
|
|
@@ -7709,9 +8025,9 @@ window.__ModuleLoader__.load({
|
|
|
7709
8025
|
if (wrote) setPreviewEpoch((value) => value + 1);
|
|
7710
8026
|
refreshDoc();
|
|
7711
8027
|
} catch (error) {
|
|
7712
|
-
setStatus(`批量接受失败:${normalizeError(error)}`);
|
|
8028
|
+
if (generationTargetRef.current === target) setStatus(`批量接受失败:${normalizeError(error)}`);
|
|
7713
8029
|
} finally {
|
|
7714
|
-
setBusyAction('');
|
|
8030
|
+
writeBusyRef.current = false; setBusyAction('');
|
|
7715
8031
|
}
|
|
7716
8032
|
}, [store, refreshDoc]);
|
|
7717
8033
|
|
|
@@ -7779,18 +8095,48 @@ window.__ModuleLoader__.load({
|
|
|
7779
8095
|
if (!instruction) return;
|
|
7780
8096
|
// 只读格式即使工具条漏了按钮,也不再打出「改写失败」。
|
|
7781
8097
|
if (!revisionAllowed(doc)) return;
|
|
8098
|
+
const isWordEdit = doc?.format === 'docx' && (actionId === 'polish' || actionId === 'expand');
|
|
8099
|
+
if (isWordEdit && wordListMarker(hit.text)) {
|
|
8100
|
+
setNoChangeNotice('选区只有列表标记,没有需要修改的正文。');
|
|
8101
|
+
setStatus('已保留原列表标记,内容无变化。');
|
|
8102
|
+
return;
|
|
8103
|
+
}
|
|
7782
8104
|
const anchor = hit.anchor || {};
|
|
7783
8105
|
const hasRange = Number.isInteger(anchor.srcStart) && Number.isInteger(anchor.srcEnd);
|
|
7784
8106
|
const parts = Array.isArray(hit.parts) ? hit.parts : [];
|
|
7785
8107
|
const shapeNote = hit.matched === 'shape' ? '识别和原文不一致,已按整个文本框改写。' : '';
|
|
8108
|
+
if (isWordEdit && (parts.length > 1 || Array.isArray(anchor.segments))) {
|
|
8109
|
+
const allParts = parts.length ? parts : (anchor.segments || []).map(segment => ({ anchor: segment,
|
|
8110
|
+
text: wordAnchorText(doc, segment) }));
|
|
8111
|
+
if (!wordCompoundAnchor(doc, allParts)) {
|
|
8112
|
+
cannotLocate(); return;
|
|
8113
|
+
}
|
|
8114
|
+
let firstBody = 0;
|
|
8115
|
+
while (firstBody < allParts.length && wordListMarker(allParts[firstBody].text)) firstBody += 1;
|
|
8116
|
+
const body = allParts.slice(firstBody), combined = wordCompoundAnchor(doc, body);
|
|
8117
|
+
if (!combined || body.some(part => wordListMarker(part.text)) || !continuousWordAnchor(doc, combined)) {
|
|
8118
|
+
const message = '选区中间有未选中的文字或列表标记,请连续选择一段正文后再修改。';
|
|
8119
|
+
setSelectionNotice(message); setStatus(message); return;
|
|
8120
|
+
}
|
|
8121
|
+
// One model request sees the complete selected paragraph. Per-line
|
|
8122
|
+
// rewriting could skip an unchanged first line and fragment the result.
|
|
8123
|
+
generate(combined, instruction, actionId);
|
|
8124
|
+
return;
|
|
8125
|
+
}
|
|
7786
8126
|
if (!hasRange && anchor.kind !== 'cell' && parts.length > 1) {
|
|
7787
|
-
const
|
|
8127
|
+
const located = parts.filter((part) => (
|
|
7788
8128
|
part && part.anchor && Number.isInteger(part.anchor.srcStart) && Number.isInteger(part.anchor.srcEnd)
|
|
7789
8129
|
));
|
|
7790
|
-
if (
|
|
8130
|
+
if (located.length === 0 || (doc && doc.format === 'docx' && located.length < parts.length)) {
|
|
7791
8131
|
cannotLocate();
|
|
7792
8132
|
return;
|
|
7793
8133
|
}
|
|
8134
|
+
const ready = located;
|
|
8135
|
+
if (!ready.length) {
|
|
8136
|
+
setNoChangeNotice('选区只有列表标记,没有需要修改的正文。');
|
|
8137
|
+
setStatus('已保留原列表标记,内容无变化。');
|
|
8138
|
+
return;
|
|
8139
|
+
}
|
|
7794
8140
|
(async () => {
|
|
7795
8141
|
let unchanged = 0, changed = 0, failed = 0;
|
|
7796
8142
|
for (let i = 0; i < ready.length; i += 1) {
|
|
@@ -7804,7 +8150,7 @@ window.__ModuleLoader__.load({
|
|
|
7804
8150
|
if (unchanged) setNoChangeNotice(unchanged === ready.length
|
|
7805
8151
|
? '所选内容与原文一致,内容无变化。' : `${unchanged} 段内容无变化,已跳过。`);
|
|
7806
8152
|
setStatus([changed ? `已生成 ${changed} 段修订` : '', unchanged ? `${unchanged} 段内容无变化` : '',
|
|
7807
|
-
failed ? `${failed} 段处理失败` : '',
|
|
8153
|
+
failed ? `${failed} 段处理失败` : '', located.length < parts.length ? `${parts.length - located.length} 段对不上原文` : ''].filter(Boolean).join(';'));
|
|
7808
8154
|
})();
|
|
7809
8155
|
return;
|
|
7810
8156
|
}
|
|
@@ -7940,6 +8286,28 @@ window.__ModuleLoader__.load({
|
|
|
7940
8286
|
: treeNodes('', 0, treeCtx)),
|
|
7941
8287
|
]);
|
|
7942
8288
|
|
|
8289
|
+
const convertLegacyWord = async () => {
|
|
8290
|
+
if (legacyConvertRef.current || docBusy || busyAction || !projectId || !docPath) return;
|
|
8291
|
+
const target = `${projectId}\u0000${docPath}`, operation = { target };
|
|
8292
|
+
legacyConvertRef.current = operation; setLegacyConverting(true); setOfficeNotice(null);
|
|
8293
|
+
setStatus('正在转换为可修订的 Word 文档…');
|
|
8294
|
+
try {
|
|
8295
|
+
const entry = await transport.convertWord(projectId, docPath);
|
|
8296
|
+
if (!entry?.relPath) throw new Error('转换未返回可编辑的 Word 文件');
|
|
8297
|
+
setShelfReloadKey(value => value + 1);
|
|
8298
|
+
if (chatTargetRef.current === target) {
|
|
8299
|
+
await openDoc(entry.relPath, { projectId });
|
|
8300
|
+
setStatus(`已打开可修订副本 ${entry.name || entry.relPath}`);
|
|
8301
|
+
}
|
|
8302
|
+
} catch (error) {
|
|
8303
|
+
if (chatTargetRef.current === target) {
|
|
8304
|
+
setOfficeNotice({ message: normalizeError(error) }); setStatus(`转换失败:${normalizeError(error)}`);
|
|
8305
|
+
}
|
|
8306
|
+
} finally {
|
|
8307
|
+
if (legacyConvertRef.current === operation) { legacyConvertRef.current = null; setLegacyConverting(false); }
|
|
8308
|
+
}
|
|
8309
|
+
};
|
|
8310
|
+
|
|
7943
8311
|
// ---- 渲染:中栏正文
|
|
7944
8312
|
const center = h(DocReader, {
|
|
7945
8313
|
key: 'center',
|
|
@@ -7947,6 +8315,7 @@ window.__ModuleLoader__.load({
|
|
|
7947
8315
|
docBusy,
|
|
7948
8316
|
docError,
|
|
7949
8317
|
pendingByBlock,
|
|
8318
|
+
visionRoute: chosenModel(chatModel),
|
|
7950
8319
|
busyAction: busyAction || (chatBusy && chatKind === 'translate' ? 'translate' : ''),
|
|
7951
8320
|
actions: SELECTION_ACTIONS,
|
|
7952
8321
|
projectId,
|
|
@@ -7966,6 +8335,9 @@ window.__ModuleLoader__.load({
|
|
|
7966
8335
|
onAction: runAction,
|
|
7967
8336
|
previewEpoch,
|
|
7968
8337
|
officeNotice,
|
|
8338
|
+
onConvertWord: convertLegacyWord,
|
|
8339
|
+
convertingWord: legacyConverting,
|
|
8340
|
+
conversionDisabled: docBusy || Boolean(busyAction),
|
|
7969
8341
|
onRefresh: reloadDocument,
|
|
7970
8342
|
refreshDisabled: docBusy || Boolean(busyAction),
|
|
7971
8343
|
onAccept: acceptRevision,
|
|
@@ -8045,7 +8417,7 @@ window.__ModuleLoader__.load({
|
|
|
8045
8417
|
messageFooter(message),
|
|
8046
8418
|
])),
|
|
8047
8419
|
chatBusy ? h('div', { key: 'thinking', className: P + 'Thinking', 'data-chat-thinking': '1' },
|
|
8048
|
-
h(Processing, { kind: chatKind, text: chatKind === 'translate' ? '正在翻译…' : '思考中…' })) : null,
|
|
8420
|
+
h(Processing, { kind: chatKind, text: chatProgress || (chatKind === 'translate' ? '正在翻译…' : '思考中…') })) : null,
|
|
8049
8421
|
),
|
|
8050
8422
|
chatError ? h('div', { key: 'chatErr', className: P + 'Err', 'data-chat-error': '1' }, chatError) : null,
|
|
8051
8423
|
h('div', { key: 'composer', className: P + 'Composer' }, [
|
|
@@ -8107,7 +8479,7 @@ window.__ModuleLoader__.load({
|
|
|
8107
8479
|
h('option', { key: 'default', value: '' }, chatModelsBusy ? '加载模型…' : `跟随 DSH${chatDefaultModel ? ` · ${chatDefaultModel.model}` : ''}`),
|
|
8108
8480
|
chatModel && !chatModels.some(item => JSON.stringify([item.provider, item.model]) === chatModel)
|
|
8109
8481
|
? h('option', { key: 'unavailable', value: chatModel }, '所选模型暂不可用') : null,
|
|
8110
|
-
...chatModels.map(item => h('option', { key: JSON.stringify([item.provider, item.model]), value: JSON.stringify([item.provider, item.model]) }, `${item.label} · ${item.providerLabel}`)),
|
|
8482
|
+
...chatModels.map(item => h('option', { key: JSON.stringify([item.provider, item.model]), value: JSON.stringify([item.provider, item.model]) }, `${item.label} · ${item.providerLabel}${item.inputModalities?.includes('image') ? ' · 图片' : ''}`)),
|
|
8111
8483
|
]),
|
|
8112
8484
|
]),
|
|
8113
8485
|
h('div', { key: 'hint', className: P + 'Hint' },
|
|
@@ -8123,7 +8495,11 @@ window.__ModuleLoader__.load({
|
|
|
8123
8495
|
collapsed: collapsedRevisions.includes(item.id),
|
|
8124
8496
|
onToggleCollapsed: id => setCollapsedRevisions(previous => previous.includes(id)
|
|
8125
8497
|
? previous.filter(value => value !== id) : previous.concat(id).slice(-500)),
|
|
8126
|
-
busy:
|
|
8498
|
+
busy: store.isBusy(`rev:${item.id}`),
|
|
8499
|
+
writeBusy: Boolean(busyAction),
|
|
8500
|
+
acceptError: revisionErrors.get(item.id),
|
|
8501
|
+
progress: acceptingRevisionId === item.id ? h(Processing, { kind: 'accept',
|
|
8502
|
+
text: /\.docx$/i.test(item.docPath || '') ? '正在核对版式并写回…' : '正在写回修订…' }) : null,
|
|
8127
8503
|
writeGroup: (revision) => store.writeGroup(revision),
|
|
8128
8504
|
onAccept: acceptRevision,
|
|
8129
8505
|
onReject: rejectRevision,
|
|
@@ -8141,16 +8517,26 @@ window.__ModuleLoader__.load({
|
|
|
8141
8517
|
store.loading ? h('div', { className: P + 'Hint' }, '加载修订中…') : null,
|
|
8142
8518
|
store.error ? h('div', { className: P + 'Err' }, store.error) : null,
|
|
8143
8519
|
noChangeNotice ? h('div', { className: P + 'NoChange', role: 'status', 'data-revision-unchanged': '1' }, noChangeNotice) : null,
|
|
8144
|
-
|
|
8145
|
-
h('div', { key:
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8520
|
+
Array.from(generationJobs.values()).filter(job => job.target === generationTargetRef.current).reverse().map(job =>
|
|
8521
|
+
h('div', { key: job.id, className: P + 'RevCard', 'data-generation-id': job.id,
|
|
8522
|
+
'data-generation-state': job.state, 'data-generation-action': job.snapshot.action,
|
|
8523
|
+
'data-generation-error': job.state === 'failed' ? '1' : undefined,
|
|
8524
|
+
role: job.state === 'failed' ? 'alert' : 'status' }, [
|
|
8525
|
+
h('div', { key: 'label', className: P + 'RevCollapse' }, `${job.label} · ${anchorLabel(job.snapshot.anchor)}`),
|
|
8526
|
+
typeof job.original === 'string' ? h('div', { key: 'original', className: P + 'GenerationOriginal',
|
|
8527
|
+
title: job.original }, job.original) : null,
|
|
8528
|
+
job.state === 'running' ? h(Processing, { key: 'progress', kind: job.snapshot.action,
|
|
8529
|
+
text: `正在${job.label}…` }) : null,
|
|
8530
|
+
job.message ? h('div', { key: 'message', className: job.state === 'failed' ? P + 'Err' : P + 'NoChange',
|
|
8531
|
+
'data-revision-unchanged': job.state === 'unchanged' ? '1' : undefined }, job.message) : null,
|
|
8532
|
+
job.state === 'failed' ? h('button', { key: 'retry', type: 'button', className: P + 'Btn',
|
|
8533
|
+
onClick: () => generate(job.snapshot.anchor, job.snapshot.instruction, job.snapshot.action,
|
|
8534
|
+
{ request: job.snapshot, replaceId: job.replaceId, jobId: job.id }) }, '重试') : null,
|
|
8535
|
+
job.state !== 'running' ? h('button', { key: 'dismiss', type: 'button', className: P + 'Btn',
|
|
8536
|
+
onClick: () => updateGenerationJobs(previous => { const next = new Map(previous); next.delete(job.id); return next; }) }, '删除') : null,
|
|
8537
|
+
])),
|
|
8538
|
+
items.length === 0 && !store.loading && !noChangeNotice
|
|
8539
|
+
&& !Array.from(generationJobs.values()).some(job => job.target === generationTargetRef.current)
|
|
8154
8540
|
? h('div', { className: P + 'Hint' }, revisionAllowed(doc)
|
|
8155
8541
|
? '选中正文后点「润色 / 扩写」生成修订;接受才会写回文件。'
|
|
8156
8542
|
: '这份文档只能阅读,不能生成写回修订。')
|
|
@@ -8482,6 +8868,8 @@ window.__ModuleLoader__.load({
|
|
|
8482
8868
|
.${P}NoChange{padding:12px;border:1px solid var(--dsw-alias-border-l1);border-radius:var(--dsw-radius-md,8px);
|
|
8483
8869
|
background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;margin-bottom:10px}
|
|
8484
8870
|
.${P}GenerationError{padding:8px 10px;margin-bottom:10px}
|
|
8871
|
+
.${P}GenerationOriginal{max-height:90px;overflow:auto;white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.6;
|
|
8872
|
+
color:var(--dsw-alias-label-secondary);padding:6px 10px}
|
|
8485
8873
|
.${P}Processing{display:flex;align-items:center;gap:8px;padding:8px 10px;color:var(--dsw-alias-label-secondary);font-size:12px;flex:none}
|
|
8486
8874
|
.${P}WhaleTail{width:20px;height:20px;contain:strict;flex:none;display:inline-flex;position:relative;overflow:hidden}
|
|
8487
8875
|
.${P}WhaleAnimated{display:none;position:absolute;inset:0}
|
|
@@ -8691,8 +9079,18 @@ window.__ModuleLoader__.load({
|
|
|
8691
9079
|
.${P}SlidePrev{left:12px}
|
|
8692
9080
|
.${P}SlideNext{right:12px}
|
|
8693
9081
|
.${P}SlideCount{position:absolute;left:50%;bottom:14px;transform:translateX(-50%);z-index:2;
|
|
8694
|
-
color:#fff;background:rgba(0,0,0,.
|
|
9082
|
+
color:#fff;background:rgba(0,0,0,.65);border-radius:12px;padding:5px 8px;font-size:12px;max-width:calc(100% - 24px);
|
|
8695
9083
|
font-variant-numeric:tabular-nums}
|
|
9084
|
+
.${P}SlideCountControls{display:flex;align-items:center;justify-content:center;gap:6px;white-space:nowrap}
|
|
9085
|
+
.${P}SlidePageInput{box-sizing:border-box;width:54px;min-width:0;padding:3px 4px;border:1px solid rgba(255,255,255,.4);
|
|
9086
|
+
border-radius:5px;background:rgba(255,255,255,.12);color:#fff;font:inherit;line-height:18px;text-align:center}
|
|
9087
|
+
.${P}SlidePageInput:focus-visible{outline:2px solid #fff;outline-offset:1px}
|
|
9088
|
+
.${P}SlidePageInput[aria-invalid="true"]{border-color:#ffaeae}
|
|
9089
|
+
.${P}SlideJumpButton{padding:3px 7px;border:0;border-radius:5px;background:rgba(255,255,255,.18);color:#fff;
|
|
9090
|
+
font:inherit;line-height:20px;cursor:pointer}
|
|
9091
|
+
.${P}SlideJumpButton:hover:not(:disabled){background:rgba(255,255,255,.3)}
|
|
9092
|
+
.${P}SlidePageInput:disabled,.${P}SlideJumpButton:disabled{opacity:.5;cursor:default}
|
|
9093
|
+
.${P}SlideJumpError{margin-top:4px;max-width:240px;white-space:normal;line-height:18px;color:#ffcdcd;text-align:center}
|
|
8696
9094
|
.${P}ViewBar .${P}Hint{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:0}
|
|
8697
9095
|
.${P}SelectionNotice{flex:none;padding:10px 12px;font-size:13px;line-height:1.6;
|
|
8698
9096
|
color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border-bottom:1px solid var(--dsw-alias-border-l1)}
|
|
@@ -8762,6 +9160,12 @@ window.__ModuleLoader__.load({
|
|
|
8762
9160
|
exports.bindWordLayoutLines = bindWordLayoutLines;
|
|
8763
9161
|
exports.wordNativeSelection = wordNativeSelection;
|
|
8764
9162
|
exports.wordBoxSelection = wordBoxSelection;
|
|
9163
|
+
exports.wordCompoundAnchor = wordCompoundAnchor;
|
|
9164
|
+
exports.wordAnchorText = wordAnchorText;
|
|
9165
|
+
exports.continuousWordAnchor = continuousWordAnchor;
|
|
9166
|
+
exports.wordListMarker = wordListMarker;
|
|
9167
|
+
exports.captureRequest = captureRequest;
|
|
9168
|
+
exports.groupRevisionVariants = groupRevisionVariants;
|
|
8765
9169
|
exports.rectangleSpans = rectangleSpans;
|
|
8766
9170
|
exports.placeByShapes = placeByShapes;
|
|
8767
9171
|
exports.mapOffsetToSrc = mapOffsetToSrc;
|
|
@@ -9155,7 +9559,13 @@ window.__ModuleLoader__.load({
|
|
|
9155
9559
|
const p = props || {};
|
|
9156
9560
|
const cover = normalizeCover(p.cover, p.relPath, p.format);
|
|
9157
9561
|
const [failed, setFailed] = React.useState(false);
|
|
9562
|
+
const [loaded, setLoaded] = React.useState(false);
|
|
9158
9563
|
const [naturalShape, setNaturalShape] = React.useState(null);
|
|
9564
|
+
React.useEffect(() => {
|
|
9565
|
+
setFailed(false);
|
|
9566
|
+
setLoaded(false);
|
|
9567
|
+
setNaturalShape(null);
|
|
9568
|
+
}, [cover.imageUrl, cover.relPath]);
|
|
9159
9569
|
// 形状优先级:父级/显式给的 > 图片实际尺寸测出来的 > 按 cover.width/height 推断 > 竖版。
|
|
9160
9570
|
// 即使图片失败回落生成封面也保持同一卡片形状,避免书架在 <img> 出错时跳版(§17.3.1)。
|
|
9161
9571
|
const controlled = p.shape && p.shape.key ? p.shape : null;
|
|
@@ -9173,6 +9583,7 @@ window.__ModuleLoader__.load({
|
|
|
9173
9583
|
setFailed(true);
|
|
9174
9584
|
return;
|
|
9175
9585
|
}
|
|
9586
|
+
setLoaded(true);
|
|
9176
9587
|
const next = shapeFor({ width: w, height: hgt });
|
|
9177
9588
|
if (next.key === shape.key) return;
|
|
9178
9589
|
if (controlled) {
|
|
@@ -9190,10 +9601,13 @@ window.__ModuleLoader__.load({
|
|
|
9190
9601
|
'data-cover-kind': 'page',
|
|
9191
9602
|
'data-cover-shape': shape.key,
|
|
9192
9603
|
'data-cover-src': cover.imageUrl,
|
|
9604
|
+
'aria-busy': !loaded,
|
|
9193
9605
|
}, [
|
|
9606
|
+
!loaded ? h(CoverArt, { key: 'placeholder', cover, shape, size: width,
|
|
9607
|
+
className: P + 'CoverPlaceholder', relPath: cover.relPath, format: cover.format }) : null,
|
|
9194
9608
|
h('img', {
|
|
9195
9609
|
key: 'img',
|
|
9196
|
-
className: P + 'CoverImg',
|
|
9610
|
+
className: cx(P + 'CoverImg', !loaded && P + 'CoverImgLoading'),
|
|
9197
9611
|
src: cover.imageUrl,
|
|
9198
9612
|
alt: cover.title,
|
|
9199
9613
|
loading: 'lazy',
|
|
@@ -9856,8 +10270,10 @@ window.__ModuleLoader__.load({
|
|
|
9856
10270
|
.${P}Book_landscape .${P}Cover{box-shadow:0 6px 18px rgba(0,0,0,.28)}
|
|
9857
10271
|
.${P}Book_square .${P}Cover{border-radius:8px}
|
|
9858
10272
|
.${P}CoverPage{background:var(--dsw-alias-bg-layer-2)}
|
|
9859
|
-
.${P}
|
|
10273
|
+
.${P}CoverPlaceholder{position:absolute;inset:0;box-shadow:none}
|
|
10274
|
+
.${P}CoverImg{position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:cover;object-position:top center;
|
|
9860
10275
|
background:var(--dsw-alias-bg-layer-2)}
|
|
10276
|
+
.${P}CoverImgLoading{opacity:0}
|
|
9861
10277
|
.${P}PageFade{position:absolute;left:0;right:0;bottom:0;height:26%;pointer-events:none;
|
|
9862
10278
|
background:linear-gradient(to top,rgba(0,0,0,.28),rgba(0,0,0,0))}
|
|
9863
10279
|
.${P}PageBadge{position:absolute;left:8px;bottom:8px;background:rgba(0,0,0,.42);color:#fff;
|