export_pallet_data Identifier Update Implementation PlanFor agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Change export_pallet_data from inbound-order-number filtering to an explicit three-way choice of container numbers, bill-of-lading numbers, or overseas-warehouse inbound-time range, while preserving the 200-item limit, company isolation, async task contract, and pallet export output.
Architecture: Keep the Gateway as a schema/forwarding boundary and keep all business resolution in fmsoperate. Gateway accepts container_codes and bl_numbers as separate arrays and rejects ambiguous or mixed selectors; fmsoperate resolves each selector through its own indexed database branch before reusing the existing inbound-record → order → pallet-batch pipeline. The old inbound_numbers field is rejected at both the public schema and PHP task boundary.
Tech Stack: Python 3 standard-library MCP Gateway, ThinkPHP 6/PHP 7.1+, ThinkORM/MySQL, Redis queue, PHPUnit contract tests, Markdown project documentation.
Delivery boundary: Do not execute registration SQL, restart Gateway or workers, or commit Git changes. Preserve all unrelated user changes already present in the three repositories.
Y:/mcp)tools/export_pallet_data.py for metadata, runtime validation, and forwarding.app.py for CLI selector flags and payload construction.services/output_presenter.py to map new parameter errors to Chinese labels.tests/test_export_pallet_data_tool.py for the new closed schema, forwarding, limits, and CLI contract.tests/test_tool_description_boundaries.py so this number-based export requires explicit type clarification.tests/test_output_presenter.py for new field-label error mapping.README.md and CONTEXT.md to remove the old selector contract from Gateway-facing material.Y:/fmsoperate)app/mcp/validate/McpToolValidate.php for the new selector XOR rules and explicit legacy-field rejection.app/mcp/logic/McpPalletDataExportLogic.php for normalization, validation, task parameters, and model calls.app/mcp/model/McpPalletDataExportModel.php for separate container and bill-of-lading lookup branches.app/mcp/logic/McpPalletDataExportService.php for the new persisted task parameters.app/mcp/controller/McpToolsController.php so audit payload allowlisting records the new selector fields.app/job/think/handle/inside/McpPalletDataExport.php so queued-task validation accepts only the new selector contract.Y:/settlement_tests)tests/Unit/FmsOperate/McpPalletDataExportTest.php for validator, logic, model-shape, identity, and new selector behavior.tests/Unit/FmsOperate/McpPalletDataExportJobTest.php for worker payload validation and legacy-task rejection.docs/manual/mcp-async-export-workbuddy-cases.md with new pallet selector placeholders and manual cases.Y:/all_project_docs)fmsoperate/requirements.md, fmsoperate/tech-specs.md, fmsoperate/user-structure.md, and fmsoperate/timeline.md.mcp/overview.md, mcp/requirements.md, mcp/tech-specs.md, mcp/user-structure.md, and mcp/timeline.md.fmsoperate/sql/mcp_add_pallet_data_export_tool.sql description only; do not execute it.Files:
Y:/mcp/tests/test_export_pallet_data_tool.pyY:/mcp/tests/test_tool_description_boundaries.pyModify: Y:/mcp/tests/test_output_presenter.py
[ ] Step 1: Replace the old schema expectations.
Change the schema test to require exactly:
{
'container_codes',
'bl_numbers',
'inbound_time_start',
'inbound_time_end',
}
Keep additionalProperties=False, assert both number fields have maxItems == 200, and assert that inbound_numbers, page, limit, ids, and inbound_id are absent.
Add separate tests that call the tool with trimmed duplicate values and assert the exact payload:
ExportPalletDataTool(client).call(
container_codes=[' CONT-1 ', 'CONT-1', 'CONT-2'],
)
assert client.calls[-1][2] == {
'container_codes': ['CONT-1', 'CONT-2'],
}
ExportPalletDataTool(client).call(
bl_numbers=[' BL-1 ', 'BL-1'],
)
assert client.calls[-1][2] == {
'bl_numbers': ['BL-1'],
}
Retain the existing date-only and date-time boundary tests, changing only their surrounding selector assumptions.
Cover all of these cases:
tool.call(container_codes=['CONT-1'], bl_numbers=['BL-1']) # raises ValueError
tool.call(container_codes=['CONT-1'], inbound_time_start='2026-09-01',
inbound_time_end='2026-09-02') # raises ValueError
tool.call(bl_numbers=['BL-1'], inbound_time_start='2026-09-01',
inbound_time_end='2026-09-02') # raises ValueError
tool.call(container_codes=[]) # raises ValueError
tool.call(bl_numbers=['BL-' + 'x' * 100]) # accepted at 100 chars
tool.call(container_codes=['x' * 101]) # raises ValueError
tool.call(bl_numbers=['BL-{0}'.format(i) for i in range(201)]) # raises ValueError
tool.call() # raises ValueError
The metadata test must also prove that passing the old field is impossible through the closed schema; do not add inbound_numbers back to the Python call signature.
Include ExportPalletDataTool() in the number-type clarification group and require its description to contain:
号码类型不明确时必须先询问用户不得根据号码格式猜测不得跨字段或跨工具试查Keep the existing asynchronous export boundary assertions.
Add a MCP_1401 case for container_codes and one for bl_numbers, asserting the public message is respectively 柜号参数不正确 and 提单号参数不正确, and that raw Python field names are absent from serialized output.
Run from Y:/mcp:
python -m unittest tests.test_export_pallet_data_tool tests.test_tool_description_boundaries tests.test_output_presenter
Expected result: failures identify the still-existing inbound_numbers schema/signature/CLI behavior and missing field labels. Do not change production code until these tests demonstrate the contract gap.
Files:
Y:/mcp/tools/export_pallet_data.pyY:/mcp/app.pyModify: Y:/mcp/services/output_presenter.py
[ ] Step 1: Replace the metadata fields and descriptions.
Define container_codes and bl_numbers with the same array constraints as the former number field:
{
'type': 'array',
'minItems': 1,
'maxItems': 200,
'items': {
'type': 'string',
'minLength': 1,
'maxLength': 100,
'pattern': r'.*\S.*',
},
}
Describe each field using its Chinese business label, say that it is only valid when the user explicitly identifies that type, and prohibit other number types and internal IDs. Update the tool description to say the selector is exactly “柜号 / 提单号 / 海外仓入库时间”三选一. Explicitly state that an unclear number type requires a user question before calling and that the tool must not guess or try another field.
Use one helper with the following behavior for both fields:
def _clean_numbers(self, field_name, values):
if not isinstance(values, list) or not values:
raise ValueError('{0} must be a non-empty list'.format(field_name))
cleaned = []
for value in values:
if not isinstance(value, str):
raise ValueError('{0} items must be strings'.format(field_name))
item = value.strip()
if not item or len(item) > 100:
raise ValueError(
'{0} items must be 1 to 100 chars'.format(field_name)
)
if item not in cleaned:
cleaned.append(item)
if len(cleaned) > 200:
raise ValueError('at most 200 {0}'.format(field_name))
return cleaned
call() to enforce the three-way XOR.The public signature must be:
def call(
self,
container_codes=None,
bl_numbers=None,
inbound_time_start=None,
inbound_time_end=None,
request_id='rq_export_pallet_data',
):
Treat a non-None number argument as supplied, even if it is an empty list, so malformed empty arrays cannot silently disappear. Reject both number arguments together, reject any number argument combined with a time range, require both time bounds when time is selected, and reject an entirely empty payload. Forward exactly one cleaned selector or the two original time-bound strings.
--container-codes and --bl-numbers already exist as shared CLI options for other tools. Remove only the obsolete --inbound-numbers parser option and change the export_pallet_data branch to build:
if args.container_codes:
tool_args['container_codes'] = parse_string_list(args.container_codes)
if args.bl_numbers:
tool_args['bl_numbers'] = parse_string_list(args.bl_numbers)
if args.inbound_time_start:
tool_args['inbound_time_start'] = args.inbound_time_start
if args.inbound_time_end:
tool_args['inbound_time_end'] = args.inbound_time_end
tool_args.pop('page', None)
tool_args.pop('limit', None)
Extend OutputPresenter.FIELD_LABELS['export_pallet_data'] with:
'container_codes': '柜号',
'bl_numbers': '提单号',
'inbound_time_start': '入库时间开始',
'inbound_time_end': '入库时间结束',
Do not expose raw field names in the public error text.
Run:
python -m unittest tests.test_export_pallet_data_tool tests.test_tool_description_boundaries tests.test_output_presenter
Expected result: all focused tests pass, including the unchanged 26-tool registry and 25-safe-tool assertions.
Files:
Y:/settlement_tests/tests/Unit/FmsOperate/McpPalletDataExportTest.phpModify: Y:/settlement_tests/tests/Unit/FmsOperate/McpPalletDataExportJobTest.php
[ ] Step 1: Update Validate test inputs to the new selector names.
Change the accepted cases to:
$this->assertTrue($this->validator()->check([
'container_codes' => ['CONT-1'],
]));
$this->assertTrue($this->validator()->check([
'bl_numbers' => ['BL-1'],
]));
$this->assertTrue($this->validator()->check([
'inbound_time_start' => '2026-09-01',
'inbound_time_end' => '2026-09-07 18:30:00',
]));
Assert false for:
[
'container_codes' => ['CONT-1'],
'bl_numbers' => ['BL-1'],
]
[
'container_codes' => ['CONT-1'],
'inbound_time_start' => '2026-09-01',
'inbound_time_end' => '2026-09-02',
]
[
'inbound_numbers' => ['INB-1'],
]
Retain assertions for one-sided time, over-31-day time, 201 items, forbidden pagination, ids, and inbound_id.
Use container_codes in the submission test and assert the submitted task parameters contain the normalized new field, trusted company_id, operator_id, is_super, and mcp_source_tool, while still excluding ADMIN_ID.
Add a second submission assertion using bl_numbers so both code paths persist the correct selector.
Assert the model source contains fms_outbound, container_code, fms_booking_detail, and bl_number, keeps company_id, does not contain CONVERT_TZ or COUNT(, and does not contain a combined container_code|bl_number OR expression. This freezes the performance requirement that the two number types use separate branches.
Change existing queued-task JSON fixtures from inbound_numbers to container_codes. Add a successful bl_numbers fixture. Add a test whose task JSON contains only inbound_numbers; assert the worker does not start the task, exporter is not called, and the task is marked failed.
Run from Y:/settlement_tests:
bin\test.bat fmsoperate --filter McpPalletDataExport
Expected result: failures identify the old validator field, old Logic parameter payload, missing model branches, and worker assumptions. Do not weaken the new assertions to accommodate the old contract.
Files:
Y:/fmsoperate/app/mcp/validate/McpToolValidate.phpY:/fmsoperate/app/mcp/logic/McpPalletDataExportLogic.phpModify: Y:/fmsoperate/app/mcp/controller/McpToolsController.php
[ ] Step 1: Collapse the duplicate exportPalletData scene definition.
Keep one scene definition containing only:
'container_codes',
'bl_numbers',
'inbound_time_start',
'inbound_time_end',
'page',
'limit',
'ids',
'inbound_id',
'inbound_numbers',
The last five legacy/forbidden keys must be attached to palletExportForbidden, with inbound_numbers included only to return MCP_1401 when an old caller sends it. Do not expose it in the Gateway schema or accept it in Logic normalization.
Keep the existing 1–200, string, non-empty, 100-character palletNumberArray behavior and attach it to both container_codes and bl_numbers. Update the XOR callback to evaluate:
$hasContainer = is_array($data['container_codes'] ?? null)
&& $data['container_codes'] !== [];
$hasBl = is_array($data['bl_numbers'] ?? null)
&& $data['bl_numbers'] !== [];
$hasStart = trim((string)($data['inbound_time_start'] ?? '')) !== '';
$hasEnd = trim((string)($data['inbound_time_end'] ?? '')) !== '';
return !($hasContainer && $hasBl)
&& !($hasContainer && ($hasStart || $hasEnd))
&& !($hasBl && ($hasStart || $hasEnd))
&& (($hasContainer || $hasBl) || ($hasStart && $hasEnd));
Retain the existing date parser and maximum 31-calendar-day rule.
Replace inbound_numbers normalization with two independent de-duplicating loops for container_codes and bl_numbers. The normalized result must always contain both arrays, even when one is empty:
[
'container_codes' => $containerCodes,
'bl_numbers' => $blNumbers,
'inbound_time_start' => ...,
'inbound_time_end' => ...,
'request_id' => ...,
]
buildExportParams() must add only these normalized selectors plus trusted company_id, timezone, operator, operator_id, is_super, and mcp_source_tool; never copy caller-supplied identity fields.
validateFilters() must enforce the same three-way XOR as Validate, including per-field 200/100 limits. hasExportableData() and findPalletIds() calls must receive both arrays and both time bounds. Keep the existing permission checks, no-data MCP_1601, async submission, task reference, and retry response unchanged.
In exportPalletData(), replace the old log-field list with:
[
'container_codes',
'bl_numbers',
'inbound_time_start',
'inbound_time_end',
]
The route, route code, permission checks, response envelope, and task-source mapping remain unchanged.
Run:
bin\test.bat fmsoperate --filter McpPalletDataExport
Expected result: Validate and Logic tests pass; model and worker tests remain red until Tasks 5 and 6 are complete.
Files:
Y:/fmsoperate/app/mcp/model/McpPalletDataExportModel.phpModify: Y:/fmsoperate/app/mcp/logic/McpPalletDataExportService.php
[ ] Step 1: Change the model method signatures.
Use this argument order consistently:
public function hasExportableData(
$companyId,
array $containerCodes,
array $blNumbers,
$timeStart,
$timeEnd
)
public function findPalletIds(
$companyId,
array $containerCodes,
array $blNumbers,
$timeStart,
$timeEnd
)
Add findInboundIdsByContainerCodes($companyId, array $codes) using one query that:
Db::table('fms_destination_inbound')->alias('di')
->join(['fms_outbound' => 'o'], 'di.outbound_id = o.id')
->where('di.company_id', (int)$companyId)
->where('di.is_delete', 0)
->where('o.container_code', 'in', $codes)
->field('di.id')
->group('di.id')
Return integer inbound IDs. Do not loop over the 200 values.
Add findInboundIdsByBlNumbers($companyId, array $numbers) using:
Db::table('fms_destination_inbound')->alias('di')
->join(['fms_outbound' => 'o'], 'di.outbound_id = o.id')
->join(['fms_booking_detail' => 'bd'], 'o.booking_detail_id = bd.id')
->where('di.company_id', (int)$companyId)
->where('di.is_delete', 0)
->where('bd.bl_number', 'in', $numbers)
->field('di.id')
->group('di.id')
Return integer inbound IDs and keep the branch separate from the container query. Do not use where('o.container_code|bd.bl_number', ...).
findPalletIds().Use this branch order:
if ($containerCodes !== []) {
$inboundIds = $this->findInboundIdsByContainerCodes($companyId, $containerCodes);
} elseif ($blNumbers !== []) {
$inboundIds = $this->findInboundIdsByBlNumbers($companyId, $blNumbers);
} else {
$inboundIds = $this->findInboundIdsByTime($companyId, $timeStart, $timeEnd);
}
Keep the current inbound ID → order ID → pallet ID stages, company condition on pallet batches, and no COUNT behavior.
Replace the old Service call with:
$palletIds = $this->model->findPalletIds(
$companyId,
is_array($params['container_codes'] ?? null)
? $params['container_codes'] : [],
is_array($params['bl_numbers'] ?? null)
? $params['bl_numbers'] : [],
(string)($params['inbound_time_start'] ?? ''),
(string)($params['inbound_time_end'] ?? '')
);
Do not change Excel headers, row grouping, attachment download behavior, zip layout, uploader behavior, or temporary-file cleanup.
Run:
bin\test.bat fmsoperate --filter McpPalletDataExport
php -l Y:\fmsoperate\app\mcp\validate\McpToolValidate.php
php -l Y:\fmsoperate\app\mcp\logic\McpPalletDataExportLogic.php
php -l Y:\fmsoperate\app\mcp\model\McpPalletDataExportModel.php
php -l Y:\fmsoperate\app\mcp\logic\McpPalletDataExportService.php
php -l Y:\fmsoperate\app\mcp\controller\McpToolsController.php
Expected result: focused tests pass and every changed PHP file reports no syntax errors.
Files:
Y:/fmsoperate/app/job/think/handle/inside/McpPalletDataExport.phpModify: Y:/settlement_tests/tests/Unit/FmsOperate/McpPalletDataExportJobTest.php
[ ] Step 1: Validate the persisted selector contract before starting.
In validatedParams(), reject any task containing inbound_numbers. Accept exactly one of:
$containerCodes = $params['container_codes'] ?? [];
$blNumbers = $params['bl_numbers'] ?? [];
$hasContainer = is_array($containerCodes) && $containerCodes !== [];
$hasBl = is_array($blNumbers) && $blNumbers !== [];
$hasTime = trim((string)($params['inbound_time_start'] ?? '')) !== ''
&& trim((string)($params['inbound_time_end'] ?? '')) !== '';
if (
($hasContainer && $hasBl)
|| (($hasContainer || $hasBl) && $hasTime)
|| (!$hasContainer && !$hasBl && !$hasTime)
) {
return false;
}
Also require each new array to contain strings with 1–100-character trimmed values and at most 200 entries, so a manually altered task cannot bypass the HTTP validator.
Do not change message/company/operator matching, atomic queued-task start, exporter invocation, completed file mapping, failed-state update, or exception rethrow. Only the persisted selector validation changes.
Keep the existing success, identity mismatch, and exception tests, replacing old task JSON with container_codes. Add a bill-of-lading success fixture and a legacy inbound_numbers fixture that never reaches the exporter and ends in status 3.
Run:
bin\test.bat fmsoperate --filter McpPalletDataExportJob
Expected result: all worker identity, success, failure, and legacy-selector tests pass.
Files:
Y:/mcp/README.mdY:/mcp/CONTEXT.mdY:/all_project_docs/fmsoperate/requirements.mdY:/all_project_docs/fmsoperate/tech-specs.mdY:/all_project_docs/fmsoperate/user-structure.mdY:/all_project_docs/fmsoperate/timeline.mdY:/all_project_docs/fmsoperate/sql/mcp_add_pallet_data_export_tool.sqlY:/all_project_docs/mcp/overview.mdY:/all_project_docs/mcp/requirements.mdY:/all_project_docs/mcp/tech-specs.mdY:/all_project_docs/mcp/user-structure.mdY:/all_project_docs/mcp/timeline.mdModify: Y:/settlement_tests/docs/manual/mcp-async-export-workbuddy-cases.md
[ ] Step 1: Replace the fmsoperate business contract.
In the export_pallet_data sections, state that the selector is exactly one of:
container_codes(柜号数组,1–200)
bl_numbers(提单号数组,1–200)
inbound_time_start + inbound_time_end(最多31个日历日)
State that each number is at most 100 characters, container and bill-of-lading arrays cannot coexist, neither can mix with time, and inbound_numbers/internal IDs/order numbers/pallet-batch numbers are unsupported. Preserve the existing company isolation, permissions, async task, output, attachment, and MCP_1601 behavior.
Update the Gateway README and central MCP documents to tell the caller:
query_export_task.Remove statements saying the tool accepts an inbound number.
Replace stale terminology in Y:/mcp/CONTEXT.md. Add pallet export placeholders for <有权柜号> and <有权提单号> to the manual, then add cases for:
Do not add real credentials, URLs, task references, or customer data.
Change the st_mcp_tool_registry.description text in mcp_add_pallet_data_export_tool.sql from inbound-number/time wording to container-number/bill-of-lading/time wording. Keep status defaults and the idempotent ON DUPLICATE KEY UPDATE behavior unchanged. Do not run the SQL or change the execution status in sql/README.md.
Search all affected documentation for the old active contract:
rg -n "入仓单号数组|入仓单号或海外仓入库时间|inbound_numbers" Y:\mcp Y:\all_project_docs\fmsoperate Y:\all_project_docs\mcp Y:\settlement_tests\docs
Remaining matches must be limited to the design/implementation history explicitly saying that the old field is removed or rejected; no current usage instructions may tell callers to send it.
Files: all changed files from Tasks 1–7.
From Y:/mcp:
python -m unittest discover -s tests -p "test_*.py"
python -m coverage run -m unittest discover -s tests -p "test_*.py"
python -m coverage report -m --fail-under=100
Expected result: all tests pass and the strict coverage threshold is satisfied. Do not claim coverage success without the actual report.
From Y:/settlement_tests:
bin\test.bat fmsoperate --filter McpPalletDataExport
Then run the full fmsoperate suite:
bin\test.bat fmsoperate
Expected result: both the focused pallet-export tests and the full fmsoperate test configuration pass.
Run php -l for:
Y:/fmsoperate/app/mcp/validate/McpToolValidate.php
Y:/fmsoperate/app/mcp/logic/McpPalletDataExportLogic.php
Y:/fmsoperate/app/mcp/model/McpPalletDataExportModel.php
Y:/fmsoperate/app/mcp/logic/McpPalletDataExportService.php
Y:/fmsoperate/app/mcp/controller/McpToolsController.php
Y:/fmsoperate/app/job/think/handle/inside/McpPalletDataExport.php
Run from each repository:
git diff --check
git status --short
Run the same checks from Y:/all_project_docs. Review that only the requested files and the approved design/plan are changed; do not reset, checkout, clean, commit, execute SQL, or restart services.
Confirm all of the following from code and tests:
fms_outbound.container_code.fms_booking_detail.bl_number.