@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 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.2.tgz
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.2",
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
- await handleOutline(parsed, state);
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
+ }