打通 Boss 直聘落档挂载与真值→投影映射,并修正带 tools 时的推理强度。
落档自动把产物挂进 context_order;创作期用 declare_variable/declare_map 钉目录与分档;真值变更持续重投影。顺带强制 tools 请求 reasoning_effort=none,并支持消息 restart。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* 真值 → 投影 tag:按当前值查档,改投影 tag 内容(槽位固定)。
|
||||
* schema: value-map.v1 · 存黑板「设计.变量映射」
|
||||
*/
|
||||
import type { Blackboard } from "../blackboard/blackboard.js";
|
||||
import { parseTableDoc, type TableDoc } from "../blackboard/table-cells.js";
|
||||
import { tableValueMap } from "../blackboard/table-side-effects.js";
|
||||
import { extractJsonObjectText } from "./worker-set-parse.js";
|
||||
|
||||
export const VALUE_MAP_SCHEMA = "value-map.v1" as const;
|
||||
export const VALUE_MAP_TAG = "设计.变量映射";
|
||||
const VARS_TAG = "变量.当前";
|
||||
|
||||
export type ValueMapBand =
|
||||
| { when: "range"; min?: number; max?: number; content: string }
|
||||
| { when: "eq"; value: unknown; content: string }
|
||||
| { when: "default"; content: string };
|
||||
|
||||
export type ValueMapEntry = {
|
||||
id: string;
|
||||
/** 真值字段名(变量.当前 的 key) */
|
||||
field: string;
|
||||
/** 投影 tag(固定槽;内容随真值替换) */
|
||||
target_tag: string;
|
||||
bands: ValueMapBand[];
|
||||
note?: string;
|
||||
};
|
||||
|
||||
export type ValueMapDoc = {
|
||||
schema: typeof VALUE_MAP_SCHEMA;
|
||||
maps: ValueMapEntry[];
|
||||
};
|
||||
|
||||
function asString(v: unknown): string | undefined {
|
||||
if (typeof v !== "string") return undefined;
|
||||
const t = v.trim();
|
||||
return t || undefined;
|
||||
}
|
||||
|
||||
function toNumber(v: unknown): number | null {
|
||||
if (typeof v === "number" && Number.isFinite(v)) return v;
|
||||
if (typeof v === "string" && v.trim() && !Number.isNaN(Number(v))) {
|
||||
return Number(v);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function sameValue(a: unknown, b: unknown): boolean {
|
||||
if (a === b) return true;
|
||||
if (a == null || b == null) return a == null && b == null;
|
||||
const na = toNumber(a);
|
||||
const nb = toNumber(b);
|
||||
if (na != null && nb != null) return na === nb;
|
||||
return String(a) === String(b);
|
||||
}
|
||||
|
||||
function parseBand(raw: unknown): ValueMapBand | null {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
||||
const row = raw as Record<string, unknown>;
|
||||
const content = asString(row.content) ?? asString(row.正文) ?? asString(row.text);
|
||||
if (!content) return null;
|
||||
|
||||
const when = asString(row.when) ?? asString(row.op);
|
||||
if (when === "default" || when === "默认" || row.default === true) {
|
||||
return { when: "default", content };
|
||||
}
|
||||
if (when === "eq" || when === "等于") {
|
||||
return {
|
||||
when: "eq",
|
||||
value: row.value ?? row.eq ?? row.值,
|
||||
content,
|
||||
};
|
||||
}
|
||||
|
||||
const min =
|
||||
typeof row.min === "number"
|
||||
? row.min
|
||||
: typeof row.gte === "number"
|
||||
? row.gte
|
||||
: toNumber(row.min);
|
||||
const max =
|
||||
typeof row.max === "number"
|
||||
? row.max
|
||||
: typeof row.lt === "number"
|
||||
? row.lt
|
||||
: toNumber(row.max);
|
||||
|
||||
if (min != null || max != null || when === "range" || when === "区间") {
|
||||
return {
|
||||
when: "range",
|
||||
min: min ?? undefined,
|
||||
max: max ?? undefined,
|
||||
content,
|
||||
};
|
||||
}
|
||||
|
||||
// 仅给了 value → 当精确匹配
|
||||
if (row.value !== undefined || row.eq !== undefined || row.值 !== undefined) {
|
||||
return {
|
||||
when: "eq",
|
||||
value: row.value ?? row.eq ?? row.值,
|
||||
content,
|
||||
};
|
||||
}
|
||||
return { when: "default", content };
|
||||
}
|
||||
|
||||
export function parseValueMapDoc(raw: string | undefined | null): ValueMapDoc | null {
|
||||
if (!raw?.trim()) return null;
|
||||
const text = extractJsonObjectText(raw) ?? raw.trim();
|
||||
try {
|
||||
const parsed = JSON.parse(text) as unknown;
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
||||
const row = parsed as Record<string, unknown>;
|
||||
const mapsRaw = row.maps ?? row.映射 ?? row.Data映射;
|
||||
if (!Array.isArray(mapsRaw)) return null;
|
||||
const maps: ValueMapEntry[] = [];
|
||||
for (const item of mapsRaw) {
|
||||
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
||||
const m = item as Record<string, unknown>;
|
||||
const id = asString(m.id) ?? asString(m.映射id) ?? "";
|
||||
const field = asString(m.field) ?? asString(m.键依真值) ?? asString(m.key) ?? "";
|
||||
const target =
|
||||
asString(m.target_tag) ??
|
||||
asString(m.targetTag) ??
|
||||
asString(m.投影tag) ??
|
||||
asString(m.tag) ??
|
||||
"";
|
||||
if (!id || !field || !target) continue;
|
||||
const bandsRaw = m.bands ?? m.档 ?? m.分档;
|
||||
if (!Array.isArray(bandsRaw)) continue;
|
||||
const bands = bandsRaw.map(parseBand).filter((b): b is ValueMapBand => Boolean(b));
|
||||
if (!bands.length) continue;
|
||||
maps.push({
|
||||
id,
|
||||
field,
|
||||
target_tag: target,
|
||||
bands,
|
||||
note: asString(m.note) ?? asString(m.说明),
|
||||
});
|
||||
}
|
||||
if (!maps.length) return null;
|
||||
return { schema: VALUE_MAP_SCHEMA, maps };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeValueMapDoc(doc: ValueMapDoc): string {
|
||||
return JSON.stringify(
|
||||
{
|
||||
schema: VALUE_MAP_SCHEMA,
|
||||
maps: doc.maps.map((m) => ({
|
||||
id: m.id,
|
||||
field: m.field,
|
||||
target_tag: m.target_tag,
|
||||
bands: m.bands,
|
||||
...(m.note ? { note: m.note } : {}),
|
||||
})),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
export function isValidProjectionTag(tag: string): boolean {
|
||||
const t = tag.trim();
|
||||
if (!t || t.includes("\n")) return false;
|
||||
return t.startsWith("上下文.") || t.startsWith("大纲.");
|
||||
}
|
||||
|
||||
export type UpsertMapInput = {
|
||||
id: string;
|
||||
field: string;
|
||||
target_tag: string;
|
||||
bands: unknown;
|
||||
note?: string;
|
||||
};
|
||||
|
||||
export function upsertValueMapEntry(
|
||||
current: ValueMapDoc | null,
|
||||
input: UpsertMapInput,
|
||||
): { doc: ValueMapDoc; error?: string } {
|
||||
const id = input.id.trim();
|
||||
const field = input.field.trim();
|
||||
const target = input.target_tag.trim();
|
||||
if (!id) {
|
||||
return { doc: current ?? { schema: VALUE_MAP_SCHEMA, maps: [] }, error: "id 不能为空" };
|
||||
}
|
||||
if (!field) {
|
||||
return { doc: current ?? { schema: VALUE_MAP_SCHEMA, maps: [] }, error: "field 不能为空" };
|
||||
}
|
||||
if (!isValidProjectionTag(target)) {
|
||||
return {
|
||||
doc: current ?? { schema: VALUE_MAP_SCHEMA, maps: [] },
|
||||
error: "target_tag 须以「上下文.」或「大纲.」开头",
|
||||
};
|
||||
}
|
||||
let bandsRaw = input.bands;
|
||||
if (typeof bandsRaw === "string") {
|
||||
try {
|
||||
bandsRaw = JSON.parse(bandsRaw);
|
||||
} catch {
|
||||
return {
|
||||
doc: current ?? { schema: VALUE_MAP_SCHEMA, maps: [] },
|
||||
error: "bands 须为 JSON 数组",
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(bandsRaw) || !bandsRaw.length) {
|
||||
return {
|
||||
doc: current ?? { schema: VALUE_MAP_SCHEMA, maps: [] },
|
||||
error: "bands 至少一档",
|
||||
};
|
||||
}
|
||||
const bands = bandsRaw.map(parseBand).filter((b): b is ValueMapBand => Boolean(b));
|
||||
if (!bands.length) {
|
||||
return {
|
||||
doc: current ?? { schema: VALUE_MAP_SCHEMA, maps: [] },
|
||||
error: "bands 无法解析(需 content,以及 min/max 或 value 或 default)",
|
||||
};
|
||||
}
|
||||
const entry: ValueMapEntry = {
|
||||
id,
|
||||
field,
|
||||
target_tag: target,
|
||||
bands,
|
||||
note: input.note?.trim() || undefined,
|
||||
};
|
||||
const maps = [...(current?.maps ?? [])];
|
||||
const idx = maps.findIndex((m) => m.id === id);
|
||||
if (idx >= 0) maps[idx] = entry;
|
||||
else maps.push(entry);
|
||||
return { doc: { schema: VALUE_MAP_SCHEMA, maps } };
|
||||
}
|
||||
|
||||
/** 按真值查一档正文;无命中返回 null */
|
||||
export function lookupValueMapContent(
|
||||
entry: ValueMapEntry,
|
||||
fieldValue: unknown,
|
||||
): string | null {
|
||||
let defaultContent: string | null = null;
|
||||
for (const band of entry.bands) {
|
||||
if (band.when === "default") {
|
||||
defaultContent = band.content;
|
||||
continue;
|
||||
}
|
||||
if (band.when === "eq") {
|
||||
if (sameValue(fieldValue, band.value)) return band.content;
|
||||
continue;
|
||||
}
|
||||
const n = toNumber(fieldValue);
|
||||
if (n == null) continue;
|
||||
const minOk = band.min == null || n >= band.min;
|
||||
const maxOk = band.max == null || n < band.max;
|
||||
if (minOk && maxOk) return band.content;
|
||||
}
|
||||
return defaultContent;
|
||||
}
|
||||
|
||||
export type ReprojectResult = {
|
||||
written: Array<{ mapId: string; tag: string; field: string; value: unknown }>;
|
||||
skipped: Array<{ mapId: string; reason: string }>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 按 变量.当前 重写所有映射投影 tag(持续投影,非 once)。
|
||||
*/
|
||||
export function reprojectValueMaps(params: {
|
||||
blackboard: Blackboard;
|
||||
mapsRaw?: string | null;
|
||||
currentRaw?: string | null;
|
||||
source?: string;
|
||||
}): ReprojectResult {
|
||||
const maps =
|
||||
parseValueMapDoc(
|
||||
params.mapsRaw ?? params.blackboard.getContentByTag(VALUE_MAP_TAG),
|
||||
)?.maps ?? [];
|
||||
const written: ReprojectResult["written"] = [];
|
||||
const skipped: ReprojectResult["skipped"] = [];
|
||||
if (!maps.length) return { written, skipped };
|
||||
|
||||
const currentDoc =
|
||||
parseTableDoc(
|
||||
params.currentRaw ?? params.blackboard.getContentByTag(VARS_TAG),
|
||||
) ?? ({ rows: [] } as TableDoc);
|
||||
const values = tableValueMap(currentDoc);
|
||||
const source = params.source ?? "system:value-map";
|
||||
|
||||
for (const entry of maps) {
|
||||
if (!values.has(entry.field)) {
|
||||
skipped.push({ mapId: entry.id, reason: `缺真值字段 ${entry.field}` });
|
||||
continue;
|
||||
}
|
||||
const value = values.get(entry.field);
|
||||
const content = lookupValueMapContent(entry, value);
|
||||
if (content == null) {
|
||||
skipped.push({ mapId: entry.id, reason: "无命中档" });
|
||||
continue;
|
||||
}
|
||||
params.blackboard.write({
|
||||
tag: entry.target_tag,
|
||||
content,
|
||||
source,
|
||||
});
|
||||
written.push({
|
||||
mapId: entry.id,
|
||||
tag: entry.target_tag,
|
||||
field: entry.field,
|
||||
value,
|
||||
});
|
||||
}
|
||||
return { written, skipped };
|
||||
}
|
||||
|
||||
/** 映射表里声明的全部投影 tag(供 context_order 挂固定槽) */
|
||||
export function listValueMapTargetTags(raw: string | undefined | null): string[] {
|
||||
const doc = parseValueMapDoc(raw);
|
||||
if (!doc) return [];
|
||||
return [...new Set(doc.maps.map((m) => m.target_tag))];
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* 变量目录(创作期对话钉死;落档时种进 变量.当前)。
|
||||
* schema: variable-catalog.v1
|
||||
*/
|
||||
import {
|
||||
createTableFromValues,
|
||||
parseTableDoc,
|
||||
stringifyTableDoc,
|
||||
type TableDoc,
|
||||
} from "../blackboard/table-cells.js";
|
||||
import { extractJsonObjectText } from "./worker-set-parse.js";
|
||||
|
||||
export const VARIABLE_CATALOG_SCHEMA = "variable-catalog.v1" as const;
|
||||
export const VARIABLE_CATALOG_TAG = "设计.变量目录";
|
||||
export const CURRENT_VARS_TAG = "变量.当前";
|
||||
|
||||
export type VariableFieldType = "number" | "string" | "boolean" | "enum";
|
||||
|
||||
export type VariableCatalogField = {
|
||||
key: string;
|
||||
type: VariableFieldType;
|
||||
initial: unknown;
|
||||
/** 对用户可见(监控栏等);默认 true */
|
||||
user_visible: boolean;
|
||||
note?: string;
|
||||
};
|
||||
|
||||
export type VariableCatalogDoc = {
|
||||
schema: typeof VARIABLE_CATALOG_SCHEMA;
|
||||
fields: VariableCatalogField[];
|
||||
};
|
||||
|
||||
function asString(v: unknown): string | undefined {
|
||||
if (typeof v !== "string") return undefined;
|
||||
const t = v.trim();
|
||||
return t || undefined;
|
||||
}
|
||||
|
||||
function parseType(raw: unknown): VariableFieldType {
|
||||
const s = asString(raw)?.toLowerCase();
|
||||
if (s === "number" || s === "数字") return "number";
|
||||
if (s === "boolean" || s === "布尔") return "boolean";
|
||||
if (s === "enum" || s === "枚举") return "enum";
|
||||
return "string";
|
||||
}
|
||||
|
||||
function coerceInitial(type: VariableFieldType, raw: unknown): unknown {
|
||||
if (type === "number") {
|
||||
if (typeof raw === "number" && Number.isFinite(raw)) return raw;
|
||||
if (typeof raw === "string" && raw.trim() && !Number.isNaN(Number(raw))) {
|
||||
return Number(raw);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (type === "boolean") {
|
||||
if (typeof raw === "boolean") return raw;
|
||||
if (raw === "true" || raw === "是" || raw === 1) return true;
|
||||
if (raw === "false" || raw === "否" || raw === 0) return false;
|
||||
return false;
|
||||
}
|
||||
if (raw === undefined || raw === null) return type === "string" ? "" : raw;
|
||||
return raw;
|
||||
}
|
||||
|
||||
export function parseVariableCatalog(raw: string | undefined | null): VariableCatalogDoc | null {
|
||||
if (!raw?.trim()) return null;
|
||||
const text = extractJsonObjectText(raw) ?? raw.trim();
|
||||
try {
|
||||
const parsed = JSON.parse(text) as unknown;
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
||||
const row = parsed as Record<string, unknown>;
|
||||
const fieldsRaw = row.fields ?? row.真值 ?? row.变量;
|
||||
if (!Array.isArray(fieldsRaw)) return null;
|
||||
const fields: VariableCatalogField[] = [];
|
||||
for (const item of fieldsRaw) {
|
||||
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
||||
const f = item as Record<string, unknown>;
|
||||
const key = asString(f.key) ?? asString(f.名) ?? asString(f.name);
|
||||
if (!key) continue;
|
||||
const type = parseType(f.type ?? f.类型);
|
||||
const userVisible =
|
||||
f.user_visible === false ||
|
||||
f.对用户可见 === false ||
|
||||
f.userVisible === false
|
||||
? false
|
||||
: true;
|
||||
fields.push({
|
||||
key,
|
||||
type,
|
||||
initial: coerceInitial(type, f.initial ?? f.初值 ?? f.value),
|
||||
user_visible: userVisible,
|
||||
note: asString(f.note) ?? asString(f.备注),
|
||||
});
|
||||
}
|
||||
if (!fields.length) return null;
|
||||
return { schema: VARIABLE_CATALOG_SCHEMA, fields };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeVariableCatalog(doc: VariableCatalogDoc): string {
|
||||
return JSON.stringify(
|
||||
{
|
||||
schema: VARIABLE_CATALOG_SCHEMA,
|
||||
fields: doc.fields.map((f) => ({
|
||||
key: f.key,
|
||||
type: f.type,
|
||||
initial: f.initial,
|
||||
user_visible: f.user_visible,
|
||||
...(f.note ? { note: f.note } : {}),
|
||||
})),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
export type UpsertVariableInput = {
|
||||
key: string;
|
||||
type?: string;
|
||||
initial?: unknown;
|
||||
user_visible?: boolean;
|
||||
note?: string;
|
||||
};
|
||||
|
||||
export function upsertVariableField(
|
||||
current: VariableCatalogDoc | null,
|
||||
input: UpsertVariableInput,
|
||||
): { doc: VariableCatalogDoc; error?: string } {
|
||||
const key = input.key.trim();
|
||||
if (!key) return { doc: current ?? { schema: VARIABLE_CATALOG_SCHEMA, fields: [] }, error: "key 不能为空" };
|
||||
const type = parseType(input.type);
|
||||
const field: VariableCatalogField = {
|
||||
key,
|
||||
type,
|
||||
initial: coerceInitial(type, input.initial),
|
||||
user_visible: input.user_visible !== false,
|
||||
note: input.note?.trim() || undefined,
|
||||
};
|
||||
const fields = [...(current?.fields ?? [])];
|
||||
const idx = fields.findIndex((f) => f.key === key);
|
||||
if (idx >= 0) fields[idx] = field;
|
||||
else fields.push(field);
|
||||
return { doc: { schema: VARIABLE_CATALOG_SCHEMA, fields } };
|
||||
}
|
||||
|
||||
/** 目录 → 表文档(初值);用于种 运行.初始变量 / 变量.当前 */
|
||||
export function catalogToTableDoc(
|
||||
catalog: VariableCatalogDoc,
|
||||
actor: string,
|
||||
): TableDoc {
|
||||
const values: Record<string, unknown> = {};
|
||||
const meta: Record<string, { visibility?: "visible" | "hidden"; note?: string }> = {};
|
||||
for (const f of catalog.fields) {
|
||||
values[f.key] = f.initial;
|
||||
meta[f.key] = {
|
||||
visibility: f.user_visible ? "visible" : "hidden",
|
||||
note: f.note,
|
||||
};
|
||||
}
|
||||
return createTableFromValues(values, actor, meta);
|
||||
}
|
||||
|
||||
/**
|
||||
* 若 变量.当前 为空则用目录初值种入;已有格不覆盖。
|
||||
* 同步补齐 运行.初始变量(仅当其为空)。
|
||||
*/
|
||||
export function seedVariablesFromCatalog(params: {
|
||||
catalogRaw: string | undefined | null;
|
||||
currentRaw: string | undefined | null;
|
||||
initialRaw: string | undefined | null;
|
||||
actor?: string;
|
||||
}): {
|
||||
current: string | null;
|
||||
initial: string | null;
|
||||
seededKeys: string[];
|
||||
} {
|
||||
const catalog = parseVariableCatalog(params.catalogRaw);
|
||||
if (!catalog?.fields.length) {
|
||||
return { current: null, initial: null, seededKeys: [] };
|
||||
}
|
||||
const actor = params.actor ?? "system:variable-catalog";
|
||||
const fromCatalog = catalogToTableDoc(catalog, actor);
|
||||
const seededKeys = fromCatalog.rows.map((r) => r.key);
|
||||
|
||||
const existingCurrent = parseTableDoc(params.currentRaw);
|
||||
let currentOut: string | null = null;
|
||||
if (!existingCurrent?.rows.length) {
|
||||
currentOut = stringifyTableDoc(fromCatalog);
|
||||
} else {
|
||||
// 补缺键,不覆盖已有
|
||||
const have = new Set(existingCurrent.rows.map((r) => r.key));
|
||||
const patchValues: Record<string, unknown> = {};
|
||||
const meta: Record<string, { visibility?: "visible" | "hidden"; note?: string }> = {};
|
||||
for (const f of catalog.fields) {
|
||||
if (have.has(f.key)) continue;
|
||||
patchValues[f.key] = f.initial;
|
||||
meta[f.key] = {
|
||||
visibility: f.user_visible ? "visible" : "hidden",
|
||||
note: f.note,
|
||||
};
|
||||
}
|
||||
if (Object.keys(patchValues).length) {
|
||||
const patch = createTableFromValues(patchValues, actor, meta);
|
||||
const merged = {
|
||||
rows: [...existingCurrent.rows, ...patch.rows].sort((a, b) =>
|
||||
a.key.localeCompare(b.key),
|
||||
),
|
||||
};
|
||||
currentOut = stringifyTableDoc(merged);
|
||||
}
|
||||
}
|
||||
|
||||
const existingInitial = parseTableDoc(params.initialRaw);
|
||||
let initialOut: string | null = null;
|
||||
if (!existingInitial?.rows.length) {
|
||||
initialOut = stringifyTableDoc(fromCatalog);
|
||||
}
|
||||
|
||||
return { current: currentOut, initial: initialOut, seededKeys };
|
||||
}
|
||||
Reference in New Issue
Block a user