weaver-work-cli 0.1.38 → 0.1.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/chunks/{archive-PDV724YK.js → archive-JHTONRST.js} +556 -66
- package/dist/chunks/{archive-oa-4XE4MNXQ.js → archive-oa-FCHLCPHC.js} +2 -2
- package/dist/chunks/{asset-PDL2QE2N.js → asset-RLIHC7DB.js} +2 -2
- package/dist/chunks/asset-idle-KVOFTLCP.js +670 -0
- package/dist/chunks/{attachment-content-MDDGOE5E.js → attachment-content-RZYDB3MB.js} +3 -3
- package/dist/chunks/{blog-UYR3EUXW.js → blog-42OZTJNP.js} +2 -2
- package/dist/chunks/{caizhilian-LG2X5NMF.js → caizhilian-KRK547K5.js} +2 -2
- package/dist/chunks/{calendar-FG4BAZHE.js → calendar-RTWAMKHD.js} +2 -2
- package/dist/chunks/{chunk-J7USS3ED.js → chunk-4CTESSKI.js} +4 -4
- package/dist/chunks/{chunk-OJQUJZQF.js → chunk-4TEDNCON.js} +45 -5
- package/dist/chunks/chunk-57IECMWX.js +487 -0
- package/dist/chunks/{chunk-ELYDWBZJ.js → chunk-7BRU4EFH.js} +13 -7
- package/dist/chunks/{chunk-6LTFIFLM.js → chunk-7E6MUOEK.js} +2 -2
- package/dist/chunks/{chunk-KKRL3REV.js → chunk-7U577Q7U.js} +2 -2
- package/dist/chunks/{chunk-UJJD4QWF.js → chunk-CAQNBBQA.js} +2 -2
- package/dist/chunks/{chunk-CORFRD7Q.js → chunk-CXRQWOZ5.js} +2 -2
- package/dist/chunks/{chunk-GGJFMNJM.js → chunk-FLS4DPGP.js} +110 -16
- package/dist/chunks/{chunk-JSMFPO43.js → chunk-I566ZYNF.js} +1 -1
- package/dist/chunks/{chunk-6FTTSA45.js → chunk-IZSSVYAG.js} +2 -2
- package/dist/chunks/{chunk-XJY7G4DD.js → chunk-JWRZM2H2.js} +3 -3
- package/dist/chunks/{chunk-NI6GIPIM.js → chunk-LFMSDBZN.js} +529 -395
- package/dist/chunks/{chunk-VRYHGG6O.js → chunk-LN447PY2.js} +92 -51
- package/dist/chunks/{chunk-NR6RKXPA.js → chunk-NETCCARD.js} +4 -2
- package/dist/chunks/{chunk-Z32KWY6G.js → chunk-NJKREDA3.js} +1 -1
- package/dist/chunks/{chunk-FUWVHMVV.js → chunk-SPRHHSVC.js} +2 -2
- package/dist/chunks/{chunk-IZUDA353.js → chunk-T6EGVBKU.js} +1 -1
- package/dist/chunks/{chunk-DMV2TOMH.js → chunk-VNBU55SL.js} +428 -145
- package/dist/chunks/{chunk-UZZBA6PV.js → chunk-WFFNM65B.js} +47 -6
- package/dist/chunks/{chunk-EG6ZU444.js → chunk-ZUX3P6BQ.js} +10 -5
- package/dist/chunks/{conflict-BQZEQ4SP.js → conflict-VR55AOOO.js} +2 -2
- package/dist/chunks/{contract-XIRAFTEV.js → contract-HI4FNPYE.js} +2 -2
- package/dist/chunks/{cowork-BLG62BL2.js → cowork-HJLI353D.js} +12 -5
- package/dist/chunks/{ebuilder-form-NUKG5QW4.js → ebuilder-form-HTGI63UU.js} +3 -3
- package/dist/chunks/{ehr-2STW5ILH.js → ehr-GYXIYH3A.js} +2 -2
- package/dist/chunks/env-DW3G4HUO.js +19 -0
- package/dist/chunks/{esb-UUDQUNKY.js → esb-I5INCEFE.js} +4 -4
- package/dist/chunks/factory-TKXGXS6L.js +30 -0
- package/dist/chunks/{fna-6RCY4NU5.js → fna-K2MHTQ24.js} +2 -2
- package/dist/chunks/group-chat-VYBNMJLG.js +18 -0
- package/dist/chunks/{host-FWDOSRH5.js → host-256GVCAH.js} +3 -3
- package/dist/chunks/{host-EJZFROU3.js → host-D5SERMAJ.js} +3 -3
- package/dist/chunks/{host-RHM3NPXV.js → host-JM4YVHFM.js} +3 -3
- package/dist/chunks/{host-7RXEDMAH.js → host-L3VLITMT.js} +3 -3
- package/dist/chunks/{host-UFF33TWJ.js → host-ZUQTJYYK.js} +3 -3
- package/dist/chunks/{hrm-PKJ6ELSH.js → hrm-VLRBKTZB.js} +3 -3
- package/dist/chunks/{im-YJAXS7CO.js → im-QK3VTLXK.js} +8 -8
- package/dist/chunks/{invoice-HWVSXGX6.js → invoice-T5JTA32H.js} +3 -3
- package/dist/chunks/{jiuchuanhui-L7VKILZB.js → jiuchuanhui-P2FNEQVZ.js} +2 -2
- package/dist/chunks/{lazy-dispatch-NMYEIHRH.js → lazy-dispatch-XRRI46M6.js} +5 -5
- package/dist/chunks/{mail-B42RRU7G.js → mail-3Y7MJ6PS.js} +9 -4
- package/dist/chunks/{meeting-CFYVBJSS.js → meeting-M7LTXGJE.js} +11 -10
- package/dist/chunks/{okr-GCLF6V7W.js → okr-OBC6S67L.js} +2 -2
- package/dist/chunks/{plan-ZVL7GBRI.js → plan-5H6I53SK.js} +2 -2
- package/dist/chunks/{project-4YWAAE2K.js → project-L75QZV4S.js} +2 -2
- package/dist/chunks/{registry-EP4VTHZS.js → registry-KMHCPAAD.js} +9 -8
- package/dist/chunks/{registry-OZSMDVPP.js → registry-QGHFNUWS.js} +2 -2
- package/dist/chunks/{registry-FRJ6H7BT.js → registry-RXPN6BBT.js} +4 -4
- package/dist/chunks/{registry-SG2PGVCF.js → registry-UGRBCBHO.js} +3 -3
- package/dist/chunks/{room-recommend-R3JVUUAC.js → room-recommend-A3UDC3CA.js} +4 -4
- package/dist/chunks/{schedule-NIJBTYDO.js → schedule-5GCACCK7.js} +4 -4
- package/dist/chunks/{workflow-BB24DFBY.js → workflow-M5ZJ7DWF.js} +11 -8
- package/dist/core/output.js +16 -6
- package/dist/index.js +33 -28
- package/dist/internal/e10/auth/commands.js +109 -56
- package/dist/internal/e10/auth/messages.js +1 -0
- package/dist/internal/e10/auth/session.js +11 -1
- package/dist/internal/e10/auth/xiaoe.js +29 -4
- package/dist/internal/e10/auth-recovery.js +13 -6
- package/dist/internal/e10/client.js +13 -0
- package/dist/internal/e10/context.js +24 -3
- package/dist/internal/e10/request-runtime.js +12 -1
- package/dist/shortcuts/archive/host.js +2 -0
- package/dist/shortcuts/archive/manifest.js +1 -1
- package/dist/shortcuts/archive/operations/borrow-list.js +13 -1
- package/dist/shortcuts/archive/operations/flow-url.js +1 -1
- package/dist/shortcuts/archive/operations/prelib.js +472 -0
- package/dist/shortcuts/archive/operations/registry.js +2 -0
- package/dist/shortcuts/archive/operations/upload.js +1 -1
- package/dist/shortcuts/asset-idle/errors.js +44 -0
- package/dist/shortcuts/asset-idle/host.js +161 -0
- package/dist/shortcuts/asset-idle/index.js +114 -0
- package/dist/shortcuts/asset-idle/manifest.js +159 -0
- package/dist/shortcuts/asset-idle/operations/idle.js +47 -0
- package/dist/shortcuts/asset-idle/operations/registry.js +33 -0
- package/dist/shortcuts/asset-idle/operations/shared.js +142 -0
- package/dist/shortcuts/asset-idle/operations/types.js +9 -0
- package/dist/shortcuts/asset-idle/operations.js +15 -0
- package/dist/shortcuts/cowork/operations/item-write.js +10 -2
- package/dist/shortcuts/index.js +5 -0
- package/dist/shortcuts/mail/operations/mail-send.js +8 -2
- package/dist/shortcuts/meeting/operations/eb/catalog.js +5 -1
- package/dist/shortcuts/meeting/operations/eb/create-flow.js +381 -104
- package/dist/shortcuts/meeting/operations/eb/crm-search.js +87 -5
- package/dist/shortcuts/meeting/operations/eb/flow-memory.js +49 -0
- package/dist/shortcuts/meeting/operations/eb/group-chat.js +583 -0
- package/dist/shortcuts/meeting/operations/eb/index.js +2 -0
- package/dist/shortcuts/meeting/operations/eb/room-recommend.js +19 -3
- package/dist/shortcuts/meeting/operations/eb/room-summary.js +2 -0
- package/dist/shortcuts/meeting/operations/eb/usage-cache.js +99 -0
- package/dist/shortcuts/meeting/operations/from-group-chat.js +35 -12
- package/dist/shortcuts/workflow/host.js +3 -0
- package/dist/shortcuts/workflow/index.js +1 -1
- package/dist/shortcuts/workflow/operations/form.js +65 -171
- package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +129 -27
- package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupStore.js +6 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +60 -117
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +44 -7
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +37 -29
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +48 -8
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +1 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +22 -18
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +17 -9
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +34 -10
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +5 -2
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +9 -2
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +122 -20
- package/docs/_catalog.md +3 -0
- package/docs/asset-idle.md +45 -0
- package/docs/e10-auth.md +16 -1
- package/docs/todo-smart-convert/subgroup-rules.md +1 -1
- package/package.json +1 -1
- package/scripts/mcp-meeting.mjs +234 -0
- package/scripts/package-skill.mjs +2 -0
- package/skill-template/business-info.json +14 -1
- package/skill-template/domains/asset-idle.md +13 -0
- package/skill-template/domains/wenshuding.md +3 -0
- package/skills/weaver-e10-archive-oa/SKILL.md +1 -0
- package/skills/weaver-e10-archive-oa/references/my-borrow.md +49 -0
- package/skills/weaver-e10-asset-idle/SKILL.md +73 -0
- package/skills/weaver-e10-asset-idle/references/idle-list.md +62 -0
- package/skills/weaver-e10-asset-idle/references/idle-stats-and-charts.md +65 -0
- package/skills/weaver-e10-asset-idle/references/source-manifest.json +193 -0
- package/skills/weaver-e10-asset-idle/references/widget-style.md +205 -0
- package/skills/weaver-e10-caizhilian/SKILL.md +29 -11
- package/skills/weaver-e10-caizhilian/references/auth-and-install.md +12 -0
- package/skills/weaver-e10-caizhilian/references/files.md +1 -1
- package/skills/weaver-e10-caizhilian/references/setting-encrypt.md +1 -1
- package/skills/weaver-e10-caizhilian/references/source-manifest.json +45 -45
- package/skills/weaver-e10-cowork/SKILL.md +2 -0
- package/skills/weaver-e10-cowork/references/cowork-write.md +1 -1
- package/skills/weaver-e10-jucailin/SKILL.md +2 -0
- package/skills/weaver-e10-jucailin/references/recruit-read.md +2 -0
- package/skills/weaver-e10-jucailin/references/recruit-write.md +1 -1
- package/skills/weaver-e10-mail/SKILL.md +3 -0
- package/skills/weaver-e10-mail/references/mail-send.md +2 -0
- package/skills/weaver-e10-meeting/SKILL.md +88 -25
- package/skills/weaver-e10-meeting/references/meeting-minute-file.md +25 -11
- package/skills/weaver-e10-meeting.zip +0 -0
- package/skills/weaver-e10-shared/SKILL.md +5 -5
- package/skills/weaver-e10-shared/references/e10-auth-and-session.md +3 -3
- package/skills/weaver-e10-wenshuding/SKILL.md +29 -6
- package/skills/weaver-e10-wenshuding/references/archive-borrow-list.md +14 -0
- package/skills/weaver-e10-wenshuding/references/archive-download.md +2 -0
- package/skills/weaver-e10-wenshuding/references/archive-flow.md +1 -1
- package/skills/weaver-e10-wenshuding/references/archive-prelib-arrange.md +76 -0
- package/skills/weaver-e10-wenshuding/references/archive-prelib-list.md +107 -0
- package/skills/weaver-e10-wenshuding/references/archive-prelib-open.md +59 -0
- package/skills/weaver-e10-wenshuding/references/archive-upload.md +4 -1
- package/skills/weaver-e10-wenshuding/references/source-manifest.json +108 -65
- package/skills/weaver-e10-workflow/SKILL.md +9 -2
- package/skills/weaver-e10-workflow/aigroup/default.json +166 -19
- package/skills/weaver-e10-workflow/references/todo-smart-convert.md +72 -34
- package/skills/weaver-e10-workflow/tools/todo-serve.cjs +113 -25
- package/skills/weaver-e10-workflow/tools/todo-summary.cjs +350 -50
- package/dist/chunks/{chunk-2LV74IWP.js → chunk-E7OK5WD5.js} +5 -5
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js';
|
|
2
|
+
import { requireE10Session } from '../../internal/e10/context.js';
|
|
3
|
+
import { getActiveProfile } from '../../internal/e10/auth/session.js';
|
|
4
|
+
import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
|
|
5
|
+
import { isE10SessionExpiredResponse } from '../../internal/e10/auth-recovery.js';
|
|
6
|
+
import { assetIdleApiError, assetIdleAuthError, assetIdleNetworkError, assetIdleValidationError, } from './errors.js';
|
|
7
|
+
/** 源接口(find-idle-assents):POST {baseurl}/api/esb/server/event/triggerActionFlow。 */
|
|
8
|
+
export const ASSET_IDLE_TRIGGER_PATH = '/api/esb/server/event/triggerActionFlow';
|
|
9
|
+
/** 源资料固定参数:moduleSource=ebuilder(不暴露给调用方)。 */
|
|
10
|
+
export const ASSET_IDLE_MODULE_SOURCE = 'ebuilder';
|
|
11
|
+
/** 每页条数由服务端固定为 1000(源资料 2026-10-27 实测)。 */
|
|
12
|
+
export const ASSET_IDLE_PAGE_SIZE = 1000;
|
|
13
|
+
function assertPath(requestPath) {
|
|
14
|
+
if (typeof requestPath !== 'string' || !requestPath.startsWith('/') || requestPath.startsWith('//')) {
|
|
15
|
+
throw assetIdleValidationError('path_invalid', '请求路径必须是相对绝对路径');
|
|
16
|
+
}
|
|
17
|
+
if (/^https?:/i.test(requestPath) || /[\r\n]/.test(requestPath) || requestPath.includes('/../')) {
|
|
18
|
+
throw assetIdleValidationError('path_invalid', '请求路径不允许跨域或路径逃逸');
|
|
19
|
+
}
|
|
20
|
+
if (!requestPath.startsWith('/api/')) {
|
|
21
|
+
throw assetIdleValidationError('path_not_allowed', '闲置资产查询只能调用 /api/ 路径');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function mapRuntimeNetworkError(error) {
|
|
25
|
+
if (error instanceof E10ApiError && error.type === 'network') {
|
|
26
|
+
throw assetIdleNetworkError(error.subtype, error.message, {
|
|
27
|
+
retryable: error.retryable,
|
|
28
|
+
details: error.details,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 源资料实测:会话失效时接口返回 {"error":"authentication required","code":-2}(HTTP 200)。
|
|
35
|
+
* 该形态由共享 runtime 的 isE10SessionExpiredResponse 兜底之外再显式识别一次。
|
|
36
|
+
*/
|
|
37
|
+
export function isIdleAuthRejected(value) {
|
|
38
|
+
if (!value || typeof value !== 'object')
|
|
39
|
+
return false;
|
|
40
|
+
const record = value;
|
|
41
|
+
if (record.error === 'authentication required')
|
|
42
|
+
return true;
|
|
43
|
+
return String(record.code ?? '') === '-2' && typeof record.error === 'string';
|
|
44
|
+
}
|
|
45
|
+
/** mainTable 在 actionData.responseData.customData 下;缺失视为协议异常而不是空结果。 */
|
|
46
|
+
export function pickMainTable(value) {
|
|
47
|
+
const root = value;
|
|
48
|
+
const mainTable = root?.actionData?.responseData?.customData?.mainTable;
|
|
49
|
+
if (!mainTable || typeof mainTable !== 'object' || Array.isArray(mainTable))
|
|
50
|
+
return undefined;
|
|
51
|
+
const idleAssets = Array.isArray(mainTable.idleAssets) ? mainTable.idleAssets : undefined;
|
|
52
|
+
if (idleAssets === undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
return { idleAssets, hasMore: mainTable.hasMore === true };
|
|
55
|
+
}
|
|
56
|
+
export function resultCodeOf(value) {
|
|
57
|
+
if (!value || typeof value !== 'object')
|
|
58
|
+
return undefined;
|
|
59
|
+
const raw = value.resultCode;
|
|
60
|
+
if (raw === undefined || raw === null || raw === '')
|
|
61
|
+
return undefined;
|
|
62
|
+
const numeric = Number(raw);
|
|
63
|
+
return Number.isFinite(numeric) ? numeric : undefined;
|
|
64
|
+
}
|
|
65
|
+
export function createAssetIdleHost(ctx, dependencies = {}) {
|
|
66
|
+
let e10Session = null;
|
|
67
|
+
let sessionContext = null;
|
|
68
|
+
const requireSession = dependencies.requireSession || requireE10Session;
|
|
69
|
+
async function ensureSession() {
|
|
70
|
+
if (!e10Session) {
|
|
71
|
+
e10Session = await requireSession(ctx);
|
|
72
|
+
sessionContext = {
|
|
73
|
+
baseUrl: e10Session.getBaseUrl(),
|
|
74
|
+
profile: ctx.getGlobalOptions().profile || getActiveProfile(),
|
|
75
|
+
userId: e10Session.userId,
|
|
76
|
+
tenantKey: e10Session.tenantKey,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return e10Session;
|
|
80
|
+
}
|
|
81
|
+
async function getSession() {
|
|
82
|
+
await ensureSession();
|
|
83
|
+
return sessionContext;
|
|
84
|
+
}
|
|
85
|
+
async function runtime() {
|
|
86
|
+
return createE10RequestRuntime(ctx, await ensureSession(), { fetch: dependencies.fetch });
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
async fetchPage(esbFlowId, curPage) {
|
|
90
|
+
const page = Number(curPage);
|
|
91
|
+
if (!Number.isInteger(page) || page < 1) {
|
|
92
|
+
throw assetIdleValidationError('cur_page_invalid', `curPage 必须是 >= 1 的整数,收到:${String(curPage)}`);
|
|
93
|
+
}
|
|
94
|
+
assertPath(ASSET_IDLE_TRIGGER_PATH);
|
|
95
|
+
// 请求体形状来自源接口(find-idle-assents):esbFlowId + moduleSource + customParams.mainTable.curPage。
|
|
96
|
+
const body = {
|
|
97
|
+
esbFlowId,
|
|
98
|
+
moduleSource: ASSET_IDLE_MODULE_SOURCE,
|
|
99
|
+
customParams: { mainTable: { curPage: page } },
|
|
100
|
+
};
|
|
101
|
+
let response;
|
|
102
|
+
try {
|
|
103
|
+
response = await (await runtime()).requestJson({
|
|
104
|
+
module: 'asset-idle',
|
|
105
|
+
operation: 'asset-idle.idle.list',
|
|
106
|
+
method: 'POST',
|
|
107
|
+
path: ASSET_IDLE_TRIGGER_PATH,
|
|
108
|
+
body,
|
|
109
|
+
bigIntStrings: true,
|
|
110
|
+
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
|
111
|
+
timeoutMs: 60_000,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
mapRuntimeNetworkError(error);
|
|
116
|
+
}
|
|
117
|
+
if (isE10SessionExpiredResponse(response)) {
|
|
118
|
+
throw assetIdleAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
|
|
119
|
+
}
|
|
120
|
+
if (response.parseError) {
|
|
121
|
+
if (response.status < 200 || response.status >= 300) {
|
|
122
|
+
throw assetIdleApiError('http_error', `泛微接口 HTTP ${response.status}`, {
|
|
123
|
+
code: response.status,
|
|
124
|
+
details: { status: response.status },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
throw assetIdleNetworkError('protocol', `接口返回不是合法 JSON(HTTP ${response.status})`, { retryable: false });
|
|
128
|
+
}
|
|
129
|
+
if (response.status < 200 || response.status >= 300) {
|
|
130
|
+
throw assetIdleApiError('http_error', `泛微接口 HTTP ${response.status}`, {
|
|
131
|
+
code: response.status,
|
|
132
|
+
details: { status: response.status },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const value = response.value;
|
|
136
|
+
if (isIdleAuthRejected(value)) {
|
|
137
|
+
throw assetIdleAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { details: { code: -2 } });
|
|
138
|
+
}
|
|
139
|
+
const code = resultCodeOf(value);
|
|
140
|
+
if (code === undefined) {
|
|
141
|
+
throw assetIdleNetworkError('protocol', '动作流接口未返回 resultCode,无法判定执行结果', { retryable: false });
|
|
142
|
+
}
|
|
143
|
+
if (code !== 200) {
|
|
144
|
+
const record = (value && typeof value === 'object' ? value : {});
|
|
145
|
+
const serverMessage = typeof record.resultMsg === 'string' ? record.resultMsg.trim() : '';
|
|
146
|
+
throw assetIdleApiError('actionflow_failed', `动作流执行未成功(resultCode=${code}):${serverMessage || '服务端未返回原因'}`, {
|
|
147
|
+
code,
|
|
148
|
+
details: { resultCode: code, resultMsg: serverMessage },
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
const mainTable = pickMainTable(value);
|
|
152
|
+
if (!mainTable) {
|
|
153
|
+
throw assetIdleNetworkError('protocol', '返回缺少 actionData.responseData.customData.mainTable.idleAssets,无法解析判定结果', {
|
|
154
|
+
retryable: false,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return { items: mainTable.idleAssets, hasMore: mainTable.hasMore, resultCode: code };
|
|
158
|
+
},
|
|
159
|
+
getSession,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { ExitCode, isCliError, toErrorEnvelope } from '../../core/errors.js';
|
|
3
|
+
import { createAssetIdleHost } from './host.js';
|
|
4
|
+
import { assetIdleManifest } from './manifest.js';
|
|
5
|
+
import { executeAssetIdleOperation } from './operations.js';
|
|
6
|
+
import { assetIdleValidationError } from './errors.js';
|
|
7
|
+
function asOptions(value) {
|
|
8
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
9
|
+
}
|
|
10
|
+
async function readStdin() {
|
|
11
|
+
const chunks = [];
|
|
12
|
+
for await (const chunk of process.stdin) {
|
|
13
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
14
|
+
}
|
|
15
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Windows PowerShell 的管道与 Set-Content -Encoding utf8 会在内容前写入 UTF-8 BOM,
|
|
19
|
+
* 且可能重复写入多次;JSON.parse 不接受前导 BOM,必须先剥离。
|
|
20
|
+
*/
|
|
21
|
+
function stripBom(value) {
|
|
22
|
+
return value.replace(/^\uFEFF+/, '');
|
|
23
|
+
}
|
|
24
|
+
export async function inputFrom(options, extra = {}) {
|
|
25
|
+
const opts = asOptions(options);
|
|
26
|
+
let input = {};
|
|
27
|
+
if (opts.input && opts.inputJson) {
|
|
28
|
+
throw assetIdleValidationError('input_conflict', '不能同时传 --input 和 --input-json');
|
|
29
|
+
}
|
|
30
|
+
if (opts.inputJson) {
|
|
31
|
+
input = parseJsonInput(String(opts.inputJson), '--input-json');
|
|
32
|
+
}
|
|
33
|
+
if (opts.input) {
|
|
34
|
+
let raw;
|
|
35
|
+
try {
|
|
36
|
+
raw = stripBom(opts.input === '-' ? await readStdin() : await readFile(String(opts.input), 'utf8'));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw assetIdleValidationError('input_unreadable', `无法读取 --input:${String(opts.input)}`);
|
|
40
|
+
}
|
|
41
|
+
input = parseJsonInput(raw, '--input');
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
...input,
|
|
45
|
+
...Object.fromEntries(Object.entries(extra).filter(([, value]) => value !== undefined && value !== '')),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function parseJsonInput(raw, label) {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(stripBom(raw));
|
|
51
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
52
|
+
throw new Error('input must be an object');
|
|
53
|
+
}
|
|
54
|
+
return parsed;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw assetIdleValidationError('input_invalid', `${label} 不是合法 JSON 对象`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function successEnvelope(operation, result) {
|
|
61
|
+
return {
|
|
62
|
+
schemaVersion: 1,
|
|
63
|
+
ok: true,
|
|
64
|
+
operation,
|
|
65
|
+
data: result.data === undefined ? null : result.data,
|
|
66
|
+
...(result.meta ? { meta: result.meta } : {}),
|
|
67
|
+
warnings: result.warnings ?? [],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function failureEnvelope(error) {
|
|
71
|
+
return {
|
|
72
|
+
schemaVersion: 1,
|
|
73
|
+
ok: false,
|
|
74
|
+
error: toErrorEnvelope(error),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async function executeAndPrint(ctx, operation, input) {
|
|
78
|
+
try {
|
|
79
|
+
const result = await executeAssetIdleOperation(operation, input, {
|
|
80
|
+
ctx,
|
|
81
|
+
host: createAssetIdleHost(ctx),
|
|
82
|
+
});
|
|
83
|
+
ctx.output.write(successEnvelope(operation, result), { json: true });
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.error(JSON.stringify(failureEnvelope(error), null, 2));
|
|
87
|
+
process.exitCode = isCliError(error) ? error.exitCode : ExitCode.GeneralError;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export const assetIdleShortcut = {
|
|
91
|
+
name: 'asset-idle',
|
|
92
|
+
description: 'E10 闲置资产判定结果查询 CLI(分页获取/聚合统计)',
|
|
93
|
+
category: 'shortcut',
|
|
94
|
+
register({ program, action }) {
|
|
95
|
+
const assetIdle = program
|
|
96
|
+
.command('asset-idle')
|
|
97
|
+
.description('E10 闲置资产判定结果查询(服务端已判定,只读)');
|
|
98
|
+
assetIdle
|
|
99
|
+
.command('schema')
|
|
100
|
+
.description('Print asset-idle operation manifest for agents')
|
|
101
|
+
.action(action((ctx) => {
|
|
102
|
+
ctx.output.write(assetIdleManifest, { json: true });
|
|
103
|
+
}));
|
|
104
|
+
assetIdle
|
|
105
|
+
.command('run')
|
|
106
|
+
.description('Run an asset-idle operation with JSON input')
|
|
107
|
+
.argument('<operation>', 'operation name, e.g. asset-idle.idle.list')
|
|
108
|
+
.option('--input <path>', 'JSON input file, or - for stdin')
|
|
109
|
+
.option('--input-json <json>', 'inline JSON input object')
|
|
110
|
+
.action(action(async (ctx, operation, options) => {
|
|
111
|
+
await executeAndPrint(ctx, String(operation), await inputFrom(options));
|
|
112
|
+
}));
|
|
113
|
+
},
|
|
114
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/** 动作流 ID 在源资料示例中为长整型数字串,一律按字符串传递,避免精度丢失。 */
|
|
2
|
+
const flowId = { type: 'string', pattern: '^\\d+$', minLength: 1, maxLength: 32 };
|
|
3
|
+
const listInputProperties = {
|
|
4
|
+
esbFlowId: {
|
|
5
|
+
...flowId,
|
|
6
|
+
description: '「自用-skill获取闲置资产」动作流 ID。按名称解析需要 ESB 应用列表查询接口,本模块未实现(见 withheldOperations);可由用户在 ESB 中心查得后传入',
|
|
7
|
+
},
|
|
8
|
+
maxPages: {
|
|
9
|
+
type: 'integer',
|
|
10
|
+
minimum: 1,
|
|
11
|
+
maximum: 100,
|
|
12
|
+
default: 20,
|
|
13
|
+
description: '单次最多翻页数(每页 1000 条,服务端固定);超过即停止并在 warnings 标记 truncated',
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
/** 服务端已判定的闲置资产条目字段(2026-10-30 起动作流返回判定结果集,字段为拼音缩写)。 */
|
|
17
|
+
const idleItemSchema = {
|
|
18
|
+
type: 'object',
|
|
19
|
+
additionalProperties: false,
|
|
20
|
+
properties: {
|
|
21
|
+
zclx: { type: ['string', 'null'], description: '资产类型,如 笔记本电脑 / 显示器 / 乘用车' },
|
|
22
|
+
xzsz: { type: ['string', 'null'], description: '闲置时长(天),字符串数字,服务端已算好,直接使用;缺失标「无法判断」' },
|
|
23
|
+
pdyj: { type: ['string', 'null'], description: '判定依据,原样展示,不重新表述' },
|
|
24
|
+
zxd: { type: ['number', 'null'], description: '置信度,服务端给出(实测 0.8 / 0.85 / 0.9 / 0.95)' },
|
|
25
|
+
czjy: { type: ['string', 'null'], description: '处置建议编码:0=待调拨、1=待调剂、2=待评估;缺失标「无法判断」' },
|
|
26
|
+
xzdj: { type: ['string', 'null'], description: '闲置等级编码(0/1/2),含义待确认,原样透出' },
|
|
27
|
+
zcmc: { type: ['string', 'null'], description: '资产标识,当前返回资产记录 ID(不是名称)' },
|
|
28
|
+
unique_key: { type: ['string', 'null'], description: '记录主键' },
|
|
29
|
+
id: { type: ['string', 'null'], description: '记录主键' },
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
const listOutputProperties = {
|
|
33
|
+
esbFlowId: { type: 'string', description: '本次查询使用的动作流 ID' },
|
|
34
|
+
items: { type: 'array', items: idleItemSchema, description: '各页 idleAssets 合并后的判定结果集,返回条数即服务端判定的闲置资产全集' },
|
|
35
|
+
total: { type: 'integer', minimum: 0, description: '合并后的总条数(统计分母)' },
|
|
36
|
+
pages: { type: 'integer', minimum: 0, description: '实际请求页数' },
|
|
37
|
+
hasMore: { type: 'boolean', description: '停止时服务端是否仍标记有下一页' },
|
|
38
|
+
truncated: { type: 'boolean', description: '是否因达到 maxPages 上限提前停止' },
|
|
39
|
+
};
|
|
40
|
+
const agingBucketSchema = {
|
|
41
|
+
type: 'object',
|
|
42
|
+
additionalProperties: false,
|
|
43
|
+
properties: {
|
|
44
|
+
bucket: { type: 'string', enum: ['1个月以内', '1~3个月', '3~6个月', '6~12个月', '一年以上'], description: '闲置时长档位(1 个月按 30 天计,边界值归前一档)' },
|
|
45
|
+
minDays: { type: 'integer', description: '档位下界(天,含)' },
|
|
46
|
+
maxDays: { type: ['integer', 'null'], description: '档位上界(天,含);一年以上为 null' },
|
|
47
|
+
count: { type: 'integer', minimum: 0, description: '档位内条数;数量为 0 的档位不出现' },
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
const statsOutputProperties = {
|
|
51
|
+
esbFlowId: { type: 'string', description: '本次查询使用的动作流 ID' },
|
|
52
|
+
total: { type: 'integer', minimum: 0, description: '服务端判定结果集总条数(统计分母)' },
|
|
53
|
+
pages: { type: 'integer', minimum: 0, description: '实际请求页数' },
|
|
54
|
+
truncated: { type: 'boolean', description: '是否因达到 maxPages 上限提前停止(此时统计不完整)' },
|
|
55
|
+
czjyCounts: {
|
|
56
|
+
type: 'array',
|
|
57
|
+
description: '处置建议分组计数(编码映射固定:0=待调拨、1=待调剂、2=待评估;缺失标「无法判断」)',
|
|
58
|
+
items: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
additionalProperties: false,
|
|
61
|
+
properties: {
|
|
62
|
+
code: { type: ['string', 'null'], description: 'czjy 编码;缺失为 null' },
|
|
63
|
+
label: { type: 'string', enum: ['待调拨', '待调剂', '待评估', '无法判断'] },
|
|
64
|
+
count: { type: 'integer', minimum: 0 },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
typeCounts: {
|
|
69
|
+
type: 'array',
|
|
70
|
+
description: '按 zclx 计数,降序;Top10 + 其他 的合并展示由渲染侧处理',
|
|
71
|
+
items: {
|
|
72
|
+
type: 'object',
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
properties: {
|
|
75
|
+
type: { type: 'string', description: 'zclx 资产类型;缺失按「未知类型」计入' },
|
|
76
|
+
count: { type: 'integer', minimum: 0 },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
agingBuckets: {
|
|
81
|
+
type: 'array',
|
|
82
|
+
items: agingBucketSchema,
|
|
83
|
+
description: '闲置时长分档计数,固定顺序,仅含数量大于 0 的档位;xzsz 缺失的记录排除出所有档位',
|
|
84
|
+
},
|
|
85
|
+
unableToJudge: {
|
|
86
|
+
type: 'integer',
|
|
87
|
+
minimum: 0,
|
|
88
|
+
description: 'xzsz 缺失(无法判断)条数:不参与类型统计与时长分档,也不计入 agingBuckets',
|
|
89
|
+
},
|
|
90
|
+
unableToJudgeByType: {
|
|
91
|
+
type: 'integer',
|
|
92
|
+
minimum: 0,
|
|
93
|
+
description: 'zclx 也缺失且 xzsz 缺失的条数(既不进类型统计也不进时长分档的口径提示)',
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
export const assetIdleOperations = [
|
|
97
|
+
{
|
|
98
|
+
name: 'asset-idle.idle.list',
|
|
99
|
+
summary: '触发「自用-skill获取闲置资产」动作流,按 hasMore 自动翻页并合并各页闲置资产判定结果(只读查询)',
|
|
100
|
+
risk: 'read',
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
additionalProperties: false,
|
|
104
|
+
properties: listInputProperties,
|
|
105
|
+
required: ['esbFlowId'],
|
|
106
|
+
},
|
|
107
|
+
outputSchema: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
additionalProperties: false,
|
|
110
|
+
properties: listOutputProperties,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'asset-idle.idle.stats',
|
|
115
|
+
summary: '在 idle.list 合并结果上做聚合统计:czjy 处置建议分组、zclx 类型计数、xzsz 闲置时长分档(不做判定、不重算)',
|
|
116
|
+
risk: 'read',
|
|
117
|
+
inputSchema: {
|
|
118
|
+
type: 'object',
|
|
119
|
+
additionalProperties: false,
|
|
120
|
+
properties: listInputProperties,
|
|
121
|
+
required: ['esbFlowId'],
|
|
122
|
+
},
|
|
123
|
+
outputSchema: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
additionalProperties: false,
|
|
126
|
+
properties: statsOutputProperties,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
export const assetIdleManifest = {
|
|
131
|
+
schemaVersion: 1,
|
|
132
|
+
name: 'asset-idle',
|
|
133
|
+
title: 'E10 闲置资产判定结果查询',
|
|
134
|
+
version: '0.1.0',
|
|
135
|
+
host: 'e10-asset-idle',
|
|
136
|
+
status: 'builtin',
|
|
137
|
+
sourceSkill: 'weaver-e10-ziguanjia-idle',
|
|
138
|
+
operations: assetIdleOperations,
|
|
139
|
+
withheldOperations: [
|
|
140
|
+
{
|
|
141
|
+
name: 'asset-idle.flow.resolve',
|
|
142
|
+
reason: '按名称解析「自用-skill获取闲置资产」动作流 ID 需要 ESB 应用列表查询接口;源资料只提供旧版 e10-cli 的 actionflow application-list 命令名,没有 weaver-work-cli 可用的接口路径、请求字段与返回结构,无法安全实现。esbFlowId 必须由调用方传入(可在旧版 e10-cli 执行 actionflow application-list 按 name 精确匹配 moudle=ebuilder 取 id,匹配不到或多条同名时停止)。',
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
/** 源资料明确:接口不返回 idle_date / stockindate / use_term 等原始台账字段,不做基于日期的判定与阈值反推。 */
|
|
146
|
+
disabledCapabilities: [
|
|
147
|
+
{
|
|
148
|
+
name: 'idle-date-recalculation',
|
|
149
|
+
reason: '闲置判定由服务端动作流完成(xzsz / pdyj / zxd / czjy 直接使用),CLI 不做二次判定、不重算、不补算、不改写结论;接口不返回 idle_date / stockindate / use_term,不得反查或推算日期补齐。',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'display-only-fields',
|
|
153
|
+
reason: 'czjy_obj / xzdj_obj / yjzczt_obj / czlc / czlc_obj / zcxzczrq 实测恒为空,不可依赖,条目归一化时不透出;字段语义变化时以 source-manifest 的更新检测为准。',
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
export function hasAssetIdleOperation(operation) {
|
|
158
|
+
return assetIdleOperations.some((item) => item.name === operation);
|
|
159
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { assetIdleSuccess } from './types.js';
|
|
2
|
+
import { aggregateIdleAssets, maxPagesOf, normalizeIdleAsset, requiredFlowId, } from './shared.js';
|
|
3
|
+
/**
|
|
4
|
+
* 分页合并循环:源资料口径——curPage 从 1 开始;resultCode=200 且 hasMore=true 则 curPage+1 继续;
|
|
5
|
+
* hasMore=false 表示分页查询完成;每页数据在 idleAssets 数组里,各页合并成总列表。
|
|
6
|
+
*/
|
|
7
|
+
export async function fetchAllIdleAssets(esbFlowId, maxPages, env) {
|
|
8
|
+
const items = [];
|
|
9
|
+
let curPage = 1;
|
|
10
|
+
let hasMore = true;
|
|
11
|
+
let truncated = false;
|
|
12
|
+
while (hasMore) {
|
|
13
|
+
const page = await env.host.fetchPage(esbFlowId, curPage);
|
|
14
|
+
for (const raw of page.items) {
|
|
15
|
+
items.push(normalizeIdleAsset(raw));
|
|
16
|
+
}
|
|
17
|
+
hasMore = page.hasMore;
|
|
18
|
+
if (hasMore && curPage >= maxPages) {
|
|
19
|
+
truncated = true;
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
curPage += 1;
|
|
23
|
+
}
|
|
24
|
+
return { items, pages: truncated ? maxPages : curPage - 1, hasMore, truncated };
|
|
25
|
+
}
|
|
26
|
+
function listWarnings(truncated) {
|
|
27
|
+
const warnings = [];
|
|
28
|
+
if (truncated) {
|
|
29
|
+
warnings.push(`已达单次翻页上限,hasMore 仍为 true:返回结果可能不是闲置资产全集,统计前请增大 maxPages 后重查`);
|
|
30
|
+
}
|
|
31
|
+
return warnings;
|
|
32
|
+
}
|
|
33
|
+
export const assetIdleHandlers = {
|
|
34
|
+
'asset-idle.idle.list': async (input, env) => {
|
|
35
|
+
const esbFlowId = requiredFlowId(input.esbFlowId);
|
|
36
|
+
const maxPages = maxPagesOf(input.maxPages);
|
|
37
|
+
const { items, pages, hasMore, truncated } = await fetchAllIdleAssets(esbFlowId, maxPages, env);
|
|
38
|
+
return assetIdleSuccess('asset-idle.idle.list', { esbFlowId, items, total: items.length, pages, hasMore, truncated }, { status: 'COMPLETE' }, listWarnings(truncated));
|
|
39
|
+
},
|
|
40
|
+
'asset-idle.idle.stats': async (input, env) => {
|
|
41
|
+
const esbFlowId = requiredFlowId(input.esbFlowId);
|
|
42
|
+
const maxPages = maxPagesOf(input.maxPages);
|
|
43
|
+
const { items, pages, truncated } = await fetchAllIdleAssets(esbFlowId, maxPages, env);
|
|
44
|
+
const stats = aggregateIdleAssets(items);
|
|
45
|
+
return assetIdleSuccess('asset-idle.idle.stats', { esbFlowId, total: items.length, pages, truncated, ...stats }, { status: 'COMPLETE' }, listWarnings(truncated));
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { assetIdleManifest } from '../manifest.js';
|
|
2
|
+
import { assetIdleValidationError } from '../errors.js';
|
|
3
|
+
import { assetIdleHandlers } from './idle.js';
|
|
4
|
+
/**
|
|
5
|
+
* manifest 提供 WHAT(name/summary/risk/inputSchema/outputSchema),handler 提供 HOW。
|
|
6
|
+
* 两侧必须严格一一对应:任一侧多出条目都直接抛错,避免 schema 与实际能力漂移。
|
|
7
|
+
*/
|
|
8
|
+
function buildRegistry() {
|
|
9
|
+
const registry = new Map();
|
|
10
|
+
const handlerMap = assetIdleHandlers;
|
|
11
|
+
for (const contract of assetIdleManifest.operations) {
|
|
12
|
+
const handler = handlerMap[contract.name];
|
|
13
|
+
if (!handler)
|
|
14
|
+
throw new Error(`asset-idle operation ${contract.name} has no handler`);
|
|
15
|
+
registry.set(contract.name, { ...contract, handler });
|
|
16
|
+
}
|
|
17
|
+
for (const name of Object.keys(handlerMap)) {
|
|
18
|
+
if (!registry.has(name))
|
|
19
|
+
throw new Error(`asset-idle handler ${name} is not declared in manifest`);
|
|
20
|
+
}
|
|
21
|
+
return registry;
|
|
22
|
+
}
|
|
23
|
+
const assetIdleOperationRegistry = buildRegistry();
|
|
24
|
+
export function getAssetIdleOperation(operation) {
|
|
25
|
+
const definition = assetIdleOperationRegistry.get(operation);
|
|
26
|
+
if (!definition) {
|
|
27
|
+
throw assetIdleValidationError('operation_unknown', `闲置资产查询不支持操作:${operation}`);
|
|
28
|
+
}
|
|
29
|
+
return definition;
|
|
30
|
+
}
|
|
31
|
+
export function assetIdleOperationDefinitions() {
|
|
32
|
+
return [...assetIdleOperationRegistry.values()];
|
|
33
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { assetIdleValidationError } from '../errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* 闲置资产条目归一化与聚合统计(纯函数,供单测直接断言业务口径)。
|
|
4
|
+
* 口径全部来自源 skill weaver-e10-ziguanjia-idle:
|
|
5
|
+
* - 判定由服务端完成(xzsz/pdyj/zxd/czjy 直接使用),CLI 不重算、不补算;
|
|
6
|
+
* - czjy 编码固定:0=待调拨、1=待调剂、2=待评估;缺失标「无法判断」;
|
|
7
|
+
* - xzsz 缺失的条目标「无法判断」,排除出所有统计,不参与类型统计与时长分档;
|
|
8
|
+
* - 闲置时长分档(1 个月按 30 天计,边界值归前一档):
|
|
9
|
+
* 1个月以内 ≤30、1~3个月 31–90、3~6个月 91–180、6~12个月 181–365、一年以上 >365。
|
|
10
|
+
*/
|
|
11
|
+
/** 源资料实测恒为空的展示字段:不可依赖,归一化时不透出。 */
|
|
12
|
+
export const DISPLAY_ONLY_FIELDS = [
|
|
13
|
+
'czjy_obj',
|
|
14
|
+
'xzdj_obj',
|
|
15
|
+
'yjzczt_obj',
|
|
16
|
+
'czlc',
|
|
17
|
+
'czlc_obj',
|
|
18
|
+
'zcxzczrq',
|
|
19
|
+
];
|
|
20
|
+
function asText(value) {
|
|
21
|
+
if (value === undefined || value === null)
|
|
22
|
+
return null;
|
|
23
|
+
if (typeof value === 'object')
|
|
24
|
+
return null;
|
|
25
|
+
const text = String(value).trim();
|
|
26
|
+
return text === '' ? null : text;
|
|
27
|
+
}
|
|
28
|
+
export function normalizeIdleAsset(raw) {
|
|
29
|
+
const record = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
30
|
+
const zxdText = asText(record.zxd);
|
|
31
|
+
const zxd = zxdText === null ? null : Number.isFinite(Number(zxdText)) ? Number(zxdText) : null;
|
|
32
|
+
return {
|
|
33
|
+
zclx: asText(record.zclx),
|
|
34
|
+
xzsz: asText(record.xzsz),
|
|
35
|
+
pdyj: asText(record.pdyj),
|
|
36
|
+
zxd,
|
|
37
|
+
czjy: asText(record.czjy),
|
|
38
|
+
xzdj: asText(record.xzdj),
|
|
39
|
+
zcmc: asText(record.zcmc),
|
|
40
|
+
unique_key: asText(record.unique_key),
|
|
41
|
+
id: asText(record.id),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export const CZJY_LABELS = {
|
|
45
|
+
'0': '待调拨',
|
|
46
|
+
'1': '待调剂',
|
|
47
|
+
'2': '待评估',
|
|
48
|
+
};
|
|
49
|
+
/** 档位定义:maxDays 为 null 表示一年以上(>365)。 */
|
|
50
|
+
export const AGING_BUCKETS = [
|
|
51
|
+
{ bucket: '1个月以内', minDays: 0, maxDays: 30 },
|
|
52
|
+
{ bucket: '1~3个月', minDays: 31, maxDays: 90 },
|
|
53
|
+
{ bucket: '3~6个月', minDays: 91, maxDays: 180 },
|
|
54
|
+
{ bucket: '6~12个月', minDays: 181, maxDays: 365 },
|
|
55
|
+
{ bucket: '一年以上', minDays: 366, maxDays: null },
|
|
56
|
+
];
|
|
57
|
+
/** 边界值归前一档(30 天算「1个月以内」、90 天算「1~3个月」);负数或非整数天数为无法判断。 */
|
|
58
|
+
export function agingBucketOf(days) {
|
|
59
|
+
if (!Number.isInteger(days) || days < 0)
|
|
60
|
+
return null;
|
|
61
|
+
for (const bucket of AGING_BUCKETS) {
|
|
62
|
+
if (bucket.maxDays === null || days <= bucket.maxDays)
|
|
63
|
+
return bucket.bucket;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
export function aggregateIdleAssets(items) {
|
|
68
|
+
const czjyOrder = [
|
|
69
|
+
{ code: '0', label: '待调拨' },
|
|
70
|
+
{ code: '1', label: '待调剂' },
|
|
71
|
+
{ code: '2', label: '待评估' },
|
|
72
|
+
{ code: null, label: '无法判断' },
|
|
73
|
+
];
|
|
74
|
+
const czjyCounts = czjyOrder.map(({ code, label }) => ({
|
|
75
|
+
code,
|
|
76
|
+
label,
|
|
77
|
+
count: items.filter((item) => item.czjy === code).length,
|
|
78
|
+
}));
|
|
79
|
+
// xzsz 缺失(无法判断)的条目:排除出类型统计与时长分档。
|
|
80
|
+
const judgedItems = items.filter((item) => item.xzsz !== null);
|
|
81
|
+
const unableToJudge = items.length - judgedItems.length;
|
|
82
|
+
const typeCounter = new Map();
|
|
83
|
+
let unableToJudgeByType = 0;
|
|
84
|
+
for (const item of judgedItems) {
|
|
85
|
+
const type = item.zclx ?? '未知类型';
|
|
86
|
+
if (item.zclx === null)
|
|
87
|
+
unableToJudgeByType += 1;
|
|
88
|
+
typeCounter.set(type, (typeCounter.get(type) ?? 0) + 1);
|
|
89
|
+
}
|
|
90
|
+
const typeCounts = [...typeCounter.entries()]
|
|
91
|
+
.map(([type, count]) => ({ type, count }))
|
|
92
|
+
.sort((a, b) => b.count - a.count || a.type.localeCompare(b.type, 'zh-Hans-CN'));
|
|
93
|
+
const agingCounts = new Map();
|
|
94
|
+
for (const item of judgedItems) {
|
|
95
|
+
const days = Number(item.xzsz);
|
|
96
|
+
const bucket = Number.isFinite(days) ? agingBucketOf(Math.trunc(days)) : null;
|
|
97
|
+
if (bucket === null)
|
|
98
|
+
continue;
|
|
99
|
+
agingCounts.set(bucket, (agingCounts.get(bucket) ?? 0) + 1);
|
|
100
|
+
}
|
|
101
|
+
const agingBuckets = AGING_BUCKETS.filter(({ bucket }) => (agingCounts.get(bucket) ?? 0) > 0).map(({ bucket, minDays, maxDays }) => ({ bucket, minDays, maxDays, count: agingCounts.get(bucket) ?? 0 }));
|
|
102
|
+
return { czjyCounts, typeCounts, agingBuckets, unableToJudge, unableToJudgeByType };
|
|
103
|
+
}
|
|
104
|
+
/** 顶层输入白名单(与 manifest inputSchema.properties 对齐,schema 即运行时约束)。 */
|
|
105
|
+
export function assertNoForbiddenKeys(input) {
|
|
106
|
+
for (const key of ['cookie', 'eteamsid', 'token', 'access_token']) {
|
|
107
|
+
if (key in input) {
|
|
108
|
+
throw assetIdleValidationError('forbidden_key', `输入不允许携带凭据类字段:${key}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export function assertKnownKeys(input, allowed, operation) {
|
|
113
|
+
for (const key of Object.keys(input)) {
|
|
114
|
+
if (!allowed.includes(key)) {
|
|
115
|
+
throw assetIdleValidationError('input_unknown_key', `operation ${operation} 不支持输入字段:${key}(可用字段:${allowed.join(', ') || '无'})`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export function asObject(value) {
|
|
120
|
+
if (value === undefined || value === null)
|
|
121
|
+
return {};
|
|
122
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
123
|
+
throw assetIdleValidationError('input_invalid', '输入必须是 JSON 对象');
|
|
124
|
+
}
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
export function requiredFlowId(value) {
|
|
128
|
+
const text = typeof value === 'string' ? value.trim() : String(value ?? '');
|
|
129
|
+
if (!/^\d{1,32}$/.test(text)) {
|
|
130
|
+
throw assetIdleValidationError('input_invalid', 'esbFlowId 必须是 1-32 位数字字符串');
|
|
131
|
+
}
|
|
132
|
+
return text;
|
|
133
|
+
}
|
|
134
|
+
export function maxPagesOf(value) {
|
|
135
|
+
if (value === undefined || value === null || value === '')
|
|
136
|
+
return 20;
|
|
137
|
+
const num = Number(value);
|
|
138
|
+
if (!Number.isInteger(num) || num < 1 || num > 100) {
|
|
139
|
+
throw assetIdleValidationError('input_invalid', 'maxPages 必须是 1-100 的整数');
|
|
140
|
+
}
|
|
141
|
+
return num;
|
|
142
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const READ = 'read';
|
|
2
|
+
export function assetIdleSuccess(action, data, meta, warnings = []) {
|
|
3
|
+
const payload = { ok: true, action, data };
|
|
4
|
+
if (meta && Object.keys(meta).length > 0)
|
|
5
|
+
payload.meta = meta;
|
|
6
|
+
if (warnings.length > 0)
|
|
7
|
+
payload.warnings = warnings;
|
|
8
|
+
return payload;
|
|
9
|
+
}
|