@xqyz/xq-cli 0.1.2 → 0.1.3
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 +42 -1
- package/package.json +3 -2
- package/src/cli.mjs +186 -1
- package/src/outline-edit.mjs +298 -0
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ npm pack
|
|
|
40
40
|
That command produces a `.tgz` tarball which can be shared and installed with:
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
|
-
npm install -g .\xqyz-xq-cli-0.1.
|
|
43
|
+
npm install -g .\xqyz-xq-cli-0.1.3.tgz
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
## Usage
|
|
@@ -54,6 +54,10 @@ xq-cli wizard --file D:\bids\tender.docx
|
|
|
54
54
|
xq-cli init --file D:\bids\tender.docx --wait
|
|
55
55
|
xq-cli outline --cid <cid> --interactive --wait
|
|
56
56
|
xq-cli outline --cid <cid> --wait
|
|
57
|
+
xq-cli outline view --cid <cid>
|
|
58
|
+
xq-cli outline view --cid <cid> --out D:\output\outline.json
|
|
59
|
+
xq-cli outline update --cid <cid> --file D:\output\outline.json --dry-run --json
|
|
60
|
+
xq-cli outline update --cid <cid> --file D:\output\outline.json --yes
|
|
57
61
|
xq-cli write --cid <cid> --wait
|
|
58
62
|
xq-cli status --cid <cid> --content --json
|
|
59
63
|
xq-cli export --cid <cid> --interactive --out D:\output
|
|
@@ -72,6 +76,43 @@ When `outline --wait` or `write --wait` hits a required response-basis step, the
|
|
|
72
76
|
|
|
73
77
|
`order/charge -> c/viewStep(101) -> task/autoGenerateContent`
|
|
74
78
|
|
|
79
|
+
## View and update an existing outline
|
|
80
|
+
|
|
81
|
+
View the complete current outline without triggering outline generation:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
xq-cli outline view --cid <cid>
|
|
85
|
+
xq-cli outline view --cid <cid> --json
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Export an editable JSON copy:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
xq-cli outline view --cid <cid> --out D:\output\outline.json
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Edit the `outLine` array in that file. Keep an existing chapter's `id`, change `text`, `theme`, or `important`, reorder the array to reorder chapters, add a chapter without an `id`, and remove a chapter from the array to delete it. `important` accepts `1` (key chapter) or `2` (normal chapter).
|
|
95
|
+
|
|
96
|
+
Always preview the server-side diff first:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
xq-cli outline update --cid <cid> --file D:\output\outline.json --dry-run
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Apply after reviewing the preview:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
xq-cli outline update --cid <cid> --file D:\output\outline.json --yes
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Deleting existing chapters has an additional protection flag:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
xq-cli outline update --cid <cid> --file D:\output\outline.json --yes --allow-delete
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The update command automatically calculates `updateFlag` by comparing the edited file with the latest server outline. Changes to chapter titles or themes may cause the backend to regenerate affected child directories asynchronously.
|
|
115
|
+
|
|
75
116
|
## Interactive menus
|
|
76
117
|
|
|
77
118
|
Use the full wizard if you want a CLI flow close to the frontend configuration panel:
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xqyz/xq-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "CLI for xique bid-book task workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"pack": "npm pack"
|
|
7
|
+
"pack": "npm pack",
|
|
8
|
+
"test": "node --test"
|
|
8
9
|
},
|
|
9
10
|
"bin": {
|
|
10
11
|
"xq-cli": "./bin/xq-cli.js"
|
package/src/cli.mjs
CHANGED
|
@@ -12,6 +12,13 @@ import axios from 'axios';
|
|
|
12
12
|
import FormData from 'form-data';
|
|
13
13
|
import { templateData } from './templateData.mjs';
|
|
14
14
|
import { AI_PURPOSE_MAP, detectFileType } from './fileTypeConfig.mjs';
|
|
15
|
+
import {
|
|
16
|
+
buildEditableOutlineDocument,
|
|
17
|
+
extractOutlineRows,
|
|
18
|
+
formatOutlineDiffLines,
|
|
19
|
+
formatOutlineLines,
|
|
20
|
+
prepareOutlineUpdate,
|
|
21
|
+
} from './outline-edit.mjs';
|
|
15
22
|
|
|
16
23
|
const APP_DIR = path.join(os.homedir(), '.xq-opencli');
|
|
17
24
|
const CONFIG_FILE = path.join(APP_DIR, 'config.json');
|
|
@@ -328,7 +335,19 @@ async function main() {
|
|
|
328
335
|
await handleParseStatus(parsed, state);
|
|
329
336
|
return;
|
|
330
337
|
case 'outline':
|
|
331
|
-
|
|
338
|
+
if (isOutlineAction(parsed, ['view', 'show'])) {
|
|
339
|
+
await handleOutlineView(parsed, state);
|
|
340
|
+
} else if (isOutlineAction(parsed, ['update', 'edit'])) {
|
|
341
|
+
await handleOutlineUpdate(parsed, state);
|
|
342
|
+
} else {
|
|
343
|
+
await handleOutline(parsed, state);
|
|
344
|
+
}
|
|
345
|
+
return;
|
|
346
|
+
case 'outline-view':
|
|
347
|
+
await handleOutlineView(parsed, state);
|
|
348
|
+
return;
|
|
349
|
+
case 'outline-update':
|
|
350
|
+
await handleOutlineUpdate(parsed, state);
|
|
332
351
|
return;
|
|
333
352
|
case 'write':
|
|
334
353
|
await handleWrite(parsed, state);
|
|
@@ -359,6 +378,8 @@ Commands
|
|
|
359
378
|
init Upload files and initialize a task
|
|
360
379
|
parse-status Query or wait for parse completion
|
|
361
380
|
outline Pre-set task params and trigger outline generation
|
|
381
|
+
outline view View the current outline without triggering generation
|
|
382
|
+
outline update Update the current outline from an edited JSON file
|
|
362
383
|
write Charge if needed, enter content phase, and trigger content generation
|
|
363
384
|
status Query task status
|
|
364
385
|
choices Print selectable outline/export config values
|
|
@@ -382,6 +403,10 @@ Examples
|
|
|
382
403
|
xq-cli outline --cid <cid> --wait --ignore-missing-bill
|
|
383
404
|
xq-cli outline --cid <cid> --wait --reference-type 0
|
|
384
405
|
xq-cli outline --cid <cid> --page-scope 3 --base expert --theme-style 4 --table-quantity rich --plan-mode quick --wait
|
|
406
|
+
xq-cli outline view --cid <cid>
|
|
407
|
+
xq-cli outline view --cid <cid> --out D:\\output\\outline.json
|
|
408
|
+
xq-cli outline update --cid <cid> --file D:\\output\\outline.json --dry-run --json
|
|
409
|
+
xq-cli outline update --cid <cid> --file D:\\output\\outline.json --yes
|
|
385
410
|
xq-cli write --cid <cid> --type 1 --wait
|
|
386
411
|
xq-cli status --cid <cid> --content
|
|
387
412
|
xq-cli export --cid <cid> --interactive --out D:\\output\\
|
|
@@ -396,6 +421,9 @@ Common options
|
|
|
396
421
|
--reference-type <0|1|2> Outline reference choice: 0=merge, 1=format only, 2=score only
|
|
397
422
|
--browser Force browser authorization login; browser is the default when credentials are not provided
|
|
398
423
|
--interactive Open prompt menus for the current command
|
|
424
|
+
--dry-run Preview an outline update without saving it
|
|
425
|
+
--yes Confirm an outline update without an interactive prompt
|
|
426
|
+
--allow-delete Allow an outline update that removes existing chapters
|
|
399
427
|
--ignore-short-requirement Follow the frontend ignore/continue path for requirement_judgement=0
|
|
400
428
|
--ignore-missing-bill Follow the frontend ignore/continue path for billOfQuantities_judgement=0
|
|
401
429
|
|
|
@@ -443,6 +471,11 @@ Export style
|
|
|
443
471
|
`.trim());
|
|
444
472
|
}
|
|
445
473
|
|
|
474
|
+
function isOutlineAction(args, actions) {
|
|
475
|
+
const action = String(args?._?.[1] || '').trim().toLowerCase();
|
|
476
|
+
return actions.includes(action);
|
|
477
|
+
}
|
|
478
|
+
|
|
446
479
|
async function handleLogin(args, state) {
|
|
447
480
|
const baseUrl = normalizeBaseUrl(stringOption(args, 'baseUrl', state.baseUrl));
|
|
448
481
|
if (!baseUrl) {
|
|
@@ -755,6 +788,158 @@ async function handleOutline(args, state) {
|
|
|
755
788
|
return resultPayload;
|
|
756
789
|
}
|
|
757
790
|
|
|
791
|
+
async function handleOutlineView(args, state) {
|
|
792
|
+
const client = createAuthorizedClient(args, state);
|
|
793
|
+
const cid = resolveCid(args, state);
|
|
794
|
+
const response = await fetchOutlineDetail(client, cid);
|
|
795
|
+
const outlineDetail = response?.data || {};
|
|
796
|
+
const editableDocument = buildEditableOutlineDocument(cid, outlineDetail);
|
|
797
|
+
const savedPath = stringOption(args, 'out')
|
|
798
|
+
? writeOutlineEditFile(stringOption(args, 'out'), cid, editableDocument)
|
|
799
|
+
: null;
|
|
800
|
+
|
|
801
|
+
const resultPayload = {
|
|
802
|
+
command: 'outline-view',
|
|
803
|
+
cid,
|
|
804
|
+
outlineDetail,
|
|
805
|
+
editableDocument: savedPath ? editableDocument : undefined,
|
|
806
|
+
savedPath,
|
|
807
|
+
};
|
|
808
|
+
outputResult(args, resultPayload, [
|
|
809
|
+
...formatOutlineLines(cid, outlineDetail),
|
|
810
|
+
savedPath ? `savedPath: ${savedPath}` : 'hint: pass --out <file-or-directory> to save an editable JSON file.',
|
|
811
|
+
]);
|
|
812
|
+
return resultPayload;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
async function handleOutlineUpdate(args, state) {
|
|
816
|
+
const client = createAuthorizedClient(args, state);
|
|
817
|
+
const cid = resolveCid(args, state);
|
|
818
|
+
const filePath = path.resolve(requiredOption(args, 'file'));
|
|
819
|
+
const editDocument = readOutlineEditFile(filePath);
|
|
820
|
+
validateOutlineEditCid(editDocument, cid, filePath);
|
|
821
|
+
|
|
822
|
+
const currentResponse = await fetchOutlineDetail(client, cid);
|
|
823
|
+
const currentOutlineDetail = currentResponse?.data || {};
|
|
824
|
+
const proposedRows = extractOutlineRows(editDocument);
|
|
825
|
+
const prepared = prepareOutlineUpdate(getOutlineRows(currentOutlineDetail), proposedRows);
|
|
826
|
+
const requestData = {
|
|
827
|
+
cid,
|
|
828
|
+
outLine: prepared.payload,
|
|
829
|
+
source: 2,
|
|
830
|
+
};
|
|
831
|
+
const baseResult = {
|
|
832
|
+
command: 'outline-update',
|
|
833
|
+
cid,
|
|
834
|
+
file: filePath,
|
|
835
|
+
dryRun: booleanOption(args, 'dryRun'),
|
|
836
|
+
applied: false,
|
|
837
|
+
hasChanges: prepared.hasChanges,
|
|
838
|
+
diff: prepared.diff,
|
|
839
|
+
requestPreview: requestData,
|
|
840
|
+
};
|
|
841
|
+
const diffLines = formatOutlineDiffLines(prepared.diff);
|
|
842
|
+
|
|
843
|
+
if (!prepared.hasChanges) {
|
|
844
|
+
const resultPayload = {
|
|
845
|
+
...baseResult,
|
|
846
|
+
reason: 'no_changes',
|
|
847
|
+
};
|
|
848
|
+
outputResult(args, resultPayload, [
|
|
849
|
+
`outline unchanged: cid=${cid}`,
|
|
850
|
+
...diffLines,
|
|
851
|
+
]);
|
|
852
|
+
return resultPayload;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (booleanOption(args, 'dryRun')) {
|
|
856
|
+
const resultPayload = {
|
|
857
|
+
...baseResult,
|
|
858
|
+
reason: 'dry_run',
|
|
859
|
+
};
|
|
860
|
+
outputResult(args, resultPayload, [
|
|
861
|
+
`outline update preview: cid=${cid}`,
|
|
862
|
+
...diffLines,
|
|
863
|
+
'dry-run only: no server data was changed.',
|
|
864
|
+
prepared.diff.deleted.length > 0
|
|
865
|
+
? 'delete protection: the real update also requires --allow-delete.'
|
|
866
|
+
: 'hint: rerun with --yes to apply this update.',
|
|
867
|
+
]);
|
|
868
|
+
return resultPayload;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (prepared.diff.deleted.length > 0 && !booleanOption(args, 'allowDelete')) {
|
|
872
|
+
throw new Error(`Outline update would delete ${prepared.diff.deleted.length} existing chapter(s). Review with --dry-run, then pass --allow-delete to confirm that deletion scope.`);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const confirmed = booleanOption(args, 'yes') || await confirmOutlineUpdate(args, cid, prepared.diff);
|
|
876
|
+
if (!confirmed) {
|
|
877
|
+
const resultPayload = {
|
|
878
|
+
...baseResult,
|
|
879
|
+
reason: 'cancelled',
|
|
880
|
+
};
|
|
881
|
+
outputResult(args, resultPayload, [
|
|
882
|
+
`outline update cancelled: cid=${cid}`,
|
|
883
|
+
...diffLines,
|
|
884
|
+
]);
|
|
885
|
+
return resultPayload;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const response = await postJson(client, '/c/updateOutLine', requestData);
|
|
889
|
+
saveTaskSnapshot(state, cid, {
|
|
890
|
+
outlineUpdatedAt: new Date().toISOString(),
|
|
891
|
+
});
|
|
892
|
+
const resultPayload = {
|
|
893
|
+
...baseResult,
|
|
894
|
+
applied: true,
|
|
895
|
+
response,
|
|
896
|
+
};
|
|
897
|
+
outputResult(args, resultPayload, [
|
|
898
|
+
`outline update accepted: cid=${cid}`,
|
|
899
|
+
...diffLines,
|
|
900
|
+
'note: changed chapters may trigger asynchronous directory regeneration on the server.',
|
|
901
|
+
`next: xq-cli outline view --cid ${cid}`,
|
|
902
|
+
]);
|
|
903
|
+
return resultPayload;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
async function confirmOutlineUpdate(args, cid, diff) {
|
|
907
|
+
if (!supportsInteractivePrompt()) {
|
|
908
|
+
throw new Error('Outline update changes server data. Review it with --dry-run, then rerun with --yes.');
|
|
909
|
+
}
|
|
910
|
+
return runPromptSession(args, async context => {
|
|
911
|
+
context.promptStream.write(`\n${formatOutlineDiffLines(diff).join('\n')}\n`);
|
|
912
|
+
return promptYesNo(context, `Apply this outline update to cid=${cid}`, false);
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function readOutlineEditFile(filePath) {
|
|
917
|
+
ensureFileExists(filePath);
|
|
918
|
+
try {
|
|
919
|
+
const raw = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '');
|
|
920
|
+
return JSON.parse(raw);
|
|
921
|
+
} catch (error) {
|
|
922
|
+
throw new Error(`Failed to parse outline JSON: ${filePath}`);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function validateOutlineEditCid(document, cid, filePath) {
|
|
927
|
+
const documentCid = document && !Array.isArray(document) ? String(document.cid || '').trim() : '';
|
|
928
|
+
if (documentCid && documentCid !== cid) {
|
|
929
|
+
throw new Error(`Outline file cid (${documentCid}) does not match --cid (${cid}): ${filePath}`);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function writeOutlineEditFile(outputOption, cid, document) {
|
|
934
|
+
const resolvedOutput = path.resolve(outputOption);
|
|
935
|
+
const outputIsDirectory = !path.extname(resolvedOutput);
|
|
936
|
+
const defaultName = `outline-${normalizeDownloadFileName(cid) || 'task'}.json`;
|
|
937
|
+
const finalPath = outputIsDirectory ? path.join(resolvedOutput, defaultName) : resolvedOutput;
|
|
938
|
+
fs.mkdirSync(path.dirname(finalPath), { recursive: true });
|
|
939
|
+
fs.writeFileSync(finalPath, `${JSON.stringify(document, null, 2)}\n`, 'utf8');
|
|
940
|
+
return finalPath;
|
|
941
|
+
}
|
|
942
|
+
|
|
758
943
|
async function handleWrite(args, state) {
|
|
759
944
|
const client = createAuthorizedClient(args, state);
|
|
760
945
|
const cid = resolveCid(args, state);
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
const CHAPTER_PREFIX_PATTERNS = [
|
|
2
|
+
/^\s*第[零〇一二三四五六七八九十百千万两\d]+章\s*/u,
|
|
3
|
+
/^\s*\d+\s*[、..]\s*/u,
|
|
4
|
+
];
|
|
5
|
+
|
|
6
|
+
export function extractOutlineRows(document) {
|
|
7
|
+
const candidates = [
|
|
8
|
+
document,
|
|
9
|
+
document?.outLine,
|
|
10
|
+
document?.data?.outLine,
|
|
11
|
+
document?.outlineDetail?.outLine,
|
|
12
|
+
document?.waitResult?.outlineDetail?.outLine,
|
|
13
|
+
];
|
|
14
|
+
const rows = candidates.find(Array.isArray);
|
|
15
|
+
if (!rows) {
|
|
16
|
+
throw new Error('Outline JSON must be an array or contain an outLine array.');
|
|
17
|
+
}
|
|
18
|
+
return rows;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildEditableOutlineDocument(cid, outlineDetail = {}) {
|
|
22
|
+
return {
|
|
23
|
+
format: 'xq-cli-outline@1',
|
|
24
|
+
cid,
|
|
25
|
+
outlineStatus: Boolean(outlineDetail.outline_status),
|
|
26
|
+
outLine: extractOutlineRows(outlineDetail).map(row => compactObject({
|
|
27
|
+
id: normalizeOptionalString(row?.id),
|
|
28
|
+
text: normalizeText(row?.text),
|
|
29
|
+
theme: normalizeTheme(row?.theme),
|
|
30
|
+
important: normalizeImportant(row?.important, 2),
|
|
31
|
+
})),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function prepareOutlineUpdate(currentRows, proposedRows) {
|
|
36
|
+
if (!Array.isArray(currentRows)) {
|
|
37
|
+
throw new Error('Current outline data is invalid: outLine must be an array.');
|
|
38
|
+
}
|
|
39
|
+
if (!Array.isArray(proposedRows) || proposedRows.length === 0) {
|
|
40
|
+
throw new Error('Updated outline must contain at least one chapter.');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const current = currentRows.map((row, index) => normalizeCurrentRow(row, index));
|
|
44
|
+
const currentById = new Map(current.filter(row => row.id).map(row => [row.id, row]));
|
|
45
|
+
const seenIds = new Set();
|
|
46
|
+
|
|
47
|
+
const proposed = proposedRows.map((row, index) => {
|
|
48
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) {
|
|
49
|
+
throw new Error(`Outline chapter ${index + 1} must be a JSON object.`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const id = normalizeOptionalString(row.id);
|
|
53
|
+
if (id) {
|
|
54
|
+
if (seenIds.has(id)) {
|
|
55
|
+
throw new Error(`Duplicate outline chapter id: ${id}`);
|
|
56
|
+
}
|
|
57
|
+
if (!currentById.has(id)) {
|
|
58
|
+
throw new Error(`Outline chapter id does not belong to the current task: ${id}`);
|
|
59
|
+
}
|
|
60
|
+
seenIds.add(id);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const text = normalizeText(row.text);
|
|
64
|
+
if (!stripChapterPrefix(text)) {
|
|
65
|
+
throw new Error(`Outline chapter ${index + 1} has an empty text field.`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const existing = id ? currentById.get(id) : null;
|
|
69
|
+
const important = normalizeImportant(row.important, existing?.important ?? 2, index);
|
|
70
|
+
const theme = normalizeTheme(row.theme);
|
|
71
|
+
const sequelFlag = normalizeSequelFlag(row.sequelFlag, index);
|
|
72
|
+
return {
|
|
73
|
+
id,
|
|
74
|
+
text,
|
|
75
|
+
theme,
|
|
76
|
+
important,
|
|
77
|
+
sequelFlag,
|
|
78
|
+
index,
|
|
79
|
+
existing,
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const added = [];
|
|
84
|
+
const updated = [];
|
|
85
|
+
const importanceChanged = [];
|
|
86
|
+
const moved = [];
|
|
87
|
+
const sequelRequested = [];
|
|
88
|
+
const payload = proposed.map(row => {
|
|
89
|
+
const existing = row.existing;
|
|
90
|
+
const changedFields = existing
|
|
91
|
+
? collectChangedFields(existing, row)
|
|
92
|
+
: [];
|
|
93
|
+
const contentChanged = changedFields.includes('text') || changedFields.includes('theme');
|
|
94
|
+
|
|
95
|
+
if (!existing) {
|
|
96
|
+
added.push(describeAddedRow(row));
|
|
97
|
+
} else {
|
|
98
|
+
if (contentChanged) {
|
|
99
|
+
updated.push(describeUpdatedRow(existing, row, changedFields));
|
|
100
|
+
}
|
|
101
|
+
if (existing.important !== row.important) {
|
|
102
|
+
importanceChanged.push({
|
|
103
|
+
id: row.id,
|
|
104
|
+
text: row.text,
|
|
105
|
+
before: existing.important,
|
|
106
|
+
after: row.important,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (existing.index !== row.index) {
|
|
110
|
+
moved.push({
|
|
111
|
+
id: row.id,
|
|
112
|
+
text: row.text,
|
|
113
|
+
from: existing.index + 1,
|
|
114
|
+
to: row.index + 1,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (row.sequelFlag === 1) {
|
|
119
|
+
sequelRequested.push({ id: row.id || null, text: row.text, index: row.index + 1 });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return compactObject({
|
|
123
|
+
id: row.id,
|
|
124
|
+
text: row.text,
|
|
125
|
+
theme: row.theme,
|
|
126
|
+
updateFlag: contentChanged ? 1 : 0,
|
|
127
|
+
important: row.important,
|
|
128
|
+
sequelFlag: row.sequelFlag === 1 ? 1 : undefined,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const proposedIds = new Set(proposed.map(row => row.id).filter(Boolean));
|
|
133
|
+
const deleted = current
|
|
134
|
+
.filter(row => row.id && !proposedIds.has(row.id))
|
|
135
|
+
.map(row => ({
|
|
136
|
+
id: row.id,
|
|
137
|
+
text: row.text,
|
|
138
|
+
index: row.index + 1,
|
|
139
|
+
}));
|
|
140
|
+
|
|
141
|
+
const diff = {
|
|
142
|
+
added,
|
|
143
|
+
updated,
|
|
144
|
+
deleted,
|
|
145
|
+
moved,
|
|
146
|
+
importanceChanged,
|
|
147
|
+
sequelRequested,
|
|
148
|
+
};
|
|
149
|
+
return {
|
|
150
|
+
payload,
|
|
151
|
+
diff,
|
|
152
|
+
hasChanges: Object.values(diff).some(items => items.length > 0),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function formatOutlineLines(cid, outlineDetail = {}) {
|
|
157
|
+
const rows = Array.isArray(outlineDetail?.outLine) ? outlineDetail.outLine : [];
|
|
158
|
+
const lines = [
|
|
159
|
+
`outline: cid=${cid}`,
|
|
160
|
+
`outline_status: ${outlineDetail?.outline_status ? 'ready' : 'not-ready'}`,
|
|
161
|
+
`chapters: ${rows.length}`,
|
|
162
|
+
];
|
|
163
|
+
rows.forEach((row, index) => {
|
|
164
|
+
const id = normalizeOptionalString(row?.id);
|
|
165
|
+
const important = normalizeImportant(row?.important, 2) === 1 ? '重点' : '普通';
|
|
166
|
+
lines.push(`${index + 1}. ${normalizeText(row?.text) || '(未命名章节)'} [${important}${id ? `, id=${id}` : ''}]`);
|
|
167
|
+
const themeLines = normalizeTheme(row?.theme)
|
|
168
|
+
.split(/\r?\n/)
|
|
169
|
+
.map(line => line.trim())
|
|
170
|
+
.filter(Boolean);
|
|
171
|
+
if (themeLines.length === 0) {
|
|
172
|
+
lines.push(' 主题: -');
|
|
173
|
+
} else {
|
|
174
|
+
lines.push(' 主题:');
|
|
175
|
+
themeLines.forEach(line => lines.push(` - ${line}`));
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
return lines;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function formatOutlineDiffLines(diff = {}) {
|
|
182
|
+
const added = diff.added || [];
|
|
183
|
+
const updated = diff.updated || [];
|
|
184
|
+
const deleted = diff.deleted || [];
|
|
185
|
+
const moved = diff.moved || [];
|
|
186
|
+
const importanceChanged = diff.importanceChanged || [];
|
|
187
|
+
const sequelRequested = diff.sequelRequested || [];
|
|
188
|
+
const lines = [
|
|
189
|
+
`changes: added=${added.length}, updated=${updated.length}, deleted=${deleted.length}, moved=${moved.length}, importance=${importanceChanged.length}, sequel=${sequelRequested.length}`,
|
|
190
|
+
];
|
|
191
|
+
added.forEach(item => lines.push(` + ${item.index}. ${item.text}`));
|
|
192
|
+
updated.forEach(item => lines.push(` ~ ${item.index}. ${item.text} (${item.fields.join(', ')})`));
|
|
193
|
+
deleted.forEach(item => lines.push(` - ${item.index}. ${item.text} [id=${item.id}]`));
|
|
194
|
+
moved.forEach(item => lines.push(` ⇅ ${item.text}: ${item.from} -> ${item.to}`));
|
|
195
|
+
importanceChanged.forEach(item => lines.push(` ! ${item.text}: important ${item.before} -> ${item.after}`));
|
|
196
|
+
sequelRequested.forEach(item => lines.push(` > ${item.index}. ${item.text}: request sequel`));
|
|
197
|
+
return lines;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function normalizeCurrentRow(row, index) {
|
|
201
|
+
return {
|
|
202
|
+
id: normalizeOptionalString(row?.id),
|
|
203
|
+
text: normalizeText(row?.text),
|
|
204
|
+
theme: normalizeTheme(row?.theme),
|
|
205
|
+
important: normalizeImportant(row?.important, 2),
|
|
206
|
+
index,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function collectChangedFields(existing, proposed) {
|
|
211
|
+
const fields = [];
|
|
212
|
+
if (stripChapterPrefix(existing.text) !== stripChapterPrefix(proposed.text)) {
|
|
213
|
+
fields.push('text');
|
|
214
|
+
}
|
|
215
|
+
if (existing.theme.trim() !== proposed.theme.trim()) {
|
|
216
|
+
fields.push('theme');
|
|
217
|
+
}
|
|
218
|
+
return fields;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function describeAddedRow(row) {
|
|
222
|
+
return {
|
|
223
|
+
id: null,
|
|
224
|
+
text: row.text,
|
|
225
|
+
index: row.index + 1,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function describeUpdatedRow(existing, row, fields) {
|
|
230
|
+
return {
|
|
231
|
+
id: row.id,
|
|
232
|
+
text: row.text,
|
|
233
|
+
index: row.index + 1,
|
|
234
|
+
fields,
|
|
235
|
+
before: {
|
|
236
|
+
text: existing.text,
|
|
237
|
+
theme: existing.theme,
|
|
238
|
+
},
|
|
239
|
+
after: {
|
|
240
|
+
text: row.text,
|
|
241
|
+
theme: row.theme,
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function normalizeText(value) {
|
|
247
|
+
return value === undefined || value === null ? '' : String(value).trim();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function normalizeTheme(value) {
|
|
251
|
+
if (Array.isArray(value)) {
|
|
252
|
+
return value.map(item => String(item).trim()).filter(Boolean).join('\n');
|
|
253
|
+
}
|
|
254
|
+
return value === undefined || value === null ? '' : String(value).trim();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function normalizeOptionalString(value) {
|
|
258
|
+
if (value === undefined || value === null) {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
const normalized = String(value).trim();
|
|
262
|
+
return normalized || undefined;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function normalizeImportant(value, fallback, index = null) {
|
|
266
|
+
if (value === undefined || value === null || value === '') {
|
|
267
|
+
return fallback;
|
|
268
|
+
}
|
|
269
|
+
const parsed = Number(value);
|
|
270
|
+
if (parsed !== 1 && parsed !== 2) {
|
|
271
|
+
const location = index === null ? '' : ` for chapter ${index + 1}`;
|
|
272
|
+
throw new Error(`Outline important${location} must be 1 (重点) or 2 (普通).`);
|
|
273
|
+
}
|
|
274
|
+
return parsed;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function normalizeSequelFlag(value, index) {
|
|
278
|
+
if (value === undefined || value === null || value === '') {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
const parsed = Number(value);
|
|
282
|
+
if (parsed !== 0 && parsed !== 1) {
|
|
283
|
+
throw new Error(`Outline sequelFlag for chapter ${index + 1} must be 0 or 1.`);
|
|
284
|
+
}
|
|
285
|
+
return parsed;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function stripChapterPrefix(value) {
|
|
289
|
+
let normalized = normalizeText(value).replace(/<[^>]+>/g, '').trim();
|
|
290
|
+
for (const pattern of CHAPTER_PREFIX_PATTERNS) {
|
|
291
|
+
normalized = normalized.replace(pattern, '').trim();
|
|
292
|
+
}
|
|
293
|
+
return normalized;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function compactObject(value) {
|
|
297
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
298
|
+
}
|