weaver-e9-assistant-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/args.js +36 -0
- package/dist/auth-token.js +690 -0
- package/dist/auth.js +187 -0
- package/dist/blog.js +469 -0
- package/dist/cli.js +4041 -0
- package/dist/client.js +199 -0
- package/dist/config.js +124 -0
- package/dist/createcontext.js +41 -0
- package/dist/crypto.js +139 -0
- package/dist/cube.js +1633 -0
- package/dist/email.js +838 -0
- package/dist/fileutil.js +20 -0
- package/dist/kq.js +204 -0
- package/dist/links.js +34 -0
- package/dist/odoc.js +194 -0
- package/dist/pageopen.js +29 -0
- package/dist/parse.js +159 -0
- package/dist/querystate.js +38 -0
- package/dist/render.js +21 -0
- package/dist/report.js +50 -0
- package/dist/submissionplan.js +43 -0
- package/dist/submissionstate.js +81 -0
- package/dist/todoaskidea.js +54 -0
- package/dist/todobatch.js +154 -0
- package/dist/todocc.js +128 -0
- package/dist/todoforward.js +66 -0
- package/dist/todoreject.js +215 -0
- package/dist/todosubmit.js +277 -0
- package/dist/todotransfer.js +54 -0
- package/dist/util.js +25 -0
- package/dist/wfcreate.js +102 -0
- package/dist/wfform.js +194 -0
- package/dist/wfops.js +40 -0
- package/dist/wfreceiver.js +43 -0
- package/dist/wfstart.js +717 -0
- package/dist/wfsummary.js +89 -0
- package/dist/wfupload.js +78 -0
- package/dist/wfuser.js +60 -0
- package/dist/workflow.js +461 -0
- package/dist/workplan.js +394 -0
- package/package.json +25 -0
package/dist/auth.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import readline from 'node:readline';
|
|
3
|
+
import { endpointFor, profileNameFromBaseUrl } from './config.js';
|
|
4
|
+
import { AuthTokenSession, ensurePlaywright, canReach } from './auth-token.js';
|
|
5
|
+
import { createApiFromProfile } from './client.js';
|
|
6
|
+
/** 登录前隐私确认(硬门槛):未持久化同意且未带 --consent 时拒绝执行登录/鉴权。
|
|
7
|
+
* 由 SKILL.md 行为规范保证:AI 必须先向用户展示选择框确认「信息会经过大模型处理」,
|
|
8
|
+
* 用户明确同意后才带 --consent 调用本命令;本函数作为 CLI 兜底,防 AI 跳过询问直接登录。 */
|
|
9
|
+
function ensureLlmConsent(cfg, args) {
|
|
10
|
+
const persisted = cfg.llmConsent === true || String(cfg.llmConsent).toLowerCase() === 'true';
|
|
11
|
+
if (persisted)
|
|
12
|
+
return;
|
|
13
|
+
if (args?.flags?.consent)
|
|
14
|
+
return;
|
|
15
|
+
console.error('⚠ 隐私确认:你发送或获取的信息(待办/台账/邮件/日报等)会经过大模型处理。是否同意?');
|
|
16
|
+
console.error(' 同意本次登录:在命令后加 --consent 重试;');
|
|
17
|
+
console.error(' 或一次性同意(写入配置,后续登录不再询问):e9 config set llmConsent true');
|
|
18
|
+
console.error(' 不同意则中止登录,不执行任何鉴权操作。');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 申请/续期当前环境的 aiAgentToken(单一端点)。
|
|
23
|
+
* 带 preflight:① baseUrl 可达(避免错地址白耗浏览器);② playwright 就位(缺则联网自动装模块+内核);
|
|
24
|
+
* ③ 开浏览器登录拿 token(开不出窗口由 loginForCookie 抛清晰错并引导 cookie-login)。
|
|
25
|
+
* 登录前需隐私确认:--consent 或 config llmConsent=true,见 ensureLlmConsent。
|
|
26
|
+
*/
|
|
27
|
+
export async function cmdTokenLogin(cfg, args) {
|
|
28
|
+
ensureLlmConsent(cfg, args);
|
|
29
|
+
const ep = endpointFor(cfg);
|
|
30
|
+
if (!ep.baseUrl || ep.baseUrl.replace(/\/+$/, '') === 'http://localhost:8080') {
|
|
31
|
+
console.error('✗ baseUrl 未配置,先跑 e9 config set baseUrl <你的E9地址>');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const profileName = profileNameFromBaseUrl(ep.baseUrl);
|
|
35
|
+
// ── preflight ①:baseUrl 可达 ──
|
|
36
|
+
console.log(`\n[1/3] 探测 baseUrl 可达:${ep.baseUrl}`);
|
|
37
|
+
const reachable = await canReach(ep.baseUrl, 8000);
|
|
38
|
+
if (!reachable) {
|
|
39
|
+
console.error(`✗ 地址不可达:${ep.baseUrl}\n 请确认 baseUrl 正确(端口/协议/网络)。曾出现过 :9000 写错白耗几轮。\n 更正:e9 config set baseUrl <正确地址>,再重试。`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
console.log('✔ 地址可达');
|
|
43
|
+
// ── preflight ②:playwright 就位(缺则联网自动装模块+内核;装不上会清晰报错并引导 cookie-login)──
|
|
44
|
+
console.log('[2/3] 检查 playwright ...');
|
|
45
|
+
try {
|
|
46
|
+
await ensurePlaywright();
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
console.error(`✗ ${e.message}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
console.log('✔ playwright 就位');
|
|
53
|
+
// ── 第3步:开浏览器登录拿 token ──
|
|
54
|
+
console.log(`[3/3] 为环境 "${profileName}" (${cfg.username || '?'}@${ep.baseUrl}) 开浏览器登录拿 aiAgentToken ...`);
|
|
55
|
+
if (!cfg.username || !cfg.encPassword) {
|
|
56
|
+
console.log('⚠ 未配置 username/encPassword,将启动有头浏览器手动登录(输账密并点「确认授权」)');
|
|
57
|
+
console.log(' (在 .e9/config.json 配置后可 autoFill 自动填)');
|
|
58
|
+
}
|
|
59
|
+
const session = AuthTokenSession.fromProfile(cfg);
|
|
60
|
+
const token = await session.acquireToken('first');
|
|
61
|
+
if (token)
|
|
62
|
+
console.log('✔ aiAgentToken 申请成功');
|
|
63
|
+
else
|
|
64
|
+
console.log('⚠ 未拿到 token(已切 cookie 鉴权,见上方诊断)。跑 `e9 todo list` 验证,或 `e9 auth cookie-login` 手动导入。');
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 手动导入 cookie 走 cookie 鉴权(无需浏览器,给无桌面/无 playwright 机器的后路)。
|
|
68
|
+
* 接受 --cookie <串> / --file <路径>,缺省交互式提示粘贴。
|
|
69
|
+
*/
|
|
70
|
+
export async function cmdCookieLogin(cfg, args) {
|
|
71
|
+
ensureLlmConsent(cfg, args);
|
|
72
|
+
const ep = endpointFor(cfg);
|
|
73
|
+
if (!ep.baseUrl || ep.baseUrl.replace(/\/+$/, '') === 'http://localhost:8080') {
|
|
74
|
+
console.error('✗ baseUrl 未配置,先跑 e9 config set baseUrl <你的E9地址>');
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
let cookie = '';
|
|
78
|
+
if (args?.flags && typeof args.flags.cookie === 'string' && args.flags.cookie)
|
|
79
|
+
cookie = args.flags.cookie;
|
|
80
|
+
else if (args?.flags?.file) {
|
|
81
|
+
try {
|
|
82
|
+
cookie = fs.readFileSync(args.flags.file, 'utf8').trim();
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
console.error(`✗ 读文件失败:${e.message}`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!cookie) {
|
|
90
|
+
console.log('请粘贴整条 Cookie(含 loginidweaver=...),回车结束:');
|
|
91
|
+
console.log(' 获取:浏览器登录 OA → F12 → Network → 任一请求 → Request Headers → Cookie 整行复制');
|
|
92
|
+
cookie = await readCookieLine();
|
|
93
|
+
}
|
|
94
|
+
cookie = cookie.trim();
|
|
95
|
+
if (!/loginidweaver=/.test(cookie)) {
|
|
96
|
+
console.error('✗ cookie 中未找到 loginidweaver=,请确认复制的是登录后的整条 Cookie(未登录态没有它)。');
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
const session = AuthTokenSession.fromProfile(cfg);
|
|
100
|
+
session.saveCookieToProfile(cookie); // 写回 config.json 的 cookie 字段(含默认 tabs 兜底)
|
|
101
|
+
session.data = { token: '', expireTime: '', expireSeconds: 0, cookieSavedAt: Date.now(), noToken: true };
|
|
102
|
+
session.save(); // 标 noToken:运行时走 cookie,不弹浏览器
|
|
103
|
+
console.log(`✔ 已导入 cookie 鉴权(${cookie.length} 字符),写入 ${cfg._configPath ?? '~/.e9/config.json'}`);
|
|
104
|
+
console.log(' 运行 `e9 auth status` 查看模式,`e9 todo list` 验证可用。');
|
|
105
|
+
}
|
|
106
|
+
/** 读 stdin 第一条非空行(交互式粘贴 cookie)。5 分钟兜底超时。 */
|
|
107
|
+
function readCookieLine() {
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
let got = '';
|
|
110
|
+
let settled = false;
|
|
111
|
+
const done = (v) => { if (!settled) {
|
|
112
|
+
settled = true;
|
|
113
|
+
try {
|
|
114
|
+
rl.close();
|
|
115
|
+
}
|
|
116
|
+
catch { /* ignore */ }
|
|
117
|
+
resolve(v);
|
|
118
|
+
} };
|
|
119
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
120
|
+
rl.on('line', (line) => { const t = line.trim(); if (t)
|
|
121
|
+
done(t); });
|
|
122
|
+
rl.on('close', () => done(got));
|
|
123
|
+
setTimeout(() => done(got), 300000);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 打印当前环境 / token 状态(单一端点)。加 --check 联网探活:baseUrl 可达 + 鉴权是否真有效。
|
|
128
|
+
* 不加 --check 行为不变(纯本地读,保护 cron)。
|
|
129
|
+
*/
|
|
130
|
+
export async function cmdAuthStatus(cfg, args) {
|
|
131
|
+
const ep = endpointFor(cfg);
|
|
132
|
+
const profileName = profileNameFromBaseUrl(ep.baseUrl);
|
|
133
|
+
const session = AuthTokenSession.fromProfile(cfg);
|
|
134
|
+
let tokenLine = '❌ 未申请(跑 e9 auth token-login 或 e9 auth cookie-login)';
|
|
135
|
+
let extra = '';
|
|
136
|
+
if (session.data.token) {
|
|
137
|
+
const left = Math.round(((session.data.cookieSavedAt || 0) + (session.data.expireSeconds || 0) * 1000 - Date.now()) / 60000);
|
|
138
|
+
tokenLine = session.isExpired ? '⚠ 本地已过期' : `✅ 剩余约 ${left} 分钟`;
|
|
139
|
+
extra = `\n token: ${session.data.token.slice(0, 16)}… | expireTime: ${session.data.expireTime || '?'}`;
|
|
140
|
+
}
|
|
141
|
+
else if (session.data.noToken) {
|
|
142
|
+
tokenLine = '⚠ noToken(走 cookie 鉴权:系统不支持 aiAgentToken 或已手动导入 cookie)';
|
|
143
|
+
}
|
|
144
|
+
console.log('════════════════════════════════════════════════════');
|
|
145
|
+
console.log(` E9 认证状态 · ${profileName}`);
|
|
146
|
+
console.log('─────────────────────────────────────────────────────');
|
|
147
|
+
console.log(` 配置文件 ${cfg._configPath ?? '?'}`);
|
|
148
|
+
console.log(` baseUrl ${cfg.baseUrl}`);
|
|
149
|
+
console.log(` username ${cfg.username ?? '(未配置)'}`);
|
|
150
|
+
console.log(` encPwd ${cfg.encPassword ? '✅ 已配置(autoFill 可用)' : '❌ 未配置(手动登录)'}`);
|
|
151
|
+
console.log(` cookie ${cfg.cookie ? `已配置 (${cfg.cookie.length} 字符)` : '未配置'}`);
|
|
152
|
+
console.log(` aiAgentToken ${tokenLine}${extra}`);
|
|
153
|
+
if (args?.flags?.check) {
|
|
154
|
+
console.log('─────────────────────────────────────────────────────');
|
|
155
|
+
console.log(' [--check] 联网探活 ...');
|
|
156
|
+
const reach = await canReach(ep.baseUrl, 8000);
|
|
157
|
+
console.log(` baseUrl 可达 ${reach ? '✅' : '❌ 不可达(地址/网络问题)'}`);
|
|
158
|
+
if (reach && (cfg.cookie || session.data.token)) {
|
|
159
|
+
// ★ #11 --check 是探活命令,必须无副作用:临时置 E9_NO_REAUTH=1,
|
|
160
|
+
// 让 ensureValid(cookie 优先/退避)+ requestWithRetry(reactive)都不弹浏览器续期;finally 恢复
|
|
161
|
+
const savedNoReauth = process.env.E9_NO_REAUTH;
|
|
162
|
+
process.env.E9_NO_REAUTH = '1';
|
|
163
|
+
try {
|
|
164
|
+
const api = await createApiFromProfile(cfg);
|
|
165
|
+
const r = await api.get('/api/blog/at/getAtAllList', { name: 'e9check' });
|
|
166
|
+
const j = r?.json;
|
|
167
|
+
const msg = String(j?.msg ?? '');
|
|
168
|
+
if (r && r.status === 200 && j && j.status !== false)
|
|
169
|
+
console.log(' 鉴权有效性 ✅ cookie/token 可用,能取数据');
|
|
170
|
+
else if (r && (r.status === 401 || r.status === 403 || /登录信息超时|无效认证|E9AIAgent/.test(msg)))
|
|
171
|
+
console.log(' 鉴权有效性 ⚠ 鉴权失效(cookie/token 已过期),需重新登录或导入');
|
|
172
|
+
else
|
|
173
|
+
console.log(` 鉴权有效性 ⚠ 响应异常 HTTP ${r?.status} msg=${msg.slice(0, 80)}`);
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
console.log(` 鉴权有效性 ⚠ 探测请求失败:${e.message}`);
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
process.env.E9_NO_REAUTH = savedNoReauth;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else if (reach) {
|
|
183
|
+
console.log(' 鉴权有效性 — 未配置 cookie/token,无法探测');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
console.log('════════════════════════════════════════════════════');
|
|
187
|
+
}
|
package/dist/blog.js
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
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 映射:
|
|
89
|
+
* 全部(-1) / 关注(-2) / 共享(-3) / 下级(-5)
|
|
90
|
+
*/
|
|
91
|
+
export async function getBlogHomepageList(cfg, opts = {}) {
|
|
92
|
+
const api = await createApiFromProfile(cfg);
|
|
93
|
+
const scopeMap = { '全部': '-1', '关注': '-2', '共享': '-3', '下级': '-5', 'subordinate': '-5', 'all': '-1', 'attention': '-2', 'share': '-3', 'sub': '-5' };
|
|
94
|
+
const groupId = scopeMap[opts.scope] ?? opts.scope ?? '-1';
|
|
95
|
+
const endDate = opts.endDate ?? todayStr();
|
|
96
|
+
// 默认查最近 30 天
|
|
97
|
+
const startDate = opts.startDate ?? offsetDate(endDate, -30);
|
|
98
|
+
const params = {
|
|
99
|
+
content: opts.content ?? '',
|
|
100
|
+
blogUserId: opts.blogUserId ?? '',
|
|
101
|
+
deptId: opts.deptId ?? '',
|
|
102
|
+
subId: opts.subId ?? '',
|
|
103
|
+
groupId,
|
|
104
|
+
startDate,
|
|
105
|
+
endDate,
|
|
106
|
+
currentPage: opts.page ?? 1,
|
|
107
|
+
pageSize: 10,
|
|
108
|
+
};
|
|
109
|
+
const r = await api.get('/api/blog/base/getBlogHomepageList', params);
|
|
110
|
+
const all = extractBlogItems(r.json);
|
|
111
|
+
const items = all.filter((it) => it.id || it.content);
|
|
112
|
+
return { scope: opts.scope ?? '全部', groupId, startDate, endDate, page: opts.page ?? 1, total: r.json.total ?? items.length, items, raw: r.json };
|
|
113
|
+
}
|
|
114
|
+
// ─── 微博搜索 ────────────────────────────────────────────
|
|
115
|
+
export async function searchMyBlog(cfg, opts = {}) {
|
|
116
|
+
const api = await createApiFromProfile(cfg);
|
|
117
|
+
const params = {
|
|
118
|
+
content: opts.content ?? '',
|
|
119
|
+
startDate: opts.startDate ?? '',
|
|
120
|
+
endDate: opts.endDate ?? '',
|
|
121
|
+
currentPage: opts.page ?? 1,
|
|
122
|
+
pageSize: 10,
|
|
123
|
+
};
|
|
124
|
+
const r = await api.get('/api/blog/base/getMyBlogListBySearch', params);
|
|
125
|
+
const all = extractBlogItems(r.json);
|
|
126
|
+
const items = all.filter((it) => it.id || it.content);
|
|
127
|
+
return { content: opts.content, startDate: opts.startDate, endDate: opts.endDate, page: opts.page ?? 1, total: r.json.total ?? items.length, items, raw: r.json };
|
|
128
|
+
}
|
|
129
|
+
// ─── 个人主页信息 ────────────────────────────────────────
|
|
130
|
+
export async function getBlogPersonalInfo(cfg, blogId) {
|
|
131
|
+
const api = await createApiFromProfile(cfg);
|
|
132
|
+
const r = await api.get('/api/blog/base/getBlogPersonalInfo', { blogId: String(blogId) });
|
|
133
|
+
const j = r.json || {};
|
|
134
|
+
return {
|
|
135
|
+
userId: stringify(j.userId),
|
|
136
|
+
username: stringify(j.username),
|
|
137
|
+
deptName: stringify(j.deptName),
|
|
138
|
+
deptId: stringify(j.deptId),
|
|
139
|
+
blogCount: stringify(j.blogCount),
|
|
140
|
+
isLower: stringify(j.islower) === '1',
|
|
141
|
+
attentionStatus: stringify(j.attentionStatus),
|
|
142
|
+
myAttentionCount: stringify(j.myAttentionCount),
|
|
143
|
+
attentionMeCount: stringify(j.attentionMeCount),
|
|
144
|
+
imageUrl: stringify(j.imageUrl),
|
|
145
|
+
raw: j,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// ─── 催办 ────────────────────────────────────────────────
|
|
149
|
+
/**
|
|
150
|
+
* 催办(提醒某人写日报)。
|
|
151
|
+
* 前端 SPA 实证(/spa/blog/static/index.js):onRemindClick → remind({workdate, remindId})
|
|
152
|
+
* → setNotice → GET /api/blog/base/sendRemindUnsubmit?remindId=<uid>&workdate=<YYYY-MM-DD>。
|
|
153
|
+
* ⚠ 不能用 POST,参数名也不是 userId——POST+userId 会 500(remindId 为空抛
|
|
154
|
+
* `For input string: ""`),且缺少 workdate。opts.remindId 兼容旧 userId 别名。
|
|
155
|
+
*/
|
|
156
|
+
export async function sendRemindUnsubmit(cfg, opts = {}) {
|
|
157
|
+
const ep = endpointFor(cfg);
|
|
158
|
+
const remindId = String(opts.remindId ?? opts.userId ?? '');
|
|
159
|
+
const workdate = String(opts.workdate ?? '');
|
|
160
|
+
if (!opts.send)
|
|
161
|
+
return { sent: false, endpoint: { baseUrl: ep.baseUrl }, remindId, workdate };
|
|
162
|
+
const api = await createApiFromProfile(cfg);
|
|
163
|
+
const r = await api.get('/api/blog/base/sendRemindUnsubmit', { remindId, workdate });
|
|
164
|
+
return { sent: true, endpoint: { baseUrl: ep.baseUrl }, remindId, workdate, response: r };
|
|
165
|
+
}
|
|
166
|
+
// ─── 回复微博 ────────────────────────────────────────────
|
|
167
|
+
export async function addBlogReply(cfg, opts = {}) {
|
|
168
|
+
const ep = endpointFor(cfg);
|
|
169
|
+
if (!opts.send)
|
|
170
|
+
return { sent: false, endpoint: { baseUrl: ep.baseUrl }, blogId: opts.blogId };
|
|
171
|
+
const api = await createApiFromProfile(cfg);
|
|
172
|
+
// 抓包实证(微博抓包2.har):bediscussantId(被评论人 userid,大写 I) + discussId(微博 id) +
|
|
173
|
+
// workdate + commentType=0 + comefrom=0 + content(HTML)。缺 bediscussantId 报“无被评论人”。
|
|
174
|
+
const body = {
|
|
175
|
+
content: textToBlogHtml(opts.content ?? ''),
|
|
176
|
+
bediscussantId: String(opts.discussantId ?? ''),
|
|
177
|
+
workdate: opts.workdate ?? '',
|
|
178
|
+
commentType: '0',
|
|
179
|
+
comefrom: '0',
|
|
180
|
+
discussId: String(opts.blogId ?? ''),
|
|
181
|
+
};
|
|
182
|
+
const r = await api.post('/api/blog/base/addBlogReply', body);
|
|
183
|
+
return { sent: true, endpoint: { baseUrl: ep.baseUrl }, blogId: opts.blogId, response: r };
|
|
184
|
+
}
|
|
185
|
+
// ─── 点赞/取消点赞 ────────────────────────────────────────
|
|
186
|
+
// 修复:SPA bundle(/spa/blog/static/index.js)实证 addOrDeleteZan 请求体为
|
|
187
|
+
// { workdate, userId, optType(1=赞 / 0=取消赞), discussId(微博id) }。
|
|
188
|
+
// 旧实现只发 { blogId },后端按 discussId 匹配不到 → 静默 no-op(status:1 但 isZan 不变)。
|
|
189
|
+
export async function addOrDeleteZan(cfg, opts = {}) {
|
|
190
|
+
const ep = endpointFor(cfg);
|
|
191
|
+
if (!opts.send)
|
|
192
|
+
return { sent: false, endpoint: { baseUrl: ep.baseUrl }, blogId: opts.blogId, optType: opts.optType ?? 1, workdate: opts.workdate, userId: opts.userId };
|
|
193
|
+
const api = await createApiFromProfile(cfg);
|
|
194
|
+
const optType = String(opts.optType ?? 1); // 1=赞 0=取消赞
|
|
195
|
+
const body = {
|
|
196
|
+
workdate: opts.workdate ?? '',
|
|
197
|
+
userId: String(opts.userId ?? ''),
|
|
198
|
+
optType,
|
|
199
|
+
discussId: String(opts.blogId ?? ''),
|
|
200
|
+
};
|
|
201
|
+
const r = await api.post('/api/blog/base/addOrDeleteZan', body);
|
|
202
|
+
const j = r.json || {};
|
|
203
|
+
const zan = j.blogZanBean || {};
|
|
204
|
+
const isZan = zan.isZan === '1' || zan.isZan === 1 || zan.isZan === true
|
|
205
|
+
|| (Array.isArray(zan.blogZanPoList) && zan.blogZanPoList.some((z) => String(z.zanHrmid ?? '') === String(opts.userId)));
|
|
206
|
+
return {
|
|
207
|
+
sent: true, endpoint: { baseUrl: ep.baseUrl }, blogId: opts.blogId, optType, workdate: opts.workdate, userId: opts.userId,
|
|
208
|
+
response: r, isZan,
|
|
209
|
+
ok: j.status === '1' || j.status === 1 || j.api_status === '1' || j.api_status === true,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
// ─── 微博发布 ────────────────────────────────────────────
|
|
213
|
+
export async function createBlog(cfg, opts) {
|
|
214
|
+
const ep = endpointFor(cfg);
|
|
215
|
+
const workdate = opts.workdate ?? '';
|
|
216
|
+
const html = textToBlogHtml(opts.content);
|
|
217
|
+
const body = {
|
|
218
|
+
appItemId: opts.discussId ? '4' : '1', comefrom: '0', content: html, workdate,
|
|
219
|
+
shareType: opts.shareType ?? '1', shareValue: '', atChooseId: '', hideContent: '0', dbCheck: '0',
|
|
220
|
+
};
|
|
221
|
+
let mode;
|
|
222
|
+
if (opts.discussId) {
|
|
223
|
+
body.discussId = opts.discussId;
|
|
224
|
+
mode = 'update';
|
|
225
|
+
}
|
|
226
|
+
else if (workdate) {
|
|
227
|
+
mode = 'create-replenish';
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
body.fromTag = '2';
|
|
231
|
+
mode = 'create-today';
|
|
232
|
+
}
|
|
233
|
+
if (!opts.send)
|
|
234
|
+
return { sent: false, endpoint: { baseUrl: ep.baseUrl }, mode, body };
|
|
235
|
+
const api = await createApiFromProfile(cfg);
|
|
236
|
+
const r = await api.post('/api/blog/base/saveOrUpdateBlog', body);
|
|
237
|
+
return { sent: true, endpoint: { baseUrl: ep.baseUrl }, mode, body, response: r };
|
|
238
|
+
}
|
|
239
|
+
export function textToBlogHtml(text) {
|
|
240
|
+
return text.split(/\r?\n/).map((l) => `<p>${escapeHtml(l)}</p>`).join('');
|
|
241
|
+
}
|
|
242
|
+
// ─── 微博工作指数 + 心情指数 ──────────────────────────────
|
|
243
|
+
export async function getBlogWorkIndex(cfg) {
|
|
244
|
+
const api = await createApiFromProfile(cfg);
|
|
245
|
+
const r = await api.get('/api/blog/base/getBlogWorkIndex');
|
|
246
|
+
const j = r.json || {};
|
|
247
|
+
return {
|
|
248
|
+
mood: { name: j.mood?.name, title: j.mood?.moodIndexTitle, index: j.mood?.moodIndex },
|
|
249
|
+
work: { name: j.work?.name, title: j.work?.workIndexTitle, index: j.work?.workIndex },
|
|
250
|
+
raw: j,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
// ─── 微博菜单计数 ────────────────────────────────────────
|
|
254
|
+
export async function getBlogMenuCountdata(cfg) {
|
|
255
|
+
const api = await createApiFromProfile(cfg);
|
|
256
|
+
const r = await api.get('/api/blog/base/getBlogMenuCountdata');
|
|
257
|
+
const j = r.json || {};
|
|
258
|
+
return {
|
|
259
|
+
counts: j.countData ?? {},
|
|
260
|
+
templateId: j.templateId,
|
|
261
|
+
templateType: j.templateType,
|
|
262
|
+
raw: j,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
// ─── 微博访客列表 ────────────────────────────────────────
|
|
266
|
+
export async function getVisitorList(cfg, opts = {}) {
|
|
267
|
+
const api = await createApiFromProfile(cfg);
|
|
268
|
+
const recordType = opts.recordType ?? 'visit';
|
|
269
|
+
const page = opts.page ?? 1;
|
|
270
|
+
const pageSize = opts.pageSize ?? 5;
|
|
271
|
+
const r = await api.get('/api/blog/base/getVisitorList', { recordType, currentPage: page, pageSize });
|
|
272
|
+
const j = r.json || {};
|
|
273
|
+
return {
|
|
274
|
+
recordType,
|
|
275
|
+
page: j.currentPage ?? page,
|
|
276
|
+
pageSize: j.pageSize ?? pageSize,
|
|
277
|
+
total: j.totalSize ?? 0,
|
|
278
|
+
items: (j.list || []).map((it) => ({
|
|
279
|
+
id: stringify(it.id), name: stringify(it.name),
|
|
280
|
+
visitor: stringify(it.visitor), visitorName: stringify(it.visitorName),
|
|
281
|
+
visitDate: stringify(it.visitDate), visitCount: stringify(it.visitCount),
|
|
282
|
+
raw: it,
|
|
283
|
+
})),
|
|
284
|
+
raw: j,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
// ─── 微博草稿/工作笔记 ────────────────────────────────────
|
|
288
|
+
export async function getBlogNote(cfg, workdate) {
|
|
289
|
+
const api = await createApiFromProfile(cfg);
|
|
290
|
+
const params = {};
|
|
291
|
+
if (workdate)
|
|
292
|
+
params.workdate = workdate;
|
|
293
|
+
const r = await api.get('/api/blog/base/getBlogNote', params);
|
|
294
|
+
const j = r.json || {};
|
|
295
|
+
return {
|
|
296
|
+
id: j.id,
|
|
297
|
+
content: j.content ? stripHtml(decodeBase64Html(j.content)) : '',
|
|
298
|
+
contentHtml: j.content ? decodeBase64Html(j.content) : '',
|
|
299
|
+
updateDate: j.updateDate,
|
|
300
|
+
workdate,
|
|
301
|
+
raw: j,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
// ─── 微博草稿(便签)写入 ────────────────────────────────
|
|
305
|
+
// 修复 #4:E9 微博无独立草稿接口,所谓"草稿/工作笔记"走 saveBlogNote(POST /api/blog/base/saveBlogNote)。
|
|
306
|
+
// 注:排查报告"saveOrUpdateBlog+dbCheck=1 存草稿"为误判——dbCheck 是保密勾选项非草稿标志,
|
|
307
|
+
// 内容追加是因未带 discussId 走了新建模式。草稿写入应走本接口。
|
|
308
|
+
export async function saveBlogNote(cfg, opts = {}) {
|
|
309
|
+
const ep = endpointFor(cfg);
|
|
310
|
+
if (!opts.send)
|
|
311
|
+
return { sent: false, endpoint: { baseUrl: ep.baseUrl }, workdate: opts.workdate ?? todayStr() };
|
|
312
|
+
const api = await createApiFromProfile(cfg);
|
|
313
|
+
const workdate = opts.workdate ?? todayStr();
|
|
314
|
+
const html = textToBlogHtml(opts.content ?? '');
|
|
315
|
+
const r = await api.post('/api/blog/base/saveBlogNote', { workdate, content: html });
|
|
316
|
+
const j = r.json || {};
|
|
317
|
+
return { sent: true, endpoint: { baseUrl: ep.baseUrl }, workdate, response: r, ok: j.status === '1' || j.status === 1 || j.api_status === '1' };
|
|
318
|
+
}
|
|
319
|
+
// ─── 微博撤回 / 恢复 ─────────────────────────────────────
|
|
320
|
+
// 修复 #4:E9 微博不可删除(产品无 deleteBlog 接口),收回已发布微博的唯一手段是撤回。
|
|
321
|
+
// POST /api/blog/myblog/recallblog,body: blogId + workdate + opType(1=撤回, 0=恢复)。
|
|
322
|
+
export async function recallBlog(cfg, opts = {}) {
|
|
323
|
+
const ep = endpointFor(cfg);
|
|
324
|
+
if (!opts.send)
|
|
325
|
+
return { sent: false, endpoint: { baseUrl: ep.baseUrl }, blogId: opts.blogId, opType: opts.opType ?? 1 };
|
|
326
|
+
const api = await createApiFromProfile(cfg);
|
|
327
|
+
const opType = String(opts.opType ?? 1); // 1=撤回 0=恢复
|
|
328
|
+
const r = await api.post('/api/blog/myblog/recallblog', {
|
|
329
|
+
blogId: String(opts.blogId ?? ''),
|
|
330
|
+
workdate: opts.workdate ?? '',
|
|
331
|
+
opType,
|
|
332
|
+
});
|
|
333
|
+
const j = r.json || {};
|
|
334
|
+
return { sent: true, endpoint: { baseUrl: ep.baseUrl }, blogId: opts.blogId, opType, response: r, ok: j.api_status === '1' || j.api_status === true || j.status === '1' };
|
|
335
|
+
}
|
|
336
|
+
// ─── 删除微博评论/回复 ───────────────────────────────────
|
|
337
|
+
// 修复 #4 补充:E9 微博正文不可删,但评论可删。POST /api/blog/base/deleteBlogReplay。
|
|
338
|
+
// body: replyId + workdate + discussId。
|
|
339
|
+
export async function deleteBlogReply(cfg, opts = {}) {
|
|
340
|
+
const ep = endpointFor(cfg);
|
|
341
|
+
if (!opts.send)
|
|
342
|
+
return { sent: false, endpoint: { baseUrl: ep.baseUrl }, replyId: opts.replyId };
|
|
343
|
+
const api = await createApiFromProfile(cfg);
|
|
344
|
+
const r = await api.post('/api/blog/base/deleteBlogReplay', {
|
|
345
|
+
replyId: String(opts.replyId ?? ''),
|
|
346
|
+
workdate: opts.workdate ?? '',
|
|
347
|
+
discussId: String(opts.discussId ?? ''),
|
|
348
|
+
});
|
|
349
|
+
const j = r.json || {};
|
|
350
|
+
return { sent: true, endpoint: { baseUrl: ep.baseUrl }, replyId: opts.replyId, response: r, ok: j.status === '1' || j.status === 1 || j.delStatus === '1' };
|
|
351
|
+
}
|
|
352
|
+
// ─── 微博当前用户信息 ────────────────────────────────────
|
|
353
|
+
export async function getCurrentUserInfo(cfg) {
|
|
354
|
+
const api = await createApiFromProfile(cfg);
|
|
355
|
+
const r = await api.get('/api/blog/base/getCurrentUserInfo');
|
|
356
|
+
const j = r.json || {};
|
|
357
|
+
return {
|
|
358
|
+
id: j.currentUser?.id,
|
|
359
|
+
lastname: j.currentUser?.lastname,
|
|
360
|
+
departmentName: j.currentUser?.departmentName,
|
|
361
|
+
jobtitle: j.currentUser?.jobtitle,
|
|
362
|
+
imageUrl: j.currentUser?.imageUrl,
|
|
363
|
+
todo: j.todo,
|
|
364
|
+
raw: j,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
// ─── 微博消息提醒 ────────────────────────────────────────
|
|
368
|
+
export async function getMsgRemindList(cfg) {
|
|
369
|
+
const api = await createApiFromProfile(cfg);
|
|
370
|
+
const r = await api.get('/api/blog/base/getMsgRemindList');
|
|
371
|
+
const j = r.json || {};
|
|
372
|
+
return {
|
|
373
|
+
items: (j.remindList || []).map((it) => ({
|
|
374
|
+
id: stringify(it.id), content: stringify(it.content),
|
|
375
|
+
creator: stringify(it.creator), createDate: stringify(it.createDate),
|
|
376
|
+
type: stringify(it.type), raw: it,
|
|
377
|
+
})),
|
|
378
|
+
raw: j,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
// ─── 微博默认模板 ────────────────────────────────────────
|
|
382
|
+
export async function getDefaultTemplate(cfg) {
|
|
383
|
+
const api = await createApiFromProfile(cfg);
|
|
384
|
+
const r = await api.get('/api/blog/base/getDefaultTemplate');
|
|
385
|
+
const j = r.json || {};
|
|
386
|
+
return {
|
|
387
|
+
template: j.defaultTemplate ?? {},
|
|
388
|
+
showTmButton: j.showTmButton,
|
|
389
|
+
isenableEcme: j.isenableecme,
|
|
390
|
+
raw: j,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
// ─── 微博可用应用列表 ────────────────────────────────────
|
|
394
|
+
export async function getBlogAppList(cfg) {
|
|
395
|
+
const api = await createApiFromProfile(cfg);
|
|
396
|
+
const r = await api.get('/api/blog/base/getBlogAppList');
|
|
397
|
+
const j = r.json || {};
|
|
398
|
+
return {
|
|
399
|
+
appList: (j.appList || []).map((g) => ({
|
|
400
|
+
name: g.name,
|
|
401
|
+
items: g.items || [],
|
|
402
|
+
secretLevel: g.secretLevel,
|
|
403
|
+
})),
|
|
404
|
+
isBlogNote: j.isBlogNote,
|
|
405
|
+
raw: j,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
// ─── @ 提及用户列表 ──────────────────────────────────────
|
|
409
|
+
export async function getAtAllList(cfg, opts = {}) {
|
|
410
|
+
const api = await createApiFromProfile(cfg);
|
|
411
|
+
const pageSize = opts.pageSize ?? 20;
|
|
412
|
+
const pageIndex = opts.pageIndex ?? 1;
|
|
413
|
+
const name = opts.name ?? '';
|
|
414
|
+
const r = await api.get('/api/blog/at/getAtAllList', { pageSize, pageIndex, name });
|
|
415
|
+
const j = r.json || {};
|
|
416
|
+
return {
|
|
417
|
+
pageIndex,
|
|
418
|
+
pageSize,
|
|
419
|
+
noMore: j.noMore,
|
|
420
|
+
items: (j.operators || []).map((it) => ({
|
|
421
|
+
uid: it.uid,
|
|
422
|
+
name: it.data,
|
|
423
|
+
py: it.datapy,
|
|
424
|
+
})),
|
|
425
|
+
raw: j,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
// ─── 微博搜索条件字段 ─────────────────────────────────────
|
|
429
|
+
export async function getBlogPageSearchCondition(cfg) {
|
|
430
|
+
const api = await createApiFromProfile(cfg);
|
|
431
|
+
const r = await api.get('/api/blog/backstageshare/getBlogPageSearchCondition');
|
|
432
|
+
const j = r.json || {};
|
|
433
|
+
const fields = [];
|
|
434
|
+
for (const grp of (j.conditioninfo || [])) {
|
|
435
|
+
for (const item of (grp.items || [])) {
|
|
436
|
+
fields.push({
|
|
437
|
+
group: grp.title || '',
|
|
438
|
+
label: item.label || '',
|
|
439
|
+
domkey: (item.domkey || []).join(', '),
|
|
440
|
+
type: item.conditionType || '',
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return { fields, raw: j };
|
|
445
|
+
}
|
|
446
|
+
// ─── 工具函数 ────────────────────────────────────────────
|
|
447
|
+
function decodeBase64Html(v) {
|
|
448
|
+
if (typeof v !== 'string' || !v)
|
|
449
|
+
return undefined;
|
|
450
|
+
try {
|
|
451
|
+
const txt = Buffer.from(v.replace(/\s+/g, ''), 'base64').toString('utf8');
|
|
452
|
+
return txt || undefined;
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
return undefined;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function escapeHtml(s) { return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); }
|
|
459
|
+
function stringify(v) { if (v === undefined || v === null || v === '')
|
|
460
|
+
return undefined; return String(v); }
|
|
461
|
+
function todayStr() {
|
|
462
|
+
const d = new Date();
|
|
463
|
+
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
|
464
|
+
}
|
|
465
|
+
function offsetDate(dateStr, days) {
|
|
466
|
+
const d = new Date(dateStr);
|
|
467
|
+
d.setDate(d.getDate() + days);
|
|
468
|
+
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
|
469
|
+
}
|