weaver-e9-assistant-cli 1.1.1 → 1.1.2
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/blog.js +37 -7
- package/dist/cli.js +34 -9
- package/dist/subordinate.js +433 -419
- package/dist/workflow.js +33 -19
- package/package.json +1 -1
package/dist/blog.js
CHANGED
|
@@ -85,12 +85,14 @@ export function isBlogRecalled(item) {
|
|
|
85
85
|
// ─── 微博首页列表(支持组织视图)─────────────────────────
|
|
86
86
|
/**
|
|
87
87
|
* 获取微博首页列表,支持按 scope 查看不同范围。
|
|
88
|
-
* scope
|
|
89
|
-
*
|
|
88
|
+
* scope 映射(groupId,抓包实证不同 scope 走不同接口):
|
|
89
|
+
* 全部关注/关注/下级(-1)、特别关注(-5) → getBlogHomepageList
|
|
90
|
+
* 未读(-2) → getMyBlogHomepageNew
|
|
91
|
+
* 分享给我/共享(-3) → getWeiboPageShareToMeList
|
|
90
92
|
*/
|
|
91
93
|
export async function getBlogHomepageList(cfg, opts = {}) {
|
|
92
94
|
const api = await createApiFromProfile(cfg);
|
|
93
|
-
const scopeMap = { '全部': '-1', '关注': '-2', '共享': '-3', '
|
|
95
|
+
const scopeMap = { '全部': '-1', '全部关注': '-1', '关注': '-1', '下级': '-1', '未读': '-2', '共享': '-3', '分享给我': '-3', '特别关注': '-5', 'all': '-1', 'attention': '-1', 'subordinate': '-1', 'sub': '-1', 'unread': '-2', 'share': '-3', 'special': '-5' };
|
|
94
96
|
const groupId = scopeMap[opts.scope] ?? opts.scope ?? '-1';
|
|
95
97
|
const endDate = opts.endDate ?? todayStr();
|
|
96
98
|
// 默认查最近 30 天
|
|
@@ -106,11 +108,34 @@ export async function getBlogHomepageList(cfg, opts = {}) {
|
|
|
106
108
|
currentPage: opts.page ?? 1,
|
|
107
109
|
pageSize: 10,
|
|
108
110
|
};
|
|
109
|
-
const
|
|
111
|
+
const endpoint = groupId === '-2' ? '/api/blog/base/getMyBlogHomepageNew'
|
|
112
|
+
: groupId === '-3' ? '/api/blog/base/getWeiboPageShareToMeList'
|
|
113
|
+
: '/api/blog/base/getBlogHomepageList';
|
|
114
|
+
const r = await api.get(endpoint, params);
|
|
110
115
|
const all = extractBlogItems(r.json);
|
|
111
116
|
const items = all.filter((it) => it.id || it.content);
|
|
112
117
|
return { scope: opts.scope ?? '全部', groupId, startDate, endDate, page: opts.page ?? 1, total: r.json.total ?? items.length, items, raw: r.json };
|
|
113
118
|
}
|
|
119
|
+
// ─── 分享给我的人(用户列表)─────────────────────────────
|
|
120
|
+
/**
|
|
121
|
+
* 获取「分享给我」的用户列表(区别于 getWeiboPageShareToMeList 查分享给我的日报内容)。
|
|
122
|
+
* GET /api/blog/base/getShareToMeUserList?groupId=all
|
|
123
|
+
*/
|
|
124
|
+
export async function getShareToMeUserList(cfg, opts = {}) {
|
|
125
|
+
const api = await createApiFromProfile(cfg);
|
|
126
|
+
const r = await api.get('/api/blog/base/getShareToMeUserList', { groupId: opts.groupId ?? 'all' });
|
|
127
|
+
const j = r.json || {};
|
|
128
|
+
const items = (j.shareToMeUserList || []).map((u) => ({
|
|
129
|
+
userId: stringify(u.userId),
|
|
130
|
+
username: stringify(u.username),
|
|
131
|
+
deptName: stringify(u.deptName),
|
|
132
|
+
jobtitle: stringify(u.jobtitle),
|
|
133
|
+
isattention: stringify(u.isattention) === '1',
|
|
134
|
+
islower: stringify(u.islower) === '1',
|
|
135
|
+
})).filter((u) => u.userId);
|
|
136
|
+
return { count: j.shareToMeUserCount, items, raw: j };
|
|
137
|
+
}
|
|
138
|
+
|
|
114
139
|
// ─── 微博搜索 ────────────────────────────────────────────
|
|
115
140
|
export async function searchMyBlog(cfg, opts = {}) {
|
|
116
141
|
const api = await createApiFromProfile(cfg);
|
|
@@ -245,6 +270,7 @@ export async function getBlogWorkIndex(cfg) {
|
|
|
245
270
|
const r = await api.get('/api/blog/base/getBlogWorkIndex');
|
|
246
271
|
const j = r.json || {};
|
|
247
272
|
return {
|
|
273
|
+
schedule: { name: j.schedule?.name, title: j.schedule?.scheduleTitle, index: j.schedule?.scheduleIndex },
|
|
248
274
|
mood: { name: j.mood?.name, title: j.mood?.moodIndexTitle, index: j.mood?.moodIndex },
|
|
249
275
|
work: { name: j.work?.name, title: j.work?.workIndexTitle, index: j.work?.workIndex },
|
|
250
276
|
raw: j,
|
|
@@ -377,9 +403,13 @@ export async function getMsgRemindList(cfg) {
|
|
|
377
403
|
const j = r.json || {};
|
|
378
404
|
return {
|
|
379
405
|
items: (j.remindList || []).map((it) => ({
|
|
380
|
-
id: stringify(it.id),
|
|
381
|
-
|
|
382
|
-
|
|
406
|
+
id: stringify(it.id),
|
|
407
|
+
// 提醒内容在 msg 字段(含 HTML,需去标签);旧字段 content 仅兜底
|
|
408
|
+
content: stripHtml(it.msg) || stringify(it.content),
|
|
409
|
+
creator: stringify(it.creator ?? it.username),
|
|
410
|
+
createDate: stringify(it.createdate ?? it.createDate),
|
|
411
|
+
createTime: stringify(it.createtime),
|
|
412
|
+
type: stringify(it.remindType ?? it.remindtype ?? it.type),
|
|
383
413
|
// 关注申请专用字段(抓包实证):remindid=申请人 userid、relatedid=当前登录者、remindType=1
|
|
384
414
|
remindid: stringify(it.remindid),
|
|
385
415
|
relatedid: stringify(it.relatedid),
|
package/dist/cli.js
CHANGED
|
@@ -18,7 +18,7 @@ import { transferTodo } from './todotransfer.js';
|
|
|
18
18
|
import { forwardTodo, circulateTodo } from './todoforward.js';
|
|
19
19
|
import { ccTodo } from './todocc.js';
|
|
20
20
|
import { askIdeaTodo } from './todoaskidea.js';
|
|
21
|
-
import { getMyBlogList, getMyBlogAll, isBlogRecalled, createBlog, getBlogWorkIndex, getBlogMenuCountdata, getVisitorList, getBlogNote, getCurrentUserInfo, getMsgRemindList, getDefaultTemplate, getBlogAppList, getAtAllList, getBlogPageSearchCondition, getBlogHomepageList, searchMyBlog, getBlogPersonalInfo, sendRemindUnsubmit, addBlogReply, addOrDeleteZan, saveBlogNote, recallBlog, deleteBlogReply, attentionOpt, dealMsgRemind } from './blog.js';
|
|
21
|
+
import { getMyBlogList, getMyBlogAll, isBlogRecalled, createBlog, getBlogWorkIndex, getBlogMenuCountdata, getVisitorList, getBlogNote, getCurrentUserInfo, getMsgRemindList, getDefaultTemplate, getBlogAppList, getAtAllList, getBlogPageSearchCondition, getBlogHomepageList, searchMyBlog, getBlogPersonalInfo, sendRemindUnsubmit, addBlogReply, addOrDeleteZan, saveBlogNote, recallBlog, deleteBlogReply, attentionOpt, dealMsgRemind, getShareToMeUserList } from './blog.js';
|
|
22
22
|
import { getCreateWfList, buildCreateWfUrl, buildRequestFormUrl, findCreateWfByName } from './wfcreate.js';
|
|
23
23
|
import { describeFormFields } from './wfform.js';
|
|
24
24
|
import { getStartableFields, prepareCreateSubmission, submitCreateRequest, resolveWorkflowId, getOdocNextNodes, odocFlowNext } from './wfstart.js';
|
|
@@ -30,7 +30,7 @@ import { punch, getButtons, resolvePunchItem, signSecret, buildEncryptObj } from
|
|
|
30
30
|
import { collectQueryableBusiness } from './report.js';
|
|
31
31
|
import { listCube, getCubeCondition, getCubeInit, exportCubeExcel, getSearchColumnInfo, listCubeExcel, getCubeCountData, getCubeGroupData, getExpCardConfig, getModeBrowserOptions, getCardFields, submitCubeCard, deleteCubeData, printCubePdf, listCubeMenus, findCubeByName, fetchMenuMounts, previewCubeCard, fetchCubeRowsByIds, resolveCubeCondition, downloadCubeImportTemplate, importCubeExcel } from './cube.js';
|
|
32
32
|
import { getCurrentUser, getCurrentUserIsAdmin } from './wfuser.js';
|
|
33
|
-
import { getSubordinates, assertUserIdsInScope, assertNamesInScope } from './subordinate.js';
|
|
33
|
+
import { getSubordinates, assertUserIdsInScope, assertNamesInScope, isScopeEnforced } from './subordinate.js';
|
|
34
34
|
import { parseArgs, flagStr, flagBool } from './args.js';
|
|
35
35
|
import { todayStr } from './util.js';
|
|
36
36
|
import { BOX_TOP, DIVIDER } from './render.js';
|
|
@@ -60,7 +60,7 @@ function usage() {
|
|
|
60
60
|
' e9 todo batch-attention --request-ids "1,2" [--attention-id <类型id>] [--yes] 批量关注(先预览,默认加入首个关注类型)',
|
|
61
61
|
' e9 todo done [--page <n>] [--keyword <text>] [--workflow <id>] [--creator <id>] [--sort <spec>] [--sub-category <tab>] [--json] [--raw] 已办列表',
|
|
62
62
|
' e9 todo mine [--page <n>] [--keyword <text>] [--workflow <id>] [--creator <id>] [--sort <spec>] [--sub-category <tab>] [--json] [--raw] 我发起的流程',
|
|
63
|
-
' e9 blog list [--date YYYY-MM-DD] [--scope
|
|
63
|
+
' e9 blog list [--date YYYY-MM-DD] [--scope <全部|关注|下级|未读|共享|分享给我|特别关注>] [--page <n>] [--json] [--raw]',
|
|
64
64
|
' e9 blog search --content <关键词> [--start-date <YYYY-MM-DD>] [--end-date <YYYY-MM-DD>] [--page <n>] 搜索我的微博',
|
|
65
65
|
' e9 blog view --user-id <id> 查看某人微博主页',
|
|
66
66
|
' e9 blog remind --user-id <id> [--workdate YYYY-MM-DD] [--yes] 催办(提醒某人写日报,默认今天)',
|
|
@@ -68,6 +68,7 @@ function usage() {
|
|
|
68
68
|
' e9 blog like --id <微博id> [--owner <作者userid>] [--yes] 点赞/取消点赞(默认 dry-run;跨账号加 --owner)',
|
|
69
69
|
' e9 blog follow --id <目标userid> [--yes] 发起微博关注申请(对方同意后互关可访问主页)',
|
|
70
70
|
' e9 blog agree-follow --id <申请人userid> [--yes] 同意对方的关注申请',
|
|
71
|
+
' e9 blog share-users 分享给我的人(用户列表)',
|
|
71
72
|
' e9 blog check [--date YYYY-MM-DD] 查某天是否已写日报(按 isReCalled 区分正常发布/已撤回);默认今天,无正常发布记录退出码 1',
|
|
72
73
|
' e9 report collect [--date YYYY-MM-DD] [--json] 汇总当前环境可查询业务数据(非操作审计)',
|
|
73
74
|
' e9 blog create --content "<文本>" [--content-file <路径>] [--share-type N] [--date YYYY-MM-DD] [--discuss-id <id>] [--yes]',
|
|
@@ -1522,8 +1523,9 @@ async function runBlog(args) {
|
|
|
1522
1523
|
return;
|
|
1523
1524
|
}
|
|
1524
1525
|
console.log(`${BOX_TOP}`);
|
|
1525
|
-
console.log(`E9 微博 工作指数 & 心情指数`);
|
|
1526
|
+
console.log(`E9 微博 考勤指数 & 工作指数 & 心情指数`);
|
|
1526
1527
|
console.log(DIVIDER);
|
|
1528
|
+
console.log(` 考勤指数: ${wi.schedule.index ?? '?'} ${wi.schedule.title ?? ''}`);
|
|
1527
1529
|
console.log(` 工作指数: ${wi.work.index ?? '?'} ${wi.work.title ?? ''}`);
|
|
1528
1530
|
console.log(` 心情指数: ${wi.mood.index ?? '?'} ${wi.mood.title ?? ''}`);
|
|
1529
1531
|
console.log(DIVIDER);
|
|
@@ -1696,9 +1698,12 @@ async function runBlog(args) {
|
|
|
1696
1698
|
console.log('(无提醒)');
|
|
1697
1699
|
return;
|
|
1698
1700
|
}
|
|
1701
|
+
const TYPE_LABEL = { '1': '关注申请', '8': '系统提醒' };
|
|
1699
1702
|
for (let i = 0; i < r.items.length; i++) {
|
|
1700
1703
|
const it = r.items[i];
|
|
1701
|
-
|
|
1704
|
+
const label = TYPE_LABEL[it.remindType] ?? (it.type ? `类型${it.type}` : '提醒');
|
|
1705
|
+
const time = [it.createDate, it.createTime].filter(Boolean).join(' ');
|
|
1706
|
+
console.log(`${i + 1}. [${label}] ${it.content ?? ''}${time ? ` (${time})` : ''}`);
|
|
1702
1707
|
}
|
|
1703
1708
|
return;
|
|
1704
1709
|
}
|
|
@@ -1763,6 +1768,24 @@ async function runBlog(args) {
|
|
|
1763
1768
|
}
|
|
1764
1769
|
return;
|
|
1765
1770
|
}
|
|
1771
|
+
if (sub === 'share-users') {
|
|
1772
|
+
const r = await getShareToMeUserList(cfg);
|
|
1773
|
+
if (args.flags.json) {
|
|
1774
|
+
console.log(JSON.stringify(r, null, 2));
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
console.log(`E9 微博 分享给我的人 共 ${r.count ?? r.items.length} 人`);
|
|
1778
|
+
if (r.items.length === 0) {
|
|
1779
|
+
console.log('(无人分享给我)');
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
for (let i = 0; i < r.items.length; i++) {
|
|
1783
|
+
const it = r.items[i];
|
|
1784
|
+
const meta = [it.deptName, it.jobtitle, it.isattention ? '已关注' : null].filter(Boolean).join(' | ');
|
|
1785
|
+
console.log(`${i + 1}. ${it.username ?? '(未知)'}${meta ? ` (${meta})` : ''}`);
|
|
1786
|
+
}
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1766
1789
|
if (sub === 'search-fields') {
|
|
1767
1790
|
const r = await getBlogPageSearchCondition(cfg);
|
|
1768
1791
|
if (args.flags.json) {
|
|
@@ -1794,7 +1817,7 @@ async function runBlog(args) {
|
|
|
1794
1817
|
console.log(JSON.stringify(r.template, null, 2));
|
|
1795
1818
|
return;
|
|
1796
1819
|
}
|
|
1797
|
-
console.error(`blog 暂只支持 list / search / view / remind / reply / like / create / check / stats / note / recall / delete-reply / follow / agree-follow / user-info / reminders / apps / at-list / visitors / search-fields / template,收到: ${sub}`);
|
|
1820
|
+
console.error(`blog 暂只支持 list / search / view / remind / reply / like / create / check / stats / note / recall / delete-reply / follow / agree-follow / user-info / reminders / apps / at-list / visitors / share-users / search-fields / template,收到: ${sub}`);
|
|
1798
1821
|
process.exit(1);
|
|
1799
1822
|
}
|
|
1800
1823
|
async function runWorkplan(args) {
|
|
@@ -3655,7 +3678,8 @@ async function runCube(args) {
|
|
|
3655
3678
|
if (condRes.unresolved.length)
|
|
3656
3679
|
printConditionUnresolved(condRes.unresolved);
|
|
3657
3680
|
// ★越权防护(fail-closed):条件中存在无法确认是否按人过滤的条件(定义读取失败/未知 domkey)→ 硬拒绝
|
|
3658
|
-
|
|
3681
|
+
// 策略未启用(默认)时不拦,仅保留代码路径。
|
|
3682
|
+
if (isScopeEnforced() && condRes.unverifiablePerson)
|
|
3659
3683
|
throw new Error('拒绝越权查询:台账查询条件无法确认是否按人过滤(条件定义读取失败或含未识别字段)。为防越权,本次查询已拒绝,请稍后重试或改用中文字段名。');
|
|
3660
3684
|
// ★越权防护:人员条件(填报人/审批人/报销人/经手人等)目标必须 ∈ {本人 + 全部层级下属}
|
|
3661
3685
|
if (condRes.personConditions?.length)
|
|
@@ -3683,7 +3707,8 @@ async function runCube(args) {
|
|
|
3683
3707
|
if (condRes.unresolved.length)
|
|
3684
3708
|
printConditionUnresolved(condRes.unresolved);
|
|
3685
3709
|
// ★越权防护(fail-closed):条件中存在无法确认是否按人过滤的条件(定义读取失败/未知 domkey)→ 硬拒绝
|
|
3686
|
-
|
|
3710
|
+
// 策略未启用(默认)时不拦,仅保留代码路径。
|
|
3711
|
+
if (isScopeEnforced() && condRes.unverifiablePerson)
|
|
3687
3712
|
throw new Error('拒绝越权查询:台账查询条件无法确认是否按人过滤(条件定义读取失败或含未识别字段)。为防越权,本次查询已拒绝,请稍后重试或改用中文字段名。');
|
|
3688
3713
|
// ★越权防护:人员条件(填报人/审批人/报销人/经手人等)目标必须 ∈ {本人 + 全部层级下属}
|
|
3689
3714
|
if (condRes.personConditions?.length)
|
|
@@ -3857,7 +3882,7 @@ async function main() {
|
|
|
3857
3882
|
const cmd = args._[0] ?? 'todo';
|
|
3858
3883
|
if (cmd === 'version') {
|
|
3859
3884
|
const buildHash = createHash('sha256').update(fs.readFileSync(new URL(import.meta.url))).digest('hex').slice(0, 12);
|
|
3860
|
-
console.log(JSON.stringify({ name: 'weaver-e9-assistant-cli', version: '1.1.
|
|
3885
|
+
console.log(JSON.stringify({ name: 'weaver-e9-assistant-cli', version: '1.1.2', buildHash }, null, 2));
|
|
3861
3886
|
return;
|
|
3862
3887
|
}
|
|
3863
3888
|
if (cmd === 'whoami') {
|
package/dist/subordinate.js
CHANGED
|
@@ -1,419 +1,433 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 我的下属:查询直接/全部层级下属 + 越权防护(fail-closed)。
|
|
3
|
-
*
|
|
4
|
-
* 链路(用户抓包确认,POST 均表单编码):
|
|
5
|
-
* 1) /api/hrm/subordinateinfo/getSearchResult body { id, lastname, showAllLevel } → dataKey
|
|
6
|
-
* 2) /api/ec/dev/table/datas body { dataKey, current, sortParams } → 行 + columns(不带 pageSize,跟随服务端默认)
|
|
7
|
-
* 3) /api/ec/dev/table/counts body { dataKey } → 总数 {count:N,status:true}
|
|
8
|
-
*
|
|
9
|
-
* 越权边界(用户明确要求,fail-closed):
|
|
10
|
-
* 查询数据只能查「当前登录用户 + 全部层级下属」,目标人员不在授权集合 → 拒绝。
|
|
11
|
-
* 授权集合加载失败/不可靠时,非本人查询一律拒绝;本人永远允许。
|
|
12
|
-
*
|
|
13
|
-
* 安全设计要点:
|
|
14
|
-
* - 授权集合按环境隔离缓存 ~/.e9/subordinate_scope_<envkey>.json,TTL 12h(E9_SCOPE_TTL_HOURS 可调)。
|
|
15
|
-
* - 缓存必须 selfId 匹配才可用(错用户/过期/不可靠一律不用,防离职下属仍被放行)。
|
|
16
|
-
* - 姓名→userid 解析唯一入口是 blog at-list(SKILL.md 硬规则),候选按 精确>前缀>拼音 排序后过滤到范围。
|
|
17
|
-
* - 下属表找不到 id 列 → scope 标记 reliable:false → 非本人查询拒绝(安全但功能受限,实跑校准回填规则)。
|
|
18
|
-
*/
|
|
19
|
-
import fs from 'node:fs';
|
|
20
|
-
import path from 'node:path';
|
|
21
|
-
import os from 'node:os';
|
|
22
|
-
import { createApiFromProfile } from './client.js';
|
|
23
|
-
import { extractDataKey, extractRows, extractColumns, extractTotal, pickByPaths, stripHtml } from './parse.js';
|
|
24
|
-
import { getCurrentUser } from './wfuser.js';
|
|
25
|
-
import { getAtAllList } from './blog.js';
|
|
26
|
-
import { profileNameFromBaseUrl } from './config.js';
|
|
27
|
-
|
|
28
|
-
/** 全层级下属翻页上限(超限仍未收全 → reliable:false)。 */
|
|
29
|
-
export const MAX_SCOPE_PAGES = 20;
|
|
30
|
-
/** 授权集合缓存 TTL(默认 12h,比台账目录的 7 天短——越权集合同理会随组织变动,宁短勿长)。 */
|
|
31
|
-
export const SCOPE_TTL_MS = (Number(process.env.E9_SCOPE_TTL_HOURS ?? '12') || 12) * 3600 * 1000;
|
|
32
|
-
/** 缓存目录(测试可用 E9_SCOPE_CACHE_DIR 覆盖,避免污染真实 ~/.e9)。 */
|
|
33
|
-
function scopeCacheDir() {
|
|
34
|
-
return process.env.E9_SCOPE_CACHE_DIR || path.join(os.homedir(), '.e9');
|
|
35
|
-
}
|
|
36
|
-
function scopeCachePath(cfg) {
|
|
37
|
-
const envkey = profileNameFromBaseUrl(cfg.baseUrl);
|
|
38
|
-
return path.join(scopeCacheDir(), `subordinate_scope_${envkey}.json`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ─── 链路函数 ────────────────────────────────────────────
|
|
42
|
-
|
|
43
|
-
/** 拿下属列表 dataKey。token no-op 逃生:200 但无 dataKey 且配了 cookie → 强制 cookie 重试一次。 */
|
|
44
|
-
export async function fetchSubordinateDataKey(cfg, { lastname = '', showAllLevel = false } = {}) {
|
|
45
|
-
const api = await createApiFromProfile(cfg);
|
|
46
|
-
const body = { id: '', lastname: String(lastname ?? ''), showAllLevel: showAllLevel ? 'true' : 'false' };
|
|
47
|
-
let r = await api.post('/api/hrm/subordinateinfo/getSearchResult', body);
|
|
48
|
-
let dataKey = extractDataKey(r?.json);
|
|
49
|
-
if (!dataKey && cfg.cookie) {
|
|
50
|
-
r = await api.post('/api/hrm/subordinateinfo/getSearchResult', body, { forceCookie: true });
|
|
51
|
-
dataKey = extractDataKey(r?.json);
|
|
52
|
-
}
|
|
53
|
-
if (!dataKey) {
|
|
54
|
-
throw new Error('未能从 getSearchResult 响应中提取 dataKey。\n' + `响应(前300字): ${truncate(r?.text)}`);
|
|
55
|
-
}
|
|
56
|
-
return { dataKey, raw: r?.json ?? null };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** 取下属表一页数据(不带 pageSize,跟随服务端默认每页条数)。 */
|
|
60
|
-
export async function fetchSubordinateDataPage(cfg, dataKey, page) {
|
|
61
|
-
const api = await createApiFromProfile(cfg);
|
|
62
|
-
const r = await api.post('/api/ec/dev/table/datas', { dataKey, current: String(page), sortParams: '[]' });
|
|
63
|
-
const json = r?.json ?? {};
|
|
64
|
-
return {
|
|
65
|
-
rows: extractRows(json),
|
|
66
|
-
columns: extractColumns(json),
|
|
67
|
-
pageSize: pickNum(json, ['pageSize', 'data.pageSize', 'rootMap.pagesize', 'data.rootMap.pagesize']),
|
|
68
|
-
raw: json,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** 下属表总数。 */
|
|
73
|
-
export async function fetchSubordinateCount(cfg, dataKey) {
|
|
74
|
-
const api = await createApiFromProfile(cfg);
|
|
75
|
-
const r = await api.post('/api/ec/dev/table/counts', { dataKey });
|
|
76
|
-
return { total: extractTotal(r?.json), raw: r?.json ?? null };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ─── 列检测(校准锚点,fail-closed 关键) ────────────────
|
|
80
|
-
|
|
81
|
-
/** 找下属表的 id 列(拿不到 → scope 不可靠 → 非本人查询拒绝)。 */
|
|
82
|
-
export function detectSubordinateIdColumn(columns) {
|
|
83
|
-
if (!Array.isArray(columns))
|
|
84
|
-
return null;
|
|
85
|
-
return columns.find((c) => {
|
|
86
|
-
const di = String(c?.dataIndex ?? '').toLowerCase();
|
|
87
|
-
const db = String(c?.dbField ?? '').toLowerCase();
|
|
88
|
-
return /^(id|userid|hrmid|randomfieldid)$/i.test(di) || /^(id|userid|hrmid)$/i.test(db);
|
|
89
|
-
}) ?? null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** 找下属表的姓名列(取不到 → userName 匹配失效,但 userId 匹配仍生效)。 */
|
|
93
|
-
export function detectSubordinateNameColumn(columns) {
|
|
94
|
-
if (!Array.isArray(columns))
|
|
95
|
-
return null;
|
|
96
|
-
return columns.find((c) => {
|
|
97
|
-
const di = String(c?.dataIndex ?? '').toLowerCase();
|
|
98
|
-
const title = String(c?.title ?? '');
|
|
99
|
-
return /^(lastname|name)$/i.test(di) || /姓名|人员|名字/.test(title);
|
|
100
|
-
}) ?? null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ─── 列表命令后端 ────────────────────────────────────────
|
|
104
|
-
|
|
105
|
-
/** 规范化一行下属:id/name + 全部列(<field>span 优先去 HTML)。人员无独立页面 → 纯文本。 */
|
|
106
|
-
export function normalizeSubordinateRow(row, columns) {
|
|
107
|
-
if (!row || typeof row !== 'object')
|
|
108
|
-
return {};
|
|
109
|
-
const idCol = detectSubordinateIdColumn(columns);
|
|
110
|
-
const nameCol = detectSubordinateNameColumn(columns);
|
|
111
|
-
const get = (k) => pickByPaths(row, [k]);
|
|
112
|
-
const id = idCol ? stripHtml(get(idCol.dataIndex)) : '';
|
|
113
|
-
const name = nameCol ? stripHtml(get(`${nameCol.dataIndex}span`) ?? get(nameCol.dataIndex)) : '';
|
|
114
|
-
const fields = (columns || []).map((c) => ({
|
|
115
|
-
dataIndex: c.dataIndex,
|
|
116
|
-
title: c.title,
|
|
117
|
-
value: stripHtml(get(`${c.dataIndex}span`) ?? get(c.dataIndex)),
|
|
118
|
-
}));
|
|
119
|
-
return { id, name, fields, raw: row };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* 查询我的下属列表。showAllLevel=false=直接下属(抓包默认)/true=全部层级。
|
|
124
|
-
* 返回 { lastname, allLevel, page, pageSize, total, count, dataKey, columns, items, raw }。
|
|
125
|
-
*/
|
|
126
|
-
export async function getSubordinates(cfg, opts = {}) {
|
|
127
|
-
const lastname = opts.lastname ?? '';
|
|
128
|
-
const allLevel = !!opts.allLevel;
|
|
129
|
-
const page = Number(opts.page ?? '1') || 1;
|
|
130
|
-
const { dataKey, raw: searchRaw } = await fetchSubordinateDataKey(cfg, { lastname, showAllLevel: allLevel });
|
|
131
|
-
const data = await fetchSubordinateDataPage(cfg, dataKey, page);
|
|
132
|
-
const count = await fetchSubordinateCount(cfg, dataKey);
|
|
133
|
-
const total = count.total ?? null;
|
|
134
|
-
const items = data.rows.map((r) => normalizeSubordinateRow(r, data.columns));
|
|
135
|
-
return {
|
|
136
|
-
lastname,
|
|
137
|
-
allLevel,
|
|
138
|
-
page,
|
|
139
|
-
pageSize: data.pageSize ?? (data.rows.length || null),
|
|
140
|
-
total,
|
|
141
|
-
count: items.length,
|
|
142
|
-
dataKey,
|
|
143
|
-
columns: data.columns,
|
|
144
|
-
items,
|
|
145
|
-
raw: { getSearchResult: searchRaw, datas: data.raw, counts: count.raw },
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// ─── 授权集合加载 + 缓存(严格 fail-closed) ─────────────
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* 加载授权集合 { 当前用户 } ∪ { 全部层级下属 }。
|
|
153
|
-
* 返回 { reliable, error?, selfId, selfName, userIds:Set, userNames:Set, fromCache, fetchedAt }。
|
|
154
|
-
* reliable:false 表示集合不可靠(身份未确认/拉取失败/未收全/id 列缺失)→ 调用方只允许放行本人。
|
|
155
|
-
*/
|
|
156
|
-
export async function loadScope(cfg, opts = {}) {
|
|
157
|
-
// 1. 自证身份:getCurrentUser → cookie 兜底 → 仍空则连本人都无法确认,一切带人查询拒绝
|
|
158
|
-
let selfId = '';
|
|
159
|
-
let selfName = '';
|
|
160
|
-
try {
|
|
161
|
-
const u = await getCurrentUser(cfg);
|
|
162
|
-
selfId = String(u?.userid ?? '');
|
|
163
|
-
selfName = String(u?.username ?? '');
|
|
164
|
-
}
|
|
165
|
-
catch { /* 走 cookie 兜底 */ }
|
|
166
|
-
if (!selfId)
|
|
167
|
-
selfId = parseUserIdFromCookie(cfg?.cookie);
|
|
168
|
-
if (!selfId) {
|
|
169
|
-
return { reliable: false, error: 'no-self', selfId: '', selfName: '', userIds: new Set(), userNames: new Set() };
|
|
170
|
-
}
|
|
171
|
-
// 2. 缓存命中:selfId 匹配 + 未过期 + reliable 才可用(错用户/过期/不可靠一律弃)
|
|
172
|
-
if (!opts.refresh && !opts.noCache) {
|
|
173
|
-
const cached = readScopeCache(cfg, selfId);
|
|
174
|
-
if (cached) {
|
|
175
|
-
return { ...cached, selfId, selfName, userIds: new Set(cached.userIds), userNames: new Set(cached.userNames), fromCache: true };
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
// 3. 拉全层级下属,逐页收行
|
|
179
|
-
const userIds = new Set([selfId]);
|
|
180
|
-
const userNames = new Set([selfName].filter(Boolean));
|
|
181
|
-
let reliable = false;
|
|
182
|
-
let idCol = null;
|
|
183
|
-
let nameCol = null;
|
|
184
|
-
let rows = [];
|
|
185
|
-
let total = null;
|
|
186
|
-
try {
|
|
187
|
-
const { dataKey } = await fetchSubordinateDataKey(cfg, { lastname: '', showAllLevel: true });
|
|
188
|
-
let page = 1;
|
|
189
|
-
let hitEnd = false;
|
|
190
|
-
while (page <= MAX_SCOPE_PAGES) {
|
|
191
|
-
const d = await fetchSubordinateDataPage(cfg, dataKey, page);
|
|
192
|
-
if (page === 1) {
|
|
193
|
-
idCol = detectSubordinateIdColumn(d.columns);
|
|
194
|
-
nameCol = detectSubordinateNameColumn(d.columns);
|
|
195
|
-
}
|
|
196
|
-
const pageRows = d.rows;
|
|
197
|
-
rows = rows.concat(pageRows);
|
|
198
|
-
if (pageRows.length === 0) {
|
|
199
|
-
hitEnd = true;
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
202
|
-
// 短页 = 末尾(响应给了 pageSize 时能判;不给就靠 counts total 判完整)
|
|
203
|
-
if (d.pageSize && pageRows.length < d.pageSize) {
|
|
204
|
-
hitEnd = true;
|
|
205
|
-
break;
|
|
206
|
-
}
|
|
207
|
-
page++;
|
|
208
|
-
}
|
|
209
|
-
const count = await fetchSubordinateCount(cfg, dataKey);
|
|
210
|
-
total = count.total;
|
|
211
|
-
// 完整性判定:有 total 时收行数必须 >= total;无 total 时必须见到空页/短页才算到底
|
|
212
|
-
reliable = idCol != null && (total != null ? rows.length >= total : hitEnd);
|
|
213
|
-
if (reliable) {
|
|
214
|
-
for (const row of rows) {
|
|
215
|
-
const uid = stripHtml(pickByPaths(row, [idCol.dataIndex]));
|
|
216
|
-
if (/^\d+$/.test(uid))
|
|
217
|
-
userIds.add(uid);
|
|
218
|
-
if (nameCol) {
|
|
219
|
-
const nm = stripHtml(pickByPaths(row, [`${nameCol.dataIndex}span`, nameCol.dataIndex]));
|
|
220
|
-
if (nm)
|
|
221
|
-
userNames.add(nm);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
catch { reliable = false; }
|
|
227
|
-
const scope = { reliable, error: reliable ? undefined : 'scope-unreliable', selfId, selfName, userIds, userNames, fetchedAt: Date.now() };
|
|
228
|
-
// 仅 reliable 才写缓存(写失败不阻断)
|
|
229
|
-
if (reliable)
|
|
230
|
-
writeScopeCache(cfg, scope);
|
|
231
|
-
return scope;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/** 读缓存;selfId 不匹配 / 过期 / 不可靠 → null。 */
|
|
235
|
-
function readScopeCache(cfg, selfId) {
|
|
236
|
-
try {
|
|
237
|
-
const raw = JSON.parse(fs.readFileSync(scopeCachePath(cfg), 'utf8'));
|
|
238
|
-
if (!raw || raw.selfId !== selfId || raw.reliable !== true)
|
|
239
|
-
return null;
|
|
240
|
-
if (Date.now() - (raw.fetchedAt || 0) > SCOPE_TTL_MS)
|
|
241
|
-
return null;
|
|
242
|
-
return raw;
|
|
243
|
-
}
|
|
244
|
-
catch {
|
|
245
|
-
return null;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/** 写缓存(失败不阻断)。 */
|
|
250
|
-
function writeScopeCache(cfg, scope) {
|
|
251
|
-
try {
|
|
252
|
-
fs.mkdirSync(scopeCacheDir(), { recursive: true });
|
|
253
|
-
fs.writeFileSync(scopeCachePath(cfg), JSON.stringify({
|
|
254
|
-
envkey: profileNameFromBaseUrl(cfg.baseUrl),
|
|
255
|
-
baseUrl: cfg.baseUrl,
|
|
256
|
-
selfId: scope.selfId,
|
|
257
|
-
selfName: scope.selfName,
|
|
258
|
-
userIds: [...scope.userIds],
|
|
259
|
-
userNames: [...scope.userNames],
|
|
260
|
-
reliable: scope.reliable,
|
|
261
|
-
fetchedAt: scope.fetchedAt,
|
|
262
|
-
}, null, 2) + '\n', 'utf8');
|
|
263
|
-
}
|
|
264
|
-
catch { /* 缓存写失败不阻断查询 */ }
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/** 是否在授权集合内(纯函数,便于单测)。 */
|
|
268
|
-
export function isInScope(scope, { userId, userName } = {}) {
|
|
269
|
-
if (!scope || !scope.userIds)
|
|
270
|
-
return false;
|
|
271
|
-
if (userId != null && scope.userIds.has(String(userId).trim()))
|
|
272
|
-
return true;
|
|
273
|
-
if (userName != null && String(userName).trim() && scope.userNames?.has(String(userName).trim()))
|
|
274
|
-
return true;
|
|
275
|
-
return false;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// ─── 校验/拒绝 ───────────────────────────────────────────
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*/
|
|
284
|
-
export
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 我的下属:查询直接/全部层级下属 + 越权防护(fail-closed)。
|
|
3
|
+
*
|
|
4
|
+
* 链路(用户抓包确认,POST 均表单编码):
|
|
5
|
+
* 1) /api/hrm/subordinateinfo/getSearchResult body { id, lastname, showAllLevel } → dataKey
|
|
6
|
+
* 2) /api/ec/dev/table/datas body { dataKey, current, sortParams } → 行 + columns(不带 pageSize,跟随服务端默认)
|
|
7
|
+
* 3) /api/ec/dev/table/counts body { dataKey } → 总数 {count:N,status:true}
|
|
8
|
+
*
|
|
9
|
+
* 越权边界(用户明确要求,fail-closed):
|
|
10
|
+
* 查询数据只能查「当前登录用户 + 全部层级下属」,目标人员不在授权集合 → 拒绝。
|
|
11
|
+
* 授权集合加载失败/不可靠时,非本人查询一律拒绝;本人永远允许。
|
|
12
|
+
*
|
|
13
|
+
* 安全设计要点:
|
|
14
|
+
* - 授权集合按环境隔离缓存 ~/.e9/subordinate_scope_<envkey>.json,TTL 12h(E9_SCOPE_TTL_HOURS 可调)。
|
|
15
|
+
* - 缓存必须 selfId 匹配才可用(错用户/过期/不可靠一律不用,防离职下属仍被放行)。
|
|
16
|
+
* - 姓名→userid 解析唯一入口是 blog at-list(SKILL.md 硬规则),候选按 精确>前缀>拼音 排序后过滤到范围。
|
|
17
|
+
* - 下属表找不到 id 列 → scope 标记 reliable:false → 非本人查询拒绝(安全但功能受限,实跑校准回填规则)。
|
|
18
|
+
*/
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import os from 'node:os';
|
|
22
|
+
import { createApiFromProfile } from './client.js';
|
|
23
|
+
import { extractDataKey, extractRows, extractColumns, extractTotal, pickByPaths, stripHtml } from './parse.js';
|
|
24
|
+
import { getCurrentUser } from './wfuser.js';
|
|
25
|
+
import { getAtAllList } from './blog.js';
|
|
26
|
+
import { profileNameFromBaseUrl } from './config.js';
|
|
27
|
+
|
|
28
|
+
/** 全层级下属翻页上限(超限仍未收全 → reliable:false)。 */
|
|
29
|
+
export const MAX_SCOPE_PAGES = 20;
|
|
30
|
+
/** 授权集合缓存 TTL(默认 12h,比台账目录的 7 天短——越权集合同理会随组织变动,宁短勿长)。 */
|
|
31
|
+
export const SCOPE_TTL_MS = (Number(process.env.E9_SCOPE_TTL_HOURS ?? '12') || 12) * 3600 * 1000;
|
|
32
|
+
/** 缓存目录(测试可用 E9_SCOPE_CACHE_DIR 覆盖,避免污染真实 ~/.e9)。 */
|
|
33
|
+
function scopeCacheDir() {
|
|
34
|
+
return process.env.E9_SCOPE_CACHE_DIR || path.join(os.homedir(), '.e9');
|
|
35
|
+
}
|
|
36
|
+
function scopeCachePath(cfg) {
|
|
37
|
+
const envkey = profileNameFromBaseUrl(cfg.baseUrl);
|
|
38
|
+
return path.join(scopeCacheDir(), `subordinate_scope_${envkey}.json`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── 链路函数 ────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/** 拿下属列表 dataKey。token no-op 逃生:200 但无 dataKey 且配了 cookie → 强制 cookie 重试一次。 */
|
|
44
|
+
export async function fetchSubordinateDataKey(cfg, { lastname = '', showAllLevel = false } = {}) {
|
|
45
|
+
const api = await createApiFromProfile(cfg);
|
|
46
|
+
const body = { id: '', lastname: String(lastname ?? ''), showAllLevel: showAllLevel ? 'true' : 'false' };
|
|
47
|
+
let r = await api.post('/api/hrm/subordinateinfo/getSearchResult', body);
|
|
48
|
+
let dataKey = extractDataKey(r?.json);
|
|
49
|
+
if (!dataKey && cfg.cookie) {
|
|
50
|
+
r = await api.post('/api/hrm/subordinateinfo/getSearchResult', body, { forceCookie: true });
|
|
51
|
+
dataKey = extractDataKey(r?.json);
|
|
52
|
+
}
|
|
53
|
+
if (!dataKey) {
|
|
54
|
+
throw new Error('未能从 getSearchResult 响应中提取 dataKey。\n' + `响应(前300字): ${truncate(r?.text)}`);
|
|
55
|
+
}
|
|
56
|
+
return { dataKey, raw: r?.json ?? null };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 取下属表一页数据(不带 pageSize,跟随服务端默认每页条数)。 */
|
|
60
|
+
export async function fetchSubordinateDataPage(cfg, dataKey, page) {
|
|
61
|
+
const api = await createApiFromProfile(cfg);
|
|
62
|
+
const r = await api.post('/api/ec/dev/table/datas', { dataKey, current: String(page), sortParams: '[]' });
|
|
63
|
+
const json = r?.json ?? {};
|
|
64
|
+
return {
|
|
65
|
+
rows: extractRows(json),
|
|
66
|
+
columns: extractColumns(json),
|
|
67
|
+
pageSize: pickNum(json, ['pageSize', 'data.pageSize', 'rootMap.pagesize', 'data.rootMap.pagesize']),
|
|
68
|
+
raw: json,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 下属表总数。 */
|
|
73
|
+
export async function fetchSubordinateCount(cfg, dataKey) {
|
|
74
|
+
const api = await createApiFromProfile(cfg);
|
|
75
|
+
const r = await api.post('/api/ec/dev/table/counts', { dataKey });
|
|
76
|
+
return { total: extractTotal(r?.json), raw: r?.json ?? null };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── 列检测(校准锚点,fail-closed 关键) ────────────────
|
|
80
|
+
|
|
81
|
+
/** 找下属表的 id 列(拿不到 → scope 不可靠 → 非本人查询拒绝)。 */
|
|
82
|
+
export function detectSubordinateIdColumn(columns) {
|
|
83
|
+
if (!Array.isArray(columns))
|
|
84
|
+
return null;
|
|
85
|
+
return columns.find((c) => {
|
|
86
|
+
const di = String(c?.dataIndex ?? '').toLowerCase();
|
|
87
|
+
const db = String(c?.dbField ?? '').toLowerCase();
|
|
88
|
+
return /^(id|userid|hrmid|randomfieldid)$/i.test(di) || /^(id|userid|hrmid)$/i.test(db);
|
|
89
|
+
}) ?? null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 找下属表的姓名列(取不到 → userName 匹配失效,但 userId 匹配仍生效)。 */
|
|
93
|
+
export function detectSubordinateNameColumn(columns) {
|
|
94
|
+
if (!Array.isArray(columns))
|
|
95
|
+
return null;
|
|
96
|
+
return columns.find((c) => {
|
|
97
|
+
const di = String(c?.dataIndex ?? '').toLowerCase();
|
|
98
|
+
const title = String(c?.title ?? '');
|
|
99
|
+
return /^(lastname|name)$/i.test(di) || /姓名|人员|名字/.test(title);
|
|
100
|
+
}) ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── 列表命令后端 ────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
/** 规范化一行下属:id/name + 全部列(<field>span 优先去 HTML)。人员无独立页面 → 纯文本。 */
|
|
106
|
+
export function normalizeSubordinateRow(row, columns) {
|
|
107
|
+
if (!row || typeof row !== 'object')
|
|
108
|
+
return {};
|
|
109
|
+
const idCol = detectSubordinateIdColumn(columns);
|
|
110
|
+
const nameCol = detectSubordinateNameColumn(columns);
|
|
111
|
+
const get = (k) => pickByPaths(row, [k]);
|
|
112
|
+
const id = idCol ? stripHtml(get(idCol.dataIndex)) : '';
|
|
113
|
+
const name = nameCol ? stripHtml(get(`${nameCol.dataIndex}span`) ?? get(nameCol.dataIndex)) : '';
|
|
114
|
+
const fields = (columns || []).map((c) => ({
|
|
115
|
+
dataIndex: c.dataIndex,
|
|
116
|
+
title: c.title,
|
|
117
|
+
value: stripHtml(get(`${c.dataIndex}span`) ?? get(c.dataIndex)),
|
|
118
|
+
}));
|
|
119
|
+
return { id, name, fields, raw: row };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 查询我的下属列表。showAllLevel=false=直接下属(抓包默认)/true=全部层级。
|
|
124
|
+
* 返回 { lastname, allLevel, page, pageSize, total, count, dataKey, columns, items, raw }。
|
|
125
|
+
*/
|
|
126
|
+
export async function getSubordinates(cfg, opts = {}) {
|
|
127
|
+
const lastname = opts.lastname ?? '';
|
|
128
|
+
const allLevel = !!opts.allLevel;
|
|
129
|
+
const page = Number(opts.page ?? '1') || 1;
|
|
130
|
+
const { dataKey, raw: searchRaw } = await fetchSubordinateDataKey(cfg, { lastname, showAllLevel: allLevel });
|
|
131
|
+
const data = await fetchSubordinateDataPage(cfg, dataKey, page);
|
|
132
|
+
const count = await fetchSubordinateCount(cfg, dataKey);
|
|
133
|
+
const total = count.total ?? null;
|
|
134
|
+
const items = data.rows.map((r) => normalizeSubordinateRow(r, data.columns));
|
|
135
|
+
return {
|
|
136
|
+
lastname,
|
|
137
|
+
allLevel,
|
|
138
|
+
page,
|
|
139
|
+
pageSize: data.pageSize ?? (data.rows.length || null),
|
|
140
|
+
total,
|
|
141
|
+
count: items.length,
|
|
142
|
+
dataKey,
|
|
143
|
+
columns: data.columns,
|
|
144
|
+
items,
|
|
145
|
+
raw: { getSearchResult: searchRaw, datas: data.raw, counts: count.raw },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ─── 授权集合加载 + 缓存(严格 fail-closed) ─────────────
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 加载授权集合 { 当前用户 } ∪ { 全部层级下属 }。
|
|
153
|
+
* 返回 { reliable, error?, selfId, selfName, userIds:Set, userNames:Set, fromCache, fetchedAt }。
|
|
154
|
+
* reliable:false 表示集合不可靠(身份未确认/拉取失败/未收全/id 列缺失)→ 调用方只允许放行本人。
|
|
155
|
+
*/
|
|
156
|
+
export async function loadScope(cfg, opts = {}) {
|
|
157
|
+
// 1. 自证身份:getCurrentUser → cookie 兜底 → 仍空则连本人都无法确认,一切带人查询拒绝
|
|
158
|
+
let selfId = '';
|
|
159
|
+
let selfName = '';
|
|
160
|
+
try {
|
|
161
|
+
const u = await getCurrentUser(cfg);
|
|
162
|
+
selfId = String(u?.userid ?? '');
|
|
163
|
+
selfName = String(u?.username ?? '');
|
|
164
|
+
}
|
|
165
|
+
catch { /* 走 cookie 兜底 */ }
|
|
166
|
+
if (!selfId)
|
|
167
|
+
selfId = parseUserIdFromCookie(cfg?.cookie);
|
|
168
|
+
if (!selfId) {
|
|
169
|
+
return { reliable: false, error: 'no-self', selfId: '', selfName: '', userIds: new Set(), userNames: new Set() };
|
|
170
|
+
}
|
|
171
|
+
// 2. 缓存命中:selfId 匹配 + 未过期 + reliable 才可用(错用户/过期/不可靠一律弃)
|
|
172
|
+
if (!opts.refresh && !opts.noCache) {
|
|
173
|
+
const cached = readScopeCache(cfg, selfId);
|
|
174
|
+
if (cached) {
|
|
175
|
+
return { ...cached, selfId, selfName, userIds: new Set(cached.userIds), userNames: new Set(cached.userNames), fromCache: true };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// 3. 拉全层级下属,逐页收行
|
|
179
|
+
const userIds = new Set([selfId]);
|
|
180
|
+
const userNames = new Set([selfName].filter(Boolean));
|
|
181
|
+
let reliable = false;
|
|
182
|
+
let idCol = null;
|
|
183
|
+
let nameCol = null;
|
|
184
|
+
let rows = [];
|
|
185
|
+
let total = null;
|
|
186
|
+
try {
|
|
187
|
+
const { dataKey } = await fetchSubordinateDataKey(cfg, { lastname: '', showAllLevel: true });
|
|
188
|
+
let page = 1;
|
|
189
|
+
let hitEnd = false;
|
|
190
|
+
while (page <= MAX_SCOPE_PAGES) {
|
|
191
|
+
const d = await fetchSubordinateDataPage(cfg, dataKey, page);
|
|
192
|
+
if (page === 1) {
|
|
193
|
+
idCol = detectSubordinateIdColumn(d.columns);
|
|
194
|
+
nameCol = detectSubordinateNameColumn(d.columns);
|
|
195
|
+
}
|
|
196
|
+
const pageRows = d.rows;
|
|
197
|
+
rows = rows.concat(pageRows);
|
|
198
|
+
if (pageRows.length === 0) {
|
|
199
|
+
hitEnd = true;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
// 短页 = 末尾(响应给了 pageSize 时能判;不给就靠 counts total 判完整)
|
|
203
|
+
if (d.pageSize && pageRows.length < d.pageSize) {
|
|
204
|
+
hitEnd = true;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
page++;
|
|
208
|
+
}
|
|
209
|
+
const count = await fetchSubordinateCount(cfg, dataKey);
|
|
210
|
+
total = count.total;
|
|
211
|
+
// 完整性判定:有 total 时收行数必须 >= total;无 total 时必须见到空页/短页才算到底
|
|
212
|
+
reliable = idCol != null && (total != null ? rows.length >= total : hitEnd);
|
|
213
|
+
if (reliable) {
|
|
214
|
+
for (const row of rows) {
|
|
215
|
+
const uid = stripHtml(pickByPaths(row, [idCol.dataIndex]));
|
|
216
|
+
if (/^\d+$/.test(uid))
|
|
217
|
+
userIds.add(uid);
|
|
218
|
+
if (nameCol) {
|
|
219
|
+
const nm = stripHtml(pickByPaths(row, [`${nameCol.dataIndex}span`, nameCol.dataIndex]));
|
|
220
|
+
if (nm)
|
|
221
|
+
userNames.add(nm);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch { reliable = false; }
|
|
227
|
+
const scope = { reliable, error: reliable ? undefined : 'scope-unreliable', selfId, selfName, userIds, userNames, fetchedAt: Date.now() };
|
|
228
|
+
// 仅 reliable 才写缓存(写失败不阻断)
|
|
229
|
+
if (reliable)
|
|
230
|
+
writeScopeCache(cfg, scope);
|
|
231
|
+
return scope;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** 读缓存;selfId 不匹配 / 过期 / 不可靠 → null。 */
|
|
235
|
+
function readScopeCache(cfg, selfId) {
|
|
236
|
+
try {
|
|
237
|
+
const raw = JSON.parse(fs.readFileSync(scopeCachePath(cfg), 'utf8'));
|
|
238
|
+
if (!raw || raw.selfId !== selfId || raw.reliable !== true)
|
|
239
|
+
return null;
|
|
240
|
+
if (Date.now() - (raw.fetchedAt || 0) > SCOPE_TTL_MS)
|
|
241
|
+
return null;
|
|
242
|
+
return raw;
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** 写缓存(失败不阻断)。 */
|
|
250
|
+
function writeScopeCache(cfg, scope) {
|
|
251
|
+
try {
|
|
252
|
+
fs.mkdirSync(scopeCacheDir(), { recursive: true });
|
|
253
|
+
fs.writeFileSync(scopeCachePath(cfg), JSON.stringify({
|
|
254
|
+
envkey: profileNameFromBaseUrl(cfg.baseUrl),
|
|
255
|
+
baseUrl: cfg.baseUrl,
|
|
256
|
+
selfId: scope.selfId,
|
|
257
|
+
selfName: scope.selfName,
|
|
258
|
+
userIds: [...scope.userIds],
|
|
259
|
+
userNames: [...scope.userNames],
|
|
260
|
+
reliable: scope.reliable,
|
|
261
|
+
fetchedAt: scope.fetchedAt,
|
|
262
|
+
}, null, 2) + '\n', 'utf8');
|
|
263
|
+
}
|
|
264
|
+
catch { /* 缓存写失败不阻断查询 */ }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** 是否在授权集合内(纯函数,便于单测)。 */
|
|
268
|
+
export function isInScope(scope, { userId, userName } = {}) {
|
|
269
|
+
if (!scope || !scope.userIds)
|
|
270
|
+
return false;
|
|
271
|
+
if (userId != null && scope.userIds.has(String(userId).trim()))
|
|
272
|
+
return true;
|
|
273
|
+
if (userName != null && String(userName).trim() && scope.userNames?.has(String(userName).trim()))
|
|
274
|
+
return true;
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ─── 校验/拒绝 ───────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 「只能查本人 + 全部层级下属」策略开关(当前默认**不生效**)。
|
|
282
|
+
* 设 E9_SCOPE_ENFORCE=1 可恢复强制校验;后台 OA 权限仍是最终兜底。
|
|
283
|
+
*/
|
|
284
|
+
export function isScopeEnforced() {
|
|
285
|
+
return process.env.E9_SCOPE_ENFORCE === '1';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* 按数字 userid 校验:目标必须 == 本人 或 ∈ 下属集合,否则 throw。
|
|
290
|
+
* ids 里的每个目标都校验;scope 不可靠时本人放行、他人一律拒绝。
|
|
291
|
+
* 策略未启用(默认)时直接放行——不做任何拦截。
|
|
292
|
+
*/
|
|
293
|
+
export async function assertUserIdsInScope(cfg, ids, opts = {}) {
|
|
294
|
+
if (!isScopeEnforced())
|
|
295
|
+
return;
|
|
296
|
+
const scope = await loadScope(cfg, opts);
|
|
297
|
+
for (const id of ids) {
|
|
298
|
+
const s = String(id ?? '').trim();
|
|
299
|
+
if (!s)
|
|
300
|
+
continue;
|
|
301
|
+
// 本人恒允许(即使 scope 不可靠)——越权边界定义:本人 + 全部层级下属;
|
|
302
|
+
// no-self 时 selfId 为空,非空 id 自然落空走拒绝。
|
|
303
|
+
if (s === scope.selfId)
|
|
304
|
+
continue;
|
|
305
|
+
if (scope.reliable && scope.userIds.has(s))
|
|
306
|
+
continue;
|
|
307
|
+
throw new Error(outOfScopeMessage(s, { reason: scope.reliable ? 'not-subordinate' : (scope.error === 'no-self' ? 'no-self' : 'scope-unreliable') }));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* 按姓名校验:纯数字走 assertUserIdsInScope;否则 blog at-list 解析(SKILL.md 硬规则唯一入口),
|
|
313
|
+
* 候选按 精确>前缀>拼音 排序后过滤到授权集合:
|
|
314
|
+
* - 恰 1 个在范围 → 放行,返回其 uid;
|
|
315
|
+
* - 全部越界 / 多个在范围 → throw(拒绝,附范围内候选清单)。
|
|
316
|
+
* 返回单个 uid(names 长度 1)或 uid 数组。调用方可忽略返回值(如 cube 仅校验、值保持原样)。
|
|
317
|
+
*/
|
|
318
|
+
export async function assertNamesInScope(cfg, names, opts = {}) {
|
|
319
|
+
const scope = await loadScope(cfg, opts);
|
|
320
|
+
const resolved = [];
|
|
321
|
+
for (const name of names) {
|
|
322
|
+
const n = String(name ?? '').trim();
|
|
323
|
+
if (!n)
|
|
324
|
+
continue;
|
|
325
|
+
if (/^\d+$/.test(n)) {
|
|
326
|
+
await assertUserIdsInScope(cfg, [n], opts);
|
|
327
|
+
resolved.push(n);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const r = await getAtAllList(cfg, { name: n });
|
|
331
|
+
const candidates = sortPersonCandidates(r.items, n);
|
|
332
|
+
const { inScope, outCount } = filterCandidatesToScope(scope, candidates);
|
|
333
|
+
if (inScope.length === 0) {
|
|
334
|
+
throw new Error(outOfScopeMessage(n, { reason: 'none-in-scope', candidates: candidates.slice(0, 5), outCount }));
|
|
335
|
+
}
|
|
336
|
+
if (inScope.length === 1) {
|
|
337
|
+
resolved.push(String(inScope[0].uid ?? ''));
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
throw new Error(outOfScopeMessage(n, { reason: 'multiple-in-scope', candidates: inScope }));
|
|
341
|
+
}
|
|
342
|
+
return resolved.length === 1 ? resolved[0] : resolved;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** 候选过滤到授权集合(纯函数)。策略未启用(默认)时不过滤,全部视为在范围内。 */
|
|
346
|
+
export function filterCandidatesToScope(scope, candidates) {
|
|
347
|
+
if (!isScopeEnforced()) {
|
|
348
|
+
return { inScope: (candidates || []).map((c) => ({ ...c, name: stripHtml(String(c?.name ?? '')).trim() })), outCount: 0 };
|
|
349
|
+
}
|
|
350
|
+
const inScope = [];
|
|
351
|
+
let outCount = 0;
|
|
352
|
+
for (const c of candidates) {
|
|
353
|
+
const uid = String(c.uid ?? '');
|
|
354
|
+
const name = stripHtml(String(c.name ?? '')).trim();
|
|
355
|
+
if (uid && isInScope(scope, { userId: uid })) {
|
|
356
|
+
inScope.push({ ...c, name });
|
|
357
|
+
}
|
|
358
|
+
else if (name && isInScope(scope, { userName: name })) {
|
|
359
|
+
inScope.push({ ...c, name });
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
outCount++;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return { inScope, outCount };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** 人员候选排序:精确 > 前缀 > 其它(复刻 cli.js at-list 逻辑)。 */
|
|
369
|
+
function sortPersonCandidates(items, query) {
|
|
370
|
+
const q = String(query ?? '').trim().toLowerCase();
|
|
371
|
+
return (items || [])
|
|
372
|
+
.map((it) => {
|
|
373
|
+
const name = String(it.name ?? '').trim();
|
|
374
|
+
const nameLow = name.toLowerCase();
|
|
375
|
+
let matchType = 'other';
|
|
376
|
+
if (q && nameLow === q)
|
|
377
|
+
matchType = 'exact';
|
|
378
|
+
else if (q && nameLow.startsWith(q))
|
|
379
|
+
matchType = 'prefix';
|
|
380
|
+
return { ...it, name, matchType, _sort: matchType === 'exact' ? 0 : matchType === 'prefix' ? 1 : 2 };
|
|
381
|
+
})
|
|
382
|
+
.sort((a, b) => a._sort - b._sort)
|
|
383
|
+
.map(({ _sort, ...rest }) => rest);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** 越权拒绝文案(fail-closed 显式归因)。 */
|
|
387
|
+
export function outOfScopeMessage(target, { reason = 'not-subordinate', candidates = [], outCount = 0 } = {}) {
|
|
388
|
+
let cause;
|
|
389
|
+
if (reason === 'no-self')
|
|
390
|
+
cause = '无法确认当前登录用户身份,为安全起见拒绝所有按人查询';
|
|
391
|
+
else if (reason === 'scope-unreliable')
|
|
392
|
+
cause = '下属范围加载失败,为安全起见仅允许查询您本人的数据';
|
|
393
|
+
else if (reason === 'none-in-scope')
|
|
394
|
+
cause = `匹配到 ${outCount} 位人员,均不在您的下属名单中`;
|
|
395
|
+
else if (reason === 'multiple-in-scope')
|
|
396
|
+
cause = '匹配到多位在您授权范围内的人员,请指定唯一目标';
|
|
397
|
+
else
|
|
398
|
+
cause = '目标不在您的下属名单中';
|
|
399
|
+
const lines = [`拒绝越权查询:目标“${target}”不在您的授权范围(您本人 + 您的全部层级下属)内。`, `原因:${cause}。`];
|
|
400
|
+
if (candidates.length) {
|
|
401
|
+
lines.push('在您授权范围内匹配到以下人员:');
|
|
402
|
+
candidates.forEach((c, i) => {
|
|
403
|
+
lines.push(`${i + 1}. ${c.name} [uid=${c.uid}]${c.matchType === 'exact' ? ' ★精确匹配' : c.matchType === 'prefix' ? ' ·前缀' : ''}`);
|
|
404
|
+
});
|
|
405
|
+
lines.push('如需继续,请指定其中一人的 ID 作为目标。');
|
|
406
|
+
}
|
|
407
|
+
lines.push('如需查询其本人数据,请在 OA 中由其本人或对其有权限的上级办理。');
|
|
408
|
+
return lines.join('\n');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ─── 工具 ────────────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
function parseUserIdFromCookie(cookie) {
|
|
414
|
+
if (!cookie)
|
|
415
|
+
return '';
|
|
416
|
+
const m = String(cookie).match(/loginuuids=([^;]+)/);
|
|
417
|
+
return m ? m[1].trim() : '';
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function pickNum(obj, paths) {
|
|
421
|
+
const v = pickByPaths(obj, paths);
|
|
422
|
+
if (typeof v === 'number')
|
|
423
|
+
return v;
|
|
424
|
+
if (typeof v === 'string' && /^\d+$/.test(v))
|
|
425
|
+
return parseInt(v, 10);
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function truncate(s, n = 300) {
|
|
430
|
+
if (s == null)
|
|
431
|
+
return '(空)';
|
|
432
|
+
return String(s).length > n ? String(s).slice(0, n) + '…(已截断)' : String(s);
|
|
433
|
+
}
|
package/dist/workflow.js
CHANGED
|
@@ -86,11 +86,31 @@ export function parseTimeRange(range) {
|
|
|
86
86
|
return { from: parts[0], to: parts[0] };
|
|
87
87
|
return { from: parts[0] || "", to: parts[1] || "" };
|
|
88
88
|
}
|
|
89
|
+
/** 日期过滤的预设值(OA reqlist 的 createdateselect/recievedateselect/operatedateselect 共用):
|
|
90
|
+
* 0=全部 1=今天 2=本周 3=本月 4=本季 5=本年;自定义区间用 select=0 + from/to。 */
|
|
91
|
+
const DATE_PRESETS = { '全部': '0', '不限': '0', '今天': '1', '本周': '2', '本月': '3', '本季': '4', '本年': '5' };
|
|
92
|
+
/**
|
|
93
|
+
* 解析日期过滤,返回 { select, from, to }:
|
|
94
|
+
* - 预设名(今天/本周/本月/本季/本年/全部/不限)→ select=1~5 或 0,from/to 空;
|
|
95
|
+
* - 数字 0~5 → 直接当 select;
|
|
96
|
+
* - 日期范围("YYYY-MM-DD [YYYY-MM-DD]")→ select='0' + from/to(自定义区间)。
|
|
97
|
+
*/
|
|
98
|
+
export function resolveDateFilter(spec) {
|
|
99
|
+
const s = String(spec ?? '').trim();
|
|
100
|
+
if (!s)
|
|
101
|
+
return { select: '0', from: '', to: '' };
|
|
102
|
+
if (DATE_PRESETS[s] !== undefined)
|
|
103
|
+
return { select: DATE_PRESETS[s], from: '', to: '' };
|
|
104
|
+
if (/^[0-5]$/.test(s))
|
|
105
|
+
return { select: s, from: '', to: '' };
|
|
106
|
+
const parts = s.split(/\s+/);
|
|
107
|
+
return { select: '0', from: parts[0] || '', to: parts[1] || parts[0] || '' };
|
|
108
|
+
}
|
|
89
109
|
|
|
90
110
|
/** 子分类 viewcondition 映射表 */
|
|
91
111
|
export const TODO_TABS = {
|
|
92
112
|
"全部": "0", "待处理": "5", "待阅": "6", "被退回": "7",
|
|
93
|
-
"
|
|
113
|
+
"被督办": "4", "未读": "1", "反馈": "2", "超时": "3", "我的关注": "37",
|
|
94
114
|
};
|
|
95
115
|
|
|
96
116
|
export const DONE_TABS = {
|
|
@@ -371,28 +391,22 @@ function buildFilters(opts) {
|
|
|
371
391
|
if (opts.creatorSubcom)
|
|
372
392
|
f.creatersubcompanyid = opts.creatorSubcom;
|
|
373
393
|
if (opts.createTime) {
|
|
374
|
-
const ct =
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
f.createdateto = ct.to;
|
|
379
|
-
}
|
|
394
|
+
const ct = resolveDateFilter(opts.createTime);
|
|
395
|
+
f.createdateselect = ct.select;
|
|
396
|
+
f.createdatefrom = ct.from;
|
|
397
|
+
f.createdateto = ct.to;
|
|
380
398
|
}
|
|
381
399
|
if (opts.receiveTime) {
|
|
382
|
-
const rt =
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
f.recievedateto = rt.to;
|
|
387
|
-
}
|
|
400
|
+
const rt = resolveDateFilter(opts.receiveTime);
|
|
401
|
+
f.recievedateselect = rt.select;
|
|
402
|
+
f.recievedatefrom = rt.from;
|
|
403
|
+
f.recievedateto = rt.to;
|
|
388
404
|
}
|
|
389
405
|
if (opts.operateTime) {
|
|
390
|
-
const ot =
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
f.operatedateto = ot.to;
|
|
395
|
-
}
|
|
406
|
+
const ot = resolveDateFilter(opts.operateTime);
|
|
407
|
+
f.operatedateselect = ot.select;
|
|
408
|
+
f.operatedatefrom = ot.from;
|
|
409
|
+
f.operatedateto = ot.to;
|
|
396
410
|
}
|
|
397
411
|
if (opts.requestLevel)
|
|
398
412
|
f.requestlevel = opts.requestLevel;
|