weaver-work-cli 0.1.26 → 0.1.29

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 (163) hide show
  1. package/README.md +7 -2
  2. package/dist/cmd/setup/index.js +1 -1
  3. package/dist/internal/e10/auth/commands.js +1 -6
  4. package/dist/internal/e10/auth/messages.js +3 -0
  5. package/dist/internal/e10/auth/session.js +8 -0
  6. package/dist/internal/e10/auth-recovery.js +148 -0
  7. package/dist/internal/e10/client.js +47 -15
  8. package/dist/internal/e10/context.js +8 -8
  9. package/dist/internal/e10/index.js +1 -0
  10. package/dist/internal/e10/request-runtime.js +51 -9
  11. package/dist/shortcuts/archive/host.js +3 -2
  12. package/dist/shortcuts/archive/manifest.js +1 -1
  13. package/dist/shortcuts/archive-oa/host.js +2 -1
  14. package/dist/shortcuts/archive-oa/manifest.js +1 -1
  15. package/dist/shortcuts/asset/host.js +7 -6
  16. package/dist/shortcuts/blog/host.js +4 -3
  17. package/dist/shortcuts/caizhilian/host.js +3 -2
  18. package/dist/shortcuts/caizhilian/manifest.js +1 -1
  19. package/dist/shortcuts/calendar/host.js +2 -1
  20. package/dist/shortcuts/contract/host.js +34 -59
  21. package/dist/shortcuts/ehr/host.js +2 -1
  22. package/dist/shortcuts/esb/host.js +2 -1
  23. package/dist/shortcuts/fna/host.js +6 -3
  24. package/dist/shortcuts/fna/manifest.js +10 -5
  25. package/dist/shortcuts/fna/operations/expense-chart.js +162 -0
  26. package/dist/shortcuts/fna/operations/registry.js +2 -0
  27. package/dist/shortcuts/hrm/host.js +2 -1
  28. package/dist/shortcuts/im/host.js +3 -2
  29. package/dist/shortcuts/invoice/host.js +9 -7
  30. package/dist/shortcuts/invoice/manifest.js +23 -142
  31. package/dist/shortcuts/invoice/operations/issuing-qys.js +266 -203
  32. package/dist/shortcuts/invoice/operations/registry.js +1 -6
  33. package/dist/shortcuts/jiuchuanhui/host.js +2 -1
  34. package/dist/shortcuts/mail/host.js +2 -1
  35. package/dist/shortcuts/meeting/host.js +2 -1
  36. package/dist/shortcuts/meeting/operations/eb/conflict.js +78 -12
  37. package/dist/shortcuts/okr/host.js +2 -1
  38. package/dist/shortcuts/plan/host.js +2 -1
  39. package/dist/shortcuts/project/host.js +2 -1
  40. package/dist/shortcuts/workflow/host.js +3 -5
  41. package/dist/shortcuts/workflow/index.js +4 -1
  42. package/dist/shortcuts/workflow/operations/chatscene.js +4 -22
  43. package/dist/shortcuts/workflow/operations/form.js +281 -166
  44. package/dist/shortcuts/workflow/operations/read.js +39 -8
  45. package/dist/shortcuts/workflow/operations/registry.js +2 -0
  46. package/dist/shortcuts/workflow/operations/todoSmartConvert/businessRules.js +331 -0
  47. package/dist/shortcuts/workflow/operations/todoSmartConvert/detailFieldRules.js +133 -0
  48. package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +326 -0
  49. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +113 -0
  50. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +259 -0
  51. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoListRows.js +64 -0
  52. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoOutputPlanner.js +28 -0
  53. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.aggregate.js +70 -0
  54. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +267 -0
  55. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +392 -0
  56. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +165 -0
  57. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +448 -0
  58. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +348 -0
  59. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +222 -0
  60. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.pipeline.js +66 -0
  61. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +502 -0
  62. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +540 -0
  63. package/dist/shortcuts/workflow/stdout-compact.js +7 -0
  64. package/docs/SKILL.md +1 -1
  65. package/docs/_catalog.md +2 -1
  66. package/docs/agent-invoice.md +1 -1
  67. package/docs/caizhilian.md +1 -1
  68. package/docs/e10-auth.md +1 -1
  69. package/docs/fna.md +7 -3
  70. package/docs/invoice.md +4 -6
  71. package/docs/todo-smart-convert/README.md +171 -0
  72. package/docs/todo-smart-convert/data-contract.md +236 -0
  73. package/docs/todo-smart-convert/mock/ai-todo-query-target.json +209 -0
  74. package/docs/todo-smart-convert/subgroup-rules.md +245 -0
  75. package/package.json +1 -1
  76. package/scripts/package-skill.mjs +1 -1
  77. package/skill-template/business-info.json +4 -1
  78. package/skill-template/domains/caizhilian.md +2 -1
  79. package/skill-template/domains/qiyecheng.md +1 -0
  80. package/skill-template/domains/yepiaotong.md +6 -2
  81. package/skill-template/skill-template.md +1 -0
  82. package/skills/weaver-e10-blog/SKILL.md +6 -1
  83. package/skills/weaver-e10-blog/references/blog-query.md +3 -1
  84. package/skills/weaver-e10-caizhilian/SKILL.md +18 -9
  85. package/skills/weaver-e10-caizhilian/references/action-flow.md +70 -0
  86. package/skills/weaver-e10-caizhilian/references/agent-entry.md +6 -2
  87. package/skills/weaver-e10-caizhilian/references/auth-and-install.md +207 -0
  88. package/skills/weaver-e10-caizhilian/references/file-collection.md +56 -54
  89. package/skills/weaver-e10-caizhilian/references/file-upload.md +4 -4
  90. package/skills/weaver-e10-caizhilian/references/host-executor-prompt.md +32 -0
  91. package/skills/weaver-e10-caizhilian/references/host-platform-integration.md +46 -0
  92. package/skills/weaver-e10-caizhilian/references/rule-index.md +56 -0
  93. package/skills/weaver-e10-caizhilian/references/setting-attach.md +5 -16
  94. package/skills/weaver-e10-caizhilian/references/setting-backup.md +4 -13
  95. package/skills/weaver-e10-caizhilian/references/setting-encrypt.md +6 -15
  96. package/skills/weaver-e10-caizhilian/references/setting-storage-docs-summary.md +126 -0
  97. package/skills/weaver-e10-caizhilian/references/setting-storage-field-map.md +57 -0
  98. package/skills/weaver-e10-caizhilian/references/setting-storage.md +142 -482
  99. package/skills/weaver-e10-caizhilian/references/source-manifest.json +7 -7
  100. package/skills/weaver-e10-caizhilian/references/task-workflow.md +87 -0
  101. package/skills/weaver-e10-caizhilian/references/withheld.md +2 -2
  102. package/skills/weaver-e10-caizhilian/references/write-protocol.md +7 -0
  103. package/skills/weaver-e10-calendar/SKILL.md +6 -1
  104. package/skills/weaver-e10-esb/SKILL.md +8 -0
  105. package/skills/weaver-e10-esb/references/input-format.md +3 -1
  106. package/skills/weaver-e10-esb/references/source-manifest.json +5 -5
  107. package/skills/weaver-e10-hrm/SKILL.md +6 -1
  108. package/skills/weaver-e10-hrm/references/employee.md +1 -1
  109. package/skills/weaver-e10-hrm/references/org.md +1 -1
  110. package/skills/weaver-e10-hrm/references/position.md +1 -1
  111. package/skills/weaver-e10-jinchengda/SKILL.md +2 -1
  112. package/skills/weaver-e10-jinchengda/references/contract-agent-entry.md +1 -1
  113. package/skills/weaver-e10-jiuchuanhui/SKILL.md +6 -1
  114. package/skills/weaver-e10-jucailin/SKILL.md +6 -1
  115. package/skills/weaver-e10-jucailin/references/hr-contact.md +1 -1
  116. package/skills/weaver-e10-jucailin/references/salary.md +1 -1
  117. package/skills/weaver-e10-mail/SKILL.md +6 -1
  118. package/skills/weaver-e10-meeting/SKILL.md +11 -6
  119. package/skills/weaver-e10-meeting/references/meeting-eb.md +7 -3
  120. package/skills/weaver-e10-meeting/references/meeting-queries.md +3 -1
  121. package/skills/weaver-e10-meeting/references/meeting-verification.md +4 -2
  122. package/skills/weaver-e10-okr/SKILL.md +6 -1
  123. package/skills/weaver-e10-okr/references/source-manifest.json +5 -5
  124. package/skills/weaver-e10-plan/SKILL.md +6 -1
  125. package/skills/weaver-e10-plan/references/plan-overview-and-routing.md +3 -1
  126. package/skills/weaver-e10-plan/references/plan-report-query.md +3 -1
  127. package/skills/weaver-e10-plan/references/source-manifest.json +7 -7
  128. package/skills/weaver-e10-qiyecheng/SKILL.md +16 -6
  129. package/skills/weaver-e10-qiyecheng/references/catalog.md +6 -3
  130. package/skills/weaver-e10-qiyecheng/references/expense-center.md +2 -0
  131. package/skills/weaver-e10-qiyecheng/references/expense-chart.md +78 -0
  132. package/skills/weaver-e10-qiyecheng/references/source-manifest.json +87 -33
  133. package/skills/weaver-e10-shared/SKILL.md +37 -14
  134. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +11 -8
  135. package/skills/weaver-e10-shared/references/non-interactive-environment.md +4 -4
  136. package/skills/weaver-e10-shared/references/weaver-e10-installation.md +3 -1
  137. package/skills/weaver-e10-wenshuding/SKILL.md +6 -1
  138. package/skills/weaver-e10-wenshuding/references/source-manifest.json +3 -3
  139. package/skills/weaver-e10-workflow/SKILL.md +23 -1
  140. package/skills/weaver-e10-workflow/references/source-manifest.json +1 -2
  141. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +282 -0
  142. package/skills/weaver-e10-workflow/references/workflow-create.md +30 -55
  143. package/skills/weaver-e10-workflow/scripts/todo-summary.cjs +695 -0
  144. package/skills/weaver-e10-yepiaotong/SKILL.md +19 -17
  145. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-examples.md +75 -78
  146. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-form-fill-rules.md +179 -0
  147. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-workflow.md +238 -0
  148. package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +7 -7
  149. package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +11 -5
  150. package/skills/weaver-e10-yepiaotong/references/reim-examples.md +55 -33
  151. package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +102 -23
  152. package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +1 -1
  153. package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +53 -42
  154. package/skills/weaver-e10-yepiaotong/references/source-manifest.json +94 -214
  155. package/skills/weaver-e10-yimiaoban/SKILL.md +9 -2
  156. package/skills/weaver-e10-yimiaoban/references/file-user-i18n.md +1 -1
  157. package/skills/weaver-e10-yimiaoban/references/msg-read.md +1 -1
  158. package/skills/weaver-e10-yimiaoban/references/source-manifest.json +9 -9
  159. package/skills/weaver-e10-ziguanjia/SKILL.md +6 -1
  160. package/dist/shortcuts/invoice/operations/issuing.js +0 -198
  161. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-field-rules.md +0 -161
  162. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-qys-ledger.md +0 -66
  163. package/skills/weaver-e10-yepiaotong/references/invoice-issuing.md +0 -107
@@ -1,5 +1,7 @@
1
+ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js';
1
2
  import { requireE10Session } from '../../internal/e10/context.js';
2
3
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
4
+ import { E10ApiError, createE10RequestRuntime, } from '../../internal/e10/request-runtime.js';
3
5
  import { ContractCliError, contractAuthError, contractNetworkError, contractValidationError, } from './errors.js';
4
6
  const DEFAULT_TIMEOUT_MS = 30_000;
5
7
  function assertPath(requestPath) {
@@ -13,38 +15,6 @@ function assertPath(requestPath) {
13
15
  throw contractValidationError('path_not_allowed', '今承达合同管理只能调用 /api/ 路径');
14
16
  }
15
17
  }
16
- function withQuery(baseUrl, requestPath, query = {}) {
17
- const url = new URL(`${baseUrl}${requestPath}`);
18
- for (const [key, value] of Object.entries(query)) {
19
- if (value === undefined || value === null || value === '')
20
- continue;
21
- if (Array.isArray(value)) {
22
- for (const item of value)
23
- url.searchParams.append(key, String(item));
24
- }
25
- else {
26
- url.searchParams.set(key, String(value));
27
- }
28
- }
29
- return url;
30
- }
31
- async function fetchWithTimeout(url, options, timeoutMs) {
32
- const controller = new AbortController();
33
- const timer = setTimeout(() => controller.abort(), timeoutMs);
34
- try {
35
- return await fetch(url, { ...options, signal: controller.signal, redirect: 'manual' });
36
- }
37
- catch (error) {
38
- if (error instanceof Error && error.name === 'AbortError') {
39
- throw contractNetworkError('timeout', `请求超时(${timeoutMs}ms)`, { retryable: false });
40
- }
41
- const message = error instanceof Error ? error.message : String(error);
42
- throw contractNetworkError('transport', `请求失败:${message}`, { retryable: true });
43
- }
44
- finally {
45
- clearTimeout(timer);
46
- }
47
- }
48
18
  function textOf(value, keys) {
49
19
  for (const key of keys) {
50
20
  const candidate = value?.[key];
@@ -111,15 +81,6 @@ function inferProfile(ctx) {
111
81
  const options = ctx.getGlobalOptions();
112
82
  return options.profile || getActiveProfile();
113
83
  }
114
- function requestHeaders(session) {
115
- return {
116
- Accept: 'application/json, text/plain, */*',
117
- Cookie: session.cookieHeader(),
118
- eteamsid: session.eteamsId,
119
- 'User-Agent': session.uaHeader(),
120
- Origin: session.getBaseUrl(),
121
- };
122
- }
123
84
  export function createContractHost(ctx, dependencies = {}) {
124
85
  let e10Session = null;
125
86
  let sessionContext = null;
@@ -144,10 +105,9 @@ export function createContractHost(ctx, dependencies = {}) {
144
105
  assertPath(path);
145
106
  const session = await ensureSession();
146
107
  const context = await getSession();
147
- const headers = requestHeaders(session);
148
- const finalQuery = { ...query };
108
+ const finalQuery = Object.fromEntries(Object.entries(query).filter(([, value]) => value !== undefined && value !== null && value !== ''));
149
109
  let finalBody = body;
150
- const init = { method, headers };
110
+ let formData;
151
111
  if (userIdField) {
152
112
  if (!context.userId) {
153
113
  throw contractAuthError('user_id_missing', '当前会话没有可用的用户 ID,请确认 E10 登录态');
@@ -158,26 +118,41 @@ export function createContractHost(ctx, dependencies = {}) {
158
118
  finalBody = { ...finalBody, [userIdField]: context.userId };
159
119
  }
160
120
  if (form) {
161
- const data = new FormData();
121
+ formData = new FormData();
162
122
  for (const [key, value] of Object.entries(form))
163
- data.append(key, value);
164
- init.body = data;
123
+ formData.append(key, value);
124
+ }
125
+ const runtime = createE10RequestRuntime(ctx, session, {
126
+ interceptors: dependencies.interceptors,
127
+ fetch: dependencies.fetch,
128
+ });
129
+ let response;
130
+ try {
131
+ response = await runtime.requestJson({
132
+ module: 'contract',
133
+ method,
134
+ path,
135
+ query: finalQuery,
136
+ body: formData ? undefined : finalBody,
137
+ formData,
138
+ timeoutMs,
139
+ });
165
140
  }
166
- else if (finalBody !== undefined) {
167
- headers['Content-Type'] = 'application/json;charset=UTF-8';
168
- init.body = JSON.stringify(finalBody);
141
+ catch (error) {
142
+ if (error instanceof E10ApiError && error.type === 'network') {
143
+ throw contractNetworkError(error.subtype, error.message, {
144
+ retryable: error.retryable,
145
+ details: error.details,
146
+ });
147
+ }
148
+ throw error;
169
149
  }
170
- const url = withQuery(context.baseUrl, path, finalQuery);
171
- const response = await fetchWithTimeout(url, init, timeoutMs);
172
150
  if (response.status === 401 || response.status === 302) {
173
- throw contractAuthError('session_expired', 'E10 登录态已失效,请先向用户确认 E10 登录域名,再运行 weaver-work-cli auth login --base-url <域名> 完成登录', { httpStatus: response.status, path });
174
- }
175
- const text = await response.text();
176
- let value = {};
177
- try {
178
- value = text ? JSON.parse(text) : {};
151
+ throw contractAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status, path });
179
152
  }
180
- catch {
153
+ const text = response.text;
154
+ const value = response.value;
155
+ if (response.parseError) {
181
156
  throw contractNetworkError('protocol', `接口返回不是合法 JSON(HTTP ${response.status})`, {
182
157
  retryable: false,
183
158
  details: { path, status: response.status },
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js';
2
3
  import { requireE10Session } from '../../internal/e10/context.js';
3
4
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
4
5
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
@@ -162,7 +163,7 @@ export function createEhrHost(ctx, dependencies = {}) {
162
163
  mapRuntimeNetworkError(error);
163
164
  }
164
165
  if (response.status === 401 || response.status === 302) {
165
- throw ehrAuthError('session_expired', 'E10 登录态已失效,请先向用户确认 E10 登录域名,再运行 weaver-work-cli auth login --base-url <域名> 完成登录', { httpStatus: response.status });
166
+ throw ehrAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
166
167
  }
167
168
  if (response.parseError) {
168
169
  if (response.status < 200 || response.status >= 300) {
@@ -1,3 +1,4 @@
1
+ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js';
1
2
  import { requireE10Session } from '../../internal/e10/context.js';
2
3
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
3
4
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
@@ -117,7 +118,7 @@ export function createEsbHost(ctx, dependencies = {}) {
117
118
  mapRuntimeNetworkError(error);
118
119
  }
119
120
  if (response.status === 401 || response.status === 302) {
120
- throw esbAuthError('session_expired', 'E10 登录态已失效,请先确认 E10 登录域名,再运行 weaver-work-cli auth login --base-url <域名> 完成登录', { httpStatus: response.status });
121
+ throw esbAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
121
122
  }
122
123
  if (response.parseError) {
123
124
  if (response.status < 200 || response.status >= 300) {
@@ -1,3 +1,4 @@
1
+ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js';
1
2
  import { requireE10Session } from '../../internal/e10/context.js';
2
3
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
3
4
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
@@ -48,8 +49,10 @@ export function assertPath(requestPath) {
48
49
  if (/^https?:/i.test(requestPath) || /[\r\n]/.test(requestPath) || requestPath.includes('/../')) {
49
50
  throw fnaValidationError('path_invalid', '请求路径不允许跨域或路径逃逸');
50
51
  }
51
- if (!requestPath.startsWith('/api/fna/') && !requestPath.startsWith('/api/app/fna/')) {
52
- throw fnaValidationError('path_not_allowed', 'FNA 费控中心只能调用 /api/fna/ 或 /api/app/fna/ 路径');
52
+ if (!requestPath.startsWith('/api/fna/')
53
+ && !requestPath.startsWith('/api/app/fna/')
54
+ && !requestPath.startsWith('/api/secondev/fna/')) {
55
+ throw fnaValidationError('path_not_allowed', 'FNA 费控中心只能调用 /api/fna/、/api/app/fna/ 或 /api/secondev/fna/ 路径');
53
56
  }
54
57
  }
55
58
  function omitEmptyQueryValues(query) {
@@ -114,7 +117,7 @@ export function createFnaHost(ctx, dependencies = {}) {
114
117
  mapRuntimeNetworkError(error);
115
118
  }
116
119
  if (response.status === 401 || response.status === 302) {
117
- throw fnaAuthError('session_expired', 'E10 登录态已失效,请先向用户确认 E10 登录域名,再运行 weaver-work-cli auth login --base-url <域名> 完成登录', { httpStatus: response.status });
120
+ throw fnaAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
118
121
  }
119
122
  if (response.parseError) {
120
123
  if (response.status < 200 || response.status >= 300) {
@@ -1,10 +1,11 @@
1
1
  import { fnaOperationDefinitions } from './operations/registry.js';
2
2
  /**
3
3
  * FNA 费控中心(齐业成)模块禁用/暂缓边界。
4
- * 11 个源接口全部落地;以下几类能力不在本 CLI,写入 manifest 供 Agent 判断,
4
+ * 12 个源接口全部落地;以下几类能力不在本 CLI,写入 manifest 供 Agent 判断,
5
5
  * 不得编造 operation 或绕过 CLI 直连接口:
6
6
  * - 姓名/流程名 → employeeId/workflowId 的人员与流程解析不在本模块;
7
- * - 借款「确认已还 confirmRepay」、核销明细等页面按钮能力未随源资料提供接口,未实现。
7
+ * - 借款「确认已还 confirmRepay」、核销明细等页面按钮能力未随源资料提供接口,未实现;
8
+ * - 报销报表 AI 问答只暴露自然语言问题,不接受也不回传 AI 生成的 SQL。
8
9
  */
9
10
  const FNA_WITHHELD = [
10
11
  {
@@ -15,6 +16,10 @@ const FNA_WITHHELD = [
15
16
  name: 'fna.resolve.person.workflow',
16
17
  reason: '姓名→employeeId、流程名→workflowId 的解析依赖人员/流程基表接口,源资料未提供;需要时请使用已提供的 ID 或由用户在对话中提供。',
17
18
  },
19
+ {
20
+ name: 'fna.expense.chart.raw.sql',
21
+ reason: '报销报表 AI 问答由服务端生成 SQL 并做租户隔离改写与安全校验;CLI 只接受 userInput,不暴露、不回传 SQL,也不允许调用方绕过安全校验自写 SQL。',
22
+ },
18
23
  ];
19
24
  function buildOperations() {
20
25
  return fnaOperationDefinitions().map((definition) => ({
@@ -30,12 +35,12 @@ function buildOperations() {
30
35
  export const fnaManifest = {
31
36
  schemaVersion: 1,
32
37
  name: 'fna',
33
- title: '泛微E10齐业成费控中心(预算查询/费用中心/借款管理/发票核销)',
34
- version: '0.1.0',
38
+ title: '泛微E10齐业成费控中心(预算查询/费用中心/报销报表/借款管理/发票核销)',
39
+ version: '0.1.1',
35
40
  host: 'e10-fna',
36
41
  status: 'builtin',
37
42
  sourceSkill: 'weaver-e10-qiyecheng',
38
- capabilities: ['budget', 'expense', 'loan', 'invoice-writeoff'],
43
+ capabilities: ['budget', 'expense', 'expense-chart', 'loan', 'invoice-writeoff'],
39
44
  operations: buildOperations(),
40
45
  withheldOperations: FNA_WITHHELD,
41
46
  };
@@ -0,0 +1,162 @@
1
+ import { FnaCliError, fnaValidationError } from '../errors.js';
2
+ import { fnaSuccess, READ, schemaOf, SCHEMA_INTEGER_POSITIVE, SCHEMA_STRING_NONEMPTY } from './types.js';
3
+ import { unwrapData } from './shared.js';
4
+ /** 实测真实查询最慢约 66.6s;超时必须覆盖该耗时,且失败后不得自动重试(每次都会消耗模型额度)。 */
5
+ const CHART_TIMEOUT_MS = 180_000;
6
+ const DEFAULT_MAX_ROWS = 200;
7
+ const MAX_ROWS_LIMIT = 5000;
8
+ const USER_INPUT_LIMIT = 500;
9
+ const BLOCKED_MESSAGE = /拦截|高危|安全校验/;
10
+ const CLARIFY_MESSAGE = /不明确|请补充|补充查询/;
11
+ function requireUserInput(value) {
12
+ if (typeof value !== 'string' || value.trim().length === 0) {
13
+ throw fnaValidationError('user_input_required', 'userInput 必填(自然语言问题,不能为空白)');
14
+ }
15
+ const userInput = value.trim();
16
+ if (userInput.length > USER_INPUT_LIMIT) {
17
+ throw fnaValidationError('user_input_too_long', 'userInput 不能超过 500 个字符');
18
+ }
19
+ return userInput;
20
+ }
21
+ function resolveMaxRows(value) {
22
+ if (value === undefined || value === null || value === '')
23
+ return DEFAULT_MAX_ROWS;
24
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 1 || value > MAX_ROWS_LIMIT) {
25
+ throw fnaValidationError('max_rows_invalid', 'maxRows 必须是 1 到 5000 的整数');
26
+ }
27
+ return value;
28
+ }
29
+ function mapChartApiError(error) {
30
+ if (error instanceof FnaCliError && error.type === 'api') {
31
+ const code = Number(error.code);
32
+ if (code === 400) {
33
+ throw fnaValidationError('user_input_required', 'userInput 未通过服务端校验(缺少或非法)');
34
+ }
35
+ if (code === 500) {
36
+ throw new FnaCliError('api', 'server_error', error.message || '报销报表服务异常', {
37
+ code: error.code ?? 500,
38
+ retryable: false,
39
+ details: error.details,
40
+ });
41
+ }
42
+ }
43
+ throw error;
44
+ }
45
+ function dataTypeOf(value) {
46
+ if (value === null)
47
+ return 'null';
48
+ if (Array.isArray(value))
49
+ return 'array';
50
+ return typeof value;
51
+ }
52
+ function isRow(value) {
53
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
54
+ }
55
+ function collectColumns(rows) {
56
+ const columns = [];
57
+ const seen = new Set();
58
+ for (const row of rows) {
59
+ for (const key of Object.keys(row)) {
60
+ if (seen.has(key))
61
+ continue;
62
+ seen.add(key);
63
+ columns.push(key);
64
+ }
65
+ }
66
+ return columns;
67
+ }
68
+ function classifyMessage(message) {
69
+ if (BLOCKED_MESSAGE.test(message))
70
+ return 'blocked';
71
+ if (CLARIFY_MESSAGE.test(message))
72
+ return 'clarify';
73
+ return undefined;
74
+ }
75
+ /**
76
+ * 归一化 getExpenseChart 的 data。code 恒为 200,成败只看 data 形态:
77
+ * 对象数组 → rows/empty;追问/安全拦截文案 → clarify/blocked(按文案推断);其余 → unexpected。
78
+ * maxRows 只截断本地返回,列名仍按服务端全量结果收集。
79
+ */
80
+ function normalizeChart(data, maxRows) {
81
+ if (Array.isArray(data) && data.every(isRow)) {
82
+ const columns = collectColumns(data);
83
+ const rows = data.slice(0, maxRows);
84
+ if (data.length === 0) {
85
+ return fnaSuccess('fna.expense.chart', {
86
+ kind: 'empty',
87
+ columns,
88
+ rows,
89
+ rowCount: 0,
90
+ truncated: false,
91
+ }, {
92
+ requestedRows: 0,
93
+ returnedRows: 0,
94
+ maxRows,
95
+ });
96
+ }
97
+ return fnaSuccess('fna.expense.chart', {
98
+ kind: 'rows',
99
+ columns,
100
+ rows,
101
+ rowCount: data.length,
102
+ truncated: rows.length < data.length,
103
+ }, {
104
+ requestedRows: data.length,
105
+ returnedRows: rows.length,
106
+ maxRows,
107
+ });
108
+ }
109
+ if (typeof data === 'string') {
110
+ const kind = classifyMessage(data);
111
+ if (kind) {
112
+ return fnaSuccess('fna.expense.chart', { kind, message: data }, { messageKindInferred: true });
113
+ }
114
+ }
115
+ return fnaSuccess('fna.expense.chart', { kind: 'unexpected' }, { dataType: dataTypeOf(data) });
116
+ }
117
+ /**
118
+ * 报销报表 AI 问答(只读,但每次调用消耗一次大模型对话 + 一次 SQL)。
119
+ * GET /api/secondev/fna/getExpenseChart?userInput=...
120
+ * 只接受自然语言问题,不接受、不回传 SQL;maxRows 仅做本地截断。
121
+ */
122
+ const fnaExpenseChart = {
123
+ name: 'fna.expense.chart',
124
+ resource: 'expense',
125
+ method: 'expense.chart',
126
+ action: 'fna.expense.chart',
127
+ summary: '报销报表 AI 问答:用自然语言查询分组/排名/趋势,返回 kind=rows/empty/clarify/blocked/unexpected;maxRows 只截断本地展示',
128
+ capability: 'expense-chart',
129
+ risk: READ,
130
+ inputSchema: schemaOf({
131
+ userInput: {
132
+ ...SCHEMA_STRING_NONEMPTY,
133
+ maxLength: USER_INPUT_LIMIT,
134
+ description: '自然语言问题(必填,≤500 字符)。只写业务问题,不要传 SQL,也不要自拼当前用户/部门 ID',
135
+ },
136
+ maxRows: {
137
+ ...SCHEMA_INTEGER_POSITIVE,
138
+ maximum: MAX_ROWS_LIMIT,
139
+ description: '本地展示行数上限,默认 200,最大 5000;不传给服务端,不改变查询',
140
+ },
141
+ }, { required: ['userInput'], description: '报销报表 AI 问答(二开接口,每次消耗一次大模型对话与一次 SQL)' }),
142
+ handler: async (args, { host }) => {
143
+ const userInput = requireUserInput(args.userInput);
144
+ const maxRows = resolveMaxRows(args.maxRows);
145
+ let response;
146
+ try {
147
+ response = await host.request({
148
+ method: 'GET',
149
+ path: '/api/secondev/fna/getExpenseChart',
150
+ query: { userInput },
151
+ timeoutMs: CHART_TIMEOUT_MS,
152
+ });
153
+ }
154
+ catch (error) {
155
+ throw mapChartApiError(error);
156
+ }
157
+ return normalizeChart(unwrapData(response), maxRows);
158
+ },
159
+ };
160
+ export const expenseChartOperations = [
161
+ fnaExpenseChart,
162
+ ];
@@ -1,6 +1,7 @@
1
1
  import { fnaValidationError } from '../errors.js';
2
2
  import { budgetOperations } from './budget.js';
3
3
  import { expenseCenterOperations } from './expense-center.js';
4
+ import { expenseChartOperations } from './expense-chart.js';
4
5
  import { loanOperations } from './loan.js';
5
6
  import { invoiceWriteOffOperations } from './invoice-writeoff.js';
6
7
  function buildRegistry() {
@@ -8,6 +9,7 @@ function buildRegistry() {
8
9
  const definitions = [
9
10
  ...budgetOperations,
10
11
  ...expenseCenterOperations,
12
+ ...expenseChartOperations,
11
13
  ...loanOperations,
12
14
  ...invoiceWriteOffOperations,
13
15
  ];
@@ -1,3 +1,4 @@
1
+ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js';
1
2
  import { requireE10Session } from '../../internal/e10/context.js';
2
3
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
3
4
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
@@ -114,7 +115,7 @@ export function createHrmHost(ctx, dependencies = {}) {
114
115
  mapRuntimeNetworkError(error);
115
116
  }
116
117
  if (response.status === 401 || response.status === 302) {
117
- throw hrmAuthError('session_expired', 'E10 登录态已失效,请先向用户确认 E10 登录域名,再运行 weaver-work-cli auth login --base-url <域名> 完成登录', { httpStatus: response.status });
118
+ throw hrmAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
118
119
  }
119
120
  if (response.parseError) {
120
121
  if (response.status < 200 || response.status >= 300) {
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
4
4
  import { dirname, extname, resolve } from 'node:path';
5
+ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js';
5
6
  import { requireE10Session } from '../../internal/e10/context.js';
6
7
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
7
8
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
@@ -120,7 +121,7 @@ export function createImHost(ctx, dependencies = {}) {
120
121
  mapRuntimeNetworkError(error);
121
122
  }
122
123
  if (response.status === 401 || response.status === 302) {
123
- throw imAuthError('session_expired', 'E10 登录态已失效,请先向用户确认 E10 登录域名,再运行 weaver-work-cli auth login --base-url <域名> 完成登录', { httpStatus: response.status });
124
+ throw imAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
124
125
  }
125
126
  if (response.parseError) {
126
127
  if (response.status < 200 || response.status >= 300) {
@@ -159,7 +160,7 @@ export function createImHost(ctx, dependencies = {}) {
159
160
  signal: controller.signal,
160
161
  });
161
162
  if (resp.status === 401 || resp.status === 302) {
162
- throw imAuthError('session_expired', '登录态已失效:下载需有效登录态,请重新登录后重试');
163
+ throw imAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE);
163
164
  }
164
165
  if (resp.status !== 200) {
165
166
  throw new ImCliError('api', 'download_failed', `下载失败 HTTP ${resp.status}`, {
@@ -1,5 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { readFile } from 'node:fs/promises';
3
+ import { E10_REAUTH_GUIDANCE, E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js';
3
4
  import { requireE10Session } from '../../internal/e10/context.js';
4
5
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
5
6
  import { createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
@@ -179,12 +180,12 @@ export function createInvoiceHost(ctx, dependencies = {}) {
179
180
  await clearCachedInvoiceToken();
180
181
  if (canRetry)
181
182
  return retry();
182
- throw invoiceAuthError('invoice_session_expired', '发票业务登录态已失效,已清理本地发票 Token;请重新运行 weaver-work-cli auth login --base-url <域名> --agent_type <当前智能体名称> 后再试', { ret: code });
183
+ throw invoiceAuthError('invoice_session_expired', `发票业务登录态已失效,已清理本地发票 Token。${E10_REAUTH_GUIDANCE}`, { ret: code });
183
184
  }
184
185
  throw businessError(value);
185
186
  }
186
187
  async function invoiceRequest(request, canRetryExpiredToken = true) {
187
- const { method = 'POST', path, query, body, operation, needsInvoiceToken = true, timeoutMs = 30_000, } = request;
188
+ const { method = 'POST', path, query, headers, body, operation, needsInvoiceToken = true, timeoutMs = 30_000, } = request;
188
189
  assertPath(path);
189
190
  const token = await ensureContext(needsInvoiceToken, timeoutMs, path);
190
191
  const response = await (await runtime()).requestJson({
@@ -193,12 +194,13 @@ export function createInvoiceHost(ctx, dependencies = {}) {
193
194
  method,
194
195
  path,
195
196
  query,
197
+ headers,
196
198
  body,
197
199
  timeoutMs,
198
200
  trustedHeaders: trustedInvoiceHeaders(token),
199
201
  });
200
202
  if (response.status === 401 || response.status === 302) {
201
- throw invoiceAuthError('session_expired', 'E10 登录态已失效,请先向用户确认 E10 登录域名,再运行 weaver-work-cli auth login --base-url <域名> --agent_type <当前智能体名称> 完成登录', { httpStatus: response.status });
203
+ throw invoiceAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
202
204
  }
203
205
  if (response.parseError) {
204
206
  if (response.status < 200 || response.status >= 300) {
@@ -257,7 +259,7 @@ export function createInvoiceHost(ctx, dependencies = {}) {
257
259
  trustedHeaders: trustedInvoiceHeaders(token),
258
260
  });
259
261
  if (response.status === 401 || response.status === 302) {
260
- throw invoiceAuthError('session_expired', 'E10 登录态已失效,请先向用户确认 E10 登录域名,再运行 weaver-work-cli auth login --base-url <域名> --agent_type <当前智能体名称> 完成登录', { httpStatus: response.status });
262
+ throw invoiceAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
261
263
  }
262
264
  if (response.parseError) {
263
265
  throw invoiceNetworkError('protocol', `接口返回不是合法 JSON(HTTP ${response.status})`, { retryable: false });
@@ -292,7 +294,7 @@ export function createInvoiceHost(ctx, dependencies = {}) {
292
294
  trustedHeaders: trustedInvoiceHeaders(token),
293
295
  });
294
296
  if (response.status === 401 || response.status === 302) {
295
- throw invoiceAuthError('session_expired', 'E10 登录态已失效,请先向用户确认 E10 登录域名,再运行 weaver-work-cli auth login --base-url <域名> --agent_type <当前智能体名称> 完成登录', { httpStatus: response.status });
297
+ throw invoiceAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
296
298
  }
297
299
  if (response.parseError) {
298
300
  throw invoiceNetworkError('protocol', `接口返回不是合法 JSON(HTTP ${response.status})`, { retryable: false });
@@ -319,7 +321,7 @@ export function createInvoiceHost(ctx, dependencies = {}) {
319
321
  trustedHeaders: trustedInvoiceHeaders(token),
320
322
  });
321
323
  if (response.status === 401 || response.status === 302) {
322
- throw invoiceAuthError('session_expired', 'E10 登录态已失效,请先向用户确认 E10 登录域名,再运行 weaver-work-cli auth login --base-url <域名> --agent_type <当前智能体名称> 完成登录', { httpStatus: response.status });
324
+ throw invoiceAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
323
325
  }
324
326
  const contentType = response.contentType;
325
327
  const declaredLength = Number(response.headers.get('content-length'));
@@ -340,7 +342,7 @@ export function createInvoiceHost(ctx, dependencies = {}) {
340
342
  await clearCachedInvoiceToken();
341
343
  if (canRetryExpiredToken && token)
342
344
  return invoiceDownload({ id, fid, needLog, operation, timeoutMs }, false);
343
- throw invoiceAuthError('invoice_session_expired', '发票业务登录态已失效,已清理本地发票 Token;请重新运行 weaver-work-cli auth login --base-url <域名> --agent_type <当前智能体名称> 后再试', { ret: responseCode(value), status: response.status });
345
+ throw invoiceAuthError('invoice_session_expired', `发票业务登录态已失效,已清理本地发票 Token。${E10_REAUTH_GUIDANCE}`, { ret: responseCode(value), status: response.status });
344
346
  }
345
347
  throw new InvoiceCliError('api', 'download_failed', responseMessage(value) || `下载失败(HTTP ${response.status})`, {
346
348
  code: response.status,