qingflow-mcp 0.3.21 → 0.3.23
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 +40 -3
- package/dist/server.js +310 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -117,7 +117,7 @@ npm i -g git+https://github.com/853046310/qingflow-mcp.git
|
|
|
117
117
|
Install from npm (pinned version):
|
|
118
118
|
|
|
119
119
|
```bash
|
|
120
|
-
npm i -g qingflow-mcp@0.3.
|
|
120
|
+
npm i -g qingflow-mcp@0.3.23
|
|
121
121
|
```
|
|
122
122
|
|
|
123
123
|
Or one-click installer:
|
|
@@ -153,7 +153,7 @@ MCP client config example:
|
|
|
153
153
|
## Recommended Flow
|
|
154
154
|
|
|
155
155
|
1. `qf_apps_list` to pick app.
|
|
156
|
-
2. `qf_form_get` to inspect field ids/titles.
|
|
156
|
+
2. `qf_form_get` to inspect field ids/titles and `field_summaries[].write_format`.
|
|
157
157
|
3. `qf_record_create` or `qf_record_update`.
|
|
158
158
|
4. If create/update returns only `request_id`, call `qf_operation_get` to resolve async result.
|
|
159
159
|
|
|
@@ -180,6 +180,43 @@ Full calling contract (Chinese):
|
|
|
180
180
|
|
|
181
181
|
- [MCP 调用规范](./docs/MCP_CALLING_SPEC.md)
|
|
182
182
|
|
|
183
|
+
## Write Format Discovery
|
|
184
|
+
|
|
185
|
+
For create/update, `0.3.23` now makes special write formats explicit:
|
|
186
|
+
|
|
187
|
+
1. `qf_form_get`
|
|
188
|
+
- `field_summaries[].write_format` is populated for member/department fields.
|
|
189
|
+
2. `qf_tool_spec_get`
|
|
190
|
+
- `qf_record_create` / `qf_record_update` include member/department examples in `limits.special_field_write_formats`.
|
|
191
|
+
3. `qf_record_create` / `qf_record_update`
|
|
192
|
+
- invalid member/department values fail fast with `FIELD_VALUE_FORMAT_ERROR`.
|
|
193
|
+
|
|
194
|
+
Examples:
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{
|
|
198
|
+
"fields": {
|
|
199
|
+
"归属销售": [
|
|
200
|
+
{ "userId": "u_123", "userName": "张三" }
|
|
201
|
+
],
|
|
202
|
+
"归属部门": [
|
|
203
|
+
{ "deptId": 111, "deptName": "销售部" }
|
|
204
|
+
]
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Invalid examples that will now fail:
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
{
|
|
213
|
+
"fields": {
|
|
214
|
+
"归属销售": "张三",
|
|
215
|
+
"归属部门": "销售部"
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
183
220
|
## Unified Query (`qf_query`)
|
|
184
221
|
|
|
185
222
|
`qf_query` is the recommended read entry for agents.
|
|
@@ -429,7 +466,7 @@ If you see runtime errors around `Headers` or missing web APIs:
|
|
|
429
466
|
2. Upgrade package to latest:
|
|
430
467
|
|
|
431
468
|
```bash
|
|
432
|
-
npm i -g qingflow-mcp@
|
|
469
|
+
npm i -g qingflow-mcp@0.3.23
|
|
433
470
|
```
|
|
434
471
|
|
|
435
472
|
3. Verify runtime:
|
package/dist/server.js
CHANGED
|
@@ -68,7 +68,9 @@ const REQUEST_TIMEOUT_MS = toPositiveInt(process.env.QINGFLOW_REQUEST_TIMEOUT_MS
|
|
|
68
68
|
const EXECUTION_BUDGET_MS = toPositiveInt(process.env.QINGFLOW_EXECUTION_BUDGET_MS) ?? 20000;
|
|
69
69
|
const WAIT_RESULT_DEFAULT_TIMEOUT_MS = toPositiveInt(process.env.QINGFLOW_WAIT_RESULT_TIMEOUT_MS) ?? 5000;
|
|
70
70
|
const WAIT_RESULT_POLL_INTERVAL_MS = toPositiveInt(process.env.QINGFLOW_WAIT_RESULT_POLL_INTERVAL_MS) ?? 500;
|
|
71
|
-
const SERVER_VERSION = "0.3.
|
|
71
|
+
const SERVER_VERSION = "0.3.23";
|
|
72
|
+
const MEMBER_QUE_TYPE_KEYWORDS = ["member", "user", "成员", "人员"];
|
|
73
|
+
const DEPARTMENT_QUE_TYPE_KEYWORDS = ["department", "dept", "部门"];
|
|
72
74
|
const accessToken = process.env.QINGFLOW_ACCESS_TOKEN;
|
|
73
75
|
const baseUrl = process.env.QINGFLOW_BASE_URL;
|
|
74
76
|
if (!accessToken) {
|
|
@@ -192,6 +194,15 @@ const fieldSummarySchema = z.object({
|
|
|
192
194
|
que_id: z.union([z.number(), z.string(), z.null()]),
|
|
193
195
|
que_title: z.string().nullable(),
|
|
194
196
|
que_type: z.unknown(),
|
|
197
|
+
write_format: z
|
|
198
|
+
.object({
|
|
199
|
+
kind: z.enum(["member_list", "department_list"]),
|
|
200
|
+
description: z.string(),
|
|
201
|
+
item_shape: z.record(z.string()),
|
|
202
|
+
example: z.array(z.record(z.unknown())),
|
|
203
|
+
resolution_hint: z.string()
|
|
204
|
+
})
|
|
205
|
+
.nullable(),
|
|
195
206
|
has_sub_fields: z.boolean(),
|
|
196
207
|
sub_field_count: z.number().int().nonnegative()
|
|
197
208
|
});
|
|
@@ -2388,7 +2399,8 @@ server.registerTool("qf_record_create", {
|
|
|
2388
2399
|
}, async (args) => {
|
|
2389
2400
|
try {
|
|
2390
2401
|
const parsedArgs = createInputSchema.parse(args);
|
|
2391
|
-
const
|
|
2402
|
+
const shouldFetchForm = hasWritePayload(parsedArgs.answers, parsedArgs.fields) || Boolean(parsedArgs.force_refresh_form);
|
|
2403
|
+
const form = shouldFetchForm
|
|
2392
2404
|
? await getFormCached(parsedArgs.app_key, parsedArgs.user_id, Boolean(parsedArgs.force_refresh_form))
|
|
2393
2405
|
: null;
|
|
2394
2406
|
const normalizedAnswers = resolveAnswers({
|
|
@@ -2464,16 +2476,18 @@ server.registerTool("qf_record_update", {
|
|
|
2464
2476
|
}, async (args) => {
|
|
2465
2477
|
try {
|
|
2466
2478
|
const parsedArgs = updateInputSchema.parse(args);
|
|
2467
|
-
const
|
|
2468
|
-
|
|
2479
|
+
const resolvedAppKey = parsedArgs.app_key ?? getCachedApplyAppKey(parsedArgs.apply_id);
|
|
2480
|
+
const requiresFormByTitle = needsFormResolution(parsedArgs.fields);
|
|
2481
|
+
if (requiresFormByTitle && !resolvedAppKey) {
|
|
2469
2482
|
throw missingRequiredFieldError({
|
|
2470
2483
|
field: "app_key",
|
|
2471
2484
|
tool: "qf_record_update",
|
|
2472
2485
|
fixHint: "Provide app_key when fields uses title-based keys, or switch fields to numeric que_id."
|
|
2473
2486
|
});
|
|
2474
2487
|
}
|
|
2475
|
-
const form =
|
|
2476
|
-
|
|
2488
|
+
const form = (hasWritePayload(parsedArgs.answers, parsedArgs.fields) || Boolean(parsedArgs.force_refresh_form)) &&
|
|
2489
|
+
resolvedAppKey
|
|
2490
|
+
? await getFormCached(resolvedAppKey, parsedArgs.user_id, Boolean(parsedArgs.force_refresh_form))
|
|
2477
2491
|
: null;
|
|
2478
2492
|
const normalizedAnswers = resolveAnswers({
|
|
2479
2493
|
explicitAnswers: parsedArgs.answers,
|
|
@@ -3534,7 +3548,8 @@ function buildToolSpecCatalog() {
|
|
|
3534
3548
|
tool: "qf_form_get",
|
|
3535
3549
|
required: ["app_key"],
|
|
3536
3550
|
limits: {
|
|
3537
|
-
app_key: "required string"
|
|
3551
|
+
app_key: "required string",
|
|
3552
|
+
write_format_hint: "field_summaries[].write_format is populated for member/department fields so agents can discover exact create/update payload shapes before writing."
|
|
3538
3553
|
},
|
|
3539
3554
|
aliases: collectAliasHints(["app_key", "user_id", "force_refresh"], {}),
|
|
3540
3555
|
minimal_example: {
|
|
@@ -3751,7 +3766,11 @@ function buildToolSpecCatalog() {
|
|
|
3751
3766
|
write_mode: "Provide either answers[] or fields{}",
|
|
3752
3767
|
wait_result: "optional boolean; when true, polls qf_operation_get internally and returns resolved apply_id directly",
|
|
3753
3768
|
wait_timeout_ms: "optional int (max 20000); default 5000ms",
|
|
3754
|
-
input_contract: "strict JSON only; answers must be array and fields must be object"
|
|
3769
|
+
input_contract: "strict JSON only; answers must be array and fields must be object",
|
|
3770
|
+
special_field_write_formats: {
|
|
3771
|
+
member_list: [{ userId: "u_123", userName: "张三" }],
|
|
3772
|
+
department_list: [{ deptId: 111, deptName: "销售部" }]
|
|
3773
|
+
}
|
|
3755
3774
|
},
|
|
3756
3775
|
aliases: {},
|
|
3757
3776
|
minimal_example: {
|
|
@@ -3769,7 +3788,11 @@ function buildToolSpecCatalog() {
|
|
|
3769
3788
|
write_mode: "Provide either answers[] or fields{}",
|
|
3770
3789
|
wait_result: "optional boolean; when true, polls qf_operation_get internally and returns resolved result directly",
|
|
3771
3790
|
wait_timeout_ms: "optional int (max 20000); default 5000ms",
|
|
3772
|
-
input_contract: "strict JSON only; answers must be array and fields must be object"
|
|
3791
|
+
input_contract: "strict JSON only; answers must be array and fields must be object",
|
|
3792
|
+
special_field_write_formats: {
|
|
3793
|
+
member_list: [{ userId: "u_123", userName: "张三" }],
|
|
3794
|
+
department_list: [{ deptId: 111, deptName: "销售部" }]
|
|
3795
|
+
}
|
|
3773
3796
|
},
|
|
3774
3797
|
aliases: {},
|
|
3775
3798
|
minimal_example: {
|
|
@@ -3841,7 +3864,7 @@ function normalizeListInput(raw) {
|
|
|
3841
3864
|
strict_full: coerceBooleanLike(normalizedObj.strict_full),
|
|
3842
3865
|
include_answers: coerceBooleanLike(normalizedObj.include_answers),
|
|
3843
3866
|
output_profile: normalizeOutputProfileInput(normalizedObj.output_profile),
|
|
3844
|
-
apply_ids:
|
|
3867
|
+
apply_ids: normalizeOpaqueIdArrayInput(normalizedObj.apply_ids),
|
|
3845
3868
|
sort: normalizeSortInput(normalizedObj.sort),
|
|
3846
3869
|
filters: normalizeFiltersInput(normalizedObj.filters),
|
|
3847
3870
|
select_columns: normalizeSelectorListInput(selectColumns),
|
|
@@ -3858,7 +3881,7 @@ function normalizeRecordGetInput(raw) {
|
|
|
3858
3881
|
const selectColumns = normalizedObj.select_columns ?? normalizedObj.keep_columns;
|
|
3859
3882
|
return {
|
|
3860
3883
|
...normalizedObj,
|
|
3861
|
-
apply_id:
|
|
3884
|
+
apply_id: normalizeOpaqueIdInput(normalizedObj.apply_id),
|
|
3862
3885
|
app_key: coerceStringLike(normalizedObj.app_key),
|
|
3863
3886
|
max_columns: coerceNumberLike(normalizedObj.max_columns),
|
|
3864
3887
|
select_columns: normalizeSelectorListInput(selectColumns),
|
|
@@ -3884,12 +3907,12 @@ function normalizeQueryInput(raw) {
|
|
|
3884
3907
|
max_rows: coerceNumberLike(normalizedObj.max_rows),
|
|
3885
3908
|
max_items: coerceNumberLike(normalizedObj.max_items),
|
|
3886
3909
|
max_columns: coerceNumberLike(normalizedObj.max_columns),
|
|
3887
|
-
apply_id:
|
|
3910
|
+
apply_id: normalizeOpaqueIdInput(normalizedObj.apply_id),
|
|
3888
3911
|
strict_full: coerceBooleanLike(normalizedObj.strict_full),
|
|
3889
3912
|
include_answers: coerceBooleanLike(normalizedObj.include_answers),
|
|
3890
3913
|
output_profile: normalizeOutputProfileInput(normalizedObj.output_profile),
|
|
3891
3914
|
amount_column: normalizeAmountColumnInput(normalizedObj.amount_column),
|
|
3892
|
-
apply_ids:
|
|
3915
|
+
apply_ids: normalizeOpaqueIdArrayInput(normalizedObj.apply_ids),
|
|
3893
3916
|
sort: normalizeSortInput(normalizedObj.sort),
|
|
3894
3917
|
filters: normalizeFiltersInput(normalizedObj.filters),
|
|
3895
3918
|
select_columns: normalizeSelectorListInput(selectColumns),
|
|
@@ -3921,7 +3944,7 @@ function normalizeAggregateInput(raw) {
|
|
|
3921
3944
|
amount_column: normalizeAmountColumnInput(amountColumns),
|
|
3922
3945
|
metrics: normalizeMetricsInput(normalizedObj.metrics),
|
|
3923
3946
|
time_bucket: normalizeTimeBucketInput(normalizedObj.time_bucket),
|
|
3924
|
-
apply_ids:
|
|
3947
|
+
apply_ids: normalizeOpaqueIdArrayInput(normalizedObj.apply_ids),
|
|
3925
3948
|
sort: normalizeSortInput(normalizedObj.sort),
|
|
3926
3949
|
filters: normalizeFiltersInput(normalizedObj.filters),
|
|
3927
3950
|
time_range: timeRange,
|
|
@@ -3981,7 +4004,7 @@ function normalizeBatchGetInput(raw) {
|
|
|
3981
4004
|
return {
|
|
3982
4005
|
...normalizedObj,
|
|
3983
4006
|
app_key: coerceStringLike(normalizedObj.app_key),
|
|
3984
|
-
apply_ids:
|
|
4007
|
+
apply_ids: normalizeOpaqueIdArrayInput(normalizedObj.apply_ids),
|
|
3985
4008
|
max_columns: coerceNumberLike(normalizedObj.max_columns),
|
|
3986
4009
|
select_columns: normalizeSelectorListInput(selectColumns),
|
|
3987
4010
|
output_profile: normalizeOutputProfileInput(normalizedObj.output_profile)
|
|
@@ -4007,7 +4030,7 @@ function normalizeExportInput(raw) {
|
|
|
4007
4030
|
max_columns: coerceNumberLike(normalizedObj.max_columns),
|
|
4008
4031
|
strict_full: coerceBooleanLike(normalizedObj.strict_full),
|
|
4009
4032
|
output_profile: normalizeOutputProfileInput(normalizedObj.output_profile),
|
|
4010
|
-
apply_ids:
|
|
4033
|
+
apply_ids: normalizeOpaqueIdArrayInput(normalizedObj.apply_ids),
|
|
4011
4034
|
sort: normalizeSortInput(normalizedObj.sort),
|
|
4012
4035
|
filters: normalizeFiltersInput(normalizedObj.filters),
|
|
4013
4036
|
select_columns: normalizeSelectorListInput(selectColumns),
|
|
@@ -4134,6 +4157,34 @@ function normalizeIdArrayInput(value) {
|
|
|
4134
4157
|
}
|
|
4135
4158
|
return parsed;
|
|
4136
4159
|
}
|
|
4160
|
+
function normalizeOpaqueIdInput(value) {
|
|
4161
|
+
const parsed = parseJsonLikeDeep(value);
|
|
4162
|
+
if (parsed === undefined || parsed === null) {
|
|
4163
|
+
return parsed;
|
|
4164
|
+
}
|
|
4165
|
+
if (typeof parsed === "string") {
|
|
4166
|
+
const trimmed = parsed.trim();
|
|
4167
|
+
return trimmed ? trimmed : parsed;
|
|
4168
|
+
}
|
|
4169
|
+
if (typeof parsed === "number" && Number.isFinite(parsed)) {
|
|
4170
|
+
return String(Math.trunc(parsed));
|
|
4171
|
+
}
|
|
4172
|
+
return parsed;
|
|
4173
|
+
}
|
|
4174
|
+
function normalizeOpaqueIdArrayInput(value) {
|
|
4175
|
+
const parsed = parseJsonLikeDeep(value);
|
|
4176
|
+
if (Array.isArray(parsed)) {
|
|
4177
|
+
return parsed.map((item) => normalizeOpaqueIdInput(item));
|
|
4178
|
+
}
|
|
4179
|
+
if (typeof parsed === "string" && parsed.includes(",")) {
|
|
4180
|
+
return parsed
|
|
4181
|
+
.split(",")
|
|
4182
|
+
.map((item) => item.trim())
|
|
4183
|
+
.filter((item) => item.length > 0)
|
|
4184
|
+
.map((item) => normalizeOpaqueIdInput(item));
|
|
4185
|
+
}
|
|
4186
|
+
return parsed;
|
|
4187
|
+
}
|
|
4137
4188
|
function normalizeSortInput(value) {
|
|
4138
4189
|
const parsed = parseJsonLikeDeep(value);
|
|
4139
4190
|
if (!Array.isArray(parsed)) {
|
|
@@ -7505,8 +7556,9 @@ function normalizeRecordItem(raw, includeAnswers) {
|
|
|
7505
7556
|
return normalized;
|
|
7506
7557
|
}
|
|
7507
7558
|
function resolveAnswers(params) {
|
|
7508
|
-
const
|
|
7509
|
-
const
|
|
7559
|
+
const index = params.form ? buildFieldIndex(params.form) : null;
|
|
7560
|
+
const normalizedFromFields = resolveFieldAnswers(params.fields, index, params.tool);
|
|
7561
|
+
const normalizedExplicit = normalizeExplicitAnswers(params.explicitAnswers, index, params.tool);
|
|
7510
7562
|
const merged = new Map();
|
|
7511
7563
|
for (const answer of normalizedFromFields) {
|
|
7512
7564
|
merged.set(String(answer.queId), answer);
|
|
@@ -7519,12 +7571,12 @@ function resolveAnswers(params) {
|
|
|
7519
7571
|
}
|
|
7520
7572
|
return Array.from(merged.values());
|
|
7521
7573
|
}
|
|
7522
|
-
function normalizeExplicitAnswers(answers) {
|
|
7574
|
+
function normalizeExplicitAnswers(answers, index, tool = "qf_record_create") {
|
|
7523
7575
|
if (!answers?.length) {
|
|
7524
7576
|
return [];
|
|
7525
7577
|
}
|
|
7526
7578
|
const output = [];
|
|
7527
|
-
for (const item of answers) {
|
|
7579
|
+
for (const [itemIndex, item] of answers.entries()) {
|
|
7528
7580
|
const queId = item.que_id ?? item.queId;
|
|
7529
7581
|
if (queId === undefined || queId === null || String(queId).trim() === "") {
|
|
7530
7582
|
throw new Error("answer item requires que_id or queId");
|
|
@@ -7550,27 +7602,32 @@ function normalizeExplicitAnswers(answers) {
|
|
|
7550
7602
|
if (values === undefined) {
|
|
7551
7603
|
throw new Error(`answer item ${String(queId)} requires values or table_values`);
|
|
7552
7604
|
}
|
|
7553
|
-
|
|
7605
|
+
const field = resolveExplicitAnswerField(item, index);
|
|
7606
|
+
normalized.values = values.map((value, valueIndex) => normalizeAnswerValue(value, {
|
|
7607
|
+
field,
|
|
7608
|
+
tool,
|
|
7609
|
+
location: `answers[${itemIndex}].values[${valueIndex}]`
|
|
7610
|
+
}));
|
|
7554
7611
|
output.push(normalized);
|
|
7555
7612
|
}
|
|
7556
7613
|
return output;
|
|
7557
7614
|
}
|
|
7558
|
-
function resolveFieldAnswers(fields,
|
|
7615
|
+
function resolveFieldAnswers(fields, index, tool = "qf_record_create") {
|
|
7559
7616
|
const entries = Object.entries(fields ?? {});
|
|
7560
7617
|
if (entries.length === 0) {
|
|
7561
7618
|
return [];
|
|
7562
7619
|
}
|
|
7563
|
-
const
|
|
7620
|
+
const resolvedIndex = index ?? { byId: new Map(), byTitle: new Map() };
|
|
7564
7621
|
const answers = [];
|
|
7565
7622
|
for (const [fieldKey, fieldValue] of entries) {
|
|
7566
7623
|
let field;
|
|
7567
7624
|
if (isNumericKey(fieldKey)) {
|
|
7568
|
-
field = resolveFieldByKey(fieldKey,
|
|
7625
|
+
field = resolveFieldByKey(fieldKey, resolvedIndex);
|
|
7569
7626
|
}
|
|
7570
7627
|
else {
|
|
7571
7628
|
field = resolveFieldSelectorStrict({
|
|
7572
7629
|
fieldKey,
|
|
7573
|
-
index,
|
|
7630
|
+
index: resolvedIndex,
|
|
7574
7631
|
tool,
|
|
7575
7632
|
location: `fields.${fieldKey}`
|
|
7576
7633
|
});
|
|
@@ -7584,15 +7641,15 @@ function resolveFieldAnswers(fields, form, tool = "qf_record_create") {
|
|
|
7584
7641
|
tool,
|
|
7585
7642
|
location: `fields.${fieldKey}`,
|
|
7586
7643
|
requested: fieldKey,
|
|
7587
|
-
suggestions: buildFieldSuggestions(fieldKey,
|
|
7644
|
+
suggestions: buildFieldSuggestions(fieldKey, resolvedIndex)
|
|
7588
7645
|
}
|
|
7589
7646
|
});
|
|
7590
7647
|
}
|
|
7591
|
-
answers.push(makeAnswerFromField(field, fieldValue));
|
|
7648
|
+
answers.push(makeAnswerFromField(field, fieldValue, tool));
|
|
7592
7649
|
}
|
|
7593
7650
|
return answers;
|
|
7594
7651
|
}
|
|
7595
|
-
function makeAnswerFromField(field, value) {
|
|
7652
|
+
function makeAnswerFromField(field, value, tool = "qf_record_create") {
|
|
7596
7653
|
const base = {
|
|
7597
7654
|
queId: field.queId
|
|
7598
7655
|
};
|
|
@@ -7613,7 +7670,11 @@ function makeAnswerFromField(field, value) {
|
|
|
7613
7670
|
if ("values" in objectValue) {
|
|
7614
7671
|
return {
|
|
7615
7672
|
...base,
|
|
7616
|
-
values: asArray(objectValue.values).map((item) => normalizeAnswerValue(item
|
|
7673
|
+
values: asArray(objectValue.values).map((item, index) => normalizeAnswerValue(item, {
|
|
7674
|
+
field,
|
|
7675
|
+
tool,
|
|
7676
|
+
location: `fields.${String(field.queTitle ?? field.queId ?? "field")}.values[${index}]`
|
|
7677
|
+
}))
|
|
7617
7678
|
};
|
|
7618
7679
|
}
|
|
7619
7680
|
}
|
|
@@ -7626,10 +7687,38 @@ function makeAnswerFromField(field, value) {
|
|
|
7626
7687
|
const valueArray = Array.isArray(value) ? value : [value];
|
|
7627
7688
|
return {
|
|
7628
7689
|
...base,
|
|
7629
|
-
values: valueArray.map((item) => normalizeAnswerValue(item
|
|
7690
|
+
values: valueArray.map((item, index) => normalizeAnswerValue(item, {
|
|
7691
|
+
field,
|
|
7692
|
+
tool,
|
|
7693
|
+
location: `fields.${String(field.queTitle ?? field.queId ?? "field")}.values[${index}]`
|
|
7694
|
+
}))
|
|
7695
|
+
};
|
|
7696
|
+
}
|
|
7697
|
+
function resolveExplicitAnswerField(item, index) {
|
|
7698
|
+
if (index) {
|
|
7699
|
+
const rawQueId = item.que_id ?? item.queId;
|
|
7700
|
+
if (rawQueId !== undefined && rawQueId !== null) {
|
|
7701
|
+
const hit = index.byId.get(String(normalizeQueId(rawQueId)));
|
|
7702
|
+
if (hit) {
|
|
7703
|
+
return hit;
|
|
7704
|
+
}
|
|
7705
|
+
}
|
|
7706
|
+
const rawQueTitle = item.que_title ?? item.queTitle;
|
|
7707
|
+
if (typeof rawQueTitle === "string" && rawQueTitle.trim()) {
|
|
7708
|
+
const candidates = index.byTitle.get(rawQueTitle.trim().toLowerCase()) ?? [];
|
|
7709
|
+
if (candidates.length === 1) {
|
|
7710
|
+
return candidates[0];
|
|
7711
|
+
}
|
|
7712
|
+
}
|
|
7713
|
+
}
|
|
7714
|
+
return {
|
|
7715
|
+
queId: item.que_id ?? item.queId,
|
|
7716
|
+
queTitle: item.que_title ?? item.queTitle,
|
|
7717
|
+
queType: item.que_type ?? item.queType
|
|
7630
7718
|
};
|
|
7631
7719
|
}
|
|
7632
|
-
function normalizeAnswerValue(value) {
|
|
7720
|
+
function normalizeAnswerValue(value, params) {
|
|
7721
|
+
validateWriteValueShape(params?.field ?? null, value, params);
|
|
7633
7722
|
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
7634
7723
|
return {
|
|
7635
7724
|
value,
|
|
@@ -7638,6 +7727,70 @@ function normalizeAnswerValue(value) {
|
|
|
7638
7727
|
}
|
|
7639
7728
|
return value;
|
|
7640
7729
|
}
|
|
7730
|
+
function validateWriteValueShape(field, value, params) {
|
|
7731
|
+
const writeFormat = field ? inferFieldWriteFormat(field) : null;
|
|
7732
|
+
if (!writeFormat) {
|
|
7733
|
+
return;
|
|
7734
|
+
}
|
|
7735
|
+
const validatedField = field;
|
|
7736
|
+
if (!validatedField) {
|
|
7737
|
+
return;
|
|
7738
|
+
}
|
|
7739
|
+
if (writeFormat.kind === "member_list" && !isValidMemberWriteValue(value)) {
|
|
7740
|
+
throw fieldValueFormatError({
|
|
7741
|
+
field: validatedField,
|
|
7742
|
+
writeFormat,
|
|
7743
|
+
value,
|
|
7744
|
+
tool: params?.tool,
|
|
7745
|
+
location: params?.location
|
|
7746
|
+
});
|
|
7747
|
+
}
|
|
7748
|
+
if (writeFormat.kind === "department_list" && !isValidDepartmentWriteValue(value)) {
|
|
7749
|
+
throw fieldValueFormatError({
|
|
7750
|
+
field: validatedField,
|
|
7751
|
+
writeFormat,
|
|
7752
|
+
value,
|
|
7753
|
+
tool: params?.tool,
|
|
7754
|
+
location: params?.location
|
|
7755
|
+
});
|
|
7756
|
+
}
|
|
7757
|
+
}
|
|
7758
|
+
function isValidMemberWriteValue(value) {
|
|
7759
|
+
const obj = asObject(value);
|
|
7760
|
+
return Boolean(obj && asNullableString(obj.userId)?.trim());
|
|
7761
|
+
}
|
|
7762
|
+
function isValidDepartmentWriteValue(value) {
|
|
7763
|
+
const obj = asObject(value);
|
|
7764
|
+
if (!obj) {
|
|
7765
|
+
return false;
|
|
7766
|
+
}
|
|
7767
|
+
const deptId = obj.deptId;
|
|
7768
|
+
return ((typeof deptId === "string" && deptId.trim().length > 0) ||
|
|
7769
|
+
(typeof deptId === "number" && Number.isFinite(deptId)));
|
|
7770
|
+
}
|
|
7771
|
+
function fieldValueFormatError(params) {
|
|
7772
|
+
const fieldLabel = asNullableString(params.field.queTitle)?.trim() ||
|
|
7773
|
+
String(params.field.queId ?? "unknown_field");
|
|
7774
|
+
const formatName = params.writeFormat.kind === "member_list" ? "成员字段" : "部门字段";
|
|
7775
|
+
return new InputValidationError({
|
|
7776
|
+
message: `${formatName} "${fieldLabel}" 的写入值格式不正确`,
|
|
7777
|
+
errorCode: "FIELD_VALUE_FORMAT_ERROR",
|
|
7778
|
+
fixHint: params.writeFormat.kind === "member_list"
|
|
7779
|
+
? '传对象数组,例如 [{"userId":"u_123","userName":"张三"}];不要传纯字符串或 user_id。'
|
|
7780
|
+
: '传对象数组,例如 [{"deptId":111,"deptName":"销售部"}];不要传纯字符串或 dept_id。',
|
|
7781
|
+
details: {
|
|
7782
|
+
tool: params.tool ?? "qf_record_create",
|
|
7783
|
+
location: params.location ?? null,
|
|
7784
|
+
field: {
|
|
7785
|
+
que_id: params.field.queId ?? null,
|
|
7786
|
+
que_title: asNullableString(params.field.queTitle),
|
|
7787
|
+
que_type: params.field.queType ?? null
|
|
7788
|
+
},
|
|
7789
|
+
expected_format: params.writeFormat,
|
|
7790
|
+
received_value: params.value
|
|
7791
|
+
}
|
|
7792
|
+
});
|
|
7793
|
+
}
|
|
7641
7794
|
function needsFormResolution(fields) {
|
|
7642
7795
|
const keys = Object.keys(fields ?? {});
|
|
7643
7796
|
if (!keys.length) {
|
|
@@ -7669,11 +7822,95 @@ function extractFieldSummaries(form) {
|
|
|
7669
7822
|
que_id: field.queId ?? null,
|
|
7670
7823
|
que_title: asNullableString(field.queTitle),
|
|
7671
7824
|
que_type: field.queType,
|
|
7825
|
+
write_format: inferFieldWriteFormat(field),
|
|
7672
7826
|
has_sub_fields: sub.length > 0,
|
|
7673
7827
|
sub_field_count: sub.length
|
|
7674
7828
|
};
|
|
7675
7829
|
});
|
|
7676
7830
|
}
|
|
7831
|
+
function inferFieldWriteFormat(field) {
|
|
7832
|
+
const kind = inferFieldWriteFormatKind(field.queType);
|
|
7833
|
+
if (kind === "member_list") {
|
|
7834
|
+
return {
|
|
7835
|
+
kind,
|
|
7836
|
+
description: "Pass one or more member objects in values[]. Each item must contain userId.",
|
|
7837
|
+
item_shape: {
|
|
7838
|
+
userId: "string",
|
|
7839
|
+
userName: "string? (recommended)",
|
|
7840
|
+
name: "string? (accepted alias)"
|
|
7841
|
+
},
|
|
7842
|
+
example: [
|
|
7843
|
+
{
|
|
7844
|
+
userId: "u_123",
|
|
7845
|
+
userName: "张三"
|
|
7846
|
+
}
|
|
7847
|
+
],
|
|
7848
|
+
resolution_hint: "Use qf_users_list or qf_department_users_list to resolve valid userId values before writing."
|
|
7849
|
+
};
|
|
7850
|
+
}
|
|
7851
|
+
if (kind === "department_list") {
|
|
7852
|
+
return {
|
|
7853
|
+
kind,
|
|
7854
|
+
description: "Pass one or more department objects in values[]. Each item must contain deptId.",
|
|
7855
|
+
item_shape: {
|
|
7856
|
+
deptId: "string|number",
|
|
7857
|
+
deptName: "string? (recommended)",
|
|
7858
|
+
name: "string? (accepted alias)"
|
|
7859
|
+
},
|
|
7860
|
+
example: [
|
|
7861
|
+
{
|
|
7862
|
+
deptId: 111,
|
|
7863
|
+
deptName: "销售部"
|
|
7864
|
+
}
|
|
7865
|
+
],
|
|
7866
|
+
resolution_hint: "Use qf_departments_list to resolve valid deptId values before writing."
|
|
7867
|
+
};
|
|
7868
|
+
}
|
|
7869
|
+
return null;
|
|
7870
|
+
}
|
|
7871
|
+
function inferFieldWriteFormatKind(queType) {
|
|
7872
|
+
const tokens = collectQueTypeTokens(queType);
|
|
7873
|
+
if (tokens.some((token) => MEMBER_QUE_TYPE_KEYWORDS.some((keyword) => token.includes(keyword)))) {
|
|
7874
|
+
return "member_list";
|
|
7875
|
+
}
|
|
7876
|
+
if (tokens.some((token) => DEPARTMENT_QUE_TYPE_KEYWORDS.some((keyword) => token.includes(keyword)))) {
|
|
7877
|
+
return "department_list";
|
|
7878
|
+
}
|
|
7879
|
+
return null;
|
|
7880
|
+
}
|
|
7881
|
+
function collectQueTypeTokens(queType) {
|
|
7882
|
+
const tokens = new Set();
|
|
7883
|
+
const queue = [queType];
|
|
7884
|
+
while (queue.length > 0) {
|
|
7885
|
+
const current = queue.shift();
|
|
7886
|
+
if (current === null || current === undefined) {
|
|
7887
|
+
continue;
|
|
7888
|
+
}
|
|
7889
|
+
if (typeof current === "string") {
|
|
7890
|
+
const normalized = current.trim().toLowerCase();
|
|
7891
|
+
if (normalized) {
|
|
7892
|
+
tokens.add(normalized);
|
|
7893
|
+
}
|
|
7894
|
+
continue;
|
|
7895
|
+
}
|
|
7896
|
+
if (typeof current === "number" || typeof current === "boolean") {
|
|
7897
|
+
tokens.add(String(current));
|
|
7898
|
+
continue;
|
|
7899
|
+
}
|
|
7900
|
+
if (Array.isArray(current)) {
|
|
7901
|
+
queue.push(...current);
|
|
7902
|
+
continue;
|
|
7903
|
+
}
|
|
7904
|
+
const obj = asObject(current);
|
|
7905
|
+
if (!obj) {
|
|
7906
|
+
continue;
|
|
7907
|
+
}
|
|
7908
|
+
for (const value of Object.values(obj)) {
|
|
7909
|
+
queue.push(value);
|
|
7910
|
+
}
|
|
7911
|
+
}
|
|
7912
|
+
return Array.from(tokens);
|
|
7913
|
+
}
|
|
7677
7914
|
function buildFieldIndex(form) {
|
|
7678
7915
|
const byId = new Map();
|
|
7679
7916
|
const byTitle = new Map();
|
|
@@ -8414,6 +8651,48 @@ function buildErrorExampleCalls(params) {
|
|
|
8414
8651
|
}
|
|
8415
8652
|
];
|
|
8416
8653
|
}
|
|
8654
|
+
if (errorCode === "FIELD_VALUE_FORMAT_ERROR") {
|
|
8655
|
+
const expectedFormat = asObject(params.details?.expected_format);
|
|
8656
|
+
const kind = asNullableString(expectedFormat?.kind);
|
|
8657
|
+
if (kind === "member_list") {
|
|
8658
|
+
return [
|
|
8659
|
+
{
|
|
8660
|
+
tool: "qf_form_get",
|
|
8661
|
+
arguments: {
|
|
8662
|
+
app_key: appKey
|
|
8663
|
+
},
|
|
8664
|
+
note: "先查看字段 write_format,确认成员字段写入 shape"
|
|
8665
|
+
},
|
|
8666
|
+
{
|
|
8667
|
+
tool: "qf_department_users_list",
|
|
8668
|
+
arguments: {
|
|
8669
|
+
dept_id: 111,
|
|
8670
|
+
fetch_child: false
|
|
8671
|
+
},
|
|
8672
|
+
note: "查询有效成员 userId,再按 [{userId,userName}] 写入"
|
|
8673
|
+
}
|
|
8674
|
+
];
|
|
8675
|
+
}
|
|
8676
|
+
if (kind === "department_list") {
|
|
8677
|
+
return [
|
|
8678
|
+
{
|
|
8679
|
+
tool: "qf_form_get",
|
|
8680
|
+
arguments: {
|
|
8681
|
+
app_key: appKey
|
|
8682
|
+
},
|
|
8683
|
+
note: "先查看字段 write_format,确认部门字段写入 shape"
|
|
8684
|
+
},
|
|
8685
|
+
{
|
|
8686
|
+
tool: "qf_departments_list",
|
|
8687
|
+
arguments: {
|
|
8688
|
+
keyword: "销售",
|
|
8689
|
+
limit: 20
|
|
8690
|
+
},
|
|
8691
|
+
note: "查询有效部门 deptId,再按 [{deptId,deptName}] 写入"
|
|
8692
|
+
}
|
|
8693
|
+
];
|
|
8694
|
+
}
|
|
8695
|
+
}
|
|
8417
8696
|
if (errorCode === "INTERNAL_ERROR" || errorCode === "UNKNOWN_ERROR") {
|
|
8418
8697
|
return [
|
|
8419
8698
|
{
|