bdip-office-assistant-cli 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +37 -2
  2. package/dist/accounts.js +30 -0
  3. package/dist/args.js +36 -0
  4. package/dist/auth-token.js +778 -0
  5. package/dist/auth.js +290 -0
  6. package/dist/blog.js +562 -0
  7. package/dist/browser-links.js +223 -0
  8. package/dist/captcha-display.js +30 -0
  9. package/dist/cli.js +5479 -0
  10. package/dist/client.js +254 -0
  11. package/dist/config.js +170 -0
  12. package/dist/createcontext.js +41 -0
  13. package/dist/crypto.js +139 -0
  14. package/dist/cube.js +1771 -0
  15. package/dist/email.js +841 -0
  16. package/dist/field-preparation.js +185 -0
  17. package/dist/fileutil.js +20 -0
  18. package/dist/hrm.js +478 -0
  19. package/dist/keepalive.js +99 -0
  20. package/dist/kq.js +280 -0
  21. package/dist/links.js +34 -0
  22. package/dist/nodemap.js +242 -0
  23. package/dist/odoc.js +199 -0
  24. package/dist/office-tools.js +138 -0
  25. package/dist/pageopen.js +29 -0
  26. package/dist/parse.js +177 -0
  27. package/dist/password-input.js +84 -0
  28. package/dist/password-login.js +208 -0
  29. package/dist/querystate.js +38 -0
  30. package/dist/render.js +21 -0
  31. package/dist/report.js +50 -0
  32. package/dist/session-worker.js +54 -0
  33. package/dist/session.js +280 -0
  34. package/dist/submissionplan.js +45 -0
  35. package/dist/submissionstate.js +81 -0
  36. package/dist/subordinate.js +458 -0
  37. package/dist/supervise.js +152 -0
  38. package/dist/todoaskidea.js +54 -0
  39. package/dist/todobatch.js +172 -0
  40. package/dist/todocc.js +128 -0
  41. package/dist/todoforward.js +66 -0
  42. package/dist/todoreject.js +215 -0
  43. package/dist/todosubmit.js +325 -0
  44. package/dist/todotransfer.js +54 -0
  45. package/dist/util.js +25 -0
  46. package/dist/wfagent.js +538 -0
  47. package/dist/wfcreate.js +102 -0
  48. package/dist/wfform.js +169 -0
  49. package/dist/wfops.js +40 -0
  50. package/dist/wfreceiver.js +43 -0
  51. package/dist/wfstart.js +558 -0
  52. package/dist/wfsummary.js +89 -0
  53. package/dist/wfupload.js +78 -0
  54. package/dist/wfuser.js +96 -0
  55. package/dist/workflow-runtime.js +151 -0
  56. package/dist/workflow.js +570 -0
  57. package/dist/workplan.js +431 -0
  58. package/package.json +28 -6
package/dist/blog.js ADDED
@@ -0,0 +1,562 @@
1
+ import { endpointFor } from './config.js';
2
+ import { createApiFromProfile } from './client.js';
3
+ import { pickByPaths, stripHtml } from './parse.js';
4
+ // ─── 微博列表 ────────────────────────────────────────────
5
+ /**
6
+ * 我的微博列表(getMyBlogList)。
7
+ * 服务端固定 20 条/页且无视 pageSize 入参(已抓包实证)。按「不认 pageSize 就跟随服务端默认」原则:
8
+ * 直接读服务端页 currentPage,天然连续不跳号;每页返回条数即服务端固定值(20)。
9
+ * 响应无 total 字段,越界页会被服务端重复回放同页内容(用 getMyBlogAll 做重复检测终止)。
10
+ */
11
+ export async function getMyBlogList(cfg, endDate, page = 1) {
12
+ const api = await createApiFromProfile(cfg);
13
+ const r = await api.get('/api/blog/base/getMyBlogList', { endDate, currentPage: page });
14
+ const all = extractBlogItems(r.json);
15
+ const items = all.filter((it) => it.id || it.content);
16
+ const serverPageSize = Number(r.json?.pageSize) || 20;
17
+ return { endDate, page, pageSize: serverPageSize, total: r.json.total ?? undefined, items, raw: r.json };
18
+ }
19
+ /**
20
+ * 拉取某日期的全部微博(去重 + 重复检测终止)。
21
+ * 服务端无 total 且越界页重复回放:逐页读,遇「新增 0 条(整页重复)」或「短页(< 每页条数)」即视为末尾。
22
+ * 供 blog check / blog reply 等需要覆盖当天全部记录的调用使用。
23
+ */
24
+ export async function getMyBlogAll(cfg, endDate, { maxPages = 10 } = {}) {
25
+ const seen = new Set();
26
+ const items = [];
27
+ for (let page = 1; page <= maxPages; page++) {
28
+ const r = await getMyBlogList(cfg, endDate, page);
29
+ let added = 0;
30
+ for (const it of r.items) {
31
+ const id = String(it.id ?? '');
32
+ if (id && seen.has(id))
33
+ continue;
34
+ if (id)
35
+ seen.add(id);
36
+ items.push(it);
37
+ added++;
38
+ }
39
+ if (added === 0)
40
+ break; // 整页重复(越界回放)→ 已到末尾
41
+ if (r.items.length < r.pageSize)
42
+ break; // 短页 → 末尾
43
+ }
44
+ return { endDate, items };
45
+ }
46
+ function extractBlogItems(resp) {
47
+ const arr = pickByPaths(resp, ['discussList', 'data.discussList', 'data', 'datas', 'data.datas', 'rows', 'list']);
48
+ if (Array.isArray(arr))
49
+ return arr.map(normalizeBlog);
50
+ if (arr && typeof arr === 'object' && !Array.isArray(arr)) {
51
+ const vals = Object.values(arr).filter((v) => v && typeof v === 'object');
52
+ if (vals.length)
53
+ return vals.map(normalizeBlog);
54
+ }
55
+ return [];
56
+ }
57
+ export function normalizeBlog(row) {
58
+ if (!row || typeof row !== 'object')
59
+ return {};
60
+ const get = (keys) => pickByPaths(row, keys);
61
+ const html = get(['ecmeEditButtons[0].testcontent']) ?? decodeBase64Html(get(['content', 'mobileContent']));
62
+ const createdate = stringify(get(['createdate']));
63
+ const createtime = stringify(get(['createtime']));
64
+ const workdate = stringify(get(['workdate', 'createdate', 'noteDate']));
65
+ return {
66
+ id: stringify(get(['id', 'blogid', 'blogId'])),
67
+ content: stripHtml(html) || stringify(get(['contentText'])),
68
+ workdate,
69
+ creator: stripHtml(get(['createrspan', 'creater'])) || stringify(get(['creator', 'createrName', 'userid', 'username'])),
70
+ createdate: [createdate || workdate, createtime].filter(Boolean).join(' '),
71
+ shareType: stringify(get(['shareType', 'sharetype'])),
72
+ // isReCalled:微博撤回状态(已抓包验证)。0=正常发布,1/2=已撤回。
73
+ isReCalled: stringify(get(['isReCalled'])),
74
+ raw: row,
75
+ };
76
+ }
77
+ /**
78
+ * 判断微博是否已撤回。isReCalled 为 1 或 2 即已撤回(0=正常发布)。
79
+ * 前端 bundle 判定:2==isReCalled||1==isReCalled → 标记为已撤回样式并显示「微博已撤回」。
80
+ */
81
+ export function isBlogRecalled(item) {
82
+ const v = item?.isReCalled;
83
+ return v === 1 || v === '1' || v === 2 || v === '2';
84
+ }
85
+ // ─── 微博首页列表(支持组织视图)─────────────────────────
86
+ /**
87
+ * 获取微博首页列表,支持按 scope 查看不同范围。
88
+ * scope 映射(groupId,抓包实证不同 scope 走不同接口):
89
+ * 全部关注/关注/下级(-1)、特别关注(-5) → getBlogHomepageList
90
+ * 未读(-2) → getMyBlogHomepageNew
91
+ * 分享给我/共享(-3) → getWeiboPageShareToMeList
92
+ */
93
+ export async function getBlogHomepageList(cfg, opts = {}) {
94
+ const api = await createApiFromProfile(cfg);
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' };
96
+ const groupId = scopeMap[opts.scope] ?? opts.scope ?? '-1';
97
+ const endDate = opts.endDate ?? todayStr();
98
+ // 默认查最近 30 天
99
+ const startDate = opts.startDate ?? offsetDate(endDate, -30);
100
+ const params = {
101
+ content: opts.content ?? '',
102
+ blogUserId: opts.blogUserId ?? '',
103
+ deptId: opts.deptId ?? '',
104
+ subId: opts.subId ?? '',
105
+ groupId,
106
+ startDate,
107
+ endDate,
108
+ currentPage: opts.page ?? 1,
109
+ pageSize: 10,
110
+ };
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);
115
+ const all = extractBlogItems(r.json);
116
+ const items = all.filter((it) => it.id || it.content);
117
+ return { scope: opts.scope ?? '全部', groupId, startDate, endDate, page: opts.page ?? 1, total: r.json.total ?? items.length, items, raw: r.json };
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
+
139
+ // ─── 微博搜索 ────────────────────────────────────────────
140
+ export async function searchMyBlog(cfg, opts = {}) {
141
+ const api = await createApiFromProfile(cfg);
142
+ const params = {
143
+ content: opts.content ?? '',
144
+ startDate: opts.startDate ?? '',
145
+ endDate: opts.endDate ?? '',
146
+ currentPage: opts.page ?? 1,
147
+ pageSize: 10,
148
+ };
149
+ const r = await api.get('/api/blog/base/getMyBlogListBySearch', params);
150
+ const all = extractBlogItems(r.json);
151
+ const items = all.filter((it) => it.id || it.content);
152
+ return { content: opts.content, startDate: opts.startDate, endDate: opts.endDate, page: opts.page ?? 1, total: r.json.total ?? items.length, items, raw: r.json };
153
+ }
154
+ // ─── 个人主页信息 ────────────────────────────────────────
155
+ export async function getBlogPersonalInfo(cfg, blogId) {
156
+ const api = await createApiFromProfile(cfg);
157
+ const r = await api.get('/api/blog/base/getBlogPersonalInfo', { blogId: String(blogId) });
158
+ const j = r.json || {};
159
+ return {
160
+ userId: stringify(j.userId),
161
+ username: stringify(j.username),
162
+ deptName: stringify(j.deptName),
163
+ deptId: stringify(j.deptId),
164
+ blogCount: stringify(j.blogCount),
165
+ isLower: stringify(j.islower) === '1',
166
+ attentionStatus: stringify(j.attentionStatus),
167
+ myAttentionCount: stringify(j.myAttentionCount),
168
+ attentionMeCount: stringify(j.attentionMeCount),
169
+ imageUrl: stringify(j.imageUrl),
170
+ raw: j,
171
+ };
172
+ }
173
+ // ─── 催办 ────────────────────────────────────────────────
174
+ /**
175
+ * 催办(提醒某人写日报)。
176
+ * 前端 SPA 实证(/spa/blog/static/index.js):onRemindClick → remind({workdate, remindId})
177
+ * → setNotice → GET /api/blog/base/sendRemindUnsubmit?remindId=<uid>&workdate=<YYYY-MM-DD>。
178
+ * ⚠ 不能用 POST,参数名也不是 userId——POST+userId 会 500(remindId 为空抛
179
+ * `For input string: ""`),且缺少 workdate。opts.remindId 兼容旧 userId 别名。
180
+ */
181
+ export async function sendRemindUnsubmit(cfg, opts = {}) {
182
+ const ep = endpointFor(cfg);
183
+ const remindId = String(opts.remindId ?? opts.userId ?? '');
184
+ const workdate = String(opts.workdate ?? '');
185
+ if (!opts.send)
186
+ return { sent: false, endpoint: { baseUrl: ep.baseUrl }, remindId, workdate };
187
+ const api = await createApiFromProfile(cfg);
188
+ const r = await api.get('/api/blog/base/sendRemindUnsubmit', { remindId, workdate });
189
+ return { sent: true, endpoint: { baseUrl: ep.baseUrl }, remindId, workdate, response: r };
190
+ }
191
+ // ─── 回复微博 ────────────────────────────────────────────
192
+ export async function addBlogReply(cfg, opts = {}) {
193
+ const ep = endpointFor(cfg);
194
+ if (!opts.send)
195
+ return { sent: false, endpoint: { baseUrl: ep.baseUrl }, blogId: opts.blogId };
196
+ const api = await createApiFromProfile(cfg);
197
+ // 抓包实证(微博抓包2.har):bediscussantId(被评论人 userid,大写 I) + discussId(微博 id) +
198
+ // workdate + commentType=0 + comefrom=0 + content(HTML)。缺 bediscussantId 报“无被评论人”。
199
+ const body = {
200
+ content: textToBlogHtml(opts.content ?? ''),
201
+ bediscussantId: String(opts.discussantId ?? ''),
202
+ workdate: opts.workdate ?? '',
203
+ commentType: '0',
204
+ comefrom: '0',
205
+ discussId: String(opts.blogId ?? ''),
206
+ };
207
+ const r = await api.post('/api/blog/base/addBlogReply', body);
208
+ return { sent: true, endpoint: { baseUrl: ep.baseUrl }, blogId: opts.blogId, response: r };
209
+ }
210
+ // ─── 点赞/取消点赞 ────────────────────────────────────────
211
+ // 修复:SPA bundle(/spa/blog/static/index.js)实证 addOrDeleteZan 请求体为
212
+ // { workdate, userId, optType(1=赞 / 0=取消赞), discussId(微博id) }。
213
+ // 旧实现只发 { blogId },后端按 discussId 匹配不到 → 静默 no-op(status:1 但 isZan 不变)。
214
+ export async function addOrDeleteZan(cfg, opts = {}) {
215
+ const ep = endpointFor(cfg);
216
+ if (!opts.send)
217
+ return { sent: false, endpoint: { baseUrl: ep.baseUrl }, blogId: opts.blogId, optType: opts.optType ?? 1, workdate: opts.workdate, userId: opts.userId };
218
+ const api = await createApiFromProfile(cfg);
219
+ const optType = String(opts.optType ?? 1); // 1=赞 0=取消赞
220
+ const body = {
221
+ workdate: opts.workdate ?? '',
222
+ userId: String(opts.userId ?? ''),
223
+ optType,
224
+ discussId: String(opts.blogId ?? ''),
225
+ };
226
+ const r = await api.post('/api/blog/base/addOrDeleteZan', body);
227
+ const j = r.json || {};
228
+ const zan = j.blogZanBean || {};
229
+ const isZan = zan.isZan === '1' || zan.isZan === 1 || zan.isZan === true
230
+ || (Array.isArray(zan.blogZanPoList) && zan.blogZanPoList.some((z) => String(z.zanHrmid ?? '') === String(opts.userId)));
231
+ return {
232
+ sent: true, endpoint: { baseUrl: ep.baseUrl }, blogId: opts.blogId, optType, workdate: opts.workdate, userId: opts.userId,
233
+ response: r, isZan,
234
+ ok: j.status === '1' || j.status === 1 || j.api_status === '1' || j.api_status === true,
235
+ };
236
+ }
237
+ // ─── 微博发布 ────────────────────────────────────────────
238
+ export async function createBlog(cfg, opts) {
239
+ const ep = endpointFor(cfg);
240
+ const workdate = opts.workdate ?? '';
241
+ const html = textToBlogHtml(opts.content);
242
+ const body = {
243
+ appItemId: opts.discussId ? '4' : '1', comefrom: '0', content: html, workdate,
244
+ shareType: opts.shareType ?? '1', shareValue: '', atChooseId: '', hideContent: '0', dbCheck: '0',
245
+ };
246
+ let mode;
247
+ if (opts.discussId) {
248
+ body.discussId = opts.discussId;
249
+ mode = 'update';
250
+ }
251
+ else if (workdate) {
252
+ mode = 'create-replenish';
253
+ }
254
+ else {
255
+ body.fromTag = '2';
256
+ mode = 'create-today';
257
+ }
258
+ if (!opts.send)
259
+ return { sent: false, endpoint: { baseUrl: ep.baseUrl }, mode, body };
260
+ const api = await createApiFromProfile(cfg);
261
+ const r = await api.post('/api/blog/base/saveOrUpdateBlog', body);
262
+ return { sent: true, endpoint: { baseUrl: ep.baseUrl }, mode, body, response: r };
263
+ }
264
+ export function textToBlogHtml(text) {
265
+ return text.split(/\r?\n/).map((l) => `<p>${escapeHtml(l)}</p>`).join('');
266
+ }
267
+ // ─── 微博工作指数 + 心情指数 ──────────────────────────────
268
+ export async function getBlogWorkIndex(cfg) {
269
+ const api = await createApiFromProfile(cfg);
270
+ const r = await api.get('/api/blog/base/getBlogWorkIndex');
271
+ const j = r.json || {};
272
+ return {
273
+ schedule: { name: j.schedule?.name, title: j.schedule?.scheduleTitle, index: j.schedule?.scheduleIndex },
274
+ mood: { name: j.mood?.name, title: j.mood?.moodIndexTitle, index: j.mood?.moodIndex },
275
+ work: { name: j.work?.name, title: j.work?.workIndexTitle, index: j.work?.workIndex },
276
+ raw: j,
277
+ };
278
+ }
279
+ // ─── 微博菜单计数 ────────────────────────────────────────
280
+ export async function getBlogMenuCountdata(cfg) {
281
+ const api = await createApiFromProfile(cfg);
282
+ const r = await api.get('/api/blog/base/getBlogMenuCountdata');
283
+ const j = r.json || {};
284
+ return {
285
+ counts: j.countData ?? {},
286
+ templateId: j.templateId,
287
+ templateType: j.templateType,
288
+ raw: j,
289
+ };
290
+ }
291
+ // ─── 微博访客列表 ────────────────────────────────────────
292
+ export async function getVisitorList(cfg, opts = {}) {
293
+ const api = await createApiFromProfile(cfg);
294
+ const recordType = opts.recordType ?? 'visit';
295
+ const page = opts.page ?? 1;
296
+ const pageSize = opts.pageSize ?? 5;
297
+ const r = await api.get('/api/blog/base/getVisitorList', { recordType, currentPage: page, pageSize });
298
+ const j = r.json || {};
299
+ return {
300
+ recordType,
301
+ page: j.currentPage ?? page,
302
+ pageSize: j.pageSize ?? pageSize,
303
+ total: j.totalSize ?? 0,
304
+ items: (j.list || []).map((it) => ({
305
+ id: stringify(it.id), name: stringify(it.name),
306
+ visitor: stringify(it.visitor), visitorName: stringify(it.visitorName),
307
+ visitDate: stringify(it.visitDate), visitCount: stringify(it.visitCount),
308
+ raw: it,
309
+ })),
310
+ raw: j,
311
+ };
312
+ }
313
+ // ─── 微博草稿/工作笔记 ────────────────────────────────────
314
+ export async function getBlogNote(cfg, workdate) {
315
+ const api = await createApiFromProfile(cfg);
316
+ const params = {};
317
+ if (workdate)
318
+ params.workdate = workdate;
319
+ const r = await api.get('/api/blog/base/getBlogNote', params);
320
+ const j = r.json || {};
321
+ return {
322
+ id: j.id,
323
+ content: j.content ? stripHtml(decodeBase64Html(j.content)) : '',
324
+ contentHtml: j.content ? decodeBase64Html(j.content) : '',
325
+ updateDate: j.updateDate,
326
+ workdate,
327
+ raw: j,
328
+ };
329
+ }
330
+ // ─── 微博草稿(便签)写入 ────────────────────────────────
331
+ // 修复 #4:BDIP5 微博无独立草稿接口,所谓"草稿/工作笔记"走 saveBlogNote(POST /api/blog/base/saveBlogNote)。
332
+ // 注:排查报告"saveOrUpdateBlog+dbCheck=1 存草稿"为误判——dbCheck 是保密勾选项非草稿标志,
333
+ // 内容追加是因未带 discussId 走了新建模式。草稿写入应走本接口。
334
+ export async function saveBlogNote(cfg, opts = {}) {
335
+ const ep = endpointFor(cfg);
336
+ if (!opts.send)
337
+ return { sent: false, endpoint: { baseUrl: ep.baseUrl }, workdate: opts.workdate ?? todayStr() };
338
+ const api = await createApiFromProfile(cfg);
339
+ const workdate = opts.workdate ?? todayStr();
340
+ const html = textToBlogHtml(opts.content ?? '');
341
+ const r = await api.post('/api/blog/base/saveBlogNote', { workdate, content: html });
342
+ const j = r.json || {};
343
+ return { sent: true, endpoint: { baseUrl: ep.baseUrl }, workdate, response: r, ok: j.status === '1' || j.status === 1 || j.api_status === '1' };
344
+ }
345
+ // ─── 微博撤回 / 恢复 ─────────────────────────────────────
346
+ // 修复 #4:BDIP5 微博不可删除(产品无 deleteBlog 接口),收回已发布微博的唯一手段是撤回。
347
+ // POST /api/blog/myblog/recallblog,body: blogId + workdate + opType(1=撤回, 0=恢复)。
348
+ export async function recallBlog(cfg, opts = {}) {
349
+ const ep = endpointFor(cfg);
350
+ if (!opts.send)
351
+ return { sent: false, endpoint: { baseUrl: ep.baseUrl }, blogId: opts.blogId, opType: opts.opType ?? 1 };
352
+ const api = await createApiFromProfile(cfg);
353
+ const opType = String(opts.opType ?? 1); // 1=撤回 0=恢复
354
+ const r = await api.post('/api/blog/myblog/recallblog', {
355
+ blogId: String(opts.blogId ?? ''),
356
+ workdate: opts.workdate ?? '',
357
+ opType,
358
+ });
359
+ const j = r.json || {};
360
+ // 修复 #1:实际 API 响应为 {blogDiscussVo:{...isReCalled:2}}(无 api_status/status 字段),
361
+ // 旧判断全不匹配导致「实际撤回成功但误报失败」。成功以 blogDiscussVo.isReCalled(1/2) 或 delStatus 为准。
362
+ const jr = j.blogDiscussVo ?? j;
363
+ const isRecalled = jr?.isReCalled === 1 || jr?.isReCalled === '1' || jr?.isReCalled === 2 || jr?.isReCalled === '2';
364
+ const ok = isRecalled || jr?.delStatus === '1' || jr?.delStatus === 1
365
+ || j.api_status === '1' || j.api_status === true || j.status === '1' || j.status === 1;
366
+ return { sent: true, endpoint: { baseUrl: ep.baseUrl }, blogId: opts.blogId, opType, response: r, ok };
367
+ }
368
+ // ─── 删除微博评论/回复 ───────────────────────────────────
369
+ // 修复 #4 补充:BDIP5 微博正文不可删,但评论可删。POST /api/blog/base/deleteBlogReplay。
370
+ // body: replyId + workdate + discussId。
371
+ export async function deleteBlogReply(cfg, opts = {}) {
372
+ const ep = endpointFor(cfg);
373
+ if (!opts.send)
374
+ return { sent: false, endpoint: { baseUrl: ep.baseUrl }, replyId: opts.replyId };
375
+ const api = await createApiFromProfile(cfg);
376
+ const r = await api.post('/api/blog/base/deleteBlogReplay', {
377
+ replyId: String(opts.replyId ?? ''),
378
+ workdate: opts.workdate ?? '',
379
+ discussId: String(opts.discussId ?? ''),
380
+ });
381
+ const j = r.json || {};
382
+ return { sent: true, endpoint: { baseUrl: ep.baseUrl }, replyId: opts.replyId, response: r, ok: j.status === '1' || j.status === 1 || j.delStatus === '1' };
383
+ }
384
+ // ─── 微博当前用户信息 ────────────────────────────────────
385
+ export async function getCurrentUserInfo(cfg) {
386
+ const api = await createApiFromProfile(cfg);
387
+ const r = await api.get('/api/blog/base/getCurrentUserInfo');
388
+ const j = r.json || {};
389
+ return {
390
+ id: j.currentUser?.id,
391
+ lastname: j.currentUser?.lastname,
392
+ departmentName: j.currentUser?.departmentName,
393
+ jobtitle: j.currentUser?.jobtitle,
394
+ imageUrl: j.currentUser?.imageUrl,
395
+ todo: j.todo,
396
+ raw: j,
397
+ };
398
+ }
399
+ // ─── 微博消息提醒 ────────────────────────────────────────
400
+ export async function getMsgRemindList(cfg) {
401
+ const api = await createApiFromProfile(cfg);
402
+ const r = await api.get('/api/blog/base/getMsgRemindList');
403
+ const j = r.json || {};
404
+ return {
405
+ items: (j.remindList || []).map((it) => ({
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),
413
+ // 关注申请专用字段(抓包实证):remindid=申请人 userid、relatedid=当前登录者、remindType=1
414
+ remindid: stringify(it.remindid),
415
+ relatedid: stringify(it.relatedid),
416
+ remindType: stringify(it.remindType ?? it.remindtype),
417
+ raw: it,
418
+ })),
419
+ raw: j,
420
+ };
421
+ }
422
+ // ─── 微博关注(attentionOpt + dealMsgRemind)────────────────
423
+ /**
424
+ * 发起/取消关注(抓包实证:POST /api/blog/base/attentionOpt)。
425
+ * body: attentionStatus=3(申请关注)/4(取消?) & islower=0 & userId=<目标userid> & type=(必须为空串)。
426
+ * 响应 attnInfo.attentionStatus=4 表示申请已发出,等待对方同意;1=已是好友(互关)。
427
+ * ⚠ 此接口走 cookie 通道(token 通道实测 no-op),cookie 过期时报 errorCode 002。
428
+ */
429
+ export async function attentionOpt(cfg, opts = {}) {
430
+ const ep = endpointFor(cfg);
431
+ if (!opts.send)
432
+ return { sent: false, endpoint: { baseUrl: ep.baseUrl }, userId: opts.userId, attentionStatus: opts.attentionStatus ?? 3 };
433
+ const base = ep.baseUrl.replace(/\/+$/, '');
434
+ const r = await fetch(`${base}/api/blog/base/attentionOpt`, {
435
+ method: 'POST',
436
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', Cookie: ep.cookie, 'X-Requested-With': 'XMLHttpRequest', Referer: `${base}/spa/blog/static/index.html` },
437
+ body: new URLSearchParams({
438
+ attentionStatus: String(opts.attentionStatus ?? 3),
439
+ islower: '0',
440
+ userId: String(opts.userId ?? ''),
441
+ type: '', // 必须为空串,传 '1' 会静默 no-op(抓包实证)
442
+ }).toString(),
443
+ });
444
+ let j = null;
445
+ try { j = await r.json(); } catch { j = null; }
446
+ const attnStatus = j?.attnInfo?.attentionStatus ?? j?.attentionStatus;
447
+ const ok = attnStatus === '4' || attnStatus === 4 || j?.status === '1' || j?.status === 1 || j?.api_status === true;
448
+ return { sent: true, endpoint: { baseUrl: ep.baseUrl }, userId: opts.userId, attentionStatus: opts.attentionStatus ?? 3, attnStatus, raw: j, ok };
449
+ }
450
+ /**
451
+ * 处理关注申请(同意/忽略)。抓包实证:POST /api/blog/base/dealMsgRemind。
452
+ * body: id=<提醒记录id> & relatedId=<申请人userid> & opt=1(同意)/0(忽略)。
453
+ * 提醒记录 id 从 getMsgRemindList 获取(remindid=申请人、remindType=1=关注申请)。
454
+ * ⚠ 走 cookie 通道。返回 {"status":"1"} 表示处理成功。
455
+ */
456
+ export async function dealMsgRemind(cfg, opts = {}) {
457
+ const ep = endpointFor(cfg);
458
+ if (!opts.send)
459
+ return { sent: false, endpoint: { baseUrl: ep.baseUrl }, id: opts.id, relatedId: opts.relatedId, opt: opts.opt ?? 1 };
460
+ const base = ep.baseUrl.replace(/\/+$/, '');
461
+ const r = await fetch(`${base}/api/blog/base/dealMsgRemind`, {
462
+ method: 'POST',
463
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', Cookie: ep.cookie, 'X-Requested-With': 'XMLHttpRequest', Referer: `${base}/spa/blog/static/index.html` },
464
+ body: new URLSearchParams({
465
+ id: String(opts.id ?? ''),
466
+ relatedId: String(opts.relatedId ?? ''),
467
+ opt: String(opts.opt ?? 1),
468
+ }).toString(),
469
+ });
470
+ let j = null;
471
+ try { j = await r.json(); } catch { j = null; }
472
+ return { sent: true, endpoint: { baseUrl: ep.baseUrl }, id: opts.id, relatedId: opts.relatedId, opt: opts.opt ?? 1, raw: j, ok: j?.status === '1' || j?.status === 1 };
473
+ }
474
+ // ─── 微博默认模板 ────────────────────────────────────────
475
+ export async function getDefaultTemplate(cfg) {
476
+ const api = await createApiFromProfile(cfg);
477
+ const r = await api.get('/api/blog/base/getDefaultTemplate');
478
+ const j = r.json || {};
479
+ return {
480
+ template: j.defaultTemplate ?? {},
481
+ showTmButton: j.showTmButton,
482
+ isenableEcme: j.isenableecme,
483
+ raw: j,
484
+ };
485
+ }
486
+ // ─── 微博可用应用列表 ────────────────────────────────────
487
+ export async function getBlogAppList(cfg) {
488
+ const api = await createApiFromProfile(cfg);
489
+ const r = await api.get('/api/blog/base/getBlogAppList');
490
+ const j = r.json || {};
491
+ return {
492
+ appList: (j.appList || []).map((g) => ({
493
+ name: g.name,
494
+ items: g.items || [],
495
+ secretLevel: g.secretLevel,
496
+ })),
497
+ isBlogNote: j.isBlogNote,
498
+ raw: j,
499
+ };
500
+ }
501
+ // ─── @ 提及用户列表 ──────────────────────────────────────
502
+ export async function getAtAllList(cfg, opts = {}) {
503
+ const api = await createApiFromProfile(cfg);
504
+ const pageSize = opts.pageSize ?? 20;
505
+ const pageIndex = opts.pageIndex ?? 1;
506
+ const name = opts.name ?? '';
507
+ const r = await api.get('/api/blog/at/getAtAllList', { pageSize, pageIndex, name });
508
+ const j = r.json || {};
509
+ return {
510
+ pageIndex,
511
+ pageSize,
512
+ noMore: j.noMore,
513
+ items: (j.operators || []).map((it) => ({
514
+ uid: it.uid,
515
+ name: it.data,
516
+ py: it.datapy,
517
+ })),
518
+ raw: j,
519
+ };
520
+ }
521
+ // ─── 微博搜索条件字段 ─────────────────────────────────────
522
+ export async function getBlogPageSearchCondition(cfg) {
523
+ const api = await createApiFromProfile(cfg);
524
+ const r = await api.get('/api/blog/backstageshare/getBlogPageSearchCondition');
525
+ const j = r.json || {};
526
+ const fields = [];
527
+ for (const grp of (j.conditioninfo || [])) {
528
+ for (const item of (grp.items || [])) {
529
+ fields.push({
530
+ group: grp.title || '',
531
+ label: item.label || '',
532
+ domkey: (item.domkey || []).join(', '),
533
+ type: item.conditionType || '',
534
+ });
535
+ }
536
+ }
537
+ return { fields, raw: j };
538
+ }
539
+ // ─── 工具函数 ────────────────────────────────────────────
540
+ function decodeBase64Html(v) {
541
+ if (typeof v !== 'string' || !v)
542
+ return undefined;
543
+ try {
544
+ const txt = Buffer.from(v.replace(/\s+/g, ''), 'base64').toString('utf8');
545
+ return txt || undefined;
546
+ }
547
+ catch {
548
+ return undefined;
549
+ }
550
+ }
551
+ function escapeHtml(s) { return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;'); }
552
+ function stringify(v) { if (v === undefined || v === null || v === '')
553
+ return undefined; return String(v); }
554
+ function todayStr() {
555
+ const d = new Date();
556
+ return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
557
+ }
558
+ function offsetDate(dateStr, days) {
559
+ const d = new Date(dateStr);
560
+ d.setDate(d.getDate() + days);
561
+ return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
562
+ }