worsoft-daily-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example ADDED
@@ -0,0 +1,15 @@
1
+ # Worsoft gateway base URL. Frontend dev proxy /api maps to this target.
2
+ WORSOFT_BASE_URL=https://saas.link-work.com.cn/api
3
+
4
+ # Login credentials. Prefer setting these in your MCP client config env.
5
+ WORSOFT_USERNAME=
6
+ WORSOFT_PASSWORD=
7
+
8
+ # Frontend login uses Basic cGlnOnBpZw==, platform pc_saas, VERSION LT.
9
+ WORSOFT_BASIC_AUTH=Basic cGlnOnBpZw==
10
+ WORSOFT_PLATFORM=pc_saas
11
+ WORSOFT_VERSION=LT
12
+
13
+ # Optional defaults.
14
+ WORSOFT_DEFAULT_PAGE_SIZE=100
15
+ WORSOFT_TIMEOUT_MS=120000
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # Worsoft Daily MCP
2
+
3
+ 这个 MCP 用来让模型通过 Worsoft 后端接口读取日报、获取日报详情、收集月报总结素材,并提交日报或月报。
4
+
5
+ ## 已对齐的现有接口
6
+
7
+ - 登录:`POST /auth/oauth/token`
8
+ - 当前用户:`GET /admin/user/info`
9
+ - 日报查询:`GET /hr/HrReportDaily/page`
10
+ - 日报详情:`GET /hr/HrReportDaily/{id}`
11
+ - 新增日报/月报:`POST /hr/HrReportDaily`
12
+ - 修改日报/月报:`PUT /hr/HrReportDaily`
13
+ - 当前时间段任务:`GET /hr/HrReportDaily/getTaskByExecuteUserId`
14
+
15
+ 前端开发代理里的 `/api` 已被折叠进 `WORSOFT_BASE_URL`,默认值是 `https://saas.link-work.com.cn/api`。
16
+
17
+ ## 配置
18
+
19
+ 推荐在 MCP 客户端配置环境变量,不要把账号密码写进仓库。
20
+
21
+ ```json
22
+ {
23
+ "mcpServers": {
24
+ "worsoft-daily": {
25
+ "command": "node",
26
+ "args": ["E:\\project\\worsoft-daily-mcp\\src\\server.js"],
27
+ "env": {
28
+ "WORSOFT_BASE_URL": "https://saas.link-work.com.cn/api",
29
+ "WORSOFT_USERNAME": "你的用户名",
30
+ "WORSOFT_PASSWORD": "你的密码",
31
+ "WORSOFT_PLATFORM": "pc_saas",
32
+ "WORSOFT_VERSION": "LT",
33
+ "WORSOFT_BASIC_AUTH": "Basic cGlnOnBpZw=="
34
+ }
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ 也可以复制 `.env.example` 为 `.env` 后在本目录运行,但不要提交 `.env`。
41
+
42
+ ## 工具
43
+
44
+ - `worsoft_login`:登录并缓存 token。
45
+ - `worsoft_get_user_info`:获取当前登录用户。
46
+ - `worsoft_query_reports`:按类型、时间和范围查询汇报。
47
+ - `worsoft_get_report`:获取单条完整汇报详情。注意后端会把当前用户标记为已读。
48
+ - `worsoft_get_report_tasks`:获取指定时间段内当前用户相关任务。
49
+ - `worsoft_month_summary_source`:读取某个月的日报详情,供模型生成月报总结。
50
+ - `worsoft_submit_report`:新增或修改日报、周报、月报。建议先传 `dryRun: true` 检查 payload。
51
+
52
+ ## 查询语义
53
+
54
+ `scope` 会映射到后端的 `sign`:
55
+
56
+ - `related` -> `0`,我提交或我收到
57
+ - `submitted` -> `1`,我提交的
58
+ - `received` -> `2`,我收到的
59
+
60
+ `reportType` 会映射到后端的 `reportingType`:
61
+
62
+ - `daily` -> `0`
63
+ - `weekly` -> `1`
64
+ - `monthly` -> `2`
65
+ - `all` -> 不过滤类型
66
+
67
+ 日期过滤会按前端页面同款逻辑转换成:
68
+
69
+ - `startTime >= startDate`
70
+ - `endTime <= endDate`
71
+
72
+ ## 月报流程建议
73
+
74
+ 1. 调 `worsoft_month_summary_source`,例如 `{ "month": "2026-06", "scope": "submitted" }`。
75
+ 2. 模型根据返回的日报详情生成月报正文和下月计划。
76
+ 3. 调 `worsoft_submit_report`,`reportType` 传 `monthly`,`startDate` 传当月 1 日,`endDate` 传当月最后一天。
77
+ 4. 首次提交先用 `dryRun: true`,确认后再真实提交。
78
+
79
+ ## 本地检查
80
+
81
+ ```powershell
82
+ cd E:\project\worsoft-daily-mcp
83
+ npm run check
84
+ ```
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "worsoft-daily-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP stdio server for Worsoft daily and monthly reports.",
5
+ "type": "module",
6
+ "bin": "./src/server.js",
7
+ "scripts": {
8
+ "start": "node ./src/server.js",
9
+ "check": "node --check ./src/server.js"
10
+ },
11
+ "engines": {
12
+ "node": ">=18"
13
+ },
14
+ "license": "UNLICENSED"
15
+ }
package/src/server.js ADDED
@@ -0,0 +1,647 @@
1
+ #!/usr/bin/env node
2
+
3
+ import readline from "node:readline";
4
+ import { readFileSync, existsSync } from "node:fs";
5
+ import { resolve } from "node:path";
6
+
7
+ const SERVER_NAME = "worsoft-daily-mcp";
8
+ const SERVER_VERSION = "0.1.0";
9
+
10
+ loadDotEnv();
11
+
12
+ const config = {
13
+ baseUrl: trimTrailingSlash(env("WORSOFT_BASE_URL", "https://saas.link-work.com.cn/api")),
14
+ username: env("WORSOFT_USERNAME", ""),
15
+ password: env("WORSOFT_PASSWORD", ""),
16
+ basicAuth: env("WORSOFT_BASIC_AUTH", "Basic cGlnOnBpZw=="),
17
+ platform: env("WORSOFT_PLATFORM", "pc_saas"),
18
+ version: env("WORSOFT_VERSION", "LT"),
19
+ timeoutMs: Number(env("WORSOFT_TIMEOUT_MS", "120000")),
20
+ defaultPageSize: Number(env("WORSOFT_DEFAULT_PAGE_SIZE", "100"))
21
+ };
22
+
23
+ let tokenState = {
24
+ accessToken: "",
25
+ refreshToken: "",
26
+ expiresAt: 0
27
+ };
28
+
29
+ const tools = [
30
+ {
31
+ name: "worsoft_login",
32
+ description: "Login to Worsoft and cache the access token. Usually credentials should come from MCP environment variables.",
33
+ inputSchema: {
34
+ type: "object",
35
+ properties: {
36
+ username: { type: "string", description: "Optional username override." },
37
+ password: { type: "string", description: "Optional password override." },
38
+ baseUrl: { type: "string", description: "Optional Worsoft gateway base URL, for example https://saas.link-work.com.cn/api." }
39
+ },
40
+ additionalProperties: false
41
+ }
42
+ },
43
+ {
44
+ name: "worsoft_get_user_info",
45
+ description: "Get the currently logged-in Worsoft user information from /admin/user/info.",
46
+ inputSchema: {
47
+ type: "object",
48
+ properties: {},
49
+ additionalProperties: false
50
+ }
51
+ },
52
+ {
53
+ name: "worsoft_query_reports",
54
+ description: "Query Worsoft report records with business-friendly filters. Supports daily, weekly, and monthly reports.",
55
+ inputSchema: {
56
+ type: "object",
57
+ properties: {
58
+ reportType: {
59
+ type: "string",
60
+ enum: ["daily", "weekly", "monthly", "all"],
61
+ description: "Report type. daily=0, weekly=1, monthly=2."
62
+ },
63
+ scope: {
64
+ type: "string",
65
+ enum: ["related", "submitted", "received"],
66
+ description: "related queries reports submitted by or sent to current user. submitted queries only current user's reports. received queries reports sent to current user."
67
+ },
68
+ startDate: { type: "string", description: "Start date inclusive, yyyy-MM-dd." },
69
+ endDate: { type: "string", description: "End date inclusive, yyyy-MM-dd." },
70
+ current: { type: "integer", minimum: 1, description: "Page number." },
71
+ size: { type: "integer", minimum: 1, maximum: 500, description: "Page size." },
72
+ smartVal: { type: "string", description: "Optional keyword search." },
73
+ includeDetails: { type: "boolean", description: "Whether to fetch full detail for each returned record." }
74
+ },
75
+ additionalProperties: false
76
+ }
77
+ },
78
+ {
79
+ name: "worsoft_get_report",
80
+ description: "Get full detail for one Worsoft report by id. The backend marks the report as read for the current user.",
81
+ inputSchema: {
82
+ type: "object",
83
+ required: ["id"],
84
+ properties: {
85
+ id: { type: "string", description: "Report id." }
86
+ },
87
+ additionalProperties: false
88
+ }
89
+ },
90
+ {
91
+ name: "worsoft_get_report_tasks",
92
+ description: "Get current user's task records that overlap a date range, matching the frontend report form behavior.",
93
+ inputSchema: {
94
+ type: "object",
95
+ required: ["startDate", "endDate"],
96
+ properties: {
97
+ startDate: { type: "string", description: "Start date, yyyy-MM-dd." },
98
+ endDate: { type: "string", description: "End date, yyyy-MM-dd." }
99
+ },
100
+ additionalProperties: false
101
+ }
102
+ },
103
+ {
104
+ name: "worsoft_submit_report",
105
+ description: "Create or update a Worsoft report. Use dryRun first when a model has generated the content.",
106
+ inputSchema: {
107
+ type: "object",
108
+ required: ["reportType", "startDate", "endDate", "todayTaskConditionStatement"],
109
+ properties: {
110
+ id: { type: "string", description: "Existing report id. If set, the MCP updates that report; otherwise it creates one." },
111
+ reportType: {
112
+ type: "string",
113
+ enum: ["daily", "weekly", "monthly"],
114
+ description: "daily=0, weekly=1, monthly=2."
115
+ },
116
+ startDate: { type: "string", description: "Start date, yyyy-MM-dd." },
117
+ endDate: { type: "string", description: "End date, yyyy-MM-dd." },
118
+ todayTaskConditionStatement: { type: "string", description: "Current period completion summary. HTML is accepted because the backend form uses rich text." },
119
+ tomorrowTaskConditionStatement: { type: "string", description: "Next period plan. HTML is accepted." },
120
+ publicOrNot: { type: "string", enum: ["0", "1"], description: "Matches backend field publicOrNot. Defaults to the frontend form value 0." },
121
+ currentTaskId: { type: "string", description: "Comma-separated current task ids. If omitted and includeCurrentTasks=true, MCP fills it from getTaskByExecuteUserId." },
122
+ nextTimeTaskId: { type: "string", description: "Comma-separated next period task ids." },
123
+ recipientUsers: {
124
+ type: "array",
125
+ description: "Copied recipients.",
126
+ items: {
127
+ type: "object",
128
+ required: ["recipientUserId", "recipientUser"],
129
+ properties: {
130
+ recipientUserId: { type: ["string", "number"] },
131
+ recipientUser: { type: "string" },
132
+ signExamine: { type: "string", enum: ["0", "1"] },
133
+ recipientUserType: { type: "string", enum: ["0", "1"] }
134
+ },
135
+ additionalProperties: false
136
+ }
137
+ },
138
+ includeCurrentTasks: { type: "boolean", description: "When true, fills currentTaskId and reviewer details from overlapping tasks." },
139
+ bugIds: { type: "string", description: "Comma-separated related bug ids." },
140
+ dryRun: { type: "boolean", description: "When true, return the payload without submitting." }
141
+ },
142
+ additionalProperties: false
143
+ }
144
+ },
145
+ {
146
+ name: "worsoft_month_summary_source",
147
+ description: "Fetch detailed daily reports for a month so the calling model can summarize them into a monthly report.",
148
+ inputSchema: {
149
+ type: "object",
150
+ required: ["month"],
151
+ properties: {
152
+ month: { type: "string", description: "Month in yyyy-MM format." },
153
+ scope: {
154
+ type: "string",
155
+ enum: ["related", "submitted", "received"],
156
+ description: "Usually submitted for summarizing your own month."
157
+ },
158
+ includeWeeklyAndMonthly: { type: "boolean", description: "Include weekly/monthly reports too. Defaults to false, daily only." }
159
+ },
160
+ additionalProperties: false
161
+ }
162
+ }
163
+ ];
164
+
165
+ const handlers = {
166
+ async worsoft_login(args) {
167
+ const loginResult = await login(args);
168
+ return {
169
+ ok: true,
170
+ baseUrl: config.baseUrl,
171
+ tokenType: loginResult.token_type,
172
+ expiresIn: loginResult.expires_in,
173
+ hasRefreshToken: Boolean(loginResult.refresh_token)
174
+ };
175
+ },
176
+
177
+ async worsoft_get_user_info() {
178
+ return unwrap(await apiRequest("GET", "/admin/user/info"));
179
+ },
180
+
181
+ async worsoft_query_reports(args) {
182
+ const response = unwrap(await apiRequest("GET", "/hr/HrReportDaily/page", {
183
+ query: buildReportQuery(args)
184
+ }));
185
+ if (!args?.includeDetails) {
186
+ return response;
187
+ }
188
+ const records = Array.isArray(response?.records) ? response.records : [];
189
+ const details = [];
190
+ for (const record of records) {
191
+ if (record?.id != null) {
192
+ details.push(unwrap(await apiRequest("GET", `/hr/HrReportDaily/${record.id}`)));
193
+ }
194
+ }
195
+ return { ...response, records: details };
196
+ },
197
+
198
+ async worsoft_get_report(args) {
199
+ assertRequired(args, ["id"]);
200
+ return unwrap(await apiRequest("GET", `/hr/HrReportDaily/${encodeURIComponent(String(args.id))}`));
201
+ },
202
+
203
+ async worsoft_get_report_tasks(args) {
204
+ assertRequired(args, ["startDate", "endDate"]);
205
+ assertDate(args.startDate, "startDate");
206
+ assertDate(args.endDate, "endDate");
207
+ return unwrap(await apiRequest("GET", "/hr/HrReportDaily/getTaskByExecuteUserId", {
208
+ query: { startTime: args.startDate, endTime: args.endDate }
209
+ }));
210
+ },
211
+
212
+ async worsoft_submit_report(args) {
213
+ const payload = await buildSubmitPayload(args);
214
+ if (args?.dryRun) {
215
+ return { dryRun: true, method: payload.id ? "PUT" : "POST", path: "/hr/HrReportDaily", payload };
216
+ }
217
+ const method = payload.id ? "PUT" : "POST";
218
+ return unwrap(await apiRequest(method, "/hr/HrReportDaily", { body: payload }));
219
+ },
220
+
221
+ async worsoft_month_summary_source(args) {
222
+ assertRequired(args, ["month"]);
223
+ if (!/^\d{4}-\d{2}$/.test(args.month)) {
224
+ throw new Error("month must be yyyy-MM.");
225
+ }
226
+ const startDate = `${args.month}-01`;
227
+ const endDate = lastDateOfMonth(args.month);
228
+ const response = unwrap(await apiRequest("GET", "/hr/HrReportDaily/page", {
229
+ query: buildReportQuery({
230
+ reportType: args.includeWeeklyAndMonthly ? "all" : "daily",
231
+ scope: args.scope || "submitted",
232
+ startDate,
233
+ endDate,
234
+ current: 1,
235
+ size: 100
236
+ })
237
+ }));
238
+ const records = Array.isArray(response?.records) ? response.records : [];
239
+ const details = [];
240
+ for (const record of records) {
241
+ if (record?.id != null) {
242
+ details.push(unwrap(await apiRequest("GET", `/hr/HrReportDaily/${record.id}`)));
243
+ }
244
+ }
245
+ return {
246
+ month: args.month,
247
+ startDate,
248
+ endDate,
249
+ total: response?.total ?? details.length,
250
+ records: details,
251
+ summaryHint: "Use these records to summarize completed work, risks/blockers, task progress, and next-month plan, then call worsoft_submit_report with reportType=monthly."
252
+ };
253
+ }
254
+ };
255
+
256
+ const rl = readline.createInterface({
257
+ input: process.stdin,
258
+ output: process.stdout,
259
+ terminal: false
260
+ });
261
+
262
+ rl.on("line", async (line) => {
263
+ if (!line.trim()) return;
264
+ let request;
265
+ try {
266
+ request = JSON.parse(line);
267
+ } catch (error) {
268
+ sendError(null, -32700, "Parse error", error.message);
269
+ return;
270
+ }
271
+
272
+ if (!request || request.jsonrpc !== "2.0" || typeof request.method !== "string") {
273
+ sendError(request?.id ?? null, -32600, "Invalid Request");
274
+ return;
275
+ }
276
+
277
+ try {
278
+ if (request.method === "initialize") {
279
+ sendResult(request.id, {
280
+ protocolVersion: request.params?.protocolVersion || "2024-11-05",
281
+ capabilities: { tools: {} },
282
+ serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
283
+ });
284
+ return;
285
+ }
286
+
287
+ if (request.method === "notifications/initialized") {
288
+ return;
289
+ }
290
+
291
+ if (request.method === "tools/list") {
292
+ sendResult(request.id, { tools });
293
+ return;
294
+ }
295
+
296
+ if (request.method === "tools/call") {
297
+ const name = request.params?.name;
298
+ const args = request.params?.arguments || {};
299
+ if (!handlers[name]) {
300
+ sendError(request.id, -32602, `Unknown tool: ${name}`);
301
+ return;
302
+ }
303
+ const result = await handlers[name](args);
304
+ sendResult(request.id, {
305
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
306
+ });
307
+ return;
308
+ }
309
+
310
+ sendError(request.id, -32601, `Method not found: ${request.method}`);
311
+ } catch (error) {
312
+ sendResult(request.id, {
313
+ isError: true,
314
+ content: [{ type: "text", text: errorToText(error) }]
315
+ });
316
+ }
317
+ });
318
+
319
+ function loadDotEnv() {
320
+ const envPath = resolve(process.cwd(), ".env");
321
+ if (!existsSync(envPath)) return;
322
+ const content = readFileSync(envPath, "utf8");
323
+ for (const rawLine of content.split(/\r?\n/)) {
324
+ const line = rawLine.trim();
325
+ if (!line || line.startsWith("#")) continue;
326
+ const index = line.indexOf("=");
327
+ if (index <= 0) continue;
328
+ const key = line.slice(0, index).trim();
329
+ const value = line.slice(index + 1).trim().replace(/^['"]|['"]$/g, "");
330
+ if (!process.env[key]) {
331
+ process.env[key] = value;
332
+ }
333
+ }
334
+ }
335
+
336
+ function env(name, fallback) {
337
+ return process.env[name] == null || process.env[name] === "" ? fallback : process.env[name];
338
+ }
339
+
340
+ function trimTrailingSlash(value) {
341
+ return String(value || "").replace(/\/+$/, "");
342
+ }
343
+
344
+ function sendResult(id, result) {
345
+ process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id, result })}\n`);
346
+ }
347
+
348
+ function sendError(id, code, message, data) {
349
+ process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id, error: { code, message, data } })}\n`);
350
+ }
351
+
352
+ function errorToText(error) {
353
+ if (error?.responseBody) {
354
+ return `${error.message}\n${JSON.stringify(error.responseBody, null, 2)}`;
355
+ }
356
+ return error?.stack || error?.message || String(error);
357
+ }
358
+
359
+ async function login(args = {}) {
360
+ if (args.baseUrl) {
361
+ config.baseUrl = trimTrailingSlash(args.baseUrl);
362
+ }
363
+ const username = args.username || config.username;
364
+ const password = args.password || config.password;
365
+ if (!username || !password) {
366
+ throw new Error("Missing Worsoft credentials. Set WORSOFT_USERNAME and WORSOFT_PASSWORD or pass username/password to worsoft_login.");
367
+ }
368
+
369
+ const params = new URLSearchParams({ grant_type: "password" });
370
+ const body = new URLSearchParams({ username, password });
371
+ const response = await rawRequest("POST", `/auth/oauth/token?${params}`, {
372
+ headers: {
373
+ Authorization: config.basicAuth,
374
+ platform: config.platform,
375
+ "content-type": "application/x-www-form-urlencoded"
376
+ },
377
+ body: body.toString(),
378
+ skipAuth: true
379
+ });
380
+
381
+ const data = response.body;
382
+ const accessToken = data?.access_token || data?.data?.access_token;
383
+ if (!accessToken) {
384
+ const err = new Error("Login response did not contain access_token.");
385
+ err.responseBody = data;
386
+ throw err;
387
+ }
388
+
389
+ const expiresIn = Number(data.expires_in || data?.data?.expires_in || 3600);
390
+ tokenState = {
391
+ accessToken,
392
+ refreshToken: data.refresh_token || data?.data?.refresh_token || "",
393
+ expiresAt: Date.now() + Math.max(30, expiresIn - 60) * 1000
394
+ };
395
+ return data;
396
+ }
397
+
398
+ async function ensureLogin() {
399
+ if (tokenState.accessToken && Date.now() < tokenState.expiresAt) {
400
+ return;
401
+ }
402
+ await login();
403
+ }
404
+
405
+ async function apiRequest(method, path, options = {}) {
406
+ await ensureLogin();
407
+ return rawRequest(method, path, options);
408
+ }
409
+
410
+ async function rawRequest(method, path, options = {}) {
411
+ const url = new URL(config.baseUrl + path);
412
+ if (options.query) {
413
+ appendQuery(url.searchParams, options.query);
414
+ }
415
+
416
+ const controller = new AbortController();
417
+ const timeout = setTimeout(() => controller.abort(), config.timeoutMs);
418
+ const headers = {
419
+ platform: config.platform,
420
+ VERSION: config.version,
421
+ ...(options.headers || {})
422
+ };
423
+ if (!options.skipAuth) {
424
+ headers.Authorization = `Bearer ${tokenState.accessToken}`;
425
+ }
426
+
427
+ let body = options.body;
428
+ if (body != null && typeof body !== "string") {
429
+ headers["content-type"] = headers["content-type"] || "application/json";
430
+ body = JSON.stringify(body);
431
+ }
432
+
433
+ try {
434
+ const response = await fetch(url, {
435
+ method,
436
+ headers,
437
+ body,
438
+ signal: controller.signal
439
+ });
440
+ const responseBody = await parseResponseBody(response);
441
+ if (!response.ok || responseBody?.code === 1) {
442
+ const error = new Error(`Worsoft API ${method} ${url.pathname} failed with HTTP ${response.status}.`);
443
+ error.responseBody = responseBody;
444
+ throw error;
445
+ }
446
+ return { status: response.status, body: responseBody };
447
+ } finally {
448
+ clearTimeout(timeout);
449
+ }
450
+ }
451
+
452
+ async function parseResponseBody(response) {
453
+ const text = await response.text();
454
+ if (!text) return null;
455
+ try {
456
+ return JSON.parse(text);
457
+ } catch {
458
+ return text;
459
+ }
460
+ }
461
+
462
+ function unwrap(response) {
463
+ const body = response.body;
464
+ if (body && typeof body === "object" && Object.prototype.hasOwnProperty.call(body, "data")) {
465
+ return body.data;
466
+ }
467
+ return body;
468
+ }
469
+
470
+ function appendQuery(searchParams, query) {
471
+ for (const [key, value] of Object.entries(query)) {
472
+ if (value == null || value === "") continue;
473
+ if (Array.isArray(value)) {
474
+ for (const item of value) searchParams.append(key, String(item));
475
+ } else if (typeof value === "object") {
476
+ searchParams.append(key, JSON.stringify(value));
477
+ } else {
478
+ searchParams.append(key, String(value));
479
+ }
480
+ }
481
+ }
482
+
483
+ function buildReportQuery(args = {}) {
484
+ const current = args.current || 1;
485
+ const size = args.size || config.defaultPageSize;
486
+ const reportType = args.reportType || "daily";
487
+ const scope = args.scope || "submitted";
488
+ const query = {
489
+ current,
490
+ size,
491
+ orderBy: "endTime:desc",
492
+ sign: scopeToSign(scope)
493
+ };
494
+
495
+ const customFilters = [];
496
+ if (args.startDate) {
497
+ assertDate(args.startDate, "startDate");
498
+ customFilters.push({ name: "startTime", filterType: 13, filterValue: [args.startDate] });
499
+ }
500
+ if (args.endDate) {
501
+ assertDate(args.endDate, "endDate");
502
+ customFilters.push({ name: "endTime", filterType: 15, filterValue: [args.endDate] });
503
+ }
504
+ if (customFilters.length > 0) {
505
+ query.conditionFormat = customFilters.map(() => "1").join("a");
506
+ query.customFilters = JSON.stringify(customFilters);
507
+ }
508
+
509
+ if (reportType !== "all") {
510
+ query["entityExt[reportingType]"] = [reportTypeToCode(reportType)];
511
+ query.filterTypes = JSON.stringify({ reportingType: 30 });
512
+ }
513
+
514
+ if (args.smartVal) {
515
+ query.smartVal = args.smartVal;
516
+ query.smartNames = ["createUser", "todayTaskConditionStatement", "tomorrowTaskConditionStatement"];
517
+ }
518
+
519
+ return query;
520
+ }
521
+
522
+ async function buildSubmitPayload(args = {}) {
523
+ assertRequired(args, ["reportType", "startDate", "endDate", "todayTaskConditionStatement"]);
524
+ assertDate(args.startDate, "startDate");
525
+ assertDate(args.endDate, "endDate");
526
+
527
+ const payload = {
528
+ id: args.id,
529
+ reportingType: reportTypeToCode(args.reportType),
530
+ startTime: args.startDate,
531
+ endTime: args.endDate,
532
+ billDate: today(),
533
+ publicOrNot: args.publicOrNot || "0",
534
+ todayTaskConditionStatement: args.todayTaskConditionStatement,
535
+ tomorrowTaskConditionStatement: args.tomorrowTaskConditionStatement || "",
536
+ currentTaskId: args.currentTaskId || "",
537
+ nextTimeTaskId: args.nextTimeTaskId || "",
538
+ bugIds: args.bugIds || "",
539
+ hrReportDailyDetailList: normalizeRecipientUsers(args.recipientUsers || [])
540
+ };
541
+
542
+ if (args.id) {
543
+ const hasRecipientOverride = Array.isArray(args.recipientUsers);
544
+ const existing = unwrap(await apiRequest("GET", `/hr/HrReportDaily/${encodeURIComponent(String(args.id))}`));
545
+ Object.assign(payload, cleanUndefined({
546
+ createTime: existing?.createTime,
547
+ createUserId: existing?.createUserId,
548
+ createUser: existing?.createUser,
549
+ createDptId: existing?.createDptId,
550
+ createDpt: existing?.createDpt,
551
+ tenantId: existing?.tenantId
552
+ }));
553
+ if (!hasRecipientOverride) {
554
+ payload.hrReportDailyDetailList = normalizeRecipientUsers(existing?.hrReportDailyDetailList || existing?.HrReportDailyDetailList || []);
555
+ }
556
+ payload.evaluationContents = existing?.evaluationContents || [];
557
+ }
558
+
559
+ if (args.includeCurrentTasks) {
560
+ const tasks = unwrap(await apiRequest("GET", "/hr/HrReportDaily/getTaskByExecuteUserId", {
561
+ query: { startTime: args.startDate, endTime: args.endDate }
562
+ })) || [];
563
+ if (!payload.currentTaskId) {
564
+ payload.currentTaskId = tasks.map((task) => task.id).filter(Boolean).join(",");
565
+ }
566
+ const reviewerDetails = tasks
567
+ .filter((task) => task.submitUserId && task.submitUserName)
568
+ .map((task) => ({
569
+ recipientUserId: task.submitUserId,
570
+ recipientUser: task.submitUserName,
571
+ signExamine: "1",
572
+ recipientUserType: "1"
573
+ }));
574
+ payload.hrReportDailyDetailList = uniqueRecipients([
575
+ ...payload.hrReportDailyDetailList,
576
+ ...reviewerDetails
577
+ ]);
578
+ }
579
+
580
+ return payload;
581
+ }
582
+
583
+ function normalizeRecipientUsers(users) {
584
+ return uniqueRecipients(users.map((user) => ({
585
+ recipientUserId: user.recipientUserId,
586
+ recipientUser: user.recipientUser,
587
+ signExamine: user.signExamine || "1",
588
+ recipientUserType: user.recipientUserType || "0"
589
+ })).filter((user) => user.recipientUserId && user.recipientUser));
590
+ }
591
+
592
+ function uniqueRecipients(users) {
593
+ const seen = new Set();
594
+ const result = [];
595
+ for (const user of users) {
596
+ const key = `${user.recipientUserType || "0"}:${user.recipientUserId}`;
597
+ if (seen.has(key)) continue;
598
+ seen.add(key);
599
+ result.push(user);
600
+ }
601
+ return result;
602
+ }
603
+
604
+ function reportTypeToCode(type) {
605
+ const map = { daily: "0", weekly: "1", monthly: "2" };
606
+ if (!map[type]) throw new Error("reportType must be daily, weekly, or monthly.");
607
+ return map[type];
608
+ }
609
+
610
+ function scopeToSign(scope) {
611
+ const map = { related: 0, submitted: 1, received: 2 };
612
+ if (!Object.prototype.hasOwnProperty.call(map, scope)) {
613
+ throw new Error("scope must be related, submitted, or received.");
614
+ }
615
+ return map[scope];
616
+ }
617
+
618
+ function assertRequired(args, names) {
619
+ for (const name of names) {
620
+ if (args?.[name] == null || args[name] === "") {
621
+ throw new Error(`Missing required argument: ${name}`);
622
+ }
623
+ }
624
+ }
625
+
626
+ function assertDate(value, name) {
627
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(String(value))) {
628
+ throw new Error(`${name} must be yyyy-MM-dd.`);
629
+ }
630
+ }
631
+
632
+ function today() {
633
+ const date = new Date();
634
+ const year = date.getFullYear();
635
+ const month = String(date.getMonth() + 1).padStart(2, "0");
636
+ const day = String(date.getDate()).padStart(2, "0");
637
+ return `${year}-${month}-${day}`;
638
+ }
639
+
640
+ function lastDateOfMonth(month) {
641
+ const [year, monthNumber] = month.split("-").map(Number);
642
+ return new Date(Date.UTC(year, monthNumber, 0)).toISOString().slice(0, 10);
643
+ }
644
+
645
+ function cleanUndefined(object) {
646
+ return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== undefined));
647
+ }