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.
- package/dist/args.js +36 -0
- package/dist/auth-token.js +690 -0
- package/dist/auth.js +187 -0
- package/dist/blog.js +469 -0
- package/dist/cli.js +4041 -0
- package/dist/client.js +199 -0
- package/dist/config.js +124 -0
- package/dist/createcontext.js +41 -0
- package/dist/crypto.js +139 -0
- package/dist/cube.js +1633 -0
- package/dist/email.js +838 -0
- package/dist/fileutil.js +20 -0
- package/dist/kq.js +204 -0
- package/dist/links.js +34 -0
- package/dist/odoc.js +194 -0
- package/dist/pageopen.js +29 -0
- package/dist/parse.js +159 -0
- package/dist/querystate.js +38 -0
- package/dist/render.js +21 -0
- package/dist/report.js +50 -0
- package/dist/submissionplan.js +43 -0
- package/dist/submissionstate.js +81 -0
- package/dist/todoaskidea.js +54 -0
- package/dist/todobatch.js +154 -0
- package/dist/todocc.js +128 -0
- package/dist/todoforward.js +66 -0
- package/dist/todoreject.js +215 -0
- package/dist/todosubmit.js +277 -0
- package/dist/todotransfer.js +54 -0
- package/dist/util.js +25 -0
- package/dist/wfcreate.js +102 -0
- package/dist/wfform.js +194 -0
- package/dist/wfops.js +40 -0
- package/dist/wfreceiver.js +43 -0
- package/dist/wfstart.js +717 -0
- package/dist/wfsummary.js +89 -0
- package/dist/wfupload.js +78 -0
- package/dist/wfuser.js +60 -0
- package/dist/workflow.js +461 -0
- package/dist/workplan.js +394 -0
- package/package.json +25 -0
package/dist/fileutil.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/** 下载文件不覆盖用户已有文件;重名时追加 (1)、(2)… */
|
|
5
|
+
export function nextAvailablePath(outDir, filename) {
|
|
6
|
+
const parsed = path.parse(filename);
|
|
7
|
+
let candidate = path.join(outDir, filename);
|
|
8
|
+
for (let index = 1; fs.existsSync(candidate); index++)
|
|
9
|
+
candidate = path.join(outDir, `${parsed.name} (${index})${parsed.ext}`);
|
|
10
|
+
return candidate;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 接受 OOXML zip 或传统 OLE Excel;拒绝 HTML 登录页、JSON 错误和空响应。 */
|
|
14
|
+
export function assertExcelLike(buf, label = 'Excel 文件') {
|
|
15
|
+
const isZip = Buffer.isBuffer(buf) && buf.length >= 64 && buf.readUInt32LE(0) === 0x04034b50;
|
|
16
|
+
const isOle = Buffer.isBuffer(buf) && buf.length >= 512 && buf.subarray(0, 8).equals(Buffer.from('D0CF11E0A1B11AE1', 'hex'));
|
|
17
|
+
if (!isZip && !isOle)
|
|
18
|
+
throw new Error(`${label}响应不是有效的 xls/xlsx,可能是登录页或错误页。`);
|
|
19
|
+
return { format: isZip ? 'xlsx' : 'xls', size: buf.length };
|
|
20
|
+
}
|
package/dist/kq.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E9 考勤(kq)打卡模块。
|
|
3
|
+
*
|
|
4
|
+
* 抓包+实跑确认的接口(测试环境,2026-08-12):
|
|
5
|
+
* POST /api/hrm/kq/attendanceButton/getButtons 页面加载预发:取 timeline 班次参数(无 body)
|
|
6
|
+
* POST /api/hrm/kq/attendanceButton/punchButton 打卡(带 timeline 班次参数 + signTime + encryptObj)
|
|
7
|
+
*
|
|
8
|
+
* signSecret 签名算法从前端 signFunc.js 逆向(webpack source map 虚拟路径,靠 Sources 复制源码):
|
|
9
|
+
* signSecret(str) = md5("wea" + str) + "ver"(小写;标准 MD5,但输入前缀加 "wea")
|
|
10
|
+
* encryptObj = [timestamp, userid, signSecret(userid + groupid)] = `${ts},${uid},${md5("wea"+uid+gid)+"ver"}`
|
|
11
|
+
* timestamp = Date.now()(现算,服务端不校验时效);signTime = 现算 HH:mm:ss
|
|
12
|
+
* groupid = 考勤组 id(222222600 默认考勤组=1;每用户可能不同,待校准,CLI 可配 --group-id)
|
|
13
|
+
*
|
|
14
|
+
* 走当前 baseUrl(单一环境),aiAgentToken 鉴权(服务端只校验 md5(uid+gid) 防篡改,不校验会话/时效)。
|
|
15
|
+
*/
|
|
16
|
+
import * as crypto from 'crypto';
|
|
17
|
+
import { createApiFromProfile } from './client.js';
|
|
18
|
+
// ───────────────── signSecret(前端 signFunc.js 逆向)─────────────────
|
|
19
|
+
/** md5 小写。 */
|
|
20
|
+
function md5Hex(s) {
|
|
21
|
+
return crypto.createHash('md5').update(s, 'utf8').digest('hex');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 考勤签名:signSecret(str) = md5("wea" + str) + "ver"(与前端 signFunc.js 一致)。
|
|
25
|
+
* 前端输入经 uTF8Encode,但纯 ASCII("wea"+uid+gid)无变化,故直接 md5。
|
|
26
|
+
*/
|
|
27
|
+
export function signSecret(str) {
|
|
28
|
+
return md5Hex(`wea${str}`) + 'ver';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 构造 encryptObj = `${timestamp},${userid},${signSecret(userid+groupid)}ver`。
|
|
32
|
+
* 与 DataList.js 的 `signData=[generateTimeStamp(), userid, signSecret(userid+groupid)]` 一致
|
|
33
|
+
* (数组 toString() = "ts,uid,signSecret结果",signSecret 已含 ver 后缀)。
|
|
34
|
+
*/
|
|
35
|
+
export function buildEncryptObj(userid, groupId, timestamp) {
|
|
36
|
+
const ts = (timestamp ?? Date.now()) + '';
|
|
37
|
+
return `${ts},${userid},${signSecret(`${userid}${groupId}`)}`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 取打卡班次 timeline。POST /api/hrm/kq/attendanceButton/getButtons(无 body)。
|
|
41
|
+
* 返回 timeline[](每项=一个打卡时段,含全部班次参数)+ date/now/groupname。
|
|
42
|
+
* 不返回 encryptObj/groupid/signature(前端 JS 现算)。
|
|
43
|
+
*/
|
|
44
|
+
export async function getButtons(cfg) {
|
|
45
|
+
const api = await createApiFromProfile(cfg);
|
|
46
|
+
const r = await api.post('/api/hrm/kq/attendanceButton/getButtons', {});
|
|
47
|
+
const j = r.json || {};
|
|
48
|
+
const tlRaw = Array.isArray(j.timeline) ? j.timeline : [];
|
|
49
|
+
const timeline = tlRaw.map((t) => ({
|
|
50
|
+
date: s(t.date),
|
|
51
|
+
type: s(t.type),
|
|
52
|
+
belongtime: s(t.belongtime),
|
|
53
|
+
canSignTime: s(t.canSignTime),
|
|
54
|
+
across: s(t.across),
|
|
55
|
+
belongdate: s(t.belongdate),
|
|
56
|
+
datetime: s(t.datetime),
|
|
57
|
+
min: s(t.min),
|
|
58
|
+
signSectionEndTime: s(t.signSectionEndTime),
|
|
59
|
+
islastsign: s(t.islastsign),
|
|
60
|
+
signSectionTime: s(t.signSectionTime),
|
|
61
|
+
signSectionBeginTime: s(t.signSectionBeginTime),
|
|
62
|
+
isYellow: s(t.isYellow),
|
|
63
|
+
isPunchOpen: s(t.isPunchOpen),
|
|
64
|
+
isacross: s(t.isacross),
|
|
65
|
+
pre: s(t.pre),
|
|
66
|
+
signSection: s(t.signSection),
|
|
67
|
+
signbelongspan: s(t.signbelongspan),
|
|
68
|
+
active: s(t.active),
|
|
69
|
+
needSign: s(t.needSign),
|
|
70
|
+
workmins: s(t.workmins),
|
|
71
|
+
signbelong: s(t.signbelong),
|
|
72
|
+
reSign: s(t.reSign),
|
|
73
|
+
min_next: s(t.min_next),
|
|
74
|
+
signfrom: s(t.signfrom),
|
|
75
|
+
serialid: s(t.serialid),
|
|
76
|
+
signAcross: s(t.signAcross),
|
|
77
|
+
signAcross_next: s(t.signAcross_next),
|
|
78
|
+
time: s(t.time),
|
|
79
|
+
signTime: s(t.signTime),
|
|
80
|
+
position: s(t.position),
|
|
81
|
+
isfirstsign: s(t.isfirstsign),
|
|
82
|
+
}));
|
|
83
|
+
return {
|
|
84
|
+
date: s(j.date),
|
|
85
|
+
now: s(j.now),
|
|
86
|
+
groupname: s(j.groupname),
|
|
87
|
+
lastname: s(j.lastname),
|
|
88
|
+
timeline,
|
|
89
|
+
raw: j,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 解析本次允许操作的打卡时段。
|
|
94
|
+
*
|
|
95
|
+
* getButtons 是当天考勤页的状态,不是历史补签查询接口。历史日期若在
|
|
96
|
+
* timeline 中不存在,绝不能回退到同卡种的当天记录,否则一次失败的补卡会
|
|
97
|
+
* 变成对今天记录的真实写入。
|
|
98
|
+
*/
|
|
99
|
+
export function resolvePunchItem(buttons, opts) {
|
|
100
|
+
const targetDate = String(opts?.date ?? buttons?.date ?? '');
|
|
101
|
+
const type = String(opts?.type ?? '');
|
|
102
|
+
const currentDate = String(buttons?.date ?? '');
|
|
103
|
+
if (!currentDate)
|
|
104
|
+
throw new Error('考勤接口未返回当前日期,无法安全打卡');
|
|
105
|
+
if (targetDate !== currentDate) {
|
|
106
|
+
throw new Error(`当前考勤接口只支持当天(${currentDate})打卡;不能安全处理 ${targetDate || '未指定日期'} 的历史补卡,请在 OA 考勤补签流程办理`);
|
|
107
|
+
}
|
|
108
|
+
const item = (buttons?.timeline ?? []).find((t) => String(t.date) === targetDate && String(t.type) === type);
|
|
109
|
+
if (!item)
|
|
110
|
+
throw new Error(`未找到当天打卡时段(date=${targetDate}, type=${type}),已停止执行以避免误写其他记录`);
|
|
111
|
+
return { targetDate, item };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 打卡。链路:getButtons(取 timeline 班次参数)→ 生成 encryptObj → punchButton。
|
|
115
|
+
* - type=on 上班卡 / off 下班卡,按 date(默认今天)匹配 timeline 时段。
|
|
116
|
+
* - encryptObj = [Date.now(), userid, signSecret(userid+groupId)+"ver"](前端 signFunc.js 算法)。
|
|
117
|
+
* - signTime = 当前时刻 HH:mm:ss(或传入)。
|
|
118
|
+
* 服务端只校验 md5("wea"+uid+gid) 防篡改,不校验 timestamp/会话,token 鉴权可行。
|
|
119
|
+
*/
|
|
120
|
+
export async function punch(cfg, opts) {
|
|
121
|
+
const api = await createApiFromProfile(cfg);
|
|
122
|
+
const buttons = await getButtons(cfg);
|
|
123
|
+
const { item } = resolvePunchItem(buttons, opts);
|
|
124
|
+
// userid:优先传入;否则从 config.cookie 的 loginuuids 解析(token-login 写回 cookie 含用户 id)
|
|
125
|
+
const userid = opts.userid ?? parseUserIdFromCookie(cfg.cookie) ?? '';
|
|
126
|
+
if (!userid) {
|
|
127
|
+
throw new Error('未取到 userid:punch 需 --userid,或 config.cookie 须含 loginuuids(token-login 会写回)');
|
|
128
|
+
}
|
|
129
|
+
const groupId = opts.groupId ?? '1';
|
|
130
|
+
// punchButton 的 signTime 是本次打卡时刻,不是修改既有打卡时间的参数。
|
|
131
|
+
// 始终现算,避免调用方把历史或任意时间写入当天记录。
|
|
132
|
+
const signTime = nowHHmmss();
|
|
133
|
+
const encryptObj = buildEncryptObj(userid, groupId, opts.timestamp);
|
|
134
|
+
const body = {
|
|
135
|
+
date: item.date,
|
|
136
|
+
belongtime: item.belongtime,
|
|
137
|
+
type: item.type,
|
|
138
|
+
canSignTime: item.canSignTime,
|
|
139
|
+
across: item.across,
|
|
140
|
+
belongdate: item.belongdate,
|
|
141
|
+
datetime: item.datetime,
|
|
142
|
+
min: item.min,
|
|
143
|
+
islastsign: item.islastsign,
|
|
144
|
+
signSectionTime: item.signSectionTime,
|
|
145
|
+
signSectionBeginTime: item.signSectionBeginTime ?? '',
|
|
146
|
+
isYellow: item.isYellow,
|
|
147
|
+
isPunchOpen: item.isPunchOpen,
|
|
148
|
+
isacross: item.isacross,
|
|
149
|
+
pre: item.pre ?? '0',
|
|
150
|
+
signSection: item.signSection,
|
|
151
|
+
signbelongspan: item.signbelongspan ?? '今天',
|
|
152
|
+
active: item.active ?? '0',
|
|
153
|
+
needSign: item.needSign ?? '0',
|
|
154
|
+
workmins: item.workmins,
|
|
155
|
+
signbelong: item.signbelong ?? '今天',
|
|
156
|
+
reSign: item.reSign ?? '1',
|
|
157
|
+
min_next: item.min_next,
|
|
158
|
+
signfrom: item.signfrom ?? 'e9pc',
|
|
159
|
+
serialid: item.serialid,
|
|
160
|
+
signAcross: item.signAcross,
|
|
161
|
+
signAcross_next: item.signAcross_next,
|
|
162
|
+
time: item.time,
|
|
163
|
+
position: '',
|
|
164
|
+
longitude: '',
|
|
165
|
+
latitude: '',
|
|
166
|
+
sid: '',
|
|
167
|
+
mac: '',
|
|
168
|
+
deviceId: '',
|
|
169
|
+
deviceInfo: '',
|
|
170
|
+
signTime,
|
|
171
|
+
encryptObj,
|
|
172
|
+
...(item.isfirstsign !== undefined ? { isfirstsign: item.isfirstsign } : {}),
|
|
173
|
+
};
|
|
174
|
+
const r = await api.post('/api/hrm/kq/attendanceButton/punchButton', body);
|
|
175
|
+
const j = r.json || {};
|
|
176
|
+
return {
|
|
177
|
+
success: s(j.success),
|
|
178
|
+
message: s(j.message),
|
|
179
|
+
signtime: s(j.signtime),
|
|
180
|
+
signdate: s(j.signdate),
|
|
181
|
+
kqstatus: s(j.kqstatus),
|
|
182
|
+
status: s(j.status),
|
|
183
|
+
raw: j,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
// ───────────────── 工具 ─────────────────
|
|
187
|
+
function s(v) {
|
|
188
|
+
if (v === undefined || v === null)
|
|
189
|
+
return '';
|
|
190
|
+
return String(v);
|
|
191
|
+
}
|
|
192
|
+
/** 从 cookie 串解析 loginuuids(当前登录用户 id)。token-login 写回 cookie 含此字段。 */
|
|
193
|
+
function parseUserIdFromCookie(cookie) {
|
|
194
|
+
if (!cookie)
|
|
195
|
+
return '';
|
|
196
|
+
const m = cookie.match(/loginuuids=([^;]+)/);
|
|
197
|
+
return m ? m[1].trim() : '';
|
|
198
|
+
}
|
|
199
|
+
/** 当前时刻 HH:mm:ss(本地时区)。 */
|
|
200
|
+
function nowHHmmss() {
|
|
201
|
+
const d = new Date();
|
|
202
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
203
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
204
|
+
}
|
package/dist/links.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function baseUrl(base) {
|
|
2
|
+
return String(base ?? '').replace(/\/+$/, '');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function buildBlogMyUrl(base) {
|
|
6
|
+
return `${baseUrl(base)}/wui/index.html#/main/blog/myBlog?menuIds=392,393&menuPathIds=392,393`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function buildCubeCardUrl(base, { modeId, formId, billid, customid }) {
|
|
10
|
+
if ([modeId, formId, billid, customid].some((value) => value === undefined || value === null || value === ''))
|
|
11
|
+
return undefined;
|
|
12
|
+
return `${baseUrl(base)}/spa/cube/index.html#/main/cube/card?type=0&modeId=${modeId}&formId=${formId}&billid=${billid}&opentype=0&customid=${customid}&viewfrom=fromsearchlist`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildCubeSearchUrl(base, customid) {
|
|
16
|
+
if (customid === undefined || customid === null || customid === '')
|
|
17
|
+
return undefined;
|
|
18
|
+
return `${baseUrl(base)}/spa/cube/index.html#/main/cube/search?customid=${encodeURIComponent(String(customid))}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 把 E9 菜单返回的相对地址补成可点击的绝对地址。 */
|
|
22
|
+
export function normalizeE9Url(base, value) {
|
|
23
|
+
const raw = String(value ?? '').trim();
|
|
24
|
+
if (!raw)
|
|
25
|
+
return undefined;
|
|
26
|
+
if (/^https?:\/\//i.test(raw))
|
|
27
|
+
return raw;
|
|
28
|
+
const b = baseUrl(base);
|
|
29
|
+
if (raw.startsWith('#/'))
|
|
30
|
+
return `${b}/spa/cube/index.html${raw}`;
|
|
31
|
+
if (raw.startsWith('/main/cube/'))
|
|
32
|
+
return `${b}/spa/cube/index.html#${raw}`;
|
|
33
|
+
return `${b}/${raw.replace(/^\/+/, '')}`;
|
|
34
|
+
}
|
package/dist/odoc.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E9 公文(odoc)模块。
|
|
3
|
+
*
|
|
4
|
+
* 抓包+实跑确认的接口(测试环境):
|
|
5
|
+
* POST /api/odoc/odocCustomPage/getCustomPageList 公文待办/待阅列表入口(拿 dataKey,customListId/customTabId 定列表与 tab)
|
|
6
|
+
* POST /api/ec/dev/table/datas 取列+行(同待办 table 链路第 2 步)
|
|
7
|
+
* POST /api/ec/dev/table/counts 取总数(同待办 table 链路第 3 步)
|
|
8
|
+
* POST /api/odoc/odocTodoList/getTodoCount 待办/待阅计数(发文/收文/签报,按 officalType 分)
|
|
9
|
+
* POST /api/odoc/odocCenter/createOfficalList 发文/收文流程列表(officalType=1 发文拟稿 / 2 收文登记,结构同 create 列表)
|
|
10
|
+
* POST /api/odoc/odocCenter/getCenterInfo 公文中心首页两个新建入口(发文拟稿/收文登记)
|
|
11
|
+
* POST /api/odoc/odocCenter/getCountByOdocType 按公文种类计数
|
|
12
|
+
* POST /api/odoc/odocCenter/getCountByTopicType 按主题分类计数
|
|
13
|
+
*
|
|
14
|
+
* 走当前 baseUrl(单一环境,所有功能同地址),aiAgentToken 鉴权(e9pc 不支持 token 时回退 cookie)。
|
|
15
|
+
*/
|
|
16
|
+
import { createApiFromProfile } from './client.js';
|
|
17
|
+
import { pickByPaths, stripHtml, extractDataKey, extractColumns, extractRows, extractTotal } from './parse.js';
|
|
18
|
+
const DATERANGE_EMPTY = '0,,'; // type=0(不过滤), from 空, to 空
|
|
19
|
+
/**
|
|
20
|
+
* 公文待办/待阅列表。3 步 table 链路(POST,form-urlencoded),同待办/workplan-search:
|
|
21
|
+
* 1. /api/odoc/odocCustomPage/getCustomPageList(过滤项 + customListId/customTabId)→ dataKey(sessionkey 字段)
|
|
22
|
+
* 2. /api/ec/dev/table/datas(dataKey + current + sortParams=[])→ columns + 行
|
|
23
|
+
* 3. /api/ec/dev/table/counts(dataKey)→ 总数(失败不阻断)
|
|
24
|
+
* 行字段:原值 + `<字段>span` 渲染值(去 HTML)。
|
|
25
|
+
*/
|
|
26
|
+
export async function listOdocTodo(cfg, opts = {}) {
|
|
27
|
+
const api = await createApiFromProfile(cfg);
|
|
28
|
+
const filters = {
|
|
29
|
+
btnType: opts.btnType ?? '1',
|
|
30
|
+
readstatus: opts.readStatus ?? '',
|
|
31
|
+
odocSubject: opts.odocSubject ?? '',
|
|
32
|
+
sendnode: '',
|
|
33
|
+
topictype: opts.topicType ?? '',
|
|
34
|
+
secretlevel: opts.secretLevel ?? '',
|
|
35
|
+
send_unit: opts.sendUnit ?? '',
|
|
36
|
+
issueduserid: opts.issuedUserid ?? '',
|
|
37
|
+
writtendateselect: opts.writtenDateSelect ?? DATERANGE_EMPTY,
|
|
38
|
+
ownerid: opts.ownerId ?? '',
|
|
39
|
+
createdateselect: opts.createDateSelect ?? DATERANGE_EMPTY,
|
|
40
|
+
lastmodifydateselect: opts.lastModifyDateSelect ?? DATERANGE_EMPTY,
|
|
41
|
+
dealinguserid: opts.dealingUserid ?? '',
|
|
42
|
+
undealeduserid: opts.undealedUserid ?? '',
|
|
43
|
+
docabstract: opts.docAbstract ?? '',
|
|
44
|
+
opendateselect: opts.openDateSelect ?? DATERANGE_EMPTY,
|
|
45
|
+
deadlineselect: opts.deadlineSelect ?? DATERANGE_EMPTY,
|
|
46
|
+
receivenum: opts.receiveNum ?? '',
|
|
47
|
+
receivedateselect: opts.receiveDateSelect ?? DATERANGE_EMPTY,
|
|
48
|
+
workflowid: opts.workflowId ?? '',
|
|
49
|
+
requestname: opts.requestName ?? '',
|
|
50
|
+
dispatchnum: opts.dispatchNum ?? '',
|
|
51
|
+
urgencydegree: opts.urgencyDegree ?? '',
|
|
52
|
+
receive_unit: opts.receiveUnit ?? '',
|
|
53
|
+
issueddateselect: opts.issuedDateSelect ?? DATERANGE_EMPTY,
|
|
54
|
+
departmentid: opts.departmentId ?? '',
|
|
55
|
+
odoctype: opts.odocType ?? '',
|
|
56
|
+
customListId: opts.customListId ?? '-12',
|
|
57
|
+
customTabId: opts.customTabId ?? '-24',
|
|
58
|
+
};
|
|
59
|
+
const r1 = await api.post('/api/odoc/odocCustomPage/getCustomPageList', filters);
|
|
60
|
+
const dataKey = extractDataKey(r1.json);
|
|
61
|
+
if (!dataKey) {
|
|
62
|
+
throw new Error(`odoc list 未拿到 dataKey,getCustomPageList 响应: ${(r1.text ?? '').slice(0, 200)}`);
|
|
63
|
+
}
|
|
64
|
+
const page = opts.page ?? 1;
|
|
65
|
+
const r2 = await api.post('/api/ec/dev/table/datas', {
|
|
66
|
+
dataKey,
|
|
67
|
+
current: String(page),
|
|
68
|
+
sortParams: '[]',
|
|
69
|
+
// table/datas 认 pageSize(同待办链路):显式 10 条/页,避免服务端默认 30 条/页导致
|
|
70
|
+
// --page N 按 30 条跳页、本页第 11~30 条看不到的错位。
|
|
71
|
+
pageSize: opts.pageSize ?? '10',
|
|
72
|
+
});
|
|
73
|
+
const columns = extractColumns(r2.json);
|
|
74
|
+
const rows = extractRows(r2.json);
|
|
75
|
+
const items = rows.map((r) => normalizeOdocRow(r)).filter((it) => it.requestId || it.requestName);
|
|
76
|
+
const pageSize = pickByPaths(r2.json, ['pageSize', 'data.pageSize']);
|
|
77
|
+
let total = null;
|
|
78
|
+
let countsResp = null;
|
|
79
|
+
try {
|
|
80
|
+
const r3 = await api.post('/api/ec/dev/table/counts', { dataKey });
|
|
81
|
+
countsResp = r3.json;
|
|
82
|
+
total = extractTotal(r3.json);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// counts 失败不阻断
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
dataKey,
|
|
89
|
+
page,
|
|
90
|
+
total,
|
|
91
|
+
pageSize: typeof pageSize === 'number' ? pageSize : null,
|
|
92
|
+
columns,
|
|
93
|
+
items,
|
|
94
|
+
raw: { getCustomPageList: r1.json, datas: r2.json, counts: countsResp },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** 解析公文待办表行(原值 + span 渲染值)。 */
|
|
98
|
+
export function normalizeOdocRow(row) {
|
|
99
|
+
if (!row || typeof row !== 'object')
|
|
100
|
+
return {};
|
|
101
|
+
const get = (keys) => pickByPaths(row, keys);
|
|
102
|
+
const spanOr = (dataIndex, extraKeys = []) => stripHtml(get([`${dataIndex}span`, dataIndex, ...extraKeys]));
|
|
103
|
+
return {
|
|
104
|
+
requestId: stringify(get(['requestid', 'randomField2'])),
|
|
105
|
+
requestName: stripHtml(get(['requestnamespan'])) || stringify(get(['requestname'])),
|
|
106
|
+
odocType: stringify(get(['odoctype'])),
|
|
107
|
+
odocTypeName: stripHtml(get(['odoctypespan'])),
|
|
108
|
+
issuedNum: stringify(get(['issued_num', 'issued_numspan'])),
|
|
109
|
+
receiveNum: stripHtml(get(['receivenumspan'])) || stringify(get(['receivenum'])),
|
|
110
|
+
topicType: stringify(get(['topictype'])),
|
|
111
|
+
urgencyDegree: stringify(get(['urgencydegree'])),
|
|
112
|
+
secretLevel: stripHtml(get(['secretlevelspan'])) || stringify(get(['secretlevel'])),
|
|
113
|
+
issuedUnit: stringify(get(['issued_unit'])),
|
|
114
|
+
receiveUnit: stripHtml(get(['receiveunitspan'])) || stringify(get(['receiveunit'])),
|
|
115
|
+
issuedUserid: stripHtml(get(['issued_useridspan'])) || stringify(get(['issued_userid'])),
|
|
116
|
+
writtenDate: stripHtml(get(['written_datespan'])) || stringify(get(['written_date'])),
|
|
117
|
+
issuedDate: stringify(get(['issued_date'])),
|
|
118
|
+
createrId: stripHtml(get(['createridspan'])) || stringify(get(['createrid', 'creater'])),
|
|
119
|
+
createDate: stringify(get(['createdate'])),
|
|
120
|
+
sendDepartment: stringify(get(['senddepartment'])),
|
|
121
|
+
lastOperateDate: stringify(get(['lastoperatedate'])),
|
|
122
|
+
currentNodeId: stripHtml(get(['currentnodeidspan'])) || stringify(get(['currentnodeid'])),
|
|
123
|
+
currentNode: stripHtml(get(['currentnodespan'])) || stringify(get(['currentnode'])),
|
|
124
|
+
zbr: stringify(get(['zbr'])),
|
|
125
|
+
wbr: stringify(get(['wbr'])),
|
|
126
|
+
docAbstract: stripHtml(get(['docabstractspan'])) || stringify(get(['docabstract'])),
|
|
127
|
+
deadline: stripHtml(get(['deadlinespan'])) || stringify(get(['deadline'])),
|
|
128
|
+
openDate: stripHtml(get(['opendatespan'])) || stringify(get(['opendate'])),
|
|
129
|
+
receiveDate: stripHtml(get(['receivedatespan'])) || stringify(get(['receivedate'])),
|
|
130
|
+
operateDate: stripHtml(get(['operatedatespan'])) || stringify(get(['operatedate'])),
|
|
131
|
+
workflowId: stringify(get(['workflowid', 'randomField0'])),
|
|
132
|
+
raw: row,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* 公文待办/待阅计数。POST /api/odoc/odocTodoList/getTodoCount。
|
|
137
|
+
* - view=false(默认):待办计数(iscomplete=0)
|
|
138
|
+
* - view=true:待阅计数(isview=0)
|
|
139
|
+
* 返回 datalist:[{officalType, title, count}](发文/收文/签报)。
|
|
140
|
+
*/
|
|
141
|
+
export async function getOdocTodoCount(cfg, opts = {}) {
|
|
142
|
+
const api = await createApiFromProfile(cfg);
|
|
143
|
+
const body = opts.view
|
|
144
|
+
? { isview: '0', isNew: 'false' }
|
|
145
|
+
: { iscomplete: '0', isNew: 'false' };
|
|
146
|
+
const r = await api.post('/api/odoc/odocTodoList/getTodoCount', body);
|
|
147
|
+
const j = r.json || {};
|
|
148
|
+
const arr = pickByPaths(j, ['datalist', 'data.datalist']) ?? [];
|
|
149
|
+
const items = (Array.isArray(arr) ? arr : [])
|
|
150
|
+
.map((d) => ({
|
|
151
|
+
officalType: stringify(d?.officalType) ?? '',
|
|
152
|
+
title: stringify(d?.title) ?? '',
|
|
153
|
+
count: typeof d?.count === 'number' ? d.count : Number(d?.count ?? 0),
|
|
154
|
+
}))
|
|
155
|
+
.filter((it) => it.title);
|
|
156
|
+
return { view: !!opts.view, items, raw: j };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* 发文/收文流程列表。POST /api/odoc/odocCenter/createOfficalList。
|
|
160
|
+
* - officalType=1(默认):发文拟稿
|
|
161
|
+
* - officalType=2:收文登记
|
|
162
|
+
* 返回 datas[{typeName, id, wfCount, wfbeans[]}],结构同 create 列表,
|
|
163
|
+
* 故"新建公文"可复用 create start(wfstart.ts)的发起链路。
|
|
164
|
+
*/
|
|
165
|
+
export async function listOdocCreate(cfg, opts = {}) {
|
|
166
|
+
const api = await createApiFromProfile(cfg);
|
|
167
|
+
const officalType = opts.officalType ?? '1';
|
|
168
|
+
const r = await api.post('/api/odoc/odocCenter/createOfficalList', { officalType });
|
|
169
|
+
const j = r.json || {};
|
|
170
|
+
const arr = pickByPaths(j, ['datas', 'data.datas']) ?? [];
|
|
171
|
+
const toplabel = stringify(pickByPaths(j, ['createwflabels.toplabel']));
|
|
172
|
+
const categories = (Array.isArray(arr) ? arr : [])
|
|
173
|
+
.map((d) => ({
|
|
174
|
+
id: stringify(d?.id),
|
|
175
|
+
name: stringify(d?.typeName),
|
|
176
|
+
wfCount: typeof d?.wfCount === 'number' ? d.wfCount : Number(d?.wfCount ?? 0),
|
|
177
|
+
workflows: (Array.isArray(d?.wfbeans) ? d.wfbeans : [])
|
|
178
|
+
.map((w) => ({
|
|
179
|
+
id: stringify(w?.id),
|
|
180
|
+
name: stringify(w?.name),
|
|
181
|
+
typeId: stringify(w?.typeId),
|
|
182
|
+
spell: stringify(w?.spell),
|
|
183
|
+
letter: stringify(w?.letter),
|
|
184
|
+
}))
|
|
185
|
+
.filter((w) => w.id || w.name),
|
|
186
|
+
}))
|
|
187
|
+
.filter((c) => c.workflows.length > 0);
|
|
188
|
+
return { officalType, toplabel, categories, raw: j };
|
|
189
|
+
}
|
|
190
|
+
function stringify(v) {
|
|
191
|
+
if (v === undefined || v === null || v === '')
|
|
192
|
+
return undefined;
|
|
193
|
+
return String(v);
|
|
194
|
+
}
|
package/dist/pageopen.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { endpointFor } from './config.js';
|
|
2
|
+
|
|
3
|
+
const PAGE_MAP = {
|
|
4
|
+
center: '/wui/index.html#/main/workflow/index',
|
|
5
|
+
todo: '/wui/index.html#/main/workflow/doing',
|
|
6
|
+
done: '/wui/index.html#/main/workflow/done',
|
|
7
|
+
mine: '/wui/index.html#/main/workflow/mine',
|
|
8
|
+
draft: '/wui/index.html#/main/workflow/draft',
|
|
9
|
+
all: '/wui/index.html#/main/workflow/all',
|
|
10
|
+
newflow: '/spa/workflow/static4form/index.html#/main/workflow/req?iscreate=1',
|
|
11
|
+
monitor: '/wui/index.html#/main/workflow/monitor',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function pageUrl(cfg, page, requestId) {
|
|
15
|
+
const base = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
|
|
16
|
+
if (page === 'view') {
|
|
17
|
+
if (!requestId)
|
|
18
|
+
throw new Error('打开流程详情需要 --request-id <请求 ID>。');
|
|
19
|
+
return `${base}/spa/workflow/static4form/index.html#/main/workflow/req?iscreate=0&requestid=${encodeURIComponent(requestId)}`;
|
|
20
|
+
}
|
|
21
|
+
const path = PAGE_MAP[page];
|
|
22
|
+
if (!path)
|
|
23
|
+
throw new Error(`不支持的页面“${page}”。可选:${Object.keys(PAGE_MAP).join('、')}、view。`);
|
|
24
|
+
return base + path;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function availablePages() {
|
|
28
|
+
return [...Object.keys(PAGE_MAP), 'view'];
|
|
29
|
+
}
|
package/dist/parse.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E9 ec/dev/table/datas 响应解析。
|
|
3
|
+
*
|
|
4
|
+
* 实跑确认的结构:
|
|
5
|
+
* - splitPageKey 响应里 dataKey 在 `sessionkey` 字段(小写)。
|
|
6
|
+
* - datas 响应顶层含 `columns`(列定义,dataIndex/title/dbField)和 `datas`(行数组,50 行/页)。
|
|
7
|
+
* - 每行同时有原始字段(如 creater="1" 是 id)和 `<field>span` 渲染字段(含可读名称 / HTML)。
|
|
8
|
+
* 可读值优先取 span 字段并去 HTML 标签。
|
|
9
|
+
* - counts 响应 `{"count":N,"status":true}`。
|
|
10
|
+
*/
|
|
11
|
+
/** 从一组候选路径里取第一个非空值,路径用点号分隔、支持下标。 */
|
|
12
|
+
export function pickByPaths(obj, paths) {
|
|
13
|
+
if (obj == null)
|
|
14
|
+
return undefined;
|
|
15
|
+
for (const p of paths) {
|
|
16
|
+
const v = getPath(obj, p);
|
|
17
|
+
if (v !== undefined && v !== null && v !== '')
|
|
18
|
+
return v;
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
function getPath(obj, path) {
|
|
23
|
+
const tokens = path.replace(/\[(\d+)\]/g, '.$1').split('.').filter((t) => t.length > 0);
|
|
24
|
+
let cur = obj;
|
|
25
|
+
for (const t of tokens) {
|
|
26
|
+
if (cur == null)
|
|
27
|
+
return undefined;
|
|
28
|
+
cur = cur[t];
|
|
29
|
+
}
|
|
30
|
+
return cur;
|
|
31
|
+
}
|
|
32
|
+
/** 从 getWfListParams / splitPageKey 响应里提取 dataKey(sessionkey)。 */
|
|
33
|
+
export function extractDataKey(resp) {
|
|
34
|
+
const v = pickByPaths(resp, [
|
|
35
|
+
'sessionkey',
|
|
36
|
+
'data.sessionkey',
|
|
37
|
+
'sessionKey',
|
|
38
|
+
'data.sessionKey',
|
|
39
|
+
'dataKey',
|
|
40
|
+
'data.dataKey',
|
|
41
|
+
'data',
|
|
42
|
+
]);
|
|
43
|
+
if (typeof v === 'string' && v.trim())
|
|
44
|
+
return v.trim();
|
|
45
|
+
return scanForDataKey(resp);
|
|
46
|
+
}
|
|
47
|
+
function scanForDataKey(obj) {
|
|
48
|
+
if (obj == null)
|
|
49
|
+
return null;
|
|
50
|
+
if (typeof obj === 'string') {
|
|
51
|
+
if (/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}_[A-F0-9]{32}$/.test(obj)) {
|
|
52
|
+
return obj;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(obj)) {
|
|
57
|
+
for (const it of obj) {
|
|
58
|
+
const r = scanForDataKey(it);
|
|
59
|
+
if (r)
|
|
60
|
+
return r;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
if (typeof obj === 'object') {
|
|
65
|
+
for (const v of Object.values(obj)) {
|
|
66
|
+
const r = scanForDataKey(v);
|
|
67
|
+
if (r)
|
|
68
|
+
return r;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
/** 提取列定义(可用于把行字段映射到中文标题)。 */
|
|
74
|
+
export function extractColumns(datasResp) {
|
|
75
|
+
const cols = pickByPaths(datasResp, ['columns', 'data.columns', 'result.columns']);
|
|
76
|
+
if (!Array.isArray(cols))
|
|
77
|
+
return [];
|
|
78
|
+
return cols.map((c) => ({ dataIndex: c?.dataIndex, title: c?.title, dbField: c?.dbField })).filter((c) => c.dataIndex);
|
|
79
|
+
}
|
|
80
|
+
/** 从 datas 响应里提取行数组。 */
|
|
81
|
+
export function extractRows(datasResp) {
|
|
82
|
+
const arr = pickByPaths(datasResp, ['datas', 'data.datas', 'rows', 'data.rows', 'data', 'list']);
|
|
83
|
+
if (Array.isArray(arr))
|
|
84
|
+
return arr;
|
|
85
|
+
if (arr && typeof arr === 'object' && !Array.isArray(arr)) {
|
|
86
|
+
const vals = Object.values(arr).filter((v) => v && typeof v === 'object');
|
|
87
|
+
if (vals.length)
|
|
88
|
+
return vals;
|
|
89
|
+
}
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
/** 从 counts 或 datas 响应里提取总数。 */
|
|
93
|
+
export function extractTotal(resp) {
|
|
94
|
+
const v = pickByPaths(resp, ['count', 'data.count', 'total', 'data.total', 'totalCount', 'data.totalCount', 'result.count']);
|
|
95
|
+
if (typeof v === 'number')
|
|
96
|
+
return v;
|
|
97
|
+
if (typeof v === 'string' && /^\d+$/.test(v))
|
|
98
|
+
return parseInt(v, 10);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
/** 去掉 HTML 标签与常见实体,得到纯文本。 */
|
|
102
|
+
export function stripHtml(s) {
|
|
103
|
+
if (s == null)
|
|
104
|
+
return '';
|
|
105
|
+
let str = String(s);
|
|
106
|
+
str = str.replace(/<[^>]*>/g, '');
|
|
107
|
+
str = str
|
|
108
|
+
.replace(/ /g, ' ')
|
|
109
|
+
.replace(/&/g, '&')
|
|
110
|
+
.replace(/</g, '<')
|
|
111
|
+
.replace(/>/g, '>')
|
|
112
|
+
.replace(/"/g, '"')
|
|
113
|
+
.replace(/'/g, "'");
|
|
114
|
+
return str.trim();
|
|
115
|
+
}
|
|
116
|
+
const LEVEL_LABEL = {
|
|
117
|
+
'0': '一般',
|
|
118
|
+
'1': '重要',
|
|
119
|
+
'2': '紧急',
|
|
120
|
+
'3': '紧急',
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* 把一行数据映射成标准待办项。
|
|
124
|
+
* 可读值优先取 `<field>span`(去 HTML),其次原始字段。
|
|
125
|
+
*/
|
|
126
|
+
export function normalizeRow(row, columns) {
|
|
127
|
+
if (!row || typeof row !== 'object')
|
|
128
|
+
return {};
|
|
129
|
+
void columns; // 列定义目前仅用于 extractColumns;字段名已稳定,直接按已知 dataIndex 取
|
|
130
|
+
const get = (keys) => pickByPaths(row, keys);
|
|
131
|
+
const spanOr = (dataIndex, extraKeys = []) => stripHtml(get([`${dataIndex}span`, dataIndex, ...extraKeys]));
|
|
132
|
+
// requestnamespan 是 OA 已按管理员模板渲染的可见标题;原始 requestname 仅作回退。
|
|
133
|
+
const rawTitle = stringify(get(['requestname', 'requestName']));
|
|
134
|
+
const title = stripHtml(get(['requestnamespan'])) || rawTitle;
|
|
135
|
+
const levelRaw = stringify(get(['requestlevel', 'level'])) ?? '';
|
|
136
|
+
const level = levelRaw ? (LEVEL_LABEL[levelRaw] ?? levelRaw) : undefined;
|
|
137
|
+
const creatorIdRaw = get(['createrid', 'creatorid', 'creatorId', 'creater']);
|
|
138
|
+
const creatorId = /^\d+$/.test(String(creatorIdRaw ?? '').trim()) ? String(creatorIdRaw).trim() : undefined;
|
|
139
|
+
return {
|
|
140
|
+
requestId: stringify(get(['requestid', 'requestId', 'id', 'randomField2', 'randomFieldId'])),
|
|
141
|
+
title,
|
|
142
|
+
rawTitle,
|
|
143
|
+
creator: spanOr('creater', ['createrName', 'creator', 'createName']),
|
|
144
|
+
creatorId,
|
|
145
|
+
createdate: spanOr('createdate', ['createdatespan', 'createDate']),
|
|
146
|
+
receivedate: spanOr('receivedate', ['receivedate', 'recieveDate']),
|
|
147
|
+
node: stripHtml(get(['currentnodeidspan', 'currentnodespan'])) || stringify(get(['currentnodeid', 'nodeName', 'nodename'])),
|
|
148
|
+
status: stripHtml(get(['statusspan'])) || stringify(get(['status'])),
|
|
149
|
+
level,
|
|
150
|
+
requestmark: stringify(get(['requestmark'])),
|
|
151
|
+
currentOperator: spanOr('unoperators', ['unoperatorspan', 'currentoperator', 'operator']),
|
|
152
|
+
raw: row,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function stringify(v) {
|
|
156
|
+
if (v === undefined || v === null || v === '')
|
|
157
|
+
return undefined;
|
|
158
|
+
return String(v);
|
|
159
|
+
}
|