weaver-work-cli 0.1.26 → 0.1.28
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 +7 -2
- package/dist/cmd/setup/index.js +1 -1
- package/dist/internal/e10/auth/commands.js +1 -6
- package/dist/internal/e10/auth/messages.js +3 -0
- package/dist/internal/e10/auth/session.js +8 -0
- package/dist/internal/e10/auth-recovery.js +148 -0
- package/dist/internal/e10/client.js +47 -15
- package/dist/internal/e10/context.js +8 -8
- package/dist/internal/e10/index.js +1 -0
- package/dist/internal/e10/request-runtime.js +51 -9
- package/dist/shortcuts/archive/host.js +3 -2
- package/dist/shortcuts/archive/manifest.js +1 -1
- package/dist/shortcuts/archive-oa/host.js +2 -1
- package/dist/shortcuts/archive-oa/manifest.js +1 -1
- package/dist/shortcuts/asset/host.js +7 -6
- package/dist/shortcuts/blog/host.js +4 -3
- package/dist/shortcuts/caizhilian/host.js +3 -2
- package/dist/shortcuts/caizhilian/manifest.js +1 -1
- package/dist/shortcuts/calendar/host.js +2 -1
- package/dist/shortcuts/contract/host.js +34 -59
- package/dist/shortcuts/ehr/host.js +2 -1
- package/dist/shortcuts/esb/host.js +2 -1
- package/dist/shortcuts/fna/host.js +6 -3
- package/dist/shortcuts/fna/manifest.js +10 -5
- package/dist/shortcuts/fna/operations/expense-chart.js +162 -0
- package/dist/shortcuts/fna/operations/registry.js +2 -0
- package/dist/shortcuts/hrm/host.js +2 -1
- package/dist/shortcuts/im/host.js +3 -2
- package/dist/shortcuts/invoice/host.js +7 -6
- package/dist/shortcuts/invoice/manifest.js +21 -141
- package/dist/shortcuts/invoice/operations/issuing-qys.js +134 -167
- package/dist/shortcuts/invoice/operations/registry.js +1 -6
- package/dist/shortcuts/jiuchuanhui/host.js +2 -1
- package/dist/shortcuts/mail/host.js +2 -1
- package/dist/shortcuts/meeting/host.js +2 -1
- package/dist/shortcuts/meeting/operations/eb/conflict.js +78 -12
- package/dist/shortcuts/okr/host.js +2 -1
- package/dist/shortcuts/plan/host.js +2 -1
- package/dist/shortcuts/project/host.js +2 -1
- package/dist/shortcuts/workflow/host.js +3 -5
- package/dist/shortcuts/workflow/index.js +1 -1
- package/dist/shortcuts/workflow/operations/chatscene.js +4 -22
- package/dist/shortcuts/workflow/operations/form.js +281 -166
- package/dist/shortcuts/workflow/operations/read.js +26 -2
- package/dist/shortcuts/workflow/stdout-compact.js +7 -0
- package/docs/SKILL.md +1 -1
- package/docs/_catalog.md +1 -1
- package/docs/agent-invoice.md +1 -1
- package/docs/caizhilian.md +1 -1
- package/docs/e10-auth.md +1 -1
- package/docs/fna.md +7 -3
- package/docs/invoice.md +4 -6
- package/package.json +1 -1
- package/skill-template/business-info.json +4 -1
- package/skill-template/domains/caizhilian.md +2 -1
- package/skill-template/domains/qiyecheng.md +1 -0
- package/skill-template/domains/yepiaotong.md +6 -2
- package/skill-template/skill-template.md +1 -0
- package/skills/weaver-e10-blog/SKILL.md +6 -1
- package/skills/weaver-e10-blog/references/blog-query.md +3 -1
- package/skills/weaver-e10-caizhilian/SKILL.md +18 -9
- package/skills/weaver-e10-caizhilian/references/action-flow.md +70 -0
- package/skills/weaver-e10-caizhilian/references/agent-entry.md +6 -2
- package/skills/weaver-e10-caizhilian/references/auth-and-install.md +207 -0
- package/skills/weaver-e10-caizhilian/references/file-collection.md +56 -54
- package/skills/weaver-e10-caizhilian/references/file-upload.md +4 -4
- package/skills/weaver-e10-caizhilian/references/host-executor-prompt.md +32 -0
- package/skills/weaver-e10-caizhilian/references/host-platform-integration.md +46 -0
- package/skills/weaver-e10-caizhilian/references/rule-index.md +56 -0
- package/skills/weaver-e10-caizhilian/references/setting-attach.md +5 -16
- package/skills/weaver-e10-caizhilian/references/setting-backup.md +4 -13
- package/skills/weaver-e10-caizhilian/references/setting-encrypt.md +6 -15
- package/skills/weaver-e10-caizhilian/references/setting-storage-docs-summary.md +126 -0
- package/skills/weaver-e10-caizhilian/references/setting-storage-field-map.md +57 -0
- package/skills/weaver-e10-caizhilian/references/setting-storage.md +142 -482
- package/skills/weaver-e10-caizhilian/references/source-manifest.json +7 -7
- package/skills/weaver-e10-caizhilian/references/task-workflow.md +87 -0
- package/skills/weaver-e10-caizhilian/references/withheld.md +2 -2
- package/skills/weaver-e10-caizhilian/references/write-protocol.md +7 -0
- package/skills/weaver-e10-calendar/SKILL.md +6 -1
- package/skills/weaver-e10-esb/SKILL.md +8 -0
- package/skills/weaver-e10-esb/references/input-format.md +3 -1
- package/skills/weaver-e10-esb/references/source-manifest.json +5 -5
- package/skills/weaver-e10-hrm/SKILL.md +6 -1
- package/skills/weaver-e10-hrm/references/employee.md +1 -1
- package/skills/weaver-e10-hrm/references/org.md +1 -1
- package/skills/weaver-e10-hrm/references/position.md +1 -1
- package/skills/weaver-e10-jinchengda/SKILL.md +2 -1
- package/skills/weaver-e10-jinchengda/references/contract-agent-entry.md +1 -1
- package/skills/weaver-e10-jiuchuanhui/SKILL.md +6 -1
- package/skills/weaver-e10-jucailin/SKILL.md +6 -1
- package/skills/weaver-e10-jucailin/references/hr-contact.md +1 -1
- package/skills/weaver-e10-jucailin/references/salary.md +1 -1
- package/skills/weaver-e10-mail/SKILL.md +6 -1
- package/skills/weaver-e10-meeting/SKILL.md +11 -6
- package/skills/weaver-e10-meeting/references/meeting-eb.md +7 -3
- package/skills/weaver-e10-meeting/references/meeting-queries.md +3 -1
- package/skills/weaver-e10-meeting/references/meeting-verification.md +4 -2
- package/skills/weaver-e10-okr/SKILL.md +6 -1
- package/skills/weaver-e10-okr/references/source-manifest.json +5 -5
- package/skills/weaver-e10-plan/SKILL.md +6 -1
- package/skills/weaver-e10-plan/references/plan-overview-and-routing.md +3 -1
- package/skills/weaver-e10-plan/references/plan-report-query.md +3 -1
- package/skills/weaver-e10-plan/references/source-manifest.json +7 -7
- package/skills/weaver-e10-qiyecheng/SKILL.md +16 -6
- package/skills/weaver-e10-qiyecheng/references/catalog.md +6 -3
- package/skills/weaver-e10-qiyecheng/references/expense-center.md +2 -0
- package/skills/weaver-e10-qiyecheng/references/expense-chart.md +78 -0
- package/skills/weaver-e10-qiyecheng/references/source-manifest.json +87 -33
- package/skills/weaver-e10-shared/SKILL.md +37 -14
- package/skills/weaver-e10-shared/references/e10-auth-and-session.md +11 -8
- package/skills/weaver-e10-shared/references/non-interactive-environment.md +4 -4
- package/skills/weaver-e10-shared/references/weaver-e10-installation.md +3 -1
- package/skills/weaver-e10-wenshuding/SKILL.md +6 -1
- package/skills/weaver-e10-wenshuding/references/source-manifest.json +3 -3
- package/skills/weaver-e10-workflow/references/source-manifest.json +1 -2
- package/skills/weaver-e10-workflow/references/workflow-create.md +30 -55
- package/skills/weaver-e10-yepiaotong/SKILL.md +19 -17
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing-examples.md +72 -78
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing-form-fill-rules.md +152 -0
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing-workflow.md +210 -0
- package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +7 -7
- package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +11 -5
- package/skills/weaver-e10-yepiaotong/references/reim-examples.md +55 -33
- package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +102 -23
- package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +1 -1
- package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +53 -42
- package/skills/weaver-e10-yepiaotong/references/source-manifest.json +93 -213
- package/skills/weaver-e10-yimiaoban/SKILL.md +9 -2
- package/skills/weaver-e10-yimiaoban/references/file-user-i18n.md +1 -1
- package/skills/weaver-e10-yimiaoban/references/msg-read.md +1 -1
- package/skills/weaver-e10-yimiaoban/references/source-manifest.json +9 -9
- package/skills/weaver-e10-ziguanjia/SKILL.md +6 -1
- package/dist/shortcuts/invoice/operations/issuing.js +0 -198
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing-field-rules.md +0 -161
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing-qys-ledger.md +0 -66
- 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
|
|
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
|
-
|
|
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
|
-
|
|
121
|
+
formData = new FormData();
|
|
162
122
|
for (const [key, value] of Object.entries(form))
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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',
|
|
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
|
-
|
|
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',
|
|
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',
|
|
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/')
|
|
52
|
-
|
|
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',
|
|
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
|
-
*
|
|
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.
|
|
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',
|
|
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',
|
|
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,7 +180,7 @@ export function createInvoiceHost(ctx, dependencies = {}) {
|
|
|
179
180
|
await clearCachedInvoiceToken();
|
|
180
181
|
if (canRetry)
|
|
181
182
|
return retry();
|
|
182
|
-
throw invoiceAuthError('invoice_session_expired',
|
|
183
|
+
throw invoiceAuthError('invoice_session_expired', `发票业务登录态已失效,已清理本地发票 Token。${E10_REAUTH_GUIDANCE}`, { ret: code });
|
|
183
184
|
}
|
|
184
185
|
throw businessError(value);
|
|
185
186
|
}
|
|
@@ -198,7 +199,7 @@ export function createInvoiceHost(ctx, dependencies = {}) {
|
|
|
198
199
|
trustedHeaders: trustedInvoiceHeaders(token),
|
|
199
200
|
});
|
|
200
201
|
if (response.status === 401 || response.status === 302) {
|
|
201
|
-
throw invoiceAuthError('session_expired',
|
|
202
|
+
throw invoiceAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
|
|
202
203
|
}
|
|
203
204
|
if (response.parseError) {
|
|
204
205
|
if (response.status < 200 || response.status >= 300) {
|
|
@@ -257,7 +258,7 @@ export function createInvoiceHost(ctx, dependencies = {}) {
|
|
|
257
258
|
trustedHeaders: trustedInvoiceHeaders(token),
|
|
258
259
|
});
|
|
259
260
|
if (response.status === 401 || response.status === 302) {
|
|
260
|
-
throw invoiceAuthError('session_expired',
|
|
261
|
+
throw invoiceAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
|
|
261
262
|
}
|
|
262
263
|
if (response.parseError) {
|
|
263
264
|
throw invoiceNetworkError('protocol', `接口返回不是合法 JSON(HTTP ${response.status})`, { retryable: false });
|
|
@@ -292,7 +293,7 @@ export function createInvoiceHost(ctx, dependencies = {}) {
|
|
|
292
293
|
trustedHeaders: trustedInvoiceHeaders(token),
|
|
293
294
|
});
|
|
294
295
|
if (response.status === 401 || response.status === 302) {
|
|
295
|
-
throw invoiceAuthError('session_expired',
|
|
296
|
+
throw invoiceAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
|
|
296
297
|
}
|
|
297
298
|
if (response.parseError) {
|
|
298
299
|
throw invoiceNetworkError('protocol', `接口返回不是合法 JSON(HTTP ${response.status})`, { retryable: false });
|
|
@@ -319,7 +320,7 @@ export function createInvoiceHost(ctx, dependencies = {}) {
|
|
|
319
320
|
trustedHeaders: trustedInvoiceHeaders(token),
|
|
320
321
|
});
|
|
321
322
|
if (response.status === 401 || response.status === 302) {
|
|
322
|
-
throw invoiceAuthError('session_expired',
|
|
323
|
+
throw invoiceAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
|
|
323
324
|
}
|
|
324
325
|
const contentType = response.contentType;
|
|
325
326
|
const declaredLength = Number(response.headers.get('content-length'));
|
|
@@ -340,7 +341,7 @@ export function createInvoiceHost(ctx, dependencies = {}) {
|
|
|
340
341
|
await clearCachedInvoiceToken();
|
|
341
342
|
if (canRetryExpiredToken && token)
|
|
342
343
|
return invoiceDownload({ id, fid, needLog, operation, timeoutMs }, false);
|
|
343
|
-
throw invoiceAuthError('invoice_session_expired',
|
|
344
|
+
throw invoiceAuthError('invoice_session_expired', `发票业务登录态已失效,已清理本地发票 Token。${E10_REAUTH_GUIDANCE}`, { ret: responseCode(value), status: response.status });
|
|
344
345
|
}
|
|
345
346
|
throw new InvoiceCliError('api', 'download_failed', responseMessage(value) || `下载失败(HTTP ${response.status})`, {
|
|
346
347
|
code: response.status,
|