weaver-work-cli 0.1.5 → 0.1.7
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/README.md +14 -9
- package/dist/cmd/skills/index.js +60 -0
- package/dist/internal/e10/auth/commands.js +48 -32
- package/dist/internal/e10/auth/session.js +44 -3
- package/dist/internal/e10/auth/xiaoe.js +363 -11
- package/dist/internal/e10/context.js +4 -2
- package/dist/internal/skills/detector.js +42 -0
- package/dist/internal/skills/install.js +50 -3
- package/dist/shortcuts/archive/render/search-format.js +12 -3
- package/dist/shortcuts/ebuilder-form/continuation.js +70 -0
- package/dist/shortcuts/ebuilder-form/errors.js +54 -0
- package/dist/shortcuts/ebuilder-form/host.js +1238 -0
- package/dist/shortcuts/ebuilder-form/index.js +108 -0
- package/dist/shortcuts/ebuilder-form/manifest.js +347 -0
- package/dist/shortcuts/ebuilder-form/operations/read.js +157 -0
- package/dist/shortcuts/ebuilder-form/operations/registry.js +56 -0
- package/dist/shortcuts/ebuilder-form/operations/shared.js +132 -0
- package/dist/shortcuts/ebuilder-form/operations/types.js +19 -0
- package/dist/shortcuts/ebuilder-form/operations/write.js +277 -0
- package/dist/shortcuts/ebuilder-form/operations.js +8 -0
- package/dist/shortcuts/ehr/operations/salary.js +2 -140
- package/dist/shortcuts/im/continuation.js +70 -0
- package/dist/shortcuts/{yimiaoban → im}/errors.js +14 -14
- package/dist/shortcuts/{yimiaoban → im}/host.js +26 -26
- package/dist/shortcuts/{yimiaoban → im}/index.js +42 -42
- package/dist/shortcuts/{yimiaoban → im}/manifest.js +7 -6
- package/dist/shortcuts/{yimiaoban → im}/operations/file-user.js +54 -41
- package/dist/shortcuts/{yimiaoban → im}/operations/group-read.js +46 -44
- package/dist/shortcuts/{yimiaoban → im}/operations/group-write.js +113 -66
- package/dist/shortcuts/{yimiaoban → im}/operations/msg-sync.js +164 -141
- package/dist/shortcuts/{yimiaoban → im}/operations/msg-write.js +47 -47
- package/dist/shortcuts/{yimiaoban → im}/operations/person.js +10 -10
- package/dist/shortcuts/{yimiaoban → im}/operations/registry.js +10 -8
- package/dist/shortcuts/{yimiaoban → im}/operations/session.js +82 -83
- package/dist/shortcuts/im/operations/shared.js +442 -0
- package/dist/shortcuts/{yimiaoban → im}/operations/types.js +1 -1
- package/dist/shortcuts/im/operations/user-remind.js +201 -0
- package/dist/shortcuts/{yimiaoban → im}/operations/write-util.js +7 -7
- package/dist/shortcuts/{yimiaoban → im}/operations.js +3 -3
- package/dist/shortcuts/index.js +10 -2
- package/dist/shortcuts/invoice/manifest.js +113 -10
- package/dist/shortcuts/invoice/operations/import.js +39 -14
- package/dist/shortcuts/invoice/operations/reim.js +38 -18
- package/dist/shortcuts/invoice/operations/shared.js +35 -6
- package/dist/shortcuts/jiuchuanhui/manifest.js +3 -3
- package/dist/shortcuts/jiuchuanhui/operations/registry.js +2 -2
- package/dist/shortcuts/{yimiaoban → okr}/continuation.js +10 -10
- package/dist/shortcuts/okr/errors.js +70 -0
- package/dist/shortcuts/okr/host.js +169 -0
- package/dist/shortcuts/okr/index.js +155 -0
- package/dist/shortcuts/okr/manifest.js +70 -0
- package/dist/shortcuts/okr/operations/link.js +305 -0
- package/dist/shortcuts/okr/operations/read.js +481 -0
- package/dist/shortcuts/okr/operations/registry.js +31 -0
- package/dist/shortcuts/okr/operations/shared.js +381 -0
- package/dist/shortcuts/okr/operations/types.js +88 -0
- package/dist/shortcuts/okr/operations/write.js +838 -0
- package/dist/shortcuts/okr/operations.js +8 -0
- package/dist/shortcuts/project/continuation.js +70 -0
- package/dist/shortcuts/project/errors.js +53 -0
- package/dist/shortcuts/project/host.js +160 -0
- package/dist/shortcuts/project/index.js +106 -0
- package/dist/shortcuts/project/manifest.js +36 -0
- package/dist/shortcuts/project/operations/read.js +551 -0
- package/dist/shortcuts/project/operations/registry.js +31 -0
- package/dist/shortcuts/project/operations/shared.js +68 -0
- package/dist/shortcuts/project/operations/types.js +39 -0
- package/dist/shortcuts/project/operations/write.js +266 -0
- package/dist/shortcuts/project/operations.js +8 -0
- package/dist/shortcuts/workflow/continuation.js +508 -0
- package/dist/shortcuts/workflow/endpoints.js +149 -0
- package/dist/shortcuts/workflow/errors.js +112 -0
- package/dist/shortcuts/workflow/host.js +224 -0
- package/dist/shortcuts/workflow/index.js +116 -0
- package/dist/shortcuts/workflow/manifest.js +30 -0
- package/dist/shortcuts/workflow/operations/form.js +4159 -0
- package/dist/shortcuts/workflow/operations/read.js +3778 -0
- package/dist/shortcuts/workflow/operations/registry.js +33 -0
- package/dist/shortcuts/workflow/operations/shared.js +167 -0
- package/dist/shortcuts/workflow/operations/types.js +44 -0
- package/dist/shortcuts/workflow/operations/write.js +1991 -0
- package/dist/shortcuts/workflow/operations.js +119 -0
- package/dist/shortcuts/workflow/state.js +182 -0
- package/docs/SKILL.md +5 -3
- package/docs/_catalog.md +10 -1
- package/docs/agent-invoice.md +2 -2
- package/docs/agent-skill-install.md +2 -2
- package/docs/e10-auth.md +3 -1
- package/docs/ebuilder-form.md +38 -0
- package/docs/im.md +104 -0
- package/docs/invoice.md +5 -5
- package/docs/okr.md +122 -0
- package/docs/operation-manual.md +10 -7
- package/docs/project.md +69 -0
- package/docs/skill-review-2026-09-19.md +68 -0
- package/package.json +3 -2
- package/scripts/package-skill.mjs +38 -11
- package/scripts/sync-connector-skills.mjs +172 -0
- package/skill-template/business-info.json +357 -16
- package/skill-template/domains/ebuilder-form.md +5 -0
- package/skill-template/domains/okr.md +18 -0
- package/skill-template/domains/shijingran.md +3 -0
- package/skill-template/domains/skill-maker.md +4 -4
- package/skill-template/domains/workflow.md +22 -0
- package/skill-template/domains/yepiaotong.md +4 -2
- package/skill-template/domains/yimiaoban.md +13 -10
- package/skill-template/product.json +5 -0
- package/skill-template/skill-template.md +1 -0
- package/skills/weaver-e10-calendar/SKILL.md +5 -1
- package/skills/weaver-e10-calendar/references/calendar-query.md +7 -0
- package/skills/weaver-e10-calendar/references/source-manifest.json +314 -32
- package/skills/weaver-e10-ebuilder-form/SKILL.md +123 -0
- package/skills/weaver-e10-ebuilder-form/references/context-routing.md +90 -0
- package/skills/weaver-e10-ebuilder-form/references/custom-api.md +86 -0
- package/skills/weaver-e10-ebuilder-form/references/disabled-capabilities.md +50 -0
- package/skills/weaver-e10-ebuilder-form/references/fields-browser.md +100 -0
- package/skills/weaver-e10-ebuilder-form/references/list-nlist.md +91 -0
- package/skills/weaver-e10-ebuilder-form/references/openapi-read.md +74 -0
- package/skills/weaver-e10-ebuilder-form/references/openapi-write.md +86 -0
- package/skills/weaver-e10-ebuilder-form/references/source-manifest.json +107 -0
- package/skills/weaver-e10-esb/SKILL.md +1 -1
- package/skills/weaver-e10-esb/references/source-manifest.json +15 -14
- package/skills/weaver-e10-hrm/SKILL.md +1 -1
- package/skills/weaver-e10-hrm/references/source-manifest.json +357 -38
- package/skills/weaver-e10-jiuchuanhui/SKILL.md +22 -4
- package/skills/weaver-e10-jiuchuanhui/references/clue.md +4 -2
- package/skills/weaver-e10-jiuchuanhui/references/contact.md +1 -1
- package/skills/weaver-e10-jiuchuanhui/references/customer.md +3 -3
- package/skills/weaver-e10-jiuchuanhui/references/disabled-capabilities.md +1 -1
- package/skills/weaver-e10-jiuchuanhui/references/discovery-config.md +10 -10
- package/skills/weaver-e10-jiuchuanhui/references/general-helpers.md +3 -3
- package/skills/weaver-e10-jiuchuanhui/references/sale.md +7 -7
- package/skills/weaver-e10-jiuchuanhui/references/source-manifest.json +3 -3
- package/skills/weaver-e10-jucailin/SKILL.md +2 -2
- package/skills/weaver-e10-mail/SKILL.md +22 -6
- package/skills/weaver-e10-meeting/SKILL.md +22 -1
- package/skills/weaver-e10-okr/SKILL.md +139 -0
- package/skills/weaver-e10-okr/references/okr-align.md +59 -0
- package/skills/weaver-e10-okr/references/okr-keyresult.md +68 -0
- package/skills/weaver-e10-okr/references/okr-link-routing.md +67 -0
- package/skills/weaver-e10-okr/references/okr-query.md +105 -0
- package/skills/weaver-e10-okr/references/okr-write.md +94 -0
- package/skills/weaver-e10-okr/references/source-manifest.json +672 -0
- package/skills/weaver-e10-plan/SKILL.md +3 -3
- package/skills/weaver-e10-plan/references/plan-report-write.md +1 -1
- package/skills/weaver-e10-plan/references/source-manifest.json +20 -20
- package/skills/weaver-e10-qiyecheng/SKILL.md +21 -1
- package/skills/weaver-e10-qiyecheng/references/source-manifest.json +17 -17
- package/skills/weaver-e10-shared/SKILL.md +79 -11
- package/skills/weaver-e10-shared/references/e10-auth-and-session.md +41 -8
- package/skills/weaver-e10-shared/references/non-interactive-environment.md +78 -0
- package/skills/weaver-e10-shared/references/weaver-e10-installation.md +39 -0
- package/skills/weaver-e10-shijingran/SKILL.md +83 -0
- package/skills/weaver-e10-shijingran/references/field-resolution.md +105 -0
- package/skills/weaver-e10-shijingran/references/operations.md +154 -0
- package/skills/weaver-e10-shijingran/references/source-manifest.json +1380 -0
- package/skills/weaver-e10-shijingran/references/write-operations.md +90 -0
- package/skills/weaver-e10-skill-maker/SKILL.md +26 -9
- package/skills/weaver-e10-skill-maker/references/business-skill-generation.md +39 -8
- package/skills/weaver-e10-skill-maker/references/detector-validation.md +25 -8
- package/skills/weaver-e10-skill-maker/references/module-cli-generation.md +66 -5
- package/skills/weaver-e10-skill-maker/references/module-gap-audit-and-repair.md +86 -0
- package/skills/weaver-e10-skill-maker/references/source-update-detection.md +59 -5
- package/skills/weaver-e10-skill-maker/references/weaver-skill-style.md +16 -8
- package/skills/weaver-e10-wenshuding/SKILL.md +28 -4
- package/skills/weaver-e10-wenshuding/references/archive-search.md +14 -0
- package/skills/weaver-e10-wenshuding/references/source-manifest.json +27 -26
- package/skills/weaver-e10-workflow/SKILL.md +250 -0
- package/skills/weaver-e10-workflow/references/source-manifest.json +1062 -0
- package/skills/weaver-e10-workflow/references/workflow-agent-entry.md +96 -0
- package/skills/weaver-e10-workflow/references/workflow-batch.md +275 -0
- package/skills/weaver-e10-workflow/references/workflow-create.md +182 -0
- package/skills/weaver-e10-workflow/references/workflow-edit.md +106 -0
- package/skills/weaver-e10-workflow/references/workflow-memory.md +165 -0
- package/skills/weaver-e10-workflow/references/workflow-operate.md +153 -0
- package/skills/weaver-e10-workflow/references/workflow-query.md +821 -0
- package/skills/weaver-e10-workflow/references/workflow-share.md +117 -0
- package/skills/weaver-e10-workflow/references/workflow-view.md +189 -0
- package/skills/weaver-e10-yepiaotong/SKILL.md +28 -28
- package/skills/weaver-e10-yepiaotong/references/invoice-disabled-capabilities.md +11 -5
- package/skills/weaver-e10-yepiaotong/references/invoice-import.md +14 -8
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing-examples.md +9 -43
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing.md +8 -0
- package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +13 -17
- package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +6 -7
- package/skills/weaver-e10-yepiaotong/references/reim-examples.md +2 -2
- package/skills/weaver-e10-yepiaotong/references/source-manifest.json +1155 -553
- package/skills/weaver-e10-yimiaoban/SKILL.md +56 -38
- package/skills/weaver-e10-yimiaoban/references/ding-write.md +8 -8
- package/skills/weaver-e10-yimiaoban/references/error-codes.md +16 -0
- package/skills/weaver-e10-yimiaoban/references/field-resolution.md +10 -10
- package/skills/weaver-e10-yimiaoban/references/file-user-i18n.md +12 -12
- package/skills/weaver-e10-yimiaoban/references/group-read.md +16 -16
- package/skills/weaver-e10-yimiaoban/references/group-write.md +17 -15
- package/skills/weaver-e10-yimiaoban/references/msg-read.md +21 -17
- package/skills/weaver-e10-yimiaoban/references/msg-write.md +10 -10
- package/skills/weaver-e10-yimiaoban/references/person.md +6 -6
- package/skills/weaver-e10-yimiaoban/references/safety-boundaries.md +15 -11
- package/skills/weaver-e10-yimiaoban/references/session-sysmsg.md +11 -10
- package/skills/weaver-e10-yimiaoban/references/source-manifest.json +2164 -154
- package/skills/weaver-e10-yimiaoban/references/user-remind.md +75 -0
- package/skills/weaver-e10-ziguanjia/SKILL.md +4 -2
- package/skills/weaver-e10-ziguanjia/references/source-manifest.json +99 -29
- package/tools/weaver-skill-detector-1.0.13/SKILL.md +253 -0
- package/tools/weaver-skill-detector-1.0.13/_meta.json +6 -0
- package/tools/weaver-skill-detector-1.0.13/references/checklist.md +298 -0
- package/tools/weaver-skill-detector-1.0.13/scripts/validate_skill.py +4190 -0
- package/dist/shortcuts/yimiaoban/operations/shared.js +0 -236
- package/docs/yimiaoban.md +0 -86
- package/skills/weaver-e10-calendar/product.json +0 -8
- package/skills/weaver-e10-esb/product.json +0 -8
- package/skills/weaver-e10-hrm/product.json +0 -8
- package/skills/weaver-e10-jiuchuanhui/product.json +0 -8
- package/skills/weaver-e10-jucailin/product.json +0 -8
- package/skills/weaver-e10-mail/product.json +0 -8
- package/skills/weaver-e10-meeting/product.json +0 -8
- package/skills/weaver-e10-plan/product.json +0 -8
- package/skills/weaver-e10-qiyecheng/product.json +0 -8
- package/skills/weaver-e10-skill-maker/product.json +0 -8
- package/skills/weaver-e10-wenshuding/product.json +0 -8
- package/skills/weaver-e10-yepiaotong/product.json +0 -8
- package/skills/weaver-e10-yepiaotong/references/invoice-red.md +0 -261
- package/skills/weaver-e10-yimiaoban/product.json +0 -8
- package/skills/weaver-e10-ziguanjia/product.json +0 -8
- /package/dist/shortcuts/{yimiaoban → im}/operations/msg-parse.js +0 -0
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
import { okrApiError, okrPartialError, okrValidationError } from '../errors.js';
|
|
2
|
+
import { businessMessage, isDatajsonSuccess, isFlagSuccess } from '../host.js';
|
|
3
|
+
import { buildStandardSaveBody, createDetailUrlResolver, fetchGoalRaw, goalPrincipalId, okrAbsoluteUrl, resolveLink, } from './link.js';
|
|
4
|
+
import { assertLinkSupported, compact, ebPeriodFields, ensureConfirmLease, fingerprint, optionValue, resolvePeriod, toIdString, toPersonIdListText, toPersonIdString, } from './shared.js';
|
|
5
|
+
import { okrSuccess, schemaOf, SCHEMA_DATE, SCHEMA_ID, SCHEMA_ID_LIST_TEXT, SCHEMA_PERSON_ID, SCHEMA_PERIOD_TYPE, SCHEMA_QUARTER, SCHEMA_SCOPE, } from './types.js';
|
|
6
|
+
const CONTINUATION_TTL_SECONDS = 600;
|
|
7
|
+
/** ebuilder 新建目标接口支持的 mainTable 字段(源资料 okr_createGoal 传入字段)。 */
|
|
8
|
+
const EB_CREATE_KEYS = [
|
|
9
|
+
'principal_dept',
|
|
10
|
+
'wb_state',
|
|
11
|
+
'partners',
|
|
12
|
+
'obj_name',
|
|
13
|
+
'principalid',
|
|
14
|
+
'period_quarter',
|
|
15
|
+
'period_year',
|
|
16
|
+
'period_month',
|
|
17
|
+
'period_type',
|
|
18
|
+
'parent_id',
|
|
19
|
+
'category_id',
|
|
20
|
+
'progress',
|
|
21
|
+
'm_status',
|
|
22
|
+
'scope_id',
|
|
23
|
+
'period_range_0',
|
|
24
|
+
'period_range_1',
|
|
25
|
+
];
|
|
26
|
+
/** ebuilder 编辑目标接口支持的 mainTable 字段(源资料 okr_updateGoal 传入字段)。 */
|
|
27
|
+
const EB_UPDATE_KEYS = [
|
|
28
|
+
'id',
|
|
29
|
+
'isremind',
|
|
30
|
+
'remind_time',
|
|
31
|
+
'wb_state',
|
|
32
|
+
'partners',
|
|
33
|
+
'obj_name',
|
|
34
|
+
'principalid',
|
|
35
|
+
'period_quarter',
|
|
36
|
+
'period_year',
|
|
37
|
+
'period_type',
|
|
38
|
+
'parent_id',
|
|
39
|
+
'score',
|
|
40
|
+
'category_id',
|
|
41
|
+
'scope_id',
|
|
42
|
+
'period_range_0',
|
|
43
|
+
'period_range_1',
|
|
44
|
+
'remark',
|
|
45
|
+
];
|
|
46
|
+
const KEYRESULT_SCHEMA = {
|
|
47
|
+
type: 'object',
|
|
48
|
+
additionalProperties: false,
|
|
49
|
+
properties: {
|
|
50
|
+
name: { type: 'string', minLength: 1, description: '关键成果名称' },
|
|
51
|
+
principalid: SCHEMA_PERSON_ID,
|
|
52
|
+
target_value: { type: 'number', description: '目标值' },
|
|
53
|
+
weight: { type: 'number', description: '权重' },
|
|
54
|
+
},
|
|
55
|
+
required: ['name'],
|
|
56
|
+
};
|
|
57
|
+
/** 目标写入字段(新建与编辑共用;编辑时全部可选,未传字段沿用当前详情)。 */
|
|
58
|
+
const GOAL_WRITE_FIELDS = {
|
|
59
|
+
name: { type: 'string', minLength: 1, description: '目标名称' },
|
|
60
|
+
period_type: SCHEMA_PERIOD_TYPE,
|
|
61
|
+
period_range: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
minLength: 1,
|
|
64
|
+
description: '周期范围:年度/季度为 yyyy,月度为 yyyy-MM,自定义为「开始日期,结束日期」',
|
|
65
|
+
},
|
|
66
|
+
period_quarter: SCHEMA_QUARTER,
|
|
67
|
+
principalid: SCHEMA_PERSON_ID,
|
|
68
|
+
partners: {
|
|
69
|
+
oneOf: [SCHEMA_ID_LIST_TEXT, { type: 'array', items: SCHEMA_PERSON_ID }],
|
|
70
|
+
description: '参与人人员 ID(英文逗号拼接或数组)',
|
|
71
|
+
},
|
|
72
|
+
scope_id: SCHEMA_SCOPE,
|
|
73
|
+
scope_data_id: SCHEMA_ID,
|
|
74
|
+
principal_dept: SCHEMA_ID,
|
|
75
|
+
parent_id: SCHEMA_ID,
|
|
76
|
+
category_id: { type: 'string', description: '目标分类 id' },
|
|
77
|
+
remark: { type: 'string', description: '目标描述' },
|
|
78
|
+
progress: { type: 'number', minimum: 0, maximum: 100, description: '目标进度' },
|
|
79
|
+
status: {
|
|
80
|
+
type: 'string',
|
|
81
|
+
enum: ['0', '1'],
|
|
82
|
+
description: '标准版新建/编辑状态:0-保存草稿 1-正式提交(默认 0)',
|
|
83
|
+
},
|
|
84
|
+
m_status: {
|
|
85
|
+
type: 'string',
|
|
86
|
+
enum: ['1', '2', '3', '4', '5'],
|
|
87
|
+
description: 'ebuilder 目标进展:1-进行中 2-已完成 3-已撤销 4-审批中 5-未开始(新建默认 1)',
|
|
88
|
+
},
|
|
89
|
+
wb_state: { type: 'string', enum: ['1', '2', '3'], description: 'ebuilder 目标紧急状态:1-正常 2-紧急 3-非常紧急' },
|
|
90
|
+
keyresult_list: {
|
|
91
|
+
type: 'array',
|
|
92
|
+
items: KEYRESULT_SCHEMA,
|
|
93
|
+
description: '随目标一起创建的关键成果(仅标准版链路)',
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
function pickKeys(source, keys) {
|
|
97
|
+
const result = {};
|
|
98
|
+
for (const key of keys) {
|
|
99
|
+
if (source[key] !== undefined && source[key] !== null && source[key] !== '')
|
|
100
|
+
result[key] = source[key];
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
/** 把用户输入的目标字段映射为 ebuilder mainTable 字段(新建)。 */
|
|
105
|
+
function buildEbCreateMainTable(args, period, principalid) {
|
|
106
|
+
const scopeId = args.scope_id === undefined ? '1' : String(args.scope_id);
|
|
107
|
+
const principalDept = args.principal_dept === undefined ? undefined : toIdString(args.principal_dept, 'principal_dept');
|
|
108
|
+
if (scopeId === '2' && !principalDept) {
|
|
109
|
+
throw okrValidationError('principal_dept_required', 'ebuilder 链路创建部门目标(scope_id=2)必须提供 principal_dept(责任人部门 id)');
|
|
110
|
+
}
|
|
111
|
+
return compact({
|
|
112
|
+
...ebPeriodFields(period),
|
|
113
|
+
obj_name: String(args.name),
|
|
114
|
+
scope_id: scopeId,
|
|
115
|
+
principalid,
|
|
116
|
+
...(principalDept ? { principal_dept: principalDept } : {}),
|
|
117
|
+
...(args.partners === undefined ? {} : { partners: toPersonIdListText(args.partners, 'partners') }),
|
|
118
|
+
...(args.parent_id === undefined ? {} : { parent_id: toIdString(args.parent_id, 'parent_id') }),
|
|
119
|
+
...(args.category_id === undefined ? {} : { category_id: String(args.category_id) }),
|
|
120
|
+
// 源资料默认值:目标进展默认为进行中(m_status=1),目标进度默认 0。
|
|
121
|
+
m_status: args.m_status === undefined ? '1' : String(args.m_status),
|
|
122
|
+
progress: args.progress === undefined ? 0 : Number(args.progress),
|
|
123
|
+
...(args.wb_state === undefined ? {} : { wb_state: String(args.wb_state) }),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/** 把目标详情(标准版 obj 结构)还原为新建/编辑请求体字段,用于编辑时全量合并。 */
|
|
127
|
+
function standardBodyFromCurrent(current) {
|
|
128
|
+
return compact({
|
|
129
|
+
name: current?.name,
|
|
130
|
+
scopeId: optionValue(current?.scopeId) ?? (current?.scopeId === undefined ? undefined : String(current.scopeId)),
|
|
131
|
+
scopeDataId: current?.scopeDataId === undefined ? undefined : String(current.scopeDataId),
|
|
132
|
+
periodType: optionValue(current?.periodType) ?? (current?.periodType === undefined ? undefined : String(current.periodType)),
|
|
133
|
+
periodRange: current?.periodRange === undefined ? undefined : String(current.periodRange),
|
|
134
|
+
periodQuarter: optionValue(current?.periodQuarter) ?? (current?.periodQuarter === undefined ? undefined : String(current.periodQuarter)),
|
|
135
|
+
principalid: current?.principalid === undefined ? undefined : String(current.principalid),
|
|
136
|
+
partners: current?.partners === undefined ? undefined : String(current.partners),
|
|
137
|
+
remark: current?.remark,
|
|
138
|
+
parentId: current?.parentId === undefined ? undefined : String(current.parentId),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/** 构造标准版链路的新建/编辑请求体。 */
|
|
142
|
+
function buildStandardGoalBody(args, mode, host) {
|
|
143
|
+
const current = host.current ?? {};
|
|
144
|
+
const base = standardBodyFromCurrent(current);
|
|
145
|
+
const scopeId = args.scope_id === undefined
|
|
146
|
+
? (base.scopeId === undefined ? '1' : String(base.scopeId))
|
|
147
|
+
: String(args.scope_id);
|
|
148
|
+
const scopeDataId = args.scope_data_id === undefined
|
|
149
|
+
? (base.scopeDataId === undefined ? undefined : String(base.scopeDataId))
|
|
150
|
+
: toIdString(args.scope_data_id, 'scope_data_id');
|
|
151
|
+
if (scopeId === '2' && !scopeDataId) {
|
|
152
|
+
throw okrValidationError('scope_data_id_required', '标准版链路创建/编辑部门目标(scope_id=2)必须提供 scope_data_id(部门 id)');
|
|
153
|
+
}
|
|
154
|
+
const periodType = host.period ? host.period.periodType : (base.periodType === undefined ? undefined : String(base.periodType));
|
|
155
|
+
if (!periodType) {
|
|
156
|
+
throw okrValidationError('period_type_invalid', '无法从当前目标还原周期类型,请显式提供 period_type 与 period_range');
|
|
157
|
+
}
|
|
158
|
+
const periodRange = host.period ? host.period.periodRange : (base.periodRange === undefined ? undefined : String(base.periodRange));
|
|
159
|
+
if (!periodRange) {
|
|
160
|
+
throw okrValidationError('period_range_required', '无法从当前目标还原周期范围,请显式提供 period_type 与 period_range');
|
|
161
|
+
}
|
|
162
|
+
const periodQuarter = periodType === '2'
|
|
163
|
+
? (host.period?.periodQuarter ?? (base.periodQuarter === undefined ? undefined : String(base.periodQuarter)))
|
|
164
|
+
: undefined;
|
|
165
|
+
if (periodType === '2' && !periodQuarter) {
|
|
166
|
+
throw okrValidationError('period_quarter_invalid', '季度目标必须提供 period_quarter(1/2/3/4)');
|
|
167
|
+
}
|
|
168
|
+
const keyresultList = Array.isArray(args.keyresult_list)
|
|
169
|
+
? args.keyresult_list.map((item) => compact({
|
|
170
|
+
name: String(item.name),
|
|
171
|
+
...(item.principalid === undefined ? {} : { principalid: toPersonIdString(item.principalid, 'keyresult_list[].principalid') }),
|
|
172
|
+
weight: item.weight === undefined ? '0' : String(item.weight),
|
|
173
|
+
...(item.target_value === undefined ? {} : { targetValue: item.target_value }),
|
|
174
|
+
}))
|
|
175
|
+
: undefined;
|
|
176
|
+
// 新建默认保存草稿(源资料:新建目标时默认保存为草稿状态,不直接提交);
|
|
177
|
+
// 编辑必须显式指定 status,避免把正式目标静默回退成草稿。
|
|
178
|
+
let status;
|
|
179
|
+
if (args.status === undefined) {
|
|
180
|
+
if (mode === 'update') {
|
|
181
|
+
throw okrValidationError('status_required', '标准版编辑目标必须显式传 status(0-保存草稿 1-正式提交),避免把正式目标回退为草稿');
|
|
182
|
+
}
|
|
183
|
+
status = '0';
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
status = String(args.status);
|
|
187
|
+
}
|
|
188
|
+
return buildStandardSaveBody({
|
|
189
|
+
...(host.goalId ? { goalId: host.goalId } : {}),
|
|
190
|
+
name: args.name === undefined ? String(base.name ?? '') : String(args.name),
|
|
191
|
+
scopeId,
|
|
192
|
+
...(scopeDataId ? { scopeDataId } : {}),
|
|
193
|
+
periodType,
|
|
194
|
+
periodRange,
|
|
195
|
+
...(periodQuarter ? { periodQuarter } : {}),
|
|
196
|
+
principalid: args.principalid === undefined ? host.principalid : toPersonIdString(args.principalid, 'principalid'),
|
|
197
|
+
...(args.partners === undefined
|
|
198
|
+
? (base.partners === undefined ? {} : { partners: String(base.partners) })
|
|
199
|
+
: { partners: toPersonIdListText(args.partners, 'partners') }),
|
|
200
|
+
...(args.remark === undefined ? (base.remark === undefined ? {} : { remark: String(base.remark) }) : { remark: String(args.remark) }),
|
|
201
|
+
...(keyresultList ? { keyresultList } : {}),
|
|
202
|
+
status,
|
|
203
|
+
parentId: args.parent_id === undefined
|
|
204
|
+
? (base.parentId === undefined ? '0' : String(base.parentId))
|
|
205
|
+
: toIdString(args.parent_id, 'parent_id'),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function goalPreview(link, body) {
|
|
209
|
+
if (link === 'standard') {
|
|
210
|
+
return {
|
|
211
|
+
目标名称: body.name,
|
|
212
|
+
目标级别: body.scopeId === '2' ? '部门目标' : '个人目标',
|
|
213
|
+
周期类型: body.periodType,
|
|
214
|
+
周期范围: body.periodRange,
|
|
215
|
+
责任人: body.principalid,
|
|
216
|
+
参与人: body.partners ?? null,
|
|
217
|
+
状态: body.status === '1' ? '正式提交' : '保存草稿',
|
|
218
|
+
上级目标: body.parentId ?? '0',
|
|
219
|
+
描述: body.remark ?? null,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
目标名称: body.obj_name,
|
|
224
|
+
目标级别: body.scope_id === '2' ? '部门目标' : '个人目标',
|
|
225
|
+
周期类型: body.period_type,
|
|
226
|
+
周期范围: `${body.period_range_0 ?? ''} ~ ${body.period_range_1 ?? ''}`,
|
|
227
|
+
责任人: body.principalid,
|
|
228
|
+
参与人: body.partners ?? null,
|
|
229
|
+
进展: body.m_status ?? null,
|
|
230
|
+
进度: body.progress ?? null,
|
|
231
|
+
紧急状态: body.wb_state ?? null,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async function buildGoalPayload(args, env, mode) {
|
|
235
|
+
const { host } = env;
|
|
236
|
+
const { link } = await resolveLink(host);
|
|
237
|
+
const session = await host.getSession();
|
|
238
|
+
const notes = [];
|
|
239
|
+
const warnings = [];
|
|
240
|
+
const goalId = mode === 'update' ? toIdString(args.id, 'id') : null;
|
|
241
|
+
const hasPeriod = args.period_type !== undefined || args.period_range !== undefined;
|
|
242
|
+
let period = null;
|
|
243
|
+
if (hasPeriod) {
|
|
244
|
+
if (mode === 'create' && (args.period_type === undefined || args.period_range === undefined)) {
|
|
245
|
+
throw okrValidationError('period_required', 'okr.create 必须同时传 period_type 与 period_range');
|
|
246
|
+
}
|
|
247
|
+
period = resolvePeriod(args);
|
|
248
|
+
}
|
|
249
|
+
else if (mode === 'create') {
|
|
250
|
+
throw okrValidationError('period_required', 'okr.create 必须同时传 period_type 与 period_range');
|
|
251
|
+
}
|
|
252
|
+
if (mode === 'create' && (args.name === undefined || String(args.name).trim() === '')) {
|
|
253
|
+
throw okrValidationError('name_required', 'name(目标名称)必填');
|
|
254
|
+
}
|
|
255
|
+
const current = mode === 'update' ? await fetchGoalRaw(host, link, goalId) : null;
|
|
256
|
+
if (mode === 'update' && !current) {
|
|
257
|
+
throw okrValidationError('not_found', `未查询到目标 ${goalId},无法编辑;请确认目标 ID 或查看权限`);
|
|
258
|
+
}
|
|
259
|
+
const principalid = args.principalid === undefined
|
|
260
|
+
? (mode === 'update' ? (goalPrincipalId(current, link) ?? session.userId) : session.userId)
|
|
261
|
+
: toPersonIdString(args.principalid, 'principalid');
|
|
262
|
+
if (link === 'standard') {
|
|
263
|
+
if (args.m_status !== undefined || args.wb_state !== undefined || args.principal_dept !== undefined) {
|
|
264
|
+
notes.push('eb_only_fields_ignored:m_status/wb_state/principal_dept');
|
|
265
|
+
}
|
|
266
|
+
const body = buildStandardGoalBody(args, mode, { current, principalid, period, ...(goalId ? { goalId } : {}) });
|
|
267
|
+
return {
|
|
268
|
+
link,
|
|
269
|
+
endpoint: mode === 'create' ? 'standard-create' : 'standard-update',
|
|
270
|
+
goalId,
|
|
271
|
+
body,
|
|
272
|
+
preview: goalPreview(link, body),
|
|
273
|
+
notes,
|
|
274
|
+
warnings,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (args.keyresult_list !== undefined) {
|
|
278
|
+
notes.push('standard_only_fields_ignored:keyresult_list');
|
|
279
|
+
}
|
|
280
|
+
let mainTable;
|
|
281
|
+
if (mode === 'create') {
|
|
282
|
+
mainTable = buildEbCreateMainTable(args, period, principalid);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
const base = pickKeys(current, EB_UPDATE_KEYS);
|
|
286
|
+
mainTable = compact({
|
|
287
|
+
...base,
|
|
288
|
+
id: goalId,
|
|
289
|
+
...(hasPeriod ? ebPeriodFields(period) : {}),
|
|
290
|
+
...(args.name === undefined ? {} : { obj_name: String(args.name) }),
|
|
291
|
+
...(args.scope_id === undefined ? {} : { scope_id: String(args.scope_id) }),
|
|
292
|
+
principalid,
|
|
293
|
+
...(args.partners === undefined ? {} : { partners: toPersonIdListText(args.partners, 'partners') }),
|
|
294
|
+
...(args.parent_id === undefined ? {} : { parent_id: toIdString(args.parent_id, 'parent_id') }),
|
|
295
|
+
...(args.category_id === undefined ? {} : { category_id: String(args.category_id) }),
|
|
296
|
+
...(args.remark === undefined ? {} : { remark: String(args.remark) }),
|
|
297
|
+
...(args.wb_state === undefined ? {} : { wb_state: String(args.wb_state) }),
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
link,
|
|
302
|
+
endpoint: mode === 'create' ? 'eb-create' : 'eb-update',
|
|
303
|
+
goalId,
|
|
304
|
+
body: mainTable,
|
|
305
|
+
preview: goalPreview(link, mainTable),
|
|
306
|
+
notes,
|
|
307
|
+
warnings,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const GOAL_PREPARE_SCHEMA = schemaOf({ ...GOAL_WRITE_FIELDS }, { required: ['name', 'period_type', 'period_range'] });
|
|
311
|
+
const GOAL_UPDATE_PREPARE_SCHEMA = schemaOf({ id: SCHEMA_ID, ...GOAL_WRITE_FIELDS }, { required: ['id'] });
|
|
312
|
+
const GOAL_APPLY_EXTRA = { confirm: { const: true }, continuation: { type: 'string', minLength: 1 } };
|
|
313
|
+
function standardSavePath(endpoint) {
|
|
314
|
+
return endpoint === 'standard-create' ? '/api/workrelate/goal/create' : '/api/workrelate/goal/update';
|
|
315
|
+
}
|
|
316
|
+
function ebSavePath(endpoint) {
|
|
317
|
+
return endpoint === 'eb-create'
|
|
318
|
+
? '/api/ebuilder/form/formdata/v2/saveFormData/okr_createGoal'
|
|
319
|
+
: '/api/ebuilder/form/formdata/v2/updateFormData/okr_updateGoal';
|
|
320
|
+
}
|
|
321
|
+
/** 提交目标写入请求,并把返回的新建/编辑目标 ID 转成可点击详情页链接。 */
|
|
322
|
+
async function submitGoalWrite(env, payload, action) {
|
|
323
|
+
const { host } = env;
|
|
324
|
+
const session = await host.getSession();
|
|
325
|
+
let response;
|
|
326
|
+
try {
|
|
327
|
+
if (payload.link === 'standard') {
|
|
328
|
+
response = await host.request({
|
|
329
|
+
method: 'POST',
|
|
330
|
+
path: standardSavePath(payload.endpoint),
|
|
331
|
+
operation: action,
|
|
332
|
+
body: payload.body,
|
|
333
|
+
});
|
|
334
|
+
// 标准版新建/编辑接口源资料只给出 `{data:{...}}` 成功结构,没有独立成功码:
|
|
335
|
+
// 仅在明确返回 status=false 时判为业务失败,避免把正常返回误判为失败。
|
|
336
|
+
if (response?.status === false || response?.status === 'false') {
|
|
337
|
+
throw okrApiError('business_error', businessMessage(response), { response: businessMessage(response) });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
response = await host.request({
|
|
342
|
+
method: 'POST',
|
|
343
|
+
path: ebSavePath(payload.endpoint),
|
|
344
|
+
operation: action,
|
|
345
|
+
body: {
|
|
346
|
+
datajson: {
|
|
347
|
+
datas: [{ mainTable: payload.body }],
|
|
348
|
+
header: { operator: session.userId },
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
if (!isDatajsonSuccess(response)) {
|
|
353
|
+
throw okrApiError('business_error', businessMessage(response), { response: businessMessage(response) });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
if (error?.type === 'network') {
|
|
359
|
+
throw okrPartialError('目标写入请求发送后连接中断,是否已写入不确定,禁止自动重试,请通过 okr.get 复核', { attempted: { endpoint: payload.endpoint, goalId: payload.goalId, preview: payload.preview } }, { stage: payload.endpoint });
|
|
360
|
+
}
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
let writtenId = payload.goalId;
|
|
364
|
+
if (!writtenId && payload.link === 'standard') {
|
|
365
|
+
const rawId = response?.data?.id;
|
|
366
|
+
writtenId = rawId === undefined || rawId === null || rawId === '' ? null : String(rawId);
|
|
367
|
+
}
|
|
368
|
+
if (!writtenId && payload.link === 'eb') {
|
|
369
|
+
const dataIds = response?.datajson?.dataIds;
|
|
370
|
+
if (Array.isArray(dataIds) && dataIds.length > 0 && dataIds[0] !== undefined && dataIds[0] !== null) {
|
|
371
|
+
writtenId = String(dataIds[0]);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const resolver = await createDetailUrlResolver(host, payload.link);
|
|
375
|
+
const detailPath = await resolver.resolve(writtenId);
|
|
376
|
+
const detailUrl = okrAbsoluteUrl(session.baseUrl, detailPath);
|
|
377
|
+
return okrSuccess(action, {
|
|
378
|
+
status: 'COMPLETE',
|
|
379
|
+
link: payload.link,
|
|
380
|
+
endpoint: payload.endpoint,
|
|
381
|
+
goalId: writtenId,
|
|
382
|
+
path: detailPath,
|
|
383
|
+
detailUrl,
|
|
384
|
+
anchor: '查看目标详情',
|
|
385
|
+
result: response,
|
|
386
|
+
}, {
|
|
387
|
+
...(payload.notes.length > 0 ? { notes: payload.notes } : {}),
|
|
388
|
+
...(resolver.warnings.length > 0 ? { warnings: resolver.warnings } : {}),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
function goalWriteDefinitions(mode) {
|
|
392
|
+
const singular = mode === 'create' ? 'create' : 'update';
|
|
393
|
+
const label = mode === 'create' ? '新建目标' : '编辑目标';
|
|
394
|
+
const prepareName = `okr.${singular}.prepare`;
|
|
395
|
+
const applyName = `okr.${singular}.apply`;
|
|
396
|
+
const prepare = {
|
|
397
|
+
name: prepareName,
|
|
398
|
+
resource: 'okr',
|
|
399
|
+
method: `${singular}.prepare`,
|
|
400
|
+
action: prepareName,
|
|
401
|
+
summary: mode === 'create'
|
|
402
|
+
? '准备新建目标:归一化周期/人员/默认值(进展默认进行中、进度默认 0、责任人默认当前用户),生成确认预览,不写入'
|
|
403
|
+
: '准备编辑目标:先查询当前目标详情并合并修改(全量覆盖),生成确认预览,不写入',
|
|
404
|
+
capability: 'okr',
|
|
405
|
+
risk: 'read-before-write',
|
|
406
|
+
requiresConfirmation: true,
|
|
407
|
+
inputSchema: mode === 'create' ? GOAL_PREPARE_SCHEMA : GOAL_UPDATE_PREPARE_SCHEMA,
|
|
408
|
+
handler: async (args, env) => {
|
|
409
|
+
const payload = await buildGoalPayload(args, env, mode);
|
|
410
|
+
const session = await env.host.getSession();
|
|
411
|
+
const token = await env.continuation.issue({
|
|
412
|
+
operation: applyName,
|
|
413
|
+
baseUrl: session.baseUrl,
|
|
414
|
+
profile: session.profile,
|
|
415
|
+
userId: session.userId,
|
|
416
|
+
link: payload.link,
|
|
417
|
+
endpoint: payload.endpoint,
|
|
418
|
+
goalId: payload.goalId,
|
|
419
|
+
payload: payload.body,
|
|
420
|
+
fingerprint: fingerprint(payload.body),
|
|
421
|
+
}, CONTINUATION_TTL_SECONDS * 1000);
|
|
422
|
+
return okrSuccess(prepareName, {
|
|
423
|
+
status: 'AWAITING_CONFIRMATION',
|
|
424
|
+
link: payload.link,
|
|
425
|
+
endpoint: payload.endpoint,
|
|
426
|
+
goalId: payload.goalId,
|
|
427
|
+
preview: payload.preview,
|
|
428
|
+
continuation: token,
|
|
429
|
+
expiresInSeconds: CONTINUATION_TTL_SECONDS,
|
|
430
|
+
}, {
|
|
431
|
+
...(payload.notes.length > 0 ? { notes: payload.notes } : {}),
|
|
432
|
+
...(payload.warnings.length > 0 ? { warnings: payload.warnings } : {}),
|
|
433
|
+
});
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
const applyFields = mode === 'create'
|
|
437
|
+
? { ...GOAL_WRITE_FIELDS, ...GOAL_APPLY_EXTRA }
|
|
438
|
+
: { id: SCHEMA_ID, ...GOAL_WRITE_FIELDS, ...GOAL_APPLY_EXTRA };
|
|
439
|
+
const apply = {
|
|
440
|
+
name: applyName,
|
|
441
|
+
resource: 'okr',
|
|
442
|
+
method: `${singular}.apply`,
|
|
443
|
+
action: applyName,
|
|
444
|
+
summary: `提交${label};必须在用户明确确认后调用,且要求 prepare 返回的 continuation`,
|
|
445
|
+
capability: 'okr',
|
|
446
|
+
risk: 'high-risk-write',
|
|
447
|
+
requiresConfirmation: true,
|
|
448
|
+
inputSchema: schemaOf(applyFields, { required: mode === 'create' ? ['confirm', 'continuation'] : ['id', 'confirm', 'continuation'] }),
|
|
449
|
+
handler: async (args, env) => {
|
|
450
|
+
const lease = await ensureConfirmLease(args, applyName, env);
|
|
451
|
+
if (mode === 'update') {
|
|
452
|
+
const goalId = toIdString(args.id, 'id');
|
|
453
|
+
if (String(lease.goalId ?? '') !== goalId) {
|
|
454
|
+
throw okrValidationError('target_changed', '目标 ID 与 prepare 时的快照不一致,请重新执行 prepare');
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const payload = {
|
|
458
|
+
link: lease.link,
|
|
459
|
+
endpoint: lease.endpoint,
|
|
460
|
+
goalId: lease.goalId ?? null,
|
|
461
|
+
body: (lease.payload ?? {}),
|
|
462
|
+
preview: {},
|
|
463
|
+
notes: [],
|
|
464
|
+
warnings: [],
|
|
465
|
+
};
|
|
466
|
+
if (lease.fingerprint !== fingerprint(payload.body)) {
|
|
467
|
+
throw okrValidationError('target_changed', '写入内容与 prepare 时的快照不一致,请重新执行 prepare');
|
|
468
|
+
}
|
|
469
|
+
return submitGoalWrite(env, payload, applyName);
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
return [prepare, apply];
|
|
473
|
+
}
|
|
474
|
+
// ---------------------------------------------------------------------------
|
|
475
|
+
// 关键成果保存(新建 / 修改)
|
|
476
|
+
// ---------------------------------------------------------------------------
|
|
477
|
+
const KR_WRITE_FIELDS = {
|
|
478
|
+
kr_id: { ...SCHEMA_ID, description: '关键成果 ID;不传为新建,传了为修改' },
|
|
479
|
+
object_id: { ...SCHEMA_ID, description: '所属目标 ID(ebuilder 链路必填)' },
|
|
480
|
+
name: { type: 'string', minLength: 1, description: '关键成果名称' },
|
|
481
|
+
principalid: SCHEMA_PERSON_ID,
|
|
482
|
+
participants: {
|
|
483
|
+
oneOf: [SCHEMA_ID_LIST_TEXT, { type: 'array', items: SCHEMA_PERSON_ID }],
|
|
484
|
+
description: '参与人人员 ID(仅标准版链路)',
|
|
485
|
+
},
|
|
486
|
+
target_value: { type: 'number', description: '目标值' },
|
|
487
|
+
actual_value: { type: 'number', description: '实际值' },
|
|
488
|
+
weight: { type: 'number', description: '权重' },
|
|
489
|
+
progress: { type: 'number', minimum: 0, maximum: 100, description: '进度' },
|
|
490
|
+
score: { type: 'number', description: '分数' },
|
|
491
|
+
range: {
|
|
492
|
+
oneOf: [
|
|
493
|
+
{ type: 'array', items: SCHEMA_DATE, minItems: 2, maxItems: 2 },
|
|
494
|
+
{ type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2},\\d{4}-\\d{2}-\\d{2}$' },
|
|
495
|
+
],
|
|
496
|
+
description: '起止日期 [开始, 结束],yyyy-MM-dd',
|
|
497
|
+
},
|
|
498
|
+
kr_unit: { type: 'string', description: '单位(仅 ebuilder 链路)' },
|
|
499
|
+
confidence: { type: 'number', description: '信心指数(仅 ebuilder 链路)' },
|
|
500
|
+
risk: { type: 'string', enum: ['1', '2', '3'], description: '风险指数:1-正常 2-有风险 3-已延期(仅 ebuilder 链路)' },
|
|
501
|
+
};
|
|
502
|
+
function resolveDateRange(value) {
|
|
503
|
+
if (value === undefined || value === null || value === '')
|
|
504
|
+
return null;
|
|
505
|
+
const parts = Array.isArray(value) ? value.map((item) => String(item)) : String(value).split(',');
|
|
506
|
+
if (parts.length !== 2 || !/^\d{4}-\d{2}-\d{2}$/.test(parts[0]) || !/^\d{4}-\d{2}-\d{2}$/.test(parts[1])) {
|
|
507
|
+
throw okrValidationError('range_invalid', 'range 必须是 [开始日期, 结束日期](yyyy-MM-dd)或「开始日期,结束日期」字符串');
|
|
508
|
+
}
|
|
509
|
+
if (parts[0] > parts[1]) {
|
|
510
|
+
throw okrValidationError('range_invalid', 'range 的开始日期不能晚于结束日期');
|
|
511
|
+
}
|
|
512
|
+
return [parts[0], parts[1]];
|
|
513
|
+
}
|
|
514
|
+
async function buildKrPayload(args, env) {
|
|
515
|
+
const { host } = env;
|
|
516
|
+
const { link } = await resolveLink(host);
|
|
517
|
+
const session = await host.getSession();
|
|
518
|
+
const notes = [];
|
|
519
|
+
const krId = args.kr_id === undefined ? null : toIdString(args.kr_id, 'kr_id');
|
|
520
|
+
const name = String(args.name ?? '').trim();
|
|
521
|
+
if (!name)
|
|
522
|
+
throw okrValidationError('name_required', 'name(关键成果名称)必填');
|
|
523
|
+
const principalid = args.principalid === undefined
|
|
524
|
+
? session.userId
|
|
525
|
+
: toPersonIdString(args.principalid, 'principalid');
|
|
526
|
+
const range = resolveDateRange(args.range);
|
|
527
|
+
if (link === 'standard') {
|
|
528
|
+
for (const field of ['kr_unit', 'confidence', 'risk']) {
|
|
529
|
+
if (args[field] !== undefined)
|
|
530
|
+
notes.push(`eb_only_fields_ignored:${field}`);
|
|
531
|
+
}
|
|
532
|
+
const objectId = toIdString(args.object_id, 'object_id');
|
|
533
|
+
const body = compact({
|
|
534
|
+
baseInfo: compact({
|
|
535
|
+
...(krId ? { id: krId } : {}),
|
|
536
|
+
objectId,
|
|
537
|
+
name,
|
|
538
|
+
principalId: principalid,
|
|
539
|
+
...(args.participants === undefined
|
|
540
|
+
? {}
|
|
541
|
+
: { participants: toPersonIdListText(args.participants, 'participants') }),
|
|
542
|
+
...(args.target_value === undefined ? {} : { targetValue: args.target_value }),
|
|
543
|
+
...(args.actual_value === undefined ? {} : { actualValue: args.actual_value }),
|
|
544
|
+
...(args.weight === undefined ? {} : { weight: args.weight }),
|
|
545
|
+
...(args.progress === undefined ? {} : { progress: args.progress }),
|
|
546
|
+
...(args.score === undefined ? {} : { score: args.score }),
|
|
547
|
+
...(range ? { range } : {}),
|
|
548
|
+
}),
|
|
549
|
+
});
|
|
550
|
+
return {
|
|
551
|
+
link,
|
|
552
|
+
endpoint: 'standard-kr-save',
|
|
553
|
+
body,
|
|
554
|
+
preview: {
|
|
555
|
+
关键成果名称: name,
|
|
556
|
+
所属目标: objectId,
|
|
557
|
+
责任人: principalid,
|
|
558
|
+
目标值: args.target_value ?? null,
|
|
559
|
+
实际值: args.actual_value ?? null,
|
|
560
|
+
权重: args.weight ?? null,
|
|
561
|
+
起止日期: range ? range.join(' ~ ') : null,
|
|
562
|
+
操作: krId ? `修改(${krId})` : '新建',
|
|
563
|
+
},
|
|
564
|
+
notes,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
if (args.participants !== undefined)
|
|
568
|
+
notes.push('standard_only_fields_ignored:participants');
|
|
569
|
+
const mainTable = compact({
|
|
570
|
+
...(krId ? { id: krId } : {}),
|
|
571
|
+
object_id: toIdString(args.object_id, 'object_id'),
|
|
572
|
+
wk_name: name,
|
|
573
|
+
principalid,
|
|
574
|
+
...(args.target_value === undefined ? {} : { target_value: args.target_value }),
|
|
575
|
+
...(args.actual_value === undefined ? {} : { actual_value: args.actual_value }),
|
|
576
|
+
...(args.weight === undefined ? {} : { weight: args.weight }),
|
|
577
|
+
...(args.progress === undefined ? {} : { progress: args.progress }),
|
|
578
|
+
...(args.score === undefined ? {} : { score: args.score }),
|
|
579
|
+
...(range ? { plan_start_end_0: range[0], plan_start_end_1: range[1] } : {}),
|
|
580
|
+
...(args.kr_unit === undefined ? {} : { kr_unit: String(args.kr_unit) }),
|
|
581
|
+
...(args.confidence === undefined ? {} : { confidence: args.confidence }),
|
|
582
|
+
...(args.risk === undefined ? {} : { risk: String(args.risk) }),
|
|
583
|
+
});
|
|
584
|
+
return {
|
|
585
|
+
link,
|
|
586
|
+
endpoint: krId ? 'eb-kr-update' : 'eb-kr-create',
|
|
587
|
+
body: mainTable,
|
|
588
|
+
preview: {
|
|
589
|
+
关键成果名称: name,
|
|
590
|
+
所属目标: mainTable.object_id,
|
|
591
|
+
责任人: principalid,
|
|
592
|
+
目标值: args.target_value ?? null,
|
|
593
|
+
实际值: args.actual_value ?? null,
|
|
594
|
+
权重: args.weight ?? null,
|
|
595
|
+
起止日期: range ? range.join(' ~ ') : null,
|
|
596
|
+
操作: krId ? `修改(${krId})` : '新建',
|
|
597
|
+
},
|
|
598
|
+
notes,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
const okrKrSavePrepare = {
|
|
602
|
+
name: 'okr.kr.save.prepare',
|
|
603
|
+
resource: 'okr',
|
|
604
|
+
method: 'kr.save.prepare',
|
|
605
|
+
action: 'okr.kr.save.prepare',
|
|
606
|
+
summary: '准备新建/修改关键成果(传 kr_id 为修改,不传为新建),生成确认预览,不写入',
|
|
607
|
+
capability: 'okr',
|
|
608
|
+
risk: 'read-before-write',
|
|
609
|
+
requiresConfirmation: true,
|
|
610
|
+
inputSchema: schemaOf({ ...KR_WRITE_FIELDS }, { required: ['object_id', 'name'] }),
|
|
611
|
+
handler: async (args, env) => {
|
|
612
|
+
const payload = await buildKrPayload(args, env);
|
|
613
|
+
const session = await env.host.getSession();
|
|
614
|
+
const token = await env.continuation.issue({
|
|
615
|
+
operation: 'okr.kr.save.apply',
|
|
616
|
+
baseUrl: session.baseUrl,
|
|
617
|
+
profile: session.profile,
|
|
618
|
+
userId: session.userId,
|
|
619
|
+
link: payload.link,
|
|
620
|
+
endpoint: payload.endpoint,
|
|
621
|
+
payload: payload.body,
|
|
622
|
+
fingerprint: fingerprint(payload.body),
|
|
623
|
+
}, CONTINUATION_TTL_SECONDS * 1000);
|
|
624
|
+
return okrSuccess('okr.kr.save.prepare', {
|
|
625
|
+
status: 'AWAITING_CONFIRMATION',
|
|
626
|
+
link: payload.link,
|
|
627
|
+
endpoint: payload.endpoint,
|
|
628
|
+
preview: payload.preview,
|
|
629
|
+
continuation: token,
|
|
630
|
+
expiresInSeconds: CONTINUATION_TTL_SECONDS,
|
|
631
|
+
}, payload.notes.length > 0 ? { notes: payload.notes } : undefined);
|
|
632
|
+
},
|
|
633
|
+
};
|
|
634
|
+
const okrKrSaveApply = {
|
|
635
|
+
name: 'okr.kr.save.apply',
|
|
636
|
+
resource: 'okr',
|
|
637
|
+
method: 'kr.save.apply',
|
|
638
|
+
action: 'okr.kr.save.apply',
|
|
639
|
+
summary: '提交关键成果保存;必须在用户明确确认后调用,且要求 prepare 返回的 continuation',
|
|
640
|
+
capability: 'okr',
|
|
641
|
+
risk: 'high-risk-write',
|
|
642
|
+
requiresConfirmation: true,
|
|
643
|
+
inputSchema: schemaOf({ ...KR_WRITE_FIELDS, confirm: { const: true }, continuation: { type: 'string', minLength: 1 } }, { required: ['object_id', 'name', 'confirm', 'continuation'] }),
|
|
644
|
+
handler: async (args, env) => {
|
|
645
|
+
const lease = await ensureConfirmLease(args, 'okr.kr.save.apply', env);
|
|
646
|
+
const body = (lease.payload ?? {});
|
|
647
|
+
if (lease.fingerprint !== fingerprint(body)) {
|
|
648
|
+
throw okrValidationError('target_changed', '写入内容与 prepare 时的快照不一致,请重新执行 prepare');
|
|
649
|
+
}
|
|
650
|
+
const session = await env.host.getSession();
|
|
651
|
+
const endpoint = lease.endpoint;
|
|
652
|
+
let response;
|
|
653
|
+
try {
|
|
654
|
+
if (endpoint === 'standard-kr-save') {
|
|
655
|
+
response = await env.host.request({
|
|
656
|
+
method: 'POST',
|
|
657
|
+
path: '/api/workrelate/goal/fs/krForm/saveForm',
|
|
658
|
+
operation: 'okr.kr.save.apply',
|
|
659
|
+
body,
|
|
660
|
+
});
|
|
661
|
+
if (!isFlagSuccess(response)) {
|
|
662
|
+
throw okrApiError('business_error', businessMessage(response), { response: businessMessage(response) });
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
response = await env.host.request({
|
|
667
|
+
method: 'POST',
|
|
668
|
+
path: endpoint === 'eb-kr-create'
|
|
669
|
+
? '/api/ebuilder/form/formdata/v2/saveFormData/okr_createKeyResult'
|
|
670
|
+
: '/api/ebuilder/form/formdata/v2/updateFormData/okr_updateKeyResult',
|
|
671
|
+
operation: 'okr.kr.save.apply',
|
|
672
|
+
body: {
|
|
673
|
+
datajson: {
|
|
674
|
+
datas: [{ mainTable: body }],
|
|
675
|
+
header: { operator: session.userId },
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
});
|
|
679
|
+
if (!isDatajsonSuccess(response)) {
|
|
680
|
+
throw okrApiError('business_error', businessMessage(response), { response: businessMessage(response) });
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
if (error?.type === 'network') {
|
|
686
|
+
throw okrPartialError('关键成果保存请求发送后连接中断,是否已写入不确定,禁止自动重试,请通过 okr.get / okr.kr.list 复核', { attempted: { endpoint } }, { stage: endpoint });
|
|
687
|
+
}
|
|
688
|
+
throw error;
|
|
689
|
+
}
|
|
690
|
+
return okrSuccess('okr.kr.save.apply', {
|
|
691
|
+
status: 'COMPLETE',
|
|
692
|
+
link: lease.link,
|
|
693
|
+
endpoint,
|
|
694
|
+
result: response,
|
|
695
|
+
});
|
|
696
|
+
},
|
|
697
|
+
};
|
|
698
|
+
// ---------------------------------------------------------------------------
|
|
699
|
+
// 添加目标对齐(ebuilder 链路专属)
|
|
700
|
+
// ---------------------------------------------------------------------------
|
|
701
|
+
const ALIGN_FIELDS = {
|
|
702
|
+
current_id: { ...SCHEMA_ID, description: '当前目标 ID' },
|
|
703
|
+
align_obj_id: { ...SCHEMA_ID, description: '对齐目标 ID' },
|
|
704
|
+
cur_obj_principalid: SCHEMA_PERSON_ID,
|
|
705
|
+
ali_obj_principalid: SCHEMA_PERSON_ID,
|
|
706
|
+
};
|
|
707
|
+
async function buildAlignPayload(args, env) {
|
|
708
|
+
const { host } = env;
|
|
709
|
+
const { link } = await resolveLink(host);
|
|
710
|
+
assertLinkSupported(link, 'eb', 'okr.align.create', 'e10 标准版链路源资料未提供「添加目标对齐」接口,无法执行目标对齐');
|
|
711
|
+
const currentId = toIdString(args.current_id, 'current_id');
|
|
712
|
+
const alignObjId = toIdString(args.align_obj_id, 'align_obj_id');
|
|
713
|
+
if (currentId === alignObjId) {
|
|
714
|
+
throw okrValidationError('align_invalid', '当前目标与对齐目标不能是同一个目标');
|
|
715
|
+
}
|
|
716
|
+
// 未显式提供双方负责人时,用目标详情接口取值;取不到就明确报错,绝不臆造。
|
|
717
|
+
let curPrincipal = args.cur_obj_principalid === undefined
|
|
718
|
+
? null
|
|
719
|
+
: toPersonIdString(args.cur_obj_principalid, 'cur_obj_principalid');
|
|
720
|
+
let aliPrincipal = args.ali_obj_principalid === undefined
|
|
721
|
+
? null
|
|
722
|
+
: toPersonIdString(args.ali_obj_principalid, 'ali_obj_principalid');
|
|
723
|
+
if (!curPrincipal) {
|
|
724
|
+
const raw = await fetchGoalRaw(host, 'eb', currentId);
|
|
725
|
+
curPrincipal = goalPrincipalId(raw, 'eb');
|
|
726
|
+
}
|
|
727
|
+
if (!aliPrincipal) {
|
|
728
|
+
const raw = await fetchGoalRaw(host, 'eb', alignObjId);
|
|
729
|
+
aliPrincipal = goalPrincipalId(raw, 'eb');
|
|
730
|
+
}
|
|
731
|
+
if (!curPrincipal || !aliPrincipal) {
|
|
732
|
+
throw okrValidationError('align_principal_unresolved', '无法从目标详情解析双方负责人,请显式提供 cur_obj_principalid 与 ali_obj_principalid(人员 ID 由 weaver-e10-hrm skill 解析)', { currentId, alignObjId });
|
|
733
|
+
}
|
|
734
|
+
const body = compact({
|
|
735
|
+
current_id: currentId,
|
|
736
|
+
align_obj_id: alignObjId,
|
|
737
|
+
cur_obj_principalid: curPrincipal,
|
|
738
|
+
ali_obj_principalid: aliPrincipal,
|
|
739
|
+
});
|
|
740
|
+
return {
|
|
741
|
+
body,
|
|
742
|
+
preview: {
|
|
743
|
+
当前目标: currentId,
|
|
744
|
+
当前目标负责人: curPrincipal,
|
|
745
|
+
对齐目标: alignObjId,
|
|
746
|
+
对齐目标负责人: aliPrincipal,
|
|
747
|
+
},
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
const okrAlignCreatePrepare = {
|
|
751
|
+
name: 'okr.align.create.prepare',
|
|
752
|
+
resource: 'okr',
|
|
753
|
+
method: 'align.create.prepare',
|
|
754
|
+
action: 'okr.align.create.prepare',
|
|
755
|
+
summary: '准备添加目标对齐(仅 ebuilder 链路):补全双方负责人并生成确认预览,不写入',
|
|
756
|
+
capability: 'okr',
|
|
757
|
+
risk: 'read-before-write',
|
|
758
|
+
requiresConfirmation: true,
|
|
759
|
+
inputSchema: schemaOf({ ...ALIGN_FIELDS }, { required: ['current_id', 'align_obj_id'] }),
|
|
760
|
+
handler: async (args, env) => {
|
|
761
|
+
const payload = await buildAlignPayload(args, env);
|
|
762
|
+
const session = await env.host.getSession();
|
|
763
|
+
const token = await env.continuation.issue({
|
|
764
|
+
operation: 'okr.align.create.apply',
|
|
765
|
+
baseUrl: session.baseUrl,
|
|
766
|
+
profile: session.profile,
|
|
767
|
+
userId: session.userId,
|
|
768
|
+
link: 'eb',
|
|
769
|
+
payload: payload.body,
|
|
770
|
+
fingerprint: fingerprint(payload.body),
|
|
771
|
+
}, CONTINUATION_TTL_SECONDS * 1000);
|
|
772
|
+
return okrSuccess('okr.align.create.prepare', {
|
|
773
|
+
status: 'AWAITING_CONFIRMATION',
|
|
774
|
+
link: 'eb',
|
|
775
|
+
preview: payload.preview,
|
|
776
|
+
continuation: token,
|
|
777
|
+
expiresInSeconds: CONTINUATION_TTL_SECONDS,
|
|
778
|
+
});
|
|
779
|
+
},
|
|
780
|
+
};
|
|
781
|
+
const okrAlignCreateApply = {
|
|
782
|
+
name: 'okr.align.create.apply',
|
|
783
|
+
resource: 'okr',
|
|
784
|
+
method: 'align.create.apply',
|
|
785
|
+
action: 'okr.align.create.apply',
|
|
786
|
+
summary: '提交添加目标对齐;必须在用户明确确认后调用,且要求 prepare 返回的 continuation',
|
|
787
|
+
capability: 'okr',
|
|
788
|
+
risk: 'high-risk-write',
|
|
789
|
+
requiresConfirmation: true,
|
|
790
|
+
inputSchema: schemaOf({ ...ALIGN_FIELDS, confirm: { const: true }, continuation: { type: 'string', minLength: 1 } }, { required: ['current_id', 'align_obj_id', 'confirm', 'continuation'] }),
|
|
791
|
+
handler: async (args, env) => {
|
|
792
|
+
const lease = await ensureConfirmLease(args, 'okr.align.create.apply', env);
|
|
793
|
+
const body = (lease.payload ?? {});
|
|
794
|
+
if (String(body.current_id ?? '') !== toIdString(args.current_id, 'current_id')) {
|
|
795
|
+
throw okrValidationError('target_changed', '当前目标 ID 与 prepare 时的快照不一致,请重新执行 prepare');
|
|
796
|
+
}
|
|
797
|
+
if (lease.fingerprint !== fingerprint(body)) {
|
|
798
|
+
throw okrValidationError('target_changed', '写入内容与 prepare 时的快照不一致,请重新执行 prepare');
|
|
799
|
+
}
|
|
800
|
+
const session = await env.host.getSession();
|
|
801
|
+
let response;
|
|
802
|
+
try {
|
|
803
|
+
response = await env.host.request({
|
|
804
|
+
method: 'POST',
|
|
805
|
+
path: '/api/ebuilder/form/formdata/v2/saveFormData/okr_createGoalAlign',
|
|
806
|
+
operation: 'okr.align.create.apply',
|
|
807
|
+
body: {
|
|
808
|
+
datajson: {
|
|
809
|
+
datas: [{ mainTable: body }],
|
|
810
|
+
header: { operator: session.userId },
|
|
811
|
+
},
|
|
812
|
+
},
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
catch (error) {
|
|
816
|
+
if (error?.type === 'network') {
|
|
817
|
+
throw okrPartialError('目标对齐请求发送后连接中断,是否已写入不确定,禁止自动重试,请通过 okr.get 复核', { attempted: { current_id: body.current_id, align_obj_id: body.align_obj_id } }, { stage: 'okr.align.create.apply' });
|
|
818
|
+
}
|
|
819
|
+
throw error;
|
|
820
|
+
}
|
|
821
|
+
if (!isDatajsonSuccess(response)) {
|
|
822
|
+
throw okrApiError('business_error', businessMessage(response), { response: businessMessage(response) });
|
|
823
|
+
}
|
|
824
|
+
return okrSuccess('okr.align.create.apply', {
|
|
825
|
+
status: 'COMPLETE',
|
|
826
|
+
link: 'eb',
|
|
827
|
+
result: response,
|
|
828
|
+
});
|
|
829
|
+
},
|
|
830
|
+
};
|
|
831
|
+
export const okrWriteOperations = [
|
|
832
|
+
...goalWriteDefinitions('create'),
|
|
833
|
+
...goalWriteDefinitions('update'),
|
|
834
|
+
okrKrSavePrepare,
|
|
835
|
+
okrKrSaveApply,
|
|
836
|
+
okrAlignCreatePrepare,
|
|
837
|
+
okrAlignCreateApply,
|
|
838
|
+
];
|