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.
@@ -0,0 +1,38 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+
5
+ const stateDir = () => path.resolve(process.cwd(), 'state');
6
+ const queryDir = () => path.join(stateDir(), 'query_contexts');
7
+
8
+ export function saveCandidateQuery({ workflow, candidates, options }) {
9
+ fs.mkdirSync(queryDir(), { recursive: true });
10
+ const id = randomUUID();
11
+ fs.writeFileSync(path.join(queryDir(), `${id}.json`), JSON.stringify({ id, workflow, candidates, options, createdAt: new Date().toISOString() }, null, 2), 'utf8');
12
+ return id;
13
+ }
14
+ export function loadCandidateQuery(id) {
15
+ if (!/^[a-zA-Z0-9-]+$/.test(String(id)))
16
+ throw new Error('resume-query 格式无效。');
17
+ const file = path.join(queryDir(), `${id}.json`);
18
+ if (!fs.existsSync(file))
19
+ throw new Error('查询上下文不存在或已被清理,请重新搜索流程名称。');
20
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
21
+ }
22
+ export function persistSearchResult(result, title = '待办查询') {
23
+ const json = JSON.stringify(result, null, 2);
24
+ if (Buffer.byteLength(json, 'utf8') <= 8192)
25
+ return null;
26
+ fs.mkdirSync(stateDir(), { recursive: true });
27
+ const jsonPath = path.join(stateDir(), 'last_search.json');
28
+ const mdPath = path.join(stateDir(), 'last_search.md');
29
+ fs.writeFileSync(jsonPath, json, 'utf8');
30
+ const items = Array.isArray(result.items)
31
+ ? result.items
32
+ : (Array.isArray(result.groups) ? result.groups.flatMap((group) => group.items ?? []) : []);
33
+ const lines = [`# ${title}`, '', `共 ${result.total ?? items.length} 条,以下为前 10 条:`, ''];
34
+ for (const item of items.slice(0, 10))
35
+ lines.push(`- ${item.title ?? '(无标题)'}(${item.requestId ?? ''})`);
36
+ fs.writeFileSync(mdPath, lines.join('\n'), 'utf8');
37
+ return { jsonPath, mdPath };
38
+ }
package/dist/render.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 人类可读输出渲染:框线表格、分隔线。
3
+ * 集中框线宽度,避免散落的字面量重复。
4
+ */
5
+ export const BOX_TOP = '═'.repeat(50); // 上下封口
6
+ export const DIVIDER = '─'.repeat(53); // 区内分隔
7
+ /**
8
+ * 打印一个标题框:顶线 + title + 可选正文行 + 底线。
9
+ * title/lines 已含前导空格等格式,由调用方决定对齐。
10
+ */
11
+ export function box(title, lines = []) {
12
+ console.log(BOX_TOP);
13
+ if (title)
14
+ console.log(title);
15
+ for (const l of lines)
16
+ console.log(l);
17
+ console.log(BOX_TOP);
18
+ }
19
+ export function divider() {
20
+ console.log(DIVIDER);
21
+ }
package/dist/report.js ADDED
@@ -0,0 +1,50 @@
1
+ import { endpointFor } from './config.js';
2
+ import { getWfList } from './workflow.js';
3
+ import { getMyCalendar } from './workplan.js';
4
+ import { getEmailList } from './email.js';
5
+ import { getButtons } from './kq.js';
6
+ import { getMyBlogList, isBlogRecalled } from './blog.js';
7
+
8
+ function source(module, id, time, data) {
9
+ return { module, id: String(id ?? ''), time: String(time ?? ''), data };
10
+ }
11
+ function onDate(value, date) {
12
+ return String(value ?? '').slice(0, 10) === date;
13
+ }
14
+
15
+ /** 只汇总当前 E9 环境可回溯且状态可确认的数据;不把聊天内容、浏览行为或撤回微博当作事实。 */
16
+ export async function collectQueryableBusiness(cfg, date) {
17
+ const results = await Promise.allSettled([
18
+ getWfList(cfg, { category: 'done', page: 1, operateTime: date }),
19
+ getWfList(cfg, { category: 'mine', page: 1, createTime: date }),
20
+ getMyCalendar(cfg, { selectDate: date }),
21
+ getEmailList(cfg, { folder: 'inbox', current: 1, startdate: date, enddate: date }),
22
+ getEmailList(cfg, { folder: 'sent', current: 1, startdate: date, enddate: date }),
23
+ getButtons(cfg),
24
+ getMyBlogList(cfg, date, 1),
25
+ ]);
26
+ const value = (index, fallback) => results[index].status === 'fulfilled' ? results[index].value : fallback;
27
+ const failed = results.map((result, index) => result.status === 'rejected' ? ({ index, message: String(result.reason?.message ?? result.reason) }) : null).filter(Boolean);
28
+ const done = value(0, { items: [] }).items.filter((item) => onDate(item.operatedate ?? item.operateTime, date)).map((item) => source('已办流程', item.requestId, item.operatedate ?? item.operateTime, { title: item.title }));
29
+ const mine = value(1, { items: [] }).items.filter((item) => onDate(item.createdate ?? item.createTime, date)).map((item) => source('我发起的流程', item.requestId, item.createdate ?? item.createTime, { title: item.title }));
30
+ const calendar = value(2, { items: [] }).items.filter((item) => onDate(item.beginDateTime, date)).map((item) => source('日程', item.id, item.beginDateTime, { name: item.name, status: item.status }));
31
+ const inbox = value(3, { items: [] }).items.filter((item) => onDate(item.date, date)).map((item) => source('收件箱', item.id, item.date, { subject: item.subject, from: item.from }));
32
+ const sent = value(4, { items: [] }).items.filter((item) => onDate(item.date, date)).map((item) => source('已发送邮件', item.id, item.date, { subject: item.subject, to: item.to }));
33
+ const attendance = value(5, { timeline: [] }).timeline.map((item) => source('考勤', `${item.date}:${item.type}`, item.signTime, { type: item.type, signTime: item.signTime, status: item.kqstatus }));
34
+ // 微博:列表接口返回 isReCalled(已抓包验证,0=正常发布,1/2=已撤回)。
35
+ // 仅纳入正常发布(isReCalled=0)的微博,已撤回微博剔除并记入 notes。
36
+ const blogToday = value(6, { items: [] }).items.filter((it) => onDate(it.workdate, date) || onDate(it.createdate, date));
37
+ const recalledBlogs = blogToday.filter((it) => isBlogRecalled(it));
38
+ const blogs = blogToday.filter((it) => !isBlogRecalled(it)).map((item) => source('微博', item.id, item.workdate, { content: item.content?.slice(0, 60) }));
39
+ const notes = [];
40
+ if (recalledBlogs.length > 0)
41
+ notes.push(`微博:${recalledBlogs.length} 条已撤回(isReCalled≠0)未纳入汇总`);
42
+ return {
43
+ scope: '当前 E9 环境可查询业务数据汇总(不含操作审计、页面浏览及其他不可查询行为)',
44
+ baseUrl: endpointFor(cfg).baseUrl,
45
+ date,
46
+ sources: { done, mine, calendar, inbox, sent, attendance, blogs },
47
+ notes,
48
+ failures: failed,
49
+ };
50
+ }
@@ -0,0 +1,43 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+
5
+ const MAX_AGE_MS = 24 * 60 * 60 * 1000;
6
+
7
+ function planDir() {
8
+ return path.resolve(process.cwd(), 'state', 'create_plans');
9
+ }
10
+ function planPath(id) {
11
+ if (!/^[a-zA-Z0-9-]+$/.test(String(id)))
12
+ throw new Error('plan_id 格式无效。');
13
+ return path.join(planDir(), `${id}.json`);
14
+ }
15
+
16
+ /** 保存用户已确认的业务字段;短期 token 和签名绝不落盘。 */
17
+ export function saveSubmissionPlan(prepared) {
18
+ const plan = {
19
+ id: randomUUID(),
20
+ workflowId: String(prepared.workflowId),
21
+ workflowName: String(prepared.workflowName ?? ''),
22
+ requestName: String(prepared.requestName),
23
+ values: prepared.values,
24
+ details: prepared.details,
25
+ browserNames: prepared.browserNames,
26
+ files: prepared.files ?? [],
27
+ createdAt: new Date().toISOString(),
28
+ };
29
+ fs.mkdirSync(planDir(), { recursive: true });
30
+ fs.writeFileSync(planPath(plan.id), JSON.stringify(plan, null, 2), 'utf8');
31
+ return plan;
32
+ }
33
+ export function loadSubmissionPlan(id, workflowId) {
34
+ const file = planPath(id);
35
+ if (!fs.existsSync(file))
36
+ throw new Error('未找到确认计划。请先重新预览,确认后再提交。');
37
+ const plan = JSON.parse(fs.readFileSync(file, 'utf8'));
38
+ if (String(plan.workflowId) !== String(workflowId))
39
+ throw new Error('确认计划与当前流程不一致。请重新预览。');
40
+ if (Date.now() - Date.parse(plan.createdAt) > MAX_AGE_MS)
41
+ throw new Error('确认计划已超过 24 小时。请重新预览,避免提交过期内容。');
42
+ return plan;
43
+ }
@@ -0,0 +1,81 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const ACTIVE_STATUSES = new Set(['submitting', 'uncertain', 'created']);
6
+ const DUPLICATE_WINDOW_MS = 10 * 60 * 1000;
7
+
8
+ function stateDir() {
9
+ return path.resolve(process.cwd(), 'state', 'create_submissions');
10
+ }
11
+ function statePath(id) {
12
+ if (!/^[a-zA-Z0-9-]+$/.test(String(id)))
13
+ throw new Error('submission_id 格式无效。');
14
+ return path.join(stateDir(), `${id}.json`);
15
+ }
16
+ function write(record) {
17
+ fs.mkdirSync(stateDir(), { recursive: true });
18
+ fs.writeFileSync(statePath(record.id), JSON.stringify(record, null, 2), 'utf8');
19
+ return record;
20
+ }
21
+ function fingerprint(workflowId, requestName, values) {
22
+ return crypto.createHash('sha256')
23
+ .update(JSON.stringify({ workflowId: String(workflowId), requestName: String(requestName ?? ''), values: values ?? {} }))
24
+ .digest('hex');
25
+ }
26
+ function readRecent() {
27
+ try {
28
+ return fs.readdirSync(stateDir())
29
+ .filter((name) => name.endsWith('.json'))
30
+ .map((name) => JSON.parse(fs.readFileSync(path.join(stateDir(), name), 'utf8')))
31
+ .filter((record) => record && record.id);
32
+ }
33
+ catch {
34
+ return [];
35
+ }
36
+ }
37
+
38
+ /**
39
+ * 为一次真实发起建立本地操作记录。相同内容在十分钟内处于进行/不确定/成功状态时不再次提交。
40
+ * 只存指纹而不存字段原文,避免把敏感表单值重复落盘。
41
+ */
42
+ export function beginSubmission({ workflowId, workflowName = '', requestName = '', values = {} }) {
43
+ const now = Date.now();
44
+ const key = fingerprint(workflowId, requestName, values);
45
+ const duplicate = readRecent().find((record) => record.fingerprint === key
46
+ && ACTIVE_STATUSES.has(record.status)
47
+ && now - Date.parse(record.updatedAt ?? record.createdAt ?? 0) < DUPLICATE_WINDOW_MS);
48
+ if (duplicate)
49
+ return { record: duplicate, duplicate: true };
50
+ const record = {
51
+ id: crypto.randomUUID(),
52
+ workflowId: String(workflowId),
53
+ workflowName: String(workflowName),
54
+ requestName: String(requestName),
55
+ fingerprint: key,
56
+ status: 'submitting',
57
+ requestId: '',
58
+ createdAt: new Date(now).toISOString(),
59
+ updatedAt: new Date(now).toISOString(),
60
+ };
61
+ return { record: write(record), duplicate: false };
62
+ }
63
+ export function loadSubmission(id) {
64
+ const p = statePath(id);
65
+ if (!fs.existsSync(p))
66
+ throw new Error(`未找到发起记录“${id}”。`);
67
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
68
+ }
69
+ export function updateSubmission(id, patch) {
70
+ const old = loadSubmission(id);
71
+ return write({ ...old, ...patch, updatedAt: new Date().toISOString() });
72
+ }
73
+ export function extractCreatedRequestId(response) {
74
+ const body = response?.json ?? {};
75
+ const type = body?.data?.type ?? body?.type ?? '';
76
+ const requestId = body?.requestid ?? body?.data?.requestid ?? body?.data?.resultInfo?.requestid ?? body?.data?.requestId;
77
+ // A success type without an addressable request id cannot be presented as a
78
+ // completed creation: the caller must verify it instead of retrying.
79
+ const accepted = !!requestId && (type === 'SUCCESS' || type === 'ASYNC_SUBMIT');
80
+ return { accepted, requestId: requestId ? String(requestId) : '', type: String(type) };
81
+ }
@@ -0,0 +1,54 @@
1
+ import { createApiFromProfile } from './client.js';
2
+ import { resolveReceivers } from './wfreceiver.js';
3
+ /**
4
+ * 意见征询(向某人征询意见,不改审批人)。实跑确认链路(2026-09-02 真机 .27):
5
+ * loadForm(取 workflowid/nodeid/操作人 userid) → signInput(forwardflag=2 预载) →
6
+ * remarkOperate(operate=save, forwardflag=2, field5=征询对象 userId) → {"success":true,"message":"流程意见征询成功"}。
7
+ *
8
+ * 与转发(forwardflag=1)/传阅(forwardflag=4)/转办(forwardflag=3)同款链路:均走 signInput → remarkOperate
9
+ * (0.5.1 起转发/传阅/转办不再走 requestOperation——会被服务端当"批准")。
10
+ * forward_token_<requestid>_<操作人userid> 由客户端生成(Date.now()),signInput 与 remarkOperate 须用同一值。
11
+ */
12
+ async function buildCommon(cfg, opts) {
13
+ const { requestId } = opts;
14
+ const api = await createApiFromProfile(cfg);
15
+ const r0 = await api.post('/api/workflow/reqform/loadForm', { iscreate: '0', requestid: String(requestId) });
16
+ const p = (r0.json ?? {}).params ?? {};
17
+ const userId = String(p.f_weaver_belongto_userid ?? '1');
18
+ const userType = String(p.f_weaver_belongto_usertype ?? '0');
19
+ const workflowid = String(p.workflowid ?? '');
20
+ const nodeid = String(p.nodeid ?? '');
21
+ const field5 = await resolveReceivers(cfg, opts);
22
+ if (!field5)
23
+ throw new Error('意见征询需要征询对象:传 --receiver <userId> 或 --receiver-name <姓名>');
24
+ const remarkText = opts.remark ?? '';
25
+ const remarkHtml = remarkText.split(/\r?\n/).map((l) => `<p>${l}</p>`).join('');
26
+ const tokenKey = `forward_token_${requestId}_${userId}`;
27
+ const token = String(Date.now());
28
+ const common = {
29
+ isShowReplyNotice: '0', takOverMins: '30', remark: remarkHtml,
30
+ workflowRequestLogId: '', forwardflag: '2', requestid: String(requestId),
31
+ nodeid, workflowid, 'f_weaver_belongto_userid': userId, 'f_weaver_belongto_usertype': userType,
32
+ agentType: '0', agentorByAgentId: '-1', secLevelInfo: '[object Object]',
33
+ [tokenKey]: token,
34
+ };
35
+ return { api, common, field5 };
36
+ }
37
+ export async function askIdeaTodo(cfg, opts) {
38
+ const { api, common, field5 } = await buildCommon(cfg, opts);
39
+ // ① signInput 预载(forwardflag=2,field5 空)
40
+ await api.post('/api/workflow/reqform/signInput', {
41
+ ...common, field5: '', IsSubmitedOpinion: '1', IsBeForwardTodo: '1',
42
+ });
43
+ // ② remarkOperate 提交(operate=save,field5=征询对象)
44
+ return api.post('/api/workflow/reqform/remarkOperate', {
45
+ ...common, field5,
46
+ operate: 'save',
47
+ openByDefaultBrowser: '', IsBeForwardSubmitAlready: '', IsBeForwardAlready: '',
48
+ IsSubmitedOpinion: '', IsBeForwardTodo: '',
49
+ isSubmitSign: '0', isChuanyue: '0', isRemind: '0',
50
+ signworkflowids: '', signdocids: '', remarkLocation: '',
51
+ 'field-annexupload': '', 'field-annexupload-request': String(opts.requestId),
52
+ handWrittenSign: '0', remindTypes: '',
53
+ });
54
+ }
@@ -0,0 +1,154 @@
1
+ import { createApiFromProfile } from './client.js';
2
+ import { getWfList } from './workflow.js';
3
+ import { extractRows } from './parse.js';
4
+
5
+ /**
6
+ * 流程批量操作(待办域)。
7
+ *
8
+ * 接口契约以 2026-09-09 抓包实证为准:
9
+ * - 批量提交 POST /api/workflow/reqform/requestBatchSubmit
10
+ * multiSubIds=<rid,rid,>&belongtoUserids=<uid,uid,>&pagefromtype=1&needBatchSubmit=1
11
+ * - 批量转发 GET /api/workflow/forward/getBatchForwardInfo(预检)
12
+ * POST /api/workflow/reqform/batchForward
13
+ * forwardflag=1&field5=<接收人>&multiSubIds=<rid_uid,rid_uid>&operate=save
14
+ * - 批量关注 POST /api/workflow/requestAttention/getAttentionTypeSet(查类型)
15
+ * POST /api/workflow/requestAttention/saveAttention
16
+ * src=batchadd&requestids=<rid,rid>&ids=<类型id,>
17
+ * - 门禁 POST /api/ec/dev/table/checks(dataKey + checkId + randomDatas=每行 randomField0~14+randomFieldId)
18
+ * - 二次验证 POST /api/workflow/secondauth/getSecondAuthConfig4List(requestIds=rid_uid + __weaver_request_url__=base64(baseUrl))
19
+ *
20
+ * multiSubIds 尾逗号、ids 尾逗号、belongtoUserids 尾逗号均为抓包原样,不能去掉。
21
+ */
22
+
23
+ /** 将用户输入的逗号列表规范化,并在真正调用前阻止常见的错配。 */
24
+ export function parseIdList(value, label) {
25
+ const ids = String(value ?? '').split(',').map((item) => item.trim()).filter(Boolean);
26
+ if (!ids.length)
27
+ throw new Error(`${label}不能为空,请用英文逗号分隔多个 ID。`);
28
+ if (ids.some((id) => !/^\d+$/.test(id)))
29
+ throw new Error(`${label}只能包含数字 ID。`);
30
+ return ids;
31
+ }
32
+
33
+ /** 从待办列表原始行提取门禁所需的随机字段(与抓包 checks 的 randomDatas 结构一致)。 */
34
+ function pickRandomFields(row) {
35
+ const out = {};
36
+ for (let i = 0; i <= 14; i++)
37
+ out[`randomField${i}`] = String(row?.[`randomField${i}`] ?? '');
38
+ out.randomFieldId = String(row?.randomFieldId ?? row?.requestid ?? row?.randomField2 ?? '');
39
+ return out;
40
+ }
41
+
42
+ /** 拉待办列表并取出与 requestIds 匹配的原始行(含 randomField)+ dataKey。跨页匹配直到全部命中或到末尾。 */
43
+ async function loadRowsFor(cfg, requestIds) {
44
+ const wanted = new Set(requestIds.map(String));
45
+ const found = new Map();
46
+ let dataKey = '';
47
+ for (let current = 1; current <= 10; current++) {
48
+ const list = await getWfList(cfg, { category: 'todo', page: current, pageSize: '100' });
49
+ dataKey = list.dataKey || dataKey;
50
+ const rows = extractRows(list.raw?.datas);
51
+ for (const row of rows) {
52
+ const rid = String(row?.requestid ?? row?.randomField2 ?? row?.randomFieldId ?? '');
53
+ if (wanted.has(rid) && !found.has(rid))
54
+ found.set(rid, row);
55
+ }
56
+ if (rows.length < 100 || found.size === wanted.size)
57
+ break;
58
+ if (!rows.length)
59
+ break;
60
+ }
61
+ const missing = requestIds.map(String).filter((id) => !found.has(id));
62
+ if (missing.length)
63
+ throw new Error(`批量操作需要在待办列表中定位这 ${missing.length} 条,未找到:${missing.join(', ')}。请确认 request-id 属于当前待办(待处理),或该待办已不在列表中。`);
64
+ // 保持调用方传入顺序,便于回执与 multiSubIds 对应
65
+ const rows = requestIds.map((id) => found.get(String(id)));
66
+ return { dataKey, rows };
67
+ }
68
+
69
+ /** 批量门禁(抓包实证):checks(dataKey+randomDatas) + getSecondAuthConfig4List(requestIds=rid_uid+__weaver_request_url__=base64)。 */
70
+ async function runGate(api, checkId, rows, dataKey) {
71
+ const check = await api.post('/api/ec/dev/table/checks', {
72
+ dataKey,
73
+ checkId,
74
+ randomDatas: JSON.stringify(rows.map(pickRandomFields)),
75
+ });
76
+ const ridUids = rows
77
+ .map((r) => `${r?.randomField2 ?? r?.requestid}_${r?.randomField4 ?? r?.userid ?? ''}`)
78
+ .join(',');
79
+ const secondAuth = await api.post('/api/workflow/secondauth/getSecondAuthConfig4List', {
80
+ requestIds: ridUids,
81
+ __weaver_request_url__: Buffer.from(api.baseUrl).toString('base64'),
82
+ });
83
+ return { check, secondAuth };
84
+ }
85
+
86
+ /** 批量提交。调用方必须先展示预览,且只有 --yes 才会调用本函数。 */
87
+ export async function batchSubmit(cfg, opts) {
88
+ const requestIds = parseIdList(opts.requestIds, 'requestIds');
89
+ const userIds = parseIdList(opts.userIds, 'userIds');
90
+ if (requestIds.length !== userIds.length)
91
+ throw new Error('requestIds 与 userIds 数量必须一致,确保每条待办对应正确处理人。');
92
+ const api = await createApiFromProfile(cfg);
93
+ const { dataKey, rows } = await loadRowsFor(cfg, requestIds);
94
+ const gate = await runGate(api, 'batchSubmit', rows, dataKey);
95
+ const result = await api.post('/api/workflow/reqform/requestBatchSubmit', {
96
+ multiSubIds: requestIds.join(',') + ',',
97
+ remark: String(opts.remark ?? 'OK'),
98
+ pagefromtype: '1',
99
+ belongtoUserids: userIds.join(',') + ',',
100
+ needBatchSubmit: '1',
101
+ });
102
+ return { requestIds, userIds, gate, result };
103
+ }
104
+
105
+ /** 批量转发(抓包实证:batchForward,multiSubIds=requestId_userId)。 */
106
+ export async function batchForward(cfg, opts) {
107
+ const requestIds = parseIdList(opts.requestIds, 'requestIds');
108
+ const receiver = parseIdList(opts.receiver, 'receiver');
109
+ const api = await createApiFromProfile(cfg);
110
+ // 前置预检(抓包:getBatchForwardInfo → {"allowAllPersonGroup":true})
111
+ const info = await api.get('/api/workflow/forward/getBatchForwardInfo');
112
+ const { dataKey, rows } = await loadRowsFor(cfg, requestIds);
113
+ const gate = await runGate(api, 'batchForward', rows, dataKey);
114
+ // multiSubIds = requestId_userId(userId 为待办当前处理人,来自列表行 randomField4)
115
+ const multiSubIds = rows
116
+ .map((r) => `${r?.randomField2 ?? r?.requestid}_${r?.randomField4 ?? r?.userid ?? ''}`)
117
+ .join(',');
118
+ const result = await api.post('/api/workflow/reqform/batchForward', {
119
+ forwardflag: '1',
120
+ field5: receiver.join(','),
121
+ remark: String(opts.remark ?? ''),
122
+ multiSubIds,
123
+ operate: 'save',
124
+ });
125
+ return { requestIds, receiver, info, gate, result };
126
+ }
127
+
128
+ /** 查询关注类型(只读,供 dry-run 预览展示与 batchAttention 选择类型)。 */
129
+ export async function getAttentionTypes(cfg, requestIds) {
130
+ const api = await createApiFromProfile(cfg);
131
+ const resp = await api.post('/api/workflow/requestAttention/getAttentionTypeSet', {
132
+ src: 'getData2',
133
+ requestid: requestIds.join(','),
134
+ });
135
+ return resp?.json?.datas ?? [];
136
+ }
137
+
138
+ /** 批量关注(抓包实证:getAttentionTypeSet → saveAttention(src=batchadd, ids=类型id,))。 */
139
+ export async function batchAttention(cfg, opts) {
140
+ const requestIds = parseIdList(opts.requestIds, 'requestIds');
141
+ const api = await createApiFromProfile(cfg);
142
+ const types = await getAttentionTypes(cfg, requestIds);
143
+ const attentionId = String(opts.attentionId ?? types[0]?.id ?? '');
144
+ if (!attentionId)
145
+ throw new Error('未能获取关注类型(getAttentionTypeSet 返回空),请确认该环境的“关注类型”已配置,或手动传 --attention-id。');
146
+ const { dataKey, rows } = await loadRowsFor(cfg, requestIds);
147
+ const gate = await runGate(api, 'batchAttention', rows, dataKey);
148
+ const result = await api.post('/api/workflow/requestAttention/saveAttention', {
149
+ src: 'batchadd',
150
+ requestids: requestIds.join(','),
151
+ ids: attentionId + ',',
152
+ });
153
+ return { requestIds, attentionTypes: types, attentionId, gate, result };
154
+ }
package/dist/todocc.js ADDED
@@ -0,0 +1,128 @@
1
+ import { createApiFromProfile } from './client.js';
2
+ import { todayStr, nowTimeStr } from './util.js';
3
+ /**
4
+ * 抄送/批注操作。POST remarkOperation(isremark=9, actiontype=remarkOperation)。
5
+ * 先 loadForm 拿签名,再 remarkOperation 抄送。
6
+ *
7
+ * 修复(P0-4):submit_token key 旧硬编码 `1_<rid>_...`(前缀=1=sysadmin),非 sysadmin 取不到 token;
8
+ * 改用 `p.f_weaver_belongto_userid` 动态拼 key,兜底旧 `1_` 前缀。
9
+ */
10
+ export async function ccTodo(cfg, opts) {
11
+ const { requestId } = opts;
12
+ const remarkText = opts.remark ?? '抄送';
13
+ const remarkHtml = remarkText.split(/\r?\n/).map((l) => `<p>${l}</p>`).join('');
14
+ const api = await createApiFromProfile(cfg);
15
+ // 第一步 loadForm → 拿签名
16
+ const r0 = await api.post('/api/workflow/reqform/loadForm', { iscreate: '0', requestid: requestId });
17
+ const j0 = r0.json ?? {};
18
+ const p0 = j0.params ?? {};
19
+ const sp0 = j0.submitParams ?? {};
20
+ const wfId = String(p0.workflowid ?? '');
21
+ const nodeId = String(p0.nodeid ?? '');
22
+ let p = p0;
23
+ let sp = sp0;
24
+ if (wfId && nodeId) {
25
+ const r1 = await api.post('/api/workflow/reqform/loadForm', { iscreate: '0', requestid: requestId, workflowid: wfId, nodeid: nodeId });
26
+ const j1 = r1.json ?? {};
27
+ p = j1.params ?? p0;
28
+ sp = j1.submitParams ?? sp0;
29
+ }
30
+ const userId = String(p.f_weaver_belongto_userid ?? '1');
31
+ const submitTokenKey = `${userId}_${requestId}_request_submit_token`;
32
+ const submitToken = String(sp[submitTokenKey] ?? sp[`1_${requestId}_request_submit_token`] ?? '');
33
+ const sigDecoded = Buffer.from(String(p.signatureAttributesStr ?? ''), 'base64').toString('utf8');
34
+ const lastOpDate = (sigDecoded.match(/lastOperateDate=([^|]*)/)?.[1] ?? '').trim();
35
+ const lastOpTime = (sigDecoded.match(/lastOperateTime=([^|]*)/)?.[1] ?? '').trim();
36
+ const todayStrVal = todayStr();
37
+ const timeStr = nowTimeStr();
38
+ const body = {
39
+ formid: String(sp.formid ?? p.formid ?? ''),
40
+ isSubmitDirectNode: '',
41
+ openByDefaultBrowser: '',
42
+ iscreate: '0',
43
+ creatertype: '0',
44
+ isdialog: '1',
45
+ lastOperateDate: lastOpDate,
46
+ createdoc: '',
47
+ [submitTokenKey]: submitToken,
48
+ nodeid: nodeId,
49
+ workflowid: wfId,
50
+ isbill: String(sp.isbill ?? p.isbill ?? '1'),
51
+ authStr: String(p.authStr ?? ''),
52
+ 'f_weaver_belongto_userid': userId,
53
+ currenttime: timeStr,
54
+ 'f_weaver_belongto_usertype': '0',
55
+ agentorByAgentId: '-1',
56
+ isMultiDoc: '',
57
+ inputcheck: '',
58
+ comemessage: '',
59
+ lastOperateTime: lastOpTime,
60
+ temphasUseTempletSucceed: '',
61
+ workflowRequestLogId: '',
62
+ edesign_layout: '',
63
+ requestid: requestId,
64
+ isremark: '9',
65
+ creater: String(p.creater ?? userId),
66
+ htmlfieldids: '',
67
+ SubmitToNodeid: '',
68
+ isCptwf: 'false',
69
+ isovertime: '',
70
+ agentType: '0',
71
+ authSignatureStr: String(p.authSignatureStr ?? ''),
72
+ nodetype: String(p.nodetype ?? sp.nodetype ?? '1'),
73
+ needoutprint: '',
74
+ lastOperator: '1',
75
+ topage: '',
76
+ isFormSignature: '0',
77
+ remindTypes: '',
78
+ fromFlowDoc: '',
79
+ RejectNodes: '',
80
+ linkageUUID: String(sp.linkageUUID ?? ''),
81
+ billid: String(sp.billid ?? ''),
82
+ lastnodeid: '',
83
+ uploadType: '',
84
+ isSignMustInput: '',
85
+ RejectToNodeid: '',
86
+ isWorkflowDoc: 'false',
87
+ src: 'save',
88
+ annexmaxUploadImageSize: '',
89
+ takisremark: '0',
90
+ workflowtype: String(sp.workflowtype ?? p.workflowtype ?? ''),
91
+ remarkLocation: '',
92
+ needcheck: '',
93
+ needcheckLock: 'false',
94
+ selectfieldvalue: '',
95
+ RejectToType: '',
96
+ currentdate: todayStrVal,
97
+ needwfback: '1',
98
+ remark: remarkHtml,
99
+ 'field-annexupload': '',
100
+ signdocids: '',
101
+ signworkflowids: '',
102
+ annexdocids: '',
103
+ annexdocinfos: '',
104
+ handWrittenSign: '',
105
+ isOdocRequest: '0',
106
+ enableIntervenor: '',
107
+ verifyRequiredRange: 'field-9999,',
108
+ linkageUnFinishedKey: '',
109
+ remarkquote: remarkHtml,
110
+ actiontype: 'remarkOperation',
111
+ isFirstSubmit: '0',
112
+ existChangeRange: '',
113
+ requestname: String(p.requestname ?? ''),
114
+ requestlevel: '0',
115
+ chatsType: '-1',
116
+ messageType: '-1',
117
+ 'field-9': '0',
118
+ 'field-10': '4',
119
+ mainFieldUnEmptyCount: '0',
120
+ detailFieldUnEmptyCount: '0',
121
+ signatureAttributesStr: String(p.signatureAttributesStr ?? ''),
122
+ signatureSecretKey: String(p.signatureSecretKey ?? ''),
123
+ selectNextFlow: '0',
124
+ openDataVerify: '0',
125
+ wfTestStr: '',
126
+ };
127
+ return api.post('/api/workflow/reqform/remarkOperation', body);
128
+ }
@@ -0,0 +1,66 @@
1
+ import { createApiFromProfile } from './client.js';
2
+ import { resolveReceivers } from './wfreceiver.js';
3
+ /**
4
+ * 转发/传阅。链路(与意见征询同构,流程抓包8.har/7.har 实证 forwardflag=1/4 走同一 ForwardDialog):
5
+ * loadForm(取 workflowid/nodeid/操作人 userid) → signInput(forwardflag 预载) →
6
+ * remarkOperate(operate=save, forwardflag, field5=接收人) → {"success":true,...}。
7
+ *
8
+ * 修复(0.5.1):旧实现走 requestOperation(src=submit, forwardOperators=接收人) 且不带 forwardflag,
9
+ * 被服务端当成"批准"——用户实测转发被记录成"批准"(签字意见写"转发"然后点通过)。
10
+ * 前端 bundle 实证 doReviewE9(转发 forwardflag="1") / doChuanyue(传阅 forwardflag="4")
11
+ * 打开同一 ForwardDialog,提交统一走 signInput → remarkOperate:
12
+ * · forward_token_<requestid>_<userid> 由客户端 Date.now() 生成,signInput 与 remarkOperate 须用同一值
13
+ * · signInput:IsSubmitedOpinion=1 & IsBeForwardTodo=1 & field5= 空
14
+ * · remarkOperate:operate=save & field5=接收人 & isSubmitSign/isChuanyue/isRemind
15
+ * · 转发(flag=1) 的 signInput 带 forwardOperators= 空串;传阅(flag=4) 不带 —— 与抓包一致
16
+ * · 传阅(flag=4) 的 isSubmitSign/isChuanyue/isRemind 为空串;转发(flag=1) 为 0 —— 与抓包一致
17
+ */
18
+ async function forwardOrCirculate(cfg, opts, forwardflag) {
19
+ const { requestId } = opts;
20
+ const remarkText = opts.remark ?? '';
21
+ const remarkHtml = remarkText.split(/\r?\n/).map((l) => `<p>${l}</p>`).join('');
22
+ const api = await createApiFromProfile(cfg);
23
+ const r0 = await api.post('/api/workflow/reqform/loadForm', { iscreate: '0', requestid: String(requestId) });
24
+ const p = (r0.json ?? {}).params ?? {};
25
+ const userId = String(p.f_weaver_belongto_userid ?? '1');
26
+ const userType = String(p.f_weaver_belongto_usertype ?? '0');
27
+ const workflowid = String(p.workflowid ?? '');
28
+ const nodeid = String(p.nodeid ?? '');
29
+ const field5 = await resolveReceivers(cfg, opts);
30
+ if (!field5)
31
+ throw new Error(`${forwardflag === '1' ? '转发' : '传阅'}需要目标人:传 --receiver <userId> 或 --receiver-name <姓名>`);
32
+ const tokenKey = `forward_token_${requestId}_${userId}`;
33
+ const token = String(Date.now());
34
+ const common = {
35
+ isShowReplyNotice: '0', takOverMins: '30', remark: remarkHtml,
36
+ workflowRequestLogId: '', forwardflag, requestid: String(requestId),
37
+ nodeid, workflowid, 'f_weaver_belongto_userid': userId, 'f_weaver_belongto_usertype': userType,
38
+ agentType: '0', agentorByAgentId: '-1', secLevelInfo: '[object Object]',
39
+ [tokenKey]: token,
40
+ };
41
+ // ① signInput 预载(forwardflag,field5 空)
42
+ const signBody = forwardflag === '1'
43
+ ? { ...common, field5: '', IsSubmitedOpinion: '1', IsBeForwardTodo: '1', forwardOperators: '' }
44
+ : { ...common, field5: '', IsSubmitedOpinion: '1', IsBeForwardTodo: '1' };
45
+ await api.post('/api/workflow/reqform/signInput', signBody);
46
+ // ② remarkOperate 提交(operate=save,field5=接收人)
47
+ const remFlags = forwardflag === '4' ? '' : '0';
48
+ return api.post('/api/workflow/reqform/remarkOperate', {
49
+ ...common, field5,
50
+ operate: 'save',
51
+ openByDefaultBrowser: '', IsBeForwardSubmitAlready: '', IsBeForwardAlready: '',
52
+ IsSubmitedOpinion: '', IsBeForwardTodo: '',
53
+ isSubmitSign: remFlags, isChuanyue: remFlags, isRemind: remFlags,
54
+ signworkflowids: '', signdocids: '', remarkLocation: '',
55
+ 'field-annexupload': '', 'field-annexupload-request': String(requestId),
56
+ handWrittenSign: '0', remindTypes: '',
57
+ });
58
+ }
59
+ /** 转发(分享给某人看,不改变审批人) */
60
+ export async function forwardTodo(cfg, opts) {
61
+ return forwardOrCirculate(cfg, opts, '1');
62
+ }
63
+ /** 传阅 */
64
+ export async function circulateTodo(cfg, opts) {
65
+ return forwardOrCirculate(cfg, opts, '4');
66
+ }