@roll-agent/smart-reply-agent 0.1.0 → 0.1.1
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/dist/ai/model-registry.d.ts +0 -1
- package/dist/ai/model-registry.js +1 -205
- package/dist/ai/structured-output.d.ts +0 -1
- package/dist/ai/structured-output.js +1 -78
- package/dist/errors/app-error.d.ts +0 -1
- package/dist/errors/app-error.js +1 -95
- package/dist/errors/error-codes.d.ts +0 -1
- package/dist/errors/error-codes.js +1 -115
- package/dist/errors/error-factory.d.ts +0 -1
- package/dist/errors/error-factory.js +1 -86
- package/dist/errors/error-utils.d.ts +0 -1
- package/dist/errors/error-utils.js +1 -188
- package/dist/errors/index.d.ts +0 -1
- package/dist/errors/index.js +1 -5
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1 -12
- package/dist/log-control.d.ts +0 -1
- package/dist/log-control.js +1 -15
- package/dist/pipeline/age-eligibility.d.ts +0 -1
- package/dist/pipeline/age-eligibility.js +1 -176
- package/dist/pipeline/candidate-context.d.ts +0 -1
- package/dist/pipeline/candidate-context.js +1 -31
- package/dist/pipeline/candidate-utils.d.ts +0 -1
- package/dist/pipeline/candidate-utils.js +1 -33
- package/dist/pipeline/classification.d.ts +0 -1
- package/dist/pipeline/classification.js +1 -206
- package/dist/pipeline/context-builder.d.ts +0 -1
- package/dist/pipeline/context-builder.js +1 -404
- package/dist/pipeline/pipeline-progress.d.ts +0 -1
- package/dist/pipeline/pipeline-progress.js +1 -33
- package/dist/pipeline/reply-gate.d.ts +0 -1
- package/dist/pipeline/reply-gate.js +1 -139
- package/dist/pipeline/smart-reply.d.ts +0 -1
- package/dist/pipeline/smart-reply.js +1 -418
- package/dist/pipeline.d.ts +0 -1
- package/dist/pipeline.js +1 -12
- package/dist/services/brand-alias.d.ts +0 -1
- package/dist/services/brand-alias.js +1 -184
- package/dist/services/brand-config-selectors.d.ts +0 -1
- package/dist/services/brand-config-selectors.js +1 -30
- package/dist/services/config-loader.d.ts +0 -1
- package/dist/services/config-loader.js +1 -45
- package/dist/services/duliday-api.d.ts +0 -1
- package/dist/services/duliday-api.js +1 -160
- package/dist/services/duliday-mapper.d.ts +0 -1
- package/dist/services/duliday-mapper.js +1 -536
- package/dist/tools/generate-reply.d.ts +0 -1
- package/dist/tools/generate-reply.js +1 -132
- package/dist/tools/sync-brand-data.d.ts +0 -1
- package/dist/tools/sync-brand-data.js +1 -114
- package/dist/types/brand-resolution.d.ts +0 -1
- package/dist/types/brand-resolution.js +1 -37
- package/dist/types/classification.d.ts +0 -1
- package/dist/types/classification.js +1 -30
- package/dist/types/config.d.ts +0 -1
- package/dist/types/config.js +1 -7
- package/dist/types/duliday-api.d.ts +0 -1
- package/dist/types/duliday-api.js +1 -235
- package/dist/types/geocoding.d.ts +0 -1
- package/dist/types/geocoding.js +1 -12
- package/dist/types/reply-policy.d.ts +0 -1
- package/dist/types/reply-policy.js +1 -332
- package/dist/types/zhipin.d.ts +0 -1
- package/dist/types/zhipin.js +1 -123
- package/package.json +3 -3
- package/dist/ai/model-registry.d.ts.map +0 -1
- package/dist/ai/model-registry.js.map +0 -1
- package/dist/ai/structured-output.d.ts.map +0 -1
- package/dist/ai/structured-output.js.map +0 -1
- package/dist/errors/app-error.d.ts.map +0 -1
- package/dist/errors/app-error.js.map +0 -1
- package/dist/errors/error-codes.d.ts.map +0 -1
- package/dist/errors/error-codes.js.map +0 -1
- package/dist/errors/error-factory.d.ts.map +0 -1
- package/dist/errors/error-factory.js.map +0 -1
- package/dist/errors/error-utils.d.ts.map +0 -1
- package/dist/errors/error-utils.js.map +0 -1
- package/dist/errors/index.d.ts.map +0 -1
- package/dist/errors/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/log-control.d.ts.map +0 -1
- package/dist/log-control.js.map +0 -1
- package/dist/pipeline/age-eligibility.d.ts.map +0 -1
- package/dist/pipeline/age-eligibility.js.map +0 -1
- package/dist/pipeline/candidate-context.d.ts.map +0 -1
- package/dist/pipeline/candidate-context.js.map +0 -1
- package/dist/pipeline/candidate-utils.d.ts.map +0 -1
- package/dist/pipeline/candidate-utils.js.map +0 -1
- package/dist/pipeline/classification.d.ts.map +0 -1
- package/dist/pipeline/classification.js.map +0 -1
- package/dist/pipeline/context-builder.d.ts.map +0 -1
- package/dist/pipeline/context-builder.js.map +0 -1
- package/dist/pipeline/pipeline-progress.d.ts.map +0 -1
- package/dist/pipeline/pipeline-progress.js.map +0 -1
- package/dist/pipeline/reply-gate.d.ts.map +0 -1
- package/dist/pipeline/reply-gate.js.map +0 -1
- package/dist/pipeline/smart-reply.d.ts.map +0 -1
- package/dist/pipeline/smart-reply.js.map +0 -1
- package/dist/pipeline.d.ts.map +0 -1
- package/dist/pipeline.js.map +0 -1
- package/dist/services/brand-alias.d.ts.map +0 -1
- package/dist/services/brand-alias.js.map +0 -1
- package/dist/services/brand-config-selectors.d.ts.map +0 -1
- package/dist/services/brand-config-selectors.js.map +0 -1
- package/dist/services/config-loader.d.ts.map +0 -1
- package/dist/services/config-loader.js.map +0 -1
- package/dist/services/duliday-api.d.ts.map +0 -1
- package/dist/services/duliday-api.js.map +0 -1
- package/dist/services/duliday-mapper.d.ts.map +0 -1
- package/dist/services/duliday-mapper.js.map +0 -1
- package/dist/tools/generate-reply.d.ts.map +0 -1
- package/dist/tools/generate-reply.js.map +0 -1
- package/dist/tools/sync-brand-data.d.ts.map +0 -1
- package/dist/tools/sync-brand-data.js.map +0 -1
- package/dist/types/brand-resolution.d.ts.map +0 -1
- package/dist/types/brand-resolution.js.map +0 -1
- package/dist/types/classification.d.ts.map +0 -1
- package/dist/types/classification.js.map +0 -1
- package/dist/types/config.d.ts.map +0 -1
- package/dist/types/config.js.map +0 -1
- package/dist/types/duliday-api.d.ts.map +0 -1
- package/dist/types/duliday-api.js.map +0 -1
- package/dist/types/geocoding.d.ts.map +0 -1
- package/dist/types/geocoding.js.map +0 -1
- package/dist/types/reply-policy.d.ts.map +0 -1
- package/dist/types/reply-policy.js.map +0 -1
- package/dist/types/zhipin.d.ts.map +0 -1
- package/dist/types/zhipin.js.map +0 -1
|
@@ -1,206 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getDynamicRegistry, DEFAULT_MODEL_CONFIG, DEFAULT_PROVIDER_CONFIGS, } from "../ai/model-registry.js";
|
|
3
|
-
import { safeGenerateObject } from "../ai/structured-output.js";
|
|
4
|
-
import { FunnelStageSchema, ChannelTypeSchema, TurnPlanSchema, STAGE_DEFINITIONS, } from "../types/reply-policy.js";
|
|
5
|
-
import { BrandDataSchema } from "../types/classification.js";
|
|
6
|
-
function normalizeChannelType(channelType) {
|
|
7
|
-
const parsed = ChannelTypeSchema.safeParse(channelType);
|
|
8
|
-
return parsed.success ? parsed.data : "public";
|
|
9
|
-
}
|
|
10
|
-
function getActiveStages(channelType = "public") {
|
|
11
|
-
const normalizedChannelType = normalizeChannelType(channelType);
|
|
12
|
-
const activeStages = FunnelStageSchema.options.filter((stage) => STAGE_DEFINITIONS[stage].applicableChannels.includes(normalizedChannelType));
|
|
13
|
-
return activeStages.length > 0 ? activeStages : [...FunnelStageSchema.options];
|
|
14
|
-
}
|
|
15
|
-
function buildDynamicPlanningSchema(activeStages) {
|
|
16
|
-
return z.object({
|
|
17
|
-
stage: z.enum(activeStages),
|
|
18
|
-
subGoals: TurnPlanSchema.shape.subGoals,
|
|
19
|
-
needs: TurnPlanSchema.shape.needs,
|
|
20
|
-
primaryNeed: TurnPlanSchema.shape.primaryNeed,
|
|
21
|
-
riskFlags: TurnPlanSchema.shape.riskFlags,
|
|
22
|
-
confidence: TurnPlanSchema.shape.confidence,
|
|
23
|
-
extractedInfo: TurnPlanSchema.shape.extractedInfo,
|
|
24
|
-
reasoningText: TurnPlanSchema.shape.reasoningText,
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
const NEED_RULES = [
|
|
28
|
-
{ need: "salary", patterns: [/薪资|工资|时薪|底薪|提成|奖金|补贴|多少钱|收入/i] },
|
|
29
|
-
{ need: "schedule", patterns: [/排班|班次|几点|上班|下班|工时|周末|节假日|做几天/i] },
|
|
30
|
-
{ need: "policy", patterns: [/五险一金|社保|保险|合同|考勤|迟到|补班|试用期/i] },
|
|
31
|
-
{ need: "availability", patterns: [/还有名额|空位|可用时段|什么时候能上|明天能面/i] },
|
|
32
|
-
{ need: "location", patterns: [/在哪|位置|地址|附近|地铁|门店|哪个区|多远/i] },
|
|
33
|
-
{ need: "stores", patterns: [/门店|哪家店|哪些店|有店吗/i] },
|
|
34
|
-
{ need: "requirements", patterns: [/要求|条件|年龄|经验|学历|健康证|身高|体重/i] },
|
|
35
|
-
{ need: "interview", patterns: [/面试|到店|约时间|约面/i] },
|
|
36
|
-
{ need: "wechat", patterns: [/微信|vx|私聊|联系方式|加你/i] },
|
|
37
|
-
];
|
|
38
|
-
export const PRIMARY_NEED_PRIORITY = [
|
|
39
|
-
"salary",
|
|
40
|
-
"schedule",
|
|
41
|
-
"location",
|
|
42
|
-
"stores",
|
|
43
|
-
"policy",
|
|
44
|
-
"requirements",
|
|
45
|
-
"availability",
|
|
46
|
-
"interview",
|
|
47
|
-
"wechat",
|
|
48
|
-
"none",
|
|
49
|
-
];
|
|
50
|
-
function detectNeedsByText(text) {
|
|
51
|
-
const needs = new Set();
|
|
52
|
-
for (const rule of NEED_RULES) {
|
|
53
|
-
if (rule.patterns.some((p) => p.test(text)))
|
|
54
|
-
needs.add(rule.need);
|
|
55
|
-
}
|
|
56
|
-
if (needs.size === 0)
|
|
57
|
-
needs.add("none");
|
|
58
|
-
else
|
|
59
|
-
needs.delete("none");
|
|
60
|
-
return needs;
|
|
61
|
-
}
|
|
62
|
-
export function detectRuleNeeds(message, history) {
|
|
63
|
-
return detectNeedsByText(`${history.slice(-4).join(" ")} ${message}`);
|
|
64
|
-
}
|
|
65
|
-
function detectCurrentMessageNeeds(message) {
|
|
66
|
-
return detectNeedsByText(message);
|
|
67
|
-
}
|
|
68
|
-
export function selectContextNeeds(primaryNeed, availableNeedsInput, message, maxNeeds = 1) {
|
|
69
|
-
const availableNeeds = new Set(availableNeedsInput);
|
|
70
|
-
if (availableNeeds.size > 1 && availableNeeds.has("none"))
|
|
71
|
-
availableNeeds.delete("none");
|
|
72
|
-
const currentMessageNeeds = detectCurrentMessageNeeds(message);
|
|
73
|
-
currentMessageNeeds.delete("none");
|
|
74
|
-
const selected = [];
|
|
75
|
-
if (primaryNeed !== "none" && availableNeeds.has(primaryNeed))
|
|
76
|
-
selected.push(primaryNeed);
|
|
77
|
-
for (const need of PRIMARY_NEED_PRIORITY) {
|
|
78
|
-
if (selected.length >= maxNeeds)
|
|
79
|
-
break;
|
|
80
|
-
if (need === "none" || need === primaryNeed)
|
|
81
|
-
continue;
|
|
82
|
-
if (currentMessageNeeds.has(need) && availableNeeds.has(need))
|
|
83
|
-
selected.push(need);
|
|
84
|
-
}
|
|
85
|
-
if (selected.length > 0)
|
|
86
|
-
return selected;
|
|
87
|
-
return primaryNeed === "none" ? ["none"] : availableNeeds.has(primaryNeed) ? [primaryNeed] : ["none"];
|
|
88
|
-
}
|
|
89
|
-
export function selectPrimaryNeed(plannedPrimaryNeed, mergedNeedsInput, message) {
|
|
90
|
-
const mergedNeeds = new Set(mergedNeedsInput);
|
|
91
|
-
if (mergedNeeds.size > 1 && mergedNeeds.has("none"))
|
|
92
|
-
mergedNeeds.delete("none");
|
|
93
|
-
if (plannedPrimaryNeed && mergedNeeds.has(plannedPrimaryNeed))
|
|
94
|
-
return plannedPrimaryNeed;
|
|
95
|
-
const currentMessageNeeds = detectCurrentMessageNeeds(message);
|
|
96
|
-
currentMessageNeeds.delete("none");
|
|
97
|
-
for (const need of PRIMARY_NEED_PRIORITY) {
|
|
98
|
-
if (currentMessageNeeds.has(need) && mergedNeeds.has(need))
|
|
99
|
-
return need;
|
|
100
|
-
}
|
|
101
|
-
for (const need of PRIMARY_NEED_PRIORITY) {
|
|
102
|
-
if (mergedNeeds.has(need))
|
|
103
|
-
return need;
|
|
104
|
-
}
|
|
105
|
-
return "none";
|
|
106
|
-
}
|
|
107
|
-
export function sanitizePlan(plan, ruleNeeds, message) {
|
|
108
|
-
const mergedNeeds = new Set([...plan.needs, ...Array.from(ruleNeeds)]);
|
|
109
|
-
if (mergedNeeds.size > 1 && mergedNeeds.has("none"))
|
|
110
|
-
mergedNeeds.delete("none");
|
|
111
|
-
return {
|
|
112
|
-
...plan,
|
|
113
|
-
subGoals: plan.subGoals.slice(0, 2),
|
|
114
|
-
needs: Array.from(mergedNeeds),
|
|
115
|
-
primaryNeed: selectPrimaryNeed(plan.primaryNeed, mergedNeeds, message),
|
|
116
|
-
confidence: Number.isFinite(plan.confidence) ? Math.max(0, Math.min(1, plan.confidence)) : 0.5,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
function buildPlanningPrompt(message, history, brandData, channelType = "public", replyPolicy, knownCandidateFields) {
|
|
120
|
-
const system = [
|
|
121
|
-
"你是招聘对话回合规划器,不直接回复候选人。",
|
|
122
|
-
"你只输出结构化规划结果,用于后续回复生成。",
|
|
123
|
-
"规划目标:确定阶段目标(stage)、子目标(subGoals)、事实需求(needs)、主回答轴(primaryNeed)、风险标记(riskFlags)。",
|
|
124
|
-
].join("\n");
|
|
125
|
-
const normalizedChannelType = normalizeChannelType(channelType);
|
|
126
|
-
const activeStages = getActiveStages(normalizedChannelType);
|
|
127
|
-
const stageLines = activeStages.map((stage) => {
|
|
128
|
-
const def = STAGE_DEFINITIONS[stage];
|
|
129
|
-
const desc = replyPolicy?.stageGoals[stage]?.description || def.description;
|
|
130
|
-
return `- ${stage}: ${desc} (转入条件: ${def.transitionSignal})`;
|
|
131
|
-
});
|
|
132
|
-
const needsLine = normalizedChannelType === "private"
|
|
133
|
-
? "- stores, location, salary, schedule, policy, availability, requirements, interview, none"
|
|
134
|
-
: "- stores, location, salary, schedule, policy, availability, requirements, interview, wechat, none";
|
|
135
|
-
const prompt = [
|
|
136
|
-
"[阶段枚举与定义]",
|
|
137
|
-
...stageLines,
|
|
138
|
-
"",
|
|
139
|
-
"[needs枚举]",
|
|
140
|
-
needsLine,
|
|
141
|
-
"",
|
|
142
|
-
"[riskFlags枚举]",
|
|
143
|
-
"- insurance_promise_risk, age_sensitive, confrontation_emotion, urgency_high, qualification_mismatch",
|
|
144
|
-
"",
|
|
145
|
-
"[规则]",
|
|
146
|
-
"- 优先判断本轮主阶段(stage);subGoals 最多 2 项,只保留最关键的。",
|
|
147
|
-
"- 候选人追问事实时,必须打开对应 needs。",
|
|
148
|
-
"- primaryNeed 必须从 needs 中选择一个最主的 need;如果没有明确事实轴则填 none。",
|
|
149
|
-
"- 不确定时 confidence 降低,不要臆断。",
|
|
150
|
-
"- 根据转入条件判断阶段转化,不要停留在不匹配的阶段。",
|
|
151
|
-
...(knownCandidateFields && knownCandidateFields.length > 0
|
|
152
|
-
? [`- 候选人资料中已有:${knownCandidateFields.join("、")}。不要生成追问这些字段的 subGoal。`]
|
|
153
|
-
: []),
|
|
154
|
-
"",
|
|
155
|
-
"[品牌数据]",
|
|
156
|
-
JSON.stringify(brandData || {}),
|
|
157
|
-
"",
|
|
158
|
-
"[历史对话]",
|
|
159
|
-
history.slice(-8).join("\n") || "无",
|
|
160
|
-
"",
|
|
161
|
-
"[候选人消息]",
|
|
162
|
-
message,
|
|
163
|
-
].join("\n");
|
|
164
|
-
return { system, prompt };
|
|
165
|
-
}
|
|
166
|
-
export async function planTurn(message, options) {
|
|
167
|
-
const { providerConfigs = DEFAULT_PROVIDER_CONFIGS, modelConfig, conversationHistory = [], brandData, channelType, replyPolicy, knownCandidateFields, } = options;
|
|
168
|
-
const registry = getDynamicRegistry(providerConfigs);
|
|
169
|
-
const classifyModel = (modelConfig?.classifyModel ||
|
|
170
|
-
DEFAULT_MODEL_CONFIG.classifyModel);
|
|
171
|
-
const normalizedChannelType = normalizeChannelType(channelType);
|
|
172
|
-
const activeStages = getActiveStages(normalizedChannelType);
|
|
173
|
-
const dynamicSchema = buildDynamicPlanningSchema(activeStages);
|
|
174
|
-
const prompts = buildPlanningPrompt(message, conversationHistory, brandData, normalizedChannelType, replyPolicy, knownCandidateFields);
|
|
175
|
-
const result = await safeGenerateObject({
|
|
176
|
-
model: registry.languageModel(classifyModel),
|
|
177
|
-
schema: dynamicSchema,
|
|
178
|
-
schemaName: "TurnPlanningOutput",
|
|
179
|
-
system: prompts.system,
|
|
180
|
-
prompt: prompts.prompt,
|
|
181
|
-
});
|
|
182
|
-
const ruleNeeds = detectRuleNeeds(message, conversationHistory);
|
|
183
|
-
const fallbackPrimaryNeed = selectPrimaryNeed(undefined, ruleNeeds, message);
|
|
184
|
-
if (!result.success) {
|
|
185
|
-
return {
|
|
186
|
-
stage: "trust_building",
|
|
187
|
-
subGoals: ["保持对话并澄清需求"],
|
|
188
|
-
needs: Array.from(ruleNeeds),
|
|
189
|
-
primaryNeed: fallbackPrimaryNeed,
|
|
190
|
-
riskFlags: [],
|
|
191
|
-
confidence: 0.35,
|
|
192
|
-
extractedInfo: {
|
|
193
|
-
mentionedBrand: null,
|
|
194
|
-
city: brandData?.city || null,
|
|
195
|
-
mentionedLocations: null,
|
|
196
|
-
mentionedDistricts: null,
|
|
197
|
-
specificAge: null,
|
|
198
|
-
hasUrgency: null,
|
|
199
|
-
preferredSchedule: null,
|
|
200
|
-
},
|
|
201
|
-
reasoningText: "规划模型失败,使用规则降级策略",
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
return sanitizePlan(result.data, ruleNeeds, message);
|
|
205
|
-
}
|
|
206
|
-
//# sourceMappingURL=classification.js.map
|
|
1
|
+
import{z as e}from"zod";import{getDynamicRegistry as n,DEFAULT_MODEL_CONFIG as t,DEFAULT_PROVIDER_CONFIGS as s}from"../ai/model-registry.js";import{safeGenerateObject as i}from"../ai/structured-output.js";import{FunnelStageSchema as o,ChannelTypeSchema as r,TurnPlanSchema as a,STAGE_DEFINITIONS as c}from"../types/reply-policy.js";import{BrandDataSchema as l}from"../types/classification.js";function d(e){const n=r.safeParse(e);return n.success?n.data:"public"}function u(e="public"){const n=d(e),t=o.options.filter(e=>c[e].applicableChannels.includes(n));return t.length>0?t:[...o.options]}function p(n){return e.object({stage:e.enum(n),subGoals:a.shape.subGoals,needs:a.shape.needs,primaryNeed:a.shape.primaryNeed,riskFlags:a.shape.riskFlags,confidence:a.shape.confidence,extractedInfo:a.shape.extractedInfo,reasoningText:a.shape.reasoningText})}const f=[{need:"salary",patterns:[/薪资|工资|时薪|底薪|提成|奖金|补贴|多少钱|收入/i]},{need:"schedule",patterns:[/排班|班次|几点|上班|下班|工时|周末|节假日|做几天/i]},{need:"policy",patterns:[/五险一金|社保|保险|合同|考勤|迟到|补班|试用期/i]},{need:"availability",patterns:[/还有名额|空位|可用时段|什么时候能上|明天能面/i]},{need:"location",patterns:[/在哪|位置|地址|附近|地铁|门店|哪个区|多远/i]},{need:"stores",patterns:[/门店|哪家店|哪些店|有店吗/i]},{need:"requirements",patterns:[/要求|条件|年龄|经验|学历|健康证|身高|体重/i]},{need:"interview",patterns:[/面试|到店|约时间|约面/i]},{need:"wechat",patterns:[/微信|vx|私聊|联系方式|加你/i]}];export const PRIMARY_NEED_PRIORITY=["salary","schedule","location","stores","policy","requirements","availability","interview","wechat","none"];function m(e){const n=new Set;for(const t of f)t.patterns.some(n=>n.test(e))&&n.add(t.need);return 0===n.size?n.add("none"):n.delete("none"),n}export function detectRuleNeeds(e,n){return m(`${n.slice(-4).join(" ")} ${e}`)}function y(e){return m(e)}export function selectContextNeeds(e,n,t,s=1){const i=new Set(n);i.size>1&&i.has("none")&&i.delete("none");const o=y(t);o.delete("none");const r=[];"none"!==e&&i.has(e)&&r.push(e);for(const n of PRIMARY_NEED_PRIORITY){if(r.length>=s)break;"none"!==n&&n!==e&&(o.has(n)&&i.has(n)&&r.push(n))}return r.length>0?r:"none"===e?["none"]:i.has(e)?[e]:["none"]}export function selectPrimaryNeed(e,n,t){const s=new Set(n);if(s.size>1&&s.has("none")&&s.delete("none"),e&&s.has(e))return e;const i=y(t);i.delete("none");for(const e of PRIMARY_NEED_PRIORITY)if(i.has(e)&&s.has(e))return e;for(const e of PRIMARY_NEED_PRIORITY)if(s.has(e))return e;return"none"}export function sanitizePlan(e,n,t){const s=new Set([...e.needs,...Array.from(n)]);return s.size>1&&s.has("none")&&s.delete("none"),{...e,subGoals:e.subGoals.slice(0,2),needs:Array.from(s),primaryNeed:selectPrimaryNeed(e.primaryNeed,s,t),confidence:Number.isFinite(e.confidence)?Math.max(0,Math.min(1,e.confidence)):.5}}function h(e,n,t,s="public",i,o){const r=["你是招聘对话回合规划器,不直接回复候选人。","你只输出结构化规划结果,用于后续回复生成。","规划目标:确定阶段目标(stage)、子目标(subGoals)、事实需求(needs)、主回答轴(primaryNeed)、风险标记(riskFlags)。"].join("\n"),a=d(s);return{system:r,prompt:["[阶段枚举与定义]",...u(a).map(e=>{const n=c[e];return`- ${e}: ${i?.stageGoals[e]?.description||n.description} (转入条件: ${n.transitionSignal})`}),"","[needs枚举]","private"===a?"- stores, location, salary, schedule, policy, availability, requirements, interview, none":"- stores, location, salary, schedule, policy, availability, requirements, interview, wechat, none","","[riskFlags枚举]","- insurance_promise_risk, age_sensitive, confrontation_emotion, urgency_high, qualification_mismatch","","[规则]","- 优先判断本轮主阶段(stage);subGoals 最多 2 项,只保留最关键的。","- 候选人追问事实时,必须打开对应 needs。","- primaryNeed 必须从 needs 中选择一个最主的 need;如果没有明确事实轴则填 none。","- 不确定时 confidence 降低,不要臆断。","- 根据转入条件判断阶段转化,不要停留在不匹配的阶段。",...o&&o.length>0?[`- 候选人资料中已有:${o.join("、")}。不要生成追问这些字段的 subGoal。`]:[],"","[品牌数据]",JSON.stringify(t||{}),"","[历史对话]",n.slice(-8).join("\n")||"无","","[候选人消息]",e].join("\n")}}export async function planTurn(e,o){const{providerConfigs:r=s,modelConfig:a,conversationHistory:c=[],brandData:l,channelType:f,replyPolicy:m,knownCandidateFields:y}=o,g=n(r),b=a?.classifyModel||t.classifyModel,N=d(f),R=p(u(N)),P=h(e,c,l,N,m,y),x=await i({model:g.languageModel(b),schema:R,schemaName:"TurnPlanningOutput",system:P.system,prompt:P.prompt}),I=detectRuleNeeds(e,c),_=selectPrimaryNeed(void 0,I,e);return x.success?sanitizePlan(x.data,I,e):{stage:"trust_building",subGoals:["保持对话并澄清需求"],needs:Array.from(I),primaryNeed:_,riskFlags:[],confidence:.35,extractedInfo:{mentionedBrand:null,city:l?.city||null,mentionedLocations:null,mentionedDistricts:null,specificAge:null,hasUrgency:null,preferredSchedule:null},reasoningText:"规划模型失败,使用规则降级策略"}}
|
|
@@ -1,404 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { PRIMARY_NEED_FACT_MAP } from "../types/reply-policy.js";
|
|
3
|
-
import { verboseLog } from "../log-control.js";
|
|
4
|
-
import { getSharedBrandAliasMap } from "../services/brand-alias.js";
|
|
5
|
-
function buildSalaryDescription(salary) {
|
|
6
|
-
const { base, range, memo } = salary;
|
|
7
|
-
const isPossiblyPieceRate = base < 10;
|
|
8
|
-
let description = "";
|
|
9
|
-
if (isPossiblyPieceRate && memo) {
|
|
10
|
-
description = `${base}元(${memo.replace(/\n/g, " ").trim()})`;
|
|
11
|
-
}
|
|
12
|
-
else {
|
|
13
|
-
description = `${base}元/时`;
|
|
14
|
-
if (range && range !== `${base}-${base}`)
|
|
15
|
-
description += `,范围${range}元`;
|
|
16
|
-
if (memo && memo.length < 50)
|
|
17
|
-
description += `(${memo.replace(/\n/g, " ").trim()})`;
|
|
18
|
-
}
|
|
19
|
-
if (salary.scenarioSummary)
|
|
20
|
-
description += `(${salary.scenarioSummary})`;
|
|
21
|
-
return description;
|
|
22
|
-
}
|
|
23
|
-
export function fuzzyMatchBrand(inputBrand, availableBrands, aliasMap) {
|
|
24
|
-
if (!inputBrand)
|
|
25
|
-
return null;
|
|
26
|
-
const normalizeBrandName = (value) => value.toLowerCase().replace(/[\s._-]+/g, "");
|
|
27
|
-
if (aliasMap) {
|
|
28
|
-
const aliasResult = aliasMap.get(normalizeBrandName(inputBrand)) || aliasMap.get(inputBrand.toLowerCase());
|
|
29
|
-
if (aliasResult && availableBrands.includes(aliasResult))
|
|
30
|
-
return aliasResult;
|
|
31
|
-
}
|
|
32
|
-
const inputLower = inputBrand.toLowerCase();
|
|
33
|
-
const inputNormalized = normalizeBrandName(inputBrand);
|
|
34
|
-
const exactMatch = availableBrands.find((b) => b.toLowerCase() === inputLower);
|
|
35
|
-
if (exactMatch)
|
|
36
|
-
return exactMatch;
|
|
37
|
-
const normalizedMatch = availableBrands.find((b) => normalizeBrandName(b) === inputNormalized);
|
|
38
|
-
if (normalizedMatch)
|
|
39
|
-
return normalizedMatch;
|
|
40
|
-
const containsMatches = availableBrands.filter((brand) => {
|
|
41
|
-
const brandLower = brand.toLowerCase();
|
|
42
|
-
if (brandLower.includes(inputLower) || inputLower.includes(brandLower))
|
|
43
|
-
return true;
|
|
44
|
-
const brandNormalized = normalizeBrandName(brand);
|
|
45
|
-
return brandNormalized.includes(inputNormalized) || inputNormalized.includes(brandNormalized);
|
|
46
|
-
});
|
|
47
|
-
if (containsMatches.length > 0) {
|
|
48
|
-
return containsMatches.sort((a, b) => b.length - a.length)[0] ?? null;
|
|
49
|
-
}
|
|
50
|
-
if (inputLower.includes("山姆") || inputLower.includes("sam")) {
|
|
51
|
-
const samBrand = availableBrands.find((b) => {
|
|
52
|
-
const bl = b.toLowerCase();
|
|
53
|
-
return bl.includes("山姆") || bl.includes("sam");
|
|
54
|
-
});
|
|
55
|
-
if (samBrand)
|
|
56
|
-
return samBrand;
|
|
57
|
-
}
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
export function resolveBrandConflict(input) {
|
|
61
|
-
const { uiSelectedBrand, configDefaultBrand, conversationBrand, availableBrands, strategy = "smart", aliasMap, } = input;
|
|
62
|
-
const tryMatchBrand = (brand, _source) => {
|
|
63
|
-
if (!brand)
|
|
64
|
-
return undefined;
|
|
65
|
-
return fuzzyMatchBrand(brand, availableBrands, aliasMap) ?? undefined;
|
|
66
|
-
};
|
|
67
|
-
switch (strategy) {
|
|
68
|
-
case "user-selected": {
|
|
69
|
-
const uiMatched = tryMatchBrand(uiSelectedBrand, "UI选择");
|
|
70
|
-
if (uiMatched) {
|
|
71
|
-
return {
|
|
72
|
-
resolvedBrand: uiMatched,
|
|
73
|
-
matchType: uiMatched === uiSelectedBrand ? "exact" : "fuzzy",
|
|
74
|
-
source: "ui",
|
|
75
|
-
reason: `用户选择策略`,
|
|
76
|
-
originalInput: uiSelectedBrand,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
const configMatched = tryMatchBrand(configDefaultBrand, "配置默认");
|
|
80
|
-
if (configMatched) {
|
|
81
|
-
return {
|
|
82
|
-
resolvedBrand: configMatched,
|
|
83
|
-
matchType: configMatched === configDefaultBrand ? "exact" : "fuzzy",
|
|
84
|
-
source: "config",
|
|
85
|
-
reason: `配置默认`,
|
|
86
|
-
originalInput: configDefaultBrand,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
return {
|
|
90
|
-
resolvedBrand: availableBrands[0] ?? "",
|
|
91
|
-
matchType: "fallback",
|
|
92
|
-
source: "default",
|
|
93
|
-
reason: `系统默认`,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
case "conversation-extracted": {
|
|
97
|
-
const conversationMatched = tryMatchBrand(conversationBrand, "对话提取");
|
|
98
|
-
if (conversationMatched) {
|
|
99
|
-
return {
|
|
100
|
-
resolvedBrand: conversationMatched,
|
|
101
|
-
matchType: conversationMatched === conversationBrand ? "exact" : "fuzzy",
|
|
102
|
-
source: "conversation",
|
|
103
|
-
reason: `对话提取`,
|
|
104
|
-
originalInput: conversationBrand,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
const uiMatched = tryMatchBrand(uiSelectedBrand, "UI选择");
|
|
108
|
-
if (uiMatched) {
|
|
109
|
-
return {
|
|
110
|
-
resolvedBrand: uiMatched,
|
|
111
|
-
matchType: uiMatched === uiSelectedBrand ? "exact" : "fuzzy",
|
|
112
|
-
source: "ui",
|
|
113
|
-
reason: `UI选择`,
|
|
114
|
-
originalInput: uiSelectedBrand,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
const configMatched = tryMatchBrand(configDefaultBrand, "配置默认");
|
|
118
|
-
if (configMatched) {
|
|
119
|
-
return {
|
|
120
|
-
resolvedBrand: configMatched,
|
|
121
|
-
matchType: configMatched === configDefaultBrand ? "exact" : "fuzzy",
|
|
122
|
-
source: "config",
|
|
123
|
-
reason: `配置默认`,
|
|
124
|
-
originalInput: configDefaultBrand,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
return {
|
|
128
|
-
resolvedBrand: availableBrands[0] ?? "",
|
|
129
|
-
matchType: "fallback",
|
|
130
|
-
source: "default",
|
|
131
|
-
reason: `系统默认`,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
case "smart":
|
|
135
|
-
default: {
|
|
136
|
-
const conversationMatched = tryMatchBrand(conversationBrand, "对话提取");
|
|
137
|
-
const uiMatched = tryMatchBrand(uiSelectedBrand, "UI选择");
|
|
138
|
-
if (conversationMatched) {
|
|
139
|
-
return {
|
|
140
|
-
resolvedBrand: conversationMatched,
|
|
141
|
-
matchType: conversationMatched === conversationBrand ? "exact" : "fuzzy",
|
|
142
|
-
source: "conversation",
|
|
143
|
-
reason: `智能策略: 对话提取`,
|
|
144
|
-
originalInput: conversationBrand,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
if (uiMatched) {
|
|
148
|
-
return {
|
|
149
|
-
resolvedBrand: uiMatched,
|
|
150
|
-
matchType: uiMatched === uiSelectedBrand ? "exact" : "fuzzy",
|
|
151
|
-
source: "ui",
|
|
152
|
-
reason: `智能策略: UI选择`,
|
|
153
|
-
originalInput: uiSelectedBrand,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
const configMatched = tryMatchBrand(configDefaultBrand, "配置默认");
|
|
157
|
-
if (configMatched) {
|
|
158
|
-
return {
|
|
159
|
-
resolvedBrand: configMatched,
|
|
160
|
-
matchType: configMatched === configDefaultBrand ? "exact" : "fuzzy",
|
|
161
|
-
source: "config",
|
|
162
|
-
reason: `智能策略: 配置默认`,
|
|
163
|
-
originalInput: configDefaultBrand,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
return {
|
|
167
|
-
resolvedBrand: availableBrands[0] ?? "",
|
|
168
|
-
matchType: "fallback",
|
|
169
|
-
source: "default",
|
|
170
|
-
reason: `智能策略: 系统默认`,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
function rankStoresByTextMatch(stores, extractedInfo) {
|
|
176
|
-
const { mentionedLocations, mentionedDistricts } = extractedInfo;
|
|
177
|
-
const scoredStores = stores.map((store) => {
|
|
178
|
-
let locationMatch = 0;
|
|
179
|
-
let districtMatch = 0;
|
|
180
|
-
let positionDiversity = 0;
|
|
181
|
-
let availability = 0;
|
|
182
|
-
if (mentionedLocations && mentionedLocations.length > 0) {
|
|
183
|
-
const matchingLocation = mentionedLocations.find((loc) => store.name.includes(loc.location) ||
|
|
184
|
-
store.location.includes(loc.location) ||
|
|
185
|
-
store.subarea.includes(loc.location));
|
|
186
|
-
if (matchingLocation)
|
|
187
|
-
locationMatch = matchingLocation.confidence * 40;
|
|
188
|
-
}
|
|
189
|
-
if (mentionedDistricts && mentionedDistricts.length > 0) {
|
|
190
|
-
const matchingDistrict = mentionedDistricts.find((dist) => store.district.includes(dist.district) || store.subarea.includes(dist.district));
|
|
191
|
-
if (matchingDistrict)
|
|
192
|
-
districtMatch = matchingDistrict.confidence * 30;
|
|
193
|
-
}
|
|
194
|
-
const uniquePositionTypes = new Set(store.positions.map((p) => p.name));
|
|
195
|
-
positionDiversity = Math.min(uniquePositionTypes.size * 5, 20);
|
|
196
|
-
const availablePositions = store.positions.filter((p) => p.availableSlots?.some((slot) => slot.isAvailable));
|
|
197
|
-
availability = Math.min(availablePositions.length * 2, 10);
|
|
198
|
-
return {
|
|
199
|
-
store,
|
|
200
|
-
score: locationMatch + districtMatch + positionDiversity + availability,
|
|
201
|
-
breakdown: { locationMatch, districtMatch, positionDiversity, availability },
|
|
202
|
-
};
|
|
203
|
-
});
|
|
204
|
-
const ranked = scoredStores.sort((a, b) => b.score - a.score);
|
|
205
|
-
return ranked.map((item) => ({ store: item.store, distance: undefined }));
|
|
206
|
-
}
|
|
207
|
-
function getScheduleTypeText(scheduleType) {
|
|
208
|
-
if (!scheduleType)
|
|
209
|
-
return "灵活排班";
|
|
210
|
-
const typeMap = {
|
|
211
|
-
fixed: "固定排班",
|
|
212
|
-
flexible: "灵活排班",
|
|
213
|
-
rotating: "轮班制",
|
|
214
|
-
on_call: "随叫随到",
|
|
215
|
-
};
|
|
216
|
-
return typeMap[scheduleType] || "灵活排班";
|
|
217
|
-
}
|
|
218
|
-
function allowsFactInjection(primaryNeed) {
|
|
219
|
-
return PRIMARY_NEED_FACT_MAP[primaryNeed].length > 0;
|
|
220
|
-
}
|
|
221
|
-
function hasFactFamily(allowedFactFamilies, family) {
|
|
222
|
-
return allowedFactFamilies.has(family);
|
|
223
|
-
}
|
|
224
|
-
function shouldUseMinimalContext(turnIndex, disclosureMode, stage) {
|
|
225
|
-
if (disclosureMode === "minimal")
|
|
226
|
-
return true;
|
|
227
|
-
return turnIndex === 1 || stage === "trust_building" || stage === "private_channel";
|
|
228
|
-
}
|
|
229
|
-
// ========== Main Export ==========
|
|
230
|
-
export async function buildContextInfoByNeeds(data, turnPlan, uiSelectedBrand, toolBrand, brandPriorityStrategy, candidateInfo, replyPolicy, industryVoiceId, turnIndex = 1, disclosureMode = "minimal", factNeeds = [turnPlan.primaryNeed]) {
|
|
231
|
-
const extractedInfo = turnPlan.extractedInfo;
|
|
232
|
-
const primaryNeed = turnPlan.primaryNeed;
|
|
233
|
-
const effectiveFactNeeds = factNeeds.length > 0
|
|
234
|
-
? Array.from(new Set(factNeeds.filter((need) => need !== "none")))
|
|
235
|
-
: primaryNeed === "none"
|
|
236
|
-
? []
|
|
237
|
-
: [primaryNeed];
|
|
238
|
-
const allowedFactFamilies = new Set(effectiveFactNeeds.flatMap((need) => PRIMARY_NEED_FACT_MAP[need]));
|
|
239
|
-
const useMinimalContext = shouldUseMinimalContext(turnIndex, disclosureMode, turnPlan.stage);
|
|
240
|
-
const requiresFacts = !useMinimalContext && allowsFactInjection(primaryNeed);
|
|
241
|
-
let aliasMap;
|
|
242
|
-
let aliasLookupError;
|
|
243
|
-
try {
|
|
244
|
-
aliasMap = await getSharedBrandAliasMap();
|
|
245
|
-
}
|
|
246
|
-
catch (error) {
|
|
247
|
-
const errorMessage = typeof error === "object" &&
|
|
248
|
-
error !== null &&
|
|
249
|
-
"userMessage" in error &&
|
|
250
|
-
typeof error.userMessage === "string"
|
|
251
|
-
? error.userMessage
|
|
252
|
-
: error instanceof Error
|
|
253
|
-
? error.message
|
|
254
|
-
: String(error);
|
|
255
|
-
aliasLookupError = errorMessage;
|
|
256
|
-
verboseLog(`[buildContextInfoByNeeds] 品牌别名服务不可用,回退 fuzzy 解析: ${errorMessage}`);
|
|
257
|
-
}
|
|
258
|
-
const brandResolution = resolveBrandConflict({
|
|
259
|
-
uiSelectedBrand,
|
|
260
|
-
configDefaultBrand: resolveDefaultBrandName(data),
|
|
261
|
-
conversationBrand: toolBrand || undefined,
|
|
262
|
-
availableBrands: getAvailableBrandNames(data),
|
|
263
|
-
strategy: brandPriorityStrategy || "smart",
|
|
264
|
-
aliasMap,
|
|
265
|
-
});
|
|
266
|
-
const targetBrand = brandResolution.resolvedBrand;
|
|
267
|
-
verboseLog(`[品牌解析] 工具传参: ${toolBrand ?? "(未指定)"} → 结果: ${targetBrand} (${brandResolution.matchType}, ${brandResolution.source})`);
|
|
268
|
-
const targetBrandData = findBrandByName(data, targetBrand);
|
|
269
|
-
const brandStores = targetBrandData?.stores ?? [];
|
|
270
|
-
let relevantStores = brandStores;
|
|
271
|
-
if (relevantStores.length > 0) {
|
|
272
|
-
const locations = extractedInfo.mentionedLocations || [];
|
|
273
|
-
if (locations.length > 0) {
|
|
274
|
-
const location = locations[0]?.location?.trim();
|
|
275
|
-
if (location) {
|
|
276
|
-
const filtered = relevantStores.filter((s) => s.name.includes(location) ||
|
|
277
|
-
s.location.includes(location) ||
|
|
278
|
-
s.district.includes(location) ||
|
|
279
|
-
s.subarea.includes(location));
|
|
280
|
-
if (filtered.length > 0)
|
|
281
|
-
relevantStores = filtered;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
const districts = extractedInfo.mentionedDistricts || [];
|
|
285
|
-
if (districts.length > 0) {
|
|
286
|
-
const filtered = relevantStores.filter((s) => districts.some((d) => s.district.includes(d.district) || s.subarea.includes(d.district)));
|
|
287
|
-
if (filtered.length > 0)
|
|
288
|
-
relevantStores = filtered;
|
|
289
|
-
}
|
|
290
|
-
if (relevantStores.length === brandStores.length &&
|
|
291
|
-
candidateInfo?.jobAddress &&
|
|
292
|
-
hasFactFamily(allowedFactFamilies, "location")) {
|
|
293
|
-
const filtered = relevantStores.filter((s) => s.name.includes(candidateInfo.jobAddress || "") ||
|
|
294
|
-
s.location.includes(candidateInfo.jobAddress || "") ||
|
|
295
|
-
s.district.includes(candidateInfo.jobAddress || "") ||
|
|
296
|
-
s.subarea.includes(candidateInfo.jobAddress || ""));
|
|
297
|
-
if (filtered.length > 0)
|
|
298
|
-
relevantStores = filtered;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
let rankedStoresWithDistance = [];
|
|
302
|
-
if (relevantStores.length > 0) {
|
|
303
|
-
rankedStoresWithDistance = rankStoresByTextMatch(relevantStores, extractedInfo);
|
|
304
|
-
}
|
|
305
|
-
const storeCount = requiresFacts ? Math.min(1, rankedStoresWithDistance.length) : 0;
|
|
306
|
-
const detailLevel = requiresFacts ? "focused" : "minimal";
|
|
307
|
-
let context = `阶段目标:${turnPlan.stage}\n默认推荐品牌:${targetBrand}\n`;
|
|
308
|
-
if (aliasLookupError) {
|
|
309
|
-
context += `系统状态:品牌别名服务暂不可用,已回退为规则匹配(${aliasLookupError})。\n`;
|
|
310
|
-
}
|
|
311
|
-
if (replyPolicy) {
|
|
312
|
-
const stageGoal = replyPolicy.stageGoals[turnPlan.stage];
|
|
313
|
-
const voiceId = industryVoiceId || replyPolicy.defaultIndustryVoiceId;
|
|
314
|
-
const voice = replyPolicy.industryVoices[voiceId];
|
|
315
|
-
context += `策略目标:${stageGoal.primaryGoal}\n`;
|
|
316
|
-
context += `推进方式:${stageGoal.ctaStrategy}\n`;
|
|
317
|
-
context += `主回答轴:${primaryNeed}\n`;
|
|
318
|
-
if (voice)
|
|
319
|
-
context += `行业指纹:${voice.name} | 风格:${voice.styleKeywords.join("、")}\n`;
|
|
320
|
-
context += `红线:${replyPolicy.hardConstraints.rules.map((r) => r.rule).join(";")}\n`;
|
|
321
|
-
}
|
|
322
|
-
if (!requiresFacts) {
|
|
323
|
-
if (useMinimalContext) {
|
|
324
|
-
context += "当前处于首轮或浅层沟通,优先泛化回答,不主动展开具体门店、数字或筛选条件。\n";
|
|
325
|
-
}
|
|
326
|
-
else {
|
|
327
|
-
context += "本轮以推进沟通为主,无需展开岗位细节,请保持回答聚焦且克制。\n";
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
else if (storeCount === 0) {
|
|
331
|
-
context += "暂无可用的门店事实信息,请使用泛化回答,避免任何具体承诺。\n";
|
|
332
|
-
}
|
|
333
|
-
else {
|
|
334
|
-
context += "匹配到的门店信息:\n";
|
|
335
|
-
rankedStoresWithDistance.slice(0, storeCount).forEach(({ store }) => {
|
|
336
|
-
const includeLocationFacts = hasFactFamily(allowedFactFamilies, "location");
|
|
337
|
-
const includePositionFacts = Array.from(allowedFactFamilies).some((family) => family !== "location");
|
|
338
|
-
context += includeLocationFacts
|
|
339
|
-
? `• ${store.name}(${store.district}${store.subarea}):${store.location}\n`
|
|
340
|
-
: `• ${store.name}\n`;
|
|
341
|
-
if (!includePositionFacts) {
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
store.positions.slice(0, 3).forEach((position) => {
|
|
345
|
-
context += ` 职位:${position.name}\n`;
|
|
346
|
-
if (hasFactFamily(allowedFactFamilies, "salary")) {
|
|
347
|
-
context += ` 薪资:${buildSalaryDescription(position.salary)}\n`;
|
|
348
|
-
}
|
|
349
|
-
if (hasFactFamily(allowedFactFamilies, "schedule")) {
|
|
350
|
-
context += ` 排班:${getScheduleTypeText(position.scheduleType)}\n`;
|
|
351
|
-
context += ` 时间:${position.timeSlots.slice(0, 3).join("、")}\n`;
|
|
352
|
-
if (position.minHoursPerWeek || position.maxHoursPerWeek) {
|
|
353
|
-
context += ` 每周工时:${position.minHoursPerWeek || 0}-${position.maxHoursPerWeek || "不限"}小时\n`;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
if (hasFactFamily(allowedFactFamilies, "policy")) {
|
|
357
|
-
context += ` 考勤:最多迟到${position.attendancePolicy.lateToleranceMinutes}分钟\n`;
|
|
358
|
-
if (position.attendanceRequirement?.description) {
|
|
359
|
-
context += ` 出勤要求:${position.attendanceRequirement.description}\n`;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
if (hasFactFamily(allowedFactFamilies, "availability")) {
|
|
363
|
-
const slots = position.availableSlots?.filter((s) => s.isAvailable).slice(0, 3) || [];
|
|
364
|
-
if (slots.length > 0)
|
|
365
|
-
context += ` 可用时段:${slots.map((s) => s.slot).join("、")}\n`;
|
|
366
|
-
}
|
|
367
|
-
if (hasFactFamily(allowedFactFamilies, "requirements")) {
|
|
368
|
-
if (position.hiringRequirements) {
|
|
369
|
-
const hr = position.hiringRequirements;
|
|
370
|
-
const parts = [];
|
|
371
|
-
if (hr.minAge != null || hr.maxAge != null) {
|
|
372
|
-
parts.push(`年龄${hr.minAge ?? "不限"}-${hr.maxAge ?? "不限"}岁`);
|
|
373
|
-
}
|
|
374
|
-
if (hr.genderRequirement && hr.genderRequirement !== "0") {
|
|
375
|
-
parts.push(`性别:${hr.genderRequirement}`);
|
|
376
|
-
}
|
|
377
|
-
if (hr.education && hr.education !== "1")
|
|
378
|
-
parts.push(`学历:${hr.education}`);
|
|
379
|
-
if (parts.length > 0)
|
|
380
|
-
context += ` 要求:${parts.join("、")}\n`;
|
|
381
|
-
}
|
|
382
|
-
else if (position.requirements?.length) {
|
|
383
|
-
context += ` 要求:${position.requirements.filter((r) => r !== "无").join("、")}\n`;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
});
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
return {
|
|
390
|
-
contextInfo: context,
|
|
391
|
-
resolvedBrand: targetBrand,
|
|
392
|
-
debugInfo: {
|
|
393
|
-
relevantStores: rankedStoresWithDistance.length > 0
|
|
394
|
-
? rankedStoresWithDistance
|
|
395
|
-
: relevantStores.map((s) => ({ store: s, distance: undefined })),
|
|
396
|
-
storeCount,
|
|
397
|
-
detailLevel,
|
|
398
|
-
primaryNeed,
|
|
399
|
-
turnPlan,
|
|
400
|
-
aliasLookupError,
|
|
401
|
-
},
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
//# sourceMappingURL=context-builder.js.map
|
|
1
|
+
import{findBrandByName as e,getAvailableBrandNames as n,resolveDefaultBrandName as t}from"../services/brand-config-selectors.js";import{PRIMARY_NEED_FACT_MAP as r}from"../types/reply-policy.js";import{verboseLog as i}from"../log-control.js";import{getSharedBrandAliasMap as s}from"../services/brand-alias.js";function o(e){const{base:n,range:t,memo:r}=e;let i="";return n<10&&r?i=`${n}元(${r.replace(/\n/g," ").trim()})`:(i=`${n}元/时`,t&&t!==`${n}-${n}`&&(i+=`,范围${t}元`),r&&r.length<50&&(i+=`(${r.replace(/\n/g," ").trim()})`)),e.scenarioSummary&&(i+=`(${e.scenarioSummary})`),i}export function fuzzyMatchBrand(e,n,t){if(!e)return null;const r=e=>e.toLowerCase().replace(/[\s._-]+/g,"");if(t){const i=t.get(r(e))||t.get(e.toLowerCase());if(i&&n.includes(i))return i}const i=e.toLowerCase(),s=r(e),o=n.find(e=>e.toLowerCase()===i);if(o)return o;const a=n.find(e=>r(e)===s);if(a)return a;const c=n.filter(e=>{const n=e.toLowerCase();if(n.includes(i)||i.includes(n))return!0;const t=r(e);return t.includes(s)||s.includes(t)});if(c.length>0)return c.sort((e,n)=>n.length-e.length)[0]??null;if(i.includes("山姆")||i.includes("sam")){const e=n.find(e=>{const n=e.toLowerCase();return n.includes("山姆")||n.includes("sam")});if(e)return e}return null}export function resolveBrandConflict(e){const{uiSelectedBrand:n,configDefaultBrand:t,conversationBrand:r,availableBrands:i,strategy:s="smart",aliasMap:o}=e,a=(e,n)=>{if(e)return fuzzyMatchBrand(e,i,o)??void 0};switch(s){case"user-selected":{const e=a(n);if(e)return{resolvedBrand:e,matchType:e===n?"exact":"fuzzy",source:"ui",reason:"用户选择策略",originalInput:n};const r=a(t);return r?{resolvedBrand:r,matchType:r===t?"exact":"fuzzy",source:"config",reason:"配置默认",originalInput:t}:{resolvedBrand:i[0]??"",matchType:"fallback",source:"default",reason:"系统默认"}}case"conversation-extracted":{const e=a(r);if(e)return{resolvedBrand:e,matchType:e===r?"exact":"fuzzy",source:"conversation",reason:"对话提取",originalInput:r};const s=a(n);if(s)return{resolvedBrand:s,matchType:s===n?"exact":"fuzzy",source:"ui",reason:"UI选择",originalInput:n};const o=a(t);return o?{resolvedBrand:o,matchType:o===t?"exact":"fuzzy",source:"config",reason:"配置默认",originalInput:t}:{resolvedBrand:i[0]??"",matchType:"fallback",source:"default",reason:"系统默认"}}default:{const e=a(r),s=a(n);if(e)return{resolvedBrand:e,matchType:e===r?"exact":"fuzzy",source:"conversation",reason:"智能策略: 对话提取",originalInput:r};if(s)return{resolvedBrand:s,matchType:s===n?"exact":"fuzzy",source:"ui",reason:"智能策略: UI选择",originalInput:n};const o=a(t);return o?{resolvedBrand:o,matchType:o===t?"exact":"fuzzy",source:"config",reason:"智能策略: 配置默认",originalInput:t}:{resolvedBrand:i[0]??"",matchType:"fallback",source:"default",reason:"智能策略: 系统默认"}}}}function a(e,n){const{mentionedLocations:t,mentionedDistricts:r}=n;return e.map(e=>{let n=0,i=0,s=0,o=0;if(t&&t.length>0){const r=t.find(n=>e.name.includes(n.location)||e.location.includes(n.location)||e.subarea.includes(n.location));r&&(n=40*r.confidence)}if(r&&r.length>0){const n=r.find(n=>e.district.includes(n.district)||e.subarea.includes(n.district));n&&(i=30*n.confidence)}const a=new Set(e.positions.map(e=>e.name));s=Math.min(5*a.size,20);const c=e.positions.filter(e=>e.availableSlots?.some(e=>e.isAvailable));return o=Math.min(2*c.length,10),{store:e,score:n+i+s+o,breakdown:{locationMatch:n,districtMatch:i,positionDiversity:s,availability:o}}}).sort((e,n)=>n.score-e.score).map(e=>({store:e.store,distance:void 0}))}function c(e){if(!e)return"灵活排班";return{fixed:"固定排班",flexible:"灵活排班",rotating:"轮班制",on_call:"随叫随到"}[e]||"灵活排班"}function l(e){return r[e].length>0}function u(e,n){return e.has(n)}function d(e,n,t){return"minimal"===n||(1===e||"trust_building"===t||"private_channel"===t)}export async function buildContextInfoByNeeds(f,m,g,p,h,y,$,v,b=1,B="minimal",z=[m.primaryNeed]){const x=m.extractedInfo,I=m.primaryNeed,j=z.length>0?Array.from(new Set(z.filter(e=>"none"!==e))):"none"===I?[]:[I],M=new Set(j.flatMap(e=>r[e])),T=d(b,B,m.stage),w=!T&&l(I);let A,S;try{A=await s()}catch(e){const n="object"==typeof e&&null!==e&&"userMessage"in e&&"string"==typeof e.userMessage?e.userMessage:e instanceof Error?e.message:String(e);S=n,i(`[buildContextInfoByNeeds] 品牌别名服务不可用,回退 fuzzy 解析: ${n}`)}const C=resolveBrandConflict({uiSelectedBrand:g,configDefaultBrand:t(f),conversationBrand:p||void 0,availableBrands:n(f),strategy:h||"smart",aliasMap:A}),q=C.resolvedBrand;i(`[品牌解析] 工具传参: ${p??"(未指定)"} → 结果: ${q} (${C.matchType}, ${C.source})`);const L=e(f,q),k=L?.stores??[];let R=k;if(R.length>0){const e=x.mentionedLocations||[];if(e.length>0){const n=e[0]?.location?.trim();if(n){const e=R.filter(e=>e.name.includes(n)||e.location.includes(n)||e.district.includes(n)||e.subarea.includes(n));e.length>0&&(R=e)}}const n=x.mentionedDistricts||[];if(n.length>0){const e=R.filter(e=>n.some(n=>e.district.includes(n.district)||e.subarea.includes(n.district)));e.length>0&&(R=e)}if(R.length===k.length&&y?.jobAddress&&u(M,"location")){const e=R.filter(e=>e.name.includes(y.jobAddress||"")||e.location.includes(y.jobAddress||"")||e.district.includes(y.jobAddress||"")||e.subarea.includes(y.jobAddress||""));e.length>0&&(R=e)}}let P=[];R.length>0&&(P=a(R,x));const D=w?Math.min(1,P.length):0,N=w?"focused":"minimal";let E=`阶段目标:${m.stage}\n默认推荐品牌:${q}\n`;if(S&&(E+=`系统状态:品牌别名服务暂不可用,已回退为规则匹配(${S})。\n`),$){const e=$.stageGoals[m.stage],n=v||$.defaultIndustryVoiceId,t=$.industryVoices[n];E+=`策略目标:${e.primaryGoal}\n`,E+=`推进方式:${e.ctaStrategy}\n`,E+=`主回答轴:${I}\n`,t&&(E+=`行业指纹:${t.name} | 风格:${t.styleKeywords.join("、")}\n`),E+=`红线:${$.hardConstraints.rules.map(e=>e.rule).join(";")}\n`}return w?0===D?E+="暂无可用的门店事实信息,请使用泛化回答,避免任何具体承诺。\n":(E+="匹配到的门店信息:\n",P.slice(0,D).forEach(({store:e})=>{const n=u(M,"location"),t=Array.from(M).some(e=>"location"!==e);E+=n?`• ${e.name}(${e.district}${e.subarea}):${e.location}\n`:`• ${e.name}\n`,t&&e.positions.slice(0,3).forEach(e=>{if(E+=` 职位:${e.name}\n`,u(M,"salary")&&(E+=` 薪资:${o(e.salary)}\n`),u(M,"schedule")&&(E+=` 排班:${c(e.scheduleType)}\n`,E+=` 时间:${e.timeSlots.slice(0,3).join("、")}\n`,(e.minHoursPerWeek||e.maxHoursPerWeek)&&(E+=` 每周工时:${e.minHoursPerWeek||0}-${e.maxHoursPerWeek||"不限"}小时\n`)),u(M,"policy")&&(E+=` 考勤:最多迟到${e.attendancePolicy.lateToleranceMinutes}分钟\n`,e.attendanceRequirement?.description&&(E+=` 出勤要求:${e.attendanceRequirement.description}\n`)),u(M,"availability")){const n=e.availableSlots?.filter(e=>e.isAvailable).slice(0,3)||[];n.length>0&&(E+=` 可用时段:${n.map(e=>e.slot).join("、")}\n`)}if(u(M,"requirements"))if(e.hiringRequirements){const n=e.hiringRequirements,t=[];null==n.minAge&&null==n.maxAge||t.push(`年龄${n.minAge??"不限"}-${n.maxAge??"不限"}岁`),n.genderRequirement&&"0"!==n.genderRequirement&&t.push(`性别:${n.genderRequirement}`),n.education&&"1"!==n.education&&t.push(`学历:${n.education}`),t.length>0&&(E+=` 要求:${t.join("、")}\n`)}else e.requirements?.length&&(E+=` 要求:${e.requirements.filter(e=>"无"!==e).join("、")}\n`)})})):E+=T?"当前处于首轮或浅层沟通,优先泛化回答,不主动展开具体门店、数字或筛选条件。\n":"本轮以推进沟通为主,无需展开岗位细节,请保持回答聚焦且克制。\n",{contextInfo:E,resolvedBrand:q,debugInfo:{relevantStores:P.length>0?P:R.map(e=>({store:e,distance:void 0})),storeCount:D,detailLevel:N,primaryNeed:I,turnPlan:m,aliasLookupError:S}}}
|