dsh-data-cleaning-agent 0.6.2 → 0.7.0

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.
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { randomUUID } from 'node:crypto';
10
10
  import { QccBridgeError } from './qcc.js';
11
- import { normalizeFieldSelection } from './workflow-contract.js';
11
+ import { fieldLabel, normalizeFieldSelection } from './workflow-contract.js';
12
12
 
13
13
  export const TOOL_QCC_COMMAND = 'data_cleaning_qcc_run';
14
14
 
@@ -70,23 +70,34 @@ function normalizedInput(input = {}) {
70
70
  }
71
71
 
72
72
  export function serializeQccCommandPrompt(command) {
73
- const visible = {
74
- schemaVersion: 1,
75
- commandId: command.commandId,
76
- taskId: command.taskId,
77
- kind: command.kind,
78
- };
73
+ const input = command.input ?? {};
74
+ const action = command.kind === 'resolve'
75
+ ? '确认并继续处理已选的企业主体'
76
+ : command.kind === 'retry'
77
+ ? '重试已选的可恢复失败项'
78
+ : '执行企业主体匹配与字段补全';
79
+ const rowCount = command.kind === 'enrich'
80
+ ? (Array.isArray(input.rows) ? input.rows.length : 0)
81
+ : command.kind === 'retry'
82
+ ? (Array.isArray(input.companyNames) ? input.companyNames.length : 0)
83
+ : 1;
84
+ const fields = command.kind === 'enrich'
85
+ ? normalizeFieldSelection(input.fieldSelection).map(fieldLabel)
86
+ : [];
79
87
  return [
80
- '请执行数据清洗补全企查查任务。',
88
+ '请执行已在「数据清洗补全工作台」确认的企业数据任务。',
81
89
  '',
82
- '类型化任务意图(schemaVersion 1):',
83
- '```json',
84
- JSON.stringify(visible, null, 2),
85
- '```',
90
+ `处理内容:${action}。`,
91
+ `任务编号:${command.taskId}。`,
92
+ `处理数量:${rowCount} 条企业记录。`,
93
+ ...(fields.length ? [`需要补全:${fields.join('')}。`] : []),
94
+ '执行边界:名单、字段和额度确认已安全暂存在本机 Host;使用当前用户自己连接的企查查 MCP 账号。',
86
95
  '',
87
- `请准确调用一次 ${TOOL_QCC_COMMAND} 并只传递 commandId。`,
88
- '企业名单和字段选择已安全暂存在本机 Host,不得要求用户在对话中重复粘贴,不得直接调用任何 mcp__qcc-* 工具。',
89
- '高层工具返回后立即结束本轮;不得重试、扩大名单或追加字段。',
96
+ '你可以在发送前补充任务备注。如需修改名单、匹配规则或补全字段,请先返回工作台调整后重新生成。',
97
+ `安全任务凭证:${command.commandId}`,
98
+ '',
99
+ `发送本说明后,请仅调用一次数据清洗补全执行工具(${TOOL_QCC_COMMAND}),参数只传递上述安全任务凭证。`,
100
+ '不要要求重复粘贴企业名单,不要直接调用 mcp__qcc-* 工具,不要重试、扩大名单或追加字段。',
90
101
  ].join('\n');
91
102
  }
92
103
 
@@ -234,7 +245,7 @@ export class QccCommandStore {
234
245
  export function registerQccCommandTool(tools, commands) {
235
246
  return tools.register({
236
247
  name: TOOL_QCC_COMMAND,
237
- description: 'Execute one already-staged data-cleaning QCC command. Call only when a visible typed intent supplies commandId. The Host owns rows, billing confirmation, idempotency and result artifacts.',
248
+ description: 'Execute one already-staged data-cleaning QCC command. Call only when a visible readable workbench task summary supplies a dcq-* commandId. The Host owns rows, billing confirmation, idempotency and result artifacts.',
238
249
  parameters: {
239
250
  type: 'object',
240
251
  additionalProperties: false,
@@ -0,0 +1,185 @@
1
+ /**
2
+ * 数据清洗补全的一企一行字段目录。
3
+ *
4
+ * 这里只收录已核对 QCC MCP 一手实现、能够稳定投影为单个 Excel 单元格的字段。
5
+ * 任何明细列表(电话全集、海关资质、风险关联方等)都不得进入本目录。
6
+ */
7
+
8
+ const field = (id, label, options = {}) => Object.freeze({ id, label, ...options });
9
+ const group = (id, label, sourceTool, fields, options = {}) => Object.freeze({
10
+ id,
11
+ label,
12
+ sourceTool,
13
+ ...options,
14
+ fields: Object.freeze(fields),
15
+ });
16
+
17
+ export const SELF_RISK_FACTORS = Object.freeze([
18
+ ['dishonest', '失信信息'],
19
+ ['judgment_debtor', '被执行人'],
20
+ ['consumption_restriction', '限制高消费'],
21
+ ['terminated_case', '终本案件'],
22
+ ['judicial_document', '裁判文书'],
23
+ ['case_filing', '立案信息'],
24
+ ['hearing_announcement', '开庭公告'],
25
+ ['court_announcement', '法院公告'],
26
+ ['service_notice', '送达公告'],
27
+ ['bankruptcy_reorganization', '破产重整'],
28
+ ['equity_freeze', '股权冻结'],
29
+ ['judicial_auction', '司法拍卖'],
30
+ ['valuation_inquiry', '询价评估'],
31
+ ['pre_litigation_mediation', '诉前调解'],
32
+ ['exit_restriction', '限制出境'],
33
+ ['administrative_penalty', '行政处罚'],
34
+ ['operating_exception', '经营异常'],
35
+ ['serious_violation', '严重违法'],
36
+ ['environmental_penalty', '环保处罚'],
37
+ ['abnormal_taxpayer', '税务非正常户'],
38
+ ['tax_arrears', '欠税公告'],
39
+ ['tax_violation', '税收违法'],
40
+ ['disciplinary_list', '惩戒名单'],
41
+ ['default_matter', '违约事项'],
42
+ ['guarantee', '担保信息'],
43
+ ['equity_pledge_registration', '股权出质'],
44
+ ['stock_pledge', '股权质押'],
45
+ ['chattel_mortgage', '动产抵押'],
46
+ ['land_mortgage', '土地抵押'],
47
+ ['simple_cancellation', '简易注销'],
48
+ ['cancellation_filing', '注销备案'],
49
+ ['liquidation', '清算信息'],
50
+ ['labor_arbitration', '劳动仲裁'],
51
+ ['public_notice', '公示催告'],
52
+ ['property_reward_notice', '财产悬赏公告'],
53
+ ].map(Object.freeze));
54
+
55
+ export const RELATED_RISK_FACTORS = Object.freeze([
56
+ ['dishonest', '失信被执行人'],
57
+ ['judgment_debtor', '被执行人'],
58
+ ['consumption_restriction', '限制高消费'],
59
+ ['serious_violation', '严重违法'],
60
+ ['tax_violation', '税收违法'],
61
+ ['administrative_penalty', '行政处罚'],
62
+ ['bankruptcy_reorganization', '破产重整'],
63
+ ['terminated_case', '终本案件'],
64
+ ['equity_freeze', '股权冻结'],
65
+ ['tax_arrears', '欠税公告'],
66
+ ['operating_exception', '经营异常'],
67
+ ].map(Object.freeze));
68
+
69
+ export const RELATED_RISK_KEY_FACTORS = Object.freeze([
70
+ ['dishonest', '失信被执行人'],
71
+ ['serious_violation', '严重违法'],
72
+ ['bankruptcy_reorganization', '破产重整'],
73
+ ['tax_violation', '税收违法'],
74
+ ['judgment_debtor', '被执行人'],
75
+ ['terminated_case', '终本案件'],
76
+ ['equity_freeze', '股权冻结'],
77
+ ].map(Object.freeze));
78
+
79
+ export const RISK_FACTOR_CATALOG_VERSION = 'qcc-risk-snapshot-2026-09-05';
80
+
81
+ export const QCC_FIELD_CATALOG = Object.freeze([
82
+ group('company_registration', '企业工商信息', 'get_company_registration_info', [
83
+ field('company_name', '企业名称', { inputAnchor: true, defaultSelected: true }),
84
+ field('credit_no', '统一社会信用代码', { inputAnchor: true, defaultSelected: true }),
85
+ field('reg_no', '注册号', { inputAnchor: true }),
86
+ field('org_no', '组织机构代码'),
87
+ field('tax_no', '纳税人识别号'),
88
+ field('reg_status', '登记状态', { defaultSelected: true }),
89
+ field('legal_rep', '法定代表人', { defaultSelected: true }),
90
+ field('reg_capital', '注册资本', { defaultSelected: true }),
91
+ field('paid_capital', '实缴资本'),
92
+ field('establish_date', '成立日期', { defaultSelected: true }),
93
+ field('company_type', '企业类型'),
94
+ field('approval_date', '核准日期'),
95
+ field('registration_authority', '登记机关'),
96
+ field('taxpayer_qualification', '纳税人资质'),
97
+ field('payment_line_no', '支付系统行号'),
98
+ field('import_export_company_code', '进出口企业代码'),
99
+ field('short_name', '企业简称'),
100
+ field('english_name', '英文名'),
101
+ field('registered_address', '注册地址', { defaultSelected: true }),
102
+ field('mailing_address', '通信地址'),
103
+ field('region', '所属地区', { matchAuxiliary: true }),
104
+ field('business_scope', '经营范围'),
105
+ field('industry_category', '国标行业'),
106
+ field('operating_period', '营业期限'),
107
+ field('company_size', '人员规模'),
108
+ field('insured_count', '参保人数'),
109
+ field('branch_insured_count', '分支机构参保人数'),
110
+ ], { releaseBatch: 'current' }),
111
+ group('company_profile', '企业简介', 'get_company_profile', [
112
+ field('qcc_industry', '企查查行业'),
113
+ field('company_profile', '企业简介'),
114
+ field('industry_chain_overview', '产业链概览'),
115
+ ], { releaseBatch: 'current' }),
116
+ group('contact_info', '联系方式', 'get_contact_info', [
117
+ field('contact_preferred_phone', '首选联系电话'),
118
+ field('contact_phone_invalid_flag', '首选电话无效标记'),
119
+ field('contact_phone_tags', '首选电话标签'),
120
+ field('contact_preferred_email', '首选邮箱'),
121
+ field('contact_official_website', '官方网站'),
122
+ field('contact_official_website_icp', '官网 ICP 备案'),
123
+ ], { releaseBatch: 'batch-1' }),
124
+ group('listing_info', '上市信息', 'get_listing_info', [
125
+ field('listing_date', '上市日期'),
126
+ field('listing_short_name', '股票简称'),
127
+ field('listing_stock_code', '股票代码'),
128
+ field('listing_exchange', '上市交易所'),
129
+ field('listing_board', '上市板块'),
130
+ field('listing_former_short_name', '上市曾用名'),
131
+ field('listing_total_market_value', '总市值'),
132
+ field('listing_total_shares', '总股本'),
133
+ field('listing_predicted_pe', '预测市盈率'),
134
+ field('listing_float_market_value', '流通值'),
135
+ field('listing_float_shares', '流通股'),
136
+ field('listing_pb_ratio', '市净率'),
137
+ field('listing_eps', 'EPS'),
138
+ field('listing_voting_rights_difference', '表决权差异'),
139
+ field('listing_registration_based', '是否注册制'),
140
+ ], { releaseBatch: 'batch-1', selectionNote: '首个 A 股或红筹证券快照' }),
141
+ group('tax_invoice_info', '税务开票信息', 'get_tax_invoice_info', [
142
+ field('tax_company_name', '税务主体名称'),
143
+ field('tax_identification_no', '税务纳税人识别号'),
144
+ field('tax_company_type', '税务企业类型'),
145
+ field('tax_business_status', '税务经营状态'),
146
+ field('invoice_address', '开票地址'),
147
+ field('invoice_phone', '开票联系电话'),
148
+ field('invoice_bank', '开户行'),
149
+ field('invoice_bank_account', '开户行账号'),
150
+ ], { releaseBatch: 'batch-1' }),
151
+ group('import_export_credit', '进出口信用', 'get_import_export_credit', [
152
+ field('import_export_credit_no', '进出口统一社会信用代码'),
153
+ field('import_export_customs', '所在地海关'),
154
+ field('import_export_admin_division', '进出口行政区划'),
155
+ field('import_export_address', '进出口备案地址'),
156
+ field('import_export_economic_area', '经济区划'),
157
+ field('import_export_trade_type', '经营类别'),
158
+ field('import_export_statistical_economic_area', '统计经济区划'),
159
+ field('import_export_industry', '进出口行业种类'),
160
+ field('import_export_ecommerce_type', '跨境贸易电子商务类型'),
161
+ field('import_export_credit_grade', '海关信用等级'),
162
+ field('import_export_filing_date', '进出口备案日期'),
163
+ ], { releaseBatch: 'batch-1' }),
164
+ group('company_risk_scan', '企业自身风险扫描', 'get_company_risk_scan', [
165
+ field('risk_recorded_factor_count', '风险有记录因子数'),
166
+ field('risk_no_record_factor_count', '风险无记录因子数'),
167
+ field('risk_hit_summary', '企业自身风险命中摘要'),
168
+ ...SELF_RISK_FACTORS.map(([id, label]) => field(`risk_${id}_count`, `${label}条目数`)),
169
+ ], { releaseBatch: 'batch-2', catalogVersion: RISK_FACTOR_CATALOG_VERSION }),
170
+ group('company_related_risk_scan', '企业关联风险扫描', 'get_company_related_risk_scan', [
171
+ field('related_risk_party_count', '有风险关联方数'),
172
+ field('related_risk_summary', '企业关联风险摘要'),
173
+ ...RELATED_RISK_FACTORS.map(([id, label]) => field(`related_risk_${id}_count`, `关联风险-${label}条目数`)),
174
+ ...RELATED_RISK_KEY_FACTORS.map(([id, label]) => field(`related_risk_${id}_party_count`, `关联风险-${label}命中关联方数`)),
175
+ ], { releaseBatch: 'batch-2', catalogVersion: RISK_FACTOR_CATALOG_VERSION }),
176
+ ]);
177
+
178
+ export const QCC_FIELD_SOURCE_TOOL = Object.freeze(Object.fromEntries(
179
+ QCC_FIELD_CATALOG.flatMap((entry) => entry.fields.map((item) => [item.id, entry.sourceTool])),
180
+ ));
181
+
182
+ export function selectedSourceTools(fieldSelection, fallbackFields = []) {
183
+ const fields = Array.isArray(fieldSelection) && fieldSelection.length ? fieldSelection : fallbackFields;
184
+ return [...new Set(fields.map((id) => QCC_FIELD_SOURCE_TOOL[String(id)]).filter(Boolean))];
185
+ }
package/lib/qcc-safety.js CHANGED
@@ -63,7 +63,11 @@ export function redactSensitive(value, options = {}) {
63
63
 
64
64
  export function safeAuditEvent(event) {
65
65
  const source = event && typeof event === 'object' ? event : {};
66
- return {
66
+ const safeCatalogLabels = (value) => (Array.isArray(value) ? value : [])
67
+ .slice(0, 64)
68
+ .map((item) => redactSensitiveText(String(item ?? '')).slice(0, 128))
69
+ .filter(Boolean);
70
+ const safe = {
67
71
  at: String(source.at ?? new Date().toISOString()),
68
72
  event: 'qcc-tool-call',
69
73
  toolName: String(source.toolName ?? ''),
@@ -74,4 +78,10 @@ export function safeAuditEvent(event) {
74
78
  upstreamCode: source.upstreamCode ? String(source.upstreamCode) : null,
75
79
  durationMs: Math.max(0, Number(source.durationMs ?? 0)),
76
80
  };
81
+ if (source.catalogVersion || Array.isArray(source.missing) || Array.isArray(source.unknown)) {
82
+ safe.catalogVersion = source.catalogVersion ? String(source.catalogVersion).slice(0, 64) : null;
83
+ safe.missing = safeCatalogLabels(source.missing);
84
+ safe.unknown = safeCatalogLabels(source.unknown);
85
+ }
86
+ return safe;
77
87
  }