weaver-e9-assistant-cli 1.0.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,394 @@
1
+ /**
2
+ * E9 日程(workplan)模块。
3
+ *
4
+ * 接口:
5
+ * POST /api/workplan/calendar/AllCalendar 获取日程列表(events 位置数组)
6
+ * POST /api/workplan/calendar/MyCalendar 浏览器主用日历(与 AllCalendar 同构)
7
+ * POST /api/workplan/calendar/getAttentionData 关注的日程
8
+ * POST /api/workplan/calendar/getBaseData 日历初始化数据(含 dept/subComp/selectGroup/selectType)
9
+ * POST /api/workplan/calendar/getCalendarData 日历数据(替代 AllCalendar,支持组织视图)
10
+ * POST /api/workplan/base/add 新建日程
11
+ * POST /api/workplan/base/view 查看日程详情
12
+ * POST /api/workplan/base/delete 删除日程
13
+ * POST /api/workplan/base/finish 完成日程
14
+ * POST /api/workplan/type/getSelectType 获取日程类型列表
15
+ * POST /api/workplan/search/getCondition 获取搜索条件(分组+字段定义)
16
+ * POST /api/workplan/search/getlist 日程表搜索
17
+ */
18
+ import { endpointFor } from './config.js';
19
+ import { createApiFromProfile } from './client.js';
20
+ import { pickByPaths, stripHtml, extractDataKey, extractColumns, extractRows, extractTotal } from './parse.js';
21
+ import { getCurrentUser } from './wfuser.js';
22
+ import { todayStr } from './util.js';
23
+ import fs from 'node:fs';
24
+ import path from 'node:path';
25
+ import os from 'node:os';
26
+ import { assertExcelLike, nextAvailablePath } from './fileutil.js';
27
+ // ─── 日程列表(AllCalendar)────────────────────────────────
28
+ export async function getWorkplanList(cfg, opts = {}) {
29
+ const api = await createApiFromProfile(cfg);
30
+ const selectDate = opts.selectDate ?? todayStr();
31
+ const viewType = opts.viewType ?? '3';
32
+ const r = await api.post('/api/workplan/calendar/AllCalendar', {
33
+ selectUser: opts.selectUser ?? '', selectDate, workPlanType: opts.workPlanType ?? '', viewType,
34
+ });
35
+ const j = r.json || {};
36
+ const events = pickByPaths(j, ['events', 'data.events']) ?? [];
37
+ const items = (Array.isArray(events) ? events : []).map(normalizeWorkplanEvent).filter((it) => it.id || it.name);
38
+ return { selectDate, viewType, range: { start: stringify(j.start), end: stringify(j.end) }, workplanNumByDay: j.workplanNumByDay ?? {}, items, raw: j };
39
+ }
40
+ // ─── 日程列表(MyCalendar,浏览器主用)─────────────────────
41
+ export async function getMyCalendar(cfg, opts = {}) {
42
+ const api = await createApiFromProfile(cfg);
43
+ const selectDate = opts.selectDate ?? todayStr();
44
+ const viewType = opts.viewType ?? '3';
45
+ const r = await api.post('/api/workplan/calendar/MyCalendar', {
46
+ selectUser: opts.selectUser ?? '', selectDate, workPlanType: opts.workPlanType ?? '', viewType,
47
+ });
48
+ const j = r.json || {};
49
+ const events = pickByPaths(j, ['events', 'data.events']) ?? [];
50
+ const items = (Array.isArray(events) ? events : []).map(normalizeWorkplanEvent).filter((it) => it.id || it.name);
51
+ return { selectDate, viewType, range: { start: stringify(j.start), end: stringify(j.end) }, workplanNumByDay: j.workplanNumByDay ?? {}, items, raw: j };
52
+ }
53
+ // ─── 日历数据(getCalendarData,替代 AllCalendar)───────────
54
+ /**
55
+ * 获取日历数据,支持按组织视图(部门/分部/直接下级等)查看。
56
+ * 参数:
57
+ * bywhat=3 selectDate=YYYY-MM-DD objType=1 objIds=<组织id> workPlanType=<类型id>
58
+ */
59
+ export async function getCalendarData(cfg, opts = {}) {
60
+ const api = await createApiFromProfile(cfg);
61
+ const selectDate = opts.selectDate ?? todayStr();
62
+ const body = {
63
+ bywhat: opts.bywhat ?? '3',
64
+ selectDate,
65
+ objType: opts.objType ?? '1',
66
+ objIds: opts.objIds ?? '',
67
+ workPlanType: opts.workPlanType ?? '',
68
+ };
69
+ const r = await api.post('/api/workplan/calendar/getCalendarData', body);
70
+ const j = r.json || {};
71
+ const days = j.days ?? { title: [], value: [] };
72
+ const datas = (j.datas || []).map((d) => ({
73
+ memberid: d.memberid,
74
+ memberName: (j.memberids || []).find((m) => String(m.id) === String(d.memberid))?.name,
75
+ info: (d.info || []).map((ev) => ({
76
+ id: stringify(ev.id),
77
+ name: stringify(ev.name),
78
+ beginDate: stringify(ev.begindate),
79
+ beginTime: stringify(ev.begintime),
80
+ endDate: stringify(ev.enddate),
81
+ endTime: stringify(ev.endtime),
82
+ color: stringify(ev.color),
83
+ creater: ev.creater || {},
84
+ description: stringify(ev.description),
85
+ workplanType: stringify(ev.workplanType),
86
+ resourceid: stringify(ev.resourceid),
87
+ showname: stringify(ev.showname),
88
+ title: ev.title || [],
89
+ raw: ev,
90
+ })),
91
+ }));
92
+ const allEvents = [];
93
+ for (const d of datas) {
94
+ for (const ev of d.info) {
95
+ allEvents.push({ ...ev, memberid: d.memberid, memberName: d.memberName });
96
+ }
97
+ }
98
+ return {
99
+ selectDate, days, conf: j.conf ?? {}, datas,
100
+ memberids: j.memberids ?? [],
101
+ events: allEvents,
102
+ count: allEvents.length,
103
+ raw: j,
104
+ };
105
+ }
106
+ // ─── 关注的日程 ──────────────────────────────────────────
107
+ export async function getAttentionData(cfg) {
108
+ const api = await createApiFromProfile(cfg);
109
+ const r = await api.post('/api/workplan/calendar/getAttentionData');
110
+ const j = r.json || {};
111
+ return { totalpage: j.totalpage, items: (j.data || []), raw: j };
112
+ }
113
+ // ─── 日历初始化数据(增强版)────────────────────────────────
114
+ export async function getBaseData(cfg) {
115
+ const api = await createApiFromProfile(cfg);
116
+ const r = await api.post('/api/workplan/calendar/getBaseData');
117
+ const j = r.json || {};
118
+ return {
119
+ wpSetInfo: j.wpSetInfo ?? {},
120
+ timeRange: { start: j.timeRangeStart, end: j.timeRangeEnd },
121
+ syncDay: j.syncDay,
122
+ selectGroup: (j.selectGroup || []).map((g) => ({ key: g.key, name: g.showname })),
123
+ selectType: (j.selectType || []).map((t) => ({ key: t.key, name: t.showname })),
124
+ dept: (j.dept || []).map((d) => ({ name: d.name, id: d.id })),
125
+ subComp: (j.subComp || []).map((s) => ({ name: s.name, id: s.id })),
126
+ viewType: j.viewType,
127
+ zone_offset: j.zone_offset,
128
+ titleFold: j.titleFold,
129
+ exportListBtnTitle: j.exportListBtnTitle,
130
+ calendarInit: j.calendarInit ? {
131
+ range: { start: j.calendarInit.start, end: j.calendarInit.end },
132
+ events: (j.calendarInit.events || []).map(normalizeWorkplanEvent),
133
+ } : null,
134
+ canEWSToOa: j.canEWSToOa,
135
+ weekstartday: j.weekstartday,
136
+ raw: j,
137
+ };
138
+ }
139
+ // ─── events 位置数组解析 ──────────────────────────────────
140
+ export function normalizeWorkplanEvent(ev) {
141
+ if (!Array.isArray(ev))
142
+ return {};
143
+ const at = (i) => { const v = ev[i]; return v !== undefined && v !== null && v !== '' ? String(v) : undefined; };
144
+ return {
145
+ id: at(0), name: at(1),
146
+ beginDateTime: parseCalDate(at(2)), endDateTime: parseCalDate(at(3)),
147
+ workPlanType: at(4), color: at(7), typeName: at(12), allDay: at(13),
148
+ raw: ev,
149
+ };
150
+ }
151
+ export function parseCalDate(s) {
152
+ if (!s)
153
+ return undefined;
154
+ const m = s.match(/^(\d{2})\/(\d{2})\/(\d{4})\s+(\d{2}:\d{2})$/);
155
+ return m ? `${m[3]}-${m[1]}-${m[2]} ${m[4]}` : s;
156
+ }
157
+ // ─── 日程表搜索 ──────────────────────────────────────────
158
+ export async function searchWorkplan(cfg, opts = {}) {
159
+ const api = await createApiFromProfile(cfg);
160
+ const filters = {
161
+ planname: opts.name ?? '', plantype: opts.type ?? '', planstatus: opts.status ?? '',
162
+ createrid: opts.createrId ?? '', receiveID: opts.receiveId ?? '', timeSag: opts.timeSag ?? '0',
163
+ begindate: opts.beginDate ?? '', enddate: opts.endDate ?? '',
164
+ isOvertime: opts.isOvertime ?? '', createrDep: opts.createrDep ?? '', createrSub: opts.createrSub ?? '',
165
+ docids: opts.docids ?? '', requestids: opts.requestids ?? '',
166
+ prjids: opts.prjids ?? '', crmids: opts.crmids ?? '',
167
+ };
168
+ const r1 = await api.post('/api/workplan/search/getlist', filters);
169
+ const dataKey = extractDataKey(r1.json);
170
+ if (!dataKey)
171
+ throw new Error(`workplan search 未拿到 dataKey,getlist 响应: ${(r1.text ?? '').slice(0, 200)}`);
172
+ const page = opts.page ?? 1;
173
+ const r2 = await api.post('/api/ec/dev/table/datas', { dataKey, current: String(page), sortParams: '[]', pageSize: '10' });
174
+ const columns = extractColumns(r2.json);
175
+ const rows = extractRows(r2.json);
176
+ const items = rows.map((r) => normalizeSearchRow(r));
177
+ const pageSize = pickByPaths(r2.json, ['pageSize', 'data.pageSize']);
178
+ let total = null;
179
+ let countsResp = null;
180
+ try {
181
+ const r3 = await api.post('/api/ec/dev/table/counts', { dataKey });
182
+ countsResp = r3.json;
183
+ total = extractTotal(r3.json);
184
+ }
185
+ catch { /* counts 失败不阻断 */ }
186
+ return { dataKey, page, total, pageSize: typeof pageSize === 'number' ? pageSize : null, items, raw: { getlist: r1.json, datas: r2.json, counts: countsResp } };
187
+ }
188
+ export function normalizeSearchRow(row) {
189
+ if (!row || typeof row !== 'object')
190
+ return {};
191
+ const get = (keys) => pickByPaths(row, keys);
192
+ return {
193
+ id: stringify(get(['ID', 'id', 'randomFieldId'])),
194
+ name: stripHtml(get(['IDspan'])) || stringify(get(['ID'])),
195
+ typeId: stringify(get(['type_n', 'plantype', 'type'])), typeName: stripHtml(get(['type_nspan'])),
196
+ urgentLevel: stringify(get(['urgentLevel'])), urgentName: stripHtml(get(['urgentLevelspan'])),
197
+ createrId: stringify(get(['createrID', 'randomField0'])), creater: stripHtml(get(['createrIDspan'])),
198
+ status: stringify(get(['status', 'randomField1'])), statusName: stripHtml(get(['statusspan'])),
199
+ beginDate: stringify(get(['beginDatespan', 'beginDate'])), endDate: stringify(get(['endDatespan', 'endDate'])),
200
+ raw: row,
201
+ };
202
+ }
203
+ // ─── 搜索条件 ────────────────────────────────────────────
204
+ /** 获取日程搜索条件(分组 + 字段定义) */
205
+ export async function getWorkplanSearchCondition(cfg) {
206
+ const api = await createApiFromProfile(cfg);
207
+ const r = await api.post('/api/workplan/search/getCondition');
208
+ const j = r.json || {};
209
+ const groups = (j.groupinfo || []).map((g) => ({
210
+ id: g.groupid, title: g.title, color: g.color, viewcondition: g.viewcondition,
211
+ }));
212
+ const fields = [];
213
+ for (const grp of (j.conditioninfo || [])) {
214
+ for (const item of (grp.items || [])) {
215
+ fields.push({
216
+ group: grp.title || '',
217
+ label: item.label || '',
218
+ domkey: (item.domkey || []).join(', '),
219
+ type: item.conditionType || '',
220
+ options: (item.options || []).map((o) => ({ key: o.key, name: o.showname })),
221
+ });
222
+ }
223
+ }
224
+ return { groups, fields, raw: j };
225
+ }
226
+ // ─── 日程类型 ────────────────────────────────────────────
227
+ /** 获取日程类型列表 */
228
+ export async function getSelectType(cfg) {
229
+ const api = await createApiFromProfile(cfg);
230
+ const r = await api.post('/api/workplan/type/getSelectType', { showAll: '1' });
231
+ const j = r.json || {};
232
+ const types = (Array.isArray(j) ? j : (j.data || j.types || [])).map((t) => ({
233
+ key: stringify(t.key), name: t.showname || t.name || '',
234
+ }));
235
+ return { types, raw: r.json };
236
+ }
237
+ // ─── 新建日程 ────────────────────────────────────────────
238
+ export async function createWorkplan(cfg, opts) {
239
+ const ep = endpointFor(cfg);
240
+ let memberIDs = opts.memberIDs ?? '';
241
+ if (!memberIDs) {
242
+ try {
243
+ const me = await getCurrentUser(cfg);
244
+ memberIDs = me.userid;
245
+ }
246
+ catch {
247
+ memberIDs = '';
248
+ }
249
+ }
250
+ const description = opts.description ?? '';
251
+ const body = {
252
+ workPlanType: opts.workPlanType ?? '0', planName: opts.planName,
253
+ secretLevel: opts.secretLevel ?? '4', secretDeadline: '', secretLevelView: '',
254
+ description, memberIDs, urgentLevel: opts.urgentLevel ?? '0',
255
+ beginDateTime: opts.beginDateTime, endDateTime: opts.endDateTime,
256
+ remindType: opts.remindType ?? '3', remindImmediately: '',
257
+ remindBeforeStart: '1', remindDateBeforeStart: '0', remindTimeBeforeStart: '10',
258
+ remindBeforeEnd: '1', remindDateBeforeEnd: '0', remindTimeBeforeEnd: '10',
259
+ docIDs: opts.docIDs ?? '', requestIDs: opts.requestIDs ?? '', from: 'workplan',
260
+ };
261
+ if (!opts.send)
262
+ return { sent: false, endpoint: { baseUrl: ep.baseUrl }, body };
263
+ const api = await createApiFromProfile(cfg);
264
+ const r = await api.post('/api/workplan/base/add', body);
265
+ return { sent: true, endpoint: { baseUrl: ep.baseUrl }, body, response: r };
266
+ }
267
+ // ─── 查看日程详情 ────────────────────────────────────────
268
+ export async function viewWorkplan(cfg, workid) {
269
+ const api = await createApiFromProfile(cfg);
270
+ const r = await api.post('/api/workplan/base/view', { workid: String(workid) });
271
+ const j = r.json || {};
272
+ // 从表单 data.items 中提取字段值
273
+ const fields = {};
274
+ const sections = [];
275
+ for (const section of (j.data || [])) {
276
+ const items = [];
277
+ for (const item of (section.items || [])) {
278
+ const key = (item.domkey || [])[0] || item.label;
279
+ const val = item.value;
280
+ fields[key] = val;
281
+ items.push({ label: item.label, key, value: val, type: item.formItemType });
282
+ }
283
+ sections.push({ title: section.title, items });
284
+ }
285
+ return {
286
+ id: stringify(j.targetId ?? j.id ?? workid),
287
+ name: stringify(fields.planName),
288
+ description: stringify(fields.description),
289
+ beginDateTime: stringify(fields.beginDateTime),
290
+ endDateTime: stringify(fields.endDateTime),
291
+ workPlanType: stringify(fields.workPlanType),
292
+ urgentLevel: stringify(fields.urgentLevel),
293
+ secretLevel: stringify(j.secretLevel),
294
+ memberIDs: stringify(fields.memberIDs),
295
+ creater: fields.createrID,
296
+ isCreater: j.isCreater,
297
+ buttons: (j.btn || []).map((b) => b.title),
298
+ sections,
299
+ fields,
300
+ raw: j,
301
+ };
302
+ }
303
+ // ─── 删除日程 ────────────────────────────────────────────
304
+ export async function deleteWorkplan(cfg, workid, opts = {}) {
305
+ const ep = endpointFor(cfg);
306
+ if (!opts.send)
307
+ return { sent: false, endpoint: { baseUrl: ep.baseUrl }, workid };
308
+ const api = await createApiFromProfile(cfg);
309
+ const r = await api.post('/api/workplan/base/delete', { workid: String(workid) });
310
+ return { sent: true, endpoint: { baseUrl: ep.baseUrl }, workid, response: r };
311
+ }
312
+ // ─── 完成日程 ────────────────────────────────────────────
313
+ export async function finishWorkplan(cfg, workid, opts = {}) {
314
+ const ep = endpointFor(cfg);
315
+ if (!opts.send)
316
+ return { sent: false, endpoint: { baseUrl: ep.baseUrl }, workid };
317
+ const api = await createApiFromProfile(cfg);
318
+ const r = await api.post('/api/workplan/base/finish', { workid: String(workid) });
319
+ return { sent: true, endpoint: { baseUrl: ep.baseUrl }, workid, response: r };
320
+ }
321
+
322
+ // ─── Excel 批量导入(抓包:workplanexcel.xls → addExcelFileid → importExcel)────
323
+ export async function downloadWorkplanImportTemplate(cfg, opts = {}) {
324
+ const api = await createApiFromProfile(cfg);
325
+ const outDir = path.resolve(opts.outDir || path.join(os.homedir(), 'Downloads'));
326
+ const filename = opts.filename || 'workplanexcel.xls';
327
+ const headers = await api.getHeaders(undefined, { forceCookie: true });
328
+ delete headers['Content-Type'];
329
+ const response = await fetch(`${api.baseUrl}/workplan/config/import/workplanexcel.xls`, {
330
+ headers, signal: AbortSignal.timeout(30000),
331
+ });
332
+ if (!response.ok)
333
+ throw new Error(`日程导入模板下载失败:HTTP ${response.status}`);
334
+ const buf = Buffer.from(await response.arrayBuffer());
335
+ const validation = assertExcelLike(buf, '日程导入模板');
336
+ fs.mkdirSync(outDir, { recursive: true });
337
+ const outPath = nextAvailablePath(outDir, filename);
338
+ fs.writeFileSync(outPath, buf);
339
+ return {
340
+ downloaded: true, outPath, size: buf.length, validation,
341
+ requiredFields: ['日程名称'],
342
+ fillRules: ['只能修改数据单元格值', '不得修改表头、格式、批注、必填提示、数据验证、公式、隐藏状态及工作表结构'],
343
+ };
344
+ }
345
+
346
+ export function parseWorkplanImportResult(json) {
347
+ const businessSuccess = json?.flag === true;
348
+ const message = stripHtml(String(json?.message ?? '').replace(/<\/br>/gi, '\n')).trim();
349
+ return { success: businessSuccess, apiAccepted: json?.api_status === true, message, raw: json };
350
+ }
351
+
352
+ export async function importWorkplanExcel(cfg, opts = {}) {
353
+ const filePath = path.resolve(String(opts.file || ''));
354
+ if (!opts.yes)
355
+ return { imported: false, requiresConfirmation: true, filePath, operation: 'workplan-import' };
356
+ if (!opts.file || !fs.existsSync(filePath) || !fs.statSync(filePath).isFile())
357
+ throw new Error(`找不到日程导入文件:${filePath}`);
358
+ if (!/\.xlsx?$/i.test(filePath))
359
+ throw new Error('日程导入文件必须是 .xls 或 .xlsx。');
360
+ const api = await createApiFromProfile(cfg);
361
+ const headers = await api.getHeaders(undefined, { forceCookie: true });
362
+ delete headers['Content-Type'];
363
+ const filename = path.basename(filePath);
364
+ const form = new FormData();
365
+ form.append('name', filename);
366
+ form.append('secretLevel', String(opts.secretLevel ?? '4'));
367
+ form.append('ts', String(Date.now()));
368
+ form.append('file', new Blob([fs.readFileSync(filePath)], { type: /\.xlsx$/i.test(filePath) ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' : 'application/vnd.ms-excel' }), filename);
369
+ const upload = await fetch(`${api.baseUrl}/api/workplan/calendar/addExcelFileid`, {
370
+ method: 'POST', headers, body: form, signal: AbortSignal.timeout(60000),
371
+ });
372
+ const uploadJson = await upload.json().catch(() => null);
373
+ const fileid = pickByPaths(uploadJson, ['data.fileid', 'fileid']);
374
+ if (!upload.ok || !fileid)
375
+ throw new Error(`日程文件上传失败:HTTP ${upload.status} ${JSON.stringify(uploadJson)?.slice(0, 300)}`);
376
+ const response = await api.post('/api/workplan/calendar/importExcel', {
377
+ remindType: opts.remindType ?? '',
378
+ workPlanType: opts.workPlanType ?? '0',
379
+ secretLevel: opts.secretLevel ?? '4',
380
+ validateType: opts.validateType ?? 'lastname',
381
+ remindBeforeStart: opts.remindBeforeStart ?? 'false',
382
+ remindDateBeforeStart: opts.remindDateBeforeStart ?? '0',
383
+ remindTimeBeforeStart: opts.remindTimeBeforeStart ?? '10',
384
+ remindBeforeEnd: opts.remindBeforeEnd ?? 'false',
385
+ remindDateBeforeEnd: opts.remindDateBeforeEnd ?? '0',
386
+ remindTimeBeforeEnd: opts.remindTimeBeforeEnd ?? '10',
387
+ excelfile: String(fileid), filelist: '[object Object]', remindCanNotSub: 'false', from: 'workplan', needCheck: '1',
388
+ });
389
+ const result = parseWorkplanImportResult(response.json);
390
+ return { imported: result.success, filePath, fileid: String(fileid), ...result };
391
+ }
392
+ // ─── 工具函数 ────────────────────────────────────────────
393
+ function stringify(v) { if (v === undefined || v === null || v === '')
394
+ return undefined; return String(v); }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "weaver-e9-assistant-cli",
3
+ "version": "1.0.0",
4
+ "description": "泛微 e-cology E9 HTTP 能力 CLI 封装(待办、微博、新建流程、邮件、公文、日程、考勤、建模台账)",
5
+ "type": "module",
6
+ "bin": {
7
+ "e9": "dist/cli.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "optionalDependencies": {
13
+ "playwright": "^1.62.1"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "start": "node dist/cli.js",
20
+ "e9": "node dist/cli.js",
21
+ "build": "node --check dist/cli.js && node --check dist/cube.js && node --check dist/workplan.js && node --check dist/fileutil.js && node --check dist/kq.js && node --check dist/links.js && node --check dist/report.js && node --check dist/workflow.js && node --check dist/parse.js && node --check dist/todosubmit.js && node --check dist/wfsummary.js && node --check dist/wfstart.js && node --check dist/submissionplan.js && node --check dist/submissionstate.js && node --check dist/auth-token.js && node --check dist/todobatch.js && node --check dist/pageopen.js && node --check dist/createcontext.js && node --check dist/querystate.js && node --check dist/wfops.js",
22
+ "test": "node --test tests/*.test.js",
23
+ "dev": "node dist/cli.js"
24
+ }
25
+ }