weaver-work-cli 0.1.25 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +30 -30
  2. package/dist/cmd/root.js +1 -0
  3. package/dist/core/runtime-agent.js +19 -0
  4. package/dist/internal/e10/auth/commands.js +47 -27
  5. package/dist/internal/e10/auth/xiaoe.js +62 -43
  6. package/dist/internal/e10/context.js +5 -27
  7. package/dist/shortcuts/archive-oa/constants.js +66 -0
  8. package/dist/shortcuts/archive-oa/continuation.js +70 -0
  9. package/dist/shortcuts/archive-oa/errors.js +53 -0
  10. package/dist/shortcuts/archive-oa/host.js +157 -0
  11. package/dist/shortcuts/archive-oa/index.js +225 -0
  12. package/dist/shortcuts/archive-oa/manifest.js +62 -0
  13. package/dist/shortcuts/archive-oa/operations/borrow-car.js +159 -0
  14. package/dist/shortcuts/archive-oa/operations/borrow-url.js +108 -0
  15. package/dist/shortcuts/archive-oa/operations/confirm.js +48 -0
  16. package/dist/shortcuts/archive-oa/operations/contract.js +120 -0
  17. package/dist/shortcuts/archive-oa/operations/locate.js +50 -0
  18. package/dist/shortcuts/archive-oa/operations/registry.js +40 -0
  19. package/dist/shortcuts/archive-oa/operations/shared.js +181 -0
  20. package/dist/shortcuts/archive-oa/operations/types.js +45 -0
  21. package/dist/shortcuts/archive-oa/operations.js +13 -0
  22. package/dist/shortcuts/index.js +2 -0
  23. package/dist/shortcuts/invoice/host.js +2 -1
  24. package/dist/shortcuts/invoice/manifest.js +111 -0
  25. package/dist/shortcuts/invoice/operations/issuing-qys.js +361 -0
  26. package/dist/shortcuts/invoice/operations/issuing.js +5 -5
  27. package/dist/shortcuts/invoice/operations/registry.js +4 -0
  28. package/dist/shortcuts/workflow/endpoints.js +11 -0
  29. package/dist/shortcuts/workflow/host.js +2 -2
  30. package/dist/shortcuts/workflow/index.js +6 -36
  31. package/dist/shortcuts/workflow/operations/chatscene.js +270 -0
  32. package/dist/shortcuts/workflow/operations/form.js +899 -158
  33. package/dist/shortcuts/workflow/operations/read.js +111 -6
  34. package/dist/shortcuts/workflow/operations/sign-input.js +106 -0
  35. package/dist/shortcuts/workflow/operations/write.js +9 -1
  36. package/dist/shortcuts/workflow/stdout-compact.js +9 -1
  37. package/docs/archive-oa.md +79 -0
  38. package/docs/e10-auth.md +7 -6
  39. package/package.json +1 -1
  40. package/skill-template/business-info.json +16 -0
  41. package/skill-template/domains/archive-oa.md +19 -0
  42. package/skill-template/domains/yepiaotong.md +2 -2
  43. package/skills/weaver-e10-archive-oa/SKILL.md +159 -0
  44. package/skills/weaver-e10-archive-oa/references/archive-locate.md +49 -0
  45. package/skills/weaver-e10-archive-oa/references/borrow-car.md +76 -0
  46. package/skills/weaver-e10-archive-oa/references/borrow-flow.md +53 -0
  47. package/skills/weaver-e10-archive-oa/references/contract-search.md +63 -0
  48. package/skills/weaver-e10-archive-oa/references/disabled-capabilities.md +29 -0
  49. package/skills/weaver-e10-archive-oa/references/qys-contract-search.md +47 -0
  50. package/skills/weaver-e10-archive-oa/references/source-manifest.json +400 -0
  51. package/skills/weaver-e10-builder/SKILL.md +22 -0
  52. package/skills/weaver-e10-caizhilian/SKILL.md +37 -15
  53. package/skills/weaver-e10-caizhilian/references/agent-entry.md +7 -3
  54. package/skills/weaver-e10-caizhilian/references/collaboration.md +5 -3
  55. package/skills/weaver-e10-caizhilian/references/document.md +33 -11
  56. package/skills/weaver-e10-caizhilian/references/file-collection.md +4 -3
  57. package/skills/weaver-e10-caizhilian/references/file-upload.md +2 -3
  58. package/skills/weaver-e10-caizhilian/references/files.md +9 -3
  59. package/skills/weaver-e10-caizhilian/references/folder.md +48 -16
  60. package/skills/weaver-e10-caizhilian/references/setting-attach.md +13 -39
  61. package/skills/weaver-e10-caizhilian/references/setting-backup.md +13 -51
  62. package/skills/weaver-e10-caizhilian/references/setting-encrypt.md +20 -51
  63. package/skills/weaver-e10-caizhilian/references/setting-storage.md +82 -147
  64. package/skills/weaver-e10-caizhilian/references/withheld.md +20 -44
  65. package/skills/weaver-e10-shared/SKILL.md +22 -20
  66. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +14 -15
  67. package/skills/weaver-e10-shared/references/non-interactive-environment.md +10 -10
  68. package/skills/weaver-e10-workflow/references/source-manifest.json +32 -22
  69. package/skills/weaver-e10-workflow/references/workflow-create.md +68 -28
  70. package/skills/weaver-e10-yepiaotong/SKILL.md +10 -7
  71. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-examples.md +49 -1
  72. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-qys-ledger.md +66 -0
  73. package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +59 -36
  74. package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +54 -509
  75. package/skills/weaver-e10-yepiaotong/references/reim-examples.md +182 -345
  76. package/skills/weaver-e10-yepiaotong/references/reim-expense-subject-rules.md +4 -5
  77. package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +153 -345
  78. package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +5 -5
  79. package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +182 -154
  80. package/skills/weaver-e10-yepiaotong/references/source-manifest.json +96 -0
  81. package/skills/weaver-e10-yepiaotong/references/reim-fast-path.md +0 -119
@@ -0,0 +1,108 @@
1
+ import { BORROW_FLOW_ID, MAX_BORROW_ITEMS } from '../constants.js';
2
+ import { archiveOaValidationError } from '../errors.js';
3
+ import { normalizeListResult } from './shared.js';
4
+ import { READ, archiveOaSuccess, schemaOf, } from './types.js';
5
+ const CONTRACT_LIST_PATH = '/api/ebuilder/form/list/getListDatas';
6
+ /**
7
+ * 构造「发起借阅」流程创建页 URL。
8
+ *
9
+ * 与界面「确认发起」完全一致:`/sp/workflow/flowpage/fullCreate/<flowId>?fb=<fb>&data=<JSON>`。
10
+ * 只生成前端可打开的地址,不写数据、不触发流程,也不需要任何后台应用凭据。
11
+ */
12
+ export function buildBorrowStartUrl(baseUrl, fb, items) {
13
+ const root = baseUrl.replace(/\/+$/, '');
14
+ const data = encodeURIComponent(JSON.stringify(items));
15
+ return `${root}/sp/workflow/flowpage/fullCreate/${BORROW_FLOW_ID}?fb=${fb}&data=${data}`;
16
+ }
17
+ function toItem(row) {
18
+ return {
19
+ hth: String(row?.hth ?? ''),
20
+ /** 泛微用 htmc,契约锁用 qymc,二者取其一,缺失为空串。 */
21
+ htmc: String(row?.htmc ?? row?.qymc ?? ''),
22
+ yfhd: String(row?.yfhd ?? row?.yhhd ?? ''),
23
+ fph: String(row?.fph ?? ''),
24
+ };
25
+ }
26
+ export function createBorrowStartUrlOperation(profile) {
27
+ const name = profile.source === 'qys'
28
+ ? 'archive-oa.qys.borrow.start.url'
29
+ : 'archive-oa.borrow.start.url';
30
+ const listName = profile.source === 'qys' ? 'archive-oa.qys.borrow-car.list' : 'archive-oa.borrow-car.list';
31
+ return {
32
+ name,
33
+ resource: 'borrow',
34
+ method: 'start-url',
35
+ action: name,
36
+ summary: '按借阅车记录构造「发起借阅」流程创建页 URL(只读:不写数据、不触发流程、不需要后台应用凭据)',
37
+ capability: 'borrowCar',
38
+ risk: READ,
39
+ inputSchema: schemaOf({
40
+ contractNos: {
41
+ type: 'array',
42
+ items: { type: 'string' },
43
+ maxItems: MAX_BORROW_ITEMS,
44
+ description: `要借阅的合同号(取自借阅车),最多 ${MAX_BORROW_ITEMS} 个;不传表示使用整页借阅车记录`,
45
+ },
46
+ }, { description: '构造发起借阅流程创建页 URL' }),
47
+ handler: async (args, env) => {
48
+ const session = await env.host.getSession();
49
+ const response = await env.host.request({
50
+ method: 'POST',
51
+ path: CONTRACT_LIST_PATH,
52
+ operation: listName,
53
+ query: { passid: profile.passId, cid: profile.passId, sf_request_type: 'ajax' },
54
+ body: {
55
+ current: 1,
56
+ requiredMatch: 1,
57
+ mapResult: '1',
58
+ recheckQuery: '1',
59
+ cusMenuId: profile.cusMenuId,
60
+ urlPageTitle: profile.urlPageTitle,
61
+ searchParamData: {
62
+ searchType: '2',
63
+ type: 'and',
64
+ commonSearchFields: {},
65
+ groupFields: {},
66
+ },
67
+ listId: profile.listId,
68
+ isFollow: 0,
69
+ isShareToMe: 0,
70
+ filterCondition: {},
71
+ browser: 'Chrome',
72
+ },
73
+ });
74
+ const { rows, meta } = normalizeListResult(listName, response, profile.source);
75
+ const requested = Array.isArray(args.contractNos)
76
+ ? args.contractNos.map((value) => String(value).trim()).filter(Boolean)
77
+ : [];
78
+ let picked = rows;
79
+ if (requested.length > 0) {
80
+ if (requested.length > MAX_BORROW_ITEMS) {
81
+ throw archiveOaValidationError('too_many_items', `一次最多借阅 ${MAX_BORROW_ITEMS} 条,当前传入 ${requested.length} 条`);
82
+ }
83
+ picked = rows.filter((row) => requested.includes(String(row?.hth ?? '')));
84
+ const missing = requested.filter((no) => !picked.some((row) => String(row?.hth ?? '') === no));
85
+ if (missing.length > 0) {
86
+ throw archiveOaValidationError('contract_not_in_borrow_car', `借阅车中不存在这些合同号:${missing.join('、')}`, { missing });
87
+ }
88
+ }
89
+ if (picked.length === 0) {
90
+ throw archiveOaValidationError('borrow_car_empty', '借阅车为空,请先把合同加入借阅车');
91
+ }
92
+ if (picked.length > MAX_BORROW_ITEMS) {
93
+ throw archiveOaValidationError('too_many_items', `借阅车可发起条数最多 ${MAX_BORROW_ITEMS} 条,当前 ${picked.length} 条,请指定合同号`);
94
+ }
95
+ const fb = String(picked[0]?.fb ?? '0');
96
+ const items = picked.map(toItem);
97
+ const url = buildBorrowStartUrl(session.baseUrl, fb, items);
98
+ return archiveOaSuccess(name, {
99
+ url,
100
+ flowId: BORROW_FLOW_ID,
101
+ fb,
102
+ items,
103
+ borrowCarCount: rows.length,
104
+ columns: meta.columns,
105
+ }, { opensBrowser: false, count: items.length });
106
+ },
107
+ };
108
+ }
@@ -0,0 +1,48 @@
1
+ import { archiveOaConfirmationError, archiveOaValidationError } from '../errors.js';
2
+ /** apply 类写操作必须显式确认。 */
3
+ export function requireConfirm(args, action) {
4
+ if (args.confirm !== true) {
5
+ throw archiveOaConfirmationError(action, {
6
+ hint: `执行 ${action} 必须显式确认:传入 confirm=true(CLI 用 --yes)`,
7
+ });
8
+ }
9
+ }
10
+ /**
11
+ * prepare 阶段签发 continuation。
12
+ * 环境指纹(baseUrl + 登录账号)写进票据,apply 时校验,避免跨环境/跨账号复用。
13
+ */
14
+ export async function issueLease(env, action, payload, ttlMs = 10 * 60_000) {
15
+ const session = await env.host.getSession();
16
+ const continuation = await env.continuation.issue({
17
+ operation: action,
18
+ baseUrl: session.baseUrl,
19
+ userId: session.userId,
20
+ ...payload,
21
+ }, ttlMs);
22
+ return { continuation, expiresInMs: ttlMs };
23
+ }
24
+ /** apply 阶段校验确认 + continuation,返回 prepare 时写入的快照。 */
25
+ export async function verifyLease(args, action, env) {
26
+ requireConfirm(args, action);
27
+ if (typeof args.continuation !== 'string' || !args.continuation.trim()) {
28
+ throw archiveOaValidationError('continuation_required', `${action} 必须传入 prepare 返回的 continuation`);
29
+ }
30
+ const lease = await env.continuation.verify(args.continuation.trim(), action);
31
+ const session = await env.host.getSession();
32
+ if (lease.baseUrl !== session.baseUrl || lease.userId !== session.userId) {
33
+ throw archiveOaValidationError('continuation_context_mismatch', '确认上下文已变化(环境或登录账号不同),请重新执行 prepare');
34
+ }
35
+ return lease;
36
+ }
37
+ /** 校验 apply 入参与 prepare 快照一致,避免目标漂移。 */
38
+ export function assertLeaseTarget(lease, args, keys, action) {
39
+ for (const key of keys) {
40
+ if (args[key] === undefined || args[key] === null || args[key] === '')
41
+ continue;
42
+ const expected = String(lease[key] ?? '');
43
+ const actual = Array.isArray(args[key]) ? args[key].map(String).join(',') : String(args[key]);
44
+ if (expected !== actual) {
45
+ throw archiveOaValidationError('continuation_target_changed', `${action} 的参数 ${key} 与 prepare 时不一致,请重新执行 prepare`);
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,120 @@
1
+ import { QYS_CONTRACT, QYS_DEFAULT_ARCHIVED_FIELD, QYS_DEFAULT_ARCHIVED_VALUE, WEAVER_CONTRACT, } from '../constants.js';
2
+ import { READ, archiveOaSuccess, schemaOf, SCHEMA_PAGE_NO, SCHEMA_SEARCH_FIELDS, } from './types.js';
3
+ import { assertEnum, buildCommonSearchFields, normalizeListResult, positiveInt, unwrapData } from './shared.js';
4
+ const CONTRACT_LIST_PATH = '/api/ebuilder/form/list/getListDatas';
5
+ function filterPath(profile) {
6
+ return `/api/ebuilder/form/listFilter/showFilters/${profile.listId}`;
7
+ }
8
+ function operationName(profile, action) {
9
+ return profile.source === 'qys'
10
+ ? `archive-oa.qys.contract.${action}`
11
+ : `archive-oa.contract.${action}`;
12
+ }
13
+ /** getListDatas 的固定请求体(分页与视图参数)。 */
14
+ function listBody(profile, pageNo, commonSearchFields, matchType) {
15
+ return {
16
+ current: pageNo,
17
+ requiredMatch: 1,
18
+ mapResult: '1',
19
+ recheckQuery: '1',
20
+ cusMenuId: profile.cusMenuId,
21
+ urlPageTitle: profile.urlPageTitle,
22
+ searchParamData: {
23
+ searchType: '2',
24
+ type: matchType,
25
+ commonSearchFields,
26
+ groupFields: {},
27
+ },
28
+ listId: profile.listId,
29
+ isFollow: 0,
30
+ isShareToMe: 0,
31
+ filterCondition: {},
32
+ browser: 'Chrome',
33
+ };
34
+ }
35
+ /** 搜索字段配置(showFilters):只有源资料收录了该辅助接口才生成。 */
36
+ function makeFieldsOperation(profile) {
37
+ const name = operationName(profile, 'fields');
38
+ return {
39
+ name,
40
+ resource: 'contract',
41
+ method: 'fields',
42
+ action: name,
43
+ summary: `查询${profile.source === 'qys' ? '契约锁' : '泛微'}商务合同可搜索字段配置(构造 commonSearchFields 用)`,
44
+ capability: 'contract-search',
45
+ risk: READ,
46
+ inputSchema: schemaOf({}, { description: '查询合同搜索字段配置,无需入参' }),
47
+ handler: async (_args, env) => {
48
+ const response = await env.host.request({
49
+ method: 'GET',
50
+ path: filterPath(profile),
51
+ operation: name,
52
+ query: { passid: profile.passId, sf_request_type: 'ajax' },
53
+ });
54
+ const data = (unwrapData(response) ?? {});
55
+ const fields = Array.isArray(data.fields)
56
+ ? data.fields
57
+ : Array.isArray(data)
58
+ ? data
59
+ : [];
60
+ return archiveOaSuccess(name, { fields }, { source: profile.source, listId: profile.listId, count: Array.isArray(fields) ? fields.length : 0 });
61
+ },
62
+ };
63
+ }
64
+ /** 商务合同列表查询。 */
65
+ function makeListOperation(profile) {
66
+ const name = operationName(profile, 'list');
67
+ const isQys = profile.source === 'qys';
68
+ return {
69
+ name,
70
+ resource: 'contract',
71
+ method: 'list',
72
+ action: name,
73
+ summary: `查询${isQys ? '契约锁协作采纳(案例)' : '泛微'}商务合同列表`,
74
+ capability: 'contract-search',
75
+ risk: READ,
76
+ inputSchema: schemaOf({
77
+ pageNo: { ...SCHEMA_PAGE_NO, description: '页码,从 1 开始,缺省 1' },
78
+ fields: SCHEMA_SEARCH_FIELDS,
79
+ matchType: {
80
+ type: 'string',
81
+ enum: ['and', 'or'],
82
+ description: '多条件组合方式,缺省 and(源资料约定默认「且」)',
83
+ },
84
+ ...(isQys
85
+ ? {
86
+ archivedOnly: {
87
+ type: 'boolean',
88
+ description: `默认 true:附加 ${QYS_DEFAULT_ARCHIVED_FIELD}=${QYS_DEFAULT_ARCHIVED_VALUE}(处理进度=已归档)过滤`,
89
+ },
90
+ }
91
+ : {}),
92
+ }, { description: `查询${isQys ? '契约锁' : '泛微'}商务合同列表` }),
93
+ handler: async (args, env) => {
94
+ const pageNo = positiveInt(args.pageNo, 'pageNo', 1);
95
+ const matchType = args.matchType === undefined ? 'and' : assertEnum(args.matchType, ['and', 'or'], 'matchType');
96
+ const extra = [];
97
+ const archivedOnly = args.archivedOnly === undefined ? true : args.archivedOnly;
98
+ if (isQys && archivedOnly === true) {
99
+ extra.push({
100
+ field: QYS_DEFAULT_ARCHIVED_FIELD,
101
+ conditionType: 'like',
102
+ value: QYS_DEFAULT_ARCHIVED_VALUE,
103
+ });
104
+ }
105
+ const commonSearchFields = buildCommonSearchFields(args.fields, extra);
106
+ const response = await env.host.request({
107
+ method: 'POST',
108
+ path: CONTRACT_LIST_PATH,
109
+ operation: name,
110
+ query: { passid: profile.passId, cid: profile.passId, sf_request_type: 'ajax' },
111
+ body: listBody(profile, pageNo, commonSearchFields, matchType),
112
+ });
113
+ const { rows, meta } = normalizeListResult(name, response, profile.source);
114
+ return archiveOaSuccess(name, { rows, searchFields: commonSearchFields }, { ...meta, pageNo, archivedOnly: isQys ? archivedOnly : undefined });
115
+ },
116
+ };
117
+ }
118
+ export const weaverContractFieldsOperation = makeFieldsOperation(WEAVER_CONTRACT);
119
+ export const weaverContractListOperation = makeListOperation(WEAVER_CONTRACT);
120
+ export const qysContractListOperation = makeListOperation(QYS_CONTRACT);
@@ -0,0 +1,50 @@
1
+ import { ARCHIVE_LOCATE_PATH } from '../constants.js';
2
+ import { READ, archiveOaSuccess, schemaOf, SCHEMA_PAGE_NO, } from './types.js';
3
+ import { positiveInt, requireText } from './shared.js';
4
+ export const ARCHIVE_LOCATE = 'archive-oa.archive.locate';
5
+ /**
6
+ * 按合同号定位档案。
7
+ *
8
+ * 接口契约要点(源资料实测):合同号必须放在 body 顶层 `htbh`;
9
+ * 放进 `datajson.mainTable` 会被忽略并返回整表默认列表。
10
+ */
11
+ export const archiveLocateOperation = {
12
+ name: ARCHIVE_LOCATE,
13
+ resource: 'archive',
14
+ method: 'locate',
15
+ action: ARCHIVE_LOCATE,
16
+ summary: '按合同号定位档案:返回档案记录 ID / 题名 / 合同号',
17
+ capability: 'archive-locate',
18
+ risk: READ,
19
+ inputSchema: schemaOf({
20
+ contractNo: { type: 'string', minLength: 1, description: '合同号(对应接口顶层 htbh)' },
21
+ pageNo: { ...SCHEMA_PAGE_NO, description: '页码,从 1 开始,缺省 1' },
22
+ }, { required: ['contractNo'], description: '按合同号定位档案' }),
23
+ handler: async (args, env) => {
24
+ const contractNo = requireText(args.contractNo, 'contractNo');
25
+ const pageNo = positiveInt(args.pageNo, 'pageNo', 1);
26
+ const response = await env.host.request({
27
+ method: 'POST',
28
+ path: ARCHIVE_LOCATE_PATH,
29
+ operation: ARCHIVE_LOCATE,
30
+ body: {
31
+ htbh: contractNo,
32
+ datajson: {
33
+ mainTable: {},
34
+ pageInfo: { pageNo: String(pageNo) },
35
+ },
36
+ },
37
+ });
38
+ const datajson = (response?.datajson ?? {});
39
+ const datas = Array.isArray(datajson.datas) ? datajson.datas : [];
40
+ const rows = datas.map((item) => {
41
+ const mainTable = (item?.mainTable ?? {});
42
+ return {
43
+ id: String(mainTable.id ?? '').trim(),
44
+ title: String(mainTable.folder_title ?? '').trim(),
45
+ contractNo: String(mainTable.hth ?? contractNo).trim(),
46
+ };
47
+ });
48
+ return archiveOaSuccess(ARCHIVE_LOCATE, { rows, matched: rows.length > 0 }, { count: rows.length, contractNo, pageNo });
49
+ },
50
+ };
@@ -0,0 +1,40 @@
1
+ import { QYS_BORROW_CAR, QYS_ESB_ADD, WEAVER_BORROW_CAR, WEAVER_ESB_ADD } from '../constants.js';
2
+ import { archiveOaValidationError } from '../errors.js';
3
+ import { createBorrowCarOperations } from './borrow-car.js';
4
+ import { createBorrowStartUrlOperation } from './borrow-url.js';
5
+ import { qysContractListOperation, weaverContractFieldsOperation, weaverContractListOperation, } from './contract.js';
6
+ import { archiveLocateOperation } from './locate.js';
7
+ function buildRegistry() {
8
+ const registry = new Map();
9
+ const definitions = [
10
+ archiveLocateOperation,
11
+ weaverContractFieldsOperation,
12
+ weaverContractListOperation,
13
+ qysContractListOperation,
14
+ ...createBorrowCarOperations(WEAVER_BORROW_CAR, WEAVER_ESB_ADD),
15
+ ...createBorrowCarOperations(QYS_BORROW_CAR, QYS_ESB_ADD),
16
+ createBorrowStartUrlOperation(WEAVER_BORROW_CAR),
17
+ createBorrowStartUrlOperation(QYS_BORROW_CAR),
18
+ ];
19
+ for (const definition of definitions) {
20
+ if (registry.has(definition.name)) {
21
+ throw new Error(`duplicate archive-oa operation: ${definition.name}`);
22
+ }
23
+ registry.set(definition.name, definition);
24
+ }
25
+ return registry;
26
+ }
27
+ const archiveOaOperationRegistry = buildRegistry();
28
+ export function archiveOaOperationDefinitions() {
29
+ return [...archiveOaOperationRegistry.values()];
30
+ }
31
+ export function getArchiveOaOperation(operation) {
32
+ const definition = archiveOaOperationRegistry.get(operation);
33
+ if (!definition) {
34
+ throw archiveOaValidationError('operation_unknown', `archive-oa 不支持操作:${operation}`);
35
+ }
36
+ return definition;
37
+ }
38
+ export function hasArchiveOaOperation(operation) {
39
+ return archiveOaOperationRegistry.has(operation);
40
+ }
@@ -0,0 +1,181 @@
1
+ import { MAX_BORROW_ITEMS } from '../constants.js';
2
+ import { archiveOaValidationError } from '../errors.js';
3
+ /** 原型污染防护:禁止出现在输入对象中的键。 */
4
+ const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
5
+ const CONDITION_TYPES = new Set(['like', 'between', 'in', 'inC']);
6
+ export function asObject(value) {
7
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
8
+ throw archiveOaValidationError('input_not_object', '输入必须是 JSON 对象');
9
+ }
10
+ return value;
11
+ }
12
+ export function assertNoForbiddenKeys(input) {
13
+ for (const key of Object.keys(input)) {
14
+ if (FORBIDDEN_KEYS.has(key)) {
15
+ throw archiveOaValidationError('forbidden_key', `输入包含非法字段:${key}`);
16
+ }
17
+ }
18
+ }
19
+ /** 取出响应中的业务数据。 */
20
+ export function unwrapData(response) {
21
+ if (!response || typeof response !== 'object')
22
+ return response;
23
+ return response.data;
24
+ }
25
+ /** 正整数入参(页码)。 */
26
+ export function positiveInt(value, field, fallback) {
27
+ if (value === undefined || value === null || value === '')
28
+ return fallback;
29
+ const parsed = Number(value);
30
+ if (!Number.isInteger(parsed) || parsed <= 0) {
31
+ throw archiveOaValidationError('number_invalid', `${field} 必须是正整数`);
32
+ }
33
+ return parsed;
34
+ }
35
+ /** 非空字符串。 */
36
+ export function requireText(value, field) {
37
+ if (typeof value !== 'string' || !value.trim()) {
38
+ throw archiveOaValidationError('field_required', `${field} 必填`);
39
+ }
40
+ return value.trim();
41
+ }
42
+ /** 安全布尔。 */
43
+ export function requireBoolean(value, field) {
44
+ if (typeof value !== 'boolean') {
45
+ throw archiveOaValidationError('boolean_invalid', `${field} 必须是布尔值`);
46
+ }
47
+ return value;
48
+ }
49
+ /** 校验枚举。 */
50
+ export function assertEnum(value, allowed, field) {
51
+ const text = typeof value === 'string' ? value : String(value);
52
+ if (!allowed.includes(text)) {
53
+ throw archiveOaValidationError('enum_invalid', `${field} 取值必须是 ${allowed.join(' / ')} 之一`);
54
+ }
55
+ return text;
56
+ }
57
+ /** 数字 ID 数组(合同数据行 id / 借阅车记录 id)。 */
58
+ export function requireIdList(value, field, max = MAX_BORROW_ITEMS) {
59
+ if (!Array.isArray(value) || value.length === 0) {
60
+ throw archiveOaValidationError('id_list_required', `${field} 必填且至少一项`);
61
+ }
62
+ if (value.length > max) {
63
+ throw archiveOaValidationError('id_list_too_many', `${field} 最多 ${max} 条,当前 ${value.length} 条`);
64
+ }
65
+ const seen = new Set();
66
+ return value.map((item, index) => {
67
+ const text = typeof item === 'string' ? item.trim() : String(item ?? '').trim();
68
+ if (!/^\d+$/.test(text)) {
69
+ throw archiveOaValidationError('id_list_invalid', `${field}[${index}] 必须是数字 ID`);
70
+ }
71
+ if (seen.has(text)) {
72
+ throw archiveOaValidationError('id_list_duplicated', `${field}[${index}] 重复:${text}`);
73
+ }
74
+ seen.add(text);
75
+ return text;
76
+ });
77
+ }
78
+ /** 合同号数组(发起借阅的 hth)。 */
79
+ export function requireContractNoList(value, field, max = MAX_BORROW_ITEMS) {
80
+ if (!Array.isArray(value) || value.length === 0) {
81
+ throw archiveOaValidationError('contract_no_required', `${field} 必填且至少一项`);
82
+ }
83
+ if (value.length > max) {
84
+ throw archiveOaValidationError('contract_no_too_many', `${field} 最多 ${max} 个,当前 ${value.length} 个`);
85
+ }
86
+ return value.map((item, index) => {
87
+ if (typeof item !== 'string' || !item.trim()) {
88
+ throw archiveOaValidationError('contract_no_invalid', `${field}[${index}] 必须是非空合同号`);
89
+ }
90
+ const text = item.trim();
91
+ if (/[,\s]/.test(text)) {
92
+ throw archiveOaValidationError('contract_no_invalid', `${field}[${index}] 不能包含逗号或空白`);
93
+ }
94
+ return text;
95
+ });
96
+ }
97
+ /**
98
+ * 组装 getListDatas 的 commonSearchFields。
99
+ *
100
+ * 契约:`{ "<field>": [{ "<field>_condition_type": "<type>", "<field>": <value> }] }`。
101
+ * between 的值既支持 `{min,max}` 也支持 `[起,止]`;in / inC 的值必须是数组。
102
+ */
103
+ export function buildCommonSearchFields(fields, extra = []) {
104
+ const result = {};
105
+ const items = [...extra];
106
+ if (fields !== undefined && fields !== null) {
107
+ if (!Array.isArray(fields)) {
108
+ throw archiveOaValidationError('search_fields_invalid', 'fields 必须是数组');
109
+ }
110
+ for (const [index, raw] of fields.entries()) {
111
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
112
+ throw archiveOaValidationError('search_fields_invalid', `fields[${index}] 必须是对象`);
113
+ }
114
+ const item = raw;
115
+ const field = requireText(item.field, `fields[${index}].field`);
116
+ const conditionType = item.conditionType === undefined || item.conditionType === null
117
+ ? 'like'
118
+ : String(item.conditionType);
119
+ if (!CONDITION_TYPES.has(conditionType)) {
120
+ throw archiveOaValidationError('search_condition_invalid', `fields[${index}].conditionType 必须是 like / between / in / inC 之一`);
121
+ }
122
+ if (item.value === undefined || item.value === null) {
123
+ throw archiveOaValidationError('search_value_required', `fields[${index}].value 必填`);
124
+ }
125
+ items.push({ field, conditionType, value: item.value });
126
+ }
127
+ }
128
+ for (const item of items) {
129
+ const value = normalizeConditionValue(item.conditionType, item.value, item.field);
130
+ result[item.field] = [{ [`${item.field}_condition_type`]: item.conditionType, [item.field]: value }];
131
+ }
132
+ return result;
133
+ }
134
+ function normalizeConditionValue(conditionType, value, field) {
135
+ if (conditionType === 'between') {
136
+ if (Array.isArray(value)) {
137
+ if (value.length !== 2) {
138
+ throw archiveOaValidationError('search_value_invalid', `${field} 的 between 条件需要两个值`);
139
+ }
140
+ return { min: String(value[0]), max: String(value[1]) };
141
+ }
142
+ if (value && typeof value === 'object') {
143
+ const range = value;
144
+ if (range.min === undefined || range.max === undefined) {
145
+ throw archiveOaValidationError('search_value_invalid', `${field} 的 between 条件需要 min / max`);
146
+ }
147
+ return { min: String(range.min), max: String(range.max) };
148
+ }
149
+ throw archiveOaValidationError('search_value_invalid', `${field} 的 between 条件必须是 {min,max} 或 [起,止]`);
150
+ }
151
+ if (conditionType === 'in' || conditionType === 'inC') {
152
+ if (!Array.isArray(value)) {
153
+ throw archiveOaValidationError('search_value_invalid', `${field} 的 ${conditionType} 条件必须是数组`);
154
+ }
155
+ return value;
156
+ }
157
+ return value;
158
+ }
159
+ /** 从 getListDatas 响应里提取行与列,并对内部 ID 做静默缓存(不回显给模型以外的地方)。 */
160
+ export function normalizeListResult(action, response, source) {
161
+ const data = (unwrapData(response) ?? {});
162
+ const rows = Array.isArray(data.data) ? data.data : [];
163
+ const columns = Array.isArray(data.columns)
164
+ ? data.columns.map((column) => ({
165
+ title: String(column?.title ?? ''),
166
+ dataIndex: String(column?.dataIndex ?? ''),
167
+ }))
168
+ : [];
169
+ return {
170
+ rows,
171
+ meta: {
172
+ source,
173
+ count: rows.length,
174
+ current: data.current ?? undefined,
175
+ pageSize: data.pageSize ?? undefined,
176
+ /** 接口可能返回 total=0 但 data 非空,属于正常现象,不做无数据判定。 */
177
+ total: data.total ?? undefined,
178
+ columns,
179
+ },
180
+ };
181
+ }
@@ -0,0 +1,45 @@
1
+ export const READ = 'read';
2
+ export const READ_BEFORE_WRITE = 'read-before-write';
3
+ export const HIGH_RISK_WRITE = 'high-risk-write';
4
+ export function archiveOaSuccess(action, data, meta) {
5
+ const payload = { ok: true, action, data };
6
+ if (meta && Object.keys(meta).length > 0)
7
+ payload.meta = meta;
8
+ return payload;
9
+ }
10
+ /** 构造「对象类型 + 严格禁止额外字段」的 JSON Schema。 */
11
+ export function schemaOf(properties, options = {}) {
12
+ return {
13
+ type: 'object',
14
+ additionalProperties: false,
15
+ properties,
16
+ ...(options.required?.length ? { required: options.required } : {}),
17
+ ...(options.description ? { description: options.description } : {}),
18
+ };
19
+ }
20
+ export const SCHEMA_PAGE_NO = { type: 'integer', minimum: 1 };
21
+ export const SCHEMA_ID_STRING = { type: 'string', pattern: '^\\d+$' };
22
+ export const SCHEMA_ID_LIST = { type: 'array', items: SCHEMA_ID_STRING, minItems: 1 };
23
+ export const SCHEMA_CONTRACT_NO_LIST = {
24
+ type: 'array',
25
+ items: { type: 'string', minLength: 1 },
26
+ minItems: 1,
27
+ };
28
+ export const SCHEMA_SEARCH_FIELDS = {
29
+ type: 'array',
30
+ description: '搜索条件数组,元素为 { field, conditionType, value }',
31
+ items: {
32
+ type: 'object',
33
+ additionalProperties: false,
34
+ required: ['field', 'value'],
35
+ properties: {
36
+ field: { type: 'string', minLength: 1, description: '字段内部名,如 hth / khmc / htzje / zxfa' },
37
+ conditionType: {
38
+ type: 'string',
39
+ enum: ['like', 'between', 'in', 'inC'],
40
+ description: '条件类型,缺省 like;between 用 {min,max} 或 [起,止];in/inC 用数组',
41
+ },
42
+ value: { description: '条件值:like 为字符串,between 为对象或二元数组,in/inC 为数组' },
43
+ },
44
+ },
45
+ };
@@ -0,0 +1,13 @@
1
+ import { getArchiveOaOperation } from './operations/registry.js';
2
+ import { asObject, assertNoForbiddenKeys } from './operations/shared.js';
3
+ /**
4
+ * 执行一个 archive-oa 操作。操作定义(入参 schema、风险等级、是否必须确认)
5
+ * 全部来自 operations/registry,CLI 侧只负责校验输入形状。
6
+ */
7
+ export async function executeArchiveOaOperation(operation, rawInput, env) {
8
+ const definition = getArchiveOaOperation(operation);
9
+ const input = asObject(rawInput);
10
+ assertNoForbiddenKeys(input);
11
+ return definition.handler(input, { ctx: env.ctx, host: env.host, continuation: env.continuation });
12
+ }
13
+ export { archiveOaOperationDefinitions, getArchiveOaOperation, hasArchiveOaOperation } from './operations/registry.js';
@@ -2,6 +2,7 @@ import { caizhilianShortcut } from './caizhilian/index.js';
2
2
  import { projectShortcut } from './project/index.js';
3
3
  import { blogShortcut } from './blog/index.js';
4
4
  import { archiveShortcut } from './archive/index.js';
5
+ import { archiveOaShortcut } from './archive-oa/index.js';
5
6
  import { calendarShortcut } from './calendar/index.js';
6
7
  import { ehrShortcut } from './ehr/index.js';
7
8
  import { esbShortcut } from './esb/index.js';
@@ -21,6 +22,7 @@ export const builtinShortcuts = [
21
22
  invoiceShortcut,
22
23
  ehrShortcut,
23
24
  archiveShortcut,
25
+ archiveOaShortcut,
24
26
  calendarShortcut,
25
27
  fnaShortcut,
26
28
  hrmShortcut,
@@ -229,7 +229,7 @@ export function createInvoiceHost(ctx, dependencies = {}) {
229
229
  return handleBusinessResponse(value, () => invoiceRequest(request, false), canRetryExpiredToken && Boolean(token));
230
230
  }
231
231
  async function invoiceMultipartRequest(request, canRetryExpiredToken = true) {
232
- const { method = 'POST', path, query, fields = {}, file, operation, needsInvoiceToken = true, timeoutMs = 30_000, } = request;
232
+ const { method = 'POST', path, query, fields = {}, file, headers, operation, needsInvoiceToken = true, timeoutMs = 30_000, } = request;
233
233
  if (!file && !Object.values(fields).some((value) => value !== undefined && value !== null && value !== '')) {
234
234
  throw invoiceValidationError('input_invalid', 'multipart 请求需要文件、URL 或表单字段');
235
235
  }
@@ -251,6 +251,7 @@ export function createInvoiceHost(ctx, dependencies = {}) {
251
251
  method,
252
252
  path,
253
253
  query,
254
+ headers,
254
255
  formData: form,
255
256
  timeoutMs,
256
257
  trustedHeaders: trustedInvoiceHeaders(token),