weaver-e9-assistant-cli 1.1.1 → 1.1.3

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/blog.js CHANGED
@@ -85,12 +85,14 @@ export function isBlogRecalled(item) {
85
85
  // ─── 微博首页列表(支持组织视图)─────────────────────────
86
86
  /**
87
87
  * 获取微博首页列表,支持按 scope 查看不同范围。
88
- * scope 映射:
89
- * 全部(-1) / 关注(-2) / 共享(-3) / 下级(-5)
88
+ * scope 映射(groupId,抓包实证不同 scope 走不同接口):
89
+ * 全部关注/关注/下级(-1)、特别关注(-5) → getBlogHomepageList
90
+ * 未读(-2) → getMyBlogHomepageNew
91
+ * 分享给我/共享(-3) → getWeiboPageShareToMeList
90
92
  */
91
93
  export async function getBlogHomepageList(cfg, opts = {}) {
92
94
  const api = await createApiFromProfile(cfg);
93
- const scopeMap = { '全部': '-1', '关注': '-2', '共享': '-3', '下级': '-5', 'subordinate': '-5', 'all': '-1', 'attention': '-2', 'share': '-3', 'sub': '-5' };
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' };
94
96
  const groupId = scopeMap[opts.scope] ?? opts.scope ?? '-1';
95
97
  const endDate = opts.endDate ?? todayStr();
96
98
  // 默认查最近 30 天
@@ -106,11 +108,34 @@ export async function getBlogHomepageList(cfg, opts = {}) {
106
108
  currentPage: opts.page ?? 1,
107
109
  pageSize: 10,
108
110
  };
109
- const r = await api.get('/api/blog/base/getBlogHomepageList', params);
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);
110
115
  const all = extractBlogItems(r.json);
111
116
  const items = all.filter((it) => it.id || it.content);
112
117
  return { scope: opts.scope ?? '全部', groupId, startDate, endDate, page: opts.page ?? 1, total: r.json.total ?? items.length, items, raw: r.json };
113
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
+
114
139
  // ─── 微博搜索 ────────────────────────────────────────────
115
140
  export async function searchMyBlog(cfg, opts = {}) {
116
141
  const api = await createApiFromProfile(cfg);
@@ -245,6 +270,7 @@ export async function getBlogWorkIndex(cfg) {
245
270
  const r = await api.get('/api/blog/base/getBlogWorkIndex');
246
271
  const j = r.json || {};
247
272
  return {
273
+ schedule: { name: j.schedule?.name, title: j.schedule?.scheduleTitle, index: j.schedule?.scheduleIndex },
248
274
  mood: { name: j.mood?.name, title: j.mood?.moodIndexTitle, index: j.mood?.moodIndex },
249
275
  work: { name: j.work?.name, title: j.work?.workIndexTitle, index: j.work?.workIndex },
250
276
  raw: j,
@@ -377,9 +403,13 @@ export async function getMsgRemindList(cfg) {
377
403
  const j = r.json || {};
378
404
  return {
379
405
  items: (j.remindList || []).map((it) => ({
380
- id: stringify(it.id), content: stringify(it.content),
381
- creator: stringify(it.creator), createDate: stringify(it.createDate),
382
- type: stringify(it.type),
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),
383
413
  // 关注申请专用字段(抓包实证):remindid=申请人 userid、relatedid=当前登录者、remindType=1
384
414
  remindid: stringify(it.remindid),
385
415
  relatedid: stringify(it.relatedid),
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import fs from 'node:fs';
4
4
  import { createHash } from 'node:crypto';
5
5
  import { loadConfig, resolveViewcondition, endpointFor, setConfigField } from './config.js';
6
- import { getWfList, getSmartTodoGroups, resolveWorkflowType } from './workflow.js';
6
+ import { getWfList, getOaTabGroups, resolveWorkflowType, CATEGORY_SCOPE } from './workflow.js';
7
7
  import { parseIdList, batchSubmit, batchForward, batchAttention, getAttentionTypes } from './todobatch.js';
8
8
  import { pageUrl, availablePages } from './pageopen.js';
9
9
  import { buildBlogMyUrl, buildCubeCardUrl, buildCubeSearchUrl, normalizeE9Url } from './links.js';
@@ -18,7 +18,7 @@ import { transferTodo } from './todotransfer.js';
18
18
  import { forwardTodo, circulateTodo } from './todoforward.js';
19
19
  import { ccTodo } from './todocc.js';
20
20
  import { askIdeaTodo } from './todoaskidea.js';
21
- import { getMyBlogList, getMyBlogAll, isBlogRecalled, createBlog, getBlogWorkIndex, getBlogMenuCountdata, getVisitorList, getBlogNote, getCurrentUserInfo, getMsgRemindList, getDefaultTemplate, getBlogAppList, getAtAllList, getBlogPageSearchCondition, getBlogHomepageList, searchMyBlog, getBlogPersonalInfo, sendRemindUnsubmit, addBlogReply, addOrDeleteZan, saveBlogNote, recallBlog, deleteBlogReply, attentionOpt, dealMsgRemind } from './blog.js';
21
+ import { getMyBlogList, getMyBlogAll, isBlogRecalled, createBlog, getBlogWorkIndex, getBlogMenuCountdata, getVisitorList, getBlogNote, getCurrentUserInfo, getMsgRemindList, getDefaultTemplate, getBlogAppList, getAtAllList, getBlogPageSearchCondition, getBlogHomepageList, searchMyBlog, getBlogPersonalInfo, sendRemindUnsubmit, addBlogReply, addOrDeleteZan, saveBlogNote, recallBlog, deleteBlogReply, attentionOpt, dealMsgRemind, getShareToMeUserList } from './blog.js';
22
22
  import { getCreateWfList, buildCreateWfUrl, buildRequestFormUrl, findCreateWfByName } from './wfcreate.js';
23
23
  import { describeFormFields } from './wfform.js';
24
24
  import { getStartableFields, prepareCreateSubmission, submitCreateRequest, resolveWorkflowId, getOdocNextNodes, odocFlowNext } from './wfstart.js';
@@ -28,9 +28,10 @@ import { searchWorkplan, createWorkplan, getMyCalendar, getAttentionData, getBas
28
28
  import { listOdocTodo, getOdocTodoCount, listOdocCreate } from './odoc.js';
29
29
  import { punch, getButtons, resolvePunchItem, signSecret, buildEncryptObj } from './kq.js';
30
30
  import { collectQueryableBusiness } from './report.js';
31
- import { listCube, getCubeCondition, getCubeInit, exportCubeExcel, getSearchColumnInfo, listCubeExcel, getCubeCountData, getCubeGroupData, getExpCardConfig, getModeBrowserOptions, getCardFields, submitCubeCard, deleteCubeData, printCubePdf, listCubeMenus, findCubeByName, fetchMenuMounts, previewCubeCard, fetchCubeRowsByIds, resolveCubeCondition, downloadCubeImportTemplate, importCubeExcel } from './cube.js';
31
+ import { listCube, getCubeCondition, getCubeInit, exportCubeExcel, getSearchColumnInfo, listCubeExcel, getCubeCountData, getCubeGroupData, getExpCardConfig, getModeBrowserOptions, getCardFields, submitCubeCard, deleteCubeData, printCubePdf, listCubeMenus, findCubeByName, fetchMenuMounts, previewCubeCard, fetchCubeRowsByIds, resolveCubeCondition, downloadCubeImportTemplate, importCubeExcel, assertEditableCardValues } from './cube.js';
32
32
  import { getCurrentUser, getCurrentUserIsAdmin } from './wfuser.js';
33
- import { getSubordinates, assertUserIdsInScope, assertNamesInScope } from './subordinate.js';
33
+ import { getSubordinates, assertUserIdsInScope, assertNamesInScope, isScopeEnforced } from './subordinate.js';
34
+ import { resolveHrmTab, getHrmTabs, getHrmConditions, resolveHrmCondition, hrmList, buildHrmCardUrl, findHrmOrgNode, applyResolvedPersonIds } from './hrm.js';
34
35
  import { parseArgs, flagStr, flagBool } from './args.js';
35
36
  import { todayStr } from './util.js';
36
37
  import { BOX_TOP, DIVIDER } from './render.js';
@@ -60,7 +61,7 @@ function usage() {
60
61
  ' e9 todo batch-attention --request-ids "1,2" [--attention-id <类型id>] [--yes] 批量关注(先预览,默认加入首个关注类型)',
61
62
  ' e9 todo done [--page <n>] [--keyword <text>] [--workflow <id>] [--creator <id>] [--sort <spec>] [--sub-category <tab>] [--json] [--raw] 已办列表',
62
63
  ' e9 todo mine [--page <n>] [--keyword <text>] [--workflow <id>] [--creator <id>] [--sort <spec>] [--sub-category <tab>] [--json] [--raw] 我发起的流程',
63
- ' e9 blog list [--date YYYY-MM-DD] [--scope <全部|关注|共享|下级>] [--page <n>] [--json] [--raw]',
64
+ ' e9 blog list [--date YYYY-MM-DD] [--scope <全部|关注|下级|未读|共享|分享给我|特别关注>] [--page <n>] [--json] [--raw]',
64
65
  ' e9 blog search --content <关键词> [--start-date <YYYY-MM-DD>] [--end-date <YYYY-MM-DD>] [--page <n>] 搜索我的微博',
65
66
  ' e9 blog view --user-id <id> 查看某人微博主页',
66
67
  ' e9 blog remind --user-id <id> [--workdate YYYY-MM-DD] [--yes] 催办(提醒某人写日报,默认今天)',
@@ -68,6 +69,7 @@ function usage() {
68
69
  ' e9 blog like --id <微博id> [--owner <作者userid>] [--yes] 点赞/取消点赞(默认 dry-run;跨账号加 --owner)',
69
70
  ' e9 blog follow --id <目标userid> [--yes] 发起微博关注申请(对方同意后互关可访问主页)',
70
71
  ' e9 blog agree-follow --id <申请人userid> [--yes] 同意对方的关注申请',
72
+ ' e9 blog share-users 分享给我的人(用户列表)',
71
73
  ' e9 blog check [--date YYYY-MM-DD] 查某天是否已写日报(按 isReCalled 区分正常发布/已撤回);默认今天,无正常发布记录退出码 1',
72
74
  ' e9 report collect [--date YYYY-MM-DD] [--json] 汇总当前环境可查询业务数据(非操作审计)',
73
75
  ' e9 blog create --content "<文本>" [--content-file <路径>] [--share-type N] [--date YYYY-MM-DD] [--discuss-id <id>] [--yes]',
@@ -144,6 +146,10 @@ function usage() {
144
146
  ' e9 cube find <名称> 按名模糊匹配台账(列候选消歧,返回 customid+mode+app,供 list/create 等用)',
145
147
  ' e9 cube find <名称> --tree-detail 展开 tree 测试类台账的全部树节点路径(默认只显首条菜单路径)',
146
148
  ' e9 subordinate list [--name <姓名>] [--all-level] [--page <n>] [--json] [--raw] [--refresh] 查询我的下属(默认直接下属;--all-level 全部层级)',
149
+ ' e9 hrm tabs 列出人事搜索范围(全部人员/同部门/我的下属)',
150
+ ' e9 hrm conditions [--tab <全部人员|同部门|我的下属>] 列出该范围可用搜索条件(label→domkey)',
151
+ ' e9 hrm search [--tab <全部人员|同部门|我的下属>] [--name <姓名>] [--condition "岗位=经理;性别=男"]',
152
+ ' [--dept <部门名|id>] [--subcompany <分部名|id>] [--all-level] [--page <n>] [--json] [--raw] 人事搜索(姓名=详情卡片超链接)',
147
153
  ' e9 search <名称> 统一搜台账+菜单+流程(只说名字、不知归属时用,避免猜错域)',
148
154
  ' e9 config 查看当前配置来源与状态',
149
155
  ' e9 config set <field> <value> 设置配置(field: baseUrl/cookie/username/menuIds/...;首次用 set baseUrl)',
@@ -339,7 +345,7 @@ function escapeLinkText(text) {
339
345
  return String(text).replace(/([\[\]()\\])/g, '\\$1');
340
346
  }
341
347
  /** 统一拼各域 list/search 项的 markdown 超链接;无 id/无可链目标时回退纯文本。
342
- * domain: 'todo'|'odoc'|'create'|'blog'|'email'|'cube'|'workplan'
348
+ * domain: 'todo'|'odoc'|'create'|'blog'|'email'|'cube'|'workplan'|'hrm'
343
349
  * text: 展示文本(调用方已计算好回退值);ctx: cube 需 {modeId,formId,customid} */
344
350
  function hyperlinkFor(domain, item, base, text, ctx = {}) {
345
351
  const meta = linkMetaFor(domain, item, base, ctx);
@@ -388,6 +394,12 @@ function linkMetaFor(domain, item, base, ctx = {}) {
388
394
  url = `${b}/spa/workplan/static/index.html#/main/wp/myWorkPlan`;
389
395
  urlType = url ? 'list-generic' : '';
390
396
  break;
397
+ case 'hrm':
398
+ // 人事详情卡片(真机验证通过):SPA hash 路由 #/main/hrm/card/cardInfo/<hrmresource.id>
399
+ if (item?.id)
400
+ url = buildHrmCardUrl(b, item.id);
401
+ urlType = url ? 'detail' : '';
402
+ break;
391
403
  }
392
404
  return {
393
405
  url: url || null,
@@ -1522,8 +1534,9 @@ async function runBlog(args) {
1522
1534
  return;
1523
1535
  }
1524
1536
  console.log(`${BOX_TOP}`);
1525
- console.log(`E9 微博 工作指数 & 心情指数`);
1537
+ console.log(`E9 微博 考勤指数 & 工作指数 & 心情指数`);
1526
1538
  console.log(DIVIDER);
1539
+ console.log(` 考勤指数: ${wi.schedule.index ?? '?'} ${wi.schedule.title ?? ''}`);
1527
1540
  console.log(` 工作指数: ${wi.work.index ?? '?'} ${wi.work.title ?? ''}`);
1528
1541
  console.log(` 心情指数: ${wi.mood.index ?? '?'} ${wi.mood.title ?? ''}`);
1529
1542
  console.log(DIVIDER);
@@ -1696,9 +1709,12 @@ async function runBlog(args) {
1696
1709
  console.log('(无提醒)');
1697
1710
  return;
1698
1711
  }
1712
+ const TYPE_LABEL = { '1': '关注申请', '8': '系统提醒' };
1699
1713
  for (let i = 0; i < r.items.length; i++) {
1700
1714
  const it = r.items[i];
1701
- console.log(`${i + 1}. [${it.type ?? ''}] ${it.content ?? ''} ${it.creator ?? ''} ${it.createDate ?? ''}`);
1715
+ const label = TYPE_LABEL[it.remindType] ?? (it.type ? `类型${it.type}` : '提醒');
1716
+ const time = [it.createDate, it.createTime].filter(Boolean).join(' ');
1717
+ console.log(`${i + 1}. [${label}] ${it.content ?? ''}${time ? ` (${time})` : ''}`);
1702
1718
  }
1703
1719
  return;
1704
1720
  }
@@ -1763,6 +1779,24 @@ async function runBlog(args) {
1763
1779
  }
1764
1780
  return;
1765
1781
  }
1782
+ if (sub === 'share-users') {
1783
+ const r = await getShareToMeUserList(cfg);
1784
+ if (args.flags.json) {
1785
+ console.log(JSON.stringify(r, null, 2));
1786
+ return;
1787
+ }
1788
+ console.log(`E9 微博 分享给我的人 共 ${r.count ?? r.items.length} 人`);
1789
+ if (r.items.length === 0) {
1790
+ console.log('(无人分享给我)');
1791
+ return;
1792
+ }
1793
+ for (let i = 0; i < r.items.length; i++) {
1794
+ const it = r.items[i];
1795
+ const meta = [it.deptName, it.jobtitle, it.isattention ? '已关注' : null].filter(Boolean).join(' | ');
1796
+ console.log(`${i + 1}. ${it.username ?? '(未知)'}${meta ? ` (${meta})` : ''}`);
1797
+ }
1798
+ return;
1799
+ }
1766
1800
  if (sub === 'search-fields') {
1767
1801
  const r = await getBlogPageSearchCondition(cfg);
1768
1802
  if (args.flags.json) {
@@ -1794,7 +1828,7 @@ async function runBlog(args) {
1794
1828
  console.log(JSON.stringify(r.template, null, 2));
1795
1829
  return;
1796
1830
  }
1797
- console.error(`blog 暂只支持 list / search / view / remind / reply / like / create / check / stats / note / recall / delete-reply / follow / agree-follow / user-info / reminders / apps / at-list / visitors / search-fields / template,收到: ${sub}`);
1831
+ console.error(`blog 暂只支持 list / search / view / remind / reply / like / create / check / stats / note / recall / delete-reply / follow / agree-follow / user-info / reminders / apps / at-list / visitors / share-users / search-fields / template,收到: ${sub}`);
1798
1832
  process.exit(1);
1799
1833
  }
1800
1834
  async function runWorkplan(args) {
@@ -3409,6 +3443,12 @@ async function runCube(args) {
3409
3443
  catch {
3410
3444
  curFields = null;
3411
3445
  }
3446
+ // ★字段属性硬规则:只读字段禁止填值,编辑 dry-run 也要拦(真实提交由 submitCubeCard 兜底)。
3447
+ // 判据必须取「新建态」字段定义——curFields 带 billid 是查看态,isedit 恒为 false,会误伤全部字段。
3448
+ const defFields = (await getCardFields(cfg, { modeId, formId })).fields;
3449
+ assertEditableCardValues(defFields, values);
3450
+ if (files.length)
3451
+ assertEditableCardValues(defFields, Object.fromEntries(files.map((f) => [String(f.fieldId), '1'])));
3412
3452
  // 查看态不带值时:list 反查该条记录的行值作对比兜底
3413
3453
  if (!(curFields && curFields.fields.some((f) => f.value !== undefined && f.value !== null && f.value !== ''))) {
3414
3454
  try {
@@ -3655,7 +3695,8 @@ async function runCube(args) {
3655
3695
  if (condRes.unresolved.length)
3656
3696
  printConditionUnresolved(condRes.unresolved);
3657
3697
  // ★越权防护(fail-closed):条件中存在无法确认是否按人过滤的条件(定义读取失败/未知 domkey)→ 硬拒绝
3658
- if (condRes.unverifiablePerson)
3698
+ // 策略未启用(默认)时不拦,仅保留代码路径。
3699
+ if (isScopeEnforced() && condRes.unverifiablePerson)
3659
3700
  throw new Error('拒绝越权查询:台账查询条件无法确认是否按人过滤(条件定义读取失败或含未识别字段)。为防越权,本次查询已拒绝,请稍后重试或改用中文字段名。');
3660
3701
  // ★越权防护:人员条件(填报人/审批人/报销人/经手人等)目标必须 ∈ {本人 + 全部层级下属}
3661
3702
  if (condRes.personConditions?.length)
@@ -3683,7 +3724,8 @@ async function runCube(args) {
3683
3724
  if (condRes.unresolved.length)
3684
3725
  printConditionUnresolved(condRes.unresolved);
3685
3726
  // ★越权防护(fail-closed):条件中存在无法确认是否按人过滤的条件(定义读取失败/未知 domkey)→ 硬拒绝
3686
- if (condRes.unverifiablePerson)
3727
+ // 策略未启用(默认)时不拦,仅保留代码路径。
3728
+ if (isScopeEnforced() && condRes.unverifiablePerson)
3687
3729
  throw new Error('拒绝越权查询:台账查询条件无法确认是否按人过滤(条件定义读取失败或含未识别字段)。为防越权,本次查询已拒绝,请稍后重试或改用中文字段名。');
3688
3730
  // ★越权防护:人员条件(填报人/审批人/报销人/经手人等)目标必须 ∈ {本人 + 全部层级下属}
3689
3731
  if (condRes.personConditions?.length)
@@ -3759,6 +3801,152 @@ async function runSubordinate(args) {
3759
3801
  console.log(` ${meta}`);
3760
3802
  }
3761
3803
  }
3804
+
3805
+ // ─── 组织人事(HRM)搜索 ─────────────────────────────────
3806
+
3807
+ /** 组织节点名称 → id(用于 --dept/--subcompany 名称定位;纯数字原样直传)。 */
3808
+ async function resolveHrmOrgValue(cfg, keyword, { matchType, flag }) {
3809
+ const node = await findHrmOrgNode(cfg, keyword, { matchType });
3810
+ if (!node)
3811
+ throw new Error(`${flag} 无法在组织树中定位「${keyword}」(type=${matchType})。可先用数字 ID 直传,或确认名称在人事组织树中存在。`);
3812
+ return node.id;
3813
+ }
3814
+
3815
+ /** 未识别条件的提示(含相近候选),与 cube 的 printConditionUnresolved 一致——不给静默放空。 */
3816
+ function printHrmConditionUnresolved(unresolved) {
3817
+ for (const u of unresolved) {
3818
+ const cands = (u.candidates ?? []).join('、');
3819
+ console.error(`⚠ 人事条件「${u.label}」未匹配到搜索条件字段${cands ? `。相近字段候选: ${cands}` : ''}(可用 e9 hrm conditions 查看全部可用条件名)。`);
3820
+ }
3821
+ process.exit(1);
3822
+ }
3823
+
3824
+ async function runHrm(args) {
3825
+ const sub = args._[1] ?? 'search';
3826
+ const cfg = loadConfig(flagStr(args, 'config'));
3827
+ if (sub === 'tabs') {
3828
+ const r = await getHrmTabs(cfg);
3829
+ if (args.flags.json) {
3830
+ console.log(JSON.stringify({ tabs: r.tabs }, null, 2));
3831
+ return;
3832
+ }
3833
+ console.log('E9 人事搜索范围:');
3834
+ if (r.tabs.length === 0) {
3835
+ console.log('(无可用范围,可能无组织人事查看权限)');
3836
+ return;
3837
+ }
3838
+ for (const t of r.tabs)
3839
+ console.log(` ${t.title} (${t.typeid})`);
3840
+ return;
3841
+ }
3842
+ if (sub === 'conditions') {
3843
+ const tab = flagStr(args, 'tab') ?? '全部人员';
3844
+ const mouldid = resolveHrmTab(tab);
3845
+ const r = await getHrmConditions(cfg, { mouldid });
3846
+ if (args.flags.json) {
3847
+ console.log(JSON.stringify({ tab, mouldid, groups: r.groups }, null, 2));
3848
+ return;
3849
+ }
3850
+ console.log(`E9 人事搜索条件 [${tab}]:`);
3851
+ if (r.groups.length === 0) {
3852
+ console.log('(无可用条件)');
3853
+ return;
3854
+ }
3855
+ for (const g of r.groups) {
3856
+ console.log(` ${g.title}:`);
3857
+ for (const it of g.items)
3858
+ console.log(` - ${it.label} (${it.domkey}${it.conditionType ? `, ${it.conditionType}` : ''})`);
3859
+ }
3860
+ return;
3861
+ }
3862
+ if (sub !== 'search') {
3863
+ console.error(`hrm 子命令: tabs/conditions/search,收到: ${sub}`);
3864
+ process.exit(1);
3865
+ }
3866
+ const tab = flagStr(args, 'tab') ?? '全部人员';
3867
+ const tabkey = resolveHrmTab(tab);
3868
+ const page = Number(flagStr(args, 'page') ?? '1') || 1;
3869
+ const allLevel = flagBool(args, 'all-level');
3870
+ const name = flagStr(args, 'name');
3871
+ const dept = flagStr(args, 'dept');
3872
+ const subcompany = flagStr(args, 'subcompany');
3873
+ const spec = flagStr(args, 'condition') ?? '';
3874
+
3875
+ const values = {};
3876
+ // ★越权防护:按名过滤目标必须 ∈ {本人 + 全部层级下属}(与 subordinate/todo/cube 一致:默认停用 no-op)
3877
+ if (name) {
3878
+ await assertNamesInScope(cfg, [name], { domain: 'hrm' });
3879
+ values.resourcename = name;
3880
+ }
3881
+ // --dept/--subcompany:数字直传 domkey;名称走组织树 BFS 定位(部门 type 2 / 分部 type 1)
3882
+ if (dept)
3883
+ values.departmentid = /^\d+$/.test(dept) ? dept : await resolveHrmOrgValue(cfg, dept, { matchType: '2', flag: '--dept' });
3884
+ if (subcompany)
3885
+ values.subcompanyid1 = /^\d+$/.test(subcompany) ? subcompany : await resolveHrmOrgValue(cfg, subcompany, { matchType: '1', flag: '--subcompany' });
3886
+ // --condition 中文条件(label→domkey 自动映射)
3887
+ const condRes = await resolveHrmCondition(cfg, { mouldid: tabkey, condition: spec });
3888
+ if (condRes.unresolved.length)
3889
+ printHrmConditionUnresolved(condRes.unresolved);
3890
+ // ★越权防护(fail-closed):无法确认是否按人过滤的条件 → 硬拒绝;策略未启用(默认)不拦,仅保留代码路径
3891
+ if (isScopeEnforced() && condRes.unverifiablePerson)
3892
+ throw new Error('拒绝越权查询:人事搜索条件无法确认是否按人过滤(条件定义读取失败或含未识别字段)。为防越权,本次查询已拒绝,请稍后重试或改用中文字段名。');
3893
+ // ★越权防护:按人条件(姓名/直接上级/助理)目标必须 ∈ {本人 + 全部层级下属}
3894
+ // assertNamesInScope 同时返回解析后的 uid(blog at-list 硬规则唯一入口)。
3895
+ // ★E9 实测校准(2026-09-20 e9pc):姓名=INPUT 认姓名文本;直接上级/助理=BROWSER 人字段认 **hrmresource id**
3896
+ // (=userid,本人 7111 验证 manager=7111 → 9 个直接下属;manager=杨文元sh → 0)。故 BROWSER 人字段值回填为 uid。
3897
+ if (condRes.personConditions?.length) {
3898
+ const resolvedIds = await assertNamesInScope(cfg, condRes.personConditions.map((p) => p.value), { domain: 'hrm' });
3899
+ condRes.conditionValues = applyResolvedPersonIds(condRes.conditionValues, condRes.personConditions, resolvedIds);
3900
+ }
3901
+ Object.assign(values, condRes.conditionValues);
3902
+ // ★E9 实测校准:--condition 里的 部门/分部 已由 resolveHrmCondition 重映射为 departmentid/subcompanyid1
3903
+ // (条件定义 domkey=department/subcompany 不是搜索接口认的参数);名称值在此经组织树解析为 id(同 --dept/--subcompany 路径)。
3904
+ for (const [key, matchType, label] of [['departmentid', '2', '部门'], ['subcompanyid1', '1', '分部']]) {
3905
+ const raw = values[key];
3906
+ if (raw != null && raw !== '' && !/^\d+$/.test(String(raw)))
3907
+ values[key] = await resolveHrmOrgValue(cfg, String(raw), { matchType, flag: `--condition "${label}=${raw}"` });
3908
+ }
3909
+
3910
+ const r = await hrmList(cfg, { tabkey, values, showAllLevel: allLevel, page });
3911
+ const base = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
3912
+ if (args.flags.raw) {
3913
+ console.error('===== RAW getHrmSearchResult =====');
3914
+ console.error(JSON.stringify(r.raw.search, null, 2));
3915
+ console.error('===== RAW datas =====');
3916
+ console.error(JSON.stringify(r.raw.datas, null, 2));
3917
+ console.error('===== RAW counts =====');
3918
+ console.error(JSON.stringify(r.raw.counts, null, 2));
3919
+ console.error('===== 解析结果 =====');
3920
+ }
3921
+ if (args.flags.json) {
3922
+ console.log(JSON.stringify({
3923
+ tab, tabkey, page,
3924
+ pageSize: r.pageSize, total: r.total, count: r.count,
3925
+ sessionkey: r.sessionkey,
3926
+ columns: r.columns.map((c) => ({ dataIndex: c.dataIndex, title: c.title })),
3927
+ items: r.items.map((it) => withLink('hrm', { id: it.id, name: it.name, fields: it.fields.reduce((acc, f) => { acc[f.dataIndex] = f.value; return acc; }, {}) }, base)),
3928
+ }, null, 2));
3929
+ return;
3930
+ }
3931
+ console.log(`E9 人事 [${tab}]${allLevel ? '(含子部门)' : ''} 第 ${page} 页${r.pageSize != null ? ` · 每页 ${r.pageSize} 条` : ''}${r.total != null ? ` / 共 ${r.total} 人` : ''} 当前 ${r.count} 人`);
3932
+ if (r.count === 0) {
3933
+ console.log('(无匹配人员)');
3934
+ return;
3935
+ }
3936
+ const cols = (r.columns || []).filter((c) => c.dataIndex && !['id', 'randomField0', 'randomFieldId'].includes(c.dataIndex));
3937
+ if (cols.length)
3938
+ console.log(`字段(${cols.length}): ${cols.map((c) => c.title ?? c.dataIndex).join(' | ')}`);
3939
+ for (let i = 0; i < r.items.length; i++) {
3940
+ const it = r.items[i];
3941
+ const head = hyperlinkFor('hrm', it, base, it.name || '(无姓名)');
3942
+ console.log(`${i + 1}. ${head} [id=${it.id || ''}]`);
3943
+ // 行 meta 去掉姓名(head 已显示)与 id 类列,避免重复
3944
+ const meta = it.fields.filter((f) => !['lastname', 'id', 'randomField0', 'randomFieldId'].includes(f.dataIndex)).slice(0, 8).map((f) => `${f.title}=${f.value}`).join(' | ');
3945
+ if (meta)
3946
+ console.log(` ${meta}`);
3947
+ }
3948
+ }
3949
+
3762
3950
  function printTodoItems(items, base, { indent = '', max = 10 } = {}) {
3763
3951
  const visible = items.slice(0, max);
3764
3952
  for (let i = 0; i < visible.length; i++) {
@@ -3848,6 +4036,7 @@ async function runReport(args) {
3848
4036
  console.log(`- 未查询成功的来源:${report.failures.map((item) => item.index).join(', ')}`);
3849
4037
  }
3850
4038
 
4039
+
3851
4040
  async function main() {
3852
4041
  const args = parseArgs(process.argv.slice(2));
3853
4042
  if (args.flags.help || args.flags.h) {
@@ -3857,7 +4046,7 @@ async function main() {
3857
4046
  const cmd = args._[0] ?? 'todo';
3858
4047
  if (cmd === 'version') {
3859
4048
  const buildHash = createHash('sha256').update(fs.readFileSync(new URL(import.meta.url))).digest('hex').slice(0, 12);
3860
- console.log(JSON.stringify({ name: 'weaver-e9-assistant-cli', version: '1.1.1', buildHash }, null, 2));
4049
+ console.log(JSON.stringify({ name: 'weaver-e9-assistant-cli', version: '1.1.3', buildHash }, null, 2));
3861
4050
  return;
3862
4051
  }
3863
4052
  if (cmd === 'whoami') {
@@ -3916,6 +4105,10 @@ async function main() {
3916
4105
  await runSubordinate(args);
3917
4106
  return;
3918
4107
  }
4108
+ if (cmd === 'hrm') {
4109
+ await runHrm(args);
4110
+ return;
4111
+ }
3919
4112
  if (cmd !== 'todo') {
3920
4113
  console.error(`未知命令: ${cmd}\n\n${usage()}`);
3921
4114
  process.exit(1);
@@ -4061,33 +4254,37 @@ async function main() {
4061
4254
  debug: queryBool('debug'),
4062
4255
  excludeCreatorId,
4063
4256
  };
4064
- // 默认概览优先展示“我需要做什么”,而不是把转发和关注与待处理混在数千条列表里。
4065
- const shouldGroup = category === 'todo' && !listOpts.subCategory && !tab && !listOpts.totalOnly && !queryBool('ungrouped') && !queryBool('raw');
4257
+ // 默认按 OA 二级分类(流程中心页签)分组展示——待办/已办/我发起通用;只列有数据的分类。
4258
+ const shouldGroup = !listOpts.subCategory && !tab && !listOpts.totalOnly && !queryBool('ungrouped') && !queryBool('raw');
4066
4259
  if (shouldGroup) {
4067
- const grouped = await getSmartTodoGroups(cfg, listOpts);
4260
+ const grouped = await getOaTabGroups(cfg, { ...listOpts, category });
4261
+ const scope = CATEGORY_SCOPE[category] ?? CATEGORY_SCOPE.todo;
4068
4262
  const groupedSnapshot = {
4069
- actionTotal: grouped.actionTotal,
4263
+ category,
4070
4264
  categoryTotals: grouped.categoryTotals,
4071
4265
  categoryTotalSum: grouped.categoryTotalSum,
4072
4266
  page: grouped.page,
4073
4267
  groups: grouped.groups.map((group) => ({
4074
- key: group.key,
4268
+ viewcondition: group.viewcondition,
4075
4269
  title: group.title,
4076
- total: group.result.total,
4270
+ total: group.total,
4077
4271
  items: group.items,
4078
4272
  raw: group.result.raw,
4079
4273
  })),
4080
4274
  };
4081
- const savedGroupedResult = persistSearchResult(groupedSnapshot, 'E9 待办智能概览');
4275
+ const savedGroupedResult = persistSearchResult(groupedSnapshot, `E9 ${scope.pageTitle}分类概览`);
4276
+ const base = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
4082
4277
  if (args.flags.json) {
4083
- const base = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
4084
- console.log(JSON.stringify({ page: grouped.page, actionTotal: grouped.actionTotal, categoryTotals: grouped.categoryTotals, groups: grouped.groups.map((g) => ({ key: g.key, title: g.title, total: g.result.total, items: g.items.map((item) => withLink('todo', item, base)) })) }, null, 2));
4278
+ console.log(JSON.stringify({ category, page: grouped.page, categoryTotals: grouped.categoryTotals, groups: grouped.groups.map((g) => ({ viewcondition: g.viewcondition, title: g.title, total: g.total, items: g.items.map((item) => withLink('todo', item, base)) })) }, null, 2));
4085
4279
  return;
4086
4280
  }
4087
- const base = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
4088
- console.log('E9 待办智能概览(按处理方式分组,避免重复提醒)');
4281
+ if (!grouped.groups.length) {
4282
+ console.log(`E9 ${scope.pageTitle}:各分类均无数据。`);
4283
+ return;
4284
+ }
4285
+ console.log(`E9 ${scope.pageTitle}(按 OA 分类页签分组,只列有数据的分类)`);
4089
4286
  for (const group of grouped.groups) {
4090
- console.log(`\n### ${group.title}(共 ${group.result.total ?? '?'} 条,第 ${group.result.page} 页显示前 10 条)`);
4287
+ console.log(`\n### ${group.title}(共 ${group.total ?? '?'} 条,第 ${group.result.page} 页显示前 10 条)`);
4091
4288
  if (group.items.length)
4092
4289
  printTodoItems(group.items, base);
4093
4290
  else
package/dist/cube.js CHANGED
@@ -1161,6 +1161,25 @@ function scanChangeValue(obj) {
1161
1161
  * opts: { modeId, formId, billid?, values:{id:value}, files:[{fieldId,filePath}], trigger?, pageexpandid?, cf? }
1162
1162
  * ⚠ 必须全程 cookie 鉴权(layoutBase + doSubmit 同 session),否则 doSubmit 返 repeat。
1163
1163
  */
1164
+ /**
1165
+ * ★字段属性硬规则:只读字段(isedit=false)禁止由用户/助手填值——不得绕过卡片字段属性自行填值。
1166
+ * 返回被违规填值的只读字段清单(空数组 = 无违规)。
1167
+ */
1168
+ export function findNonEditableCardFields(fields, values) {
1169
+ return (fields ?? []).filter((f) => {
1170
+ if (f.isedit)
1171
+ return false; // 可编辑字段允许填
1172
+ const id = String(f.id);
1173
+ return values?.[id] !== undefined || values?.[`field${id}`] !== undefined;
1174
+ });
1175
+ }
1176
+ /** 只读字段被填值 → 硬拒绝。 */
1177
+ export function assertEditableCardValues(fields, values) {
1178
+ const bad = findNonEditableCardFields(fields, values);
1179
+ if (bad.length)
1180
+ throw new Error(`拒绝填写只读字段:${bad.map((f) => `${f.label ?? f.name ?? f.id}`).join('、')}。台账卡片只允许填写「可编辑」字段,不得绕过字段属性自行填值。`);
1181
+ }
1182
+
1164
1183
  export async function submitCubeCard(cfg, opts = {}) {
1165
1184
  const ep = endpointFor(cfg);
1166
1185
  const cookie = ep.cookie;
@@ -1170,6 +1189,12 @@ export async function submitCubeCard(cfg, opts = {}) {
1170
1189
  const iscreate = billid ? '0' : '1';
1171
1190
  const cf = opts.cf ?? await getCardFields(cfg, { modeId, formId, billid });
1172
1191
  const { layoutid, cardToken, _key, uuid, fields } = cf;
1192
+ // ★字段属性硬规则:只读字段禁止填值。判据取「新建态」字段定义——带 billid 时 cf 是查看态,
1193
+ // 查看态字段 isedit 恒为 false(全部只读),不能作为可编辑性判据。
1194
+ const defFields = billid ? (await getCardFields(cfg, { modeId, formId })).fields : fields;
1195
+ assertEditableCardValues(defFields, opts.values);
1196
+ if (opts.files?.length)
1197
+ assertEditableCardValues(defFields, Object.fromEntries(opts.files.map((f) => [String(f.fieldId), '1'])));
1173
1198
  const t = String(Date.now());
1174
1199
  const cq = (o = {}) => ({ type: '1', modeId, formId, guid: 'card', t, _key, uuid, billid, ...o });
1175
1200
  // ① 卡片打开链路(cookie,同 session)
@@ -1242,6 +1267,12 @@ export async function previewCubeCard(cfg, opts = {}) {
1242
1267
  const values = { ...(opts.values ?? {}) };
1243
1268
  // 附件:只记录清单(dry-run 不上传),预览提示"--yes 时上传"
1244
1269
  const files = (opts.files ?? []).map((f) => ({ fieldId: String(f.fieldId), filePath: f.filePath }));
1270
+ // ★字段属性硬规则:只读字段禁止填值——dry-run 也要拦,避免「预览通过、提交才报错」。
1271
+ // 判据取「新建态」字段定义(billid 时 fields 是查看态,全为只读,不能作判据)。
1272
+ const defFields = billid ? (await getCardFields(cfg, { modeId, formId })).fields : fields;
1273
+ assertEditableCardValues(defFields, opts.values);
1274
+ if (files.length)
1275
+ assertEditableCardValues(defFields, Object.fromEntries(files.map((f) => [f.fieldId, '1'])));
1245
1276
  // 联动解析(真实接口、无写副作用;失败不阻断——resolveCubeLinkage 内部 catch 返回原 values)
1246
1277
  let linked = [];
1247
1278
  if (opts.trigger === undefined) {