@xqyz/xq-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +159 -0
- package/bin/xq-cli.js +3 -0
- package/package.json +24 -0
- package/src/cli.mjs +3349 -0
- package/src/fileTypeConfig.mjs +29 -0
- package/src/templateData.mjs +118 -0
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,3349 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import http from 'http';
|
|
5
|
+
import https from 'https';
|
|
6
|
+
import os from 'os';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import readline from 'readline/promises';
|
|
9
|
+
import { spawn } from 'child_process';
|
|
10
|
+
import { randomUUID } from 'crypto';
|
|
11
|
+
import axios from 'axios';
|
|
12
|
+
import FormData from 'form-data';
|
|
13
|
+
import { templateData } from './templateData.mjs';
|
|
14
|
+
import { AI_PURPOSE_MAP, detectFileType } from './fileTypeConfig.mjs';
|
|
15
|
+
|
|
16
|
+
const APP_DIR = path.join(os.homedir(), '.xq-opencli');
|
|
17
|
+
const CONFIG_FILE = path.join(APP_DIR, 'config.json');
|
|
18
|
+
const DEFAULT_PARSE_INTERVAL_MS = 10_000;
|
|
19
|
+
const DEFAULT_TASK_INTERVAL_MS = 10_000;
|
|
20
|
+
const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
21
|
+
const DEFAULT_SOURCE_FROM = 'xique';
|
|
22
|
+
const DEFAULT_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
23
|
+
const DEFAULT_EXPORT_TEMPLATE_INDEX = 1;
|
|
24
|
+
const DEFAULT_PAGE_SCOPE_CODE = 1;
|
|
25
|
+
const DEFAULT_BASE = '0';
|
|
26
|
+
const DEFAULT_THEME_STYLE = 4;
|
|
27
|
+
const DEFAULT_TABLE_QUANTITY = 2;
|
|
28
|
+
const DEFAULT_TABLE_COLOR = 'BLUE';
|
|
29
|
+
const DEFAULT_TABLE_STYLE = 1;
|
|
30
|
+
const DEFAULT_MERMAID_STYLE = 1;
|
|
31
|
+
const DEFAULT_PLAN_MODE = 0;
|
|
32
|
+
const DEFAULT_INFO_IMG = 0;
|
|
33
|
+
const DEFAULT_SCENE_IMG = 0;
|
|
34
|
+
const DEFAULT_ONLINE_IMG = 0;
|
|
35
|
+
const DEFAULT_MERMAID_IMG = 0;
|
|
36
|
+
const DEFAULT_KNOWLEDGE_IMG = 0;
|
|
37
|
+
const DEFAULT_AUTO_NUMBER = 1;
|
|
38
|
+
const DEFAULT_ADD_MARK = 0;
|
|
39
|
+
const CONTENT_VIEW_STEP = 101;
|
|
40
|
+
const DIRECTORY_PORTION_STATUS = {
|
|
41
|
+
OUTLINE_GENERATING: 0,
|
|
42
|
+
OUTLINE_COMPLETED: 1,
|
|
43
|
+
SEQUEL_GENERATING: 2,
|
|
44
|
+
SEQUEL_COMPLETED: 3,
|
|
45
|
+
OUTLINE_BASE_COMPLETED: 4,
|
|
46
|
+
OUTLINE_ADDITIONAL_GENERATING: 5,
|
|
47
|
+
};
|
|
48
|
+
const DEFAULT_RETRY_COUNT = 3;
|
|
49
|
+
const DIRECTORY_SEQUEL_RETRY_INTERVAL_MS = 60_000;
|
|
50
|
+
const GENERATE_UNFINISHED_RETRY_INTERVAL_MS = 60_000;
|
|
51
|
+
const RETRYABLE_NETWORK_CODES = new Set(['ECONNRESET', 'ECONNABORTED', 'ETIMEDOUT', 'EAI_AGAIN']);
|
|
52
|
+
const SOFT_CONTENT_TRIGGER_CODES = new Set([411, 429]);
|
|
53
|
+
const BROWSER_LOGIN_CALLBACK_PATH = '/callback';
|
|
54
|
+
const HTTP_AGENT = new http.Agent({ keepAlive: true });
|
|
55
|
+
const HTTPS_AGENT = new https.Agent({ keepAlive: true });
|
|
56
|
+
const AUTO_IGNORE_ISSUE_CONFIG = {
|
|
57
|
+
requirement_judgement: {
|
|
58
|
+
optionKey: 'ignoreShortRequirement',
|
|
59
|
+
hint: '--ignore-short-requirement',
|
|
60
|
+
buildPayload: uuid => ({
|
|
61
|
+
uuid,
|
|
62
|
+
requirement: '',
|
|
63
|
+
score: '',
|
|
64
|
+
type: 'requirement_short',
|
|
65
|
+
}),
|
|
66
|
+
},
|
|
67
|
+
billOfQuantities_judgement: {
|
|
68
|
+
optionKey: 'ignoreMissingBill',
|
|
69
|
+
hint: '--ignore-missing-bill',
|
|
70
|
+
buildPayload: uuid => ({
|
|
71
|
+
uuid,
|
|
72
|
+
requirement: '',
|
|
73
|
+
score: '',
|
|
74
|
+
type: 'billOfQuantities',
|
|
75
|
+
}),
|
|
76
|
+
},
|
|
77
|
+
score_judgement: {
|
|
78
|
+
optionKey: 'ignoreMissingScore',
|
|
79
|
+
hint: '--ignore-missing-score',
|
|
80
|
+
buildPayload: uuid => ({
|
|
81
|
+
uuid,
|
|
82
|
+
requirement: '',
|
|
83
|
+
score: '',
|
|
84
|
+
type: 'score',
|
|
85
|
+
}),
|
|
86
|
+
},
|
|
87
|
+
directory_judgement: {
|
|
88
|
+
optionKey: 'ignoreDirectoryBasis',
|
|
89
|
+
hint: '--ignore-directory-basis',
|
|
90
|
+
buildPayload: uuid => ({
|
|
91
|
+
uuid,
|
|
92
|
+
requirement: '',
|
|
93
|
+
score: '',
|
|
94
|
+
directory: '',
|
|
95
|
+
modify: 0,
|
|
96
|
+
type: 'directory',
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
const DEFAULT_THEME_STYLE_SETTING = {
|
|
101
|
+
tableBolderColorOfSkeleton: '#1276C1',
|
|
102
|
+
imgTitleColor: '#1276C1',
|
|
103
|
+
heading4Color: '#49A3E7',
|
|
104
|
+
heading5Color: '#DFF7FF',
|
|
105
|
+
contentTitleColor: '#EEFBFF',
|
|
106
|
+
contentColor: '#FFFFFF',
|
|
107
|
+
heading4TextColor: '#000000',
|
|
108
|
+
heading5TextColor: '#000000',
|
|
109
|
+
contentTitleTextColor: '#000000',
|
|
110
|
+
imgTitleTextColor: '#FFFFFF',
|
|
111
|
+
};
|
|
112
|
+
const DEFAULT_PAGE_MARGIN_SETTING = {
|
|
113
|
+
topMargin: 2.54,
|
|
114
|
+
bottomMargin: 2.54,
|
|
115
|
+
leftMargin: 3.18,
|
|
116
|
+
rightMargin: 3.18,
|
|
117
|
+
};
|
|
118
|
+
const OUTLINE_REFERENCE_OPTIONS = [
|
|
119
|
+
{
|
|
120
|
+
value: 0,
|
|
121
|
+
label: '融合评分与格式要求响应',
|
|
122
|
+
shortLabel: '融合响应',
|
|
123
|
+
outlineKey: 'planAndRatingOutline',
|
|
124
|
+
aliases: ['0', 'merge', 'merged', 'combined', 'all'],
|
|
125
|
+
recommended: true,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
value: 1,
|
|
129
|
+
label: '仅响应格式要求',
|
|
130
|
+
shortLabel: '格式要求',
|
|
131
|
+
outlineKey: 'planOutline',
|
|
132
|
+
aliases: ['1', 'format', 'plan'],
|
|
133
|
+
recommended: false,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
value: 2,
|
|
137
|
+
label: '仅响应评分要求',
|
|
138
|
+
shortLabel: '评分要求',
|
|
139
|
+
outlineKey: 'ratingOutline',
|
|
140
|
+
aliases: ['2', 'score', 'rating'],
|
|
141
|
+
recommended: false,
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
const PAGE_SCOPE_OPTIONS = [
|
|
145
|
+
{ value: -1, label: 'mini', description: '50-100 pages', aliases: ['-1', 'mini'] },
|
|
146
|
+
{ value: 0, label: 'extra-short', description: '100-200 pages', aliases: ['0', 'extra-short', 'xshort'] },
|
|
147
|
+
{ value: 1, label: 'short', description: '200-350 pages', aliases: ['1', 'short'] },
|
|
148
|
+
{ value: 2, label: 'medium', description: '350-500 pages', aliases: ['2', 'medium'] },
|
|
149
|
+
{ value: 3, label: 'medium-long', description: '500-800 pages', aliases: ['3', 'medium-long', 'mlong'] },
|
|
150
|
+
{ value: 4, label: 'long', description: '800-1200 pages', aliases: ['4', 'long'] },
|
|
151
|
+
{ value: 5, label: 'extra-long', description: '1200-1500 pages', aliases: ['5', 'extra-long', 'xlong'] },
|
|
152
|
+
];
|
|
153
|
+
const BASE_OPTIONS = [
|
|
154
|
+
{ value: '0', label: 'standard', description: 'standard writing', aliases: ['0', 'standard', 'default'] },
|
|
155
|
+
{ value: '1', label: 'expert', description: 'expert polish / public knowledge reference', aliases: ['1', 'expert', 'polish'] },
|
|
156
|
+
];
|
|
157
|
+
const THEME_STYLE_OPTIONS = [
|
|
158
|
+
{ value: 1, label: 'enhanced', description: 'enhanced image-rich layout', aliases: ['1', 'enhanced'] },
|
|
159
|
+
{ value: 2, label: 'table', description: 'table-first layout', aliases: ['2', 'table', 'table-first'] },
|
|
160
|
+
{ value: 3, label: 'rich', description: 'rich mixed media layout', aliases: ['3', 'rich'] },
|
|
161
|
+
{ value: 4, label: 'basic', description: 'basic illustrated layout', aliases: ['4', 'basic', 'base'] },
|
|
162
|
+
{ value: 5, label: 'text', description: 'text only', aliases: ['5', 'text', 'text-only'] },
|
|
163
|
+
];
|
|
164
|
+
const TABLE_QUANTITY_OPTIONS = [
|
|
165
|
+
{ value: 0, label: 'none', description: 'no tables', aliases: ['0', 'none'] },
|
|
166
|
+
{ value: 1, label: 'few', description: 'about one table every 4 pages', aliases: ['1', 'few', 'light'] },
|
|
167
|
+
{ value: 2, label: 'rich', description: 'about one table every 2 pages', aliases: ['2', 'rich', 'dense'] },
|
|
168
|
+
];
|
|
169
|
+
const TABLE_COLOR_OPTIONS = [
|
|
170
|
+
{ value: 'BLACK', label: 'black', aliases: ['black'] },
|
|
171
|
+
{ value: 'BLUE', label: 'blue', aliases: ['blue'] },
|
|
172
|
+
{ value: 'RED', label: 'red', aliases: ['red'] },
|
|
173
|
+
{ value: 'GREEN', label: 'green', aliases: ['green'] },
|
|
174
|
+
{ value: 'PURPLE', label: 'purple', aliases: ['purple'] },
|
|
175
|
+
{ value: 'CYAN', label: 'cyan', aliases: ['cyan'] },
|
|
176
|
+
{ value: 'ORANGE', label: 'orange', aliases: ['orange'] },
|
|
177
|
+
];
|
|
178
|
+
const TABLE_STYLE_OPTIONS = [
|
|
179
|
+
{ value: 1, label: 'plain', description: 'plain table style', aliases: ['1', 'plain', 'clean'] },
|
|
180
|
+
{ value: 2, label: 'header', description: 'strong header style', aliases: ['2', 'header', 'header-strong'] },
|
|
181
|
+
{ value: 3, label: 'zebra', description: 'zebra stripes', aliases: ['3', 'zebra', 'striped'] },
|
|
182
|
+
{ value: 4, label: 'filled', description: 'filled cells with white borders', aliases: ['4', 'filled', 'full-fill'] },
|
|
183
|
+
];
|
|
184
|
+
const MERMAID_STYLE_OPTIONS = [
|
|
185
|
+
{ value: 1, label: 'gray', aliases: ['1', 'gray', 'grey'] },
|
|
186
|
+
{ value: 2, label: 'purple', aliases: ['2', 'purple'] },
|
|
187
|
+
{ value: 3, label: 'blue', aliases: ['3', 'blue'] },
|
|
188
|
+
{ value: 4, label: 'green', aliases: ['4', 'green'] },
|
|
189
|
+
{ value: 5, label: 'orange', aliases: ['5', 'orange'] },
|
|
190
|
+
{ value: 6, label: 'red', aliases: ['6', 'red'] },
|
|
191
|
+
];
|
|
192
|
+
const PLAN_MODE_OPTIONS = [
|
|
193
|
+
{ value: 0, label: 'quick', description: 'quick mode', aliases: ['0', 'quick'] },
|
|
194
|
+
{ value: 1, label: 'planning', description: 'planning mode', aliases: ['1', 'planning', 'plan'] },
|
|
195
|
+
];
|
|
196
|
+
const LAYOUT_OPTIONS = [
|
|
197
|
+
{ value: 0, label: 'normal', description: 'regular layout', aliases: ['0', 'normal', 'default'] },
|
|
198
|
+
{ value: 1, label: 'table', description: 'full table layout', aliases: ['1', 'table'] },
|
|
199
|
+
{ value: 2, label: 'enhanced', description: 'enhanced / refined mixed layout', aliases: ['2', 'enhanced', 'refined'] },
|
|
200
|
+
];
|
|
201
|
+
const BINARY_OPTIONS = [
|
|
202
|
+
{ value: 0, label: 'off', aliases: ['0', 'false', 'no', 'off'] },
|
|
203
|
+
{ value: 1, label: 'on', aliases: ['1', 'true', 'yes', 'on'] },
|
|
204
|
+
];
|
|
205
|
+
const THEME_STYLE_SETTING_FIELDS = [
|
|
206
|
+
{ key: 'tableBolderColorOfSkeleton', optionKeys: ['tableBolderColorOfSkeleton', 'skeletonColor'], description: 'table skeleton accent color' },
|
|
207
|
+
{ key: 'imgTitleColor', optionKeys: ['imgTitleColor'], description: 'image title background color' },
|
|
208
|
+
{ key: 'heading4Color', optionKeys: ['heading4Color'], description: 'heading level 4 background color' },
|
|
209
|
+
{ key: 'heading5Color', optionKeys: ['heading5Color'], description: 'heading level 5 background color' },
|
|
210
|
+
{ key: 'contentTitleColor', optionKeys: ['contentTitleColor'], description: 'content title background color' },
|
|
211
|
+
{ key: 'contentColor', optionKeys: ['contentColor'], description: 'content background color' },
|
|
212
|
+
{ key: 'heading4TextColor', optionKeys: ['heading4TextColor'], description: 'heading level 4 text color' },
|
|
213
|
+
{ key: 'heading5TextColor', optionKeys: ['heading5TextColor'], description: 'heading level 5 text color' },
|
|
214
|
+
{ key: 'contentTitleTextColor', optionKeys: ['contentTitleTextColor'], description: 'content title text color' },
|
|
215
|
+
{ key: 'imgTitleTextColor', optionKeys: ['imgTitleTextColor'], description: 'image title text color' },
|
|
216
|
+
];
|
|
217
|
+
const PAGE_MARGIN_FIELDS = [
|
|
218
|
+
{ key: 'topMargin', optionKeys: ['topMargin', 'marginTop'] },
|
|
219
|
+
{ key: 'bottomMargin', optionKeys: ['bottomMargin', 'marginBottom'] },
|
|
220
|
+
{ key: 'leftMargin', optionKeys: ['leftMargin', 'marginLeft'] },
|
|
221
|
+
{ key: 'rightMargin', optionKeys: ['rightMargin', 'marginRight'] },
|
|
222
|
+
];
|
|
223
|
+
const EXPORT_TEMPLATE_OPTIONS = templateData.map((template, index) => ({
|
|
224
|
+
value: index + 1,
|
|
225
|
+
label: `template-${index + 1}`,
|
|
226
|
+
description: template.custom ? 'custom baseline template' : 'built-in template',
|
|
227
|
+
}));
|
|
228
|
+
const EPC_ENGINEER_TYPE_ALIAS_MAP = {
|
|
229
|
+
all: '\u975EEPC',
|
|
230
|
+
full: '\u975EEPC',
|
|
231
|
+
construction: '\u65BD\u5DE5',
|
|
232
|
+
build: '\u65BD\u5DE5',
|
|
233
|
+
design: '\u8BBE\u8BA1',
|
|
234
|
+
'not-epc': '\u975EEPC2',
|
|
235
|
+
'non-epc': '\u975EEPC2',
|
|
236
|
+
};
|
|
237
|
+
const PAGE_SCOPE_PROMPT_LABELS = {
|
|
238
|
+
[-1]: '\u6781\u77ed\u7BC7 (50-100\u9875)',
|
|
239
|
+
0: '\u8D85\u77ED\u7BC7 (100-200\u9875)',
|
|
240
|
+
1: '\u77ED\u7BC7 (200-350\u9875)',
|
|
241
|
+
2: '\u4E2D\u7BC7 (350-500\u9875)',
|
|
242
|
+
3: '\u4E2D\u957F\u7BC7 (500-800\u9875)',
|
|
243
|
+
4: '\u957F\u7BC7 (800-1200\u9875)',
|
|
244
|
+
5: '\u8D85\u957F\u7BC7 (1200-1500\u9875)',
|
|
245
|
+
};
|
|
246
|
+
const BASE_PROMPT_LABELS = {
|
|
247
|
+
0: '\u6807\u51C6\u5199\u4F5C',
|
|
248
|
+
1: '\u4E13\u5BB6\u667A\u7B14',
|
|
249
|
+
};
|
|
250
|
+
const THEME_STYLE_PROMPT_LABELS = {
|
|
251
|
+
1: '\u589E\u5F3A\u56FE\u6587',
|
|
252
|
+
2: '\u5168\u8868\u683C\u5F0F',
|
|
253
|
+
3: '\u4E30\u5BCC\u56FE\u6587',
|
|
254
|
+
4: '\u57FA\u7840\u914D\u56FE',
|
|
255
|
+
5: '\u7EAF\u6587\u5B57',
|
|
256
|
+
};
|
|
257
|
+
const TABLE_QUANTITY_PROMPT_LABELS = {
|
|
258
|
+
0: '\u65E0\u8868\u683C',
|
|
259
|
+
1: '\u5C11\u91CF\u8868\u683C',
|
|
260
|
+
2: '\u4E30\u5BCC\u8868\u683C',
|
|
261
|
+
};
|
|
262
|
+
const TABLE_COLOR_PROMPT_LABELS = {
|
|
263
|
+
BLACK: '\u9ED1\u8272',
|
|
264
|
+
BLUE: '\u84DD\u8272',
|
|
265
|
+
RED: '\u7EA2\u8272',
|
|
266
|
+
GREEN: '\u7EFF\u8272',
|
|
267
|
+
PURPLE: '\u7D2B\u8272',
|
|
268
|
+
CYAN: '\u9752\u8272',
|
|
269
|
+
ORANGE: '\u6A59\u8272',
|
|
270
|
+
};
|
|
271
|
+
const TABLE_STYLE_PROMPT_LABELS = {
|
|
272
|
+
1: '\u7EAF\u51C0\u7248',
|
|
273
|
+
2: '\u8868\u5934\u5F3A\u5316',
|
|
274
|
+
3: '\u6591\u9A6C\u6761\u7EB9',
|
|
275
|
+
4: '\u5168\u5E95\u8272\u767D\u6846',
|
|
276
|
+
};
|
|
277
|
+
const MERMAID_STYLE_PROMPT_LABELS = {
|
|
278
|
+
1: '\u7070\u8272',
|
|
279
|
+
2: '\u7D2B\u8272',
|
|
280
|
+
3: '\u84DD\u8272',
|
|
281
|
+
4: '\u7EFF\u8272',
|
|
282
|
+
5: '\u6A59\u8272',
|
|
283
|
+
6: '\u7EA2\u8272',
|
|
284
|
+
};
|
|
285
|
+
const PLAN_MODE_PROMPT_LABELS = {
|
|
286
|
+
0: '\u5FEB\u901F\u6A21\u5F0F',
|
|
287
|
+
1: '\u89C4\u5212\u6A21\u5F0F',
|
|
288
|
+
};
|
|
289
|
+
const LAYOUT_PROMPT_LABELS = {
|
|
290
|
+
0: '\u5E38\u89C4\u6392\u7248',
|
|
291
|
+
1: '\u5168\u8868\u683C\u5F0F',
|
|
292
|
+
2: '\u7CBE\u6392/\u589E\u5F3A\u56FE\u6587',
|
|
293
|
+
};
|
|
294
|
+
const IMAGE_TOGGLE_PROMPT_OPTIONS = [
|
|
295
|
+
{ key: 'knowledgeImg', label: '\u6211\u7684\u56FE\u5E93', defaultValue: DEFAULT_KNOWLEDGE_IMG },
|
|
296
|
+
{ key: 'sceneImg', label: '\u5B9E\u62CD\u98CE\u683C\u56FE', defaultValue: DEFAULT_SCENE_IMG },
|
|
297
|
+
{ key: 'onlineImg', label: '\u8054\u7F51\u641C\u56FE', defaultValue: DEFAULT_ONLINE_IMG },
|
|
298
|
+
{ key: 'infoImg', label: 'PPT\u5F0F\u63D2\u56FE', defaultValue: DEFAULT_INFO_IMG },
|
|
299
|
+
{ key: 'mermaidImg', label: '\u6D41\u7A0B\u56FE', defaultValue: DEFAULT_MERMAID_IMG },
|
|
300
|
+
];
|
|
301
|
+
const EPC_ENGINEER_PROMPT_OPTIONS = [
|
|
302
|
+
{ value: '', label: '\u4FDD\u6301\u5F53\u524D/\u4E0D\u8BBE\u7F6E' },
|
|
303
|
+
{ value: '\u975EEPC', label: '\u5168\u90E8\u7F16\u5199' },
|
|
304
|
+
{ value: '\u65BD\u5DE5', label: '\u53EA\u5199\u65BD\u5DE5' },
|
|
305
|
+
{ value: '\u8BBE\u8BA1', label: '\u53EA\u5199\u8BBE\u8BA1' },
|
|
306
|
+
{ value: '\u975EEPC2', label: '\u8FD9\u4E0D\u662FEPC\u9879\u76EE' },
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
async function main() {
|
|
310
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
311
|
+
const command = parsed._[0] || 'help';
|
|
312
|
+
const state = readState();
|
|
313
|
+
|
|
314
|
+
switch (command) {
|
|
315
|
+
case 'help':
|
|
316
|
+
case '--help':
|
|
317
|
+
case '-h':
|
|
318
|
+
printHelp();
|
|
319
|
+
return;
|
|
320
|
+
case 'login':
|
|
321
|
+
await handleLogin(parsed, state);
|
|
322
|
+
return;
|
|
323
|
+
case 'init':
|
|
324
|
+
await handleInit(parsed, state);
|
|
325
|
+
return;
|
|
326
|
+
case 'parse-status':
|
|
327
|
+
await handleParseStatus(parsed, state);
|
|
328
|
+
return;
|
|
329
|
+
case 'outline':
|
|
330
|
+
await handleOutline(parsed, state);
|
|
331
|
+
return;
|
|
332
|
+
case 'write':
|
|
333
|
+
await handleWrite(parsed, state);
|
|
334
|
+
return;
|
|
335
|
+
case 'status':
|
|
336
|
+
await handleStatus(parsed, state);
|
|
337
|
+
return;
|
|
338
|
+
case 'export':
|
|
339
|
+
await handleExport(parsed, state);
|
|
340
|
+
return;
|
|
341
|
+
case 'choices':
|
|
342
|
+
handleChoices(parsed);
|
|
343
|
+
return;
|
|
344
|
+
case 'wizard':
|
|
345
|
+
await handleWizard(parsed, state);
|
|
346
|
+
return;
|
|
347
|
+
default:
|
|
348
|
+
throw new Error(`Unsupported command: ${command}. Run "xq-cli help" to see available commands.`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function printHelp() {
|
|
353
|
+
console.log(`
|
|
354
|
+
xq-cli: xique brand task CLI
|
|
355
|
+
|
|
356
|
+
Commands
|
|
357
|
+
login Save xique account token locally
|
|
358
|
+
init Upload files and initialize a task
|
|
359
|
+
parse-status Query or wait for parse completion
|
|
360
|
+
outline Pre-set task params and trigger outline generation
|
|
361
|
+
write Charge if needed, enter content phase, and trigger content generation
|
|
362
|
+
status Query task status
|
|
363
|
+
choices Print selectable outline/export config values
|
|
364
|
+
wizard Run init -> outline -> write -> export with interactive menus
|
|
365
|
+
export Download the generated docx
|
|
366
|
+
|
|
367
|
+
Examples
|
|
368
|
+
xq-cli login --base-url http://localhost:9021/api
|
|
369
|
+
xq-cli login --base-url http://localhost:9021/api --name demo --password secret
|
|
370
|
+
xq-cli login --base-url http://localhost:9021/api --browser
|
|
371
|
+
xq-cli init --file D:\\bids\\tender.docx --wait
|
|
372
|
+
xq-cli parse-status --cid <cid> --uuid <uuid> --wait
|
|
373
|
+
xq-cli choices
|
|
374
|
+
xq-cli choices outline --json
|
|
375
|
+
xq-cli wizard
|
|
376
|
+
xq-cli wizard --file D:\\bids\\tender.docx
|
|
377
|
+
xq-cli outline --cid <cid> --wait
|
|
378
|
+
xq-cli outline --cid <cid> --interactive --wait
|
|
379
|
+
xq-cli outline --cid <cid> --uuid <uuid> --wait
|
|
380
|
+
xq-cli outline --cid <cid> --wait --ignore-short-requirement
|
|
381
|
+
xq-cli outline --cid <cid> --wait --ignore-missing-bill
|
|
382
|
+
xq-cli outline --cid <cid> --wait --reference-type 0
|
|
383
|
+
xq-cli outline --cid <cid> --page-scope 3 --base expert --theme-style 4 --table-quantity rich --plan-mode quick --wait
|
|
384
|
+
xq-cli write --cid <cid> --type 1 --wait
|
|
385
|
+
xq-cli status --cid <cid> --content
|
|
386
|
+
xq-cli export --cid <cid> --interactive --out D:\\output\\
|
|
387
|
+
xq-cli export --cid <cid> --template 2 --layout enhanced --table-color green --table-style zebra --out D:\\output\\
|
|
388
|
+
xq-cli export --cid <cid> --out D:\\output\\
|
|
389
|
+
|
|
390
|
+
Common options
|
|
391
|
+
--json Print machine-readable JSON output
|
|
392
|
+
--base-url <url> Override the saved base URL
|
|
393
|
+
--timeout-sec <n> Poll timeout in seconds, default 600
|
|
394
|
+
--interval-sec <n> Poll interval in seconds, default 10
|
|
395
|
+
--reference-type <0|1|2> Outline reference choice: 0=merge, 1=format only, 2=score only
|
|
396
|
+
--browser Force browser authorization login; browser is the default when credentials are not provided
|
|
397
|
+
--interactive Open prompt menus for the current command
|
|
398
|
+
--ignore-short-requirement Follow the frontend ignore/continue path for requirement_judgement=0
|
|
399
|
+
--ignore-missing-bill Follow the frontend ignore/continue path for billOfQuantities_judgement=0
|
|
400
|
+
|
|
401
|
+
Outline config
|
|
402
|
+
--page-scope <code> Full-length preset. See "xq-cli choices outline"
|
|
403
|
+
--page-scope-code <code> Alias of --page-scope
|
|
404
|
+
--base <0|1|standard|expert>
|
|
405
|
+
--theme-style <1|2|3|4|5|enhanced|table|rich|basic|text>
|
|
406
|
+
--table-quantity <0|1|2|none|few|rich>
|
|
407
|
+
--table-color <black|blue|red|green|purple|cyan|orange>
|
|
408
|
+
--table-style <1|2|3|4|plain|header|zebra|filled>
|
|
409
|
+
--info-img <0|1> PPT-style infographic toggle
|
|
410
|
+
--scene-img <0|1> real-photo style image toggle
|
|
411
|
+
--online-img <0|1> online image search toggle
|
|
412
|
+
--mermaid-img <0|1> flowchart toggle
|
|
413
|
+
--knowledge-img <0|1> knowledge-base image toggle
|
|
414
|
+
--mermaid-style <1|2|3|4|5|6|gray|purple|blue|green|orange|red>
|
|
415
|
+
--plan-mode <0|1|quick|planning>
|
|
416
|
+
--multi-bid-id <id> override the selected bid package when needed
|
|
417
|
+
--multi-bid-type <text> persist the selected bid package type
|
|
418
|
+
--epc-engineer-type <text> accepts raw backend value or aliases: all, construction, design, not-epc
|
|
419
|
+
|
|
420
|
+
Export style
|
|
421
|
+
--template <1-${templateData.length}> Same template order as the frontend download drawer
|
|
422
|
+
--style-json <file> Advanced export style JSON with startIndex/directorySettings/contentSettings
|
|
423
|
+
--start-index <n> Override the template heading numbering start index
|
|
424
|
+
--auto-number <0|1> Toggle Word auto numbering
|
|
425
|
+
--add-mark <0|1> Add suggest-modify marks
|
|
426
|
+
--layout <0|1|2|normal|table|enhanced>
|
|
427
|
+
--top-margin <cm> Page top margin
|
|
428
|
+
--bottom-margin <cm> Page bottom margin
|
|
429
|
+
--left-margin <cm> Page left margin
|
|
430
|
+
--right-margin <cm> Page right margin
|
|
431
|
+
--skeleton-color <hex> Alias of --table-bolder-color-of-skeleton
|
|
432
|
+
--img-title-color <hex>
|
|
433
|
+
--heading4-color <hex>
|
|
434
|
+
--heading5-color <hex>
|
|
435
|
+
--content-title-color <hex>
|
|
436
|
+
--content-color <hex>
|
|
437
|
+
--heading4-text-color <hex>
|
|
438
|
+
--heading5-text-color <hex>
|
|
439
|
+
--content-title-text-color <hex>
|
|
440
|
+
--img-title-text-color <hex>
|
|
441
|
+
note: write follows the frontend content flow: order/charge -> c/viewStep(101) -> task/autoGenerateContent
|
|
442
|
+
`.trim());
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function handleLogin(args, state) {
|
|
446
|
+
const baseUrl = normalizeBaseUrl(stringOption(args, 'baseUrl', state.baseUrl));
|
|
447
|
+
if (!baseUrl) {
|
|
448
|
+
throw new Error('Missing base URL. Pass --base-url <url>.');
|
|
449
|
+
}
|
|
450
|
+
const sourceFrom = stringOption(args, 'sourceFrom', DEFAULT_SOURCE_FROM);
|
|
451
|
+
const loginMode = resolveLoginMode(args);
|
|
452
|
+
const result = loginMode === 'browser'
|
|
453
|
+
? await loginWithBrowser(baseUrl, sourceFrom, args)
|
|
454
|
+
: await loginWithPassword(baseUrl, sourceFrom, args);
|
|
455
|
+
|
|
456
|
+
saveLoginState(state, {
|
|
457
|
+
baseUrl,
|
|
458
|
+
name: stringOption(args, 'name', result.data?.user?.username || ''),
|
|
459
|
+
sourceFrom,
|
|
460
|
+
result,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
outputResult(args, {
|
|
464
|
+
command: 'login',
|
|
465
|
+
mode: loginMode,
|
|
466
|
+
baseUrl,
|
|
467
|
+
username: state.user.username,
|
|
468
|
+
tokenSaved: true,
|
|
469
|
+
}, [
|
|
470
|
+
`login success: ${state.user.username}`,
|
|
471
|
+
`mode: ${loginMode}`,
|
|
472
|
+
`baseUrl: ${baseUrl}`,
|
|
473
|
+
`config: ${CONFIG_FILE}`,
|
|
474
|
+
]);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function resolveLoginMode(args) {
|
|
478
|
+
if (booleanOption(args, 'browser')) {
|
|
479
|
+
return 'browser';
|
|
480
|
+
}
|
|
481
|
+
const hasName = stringOption(args, 'name');
|
|
482
|
+
const hasPassword = stringOption(args, 'password');
|
|
483
|
+
return hasName || hasPassword ? 'password' : 'browser';
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function loginWithPassword(baseUrl, sourceFrom, args) {
|
|
487
|
+
const name = requiredOption(args, 'name');
|
|
488
|
+
const password = requiredOption(args, 'password');
|
|
489
|
+
const client = createClient(baseUrl);
|
|
490
|
+
return postJson(client, '/user/login', {
|
|
491
|
+
name,
|
|
492
|
+
password,
|
|
493
|
+
sourceFrom,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function loginWithBrowser(baseUrl, sourceFrom, args) {
|
|
498
|
+
const client = createClient(baseUrl);
|
|
499
|
+
const loginState = randomUUID();
|
|
500
|
+
const timeoutMs = secondsToMs(numberOption(args, 'timeoutSec', DEFAULT_LOGIN_TIMEOUT_MS / 1000));
|
|
501
|
+
const callbackServer = await startBrowserLoginServer();
|
|
502
|
+
const authorizeUrl = buildBrowserAuthorizeUrl(baseUrl, callbackServer.redirectUri, loginState, sourceFrom);
|
|
503
|
+
|
|
504
|
+
writeNotice(args, `browser login url: ${authorizeUrl}`);
|
|
505
|
+
if (openBrowser(authorizeUrl)) {
|
|
506
|
+
writeNotice(args, 'browser login: waiting for authorization callback');
|
|
507
|
+
} else {
|
|
508
|
+
writeNotice(args, 'browser login: automatic open failed, paste the URL above into a browser on this machine');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
const callback = await withTimeout(
|
|
513
|
+
callbackServer.result,
|
|
514
|
+
timeoutMs,
|
|
515
|
+
`Browser login timed out after ${Math.round(timeoutMs / 1000)} seconds.`,
|
|
516
|
+
);
|
|
517
|
+
if (callback.state !== loginState) {
|
|
518
|
+
throw new Error('Browser login state validation failed.');
|
|
519
|
+
}
|
|
520
|
+
return await postJson(client, '/user/cli/token', {
|
|
521
|
+
code: callback.code,
|
|
522
|
+
state: callback.state,
|
|
523
|
+
});
|
|
524
|
+
} finally {
|
|
525
|
+
await callbackServer.close();
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function writeNotice(args, message) {
|
|
530
|
+
if (booleanOption(args, 'json')) {
|
|
531
|
+
console.error(message);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
console.log(message);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async function handleInit(args, state) {
|
|
538
|
+
const client = createAuthorizedClient(args, state);
|
|
539
|
+
const files = resolveFileArgs(args);
|
|
540
|
+
if (files.length === 0) {
|
|
541
|
+
throw new Error('At least one file is required. Use --file <path>.');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const fileMeta = buildFileMetadata(files, args);
|
|
545
|
+
const annexIds = splitCsvOption(args.annexIds);
|
|
546
|
+
const knowledgeProductIds = splitCsvOption(args.knowledgeProductIds);
|
|
547
|
+
const formData = new FormData();
|
|
548
|
+
|
|
549
|
+
for (const filePath of files) {
|
|
550
|
+
ensureFileExists(filePath);
|
|
551
|
+
formData.append('files', fs.createReadStream(filePath), path.basename(filePath));
|
|
552
|
+
}
|
|
553
|
+
formData.append('sourceFrom', stringOption(args, 'sourceFrom', DEFAULT_SOURCE_FROM));
|
|
554
|
+
formData.append('types', fileMeta.types.join(','));
|
|
555
|
+
formData.append('purposes', fileMeta.purposesPayload);
|
|
556
|
+
appendOptionalField(formData, 'useframework', args.useframework);
|
|
557
|
+
appendOptionalField(formData, 'annexIds', annexIds.join(','));
|
|
558
|
+
appendOptionalField(formData, 'knowledgeProductIds', knowledgeProductIds.join(','));
|
|
559
|
+
appendOptionalField(formData, 'originType', args.originType);
|
|
560
|
+
|
|
561
|
+
const result = await postForm(client, '/task/init', formData);
|
|
562
|
+
const cid = result.data.cid;
|
|
563
|
+
const snapshot = pickTaskSnapshot({
|
|
564
|
+
...result.data,
|
|
565
|
+
cid,
|
|
566
|
+
sourceFrom: stringOption(args, 'sourceFrom', DEFAULT_SOURCE_FROM),
|
|
567
|
+
annexIds,
|
|
568
|
+
knowledgeProductIds,
|
|
569
|
+
localFiles: files,
|
|
570
|
+
fileTypes: fileMeta.types,
|
|
571
|
+
filePurposes: fileMeta.purposes,
|
|
572
|
+
originType: numberOption(args, 'originType'),
|
|
573
|
+
});
|
|
574
|
+
saveTaskSnapshot(state, cid, snapshot);
|
|
575
|
+
|
|
576
|
+
let finalParse = null;
|
|
577
|
+
if (booleanOption(args, 'wait')) {
|
|
578
|
+
finalParse = await pollParseUntilReady(client, state, cid, snapshot.uuid, args);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const resultPayload = {
|
|
582
|
+
command: 'init',
|
|
583
|
+
cid,
|
|
584
|
+
uuid: snapshot.uuid,
|
|
585
|
+
title: snapshot.projectName || snapshot.title,
|
|
586
|
+
files,
|
|
587
|
+
fileTypes: fileMeta.types,
|
|
588
|
+
parseResult: finalParse,
|
|
589
|
+
};
|
|
590
|
+
outputResult(args, resultPayload, [
|
|
591
|
+
`init success: cid=${cid}`,
|
|
592
|
+
`uuid: ${snapshot.uuid || '-'}`,
|
|
593
|
+
`title: ${snapshot.projectName || snapshot.title || '-'}`,
|
|
594
|
+
`files: ${files.map(item => path.basename(item)).join(', ')}`,
|
|
595
|
+
booleanOption(args, 'wait') && finalParse
|
|
596
|
+
? `parse ready: success=${finalParse.success}`
|
|
597
|
+
: 'parse polling: run parse-status --wait to continue',
|
|
598
|
+
].filter(Boolean));
|
|
599
|
+
return resultPayload;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async function handleParseStatus(args, state) {
|
|
603
|
+
const client = createAuthorizedClient(args, state);
|
|
604
|
+
const cid = resolveCid(args, state);
|
|
605
|
+
const snapshot = await hydrateTaskSnapshot(client, state, cid);
|
|
606
|
+
const uuid = stringOption(args, 'uuid', snapshot.uuid);
|
|
607
|
+
if (!uuid) {
|
|
608
|
+
throw new Error(`No uuid found for task ${cid}. Pass --uuid explicitly or run init first.`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const result = booleanOption(args, 'wait')
|
|
612
|
+
? await pollParseUntilReady(client, state, cid, uuid, args)
|
|
613
|
+
: await fetchParseStatus(client, state, cid, uuid);
|
|
614
|
+
|
|
615
|
+
outputResult(args, {
|
|
616
|
+
command: 'parse-status',
|
|
617
|
+
cid,
|
|
618
|
+
uuid,
|
|
619
|
+
...result,
|
|
620
|
+
}, [
|
|
621
|
+
`parse status: cid=${cid}`,
|
|
622
|
+
`uuid: ${uuid}`,
|
|
623
|
+
`success: ${result.success}`,
|
|
624
|
+
]);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function handleChoices(args) {
|
|
628
|
+
const scope = normalizeChoicesScope(args._[1] || 'all');
|
|
629
|
+
const payload = buildChoicesPayload(scope);
|
|
630
|
+
outputResult(args, payload, renderChoicesLines(payload));
|
|
631
|
+
return payload;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async function handleWizard(args, state) {
|
|
635
|
+
const wizardArgs = await collectWizardArgs(args, state);
|
|
636
|
+
writeNotice(args, `wizard config: ${buildWizardSummary(wizardArgs)}`);
|
|
637
|
+
|
|
638
|
+
const initResult = await handleInit({
|
|
639
|
+
...wizardArgs,
|
|
640
|
+
_: ['init'],
|
|
641
|
+
wait: true,
|
|
642
|
+
interactive: false,
|
|
643
|
+
}, state);
|
|
644
|
+
const cid = initResult.cid;
|
|
645
|
+
|
|
646
|
+
const outlineResult = await handleOutline({
|
|
647
|
+
...wizardArgs,
|
|
648
|
+
_: ['outline'],
|
|
649
|
+
cid,
|
|
650
|
+
uuid: initResult.uuid,
|
|
651
|
+
wait: true,
|
|
652
|
+
interactive: false,
|
|
653
|
+
}, state);
|
|
654
|
+
|
|
655
|
+
const writeResult = await handleWrite({
|
|
656
|
+
...wizardArgs,
|
|
657
|
+
_: ['write'],
|
|
658
|
+
cid,
|
|
659
|
+
wait: true,
|
|
660
|
+
interactive: false,
|
|
661
|
+
}, state);
|
|
662
|
+
|
|
663
|
+
const exportResult = await handleExport({
|
|
664
|
+
...wizardArgs,
|
|
665
|
+
_: ['export'],
|
|
666
|
+
cid,
|
|
667
|
+
interactive: false,
|
|
668
|
+
}, state);
|
|
669
|
+
|
|
670
|
+
const resultPayload = {
|
|
671
|
+
command: 'wizard',
|
|
672
|
+
cid,
|
|
673
|
+
initResult,
|
|
674
|
+
outlineResult,
|
|
675
|
+
writeResult,
|
|
676
|
+
exportResult,
|
|
677
|
+
};
|
|
678
|
+
outputResult(args, resultPayload, [
|
|
679
|
+
`wizard success: cid=${cid}`,
|
|
680
|
+
`outline: ${outlineResult?.triggerMode || '-'}`,
|
|
681
|
+
`content: ${writeResult?.waitResult?.task_status ?? '-'}`,
|
|
682
|
+
`savedPath: ${exportResult?.savedPath || '-'}`,
|
|
683
|
+
]);
|
|
684
|
+
return resultPayload;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
async function handleOutline(args, state) {
|
|
688
|
+
validateOutlineReferenceTypeOption(args);
|
|
689
|
+
const client = createAuthorizedClient(args, state);
|
|
690
|
+
const cid = resolveCid(args, state);
|
|
691
|
+
const snapshot = await hydrateTaskSnapshot(client, state, cid);
|
|
692
|
+
const outlineSnapshot = {
|
|
693
|
+
...snapshot,
|
|
694
|
+
uuid: stringOption(args, 'uuid', snapshot.uuid),
|
|
695
|
+
};
|
|
696
|
+
const effectiveArgs = booleanOption(args, 'interactive')
|
|
697
|
+
? await collectOutlineInteractiveArgs(args, outlineSnapshot)
|
|
698
|
+
: args;
|
|
699
|
+
const params = buildOutlineParams(outlineSnapshot, effectiveArgs);
|
|
700
|
+
|
|
701
|
+
const triggerMode = await triggerOutlineGeneration(client, outlineSnapshot, params);
|
|
702
|
+
saveTaskSnapshot(state, cid, {
|
|
703
|
+
...outlineSnapshot,
|
|
704
|
+
...pickTaskSnapshot(params),
|
|
705
|
+
triggerMode,
|
|
706
|
+
outlineTriggeredAt: new Date().toISOString(),
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
let waitResult = null;
|
|
710
|
+
if (booleanOption(effectiveArgs, 'wait')) {
|
|
711
|
+
waitResult = await waitForOutlineReady(client, state, cid, outlineSnapshot.uuid, effectiveArgs);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const summaryLines = [
|
|
715
|
+
`outline triggered: cid=${cid}`,
|
|
716
|
+
`pageScopeCode: ${params.pageScopeCode}`,
|
|
717
|
+
`base: ${params.base}`,
|
|
718
|
+
`config: ${buildOutlineConfigSummary(params)}`,
|
|
719
|
+
`triggerMode: ${triggerMode}`,
|
|
720
|
+
];
|
|
721
|
+
|
|
722
|
+
if (waitResult?.phase === 'ready') {
|
|
723
|
+
summaryLines.push(`outline ready: rootChapters=${countOutlineRoots(waitResult)}`);
|
|
724
|
+
} else if (waitResult?.phase === 'supplement_required') {
|
|
725
|
+
summaryLines.push(`outline blocked: supplement required (${waitResult.issues.join(', ')})`);
|
|
726
|
+
for (const hint of collectIssueHints(waitResult.issues, args)) {
|
|
727
|
+
summaryLines.push(hint);
|
|
728
|
+
}
|
|
729
|
+
} else if (waitResult?.phase === 'reference_selection_required') {
|
|
730
|
+
summaryLines.push('outline blocked: reference selection required');
|
|
731
|
+
summaryLines.push('hint: rerun with --wait in an interactive terminal, or pass --reference-type <0|1|2>.');
|
|
732
|
+
} else if (waitResult?.phase === 'failed') {
|
|
733
|
+
summaryLines.push(`outline failed: task_status=${waitResult.taskDetail?.task_status ?? '-'}`);
|
|
734
|
+
} else {
|
|
735
|
+
summaryLines.push('outline generation started');
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (triggerMode === 'multiBiddingChoice' && !booleanOption(args, 'wait') && args.referenceType !== undefined) {
|
|
739
|
+
summaryLines.push('hint: --reference-type is applied during --wait polling; rerun with --wait to auto-submit the choice.');
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const resultPayload = {
|
|
743
|
+
command: 'outline',
|
|
744
|
+
cid,
|
|
745
|
+
pageScopeCode: params.pageScopeCode,
|
|
746
|
+
base: params.base,
|
|
747
|
+
config: pickOutlineConfig(params),
|
|
748
|
+
triggerMode,
|
|
749
|
+
waitResult,
|
|
750
|
+
};
|
|
751
|
+
outputResult(args, resultPayload, [
|
|
752
|
+
...summaryLines,
|
|
753
|
+
]);
|
|
754
|
+
return resultPayload;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
async function handleWrite(args, state) {
|
|
758
|
+
const client = createAuthorizedClient(args, state);
|
|
759
|
+
const cid = resolveCid(args, state);
|
|
760
|
+
const type = numberOption(args, 'type', 1);
|
|
761
|
+
const directoryWaitResult = await ensureDirectoryReadyForWrite(client, state, cid, args);
|
|
762
|
+
|
|
763
|
+
if (directoryWaitResult.phase !== 'ready') {
|
|
764
|
+
outputResult(args, {
|
|
765
|
+
command: 'write',
|
|
766
|
+
cid,
|
|
767
|
+
type,
|
|
768
|
+
directoryWaitResult,
|
|
769
|
+
waitResult: null,
|
|
770
|
+
}, [
|
|
771
|
+
`content generation blocked: cid=${cid}`,
|
|
772
|
+
directoryWaitResult.phase === 'reference_selection_required'
|
|
773
|
+
? 'directory blocked: reference selection required'
|
|
774
|
+
: `directory blocked: phase=${directoryWaitResult.phase}`,
|
|
775
|
+
'hint: rerun in an interactive terminal, or pass --reference-type <0|1|2>.',
|
|
776
|
+
]);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const contentDetail = {
|
|
781
|
+
data: directoryWaitResult.contentDetail,
|
|
782
|
+
};
|
|
783
|
+
saveTaskSnapshot(state, cid, pickTaskSnapshot(contentDetail.data));
|
|
784
|
+
|
|
785
|
+
const writePreparation = await prepareContentGeneration(client, state, cid, contentDetail, args);
|
|
786
|
+
let triggerResult = null;
|
|
787
|
+
if (writePreparation.shouldTrigger) {
|
|
788
|
+
triggerResult = await triggerContentGeneration(client, cid, type, args);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
saveTaskSnapshot(state, cid, {
|
|
792
|
+
writeTriggeredAt: new Date().toISOString(),
|
|
793
|
+
viewStep: writePreparation.viewStep,
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
let status = null;
|
|
797
|
+
if (booleanOption(args, 'wait')) {
|
|
798
|
+
status = await pollContentUntilReady(client, state, cid, args);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const resultPayload = {
|
|
802
|
+
command: 'write',
|
|
803
|
+
cid,
|
|
804
|
+
type,
|
|
805
|
+
directoryWaitResult,
|
|
806
|
+
writePreparation,
|
|
807
|
+
triggerResult,
|
|
808
|
+
waitResult: status,
|
|
809
|
+
};
|
|
810
|
+
outputResult(args, resultPayload, [
|
|
811
|
+
`directory ready: status=${directoryWaitResult.contentDetail?.directory_portion_status ?? '-'}`,
|
|
812
|
+
directoryWaitResult.directoryAction === 'triggered'
|
|
813
|
+
? 'directory sequel: triggered'
|
|
814
|
+
: `directory sequel: ${directoryWaitResult.directoryAction}`,
|
|
815
|
+
`content generation prepared: cid=${cid}`,
|
|
816
|
+
writePreparation.chargeAction === 'charged'
|
|
817
|
+
? 'charge: success'
|
|
818
|
+
: `charge: skipped (${writePreparation.chargeReason})`,
|
|
819
|
+
writePreparation.viewStepUpdated
|
|
820
|
+
? `viewStep: set to ${CONTENT_VIEW_STEP}`
|
|
821
|
+
: `viewStep: keep ${writePreparation.viewStep ?? '-'}`,
|
|
822
|
+
triggerResult
|
|
823
|
+
? `trigger: ${triggerResult.status}${triggerResult.softError ? ` (code=${triggerResult.softError.code})` : ''}`
|
|
824
|
+
: 'trigger: skipped (content already started or finished)',
|
|
825
|
+
`type: ${type}`,
|
|
826
|
+
status?.autoCompletionAction === 'generateAllUnfinished'
|
|
827
|
+
? 'unfinished chapters: auto-triggered'
|
|
828
|
+
: 'unfinished chapters: none',
|
|
829
|
+
status
|
|
830
|
+
? `content ready: task_status=${status.task_status}, content_portion_status=${status.content_portion_status ?? '-'}`
|
|
831
|
+
: (triggerResult ? 'content generation started' : 'content generation not re-triggered'),
|
|
832
|
+
]);
|
|
833
|
+
return resultPayload;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
async function handleStatus(args, state) {
|
|
837
|
+
const client = createAuthorizedClient(args, state);
|
|
838
|
+
const cid = resolveCid(args, state);
|
|
839
|
+
const detail = await getJson(client, `/task/getTaskDetail/${encodeURIComponent(cid)}`, {}, { retries: DEFAULT_RETRY_COUNT });
|
|
840
|
+
const contentDetail = booleanOption(args, 'content')
|
|
841
|
+
? await getJson(client, '/c/getContentDetail', { cid, lite: true }, { retries: DEFAULT_RETRY_COUNT })
|
|
842
|
+
: null;
|
|
843
|
+
|
|
844
|
+
saveTaskSnapshot(state, cid, pickTaskSnapshot({
|
|
845
|
+
...detail.data,
|
|
846
|
+
...(contentDetail?.data || {}),
|
|
847
|
+
}));
|
|
848
|
+
|
|
849
|
+
outputResult(args, {
|
|
850
|
+
command: 'status',
|
|
851
|
+
cid,
|
|
852
|
+
taskDetail: detail.data,
|
|
853
|
+
contentDetail: contentDetail?.data || null,
|
|
854
|
+
}, [
|
|
855
|
+
`status: cid=${cid}`,
|
|
856
|
+
`title: ${detail.data.title || detail.data.projectName || '-'}`,
|
|
857
|
+
`task_status: ${detail.data.task_status ?? '-'}`,
|
|
858
|
+
`viewStep: ${contentDetail?.data?.viewStep ?? detail.data.viewStep ?? '-'}`,
|
|
859
|
+
`percentage: ${contentDetail?.data?.percentage ?? detail.data.percentage ?? '-'}`,
|
|
860
|
+
]);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
async function handleExport(args, state) {
|
|
864
|
+
const client = createAuthorizedClient(args, state);
|
|
865
|
+
const cid = resolveCid(args, state);
|
|
866
|
+
const snapshot = await hydrateTaskSnapshot(client, state, cid);
|
|
867
|
+
const effectiveArgs = booleanOption(args, 'interactive')
|
|
868
|
+
? await collectExportInteractiveArgs(args, snapshot)
|
|
869
|
+
: args;
|
|
870
|
+
const templateIndex = numberOption(effectiveArgs, 'template', DEFAULT_EXPORT_TEMPLATE_INDEX);
|
|
871
|
+
if (!Number.isInteger(templateIndex) || templateIndex < 1 || templateIndex > templateData.length) {
|
|
872
|
+
throw new Error(`Unknown template index: ${templateIndex}`);
|
|
873
|
+
}
|
|
874
|
+
const template = templateData[templateIndex - 1];
|
|
875
|
+
|
|
876
|
+
const taskTitle = stringOption(effectiveArgs, 'title', snapshot.title || snapshot.projectName || `xique-${cid}`);
|
|
877
|
+
const layout = resolveExportLayout(snapshot, effectiveArgs);
|
|
878
|
+
const tableColor = enumOption(effectiveArgs, 'tableColor', TABLE_COLOR_OPTIONS, snapshot.tableColor || DEFAULT_TABLE_COLOR);
|
|
879
|
+
const tableStyle = enumOption(effectiveArgs, 'tableStyle', TABLE_STYLE_OPTIONS, snapshot.tableStyle ?? DEFAULT_TABLE_STYLE);
|
|
880
|
+
const themeStyleSetting = buildThemeStyleSetting(snapshot, effectiveArgs);
|
|
881
|
+
const pageMarginSetting = buildPageMarginSetting(snapshot, effectiveArgs);
|
|
882
|
+
const templatePostData = buildTemplatePostData(template, effectiveArgs);
|
|
883
|
+
const requestData = {
|
|
884
|
+
cid,
|
|
885
|
+
title: taskTitle,
|
|
886
|
+
addMark: binaryOption(effectiveArgs, 'addMark', DEFAULT_ADD_MARK),
|
|
887
|
+
autoNumber: binaryOption(effectiveArgs, 'autoNumber', DEFAULT_AUTO_NUMBER),
|
|
888
|
+
layout,
|
|
889
|
+
tableSettings: {
|
|
890
|
+
tableColor,
|
|
891
|
+
tableStyle,
|
|
892
|
+
},
|
|
893
|
+
themeStyleSetting,
|
|
894
|
+
pageMarginSetting,
|
|
895
|
+
t: Date.now(),
|
|
896
|
+
...templatePostData,
|
|
897
|
+
};
|
|
898
|
+
const response = await postBinary(client, '/c/downWithStyle', requestData);
|
|
899
|
+
const fileName = parseFileName(response.headers['content-disposition']) || `${taskTitle}.docx`;
|
|
900
|
+
const savedPath = writeDownloadFile(effectiveArgs.out, fileName, response.data);
|
|
901
|
+
saveTaskSnapshot(state, cid, {
|
|
902
|
+
tableColor,
|
|
903
|
+
tableStyle,
|
|
904
|
+
themeStyleSetting,
|
|
905
|
+
pageMarginSetting,
|
|
906
|
+
});
|
|
907
|
+
const exportConfig = pickExportConfig(requestData, templateIndex);
|
|
908
|
+
|
|
909
|
+
const resultPayload = {
|
|
910
|
+
command: 'export',
|
|
911
|
+
cid,
|
|
912
|
+
fileName,
|
|
913
|
+
savedPath,
|
|
914
|
+
config: exportConfig,
|
|
915
|
+
};
|
|
916
|
+
outputResult(args, resultPayload, [
|
|
917
|
+
`export success: cid=${cid}`,
|
|
918
|
+
`fileName: ${fileName}`,
|
|
919
|
+
`config: ${buildExportConfigSummary(exportConfig)}`,
|
|
920
|
+
`savedPath: ${savedPath}`,
|
|
921
|
+
]);
|
|
922
|
+
return resultPayload;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
async function hydrateTaskSnapshot(client, state, cid) {
|
|
926
|
+
const cached = state.tasks?.[cid] || {};
|
|
927
|
+
let detailData = {};
|
|
928
|
+
try {
|
|
929
|
+
const detail = await getJson(client, `/task/getTaskDetail/${encodeURIComponent(cid)}`, {}, { retries: DEFAULT_RETRY_COUNT });
|
|
930
|
+
detailData = detail.data || {};
|
|
931
|
+
} catch (error) {
|
|
932
|
+
if (!cached.cid) {
|
|
933
|
+
throw error;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
const snapshot = pickTaskSnapshot({
|
|
937
|
+
...cached,
|
|
938
|
+
...detailData,
|
|
939
|
+
cid,
|
|
940
|
+
});
|
|
941
|
+
saveTaskSnapshot(state, cid, snapshot);
|
|
942
|
+
return snapshot;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function buildOutlineParams(snapshot, args) {
|
|
946
|
+
const pageScopeCode = enumOption(args, ['pageScope', 'pageScopeCode'], PAGE_SCOPE_OPTIONS, snapshot.pageScopeCode ?? DEFAULT_PAGE_SCOPE_CODE);
|
|
947
|
+
const base = enumOption(args, 'base', BASE_OPTIONS, snapshot.base ?? DEFAULT_BASE);
|
|
948
|
+
const uuid = snapshot.uuid;
|
|
949
|
+
const title = stringOption(args, 'title', snapshot.projectName || snapshot.title);
|
|
950
|
+
const plan = stringOption(args, 'plan', snapshot.plan || '');
|
|
951
|
+
const requirement = stringOption(args, 'requirement', snapshot.requirement || '');
|
|
952
|
+
const rating = stringOption(args, 'rating', snapshot.rating || '');
|
|
953
|
+
|
|
954
|
+
if (!uuid) {
|
|
955
|
+
throw new Error('Missing uuid for outline generation. Run init first or make sure the task snapshot contains uuid.');
|
|
956
|
+
}
|
|
957
|
+
if (!title) {
|
|
958
|
+
throw new Error('Missing title for outline generation.');
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
return compactObject({
|
|
962
|
+
cid: snapshot.cid,
|
|
963
|
+
themeStyle: enumOption(args, 'themeStyle', THEME_STYLE_OPTIONS, snapshot.themeStyle),
|
|
964
|
+
tableQuantity: enumOption(args, 'tableQuantity', TABLE_QUANTITY_OPTIONS, snapshot.tableQuantity),
|
|
965
|
+
mermaidStyle: enumOption(args, 'mermaidStyle', MERMAID_STYLE_OPTIONS, snapshot.mermaidStyle),
|
|
966
|
+
tableColor: enumOption(args, 'tableColor', TABLE_COLOR_OPTIONS, snapshot.tableColor),
|
|
967
|
+
tableStyle: enumOption(args, 'tableStyle', TABLE_STYLE_OPTIONS, snapshot.tableStyle),
|
|
968
|
+
annexIds: snapshot.annexIds || [],
|
|
969
|
+
knowledgeProductIds: snapshot.knowledgeProductIds || [],
|
|
970
|
+
base: pageScopeCode <= 0 ? '0' : base,
|
|
971
|
+
bidFileIds: [uuid],
|
|
972
|
+
directoryNodes: [],
|
|
973
|
+
generateType: 1,
|
|
974
|
+
pageScopeCode,
|
|
975
|
+
plan,
|
|
976
|
+
rating,
|
|
977
|
+
requirement,
|
|
978
|
+
title,
|
|
979
|
+
infoImg: binaryOption(args, 'infoImg', snapshot.infoImg),
|
|
980
|
+
sceneImg: binaryOption(args, 'sceneImg', snapshot.sceneImg),
|
|
981
|
+
onlineImg: binaryOption(args, 'onlineImg', snapshot.onlineImg),
|
|
982
|
+
mermaidImg: binaryOption(args, 'mermaidImg', snapshot.mermaidImg),
|
|
983
|
+
knowledgeImg: binaryOption(args, 'knowledgeImg', snapshot.knowledgeImg),
|
|
984
|
+
themeStyleSetting: snapshot.themeStyleSetting,
|
|
985
|
+
pageMarginSetting: snapshot.pageMarginSetting,
|
|
986
|
+
multiBidId: stringOption(args, 'multiBidId', snapshot.multiBidId),
|
|
987
|
+
multiBidType: stringOption(args, 'multiBidType', snapshot.multiBidType),
|
|
988
|
+
planMode: enumOption(args, 'planMode', PLAN_MODE_OPTIONS, snapshot.planMode ?? 0),
|
|
989
|
+
epcEngineerType: normalizeEpcEngineerType(stringOption(args, 'epcEngineerType', snapshot.epcEngineerType)),
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
async function triggerOutlineGeneration(client, snapshot, params) {
|
|
994
|
+
await postJson(client, '/task/preSet', params);
|
|
995
|
+
|
|
996
|
+
const hasTaskStatus = snapshot.task_status !== undefined && snapshot.task_status !== null;
|
|
997
|
+
if (hasTaskStatus && Number(snapshot.task_status) !== 0) {
|
|
998
|
+
await getJson(client, '/proxy/multiBiddingChoice', compactObject({
|
|
999
|
+
uuid: snapshot.uuid,
|
|
1000
|
+
multiBidId: params.multiBidId ?? snapshot.multiBidId,
|
|
1001
|
+
epcEngineerType: normalizeEpcEngineerTypeForProxy(params.epcEngineerType ?? snapshot.epcEngineerType),
|
|
1002
|
+
}));
|
|
1003
|
+
return 'multiBiddingChoice';
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
await postJson(client, '/task/generateByRequirement', params);
|
|
1007
|
+
return 'generateByRequirement';
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function buildFileMetadata(files, args) {
|
|
1011
|
+
const typeValues = ensureArray(args.type);
|
|
1012
|
+
const purposeValues = ensureArray(args.purpose);
|
|
1013
|
+
if (typeValues.length > 1 && typeValues.length !== files.length) {
|
|
1014
|
+
throw new Error('When passing multiple --type values, the count must match the number of files.');
|
|
1015
|
+
}
|
|
1016
|
+
if (purposeValues.length > 1 && purposeValues.length !== files.length) {
|
|
1017
|
+
throw new Error('When passing multiple --purpose values, the count must match the number of files.');
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const types = files.map((filePath, index) => {
|
|
1021
|
+
if (typeValues.length === files.length) {
|
|
1022
|
+
return typeValues[index];
|
|
1023
|
+
}
|
|
1024
|
+
if (typeValues.length === 1) {
|
|
1025
|
+
return typeValues[0];
|
|
1026
|
+
}
|
|
1027
|
+
const detected = detectFileType(path.basename(filePath));
|
|
1028
|
+
if (detected) {
|
|
1029
|
+
return detected;
|
|
1030
|
+
}
|
|
1031
|
+
return index === 0 ? 'BDOC01' : 'QT0000';
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
const purposes = files.map((filePath, index) => {
|
|
1035
|
+
if (purposeValues.length === files.length) {
|
|
1036
|
+
return purposeValues[index];
|
|
1037
|
+
}
|
|
1038
|
+
if (purposeValues.length === 1) {
|
|
1039
|
+
return purposeValues[0];
|
|
1040
|
+
}
|
|
1041
|
+
return AI_PURPOSE_MAP[types[index]] || '';
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
const purposesPayload = purposes.map((purpose, index) => `[${types[index]}]${purpose || ''}`).join('@@@');
|
|
1045
|
+
return {
|
|
1046
|
+
types,
|
|
1047
|
+
purposes,
|
|
1048
|
+
purposesPayload,
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
async function pollParseUntilReady(client, state, cid, uuid, args) {
|
|
1053
|
+
return pollUntil(async () => {
|
|
1054
|
+
const result = await fetchParseStatus(client, state, cid, uuid);
|
|
1055
|
+
return {
|
|
1056
|
+
done: isParseReadyForNextStep(result.success),
|
|
1057
|
+
data: result,
|
|
1058
|
+
progressLabel: `success=${result.success}`,
|
|
1059
|
+
};
|
|
1060
|
+
}, {
|
|
1061
|
+
intervalMs: secondsToMs(numberOption(args, 'intervalSec', DEFAULT_PARSE_INTERVAL_MS / 1000)),
|
|
1062
|
+
timeoutMs: secondsToMs(numberOption(args, 'timeoutSec', DEFAULT_TIMEOUT_MS / 1000)),
|
|
1063
|
+
quiet: booleanOption(args, 'json'),
|
|
1064
|
+
waitingText: 'waiting parse result',
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function isParseReadyForNextStep(success) {
|
|
1069
|
+
const parsed = Number(success);
|
|
1070
|
+
return parsed === 1 || parsed === 4;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
async function pollTaskUntil(client, state, cid, args, isDone, formatProgress) {
|
|
1074
|
+
return pollUntil(async () => {
|
|
1075
|
+
const detail = await getJson(client, `/task/getTaskDetail/${encodeURIComponent(cid)}`, {}, { retries: DEFAULT_RETRY_COUNT });
|
|
1076
|
+
saveTaskSnapshot(state, cid, pickTaskSnapshot(detail.data));
|
|
1077
|
+
return {
|
|
1078
|
+
done: isDone(detail.data),
|
|
1079
|
+
data: detail.data,
|
|
1080
|
+
progressLabel: formatProgress(detail.data),
|
|
1081
|
+
};
|
|
1082
|
+
}, {
|
|
1083
|
+
intervalMs: secondsToMs(numberOption(args, 'intervalSec', DEFAULT_TASK_INTERVAL_MS / 1000)),
|
|
1084
|
+
timeoutMs: secondsToMs(numberOption(args, 'timeoutSec', DEFAULT_TIMEOUT_MS / 1000)),
|
|
1085
|
+
quiet: booleanOption(args, 'json'),
|
|
1086
|
+
waitingText: 'waiting task status',
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
async function waitForOutlineReady(client, state, cid, uuid, args) {
|
|
1091
|
+
const handledIssues = new Set();
|
|
1092
|
+
let submittedReferenceType = null;
|
|
1093
|
+
|
|
1094
|
+
return pollUntil(async () => {
|
|
1095
|
+
const [taskDetail, outlineDetail, referenceStatus] = await Promise.all([
|
|
1096
|
+
getJson(client, `/task/getTaskDetail/${encodeURIComponent(cid)}`),
|
|
1097
|
+
fetchOutlineDetail(client, cid),
|
|
1098
|
+
fetchReferenceStatusSafe(client, cid),
|
|
1099
|
+
]);
|
|
1100
|
+
saveTaskSnapshot(state, cid, pickTaskSnapshot(taskDetail.data));
|
|
1101
|
+
|
|
1102
|
+
const outlineData = outlineDetail?.data || {};
|
|
1103
|
+
const hasOutline = isOutlineReadyForCli(outlineData);
|
|
1104
|
+
const referenceData = referenceStatus?.data || {};
|
|
1105
|
+
const referencePhase = normalizeReferenceStatus(referenceData.status);
|
|
1106
|
+
if (referencePhase !== null) {
|
|
1107
|
+
saveTaskSnapshot(state, cid, {
|
|
1108
|
+
referenceStatus: referencePhase,
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (Number(taskDetail.data?.task_status) === 7) {
|
|
1113
|
+
return {
|
|
1114
|
+
done: true,
|
|
1115
|
+
data: {
|
|
1116
|
+
phase: 'failed',
|
|
1117
|
+
taskDetail: taskDetail.data,
|
|
1118
|
+
outlineDetail: outlineData,
|
|
1119
|
+
},
|
|
1120
|
+
progressLabel: 'task_status=7',
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const forcedReferenceSubmission = await submitExplicitReferenceTypeIfNeeded(
|
|
1125
|
+
client,
|
|
1126
|
+
cid,
|
|
1127
|
+
args,
|
|
1128
|
+
taskDetail.data,
|
|
1129
|
+
submittedReferenceType,
|
|
1130
|
+
);
|
|
1131
|
+
if (forcedReferenceSubmission !== null) {
|
|
1132
|
+
submittedReferenceType = forcedReferenceSubmission;
|
|
1133
|
+
saveTaskSnapshot(state, cid, {
|
|
1134
|
+
outlineReferenceType: submittedReferenceType,
|
|
1135
|
+
});
|
|
1136
|
+
return {
|
|
1137
|
+
done: false,
|
|
1138
|
+
data: null,
|
|
1139
|
+
progressLabel: `referenceType=${submittedReferenceType} submitted from directory status, waiting outline`,
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
if (hasOutline) {
|
|
1144
|
+
return {
|
|
1145
|
+
done: true,
|
|
1146
|
+
data: {
|
|
1147
|
+
phase: 'ready',
|
|
1148
|
+
taskDetail: taskDetail.data,
|
|
1149
|
+
outlineDetail: outlineData,
|
|
1150
|
+
},
|
|
1151
|
+
progressLabel: `rootChapters=${countOutlineRoots({ outlineDetail: outlineData, taskDetail: taskDetail.data })}`,
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
if (referencePhase === 1) {
|
|
1156
|
+
if (submittedReferenceType !== null) {
|
|
1157
|
+
return {
|
|
1158
|
+
done: false,
|
|
1159
|
+
data: null,
|
|
1160
|
+
progressLabel: `reference_status=1, referenceType=${submittedReferenceType}, waiting confirmation`,
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const selection = await resolveOutlineReferenceSelection(args, referenceData);
|
|
1165
|
+
if (!selection.ready) {
|
|
1166
|
+
return {
|
|
1167
|
+
done: true,
|
|
1168
|
+
data: {
|
|
1169
|
+
phase: 'reference_selection_required',
|
|
1170
|
+
taskDetail: taskDetail.data,
|
|
1171
|
+
outlineDetail: outlineData,
|
|
1172
|
+
referenceDetail: referenceData,
|
|
1173
|
+
options: buildOutlineReferenceChoices(referenceData),
|
|
1174
|
+
},
|
|
1175
|
+
progressLabel: 'reference_selection_required',
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
await submitOutlineReferenceType(client, cid, selection.referenceType);
|
|
1180
|
+
submittedReferenceType = selection.referenceType;
|
|
1181
|
+
saveTaskSnapshot(state, cid, {
|
|
1182
|
+
outlineReferenceType: selection.referenceType,
|
|
1183
|
+
});
|
|
1184
|
+
return {
|
|
1185
|
+
done: false,
|
|
1186
|
+
data: null,
|
|
1187
|
+
progressLabel: `referenceType=${selection.referenceType} submitted, waiting outline`,
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
if (uuid) {
|
|
1192
|
+
const parseStatus = await fetchParseStatus(client, state, cid, uuid);
|
|
1193
|
+
const parseSuccess = Number(parseStatus.success);
|
|
1194
|
+
const issues = collectParseIssues(parseStatus);
|
|
1195
|
+
|
|
1196
|
+
if (parseSuccess === 4 && issues.length > 0) {
|
|
1197
|
+
const autoIgnore = resolveAutoIgnoreIssue(issues, args, uuid, handledIssues);
|
|
1198
|
+
|
|
1199
|
+
if (autoIgnore) {
|
|
1200
|
+
await submitIntermediateChoice(client, autoIgnore.payload);
|
|
1201
|
+
handledIssues.add(autoIgnore.issue);
|
|
1202
|
+
return {
|
|
1203
|
+
done: false,
|
|
1204
|
+
data: null,
|
|
1205
|
+
progressLabel: `ignored ${autoIgnore.issue}, waiting outline`,
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
return {
|
|
1210
|
+
done: true,
|
|
1211
|
+
data: {
|
|
1212
|
+
phase: 'supplement_required',
|
|
1213
|
+
issues,
|
|
1214
|
+
parseStatus,
|
|
1215
|
+
taskDetail: taskDetail.data,
|
|
1216
|
+
outlineDetail: outlineData,
|
|
1217
|
+
},
|
|
1218
|
+
progressLabel: `supplement_required=${issues.join(',')}`,
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
return {
|
|
1223
|
+
done: false,
|
|
1224
|
+
data: null,
|
|
1225
|
+
progressLabel: `success=${parseSuccess}, task_status=${taskDetail.data?.task_status ?? '-'}, directory_status=${taskDetail.data?.directory_portion_status ?? '-'}, outline_roots=${countOutlineRoots({ outlineDetail: outlineData })}, outline_status=${outlineData.outline_status ? 1 : 0}, reference_status=${referencePhase ?? '-'}`,
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
return {
|
|
1230
|
+
done: false,
|
|
1231
|
+
data: null,
|
|
1232
|
+
progressLabel: `task_status=${taskDetail.data?.task_status ?? '-'}, directory_status=${taskDetail.data?.directory_portion_status ?? '-'}, outline_roots=${countOutlineRoots({ outlineDetail: outlineData })}, outline_status=${outlineData.outline_status ? 1 : 0}, reference_status=${referencePhase ?? '-'}`,
|
|
1233
|
+
};
|
|
1234
|
+
}, {
|
|
1235
|
+
intervalMs: secondsToMs(numberOption(args, 'intervalSec', DEFAULT_TASK_INTERVAL_MS / 1000)),
|
|
1236
|
+
timeoutMs: secondsToMs(numberOption(args, 'timeoutSec', DEFAULT_TIMEOUT_MS / 1000)),
|
|
1237
|
+
quiet: booleanOption(args, 'json'),
|
|
1238
|
+
waitingText: 'waiting outline generation',
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
async function pollContentUntilReady(client, state, cid, args) {
|
|
1243
|
+
let unfinishedTriggered = false;
|
|
1244
|
+
let lastUnfinishedTriggerAt = 0;
|
|
1245
|
+
|
|
1246
|
+
return pollUntil(async () => {
|
|
1247
|
+
const detail = await fetchContentDetail(client, cid);
|
|
1248
|
+
const contentData = detail.data || {};
|
|
1249
|
+
saveTaskSnapshot(state, cid, pickTaskSnapshot(contentData));
|
|
1250
|
+
|
|
1251
|
+
const taskStatus = Number(contentData.task_status);
|
|
1252
|
+
const contentPortionStatus = Number(contentData.content_portion_status);
|
|
1253
|
+
const completedChapter = numberOrDefault(contentData.completedChapter, 0);
|
|
1254
|
+
const totalChapter = numberOrDefault(contentData.totalChapter, 0);
|
|
1255
|
+
const percentage = numberOrDefault(contentData.percentage, 0);
|
|
1256
|
+
|
|
1257
|
+
if (isContentComplete(contentData)) {
|
|
1258
|
+
return {
|
|
1259
|
+
done: true,
|
|
1260
|
+
data: {
|
|
1261
|
+
...contentData,
|
|
1262
|
+
autoCompletionAction: unfinishedTriggered ? 'generateAllUnfinished' : 'none',
|
|
1263
|
+
},
|
|
1264
|
+
progressLabel: `task_status=${taskStatus}, content_portion_status=${contentPortionStatus}, completed=${completedChapter}/${totalChapter}, percentage=${percentage}`,
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
if (shouldTriggerGenerateAllUnfinished(contentData)) {
|
|
1269
|
+
const now = Date.now();
|
|
1270
|
+
if (!unfinishedTriggered || now - lastUnfinishedTriggerAt >= GENERATE_UNFINISHED_RETRY_INTERVAL_MS) {
|
|
1271
|
+
await triggerGenerateAllUnfinished(client, cid);
|
|
1272
|
+
unfinishedTriggered = true;
|
|
1273
|
+
lastUnfinishedTriggerAt = now;
|
|
1274
|
+
saveTaskSnapshot(state, cid, {
|
|
1275
|
+
generateAllUnfinishedTriggeredAt: new Date(now).toISOString(),
|
|
1276
|
+
});
|
|
1277
|
+
return {
|
|
1278
|
+
done: false,
|
|
1279
|
+
data: null,
|
|
1280
|
+
progressLabel: `generateAllUnfinished triggered, completed=${completedChapter}/${totalChapter}, percentage=${percentage}`,
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
const done = (taskStatus === 5 || (taskStatus === 4 && contentPortionStatus === 1))
|
|
1286
|
+
&& !hasUnfinishedChapters(contentData);
|
|
1287
|
+
return {
|
|
1288
|
+
done,
|
|
1289
|
+
data: done
|
|
1290
|
+
? {
|
|
1291
|
+
...contentData,
|
|
1292
|
+
autoCompletionAction: unfinishedTriggered ? 'generateAllUnfinished' : 'none',
|
|
1293
|
+
}
|
|
1294
|
+
: contentData,
|
|
1295
|
+
progressLabel: `task_status=${taskStatus}, content_portion_status=${contentPortionStatus}, completed=${completedChapter}/${totalChapter}, percentage=${percentage}`,
|
|
1296
|
+
};
|
|
1297
|
+
}, {
|
|
1298
|
+
intervalMs: secondsToMs(numberOption(args, 'intervalSec', DEFAULT_TASK_INTERVAL_MS / 1000)),
|
|
1299
|
+
timeoutMs: secondsToMs(numberOption(args, 'timeoutSec', DEFAULT_TIMEOUT_MS / 1000)),
|
|
1300
|
+
quiet: booleanOption(args, 'json'),
|
|
1301
|
+
waitingText: 'waiting content generation',
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
async function fetchOutlineDetail(client, cid) {
|
|
1306
|
+
return getJson(client, '/c/getOutLineDetail', { cid }, { retries: DEFAULT_RETRY_COUNT });
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
async function fetchReferenceStatusSafe(client, cid) {
|
|
1310
|
+
try {
|
|
1311
|
+
return await getJson(client, `/task/directory/checkReference/${encodeURIComponent(cid)}`, {}, { retries: DEFAULT_RETRY_COUNT });
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
return null;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
async function fetchContentDetail(client, cid) {
|
|
1318
|
+
return getJson(client, '/c/getContentDetail', {
|
|
1319
|
+
cid,
|
|
1320
|
+
lite: true,
|
|
1321
|
+
}, { retries: DEFAULT_RETRY_COUNT });
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
async function fetchParseStatus(client, state, cid, uuid) {
|
|
1325
|
+
const result = await getJson(client, '/proxy/getFileAsyncResult', {
|
|
1326
|
+
cid,
|
|
1327
|
+
uuid,
|
|
1328
|
+
}, { retries: DEFAULT_RETRY_COUNT });
|
|
1329
|
+
saveTaskSnapshot(state, cid, {
|
|
1330
|
+
uuid,
|
|
1331
|
+
parseSuccess: result.data?.success,
|
|
1332
|
+
parseStatusFetchedAt: new Date().toISOString(),
|
|
1333
|
+
});
|
|
1334
|
+
return result.data || {};
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
async function submitIntermediateChoice(client, payload) {
|
|
1338
|
+
return postJson(client, '/proxy/inputTextExtend', payload);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
async function submitOutlineReferenceType(client, cid, referenceType) {
|
|
1342
|
+
return postJson(client, '/task/directory/generateByReferenceType', {
|
|
1343
|
+
cid,
|
|
1344
|
+
referenceType,
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
async function submitExplicitReferenceTypeIfNeeded(client, cid, args, detail, submittedReferenceType) {
|
|
1349
|
+
if (submittedReferenceType !== null) {
|
|
1350
|
+
return null;
|
|
1351
|
+
}
|
|
1352
|
+
const explicitRaw = stringOption(args, 'referenceType');
|
|
1353
|
+
if (explicitRaw === undefined) {
|
|
1354
|
+
return null;
|
|
1355
|
+
}
|
|
1356
|
+
const referenceType = parseOutlineReferenceType(explicitRaw);
|
|
1357
|
+
if (referenceType === null) {
|
|
1358
|
+
return null;
|
|
1359
|
+
}
|
|
1360
|
+
const directoryStatus = normalizeDirectoryPortionStatus(detail?.directory_portion_status);
|
|
1361
|
+
if (directoryStatus !== DIRECTORY_PORTION_STATUS.OUTLINE_BASE_COMPLETED) {
|
|
1362
|
+
return null;
|
|
1363
|
+
}
|
|
1364
|
+
await submitOutlineReferenceType(client, cid, referenceType);
|
|
1365
|
+
return referenceType;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
async function triggerDirectorySequel(client, cid) {
|
|
1369
|
+
return getJson(client, `/task/sequelForOutline/${encodeURIComponent(cid)}`);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
async function triggerGenerateAllUnfinished(client, cid) {
|
|
1373
|
+
return postJson(client, '/task/generateAllUnfinished', { cid });
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
async function ensureDirectoryReadyForWrite(client, state, cid, args) {
|
|
1377
|
+
let submittedReferenceType = null;
|
|
1378
|
+
let sequelTriggered = false;
|
|
1379
|
+
let lastSequelTriggerAt = 0;
|
|
1380
|
+
|
|
1381
|
+
return pollUntil(async () => {
|
|
1382
|
+
const [taskDetail, outlineDetail, contentDetail, referenceStatus] = await Promise.all([
|
|
1383
|
+
getJson(client, `/task/getTaskDetail/${encodeURIComponent(cid)}`),
|
|
1384
|
+
fetchOutlineDetail(client, cid),
|
|
1385
|
+
fetchContentDetail(client, cid),
|
|
1386
|
+
fetchReferenceStatusSafe(client, cid),
|
|
1387
|
+
]);
|
|
1388
|
+
|
|
1389
|
+
const taskData = taskDetail?.data || {};
|
|
1390
|
+
const outlineData = outlineDetail?.data || {};
|
|
1391
|
+
const contentData = contentDetail?.data || {};
|
|
1392
|
+
const effectiveContentData = {
|
|
1393
|
+
...taskData,
|
|
1394
|
+
...contentData,
|
|
1395
|
+
};
|
|
1396
|
+
const referenceData = referenceStatus?.data || {};
|
|
1397
|
+
const referencePhase = normalizeReferenceStatus(referenceData.status);
|
|
1398
|
+
|
|
1399
|
+
saveTaskSnapshot(state, cid, pickTaskSnapshot({
|
|
1400
|
+
...taskData,
|
|
1401
|
+
...contentData,
|
|
1402
|
+
}));
|
|
1403
|
+
if (referencePhase !== null) {
|
|
1404
|
+
saveTaskSnapshot(state, cid, {
|
|
1405
|
+
referenceStatus: referencePhase,
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
if (isDirectoryReadyForWrite(effectiveContentData)) {
|
|
1410
|
+
return {
|
|
1411
|
+
done: true,
|
|
1412
|
+
data: {
|
|
1413
|
+
phase: 'ready',
|
|
1414
|
+
taskDetail: taskData,
|
|
1415
|
+
outlineDetail: outlineData,
|
|
1416
|
+
contentDetail: effectiveContentData,
|
|
1417
|
+
referenceDetail: referenceData,
|
|
1418
|
+
directoryAction: sequelTriggered ? 'triggered' : 'already_ready',
|
|
1419
|
+
},
|
|
1420
|
+
progressLabel: `directory_status=${effectiveContentData.directory_portion_status}, nodes=${countDirectoryRoots(effectiveContentData)}`,
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
const forcedReferenceSubmission = await submitExplicitReferenceTypeIfNeeded(
|
|
1425
|
+
client,
|
|
1426
|
+
cid,
|
|
1427
|
+
args,
|
|
1428
|
+
effectiveContentData,
|
|
1429
|
+
submittedReferenceType,
|
|
1430
|
+
);
|
|
1431
|
+
if (forcedReferenceSubmission !== null) {
|
|
1432
|
+
submittedReferenceType = forcedReferenceSubmission;
|
|
1433
|
+
saveTaskSnapshot(state, cid, {
|
|
1434
|
+
outlineReferenceType: submittedReferenceType,
|
|
1435
|
+
});
|
|
1436
|
+
return {
|
|
1437
|
+
done: false,
|
|
1438
|
+
data: null,
|
|
1439
|
+
progressLabel: `referenceType=${submittedReferenceType} submitted from directory status, waiting outline`,
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
if (referencePhase === 1) {
|
|
1444
|
+
if (submittedReferenceType !== null) {
|
|
1445
|
+
return {
|
|
1446
|
+
done: false,
|
|
1447
|
+
data: null,
|
|
1448
|
+
progressLabel: `reference_status=1, referenceType=${submittedReferenceType}, waiting outline`,
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
const selection = await resolveOutlineReferenceSelection(args, referenceData);
|
|
1453
|
+
if (!selection.ready) {
|
|
1454
|
+
return {
|
|
1455
|
+
done: true,
|
|
1456
|
+
data: {
|
|
1457
|
+
phase: 'reference_selection_required',
|
|
1458
|
+
taskDetail: taskData,
|
|
1459
|
+
outlineDetail: outlineData,
|
|
1460
|
+
contentDetail: contentData,
|
|
1461
|
+
referenceDetail: referenceData,
|
|
1462
|
+
options: buildOutlineReferenceChoices(referenceData),
|
|
1463
|
+
directoryAction: sequelTriggered ? 'triggered' : 'not_started',
|
|
1464
|
+
},
|
|
1465
|
+
progressLabel: 'reference_selection_required',
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
await submitOutlineReferenceType(client, cid, selection.referenceType);
|
|
1470
|
+
submittedReferenceType = selection.referenceType;
|
|
1471
|
+
saveTaskSnapshot(state, cid, {
|
|
1472
|
+
outlineReferenceType: selection.referenceType,
|
|
1473
|
+
});
|
|
1474
|
+
return {
|
|
1475
|
+
done: false,
|
|
1476
|
+
data: null,
|
|
1477
|
+
progressLabel: `referenceType=${selection.referenceType} submitted, waiting outline`,
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
const directoryStatus = normalizeDirectoryPortionStatus(
|
|
1482
|
+
effectiveContentData.directory_portion_status,
|
|
1483
|
+
);
|
|
1484
|
+
if (directoryStatus === DIRECTORY_PORTION_STATUS.OUTLINE_COMPLETED) {
|
|
1485
|
+
const now = Date.now();
|
|
1486
|
+
if (!sequelTriggered || now - lastSequelTriggerAt >= DIRECTORY_SEQUEL_RETRY_INTERVAL_MS) {
|
|
1487
|
+
const isInitialTrigger = !sequelTriggered;
|
|
1488
|
+
await triggerDirectorySequel(client, cid);
|
|
1489
|
+
sequelTriggered = true;
|
|
1490
|
+
lastSequelTriggerAt = now;
|
|
1491
|
+
saveTaskSnapshot(state, cid, {
|
|
1492
|
+
directorySequelTriggeredAt: new Date(now).toISOString(),
|
|
1493
|
+
});
|
|
1494
|
+
return {
|
|
1495
|
+
done: false,
|
|
1496
|
+
data: null,
|
|
1497
|
+
progressLabel: isInitialTrigger
|
|
1498
|
+
? 'sequelForOutline triggered, waiting directory'
|
|
1499
|
+
: 'sequelForOutline retried, waiting directory',
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
return {
|
|
1503
|
+
done: false,
|
|
1504
|
+
data: null,
|
|
1505
|
+
progressLabel: 'directory_status=1, waiting sequel',
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
if (directoryStatus === DIRECTORY_PORTION_STATUS.SEQUEL_GENERATING) {
|
|
1510
|
+
sequelTriggered = true;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
return {
|
|
1514
|
+
done: false,
|
|
1515
|
+
data: null,
|
|
1516
|
+
progressLabel: `directory_status=${directoryStatus ?? '-'}, directory_nodes=${countDirectoryRoots(effectiveContentData)}, outline_roots=${countOutlineRoots({ outlineDetail: outlineData })}, outline_status=${outlineData.outline_status ? 1 : 0}, reference_status=${referencePhase ?? '-'}`,
|
|
1517
|
+
};
|
|
1518
|
+
}, {
|
|
1519
|
+
intervalMs: secondsToMs(numberOption(args, 'intervalSec', DEFAULT_TASK_INTERVAL_MS / 1000)),
|
|
1520
|
+
timeoutMs: secondsToMs(numberOption(args, 'timeoutSec', DEFAULT_TIMEOUT_MS / 1000)),
|
|
1521
|
+
quiet: booleanOption(args, 'json'),
|
|
1522
|
+
waitingText: 'waiting directory phase',
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
async function prepareContentGeneration(client, state, cid, contentDetailResponse, args) {
|
|
1527
|
+
const contentDetail = contentDetailResponse?.data || {};
|
|
1528
|
+
const viewStep = Number(contentDetail.viewStep);
|
|
1529
|
+
const taskStatus = Number(contentDetail.task_status);
|
|
1530
|
+
const contentPortionStatus = Number(contentDetail.content_portion_status);
|
|
1531
|
+
const contentAlreadyStarted = isContentStarted(contentDetail);
|
|
1532
|
+
|
|
1533
|
+
let chargeAction = 'skipped';
|
|
1534
|
+
let chargeReason = '';
|
|
1535
|
+
let viewStepUpdated = false;
|
|
1536
|
+
|
|
1537
|
+
if (viewStep === CONTENT_VIEW_STEP) {
|
|
1538
|
+
chargeReason = 'already_in_content_phase';
|
|
1539
|
+
} else if (contentAlreadyStarted) {
|
|
1540
|
+
chargeReason = 'content_already_started';
|
|
1541
|
+
} else {
|
|
1542
|
+
await chargeForContent(client, cid);
|
|
1543
|
+
chargeAction = 'charged';
|
|
1544
|
+
chargeReason = 'charged_before_write';
|
|
1545
|
+
await setContentViewStep(client, cid);
|
|
1546
|
+
viewStepUpdated = true;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
const shouldTrigger = !isContentRunningOrDone(contentDetail);
|
|
1550
|
+
const snapshotPatch = compactObject({
|
|
1551
|
+
cid,
|
|
1552
|
+
viewStep: viewStepUpdated ? CONTENT_VIEW_STEP : contentDetail.viewStep,
|
|
1553
|
+
task_status: Number.isFinite(taskStatus) ? taskStatus : undefined,
|
|
1554
|
+
content_portion_status: Number.isFinite(contentPortionStatus) ? contentPortionStatus : undefined,
|
|
1555
|
+
chargeAction,
|
|
1556
|
+
chargeReason,
|
|
1557
|
+
});
|
|
1558
|
+
saveTaskSnapshot(state, cid, snapshotPatch);
|
|
1559
|
+
|
|
1560
|
+
return {
|
|
1561
|
+
chargeAction,
|
|
1562
|
+
chargeReason,
|
|
1563
|
+
viewStep: viewStepUpdated ? CONTENT_VIEW_STEP : contentDetail.viewStep,
|
|
1564
|
+
viewStepUpdated,
|
|
1565
|
+
shouldTrigger,
|
|
1566
|
+
taskStatus,
|
|
1567
|
+
contentPortionStatus,
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
async function chargeForContent(client, cid) {
|
|
1572
|
+
return postJson(client, '/order/charge', { cid });
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
async function setContentViewStep(client, cid) {
|
|
1576
|
+
return postJson(client, '/c/viewStep', {
|
|
1577
|
+
cid,
|
|
1578
|
+
viewStep: CONTENT_VIEW_STEP,
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
async function triggerContentGeneration(client, cid, type, args) {
|
|
1583
|
+
const maxRetries = 1;
|
|
1584
|
+
let retryCount = 0;
|
|
1585
|
+
|
|
1586
|
+
while (retryCount <= maxRetries) {
|
|
1587
|
+
try {
|
|
1588
|
+
const result = await getJson(client, `/task/autoGenerateContent/${encodeURIComponent(cid)}/${type}`);
|
|
1589
|
+
return {
|
|
1590
|
+
status: 'triggered',
|
|
1591
|
+
retryCount,
|
|
1592
|
+
response: result.data || null,
|
|
1593
|
+
softError: null,
|
|
1594
|
+
};
|
|
1595
|
+
} catch (error) {
|
|
1596
|
+
const errorCode = Number(error?.code);
|
|
1597
|
+
if (errorCode === 429 && retryCount < maxRetries) {
|
|
1598
|
+
retryCount += 1;
|
|
1599
|
+
if (!booleanOption(args, 'json')) {
|
|
1600
|
+
console.log('content trigger busy: retrying in 30 seconds');
|
|
1601
|
+
}
|
|
1602
|
+
await sleep(30_000);
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
if (SOFT_CONTENT_TRIGGER_CODES.has(errorCode)) {
|
|
1606
|
+
return {
|
|
1607
|
+
status: 'accepted_with_soft_error',
|
|
1608
|
+
retryCount,
|
|
1609
|
+
response: null,
|
|
1610
|
+
softError: {
|
|
1611
|
+
code: errorCode,
|
|
1612
|
+
message: error.message,
|
|
1613
|
+
},
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
throw error;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
return {
|
|
1621
|
+
status: 'skipped',
|
|
1622
|
+
retryCount,
|
|
1623
|
+
response: null,
|
|
1624
|
+
softError: null,
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
function isContentStarted(detail) {
|
|
1629
|
+
return Number(detail?.viewStep) === CONTENT_VIEW_STEP || isContentRunningOrDone(detail);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
function isContentRunningOrDone(detail) {
|
|
1633
|
+
const taskStatus = Number(detail?.task_status);
|
|
1634
|
+
const contentPortionStatus = Number(detail?.content_portion_status);
|
|
1635
|
+
return taskStatus >= 4 || contentPortionStatus > 0;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
async function pollUntil(checkFn, options) {
|
|
1639
|
+
const startedAt = Date.now();
|
|
1640
|
+
while (true) {
|
|
1641
|
+
const result = await checkFn();
|
|
1642
|
+
if (result.done) {
|
|
1643
|
+
return result.data;
|
|
1644
|
+
}
|
|
1645
|
+
if (options.timeoutMs > 0 && Date.now() - startedAt >= options.timeoutMs) {
|
|
1646
|
+
throw new Error(`${options.waitingText} timed out after ${Math.round(options.timeoutMs / 1000)} seconds.`);
|
|
1647
|
+
}
|
|
1648
|
+
if (!options.quiet) {
|
|
1649
|
+
console.log(`${options.waitingText}: ${result.progressLabel}`);
|
|
1650
|
+
}
|
|
1651
|
+
await sleep(options.intervalMs);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
function outputResult(args, jsonPayload, lines) {
|
|
1656
|
+
if (booleanOption(args, 'json')) {
|
|
1657
|
+
console.log(JSON.stringify(jsonPayload, null, 2));
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
for (const line of lines) {
|
|
1661
|
+
console.log(line);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function collectParseIssues(result) {
|
|
1666
|
+
const issueKeys = [
|
|
1667
|
+
'requirement_exist',
|
|
1668
|
+
'requirement_judgement',
|
|
1669
|
+
'billOfQuantities_judgement',
|
|
1670
|
+
'score_judgement',
|
|
1671
|
+
'directory_judgement',
|
|
1672
|
+
'scoreAndDirectory_judgement',
|
|
1673
|
+
'bidderIdentity_judgement',
|
|
1674
|
+
];
|
|
1675
|
+
|
|
1676
|
+
return issueKeys.filter(key => {
|
|
1677
|
+
const directValue = result[key];
|
|
1678
|
+
const nestedValue = result.judgement_status?.[key.replace('_judgement', '')];
|
|
1679
|
+
return String(directValue ?? nestedValue ?? '1') === '0';
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
function normalizeReferenceStatus(value) {
|
|
1684
|
+
if (value === undefined || value === null || value === '') {
|
|
1685
|
+
return null;
|
|
1686
|
+
}
|
|
1687
|
+
const parsed = Number(value);
|
|
1688
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
function parseOutlineReferenceType(value) {
|
|
1692
|
+
if (value === undefined || value === null || value === '') {
|
|
1693
|
+
return null;
|
|
1694
|
+
}
|
|
1695
|
+
const normalized = String(value).trim().toLowerCase();
|
|
1696
|
+
const option = OUTLINE_REFERENCE_OPTIONS.find(item => item.aliases.includes(normalized));
|
|
1697
|
+
return option ? option.value : null;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function validateOutlineReferenceTypeOption(args) {
|
|
1701
|
+
const explicitRaw = stringOption(args, 'referenceType');
|
|
1702
|
+
if (explicitRaw === undefined) {
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
if (parseOutlineReferenceType(explicitRaw) === null) {
|
|
1706
|
+
throw new Error('Option --reference-type expects 0, 1, 2, merge, format, or score.');
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function normalizeChoicesScope(value) {
|
|
1711
|
+
const normalized = String(value || 'all').trim().toLowerCase();
|
|
1712
|
+
if (['all', 'outline', 'export'].includes(normalized)) {
|
|
1713
|
+
return normalized;
|
|
1714
|
+
}
|
|
1715
|
+
throw new Error('choices expects an optional section: all, outline, or export.');
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
function buildChoicesPayload(scope) {
|
|
1719
|
+
const payload = {
|
|
1720
|
+
command: 'choices',
|
|
1721
|
+
scope,
|
|
1722
|
+
};
|
|
1723
|
+
if (scope === 'all' || scope === 'outline') {
|
|
1724
|
+
payload.outline = {
|
|
1725
|
+
pageScope: PAGE_SCOPE_OPTIONS,
|
|
1726
|
+
base: BASE_OPTIONS,
|
|
1727
|
+
themeStyle: THEME_STYLE_OPTIONS,
|
|
1728
|
+
tableQuantity: TABLE_QUANTITY_OPTIONS,
|
|
1729
|
+
tableColor: TABLE_COLOR_OPTIONS,
|
|
1730
|
+
tableStyle: TABLE_STYLE_OPTIONS,
|
|
1731
|
+
imageToggles: ['infoImg', 'sceneImg', 'onlineImg', 'mermaidImg', 'knowledgeImg'],
|
|
1732
|
+
mermaidStyle: MERMAID_STYLE_OPTIONS,
|
|
1733
|
+
planMode: PLAN_MODE_OPTIONS,
|
|
1734
|
+
multiBid: ['--multi-bid-id', '--multi-bid-type'],
|
|
1735
|
+
epcEngineerType: {
|
|
1736
|
+
option: '--epc-engineer-type',
|
|
1737
|
+
aliases: Object.keys(EPC_ENGINEER_TYPE_ALIAS_MAP),
|
|
1738
|
+
},
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
if (scope === 'all' || scope === 'export') {
|
|
1742
|
+
payload.export = {
|
|
1743
|
+
template: EXPORT_TEMPLATE_OPTIONS,
|
|
1744
|
+
autoNumber: BINARY_OPTIONS,
|
|
1745
|
+
addMark: BINARY_OPTIONS,
|
|
1746
|
+
layout: LAYOUT_OPTIONS,
|
|
1747
|
+
tableColor: TABLE_COLOR_OPTIONS,
|
|
1748
|
+
tableStyle: TABLE_STYLE_OPTIONS,
|
|
1749
|
+
advancedStyle: {
|
|
1750
|
+
option: '--style-json',
|
|
1751
|
+
fields: ['startIndex', 'directorySettings', 'contentSettings'],
|
|
1752
|
+
},
|
|
1753
|
+
pageMargins: PAGE_MARGIN_FIELDS.map(field => `--${toKebabCase(field.optionKeys[0])}`),
|
|
1754
|
+
themeColors: THEME_STYLE_SETTING_FIELDS.map(field => field.optionKeys.map(key => `--${toKebabCase(key)}`).join('/')),
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
return payload;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
function renderChoicesLines(payload) {
|
|
1761
|
+
const lines = [
|
|
1762
|
+
`choices: scope=${payload.scope}`,
|
|
1763
|
+
];
|
|
1764
|
+
if (payload.outline) {
|
|
1765
|
+
lines.push('outline:');
|
|
1766
|
+
lines.push(` page-scope: ${formatChoiceOptions(PAGE_SCOPE_OPTIONS)}`);
|
|
1767
|
+
lines.push(` base: ${formatChoiceOptions(BASE_OPTIONS)}`);
|
|
1768
|
+
lines.push(` theme-style: ${formatChoiceOptions(THEME_STYLE_OPTIONS)}`);
|
|
1769
|
+
lines.push(` table-quantity: ${formatChoiceOptions(TABLE_QUANTITY_OPTIONS)}`);
|
|
1770
|
+
lines.push(` table-color: ${formatChoiceOptions(TABLE_COLOR_OPTIONS)}`);
|
|
1771
|
+
lines.push(` table-style: ${formatChoiceOptions(TABLE_STYLE_OPTIONS)}`);
|
|
1772
|
+
lines.push(` mermaid-style: ${formatChoiceOptions(MERMAID_STYLE_OPTIONS)}`);
|
|
1773
|
+
lines.push(` plan-mode: ${formatChoiceOptions(PLAN_MODE_OPTIONS)}`);
|
|
1774
|
+
lines.push(' image toggles: --info-img, --scene-img, --online-img, --mermaid-img, --knowledge-img (0|1)');
|
|
1775
|
+
lines.push(' multi-bid override: --multi-bid-id <id>, --multi-bid-type <text>');
|
|
1776
|
+
lines.push(` epc-engineer-type: --epc-engineer-type <text>; aliases=${Object.keys(EPC_ENGINEER_TYPE_ALIAS_MAP).join(', ')}`);
|
|
1777
|
+
}
|
|
1778
|
+
if (payload.export) {
|
|
1779
|
+
lines.push('export:');
|
|
1780
|
+
lines.push(` template: ${formatChoiceOptions(EXPORT_TEMPLATE_OPTIONS)}`);
|
|
1781
|
+
lines.push(` layout: ${formatChoiceOptions(LAYOUT_OPTIONS)}`);
|
|
1782
|
+
lines.push(` table-color: ${formatChoiceOptions(TABLE_COLOR_OPTIONS)}`);
|
|
1783
|
+
lines.push(` table-style: ${formatChoiceOptions(TABLE_STYLE_OPTIONS)}`);
|
|
1784
|
+
lines.push(' binary toggles: --auto-number, --add-mark (0|1)');
|
|
1785
|
+
lines.push(' advanced style: --style-json <file> with startIndex, directorySettings, contentSettings');
|
|
1786
|
+
lines.push(` margins: ${PAGE_MARGIN_FIELDS.map(field => `--${toKebabCase(field.optionKeys[0])}`).join(', ')}`);
|
|
1787
|
+
lines.push(` theme colors: ${THEME_STYLE_SETTING_FIELDS.map(field => field.optionKeys.map(key => `--${toKebabCase(key)}`).join('/')).join(', ')}`);
|
|
1788
|
+
}
|
|
1789
|
+
return lines;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
function formatChoiceOptions(options) {
|
|
1793
|
+
return options
|
|
1794
|
+
.map(option => {
|
|
1795
|
+
const desc = option.description ? ` (${option.description})` : '';
|
|
1796
|
+
return `${option.value}=${option.label}${desc}`;
|
|
1797
|
+
})
|
|
1798
|
+
.join('; ');
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
async function collectWizardArgs(args, state) {
|
|
1802
|
+
return runPromptSession(args, async context => {
|
|
1803
|
+
const wizardArgs = {
|
|
1804
|
+
...args,
|
|
1805
|
+
file: stringOption(args, 'file'),
|
|
1806
|
+
out: stringOption(args, 'out'),
|
|
1807
|
+
};
|
|
1808
|
+
context.promptStream.write('\n\u6B22\u8FCE\u4F7F\u7528 xq-cli \u5168\u6D41\u7A0B\u5411\u5BFC\u3002\u8FD9\u4E2A\u5411\u5BFC\u4F1A\u4F9D\u6B21\u6267\u884C init -> outline -> write -> export\u3002\n');
|
|
1809
|
+
|
|
1810
|
+
const existingFiles = resolveFileArgs(wizardArgs);
|
|
1811
|
+
if (existingFiles.length > 0) {
|
|
1812
|
+
wizardArgs.file = existingFiles[0];
|
|
1813
|
+
} else {
|
|
1814
|
+
wizardArgs.file = await promptText(context, {
|
|
1815
|
+
title: '\n[1/3] \u9009\u62E9\u62DB\u6807\u6587\u4EF6',
|
|
1816
|
+
question: '\u8BF7\u8F93\u5165\u62DB\u6807\u6587\u4EF6\u8DEF\u5F84',
|
|
1817
|
+
required: true,
|
|
1818
|
+
transform: value => path.resolve(value),
|
|
1819
|
+
validate: value => fs.existsSync(value) ? null : `\u6587\u4EF6\u4E0D\u5B58\u5728: ${value}`,
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
const outlineDefaults = buildInteractiveSnapshotDefaults(wizardArgs);
|
|
1824
|
+
const outlineArgs = await collectOutlineInteractiveArgs(wizardArgs, outlineDefaults, context, {
|
|
1825
|
+
header: '\n[2/3] \u8BBE\u7F6E\u5927\u7EB2/\u6B63\u6587\u751F\u6210\u53C2\u6570',
|
|
1826
|
+
});
|
|
1827
|
+
Object.assign(wizardArgs, outlineArgs);
|
|
1828
|
+
|
|
1829
|
+
const exportSnapshot = {
|
|
1830
|
+
...outlineDefaults,
|
|
1831
|
+
...pickOutlineConfig(buildOutlineParams({
|
|
1832
|
+
...outlineDefaults,
|
|
1833
|
+
cid: 'wizard-preview',
|
|
1834
|
+
uuid: 'wizard-preview',
|
|
1835
|
+
title: 'wizard-preview',
|
|
1836
|
+
projectName: 'wizard-preview',
|
|
1837
|
+
}, outlineArgs)),
|
|
1838
|
+
};
|
|
1839
|
+
const exportArgs = await collectExportInteractiveArgs({
|
|
1840
|
+
...wizardArgs,
|
|
1841
|
+
out: stringOption(wizardArgs, 'out', buildWizardDefaultOutputPath()),
|
|
1842
|
+
}, exportSnapshot, context, {
|
|
1843
|
+
header: '\n[3/3] \u8BBE\u7F6E\u5BFC\u51FA\u53C2\u6570',
|
|
1844
|
+
});
|
|
1845
|
+
Object.assign(wizardArgs, exportArgs);
|
|
1846
|
+
|
|
1847
|
+
context.promptStream.write(`\n\u5C06\u8981\u6267\u884C: ${buildWizardSummary(wizardArgs)}\n`);
|
|
1848
|
+
const confirmed = await promptYesNo(context, '\u662F\u5426\u7ACB\u5373\u5F00\u59CB\u6267\u884C\u5168\u6D41\u7A0B', true);
|
|
1849
|
+
if (!confirmed) {
|
|
1850
|
+
throw new Error('Wizard cancelled by user.');
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
wizardArgs.wait = true;
|
|
1854
|
+
wizardArgs.interactive = false;
|
|
1855
|
+
wizardArgs._ = args._;
|
|
1856
|
+
return wizardArgs;
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
async function collectOutlineInteractiveArgs(args, snapshot, context = null, options = {}) {
|
|
1861
|
+
const promptFn = async promptContext => {
|
|
1862
|
+
const nextArgs = { ...args };
|
|
1863
|
+
const defaults = buildInteractiveSnapshotDefaults(snapshot);
|
|
1864
|
+
if (options.header) {
|
|
1865
|
+
promptContext.promptStream.write(`${options.header}\n`);
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
const pageScopeCode = await promptChoice(promptContext, {
|
|
1869
|
+
title: '\u5168\u6587\u7BC7\u5E45',
|
|
1870
|
+
question: '\u8BF7\u9009\u62E9\u7BC7\u5E45\u7F16\u53F7',
|
|
1871
|
+
options: PAGE_SCOPE_OPTIONS.map(option => ({
|
|
1872
|
+
...option,
|
|
1873
|
+
promptLabel: PAGE_SCOPE_PROMPT_LABELS[option.value] || option.label,
|
|
1874
|
+
})),
|
|
1875
|
+
defaultValue: enumOption(args, ['pageScope', 'pageScopeCode'], PAGE_SCOPE_OPTIONS, defaults.pageScopeCode),
|
|
1876
|
+
});
|
|
1877
|
+
nextArgs.pageScope = pageScopeCode;
|
|
1878
|
+
|
|
1879
|
+
if (pageScopeCode <= 0) {
|
|
1880
|
+
promptContext.promptStream.write('\u5DF2\u9009\u62E9\u77ED\u7BC7\u6A21\u5F0F\uFF0C\u5199\u4F5C\u6A21\u5F0F\u5C06\u56FA\u5B9A\u4E3A\u201C\u6807\u51C6\u5199\u4F5C\u201D\u3002\n');
|
|
1881
|
+
nextArgs.base = '0';
|
|
1882
|
+
} else {
|
|
1883
|
+
nextArgs.base = await promptChoice(promptContext, {
|
|
1884
|
+
title: '\u5199\u4F5C\u6A21\u5F0F',
|
|
1885
|
+
question: '\u8BF7\u9009\u62E9\u5199\u4F5C\u6A21\u5F0F',
|
|
1886
|
+
options: BASE_OPTIONS.map(option => ({
|
|
1887
|
+
...option,
|
|
1888
|
+
promptLabel: BASE_PROMPT_LABELS[option.value] || option.label,
|
|
1889
|
+
})),
|
|
1890
|
+
defaultValue: enumOption(args, 'base', BASE_OPTIONS, defaults.base),
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
nextArgs.themeStyle = await promptChoice(promptContext, {
|
|
1895
|
+
title: '\u98CE\u683C\u8BBE\u7F6E',
|
|
1896
|
+
question: '\u8BF7\u9009\u62E9\u98CE\u683C\u7F16\u53F7',
|
|
1897
|
+
options: THEME_STYLE_OPTIONS.map(option => ({
|
|
1898
|
+
...option,
|
|
1899
|
+
promptLabel: THEME_STYLE_PROMPT_LABELS[option.value] || option.label,
|
|
1900
|
+
})),
|
|
1901
|
+
defaultValue: enumOption(args, 'themeStyle', THEME_STYLE_OPTIONS, defaults.themeStyle),
|
|
1902
|
+
});
|
|
1903
|
+
nextArgs.tableQuantity = await promptChoice(promptContext, {
|
|
1904
|
+
title: '\u8868\u683C\u6570\u91CF',
|
|
1905
|
+
question: '\u8BF7\u9009\u62E9\u8868\u683C\u6570\u91CF',
|
|
1906
|
+
options: TABLE_QUANTITY_OPTIONS.map(option => ({
|
|
1907
|
+
...option,
|
|
1908
|
+
promptLabel: TABLE_QUANTITY_PROMPT_LABELS[option.value] || option.label,
|
|
1909
|
+
})),
|
|
1910
|
+
defaultValue: enumOption(args, 'tableQuantity', TABLE_QUANTITY_OPTIONS, defaults.tableQuantity),
|
|
1911
|
+
});
|
|
1912
|
+
nextArgs.tableColor = await promptChoice(promptContext, {
|
|
1913
|
+
title: '\u8868\u683C\u4E3B\u9898\u8272',
|
|
1914
|
+
question: '\u8BF7\u9009\u62E9\u8868\u683C\u989C\u8272',
|
|
1915
|
+
options: TABLE_COLOR_OPTIONS.map(option => ({
|
|
1916
|
+
...option,
|
|
1917
|
+
promptLabel: TABLE_COLOR_PROMPT_LABELS[option.value] || option.label,
|
|
1918
|
+
})),
|
|
1919
|
+
defaultValue: enumOption(args, 'tableColor', TABLE_COLOR_OPTIONS, defaults.tableColor),
|
|
1920
|
+
});
|
|
1921
|
+
nextArgs.tableStyle = await promptChoice(promptContext, {
|
|
1922
|
+
title: '\u8868\u683C\u6837\u5F0F',
|
|
1923
|
+
question: '\u8BF7\u9009\u62E9\u8868\u683C\u6837\u5F0F',
|
|
1924
|
+
options: TABLE_STYLE_OPTIONS.map(option => ({
|
|
1925
|
+
...option,
|
|
1926
|
+
promptLabel: TABLE_STYLE_PROMPT_LABELS[option.value] || option.label,
|
|
1927
|
+
})),
|
|
1928
|
+
defaultValue: enumOption(args, 'tableStyle', TABLE_STYLE_OPTIONS, defaults.tableStyle),
|
|
1929
|
+
});
|
|
1930
|
+
nextArgs.planMode = await promptChoice(promptContext, {
|
|
1931
|
+
title: '\u751F\u6210\u6A21\u5F0F',
|
|
1932
|
+
question: '\u8BF7\u9009\u62E9\u751F\u6210\u6A21\u5F0F',
|
|
1933
|
+
options: PLAN_MODE_OPTIONS.map(option => ({
|
|
1934
|
+
...option,
|
|
1935
|
+
promptLabel: PLAN_MODE_PROMPT_LABELS[option.value] || option.label,
|
|
1936
|
+
})),
|
|
1937
|
+
defaultValue: enumOption(args, 'planMode', PLAN_MODE_OPTIONS, defaults.planMode),
|
|
1938
|
+
});
|
|
1939
|
+
|
|
1940
|
+
const customizeImages = await promptYesNo(
|
|
1941
|
+
promptContext,
|
|
1942
|
+
'\u662F\u5426\u8981\u81EA\u5B9A\u4E49\u56FE\u7247/\u6D41\u7A0B\u56FE\u5F00\u5173',
|
|
1943
|
+
hasAnyOption(args, IMAGE_TOGGLE_PROMPT_OPTIONS.map(option => option.key)) || hasSnapshotImageOverrides(defaults),
|
|
1944
|
+
);
|
|
1945
|
+
if (customizeImages) {
|
|
1946
|
+
for (const option of IMAGE_TOGGLE_PROMPT_OPTIONS) {
|
|
1947
|
+
const currentValue = binaryOption(args, option.key, defaults[option.key] ?? option.defaultValue);
|
|
1948
|
+
nextArgs[option.key] = await promptBinaryChoice(promptContext, option.label, currentValue);
|
|
1949
|
+
}
|
|
1950
|
+
if (Number(nextArgs.mermaidImg) === 1) {
|
|
1951
|
+
nextArgs.mermaidStyle = await promptChoice(promptContext, {
|
|
1952
|
+
title: '\u6D41\u7A0B\u56FE\u989C\u8272',
|
|
1953
|
+
question: '\u8BF7\u9009\u62E9\u6D41\u7A0B\u56FE\u98CE\u683C',
|
|
1954
|
+
options: MERMAID_STYLE_OPTIONS.map(option => ({
|
|
1955
|
+
...option,
|
|
1956
|
+
promptLabel: MERMAID_STYLE_PROMPT_LABELS[option.value] || option.label,
|
|
1957
|
+
})),
|
|
1958
|
+
defaultValue: enumOption(args, 'mermaidStyle', MERMAID_STYLE_OPTIONS, defaults.mermaidStyle),
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
const customizeBidInfo = await promptYesNo(
|
|
1964
|
+
promptContext,
|
|
1965
|
+
'\u662F\u5426\u8981\u8986\u76D6\u591A\u6807\u6BB5 / EPC \u53C2\u6570',
|
|
1966
|
+
hasAnyOption(args, ['multiBidId', 'multiBidType', 'epcEngineerType']) || Boolean(defaults.multiBidId || defaults.multiBidType || defaults.epcEngineerType),
|
|
1967
|
+
);
|
|
1968
|
+
if (customizeBidInfo) {
|
|
1969
|
+
nextArgs.multiBidId = await promptText(promptContext, {
|
|
1970
|
+
title: '\u591A\u6807\u6BB5 ID',
|
|
1971
|
+
question: '\u8BF7\u8F93\u5165 multiBidId (\u53EF\u7559\u7A7A)',
|
|
1972
|
+
defaultValue: stringOption(args, 'multiBidId', defaults.multiBidId || ''),
|
|
1973
|
+
required: false,
|
|
1974
|
+
});
|
|
1975
|
+
nextArgs.multiBidType = await promptText(promptContext, {
|
|
1976
|
+
title: '\u591A\u6807\u6BB5\u7C7B\u578B',
|
|
1977
|
+
question: '\u8BF7\u8F93\u5165 multiBidType (\u53EF\u7559\u7A7A)',
|
|
1978
|
+
defaultValue: stringOption(args, 'multiBidType', defaults.multiBidType || ''),
|
|
1979
|
+
required: false,
|
|
1980
|
+
});
|
|
1981
|
+
nextArgs.epcEngineerType = await promptChoice(promptContext, {
|
|
1982
|
+
title: 'EPC \u7C7B\u578B',
|
|
1983
|
+
question: '\u8BF7\u9009\u62E9 EPC \u53C2\u6570',
|
|
1984
|
+
options: EPC_ENGINEER_PROMPT_OPTIONS,
|
|
1985
|
+
defaultValue: normalizeEpcEngineerType(stringOption(args, 'epcEngineerType', defaults.epcEngineerType || '')),
|
|
1986
|
+
allowCustomValue: true,
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
nextArgs.interactive = false;
|
|
1991
|
+
return nextArgs;
|
|
1992
|
+
};
|
|
1993
|
+
return context ? promptFn(context) : runPromptSession(args, promptFn);
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
async function collectExportInteractiveArgs(args, snapshot, context = null, options = {}) {
|
|
1997
|
+
const promptFn = async promptContext => {
|
|
1998
|
+
const nextArgs = { ...args };
|
|
1999
|
+
const defaults = buildInteractiveSnapshotDefaults(snapshot);
|
|
2000
|
+
if (options.header) {
|
|
2001
|
+
promptContext.promptStream.write(`${options.header}\n`);
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
nextArgs.template = await promptChoice(promptContext, {
|
|
2005
|
+
title: '\u5BFC\u51FA\u6A21\u677F',
|
|
2006
|
+
question: '\u8BF7\u9009\u62E9\u5BFC\u51FA\u6A21\u677F',
|
|
2007
|
+
options: EXPORT_TEMPLATE_OPTIONS.map(option => ({
|
|
2008
|
+
...option,
|
|
2009
|
+
promptLabel: `\u6A21\u677F${option.value}`,
|
|
2010
|
+
})),
|
|
2011
|
+
defaultValue: numberOption(args, 'template', DEFAULT_EXPORT_TEMPLATE_INDEX),
|
|
2012
|
+
});
|
|
2013
|
+
nextArgs.layout = await promptChoice(promptContext, {
|
|
2014
|
+
title: '\u4E0B\u8F7D\u6392\u7248',
|
|
2015
|
+
question: '\u8BF7\u9009\u62E9\u6392\u7248\u65B9\u5F0F',
|
|
2016
|
+
options: LAYOUT_OPTIONS.map(option => ({
|
|
2017
|
+
...option,
|
|
2018
|
+
promptLabel: LAYOUT_PROMPT_LABELS[option.value] || option.label,
|
|
2019
|
+
})),
|
|
2020
|
+
defaultValue: enumOption(args, 'layout', LAYOUT_OPTIONS, resolveExportLayout(defaults, args)),
|
|
2021
|
+
});
|
|
2022
|
+
nextArgs.tableColor = await promptChoice(promptContext, {
|
|
2023
|
+
title: '\u5BFC\u51FA\u8868\u683C\u989C\u8272',
|
|
2024
|
+
question: '\u8BF7\u9009\u62E9\u8868\u683C\u989C\u8272',
|
|
2025
|
+
options: TABLE_COLOR_OPTIONS.map(option => ({
|
|
2026
|
+
...option,
|
|
2027
|
+
promptLabel: TABLE_COLOR_PROMPT_LABELS[option.value] || option.label,
|
|
2028
|
+
})),
|
|
2029
|
+
defaultValue: enumOption(args, 'tableColor', TABLE_COLOR_OPTIONS, defaults.tableColor),
|
|
2030
|
+
});
|
|
2031
|
+
nextArgs.tableStyle = await promptChoice(promptContext, {
|
|
2032
|
+
title: '\u5BFC\u51FA\u8868\u683C\u6837\u5F0F',
|
|
2033
|
+
question: '\u8BF7\u9009\u62E9\u8868\u683C\u6837\u5F0F',
|
|
2034
|
+
options: TABLE_STYLE_OPTIONS.map(option => ({
|
|
2035
|
+
...option,
|
|
2036
|
+
promptLabel: TABLE_STYLE_PROMPT_LABELS[option.value] || option.label,
|
|
2037
|
+
})),
|
|
2038
|
+
defaultValue: enumOption(args, 'tableStyle', TABLE_STYLE_OPTIONS, defaults.tableStyle),
|
|
2039
|
+
});
|
|
2040
|
+
nextArgs.autoNumber = await promptBinaryChoice(
|
|
2041
|
+
promptContext,
|
|
2042
|
+
'Word \u81EA\u52A8\u7F16\u53F7',
|
|
2043
|
+
binaryOption(args, 'autoNumber', DEFAULT_AUTO_NUMBER),
|
|
2044
|
+
);
|
|
2045
|
+
nextArgs.addMark = await promptBinaryChoice(
|
|
2046
|
+
promptContext,
|
|
2047
|
+
'\u5EFA\u8BAE\u4FEE\u6539\u6807\u8BB0',
|
|
2048
|
+
binaryOption(args, 'addMark', DEFAULT_ADD_MARK),
|
|
2049
|
+
);
|
|
2050
|
+
|
|
2051
|
+
const customizeMargins = await promptYesNo(
|
|
2052
|
+
promptContext,
|
|
2053
|
+
'\u662F\u5426\u8981\u81EA\u5B9A\u4E49\u9875\u8FB9\u8DDD',
|
|
2054
|
+
hasAnyOption(args, PAGE_MARGIN_FIELDS.flatMap(field => field.optionKeys)),
|
|
2055
|
+
);
|
|
2056
|
+
if (customizeMargins) {
|
|
2057
|
+
for (const field of PAGE_MARGIN_FIELDS) {
|
|
2058
|
+
nextArgs[field.optionKeys[0]] = await promptNumber(promptContext, {
|
|
2059
|
+
title: `\u9875\u8FB9\u8DDD: ${field.key}`,
|
|
2060
|
+
question: `\u8BF7\u8F93\u5165 ${field.key} (\u5355\u4F4D cm)`,
|
|
2061
|
+
defaultValue: numberOptionAny(args, field.optionKeys, defaults.pageMarginSetting?.[field.key] ?? DEFAULT_PAGE_MARGIN_SETTING[field.key]),
|
|
2062
|
+
min: 0,
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
const useStyleJson = await promptYesNo(
|
|
2068
|
+
promptContext,
|
|
2069
|
+
'\u662F\u5426\u8981\u6302\u8F7D\u9AD8\u7EA7\u6837\u5F0F JSON',
|
|
2070
|
+
Boolean(stringOption(args, 'styleJson')),
|
|
2071
|
+
);
|
|
2072
|
+
if (useStyleJson) {
|
|
2073
|
+
nextArgs.styleJson = await promptText(promptContext, {
|
|
2074
|
+
title: '\u9AD8\u7EA7\u6837\u5F0F JSON',
|
|
2075
|
+
question: '\u8BF7\u8F93\u5165 style JSON \u8DEF\u5F84',
|
|
2076
|
+
defaultValue: stringOption(args, 'styleJson', ''),
|
|
2077
|
+
required: true,
|
|
2078
|
+
transform: value => path.resolve(value),
|
|
2079
|
+
validate: value => fs.existsSync(value) ? null : `\u6587\u4EF6\u4E0D\u5B58\u5728: ${value}`,
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
nextArgs.out = await promptText(promptContext, {
|
|
2084
|
+
title: '\u5BFC\u51FA\u8DEF\u5F84',
|
|
2085
|
+
question: '\u8BF7\u8F93\u5165\u5BFC\u51FA\u76EE\u5F55\u6216\u6587\u4EF6\u8DEF\u5F84',
|
|
2086
|
+
defaultValue: stringOption(args, 'out', buildWizardDefaultOutputPath()),
|
|
2087
|
+
required: true,
|
|
2088
|
+
transform: value => path.resolve(value),
|
|
2089
|
+
});
|
|
2090
|
+
|
|
2091
|
+
nextArgs.interactive = false;
|
|
2092
|
+
return nextArgs;
|
|
2093
|
+
};
|
|
2094
|
+
return context ? promptFn(context) : runPromptSession(args, promptFn);
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
function buildInteractiveSnapshotDefaults(source = {}) {
|
|
2098
|
+
return {
|
|
2099
|
+
pageScopeCode: source.pageScopeCode ?? DEFAULT_PAGE_SCOPE_CODE,
|
|
2100
|
+
base: source.base ?? DEFAULT_BASE,
|
|
2101
|
+
themeStyle: source.themeStyle ?? DEFAULT_THEME_STYLE,
|
|
2102
|
+
tableQuantity: source.tableQuantity ?? DEFAULT_TABLE_QUANTITY,
|
|
2103
|
+
tableColor: source.tableColor ?? DEFAULT_TABLE_COLOR,
|
|
2104
|
+
tableStyle: source.tableStyle ?? DEFAULT_TABLE_STYLE,
|
|
2105
|
+
infoImg: source.infoImg ?? DEFAULT_INFO_IMG,
|
|
2106
|
+
sceneImg: source.sceneImg ?? DEFAULT_SCENE_IMG,
|
|
2107
|
+
onlineImg: source.onlineImg ?? DEFAULT_ONLINE_IMG,
|
|
2108
|
+
mermaidImg: source.mermaidImg ?? DEFAULT_MERMAID_IMG,
|
|
2109
|
+
knowledgeImg: source.knowledgeImg ?? DEFAULT_KNOWLEDGE_IMG,
|
|
2110
|
+
mermaidStyle: source.mermaidStyle ?? DEFAULT_MERMAID_STYLE,
|
|
2111
|
+
planMode: source.planMode ?? DEFAULT_PLAN_MODE,
|
|
2112
|
+
multiBidId: source.multiBidId ?? '',
|
|
2113
|
+
multiBidType: source.multiBidType ?? '',
|
|
2114
|
+
epcEngineerType: source.epcEngineerType ?? '',
|
|
2115
|
+
pageMarginSetting: source.pageMarginSetting ?? DEFAULT_PAGE_MARGIN_SETTING,
|
|
2116
|
+
themeStyleSetting: source.themeStyleSetting ?? DEFAULT_THEME_STYLE_SETTING,
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
function hasSnapshotImageOverrides(snapshot) {
|
|
2121
|
+
return IMAGE_TOGGLE_PROMPT_OPTIONS.some(option => Number(snapshot?.[option.key] ?? option.defaultValue) !== Number(option.defaultValue))
|
|
2122
|
+
|| Number(snapshot?.mermaidStyle ?? DEFAULT_MERMAID_STYLE) !== DEFAULT_MERMAID_STYLE;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
function buildWizardDefaultOutputPath() {
|
|
2126
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '-').slice(0, 19);
|
|
2127
|
+
return path.join(process.cwd(), 'output', 'doc', `wizard-${stamp}`);
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
function buildWizardSummary(args) {
|
|
2131
|
+
const file = resolveFileArgs(args)[0];
|
|
2132
|
+
return [
|
|
2133
|
+
file ? `file=${path.basename(file)}` : null,
|
|
2134
|
+
`outline=${buildOutlineConfigSummary(buildOutlineParams({
|
|
2135
|
+
...buildInteractiveSnapshotDefaults(args),
|
|
2136
|
+
cid: 'wizard-preview',
|
|
2137
|
+
uuid: 'wizard-preview',
|
|
2138
|
+
title: 'wizard-preview',
|
|
2139
|
+
projectName: 'wizard-preview',
|
|
2140
|
+
}, args))}`,
|
|
2141
|
+
`export=template${numberOption(args, 'template', DEFAULT_EXPORT_TEMPLATE_INDEX)}, out=${stringOption(args, 'out', '-')}`,
|
|
2142
|
+
].filter(Boolean).join(', ');
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
function buildOutlineReferenceChoices(referenceData = {}) {
|
|
2146
|
+
return OUTLINE_REFERENCE_OPTIONS.map(option => {
|
|
2147
|
+
const nodes = ensureArray(referenceData[option.outlineKey]);
|
|
2148
|
+
return {
|
|
2149
|
+
value: option.value,
|
|
2150
|
+
label: option.label,
|
|
2151
|
+
shortLabel: option.shortLabel,
|
|
2152
|
+
recommended: option.recommended,
|
|
2153
|
+
rootChapters: nodes.length,
|
|
2154
|
+
preview: summarizeOutlinePreview(nodes),
|
|
2155
|
+
};
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
function summarizeOutlinePreview(nodes) {
|
|
2160
|
+
const titles = ensureArray(nodes)
|
|
2161
|
+
.map(node => node?.text || node?.title || '')
|
|
2162
|
+
.filter(Boolean)
|
|
2163
|
+
.slice(0, 3);
|
|
2164
|
+
return titles.join(' / ');
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
async function resolveOutlineReferenceSelection(args, referenceData) {
|
|
2168
|
+
const explicitRaw = stringOption(args, 'referenceType');
|
|
2169
|
+
if (explicitRaw !== undefined) {
|
|
2170
|
+
const explicit = parseOutlineReferenceType(explicitRaw);
|
|
2171
|
+
return {
|
|
2172
|
+
ready: true,
|
|
2173
|
+
referenceType: explicit,
|
|
2174
|
+
source: 'arg',
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
if (!canPromptForInput()) {
|
|
2179
|
+
return {
|
|
2180
|
+
ready: false,
|
|
2181
|
+
referenceType: null,
|
|
2182
|
+
source: 'unavailable',
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
const choices = buildOutlineReferenceChoices(referenceData);
|
|
2187
|
+
const promptStream = booleanOption(args, 'json') ? process.stderr : process.stdout;
|
|
2188
|
+
const rl = readline.createInterface({
|
|
2189
|
+
input: process.stdin,
|
|
2190
|
+
output: promptStream,
|
|
2191
|
+
});
|
|
2192
|
+
|
|
2193
|
+
try {
|
|
2194
|
+
promptStream.write('检测到需要确认“响应依据”后才能继续生成大纲。\n');
|
|
2195
|
+
for (const choice of choices) {
|
|
2196
|
+
const suffix = choice.recommended ? ' (推荐)' : '';
|
|
2197
|
+
const preview = choice.preview ? `;预览:${choice.preview}` : '';
|
|
2198
|
+
promptStream.write(` ${choice.value}) ${choice.label}${suffix};一级章节数=${choice.rootChapters}${preview}\n`);
|
|
2199
|
+
}
|
|
2200
|
+
promptStream.write('请输入你要采用的响应依据编号,CLI 会在你明确选择后继续。\n');
|
|
2201
|
+
while (true) {
|
|
2202
|
+
const answer = await rl.question('请选择响应依据 [0/1/2,输入 q 取消本次等待]: ');
|
|
2203
|
+
const normalized = answer.trim().toLowerCase();
|
|
2204
|
+
if (!normalized) {
|
|
2205
|
+
promptStream.write('尚未选择响应依据,请输入 0、1 或 2。\n');
|
|
2206
|
+
continue;
|
|
2207
|
+
}
|
|
2208
|
+
if (normalized === 'q' || normalized === 'quit' || normalized === 'exit') {
|
|
2209
|
+
return {
|
|
2210
|
+
ready: false,
|
|
2211
|
+
referenceType: null,
|
|
2212
|
+
source: 'prompt_cancelled',
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
const parsed = parseOutlineReferenceType(normalized);
|
|
2216
|
+
if (parsed !== null) {
|
|
2217
|
+
return {
|
|
2218
|
+
ready: true,
|
|
2219
|
+
referenceType: parsed,
|
|
2220
|
+
source: 'prompt',
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
promptStream.write('输入无效,请输入 0、1、2,或输入 q 取消。\n');
|
|
2224
|
+
}
|
|
2225
|
+
} finally {
|
|
2226
|
+
rl.close();
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
function canPromptForInput() {
|
|
2231
|
+
return Boolean(process.stdin.isTTY && (process.stdout.isTTY || process.stderr.isTTY));
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
function ensurePromptAvailable() {
|
|
2235
|
+
if (!canPromptForInput()) {
|
|
2236
|
+
throw new Error('Interactive prompts require a TTY terminal. Please rerun this command in an interactive terminal.');
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
async function runPromptSession(args, handler) {
|
|
2241
|
+
ensurePromptAvailable();
|
|
2242
|
+
const promptStream = booleanOption(args, 'json') ? process.stderr : process.stdout;
|
|
2243
|
+
const rl = readline.createInterface({
|
|
2244
|
+
input: process.stdin,
|
|
2245
|
+
output: promptStream,
|
|
2246
|
+
});
|
|
2247
|
+
try {
|
|
2248
|
+
return await handler({ rl, promptStream, args });
|
|
2249
|
+
} finally {
|
|
2250
|
+
rl.close();
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
async function promptChoice(context, config) {
|
|
2255
|
+
const options = config.options || [];
|
|
2256
|
+
if (options.length === 0) {
|
|
2257
|
+
throw new Error(`Prompt "${config.title || config.question}" has no options.`);
|
|
2258
|
+
}
|
|
2259
|
+
const defaultIndex = findPromptDefaultIndex(options, config.defaultValue);
|
|
2260
|
+
context.promptStream.write(`\n${config.title}\n`);
|
|
2261
|
+
options.forEach((option, index) => {
|
|
2262
|
+
const label = option.promptLabel || option.label || String(option.value);
|
|
2263
|
+
const desc = option.description ? ` - ${option.description}` : '';
|
|
2264
|
+
const suffix = defaultIndex === index ? ' [default]' : '';
|
|
2265
|
+
context.promptStream.write(` ${index + 1}) ${label}${desc}${suffix}\n`);
|
|
2266
|
+
});
|
|
2267
|
+
if (config.allowCustomValue) {
|
|
2268
|
+
context.promptStream.write(' You can also type a raw value directly.\n');
|
|
2269
|
+
}
|
|
2270
|
+
while (true) {
|
|
2271
|
+
const defaultHint = defaultIndex >= 0 ? ` [default ${defaultIndex + 1}]` : '';
|
|
2272
|
+
const answer = await context.rl.question(`${config.question}${defaultHint}: `);
|
|
2273
|
+
const normalized = answer.trim();
|
|
2274
|
+
if (!normalized) {
|
|
2275
|
+
if (defaultIndex >= 0) {
|
|
2276
|
+
return options[defaultIndex].value;
|
|
2277
|
+
}
|
|
2278
|
+
context.promptStream.write('Input required. Please choose one option.\n');
|
|
2279
|
+
continue;
|
|
2280
|
+
}
|
|
2281
|
+
if (isPromptCancelAnswer(normalized)) {
|
|
2282
|
+
throw new Error('Interactive prompt cancelled.');
|
|
2283
|
+
}
|
|
2284
|
+
const optionByIndex = tryPickPromptOptionByIndex(options, normalized);
|
|
2285
|
+
if (optionByIndex) {
|
|
2286
|
+
return optionByIndex.value;
|
|
2287
|
+
}
|
|
2288
|
+
const optionByAlias = tryPickPromptOptionByAlias(options, normalized);
|
|
2289
|
+
if (optionByAlias) {
|
|
2290
|
+
return optionByAlias.value;
|
|
2291
|
+
}
|
|
2292
|
+
if (config.allowCustomValue) {
|
|
2293
|
+
return normalized;
|
|
2294
|
+
}
|
|
2295
|
+
context.promptStream.write('Invalid choice. Please enter the option number shown above.\n');
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
async function promptYesNo(context, question, defaultValue) {
|
|
2300
|
+
const defaultHint = defaultValue ? ' [Y/n]' : ' [y/N]';
|
|
2301
|
+
while (true) {
|
|
2302
|
+
const answer = await context.rl.question(`${question}${defaultHint}: `);
|
|
2303
|
+
const normalized = answer.trim().toLowerCase();
|
|
2304
|
+
if (!normalized) {
|
|
2305
|
+
return defaultValue;
|
|
2306
|
+
}
|
|
2307
|
+
if (isPromptCancelAnswer(normalized)) {
|
|
2308
|
+
throw new Error('Interactive prompt cancelled.');
|
|
2309
|
+
}
|
|
2310
|
+
if (['y', 'yes', '1', 'true'].includes(normalized)) {
|
|
2311
|
+
return true;
|
|
2312
|
+
}
|
|
2313
|
+
if (['n', 'no', '0', 'false'].includes(normalized)) {
|
|
2314
|
+
return false;
|
|
2315
|
+
}
|
|
2316
|
+
context.promptStream.write('Please answer y or n.\n');
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
async function promptBinaryChoice(context, label, defaultValue) {
|
|
2321
|
+
const enabled = await promptYesNo(context, `${label}: enable?`, Number(defaultValue) === 1);
|
|
2322
|
+
return enabled ? 1 : 0;
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
async function promptText(context, config) {
|
|
2326
|
+
if (config.title) {
|
|
2327
|
+
context.promptStream.write(`\n${config.title}\n`);
|
|
2328
|
+
}
|
|
2329
|
+
while (true) {
|
|
2330
|
+
const defaultHint = config.defaultValue ? ` [default: ${config.defaultValue}]` : '';
|
|
2331
|
+
const answer = await context.rl.question(`${config.question}${defaultHint}: `);
|
|
2332
|
+
const normalized = answer.trim();
|
|
2333
|
+
if (!normalized) {
|
|
2334
|
+
if (config.defaultValue !== undefined) {
|
|
2335
|
+
return config.defaultValue;
|
|
2336
|
+
}
|
|
2337
|
+
if (!config.required) {
|
|
2338
|
+
return '';
|
|
2339
|
+
}
|
|
2340
|
+
context.promptStream.write('Input required.\n');
|
|
2341
|
+
continue;
|
|
2342
|
+
}
|
|
2343
|
+
if (isPromptCancelAnswer(normalized)) {
|
|
2344
|
+
throw new Error('Interactive prompt cancelled.');
|
|
2345
|
+
}
|
|
2346
|
+
const transformed = config.transform ? config.transform(normalized) : normalized;
|
|
2347
|
+
const validationMessage = config.validate ? config.validate(transformed) : null;
|
|
2348
|
+
if (validationMessage) {
|
|
2349
|
+
context.promptStream.write(`${validationMessage}\n`);
|
|
2350
|
+
continue;
|
|
2351
|
+
}
|
|
2352
|
+
return transformed;
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
async function promptNumber(context, config) {
|
|
2357
|
+
const value = await promptText(context, {
|
|
2358
|
+
...config,
|
|
2359
|
+
defaultValue: config.defaultValue !== undefined ? String(config.defaultValue) : undefined,
|
|
2360
|
+
validate: rawValue => {
|
|
2361
|
+
const parsed = Number(rawValue);
|
|
2362
|
+
if (!Number.isFinite(parsed)) {
|
|
2363
|
+
return 'Please enter a valid number.';
|
|
2364
|
+
}
|
|
2365
|
+
if (config.min !== undefined && parsed < config.min) {
|
|
2366
|
+
return `Number must be greater than or equal to ${config.min}.`;
|
|
2367
|
+
}
|
|
2368
|
+
if (config.max !== undefined && parsed > config.max) {
|
|
2369
|
+
return `Number must be less than or equal to ${config.max}.`;
|
|
2370
|
+
}
|
|
2371
|
+
return config.validate ? config.validate(parsed) : null;
|
|
2372
|
+
},
|
|
2373
|
+
transform: rawValue => Number(rawValue),
|
|
2374
|
+
});
|
|
2375
|
+
return value;
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
function findPromptDefaultIndex(options, defaultValue) {
|
|
2379
|
+
if (defaultValue === undefined || defaultValue === null || defaultValue === '') {
|
|
2380
|
+
return -1;
|
|
2381
|
+
}
|
|
2382
|
+
return options.findIndex(option => String(option.value) === String(defaultValue));
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
function tryPickPromptOptionByIndex(options, answer) {
|
|
2386
|
+
const parsed = Number(answer);
|
|
2387
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > options.length) {
|
|
2388
|
+
return null;
|
|
2389
|
+
}
|
|
2390
|
+
return options[parsed - 1];
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
function tryPickPromptOptionByAlias(options, answer) {
|
|
2394
|
+
const normalized = String(answer).trim().toLowerCase();
|
|
2395
|
+
return options.find(option => {
|
|
2396
|
+
const aliases = [option.value, option.label, ...(option.aliases || [])];
|
|
2397
|
+
return aliases.some(alias => String(alias).trim().toLowerCase() === normalized);
|
|
2398
|
+
}) || null;
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
function isPromptCancelAnswer(value) {
|
|
2402
|
+
const normalized = String(value).trim().toLowerCase();
|
|
2403
|
+
return normalized === 'q' || normalized === 'quit' || normalized === 'exit';
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
function hasAnyOption(args, keys) {
|
|
2407
|
+
return ensureArray(keys).some(key => args[key] !== undefined && args[key] !== null && args[key] !== '');
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
function countOutlineRoots(result) {
|
|
2411
|
+
return getOutlineRows(result?.outlineDetail).length;
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
function getOutlineRows(outlineDetail) {
|
|
2415
|
+
return Array.isArray(outlineDetail?.outLine) ? outlineDetail.outLine : [];
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
function hasOutlineThemes(outlineRows) {
|
|
2419
|
+
return outlineRows.length > 0 && outlineRows.every(item => String(item?.theme || '').trim().length > 0);
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
function isOutlineReadyForCli(outlineDetail) {
|
|
2423
|
+
const outlineRows = getOutlineRows(outlineDetail);
|
|
2424
|
+
if (outlineRows.length === 0) {
|
|
2425
|
+
return false;
|
|
2426
|
+
}
|
|
2427
|
+
return Boolean(outlineDetail?.outline_status) || hasOutlineThemes(outlineRows);
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
function normalizeDirectoryPortionStatus(value) {
|
|
2431
|
+
if (value === undefined || value === null || value === '') {
|
|
2432
|
+
return null;
|
|
2433
|
+
}
|
|
2434
|
+
const parsed = Number(value);
|
|
2435
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
function countDirectoryRoots(contentDetail) {
|
|
2439
|
+
return Array.isArray(contentDetail?.directoryNodes) ? contentDetail.directoryNodes.length : 0;
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
function isDirectoryReadyForWrite(contentDetail) {
|
|
2443
|
+
return normalizeDirectoryPortionStatus(contentDetail?.directory_portion_status) === DIRECTORY_PORTION_STATUS.SEQUEL_COMPLETED
|
|
2444
|
+
&& countDirectoryRoots(contentDetail) > 0;
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
function numberOrDefault(value, fallback) {
|
|
2448
|
+
const parsed = Number(value);
|
|
2449
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
function hasUnfinishedChapters(contentDetail) {
|
|
2453
|
+
const totalChapter = numberOrDefault(contentDetail?.totalChapter, 0);
|
|
2454
|
+
if (totalChapter <= 0) {
|
|
2455
|
+
return false;
|
|
2456
|
+
}
|
|
2457
|
+
const completedChapter = numberOrDefault(contentDetail?.completedChapter, 0);
|
|
2458
|
+
return completedChapter < totalChapter;
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
function isContentComplete(contentDetail) {
|
|
2462
|
+
const totalChapter = numberOrDefault(contentDetail?.totalChapter, 0);
|
|
2463
|
+
if (totalChapter > 0) {
|
|
2464
|
+
return !hasUnfinishedChapters(contentDetail);
|
|
2465
|
+
}
|
|
2466
|
+
const taskStatus = numberOrDefault(contentDetail?.task_status, -1);
|
|
2467
|
+
const contentPortionStatus = numberOrDefault(contentDetail?.content_portion_status, -1);
|
|
2468
|
+
return taskStatus === 5 || (taskStatus === 4 && contentPortionStatus === 1);
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
function shouldTriggerGenerateAllUnfinished(contentDetail) {
|
|
2472
|
+
if (!hasUnfinishedChapters(contentDetail)) {
|
|
2473
|
+
return false;
|
|
2474
|
+
}
|
|
2475
|
+
const percentage = numberOrDefault(contentDetail?.percentage, 0);
|
|
2476
|
+
const taskStatus = numberOrDefault(contentDetail?.task_status, -1);
|
|
2477
|
+
return percentage >= 100 || taskStatus === 4 || taskStatus === 5;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
function resolveAutoIgnoreIssue(issues, args, uuid, handledIssues) {
|
|
2481
|
+
for (const issue of issues) {
|
|
2482
|
+
const config = AUTO_IGNORE_ISSUE_CONFIG[issue];
|
|
2483
|
+
if (!config || handledIssues.has(issue)) {
|
|
2484
|
+
continue;
|
|
2485
|
+
}
|
|
2486
|
+
if (!booleanOption(args, config.optionKey)) {
|
|
2487
|
+
continue;
|
|
2488
|
+
}
|
|
2489
|
+
return {
|
|
2490
|
+
issue,
|
|
2491
|
+
payload: config.buildPayload(uuid),
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
return null;
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
function collectIssueHints(issues, args) {
|
|
2498
|
+
const hints = [];
|
|
2499
|
+
for (const issue of issues) {
|
|
2500
|
+
const config = AUTO_IGNORE_ISSUE_CONFIG[issue];
|
|
2501
|
+
if (!config || booleanOption(args, config.optionKey)) {
|
|
2502
|
+
continue;
|
|
2503
|
+
}
|
|
2504
|
+
hints.push(`hint: rerun with ${config.hint} to follow the frontend ignore/continue path for ${issue}.`);
|
|
2505
|
+
}
|
|
2506
|
+
return hints;
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
function saveLoginState(state, options) {
|
|
2510
|
+
const username = options.result.data?.user?.username || options.name || '';
|
|
2511
|
+
state.baseUrl = options.baseUrl;
|
|
2512
|
+
state.token = options.result.data.token;
|
|
2513
|
+
state.user = {
|
|
2514
|
+
name: options.name || username,
|
|
2515
|
+
username,
|
|
2516
|
+
userId: options.result.data?.user?.userId || '',
|
|
2517
|
+
sourceFrom: options.sourceFrom,
|
|
2518
|
+
loggedInAt: new Date().toISOString(),
|
|
2519
|
+
};
|
|
2520
|
+
writeState(state);
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
function buildBrowserAuthorizeUrl(baseUrl, redirectUri, state, sourceFrom) {
|
|
2524
|
+
const query = new URLSearchParams({
|
|
2525
|
+
redirect_uri: redirectUri,
|
|
2526
|
+
state,
|
|
2527
|
+
sourceFrom,
|
|
2528
|
+
});
|
|
2529
|
+
return `${baseUrl}/user/cli/authorize?${query.toString()}`;
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
async function startBrowserLoginServer() {
|
|
2533
|
+
let settled = false;
|
|
2534
|
+
let resolveResult;
|
|
2535
|
+
let rejectResult;
|
|
2536
|
+
const result = new Promise((resolve, reject) => {
|
|
2537
|
+
resolveResult = resolve;
|
|
2538
|
+
rejectResult = reject;
|
|
2539
|
+
});
|
|
2540
|
+
const server = http.createServer((request, response) => {
|
|
2541
|
+
const origin = server.address();
|
|
2542
|
+
const requestUrl = new URL(request.url || '/', `http://127.0.0.1:${origin?.port || 0}`);
|
|
2543
|
+
const pathname = requestUrl.pathname;
|
|
2544
|
+
|
|
2545
|
+
if (pathname === '/favicon.ico') {
|
|
2546
|
+
response.writeHead(204);
|
|
2547
|
+
response.end();
|
|
2548
|
+
return;
|
|
2549
|
+
}
|
|
2550
|
+
if (pathname !== BROWSER_LOGIN_CALLBACK_PATH) {
|
|
2551
|
+
response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
2552
|
+
response.end('Not found');
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
const code = requestUrl.searchParams.get('code');
|
|
2557
|
+
const state = requestUrl.searchParams.get('state');
|
|
2558
|
+
if (!code || !state) {
|
|
2559
|
+
response.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
2560
|
+
response.end(renderBrowserLoginResultPage('登录未完成', '缺少 code 或 state,返回 CLI 重新执行登录即可。'));
|
|
2561
|
+
if (!settled) {
|
|
2562
|
+
settled = true;
|
|
2563
|
+
rejectResult(new Error('Browser login callback missing code or state.'));
|
|
2564
|
+
}
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
2569
|
+
response.end(renderBrowserLoginResultPage('登录成功', '授权已完成,现在可以回到 CLI 继续使用了。'));
|
|
2570
|
+
if (!settled) {
|
|
2571
|
+
settled = true;
|
|
2572
|
+
resolveResult({ code, state });
|
|
2573
|
+
}
|
|
2574
|
+
});
|
|
2575
|
+
server.on('error', error => {
|
|
2576
|
+
if (!settled) {
|
|
2577
|
+
settled = true;
|
|
2578
|
+
rejectResult(error);
|
|
2579
|
+
}
|
|
2580
|
+
});
|
|
2581
|
+
|
|
2582
|
+
await new Promise((resolve, reject) => {
|
|
2583
|
+
server.once('error', reject);
|
|
2584
|
+
server.listen(0, '127.0.0.1', () => {
|
|
2585
|
+
server.off('error', reject);
|
|
2586
|
+
resolve();
|
|
2587
|
+
});
|
|
2588
|
+
});
|
|
2589
|
+
|
|
2590
|
+
const address = server.address();
|
|
2591
|
+
const redirectUri = `http://127.0.0.1:${address.port}${BROWSER_LOGIN_CALLBACK_PATH}`;
|
|
2592
|
+
return {
|
|
2593
|
+
redirectUri,
|
|
2594
|
+
result,
|
|
2595
|
+
close: () => closeServer(server),
|
|
2596
|
+
};
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
function renderBrowserLoginResultPage(title, message) {
|
|
2600
|
+
return `<!doctype html>
|
|
2601
|
+
<html lang="zh-CN">
|
|
2602
|
+
<head>
|
|
2603
|
+
<meta charset="UTF-8" />
|
|
2604
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
2605
|
+
<title>${escapeHtml(title)}</title>
|
|
2606
|
+
<style>
|
|
2607
|
+
body{font-family:Segoe UI,Microsoft YaHei,sans-serif;background:#f5f7fb;margin:0;padding:40px;color:#1f2937;}
|
|
2608
|
+
.card{max-width:420px;margin:0 auto;background:#fff;border-radius:16px;padding:28px;box-shadow:0 16px 48px rgba(15,23,42,.12);}
|
|
2609
|
+
h1{margin:0 0 12px;font-size:24px;}
|
|
2610
|
+
p{margin:0;color:#64748b;line-height:1.7;}
|
|
2611
|
+
</style>
|
|
2612
|
+
</head>
|
|
2613
|
+
<body>
|
|
2614
|
+
<div class="card">
|
|
2615
|
+
<h1>${escapeHtml(title)}</h1>
|
|
2616
|
+
<p>${escapeHtml(message)}</p>
|
|
2617
|
+
</div>
|
|
2618
|
+
</body>
|
|
2619
|
+
</html>`;
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
function escapeHtml(value) {
|
|
2623
|
+
return String(value ?? '')
|
|
2624
|
+
.replace(/&/g, '&')
|
|
2625
|
+
.replace(/</g, '<')
|
|
2626
|
+
.replace(/>/g, '>')
|
|
2627
|
+
.replace(/"/g, '"')
|
|
2628
|
+
.replace(/'/g, ''');
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
function openBrowser(url) {
|
|
2632
|
+
try {
|
|
2633
|
+
if (process.platform === 'win32') {
|
|
2634
|
+
const child = spawn('rundll32', ['url.dll,FileProtocolHandler', url], {
|
|
2635
|
+
detached: true,
|
|
2636
|
+
stdio: 'ignore',
|
|
2637
|
+
});
|
|
2638
|
+
child.once('error', () => {});
|
|
2639
|
+
child.unref();
|
|
2640
|
+
return true;
|
|
2641
|
+
}
|
|
2642
|
+
if (process.platform === 'darwin') {
|
|
2643
|
+
const child = spawn('open', [url], {
|
|
2644
|
+
detached: true,
|
|
2645
|
+
stdio: 'ignore',
|
|
2646
|
+
});
|
|
2647
|
+
child.once('error', () => {});
|
|
2648
|
+
child.unref();
|
|
2649
|
+
return true;
|
|
2650
|
+
}
|
|
2651
|
+
const child = spawn('xdg-open', [url], {
|
|
2652
|
+
detached: true,
|
|
2653
|
+
stdio: 'ignore',
|
|
2654
|
+
});
|
|
2655
|
+
child.once('error', () => {});
|
|
2656
|
+
child.unref();
|
|
2657
|
+
return true;
|
|
2658
|
+
} catch (error) {
|
|
2659
|
+
return false;
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
async function closeServer(server) {
|
|
2664
|
+
if (!server.listening) {
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
await new Promise((resolve, reject) => {
|
|
2668
|
+
server.close(error => {
|
|
2669
|
+
if (error) {
|
|
2670
|
+
reject(error);
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2673
|
+
resolve();
|
|
2674
|
+
});
|
|
2675
|
+
});
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
async function withTimeout(promise, timeoutMs, message) {
|
|
2679
|
+
let timer = null;
|
|
2680
|
+
try {
|
|
2681
|
+
return await Promise.race([
|
|
2682
|
+
promise,
|
|
2683
|
+
new Promise((_, reject) => {
|
|
2684
|
+
timer = setTimeout(() => {
|
|
2685
|
+
reject(new Error(message));
|
|
2686
|
+
}, timeoutMs);
|
|
2687
|
+
}),
|
|
2688
|
+
]);
|
|
2689
|
+
} finally {
|
|
2690
|
+
if (timer) {
|
|
2691
|
+
clearTimeout(timer);
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
function createAuthorizedClient(args, state) {
|
|
2697
|
+
const baseUrl = normalizeBaseUrl(stringOption(args, 'baseUrl', state.baseUrl));
|
|
2698
|
+
if (!baseUrl) {
|
|
2699
|
+
throw new Error('Missing base URL. Run login first or pass --base-url.');
|
|
2700
|
+
}
|
|
2701
|
+
if (!state.token) {
|
|
2702
|
+
throw new Error('Missing local token. Run login first.');
|
|
2703
|
+
}
|
|
2704
|
+
return createClient(baseUrl, state.token);
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
function createClient(baseUrl, token = '') {
|
|
2708
|
+
return axios.create({
|
|
2709
|
+
baseURL: baseUrl,
|
|
2710
|
+
timeout: 1_200_000,
|
|
2711
|
+
maxBodyLength: Infinity,
|
|
2712
|
+
maxContentLength: Infinity,
|
|
2713
|
+
httpAgent: HTTP_AGENT,
|
|
2714
|
+
httpsAgent: HTTPS_AGENT,
|
|
2715
|
+
headers: token
|
|
2716
|
+
? {
|
|
2717
|
+
Authorization: token,
|
|
2718
|
+
}
|
|
2719
|
+
: {},
|
|
2720
|
+
validateStatus: () => true,
|
|
2721
|
+
});
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
async function getJson(client, url, params = {}, options = {}) {
|
|
2725
|
+
const response = await requestWithRetry(async () => {
|
|
2726
|
+
return client.get(url, { params });
|
|
2727
|
+
}, options);
|
|
2728
|
+
return unwrapApiResponse(response);
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
async function postJson(client, url, data = {}) {
|
|
2732
|
+
const response = await client.post(url, data);
|
|
2733
|
+
return unwrapApiResponse(response);
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
async function postForm(client, url, formData) {
|
|
2737
|
+
const response = await client.post(url, formData, {
|
|
2738
|
+
headers: formData.getHeaders(),
|
|
2739
|
+
});
|
|
2740
|
+
return unwrapApiResponse(response);
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
async function postBinary(client, url, data = {}) {
|
|
2744
|
+
const response = await client.post(url, data, {
|
|
2745
|
+
responseType: 'arraybuffer',
|
|
2746
|
+
});
|
|
2747
|
+
const contentType = `${response.headers['content-type'] || ''}`.toLowerCase();
|
|
2748
|
+
if (contentType.includes('application/json')) {
|
|
2749
|
+
const json = JSON.parse(Buffer.from(response.data).toString('utf8'));
|
|
2750
|
+
throw new Error(json.message || 'Binary download failed.');
|
|
2751
|
+
}
|
|
2752
|
+
if (response.status >= 400) {
|
|
2753
|
+
throw new Error(`Download request failed with status ${response.status}.`);
|
|
2754
|
+
}
|
|
2755
|
+
return response;
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
async function requestWithRetry(requestFn, options = {}) {
|
|
2759
|
+
const retries = Number.isFinite(options.retries) ? options.retries : 0;
|
|
2760
|
+
let lastError = null;
|
|
2761
|
+
|
|
2762
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
2763
|
+
try {
|
|
2764
|
+
return await requestFn();
|
|
2765
|
+
} catch (error) {
|
|
2766
|
+
lastError = error;
|
|
2767
|
+
if (!isRetryableNetworkError(error) || attempt === retries) {
|
|
2768
|
+
throw error;
|
|
2769
|
+
}
|
|
2770
|
+
await sleep((attempt + 1) * 1_000);
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
throw lastError;
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
function isRetryableNetworkError(error) {
|
|
2778
|
+
const code = String(error?.code || '').toUpperCase();
|
|
2779
|
+
const message = String(error?.message || '');
|
|
2780
|
+
if (RETRYABLE_NETWORK_CODES.has(code)) {
|
|
2781
|
+
return true;
|
|
2782
|
+
}
|
|
2783
|
+
return /socket disconnected|econnreset|tls connection|network error|timeout/i.test(message);
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
function unwrapApiResponse(response) {
|
|
2787
|
+
const payload = response.data || {};
|
|
2788
|
+
if (response.status >= 400) {
|
|
2789
|
+
throw createApiError(payload.message || `Request failed with status ${response.status}.`, {
|
|
2790
|
+
code: payload.code,
|
|
2791
|
+
payload,
|
|
2792
|
+
response,
|
|
2793
|
+
});
|
|
2794
|
+
}
|
|
2795
|
+
if (payload.code !== 200) {
|
|
2796
|
+
throw createApiError(payload.message || `Request failed with business code ${payload.code}.`, {
|
|
2797
|
+
code: payload.code,
|
|
2798
|
+
payload,
|
|
2799
|
+
response,
|
|
2800
|
+
});
|
|
2801
|
+
}
|
|
2802
|
+
return payload;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
function createApiError(message, options = {}) {
|
|
2806
|
+
const error = new Error(message);
|
|
2807
|
+
error.code = options.code;
|
|
2808
|
+
error.payload = options.payload;
|
|
2809
|
+
error.status = options.response?.status;
|
|
2810
|
+
return error;
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
function resolveCid(args, state) {
|
|
2814
|
+
const cid = stringOption(args, 'cid', state.lastCid);
|
|
2815
|
+
if (!cid) {
|
|
2816
|
+
throw new Error('Missing cid. Pass --cid <cid> or run init first.');
|
|
2817
|
+
}
|
|
2818
|
+
return cid;
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
function resolveFileArgs(args) {
|
|
2822
|
+
const optionFiles = ensureArray(args.file);
|
|
2823
|
+
if (optionFiles.length > 0) {
|
|
2824
|
+
return optionFiles.map(item => path.resolve(item));
|
|
2825
|
+
}
|
|
2826
|
+
return args._.slice(1).map(item => path.resolve(item));
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
function resolveExportLayout(snapshot, args) {
|
|
2830
|
+
const explicit = enumOption(args, 'layout', LAYOUT_OPTIONS);
|
|
2831
|
+
if (explicit !== undefined) {
|
|
2832
|
+
return explicit;
|
|
2833
|
+
}
|
|
2834
|
+
switch (Number(snapshot.themeStyle)) {
|
|
2835
|
+
case 1:
|
|
2836
|
+
return 2;
|
|
2837
|
+
case 2:
|
|
2838
|
+
return 1;
|
|
2839
|
+
default:
|
|
2840
|
+
return 0;
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
function parseArgs(argv) {
|
|
2845
|
+
const result = { _: [] };
|
|
2846
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
2847
|
+
const token = argv[index];
|
|
2848
|
+
if (!token.startsWith('--')) {
|
|
2849
|
+
result._.push(token);
|
|
2850
|
+
continue;
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
const [rawKey, inlineValue] = token.slice(2).split('=');
|
|
2854
|
+
const key = normalizeOptionKey(rawKey);
|
|
2855
|
+
if (inlineValue !== undefined) {
|
|
2856
|
+
pushOption(result, key, inlineValue);
|
|
2857
|
+
continue;
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
const next = argv[index + 1];
|
|
2861
|
+
if (!next || next.startsWith('--')) {
|
|
2862
|
+
pushOption(result, key, true);
|
|
2863
|
+
continue;
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
pushOption(result, key, next);
|
|
2867
|
+
index += 1;
|
|
2868
|
+
}
|
|
2869
|
+
return result;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
function normalizeOptionKey(value) {
|
|
2873
|
+
return value.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
function pushOption(target, key, value) {
|
|
2877
|
+
if (target[key] === undefined) {
|
|
2878
|
+
target[key] = value;
|
|
2879
|
+
return;
|
|
2880
|
+
}
|
|
2881
|
+
if (Array.isArray(target[key])) {
|
|
2882
|
+
target[key].push(value);
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2885
|
+
target[key] = [target[key], value];
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
function ensureArray(value) {
|
|
2889
|
+
if (value === undefined || value === null || value === '') {
|
|
2890
|
+
return [];
|
|
2891
|
+
}
|
|
2892
|
+
return Array.isArray(value) ? value : [value];
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
function firstOptionKey(keys) {
|
|
2896
|
+
return Array.isArray(keys) ? keys[0] : keys;
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
function optionValue(args, keys, fallback = undefined) {
|
|
2900
|
+
for (const key of ensureArray(keys)) {
|
|
2901
|
+
const value = args[key];
|
|
2902
|
+
if (value === undefined || value === null || value === '') {
|
|
2903
|
+
continue;
|
|
2904
|
+
}
|
|
2905
|
+
return Array.isArray(value) ? value[0] : value;
|
|
2906
|
+
}
|
|
2907
|
+
return fallback;
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
function requiredOption(args, key) {
|
|
2911
|
+
const value = args[key];
|
|
2912
|
+
if (value === undefined || value === null || value === '') {
|
|
2913
|
+
throw new Error(`Missing required option: --${toKebabCase(key)}`);
|
|
2914
|
+
}
|
|
2915
|
+
return Array.isArray(value) ? value[0] : value;
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
function stringOption(args, key, fallback = undefined) {
|
|
2919
|
+
const value = args[key];
|
|
2920
|
+
if (value === undefined || value === null || value === '') {
|
|
2921
|
+
return fallback;
|
|
2922
|
+
}
|
|
2923
|
+
return Array.isArray(value) ? value[0] : value;
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
function stringOptionAny(args, keys, fallback = undefined) {
|
|
2927
|
+
const value = optionValue(args, keys, fallback);
|
|
2928
|
+
if (value === undefined || value === null || value === '') {
|
|
2929
|
+
return undefined;
|
|
2930
|
+
}
|
|
2931
|
+
return String(value);
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
function numberOption(args, key, fallback = undefined) {
|
|
2935
|
+
const value = stringOption(args, key, fallback);
|
|
2936
|
+
if (value === undefined || value === null || value === '') {
|
|
2937
|
+
return undefined;
|
|
2938
|
+
}
|
|
2939
|
+
const parsed = Number(value);
|
|
2940
|
+
if (Number.isNaN(parsed)) {
|
|
2941
|
+
throw new Error(`Option --${toKebabCase(key)} expects a number.`);
|
|
2942
|
+
}
|
|
2943
|
+
return parsed;
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
function numberOptionAny(args, keys, fallback = undefined) {
|
|
2947
|
+
const value = optionValue(args, keys, fallback);
|
|
2948
|
+
if (value === undefined || value === null || value === '') {
|
|
2949
|
+
return undefined;
|
|
2950
|
+
}
|
|
2951
|
+
const parsed = Number(value);
|
|
2952
|
+
if (Number.isNaN(parsed)) {
|
|
2953
|
+
throw new Error(`Option --${toKebabCase(firstOptionKey(keys))} expects a number.`);
|
|
2954
|
+
}
|
|
2955
|
+
return parsed;
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
function booleanOption(args, key, fallback = false) {
|
|
2959
|
+
const value = args[key];
|
|
2960
|
+
if (value === undefined) {
|
|
2961
|
+
return fallback;
|
|
2962
|
+
}
|
|
2963
|
+
if (typeof value === 'boolean') {
|
|
2964
|
+
return value;
|
|
2965
|
+
}
|
|
2966
|
+
const normalized = String(Array.isArray(value) ? value[0] : value).toLowerCase();
|
|
2967
|
+
if (['false', '0', 'no'].includes(normalized)) {
|
|
2968
|
+
return false;
|
|
2969
|
+
}
|
|
2970
|
+
return true;
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
function enumOption(args, keys, options, fallback = undefined) {
|
|
2974
|
+
const value = optionValue(args, keys, fallback);
|
|
2975
|
+
if (value === undefined || value === null || value === '') {
|
|
2976
|
+
return undefined;
|
|
2977
|
+
}
|
|
2978
|
+
const normalized = String(value).trim().toLowerCase();
|
|
2979
|
+
const match = options.find(option => {
|
|
2980
|
+
const aliases = [option.value, ...(option.aliases || [])];
|
|
2981
|
+
return aliases.some(alias => String(alias).trim().toLowerCase() === normalized);
|
|
2982
|
+
});
|
|
2983
|
+
if (!match) {
|
|
2984
|
+
throw new Error(`Option --${toKebabCase(firstOptionKey(keys))} expects one of: ${options.map(option => option.value).join(', ')}.`);
|
|
2985
|
+
}
|
|
2986
|
+
return match.value;
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
function binaryOption(args, keys, fallback = undefined) {
|
|
2990
|
+
return enumOption(args, keys, BINARY_OPTIONS, fallback);
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
function colorOption(args, keys, fallback = undefined) {
|
|
2994
|
+
const value = stringOptionAny(args, keys, fallback);
|
|
2995
|
+
if (value === undefined) {
|
|
2996
|
+
return undefined;
|
|
2997
|
+
}
|
|
2998
|
+
const normalized = normalizeHexColor(value);
|
|
2999
|
+
if (!normalized) {
|
|
3000
|
+
throw new Error(`Option --${toKebabCase(firstOptionKey(keys))} expects a 6-digit hex color like #1276C1.`);
|
|
3001
|
+
}
|
|
3002
|
+
return normalized;
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
function normalizeHexColor(value) {
|
|
3006
|
+
const trimmed = String(value).trim();
|
|
3007
|
+
if (!trimmed) {
|
|
3008
|
+
return '';
|
|
3009
|
+
}
|
|
3010
|
+
const withHash = trimmed.startsWith('#') ? trimmed : `#${trimmed}`;
|
|
3011
|
+
return /^#[0-9a-fA-F]{6}$/.test(withHash) ? withHash.toUpperCase() : '';
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
function buildThemeStyleSetting(snapshot, args) {
|
|
3015
|
+
const themeStyleSetting = {
|
|
3016
|
+
...DEFAULT_THEME_STYLE_SETTING,
|
|
3017
|
+
...(snapshot.themeStyleSetting || {}),
|
|
3018
|
+
};
|
|
3019
|
+
for (const field of THEME_STYLE_SETTING_FIELDS) {
|
|
3020
|
+
const color = colorOption(args, field.optionKeys);
|
|
3021
|
+
if (color !== undefined) {
|
|
3022
|
+
themeStyleSetting[field.key] = color;
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
return themeStyleSetting;
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
function buildPageMarginSetting(snapshot, args) {
|
|
3029
|
+
const pageMarginSetting = {
|
|
3030
|
+
...DEFAULT_PAGE_MARGIN_SETTING,
|
|
3031
|
+
...(snapshot.pageMarginSetting || {}),
|
|
3032
|
+
};
|
|
3033
|
+
for (const field of PAGE_MARGIN_FIELDS) {
|
|
3034
|
+
const margin = numberOptionAny(args, field.optionKeys);
|
|
3035
|
+
if (margin === undefined) {
|
|
3036
|
+
continue;
|
|
3037
|
+
}
|
|
3038
|
+
if (!Number.isFinite(margin) || margin < 0) {
|
|
3039
|
+
throw new Error(`Option --${toKebabCase(field.optionKeys[0])} expects a non-negative number.`);
|
|
3040
|
+
}
|
|
3041
|
+
pageMarginSetting[field.key] = margin;
|
|
3042
|
+
}
|
|
3043
|
+
return pageMarginSetting;
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
function buildTemplatePostData(template, args) {
|
|
3047
|
+
const styleJsonPath = stringOption(args, 'styleJson');
|
|
3048
|
+
let styleJson = {};
|
|
3049
|
+
if (styleJsonPath) {
|
|
3050
|
+
const resolvedPath = path.resolve(styleJsonPath);
|
|
3051
|
+
ensureFileExists(resolvedPath);
|
|
3052
|
+
try {
|
|
3053
|
+
styleJson = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
|
|
3054
|
+
} catch (error) {
|
|
3055
|
+
throw new Error(`Failed to parse style JSON: ${resolvedPath}`);
|
|
3056
|
+
}
|
|
3057
|
+
if (!styleJson || Array.isArray(styleJson) || typeof styleJson !== 'object') {
|
|
3058
|
+
throw new Error('Option --style-json expects a JSON object.');
|
|
3059
|
+
}
|
|
3060
|
+
if (styleJson.directorySettings !== undefined && !Array.isArray(styleJson.directorySettings)) {
|
|
3061
|
+
throw new Error('Option --style-json expects directorySettings to be an array.');
|
|
3062
|
+
}
|
|
3063
|
+
if (styleJson.contentSettings !== undefined && (Array.isArray(styleJson.contentSettings) || typeof styleJson.contentSettings !== 'object' || styleJson.contentSettings === null)) {
|
|
3064
|
+
throw new Error('Option --style-json expects contentSettings to be an object.');
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
const startIndex = numberOption(args, 'startIndex', styleJson.startIndex ?? template.postData.startIndex ?? 1);
|
|
3068
|
+
if (!Number.isInteger(startIndex) || startIndex < 1) {
|
|
3069
|
+
throw new Error('Option --start-index expects an integer greater than or equal to 1.');
|
|
3070
|
+
}
|
|
3071
|
+
return {
|
|
3072
|
+
...template.postData,
|
|
3073
|
+
...compactObject({
|
|
3074
|
+
directorySettings: styleJson.directorySettings,
|
|
3075
|
+
contentSettings: styleJson.contentSettings,
|
|
3076
|
+
}),
|
|
3077
|
+
startIndex,
|
|
3078
|
+
};
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
function normalizeEpcEngineerType(value) {
|
|
3082
|
+
if (value === undefined || value === null || value === '') {
|
|
3083
|
+
return undefined;
|
|
3084
|
+
}
|
|
3085
|
+
const normalized = String(value).trim();
|
|
3086
|
+
const alias = EPC_ENGINEER_TYPE_ALIAS_MAP[normalized.toLowerCase()];
|
|
3087
|
+
return alias || normalized;
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
function normalizeEpcEngineerTypeForProxy(value) {
|
|
3091
|
+
if (value === '\u975EEPC2') {
|
|
3092
|
+
return '\u975EEPC';
|
|
3093
|
+
}
|
|
3094
|
+
return value;
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
function describeEnumValue(options, value) {
|
|
3098
|
+
const match = options.find(option => option.value === value);
|
|
3099
|
+
return match ? `${match.value}:${match.label}` : String(value);
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
function pickOutlineConfig(params) {
|
|
3103
|
+
return compactObject({
|
|
3104
|
+
pageScopeCode: params.pageScopeCode,
|
|
3105
|
+
base: params.base,
|
|
3106
|
+
themeStyle: params.themeStyle,
|
|
3107
|
+
tableQuantity: params.tableQuantity,
|
|
3108
|
+
tableColor: params.tableColor,
|
|
3109
|
+
tableStyle: params.tableStyle,
|
|
3110
|
+
infoImg: params.infoImg,
|
|
3111
|
+
sceneImg: params.sceneImg,
|
|
3112
|
+
onlineImg: params.onlineImg,
|
|
3113
|
+
mermaidImg: params.mermaidImg,
|
|
3114
|
+
knowledgeImg: params.knowledgeImg,
|
|
3115
|
+
mermaidStyle: params.mermaidStyle,
|
|
3116
|
+
planMode: params.planMode,
|
|
3117
|
+
multiBidId: params.multiBidId,
|
|
3118
|
+
multiBidType: params.multiBidType,
|
|
3119
|
+
epcEngineerType: params.epcEngineerType,
|
|
3120
|
+
});
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
function buildOutlineConfigSummary(params) {
|
|
3124
|
+
return [
|
|
3125
|
+
`pageScope=${describeEnumValue(PAGE_SCOPE_OPTIONS, params.pageScopeCode)}`,
|
|
3126
|
+
`base=${describeEnumValue(BASE_OPTIONS, params.base)}`,
|
|
3127
|
+
params.themeStyle !== undefined ? `themeStyle=${describeEnumValue(THEME_STYLE_OPTIONS, params.themeStyle)}` : null,
|
|
3128
|
+
params.tableQuantity !== undefined ? `tableQuantity=${describeEnumValue(TABLE_QUANTITY_OPTIONS, params.tableQuantity)}` : null,
|
|
3129
|
+
params.tableColor !== undefined ? `tableColor=${describeEnumValue(TABLE_COLOR_OPTIONS, params.tableColor)}` : null,
|
|
3130
|
+
params.tableStyle !== undefined ? `tableStyle=${describeEnumValue(TABLE_STYLE_OPTIONS, params.tableStyle)}` : null,
|
|
3131
|
+
`planMode=${describeEnumValue(PLAN_MODE_OPTIONS, params.planMode)}`,
|
|
3132
|
+
].filter(Boolean).join(', ');
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
function pickExportConfig(requestData, templateIndex) {
|
|
3136
|
+
return {
|
|
3137
|
+
template: templateIndex,
|
|
3138
|
+
startIndex: requestData.startIndex,
|
|
3139
|
+
autoNumber: requestData.autoNumber,
|
|
3140
|
+
addMark: requestData.addMark,
|
|
3141
|
+
layout: requestData.layout,
|
|
3142
|
+
tableColor: requestData.tableSettings.tableColor,
|
|
3143
|
+
tableStyle: requestData.tableSettings.tableStyle,
|
|
3144
|
+
directorySettings: requestData.directorySettings,
|
|
3145
|
+
contentSettings: requestData.contentSettings,
|
|
3146
|
+
pageMarginSetting: requestData.pageMarginSetting,
|
|
3147
|
+
themeStyleSetting: requestData.themeStyleSetting,
|
|
3148
|
+
};
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
function buildExportConfigSummary(config) {
|
|
3152
|
+
const margins = config.pageMarginSetting;
|
|
3153
|
+
return [
|
|
3154
|
+
`template=${config.template}`,
|
|
3155
|
+
`startIndex=${config.startIndex}`,
|
|
3156
|
+
`layout=${describeEnumValue(LAYOUT_OPTIONS, config.layout)}`,
|
|
3157
|
+
`tableColor=${describeEnumValue(TABLE_COLOR_OPTIONS, config.tableColor)}`,
|
|
3158
|
+
`tableStyle=${describeEnumValue(TABLE_STYLE_OPTIONS, config.tableStyle)}`,
|
|
3159
|
+
`autoNumber=${config.autoNumber}`,
|
|
3160
|
+
`addMark=${config.addMark}`,
|
|
3161
|
+
`margins=${margins.topMargin}/${margins.bottomMargin}/${margins.leftMargin}/${margins.rightMargin}`,
|
|
3162
|
+
].join(', ');
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
function splitCsvOption(value) {
|
|
3166
|
+
if (value === undefined || value === null || value === '') {
|
|
3167
|
+
return [];
|
|
3168
|
+
}
|
|
3169
|
+
const items = ensureArray(value)
|
|
3170
|
+
.flatMap(item => String(item).split(','))
|
|
3171
|
+
.map(item => item.trim())
|
|
3172
|
+
.filter(Boolean);
|
|
3173
|
+
return [...new Set(items)];
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
function appendOptionalField(formData, key, value) {
|
|
3177
|
+
if (value === undefined || value === null || value === '') {
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3180
|
+
formData.append(key, String(value));
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
function ensureFileExists(filePath) {
|
|
3184
|
+
if (!fs.existsSync(filePath)) {
|
|
3185
|
+
throw new Error(`File not found: ${filePath}`);
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
function normalizeBaseUrl(value) {
|
|
3190
|
+
if (!value) {
|
|
3191
|
+
return '';
|
|
3192
|
+
}
|
|
3193
|
+
return String(value).replace(/\/+$/, '');
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
function readState() {
|
|
3197
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
3198
|
+
return {
|
|
3199
|
+
baseUrl: '',
|
|
3200
|
+
token: '',
|
|
3201
|
+
user: {},
|
|
3202
|
+
lastCid: '',
|
|
3203
|
+
tasks: {},
|
|
3204
|
+
};
|
|
3205
|
+
}
|
|
3206
|
+
try {
|
|
3207
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
3208
|
+
} catch (error) {
|
|
3209
|
+
return {
|
|
3210
|
+
baseUrl: '',
|
|
3211
|
+
token: '',
|
|
3212
|
+
user: {},
|
|
3213
|
+
lastCid: '',
|
|
3214
|
+
tasks: {},
|
|
3215
|
+
};
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
function writeState(state) {
|
|
3220
|
+
fs.mkdirSync(APP_DIR, { recursive: true });
|
|
3221
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(state, null, 2), 'utf8');
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
function saveTaskSnapshot(state, cid, patch) {
|
|
3225
|
+
const current = state.tasks?.[cid] || {};
|
|
3226
|
+
state.tasks = state.tasks || {};
|
|
3227
|
+
state.tasks[cid] = compactObject({
|
|
3228
|
+
...current,
|
|
3229
|
+
...patch,
|
|
3230
|
+
cid,
|
|
3231
|
+
updatedAt: new Date().toISOString(),
|
|
3232
|
+
});
|
|
3233
|
+
state.lastCid = cid;
|
|
3234
|
+
writeState(state);
|
|
3235
|
+
return state.tasks[cid];
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
function pickTaskSnapshot(source) {
|
|
3239
|
+
return compactObject({
|
|
3240
|
+
cid: source.cid,
|
|
3241
|
+
uuid: source.uuid,
|
|
3242
|
+
title: source.title,
|
|
3243
|
+
projectName: source.projectName,
|
|
3244
|
+
requirement: source.requirement,
|
|
3245
|
+
rating: source.rating,
|
|
3246
|
+
plan: source.plan,
|
|
3247
|
+
base: source.base,
|
|
3248
|
+
pageScopeCode: source.pageScopeCode,
|
|
3249
|
+
task_status: source.task_status,
|
|
3250
|
+
viewStep: source.viewStep,
|
|
3251
|
+
percentage: source.percentage,
|
|
3252
|
+
content_portion_status: source.content_portion_status,
|
|
3253
|
+
directory_portion_status: source.directory_portion_status,
|
|
3254
|
+
annexIds: source.annexIds,
|
|
3255
|
+
knowledgeProductIds: source.knowledgeProductIds,
|
|
3256
|
+
sourceFrom: source.sourceFrom,
|
|
3257
|
+
localFiles: source.localFiles,
|
|
3258
|
+
fileTypes: source.fileTypes,
|
|
3259
|
+
filePurposes: source.filePurposes,
|
|
3260
|
+
originType: source.originType,
|
|
3261
|
+
infoImg: source.infoImg,
|
|
3262
|
+
sceneImg: source.sceneImg,
|
|
3263
|
+
onlineImg: source.onlineImg,
|
|
3264
|
+
mermaidImg: source.mermaidImg,
|
|
3265
|
+
knowledgeImg: source.knowledgeImg,
|
|
3266
|
+
mermaidStyle: source.mermaidStyle,
|
|
3267
|
+
themeStyle: source.themeStyle,
|
|
3268
|
+
tableColor: source.tableColor,
|
|
3269
|
+
tableStyle: source.tableStyle,
|
|
3270
|
+
tableQuantity: source.tableQuantity,
|
|
3271
|
+
planMode: source.planMode,
|
|
3272
|
+
multiBidId: source.multiBidId,
|
|
3273
|
+
multiBidType: source.multiBidType,
|
|
3274
|
+
epcEngineerType: source.epcEngineerType,
|
|
3275
|
+
outlineReferenceType: source.outlineReferenceType,
|
|
3276
|
+
referenceStatus: source.referenceStatus,
|
|
3277
|
+
themeStyleSetting: source.themeStyleSetting,
|
|
3278
|
+
pageMarginSetting: source.pageMarginSetting,
|
|
3279
|
+
outlineTriggeredAt: source.outlineTriggeredAt,
|
|
3280
|
+
triggerMode: source.triggerMode,
|
|
3281
|
+
writeTriggeredAt: source.writeTriggeredAt,
|
|
3282
|
+
});
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
function parseFileName(contentDisposition) {
|
|
3286
|
+
if (!contentDisposition) {
|
|
3287
|
+
return '';
|
|
3288
|
+
}
|
|
3289
|
+
const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i);
|
|
3290
|
+
if (utf8Match) {
|
|
3291
|
+
return normalizeDownloadFileName(decodeFileNameSafe(utf8Match[1]));
|
|
3292
|
+
}
|
|
3293
|
+
const plainMatch = contentDisposition.match(/filename="?([^";]+)"?/i);
|
|
3294
|
+
if (plainMatch) {
|
|
3295
|
+
return normalizeDownloadFileName(decodeFileNameSafe(plainMatch[1]));
|
|
3296
|
+
}
|
|
3297
|
+
return '';
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
function decodeFileNameSafe(value) {
|
|
3301
|
+
try {
|
|
3302
|
+
return decodeURIComponent(value);
|
|
3303
|
+
} catch (error) {
|
|
3304
|
+
return value;
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
function normalizeDownloadFileName(value) {
|
|
3309
|
+
return String(value).replace(/[<>:"/\\|?*\u0000-\u001F]/g, '_').trim();
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
function writeDownloadFile(outputOption, fileName, fileContent) {
|
|
3313
|
+
const resolvedOutput = outputOption ? path.resolve(outputOption) : process.cwd();
|
|
3314
|
+
const outputIsDirectory = !path.extname(resolvedOutput);
|
|
3315
|
+
const finalPath = outputIsDirectory ? path.join(resolvedOutput, fileName) : resolvedOutput;
|
|
3316
|
+
fs.mkdirSync(path.dirname(finalPath), { recursive: true });
|
|
3317
|
+
fs.writeFileSync(finalPath, Buffer.from(fileContent));
|
|
3318
|
+
return finalPath;
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
function compactObject(value) {
|
|
3322
|
+
const result = {};
|
|
3323
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
3324
|
+
if (entry === undefined) {
|
|
3325
|
+
continue;
|
|
3326
|
+
}
|
|
3327
|
+
result[key] = entry;
|
|
3328
|
+
}
|
|
3329
|
+
return result;
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
function sleep(ms) {
|
|
3333
|
+
return new Promise(resolve => {
|
|
3334
|
+
setTimeout(resolve, ms);
|
|
3335
|
+
});
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
function secondsToMs(value) {
|
|
3339
|
+
return Number(value) * 1000;
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3342
|
+
function toKebabCase(value) {
|
|
3343
|
+
return value.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`);
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
main().catch(error => {
|
|
3347
|
+
console.error(error.message || String(error));
|
|
3348
|
+
process.exit(1);
|
|
3349
|
+
});
|