@guanwenai/high-tech-project-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.
@@ -0,0 +1,252 @@
1
+ import { createWriteStream } from 'node:fs';
2
+ import { mkdir, readFile, rename, rm } from 'node:fs/promises';
3
+ import { basename, dirname, extname, isAbsolute } from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { Readable, Transform } from 'node:stream';
6
+ import { pipeline } from 'node:stream/promises';
7
+
8
+ import { CliError } from '../core/errors.mjs';
9
+ import { resolveWithinBase } from '../core/files.mjs';
10
+ import {
11
+ objectInput as sharedObjectInput,
12
+ positiveId,
13
+ valueOf,
14
+ withJavaMethod,
15
+ } from './shared.mjs';
16
+
17
+ const REQUIRED_CONTEXT = ['gatewayUrl', 'uuid'];
18
+ const ROUTES = {
19
+ supplement: {
20
+ list: '/api/project-writing/supplement-file/list',
21
+ save: '/api/project-writing/supplement-file/update',
22
+ delete: '/api/project-writing/supplement-file/delete-batch',
23
+ },
24
+ signature: {
25
+ list: '/api/project-writing/signature-file/list',
26
+ save: '/api/project-writing/signature-file/create',
27
+ delete: '/api/project-writing/signature-file/delete-batch',
28
+ },
29
+ system: {
30
+ list: '/api/project-writing/system-manage-file/list',
31
+ save: '/api/project-writing/system-manage-file/create',
32
+ delete: '/api/project-writing/system-manage-file/delete-batch',
33
+ },
34
+ };
35
+
36
+ function objectInput(input) {
37
+ return sharedObjectInput(input);
38
+ }
39
+
40
+ function kindInput(input) {
41
+ const value = objectInput(input);
42
+ if (!Object.hasOwn(ROUTES, value.kind)) {
43
+ throw new CliError('ARGUMENT_INVALID', 'kind 必须是 supplement、signature 或 system');
44
+ }
45
+ return value;
46
+ }
47
+
48
+ function idArray(value, field) {
49
+ if (!Array.isArray(value) || value.length === 0
50
+ || value.some((id) => !Number.isSafeInteger(id) || id <= 0)) {
51
+ throw new CliError('ARGUMENT_INVALID', `${field} 必须是非空正整数数组`);
52
+ }
53
+ }
54
+
55
+ function deleteInput(input) {
56
+ const value = kindInput(input);
57
+ idArray(value.ids, 'ids');
58
+ return value;
59
+ }
60
+
61
+ function safeRelativePath(value, field) {
62
+ if (typeof value !== 'string' || !value.trim() || isAbsolute(value)
63
+ || value.split(/[\\/]/u).includes('..')) {
64
+ throw new CliError('ARGUMENT_INVALID', `${field} 必须是工作目录内的相对路径`);
65
+ }
66
+ }
67
+
68
+ function uploadInput(input) {
69
+ const value = kindInput(input);
70
+ safeRelativePath(value.file, 'file');
71
+ if (typeof value.fileCode !== 'string' || !value.fileCode.trim()) {
72
+ throw new CliError('ARGUMENT_INVALID', '上传材料需要 fileCode');
73
+ }
74
+ if (value.kind === 'signature' && (typeof value.bizType !== 'string' || !value.bizType.trim())) {
75
+ throw new CliError('ARGUMENT_INVALID', '签章材料需要 bizType');
76
+ }
77
+ return value;
78
+ }
79
+
80
+ function generateInput(input) {
81
+ const value = kindInput(input);
82
+ if (value.kind !== 'system') {
83
+ throw new CliError('ARGUMENT_INVALID', '仅 system 材料支持生成递交文件');
84
+ }
85
+ idArray(value.fileIds, 'fileIds');
86
+ if (typeof value.fileCode !== 'string' || !value.fileCode.trim()) {
87
+ throw new CliError('ARGUMENT_INVALID', '生成材料需要 fileCode');
88
+ }
89
+ return value;
90
+ }
91
+
92
+ function downloadInput(input) {
93
+ const value = objectInput(input);
94
+ if (typeof value.fileUrl !== 'string' || !value.fileUrl.trim()
95
+ || typeof value.fileName !== 'string' || !value.fileName.trim()) {
96
+ throw new CliError('ARGUMENT_INVALID', '下载需要 fileUrl 和 fileName');
97
+ }
98
+ safeRelativePath(value.target, 'target');
99
+ return value;
100
+ }
101
+
102
+ export async function downloadAtomically({ client, input, baseDir }) {
103
+ if (typeof baseDir !== 'string' || !baseDir) {
104
+ throw new CliError('CONFIG_MISSING', '文件下载缺少 baseDir');
105
+ }
106
+ const resolvedPath = resolveWithinBase(input.target, baseDir);
107
+ const targetDirectory = dirname(resolvedPath);
108
+ await mkdir(targetDirectory, { recursive: true });
109
+ const temporaryPath = `${resolvedPath}.${randomUUID()}.tmp`;
110
+ const response = await client.download({
111
+ path: '/api/project-writing/file/download',
112
+ method: 'POST',
113
+ body: { fileUrl: input.fileUrl, fileName: input.fileName },
114
+ });
115
+ if (!response?.body || typeof response.body.getReader !== 'function') {
116
+ throw new CliError('FILE_WRITE_FAILED', '下载响应缺少文件流', {
117
+ relativePath: input.target, baseDir, resolvedPath,
118
+ });
119
+ }
120
+
121
+ let bytesWritten = 0;
122
+ const counter = new Transform({
123
+ transform(chunk, _encoding, callback) {
124
+ bytesWritten += chunk.length;
125
+ callback(null, chunk);
126
+ },
127
+ });
128
+ try {
129
+ await pipeline(
130
+ Readable.fromWeb(response.body),
131
+ counter,
132
+ createWriteStream(temporaryPath, { flags: 'wx' }),
133
+ );
134
+ await rename(temporaryPath, resolvedPath);
135
+ } catch (error) {
136
+ await rm(temporaryPath, { force: true }).catch(() => undefined);
137
+ throw new CliError('FILE_WRITE_FAILED', '文件下载或写入失败', {
138
+ relativePath: input.target,
139
+ baseDir,
140
+ resolvedPath,
141
+ cause: error instanceof Error ? error.message : String(error),
142
+ });
143
+ }
144
+ return { relativePath: input.target, bytesWritten };
145
+ }
146
+
147
+ export const commands = [
148
+ {
149
+ name: 'material.list', method: 'POST', path: ROUTES.supplement.list,
150
+ idempotent: true, multipart: false, requiredContext: REQUIRED_CONTEXT, validate: kindInput,
151
+ handler: async ({ client, context, input }) => {
152
+ const { kind, ...filters } = input;
153
+ return await client.request({
154
+ method: 'POST', path: ROUTES[kind].list, idempotent: true,
155
+ body: { uuid: context.uuid, ...filters },
156
+ });
157
+ },
158
+ },
159
+ withJavaMethod({
160
+ name: 'material.supplement-list', method: 'POST', path: '/api/project-writing/supplement-file/list',
161
+ idempotent: true, multipart: false, requiredContext: REQUIRED_CONTEXT, validate: objectInput,
162
+ handler: async ({ client, context, input }) => await client.request({
163
+ method: 'POST', path: '/api/project-writing/supplement-file/list', idempotent: true,
164
+ body: {
165
+ uuid: context.uuid,
166
+ ...(input.ids ? { ids: input.ids } : {}),
167
+ ...(input.fileType !== undefined || input.file_type !== undefined
168
+ ? { fileType: input.fileType ?? input.file_type } : {}),
169
+ ...(input.fileCode !== undefined || input.file_code !== undefined
170
+ ? { fileCode: input.fileCode ?? input.file_code } : {}),
171
+ ...(input.fileFormat !== undefined || input.file_format !== undefined
172
+ ? { fileFormat: input.fileFormat ?? input.file_format } : {}),
173
+ },
174
+ }),
175
+ }, 'project_writing_supplement_file_list'),
176
+ withJavaMethod({
177
+ name: 'material.supplement-update', method: 'POST', path: '/api/project-writing/supplement-file/update',
178
+ idempotent: false, multipart: false, requiredContext: REQUIRED_CONTEXT,
179
+ validate: (input) => {
180
+ const value = objectInput(input);
181
+ if (value.data === null || Array.isArray(value.data) || typeof value.data !== 'object') {
182
+ throw new CliError('ARGUMENT_INVALID', 'data 必须是对象');
183
+ }
184
+ positiveId(value.data.id, 'data.id');
185
+ return value;
186
+ },
187
+ handler: async ({ client, context, input }) => await client.request({
188
+ method: 'POST', path: '/api/project-writing/supplement-file/update', idempotent: false,
189
+ body: { ...input.data, uuid: context.uuid },
190
+ }),
191
+ }, 'project_writing_supplement_file_update'),
192
+ {
193
+ name: 'material.upload', method: 'POST', path: '/api/project-writing/file/upload',
194
+ idempotent: false, multipart: true, requiredContext: REQUIRED_CONTEXT, validate: uploadInput,
195
+ handler: async ({ client, context, input, baseDir }) => {
196
+ if (typeof baseDir !== 'string' || !baseDir) {
197
+ throw new CliError('CONFIG_MISSING', '材料上传缺少 baseDir');
198
+ }
199
+ const resolvedPath = resolveWithinBase(input.file, baseDir);
200
+ let content;
201
+ try {
202
+ content = await readFile(resolvedPath);
203
+ } catch {
204
+ throw new CliError('FILE_NOT_FOUND', '上传文件不存在', {
205
+ relativePath: input.file, baseDir, resolvedPath,
206
+ });
207
+ }
208
+ const fileName = basename(resolvedPath);
209
+ const formData = new FormData();
210
+ formData.append('uuid', context.uuid);
211
+ formData.append('file', new Blob([content]), fileName);
212
+ const uploaded = await client.upload({ path: '/api/project-writing/file/upload', formData });
213
+ const filePath = typeof uploaded === 'string' ? uploaded : uploaded?.filePath;
214
+ if (!filePath) {
215
+ throw new CliError('BUSINESS_ERROR', '文件上传未返回 filePath');
216
+ }
217
+ const { kind, file, ...metadata } = input;
218
+ const extension = extname(fileName).slice(1).toLowerCase();
219
+ return await client.request({
220
+ method: 'POST', path: ROUTES[kind].save, idempotent: false,
221
+ body: {
222
+ ...metadata,
223
+ uuid: context.uuid,
224
+ fileName,
225
+ filePath,
226
+ fileSize: content.length,
227
+ ...(extension ? { fileFormat: extension } : {}),
228
+ },
229
+ });
230
+ },
231
+ },
232
+ {
233
+ name: 'material.delete-batch', method: 'POST', path: ROUTES.supplement.delete,
234
+ idempotent: false, multipart: false, requiredContext: REQUIRED_CONTEXT, validate: deleteInput,
235
+ handler: async ({ client, input }) => await client.request({
236
+ method: 'POST', path: ROUTES[input.kind].delete, body: input.ids, idempotent: false,
237
+ }),
238
+ },
239
+ {
240
+ name: 'material.generate', method: 'POST', path: '/api/project-writing/system-manage-submit/generate',
241
+ idempotent: false, multipart: false, requiredContext: REQUIRED_CONTEXT, validate: generateInput,
242
+ handler: async ({ client, context, input }) => await client.request({
243
+ method: 'POST', path: '/api/project-writing/system-manage-submit/generate', idempotent: false,
244
+ body: { uuid: context.uuid, fileIds: input.fileIds, fileCode: input.fileCode },
245
+ }),
246
+ },
247
+ {
248
+ name: 'material.download', method: 'POST', path: '/api/project-writing/file/download',
249
+ idempotent: false, multipart: false, requiredContext: REQUIRED_CONTEXT, validate: downloadInput,
250
+ handler: downloadAtomically,
251
+ },
252
+ ];
@@ -0,0 +1,160 @@
1
+ import { CliError } from '../core/errors.mjs';
2
+ import {
3
+ PROJECT_CONTEXT,
4
+ contentInput,
5
+ createFileCommand,
6
+ idsInput as sharedIdsInput,
7
+ itemsInput as sharedItemsInput,
8
+ objectInput as sharedObjectInput,
9
+ resolveSmartImportFiles,
10
+ requiredString,
11
+ valueOf,
12
+ withJavaMethod,
13
+ } from './shared.mjs';
14
+
15
+ function objectInput(input) {
16
+ return sharedObjectInput(input);
17
+ }
18
+
19
+ function pageInput(input) {
20
+ const value = objectInput(input);
21
+ for (const field of ['pageNumber', 'pageSize']) {
22
+ if (value[field] !== undefined
23
+ && (!Number.isSafeInteger(value[field]) || value[field] <= 0)) {
24
+ throw new CliError('ARGUMENT_INVALID', `${field} 必须是正整数`);
25
+ }
26
+ }
27
+ return value;
28
+ }
29
+
30
+ function itemsInput(input) {
31
+ return sharedItemsInput(input);
32
+ }
33
+
34
+ function idsInput(input) {
35
+ return sharedIdsInput(input);
36
+ }
37
+
38
+ function command({ name, path, idempotent, validate = objectInput, body, javaMethod, handler: customHandler }) {
39
+ return withJavaMethod({
40
+ name,
41
+ method: 'POST',
42
+ path,
43
+ idempotent,
44
+ requiredContext: PROJECT_CONTEXT,
45
+ validate,
46
+ handler: customHandler ?? (async ({ client, context, input }) => await client.request({
47
+ method: 'POST', path, body: body(input, context), idempotent,
48
+ })),
49
+ }, javaMethod);
50
+ }
51
+
52
+ function fileCreateInput(input) {
53
+ const value = objectInput(input);
54
+ requiredString(valueOf(value, 'file_name', 'fileName'), 'file_name');
55
+ requiredString(valueOf(value, 'file_path', 'filePath'), 'file_path');
56
+ return value;
57
+ }
58
+
59
+ function smartImportInput(input) {
60
+ const value = objectInput(input);
61
+ const names = valueOf(value, 'file_names', 'fileNames');
62
+ if (names !== undefined && (!Array.isArray(names) || names.some((name) => typeof name !== 'string'))) {
63
+ throw new CliError('ARGUMENT_INVALID', 'file_names 必须是字符串数组');
64
+ }
65
+ return value;
66
+ }
67
+
68
+ export const commands = [
69
+ command({
70
+ name: 'people.list', path: '/api/project-writing/person/page', idempotent: true,
71
+ validate: pageInput,
72
+ body: (input, context) => ({
73
+ uuid: context.uuid,
74
+ pageNumber: input.pageNumber ?? 1,
75
+ pageSize: input.pageSize ?? 20,
76
+ ...input,
77
+ }),
78
+ }),
79
+ command({
80
+ name: 'people.list-all', path: '/api/project-writing/person/list', idempotent: true,
81
+ javaMethod: 'project_writing_person_list',
82
+ body: (input, context) => ({ ...input, uuid: context.uuid }),
83
+ }),
84
+ command({
85
+ name: 'people.save-batch', path: '/api/project-writing/person/batch-save', idempotent: false,
86
+ validate: itemsInput,
87
+ javaMethod: 'project_writing_person_batch_save',
88
+ body: (input, context) => input.items.map((item) => ({
89
+ ...item, chooseFlag: true, uuid: item.uuid ?? context.uuid,
90
+ })),
91
+ }),
92
+ command({
93
+ name: 'people.delete-batch', path: '/api/project-writing/person/batch-delete', idempotent: false,
94
+ validate: idsInput,
95
+ body: (input) => input.ids,
96
+ }),
97
+ command({
98
+ name: 'people.files', path: '/api/project-writing/person-file/list', idempotent: true,
99
+ body: (_input, context) => ({ uuid: context.uuid }),
100
+ }),
101
+ command({
102
+ name: 'people.submissions', path: '/api/project-writing/person-submit/list', idempotent: true,
103
+ body: (_input, context) => ({ uuid: context.uuid }),
104
+ }),
105
+ command({
106
+ name: 'people.save-all', path: '/api/project-writing/person/batch-save', idempotent: false,
107
+ validate: itemsInput, javaMethod: 'project_writing_person_batch_save_all',
108
+ body: (input, context) => input.items.map((item) => ({
109
+ ...item, chooseFlag: false, uuid: item.uuid ?? context.uuid,
110
+ })),
111
+ }),
112
+ command({
113
+ name: 'people.chart-stats', path: '/api/project-writing/person/chart-stats', idempotent: true,
114
+ javaMethod: 'project_writing_person_chart_stats',
115
+ body: (_input, context) => ({ uuid: context.uuid }),
116
+ }),
117
+ command({
118
+ name: 'people.situation-get', path: '/api/project-writing/person-situation/detail', idempotent: true,
119
+ javaMethod: 'project_writing_person_situation_get',
120
+ body: (_input, context) => ({ uuid: context.uuid }),
121
+ }),
122
+ command({
123
+ name: 'people.situation-save', path: '/api/project-writing/person-situation/batch-save', idempotent: false,
124
+ javaMethod: 'project_writing_person_situation_batch_save', validate: contentInput,
125
+ body: (input, context) => ({ uuid: context.uuid, content: valueOf(input, 'content') }),
126
+ }),
127
+ withJavaMethod(createFileCommand({
128
+ name: 'people.file-create', path: '/api/project-writing/person-file/create',
129
+ idField: null, validate: fileCreateInput,
130
+ }), 'project_writing_person_file_create'),
131
+ command({
132
+ name: 'people.file-smart-import', path: '/api/project-writing/person-file/smart-import', idempotent: false,
133
+ javaMethod: 'project_writing_person_file_smart_import', validate: smartImportInput,
134
+ body: (input, context) => ({
135
+ uuid: context.uuid,
136
+ ...(context.tenantCode ? { tenantCode: context.tenantCode } : {}),
137
+ ...(valueOf(input, 'file_names', 'fileNames') ? { fileNames: valueOf(input, 'file_names', 'fileNames') } : {}),
138
+ ...(input.files ? { files: input.files } : {}),
139
+ }),
140
+ handler: async ({ client, context, input, baseDir }) => {
141
+ const names = valueOf(input, 'file_names', 'fileNames') ?? [];
142
+ const resolved = await resolveSmartImportFiles({ client, context, baseDir, fileNames: names });
143
+ return await client.request({
144
+ method: 'POST', path: '/api/project-writing/person-file/smart-import', idempotent: false,
145
+ body: {
146
+ uuid: context.uuid,
147
+ ...(context.tenantCode ? { tenantCode: context.tenantCode } : {}),
148
+ ...(resolved.files.length ? { files: resolved.files } : {}),
149
+ ...(resolved.unresolved.length ? { fileNames: resolved.unresolved } : {}),
150
+ ...(input.files ? { files: input.files } : {}),
151
+ },
152
+ });
153
+ },
154
+ }),
155
+ command({
156
+ name: 'people.delete-by-uuid', path: '/api/project-writing/person/batch-delete-by-uuid', idempotent: false,
157
+ javaMethod: 'project_writing_person_batch_delete_by_uuid', validate: idsInput,
158
+ body: (input, context) => input.ids.map((id) => ({ uuid: context.uuid, items: [{ id }] })),
159
+ }),
160
+ ];
@@ -0,0 +1,209 @@
1
+ import { CliError } from '../core/errors.mjs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { basename } from 'node:path';
4
+ import {
5
+ contentInput,
6
+ createFileCommand,
7
+ objectInput as sharedObjectInput,
8
+ relativePathInput,
9
+ requiredString,
10
+ resolveFileReference,
11
+ valueOf,
12
+ withJavaMethod,
13
+ } from './shared.mjs';
14
+ import { resolveWithinBase } from '../core/files.mjs';
15
+
16
+ const REQUIRED_CONTEXT = ['gatewayUrl', 'uuid'];
17
+
18
+ function objectInput(input) {
19
+ return sharedObjectInput(input);
20
+ }
21
+
22
+ function sortInput(input) {
23
+ const value = objectInput(input);
24
+ if (typeof value.fileCode !== 'string' || !value.fileCode.trim()
25
+ || !Array.isArray(value.ids) || value.ids.length === 0
26
+ || value.ids.some((id) => !Number.isSafeInteger(id) || id <= 0)) {
27
+ throw new CliError('ARGUMENT_INVALID', '排序需要 fileCode 和非空正整数 ids');
28
+ }
29
+ return value;
30
+ }
31
+
32
+ function definition(name, path, idempotent, validate, body, javaMethod) {
33
+ return withJavaMethod({
34
+ name, method: 'POST', path, idempotent, multipart: false,
35
+ requiredContext: REQUIRED_CONTEXT, validate,
36
+ handler: async ({ client, context, input }) => await client.request({
37
+ method: 'POST', path, idempotent, body: body(input, context),
38
+ }),
39
+ }, javaMethod);
40
+ }
41
+
42
+ function systemFileCreateInput(input) {
43
+ const value = objectInput(input);
44
+ requiredString(valueOf(value, 'file_name', 'fileName'), 'file_name');
45
+ requiredString(valueOf(value, 'file_path', 'filePath'), 'file_path');
46
+ const fileCode = valueOf(value, 'file_code', 'fileCode');
47
+ if (!/^000[1-4]$/u.test(String(fileCode ?? ''))) {
48
+ throw new CliError('ARGUMENT_INVALID', 'file_code 仅支持 0001、0002、0003、0004');
49
+ }
50
+ return value;
51
+ }
52
+
53
+ function systemSmartImportInput(input) {
54
+ const value = objectInput(input);
55
+ const fileCode = valueOf(value, 'file_code', 'fileCode');
56
+ if (!/^000[1-4]$/u.test(String(fileCode ?? ''))) {
57
+ throw new CliError('ARGUMENT_INVALID', 'file_code 仅支持 0001、0002、0003、0004');
58
+ }
59
+ const names = valueOf(value, 'file_names', 'fileNames');
60
+ if (names !== undefined && (!Array.isArray(names) || names.some((name) => typeof name !== 'string'))) {
61
+ throw new CliError('ARGUMENT_INVALID', 'file_names 必须是字符串数组');
62
+ }
63
+ return value;
64
+ }
65
+
66
+ function markdownInput(input) {
67
+ const value = objectInput(input);
68
+ relativePathInput(valueOf(value, 'file_name', 'fileName'), 'file_name');
69
+ normalizeMarkdownFileType(value);
70
+ return value;
71
+ }
72
+
73
+ function normalizeMarkdownFileType(input) {
74
+ const rawType = valueOf(input, 'file_type', 'fileType');
75
+ if (rawType === undefined) return 'docx';
76
+ requiredString(rawType, 'file_type');
77
+ const normalized = rawType.trim().replace(/^\.+/u, '').toLowerCase();
78
+ if (!['docx', 'pdf'].includes(normalized)) {
79
+ throw new CliError('ARGUMENT_INVALID', 'file_type 仅支持 docx 或 pdf');
80
+ }
81
+ return normalized;
82
+ }
83
+
84
+ async function importSystemWorkspaceFile({ client, context, baseDir, fileName, fileCode }) {
85
+ if (typeof baseDir !== 'string' || !baseDir || /^https?:\/\//iu.test(fileName) || fileName.startsWith('/')) {
86
+ return false;
87
+ }
88
+ let resolvedPath;
89
+ try {
90
+ resolvedPath = resolveWithinBase(fileName, baseDir);
91
+ } catch (error) {
92
+ if (error?.code === 'FILE_OUTSIDE_BASE') return false;
93
+ throw error;
94
+ }
95
+
96
+ let content;
97
+ try {
98
+ content = await readFile(resolvedPath);
99
+ } catch (error) {
100
+ if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') return false;
101
+ throw error;
102
+ }
103
+
104
+ const formData = new FormData();
105
+ formData.append('uuid', context.uuid);
106
+ formData.append('fileCode', fileCode);
107
+ formData.append('file', new Blob([content]), basename(resolvedPath));
108
+ await client.upload({
109
+ path: '/api/internal/v1/project-writing/system-manage-file/workspace-import',
110
+ formData,
111
+ });
112
+ return true;
113
+ }
114
+
115
+ export const commands = [
116
+ definition(
117
+ 'policy.detail', '/api/project-writing/system-manage-situation/detail', true, objectInput,
118
+ (_input, context) => ({ uuid: context.uuid }),
119
+ 'project_writing_system_manage_situation_get',
120
+ ),
121
+ definition(
122
+ 'policy.save', '/api/project-writing/system-manage-situation/save-or-update', false, objectInput,
123
+ (input, context) => ({ ...input, uuid: context.uuid }),
124
+ 'project_writing_system_manage_situation_save_or_update',
125
+ ),
126
+ definition(
127
+ 'policy.files', '/api/project-writing/system-manage-file/list', true, objectInput,
128
+ (_input, context) => ({ uuid: context.uuid }),
129
+ ),
130
+ definition(
131
+ 'policy.sort-files', '/api/project-writing/system-manage-file/sort', false, sortInput,
132
+ (input, context) => ({ uuid: context.uuid, fileCode: input.fileCode, ids: input.ids }),
133
+ ),
134
+ withJavaMethod({
135
+ name: 'policy.file-smart-import', method: 'POST', path: '/api/project-writing/system-manage-file/smart-import',
136
+ idempotent: false, multipart: false, requiredContext: REQUIRED_CONTEXT,
137
+ validate: systemSmartImportInput, javaMethod: 'project_writing_system_manage_file_smart_import',
138
+ handler: async ({ client, context, input, baseDir }) => {
139
+ const fileCode = valueOf(input, 'file_code', 'fileCode');
140
+ const names = [...new Set((valueOf(input, 'file_names', 'fileNames') ?? [])
141
+ .map((name) => name.trim()).filter(Boolean))];
142
+ const unresolved = [];
143
+ for (const name of names) {
144
+ if (!await importSystemWorkspaceFile({ client, context, baseDir, fileName: name, fileCode })) {
145
+ unresolved.push(name);
146
+ }
147
+ }
148
+ if (unresolved.length === 0 && !input.documentIds?.length) return true;
149
+ return await client.request({
150
+ method: 'POST', path: '/api/project-writing/system-manage-file/smart-import', idempotent: false,
151
+ body: {
152
+ uuid: context.uuid, fileCode,
153
+ ...(unresolved.length ? { fileNames: unresolved } : {}),
154
+ ...(input.documentIds ? { documentIds: input.documentIds } : {}),
155
+ },
156
+ });
157
+ },
158
+ }, 'project_writing_system_manage_file_smart_import'),
159
+ withJavaMethod({
160
+ name: 'policy.file-create', method: 'POST', path: '/api/project-writing/system-manage-file/create',
161
+ idempotent: false, multipart: false, requiredContext: REQUIRED_CONTEXT,
162
+ validate: systemFileCreateInput, javaMethod: 'project_writing_system_manage_file_create',
163
+ handler: async ({ client, context, input, baseDir }) => {
164
+ const reference = await resolveFileReference({
165
+ client, context, baseDir,
166
+ filePath: valueOf(input, 'file_path', 'filePath'),
167
+ fileName: valueOf(input, 'file_name', 'fileName'),
168
+ });
169
+ return await client.request({
170
+ method: 'POST', path: '/api/project-writing/system-manage-file/create', idempotent: false,
171
+ body: {
172
+ uuid: context.uuid, fileType: '2',
173
+ fileCode: valueOf(input, 'file_code', 'fileCode'),
174
+ fileFormat: valueOf(input, 'file_format', 'fileFormat'),
175
+ ...reference,
176
+ },
177
+ });
178
+ },
179
+ }, 'project_writing_system_manage_file_create'),
180
+ withJavaMethod({
181
+ name: 'policy.file-md-to-docx', method: 'POST', path: '/api/project-writing/system-manage-file/md-to-docx',
182
+ idempotent: false, multipart: false, requiredContext: REQUIRED_CONTEXT,
183
+ validate: markdownInput, javaMethod: 'project_writing_system_manage_file_md_to_docx',
184
+ handler: async ({ client, context, input, baseDir }) => {
185
+ if (typeof baseDir !== 'string' || !baseDir) {
186
+ throw new CliError('CONFIG_MISSING', 'Markdown 转换缺少 baseDir');
187
+ }
188
+ const relativePath = valueOf(input, 'file_name', 'fileName');
189
+ const resolvedPath = resolveWithinBase(relativePath, baseDir);
190
+ let content;
191
+ try {
192
+ content = await readFile(resolvedPath);
193
+ } catch {
194
+ throw new CliError('FILE_NOT_FOUND', 'Markdown 文件不存在', {
195
+ relativePath, baseDir, resolvedPath,
196
+ });
197
+ }
198
+ return await client.request({
199
+ method: 'POST', path: '/api/project-writing/system-manage-file/md-to-docx', idempotent: false,
200
+ body: {
201
+ uuid: context.uuid,
202
+ fileName: basename(resolvedPath),
203
+ content: content.toString('utf8'),
204
+ fileType: normalizeMarkdownFileType(input),
205
+ },
206
+ });
207
+ },
208
+ }, 'project_writing_system_manage_file_md_to_docx'),
209
+ ];